Skip to content

Commit 9d1b469

Browse files
committed
Merge remote-tracking branch 'origin/main' into telemetry/grafana_dashboards_optimizations
# Conflicts: # telemtry/analysis/database/dashboards/orion/retrospect/chassis_suspension.json # telemtry/analysis/database/dashboards/orion/retrospect/driver_inputs.json
2 parents 03a8f8f + 5047931 commit 9d1b469

239 files changed

Lines changed: 33943 additions & 2793 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/postsubmit.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ jobs:
5050
docker network create telemetry_network || true
5151
docker volume create grafana_storage || true
5252
docker volume create telemetry_db || true
53+
docker volume create kafka_data || true
5354
5455
- name: Create .env file for Docker
5556
run: |

.github/workflows/presubmit.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jobs:
3737
docker network create telemetry_network || true
3838
docker volume create grafana_storage || true
3939
docker volume create telemetry_db || true
40+
docker volume create kafka_data || true
4041
4142
- name: Create .env file for Docker
4243
run: |

.github/workflows/schema-drift.yml

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
name: schema-drift
2+
run-name: Schema codegen drift check (${{ github.actor }})
3+
4+
# Regenerates every schema/CAN artifact from its source (can_packets.csv +
5+
# can_packets.proto) and fails if the committed artifacts are out of sync.
6+
# This makes schema / telemetry-signal updates dead-easy: change the source,
7+
# run the generators, commit — and CI guarantees nothing drifts.
8+
9+
on:
10+
pull_request:
11+
types: [opened, synchronize, reopened]
12+
paths:
13+
- 'drivers/longhorn-lib/config/**'
14+
- 'drivers/longhorn-lib/protobuf/**'
15+
- 'drivers/longhorn-lib/scripts/**'
16+
- 'telemtry/scripts/**'
17+
- 'telemtry/stack/ingest/**'
18+
- 'telemtry/stack/kafka/proto/**'
19+
- 'telemtry/analysis/database/viewer_tool/prisma/**'
20+
- 'telemtry/analysis/database/viewer_tool/protobuf/**'
21+
- 'telemtry/analysis/sql_utils/models.py'
22+
- 'BEVO/nonhermetic/assets/**'
23+
- 'BEVO/codegen.py'
24+
- 'BEVO/generated_mapping.rs'
25+
- 'BEVO/loggerd/**'
26+
- 'BEVO/config.rs'
27+
- '.github/workflows/schema-drift.yml'
28+
workflow_dispatch:
29+
30+
jobs:
31+
schema-drift:
32+
name: Schema codegen drift check
33+
runs-on: ubuntu-latest
34+
env:
35+
PYTHONDONTWRITEBYTECODE: "1"
36+
steps:
37+
- name: Checkout
38+
uses: actions/checkout@v4
39+
with:
40+
fetch-depth: 0
41+
42+
- name: Set up Python
43+
uses: actions/setup-python@v5
44+
with:
45+
python-version: "3.11"
46+
47+
- name: Install protoc + python deps
48+
run: |
49+
sudo apt-get update
50+
sudo apt-get install -y protobuf-compiler
51+
python -m pip install --upgrade pip
52+
python -m pip install "protobuf>=6,<7"
53+
54+
- name: Regenerate all schema artifacts from source
55+
env:
56+
BUILD_WORKSPACE_DIRECTORY: ${{ github.workspace }}
57+
run: |
58+
set -euxo pipefail
59+
# CSV -> proto (assigns/back-annotates stable proto field ids)
60+
python3 drivers/longhorn-lib/scripts/update_can_proto.py
61+
# CSV -> can.json + descriptor + Rust mapping
62+
bash BEVO/nonhermetic/sync_assets.sh
63+
# proto -> SQL / Prisma / SQLAlchemy models / dataclasses / sensor.proto
64+
bash telemtry/scripts/sync_schema.sh Orion
65+
66+
- name: Fail if regenerated artifacts differ from committed
67+
run: |
68+
# sensor_data.desc is a binary protoc intermediate whose bytes vary by
69+
# protoc version; exclude it and gate only on the deterministic text
70+
# artifacts (proto, can.json, mapping.rs, SQL, prisma, models, ...).
71+
if ! git diff --exit-code -- . ':(exclude)BEVO/sensor_data.desc'; then
72+
{
73+
echo "### ❌ Schema artifacts are out of sync with their sources"
74+
echo ""
75+
echo "A source (\`can_packets.csv\` / \`can_packets.proto\`) changed but the"
76+
echo "generated artifacts weren't regenerated. Run locally and commit the result:"
77+
echo ""
78+
echo '```bash'
79+
echo 'export BUILD_WORKSPACE_DIRECTORY="$PWD"'
80+
echo 'python3 drivers/longhorn-lib/scripts/update_can_proto.py'
81+
echo 'bash BEVO/nonhermetic/sync_assets.sh'
82+
echo 'bash telemtry/scripts/sync_schema.sh Orion'
83+
echo '```'
84+
} >> "$GITHUB_STEP_SUMMARY"
85+
echo "::error::Generated schema artifacts are out of sync with their sources. See job summary."
86+
exit 1
87+
fi
88+
89+
loggerd-tests:
90+
name: BEVO loggerd unit tests
91+
runs-on: ubuntu-latest
92+
steps:
93+
- name: Checkout
94+
uses: actions/checkout@v4
95+
96+
- name: Install Rust
97+
uses: dtolnay/rust-toolchain@stable
98+
99+
- name: Install protoc
100+
run: |
101+
sudo apt-get update
102+
sudo apt-get install -y protobuf-compiler
103+
104+
- name: Test loggerd (schema-driven CSV header behavior)
105+
working-directory: BEVO
106+
env:
107+
PROTOC: /usr/bin/protoc
108+
run: cargo test --bin loggerd

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,7 @@ MODULE.bazel.lock
9696
rust-project.json
9797

9898
BEVO/vpn_creds.txt.claude/
99+
100+
# Python bytecode (schema-gen scripts)
101+
__pycache__/
102+
*.pyc

BEVO/dashd/MQTT_CONTRACT.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,50 @@ its data shown on the driver's dashboard in real time.
1717
| `lhre/dash/lapDelta` | JSON number | seconds | `-0.45` |
1818
| `lhre/dash/energyDelta` | JSON number | Wh | `3.2` |
1919
| `lhre/dash/lapsRemaining` | JSON number | laps (fractional) | `15.3` |
20+
| `lhre/dash/targetPower` | JSON number | kW | `32` |
21+
| `lhre/dash/lapTrigger` | JSON number | monotonic counter | `7` |
22+
| `lhre/dash/sfGate` | JSON `[f64;4]` | `[lat1,lon1,lat2,lon2]` | `[30.39,-97.72,30.39,-97.73]` |
23+
24+
### Endurance pacing signals
25+
26+
`targetPower` and `lapTrigger` are published by the **Dash** tab on the
27+
trackside-live page (`telemtry/analysis/database/viewer_tool`) over the broker's
28+
websockets listener (port `8080`). They drive on-dash endurance pacing — the
29+
dash integrates CAN power locally and compares it to the budget, so only these
30+
two control values come off-car:
31+
32+
- **`targetPower`** — the live power budget (kW) the strategist dials in. The
33+
dash integrates real power against `targetPower * elapsed` per lap; the
34+
top energy bar runs green while under budget and red while over. It's a
35+
set-point, so the sender **republishes it ~1 Hz**. Unlike the other fields it
36+
is **held last-known on the dash across a dropout** (not nulled): instead the
37+
dash emits `targetPowerStale: true` to the frontend, which dims the bar and
38+
shows a "STALE" badge — a held budget beats a blank one for the driver.
39+
- **`lapTrigger`** — a monotonically increasing lap counter. On each **increase**
40+
the dash pops a full-screen lap card (lap time + energy used that lap) and
41+
resets the per-lap energy integrator, so pacing error can't accumulate across
42+
laps. The dash keys off the rising edge, not the absolute value. This is now a
43+
**fallback/override**: when an `sfGate` is loaded the car counts its own laps.
44+
**Reverse channel (dash → trackside).** dashd also *publishes* so the strategist
45+
can confirm the uplink and mirror the driver's screen:
46+
47+
- **`lhre/dash/state`** — JSON snapshot at ~2 Hz of what the driver sees (speed,
48+
power, soc, temperature, lap count, and the on-car pacing: `lapEnergyWh`,
49+
`budgetDeltaWh`, `lapNumber`, last-lap time/energy). The Dash tab renders this
50+
as a live mirror; if it goes silent for >3 s the panel flags the uplink down.
51+
- **`lhre/dash/ack/{targetPower,lapTrigger,sfGate}`** — retained echoes published
52+
the moment dashd ingests each control, so trackside sees "the car heard 32 kW
53+
2 s ago" rather than just "I sent it." Energy integration is authoritative
54+
on-car (survives a chromium reload), so these are the same numbers, not a
55+
re-derivation.
56+
57+
- **`sfGate`** — the start/finish line as `[lat1, lon1, lat2, lon2]`, published
58+
**retained, QoS 1** from the Dash tab's "Push S/F to car" button (sourced from
59+
the Track Builder gate). Once loaded, dashd watches `dynamics.gps` and bumps
60+
the lap counter when the car's path crosses the line — so **the per-lap reset
61+
no longer depends on the link at all.** The gate is cached to disk
62+
(`DASHD_SFGATE_PATH`, default `/tmp/BEVO_dash_sfgate.json`) so it also survives
63+
a reboot if the broker drops its retained copy.
2064

2165
## Payload format
2266

@@ -56,4 +100,8 @@ mosquitto_pub -h 18.191.225.118 -t "lhre/dash/lapDelta" -m "-0.45"
56100
mosquitto_pub -h 18.191.225.118 -t "lhre/dash/lapDelta" -m "-0.45"
57101
mosquitto_pub -h 18.191.225.118 -t "lhre/dash/energyDelta" -m "3.2"
58102
mosquitto_pub -h 18.191.225.118 -t "lhre/dash/lapsRemaining" -m "15.3"
103+
104+
# Endurance pacing: set a 32 kW budget, then trigger a lap card:
105+
mosquitto_pub -h 18.191.225.118 -t "lhre/dash/targetPower" -m "32"
106+
mosquitto_pub -h 18.191.225.118 -t "lhre/dash/lapTrigger" -m "1"
59107
```

BEVO/dashd/deploy/launch_kiosk.sh

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,38 @@ done
3737
# so this can be omitted there, but launching by
3838
# hand from SSH (where DISPLAY/WAYLAND_DISPLAY
3939
# weren't set up the same way) needs it explicit.
40+
CHROMIUM_BIN=""
4041
for bin in chromium-browser chromium; do
4142
if command -v "$bin" >/dev/null 2>&1; then
42-
exec "$bin" \
43-
--kiosk \
44-
--incognito \
45-
--ozone-platform=wayland \
46-
--noerrdialogs \
47-
--disable-restore-session-state \
48-
--disable-infobars \
49-
--disable-translate \
50-
--disable-features=TranslateUI \
51-
--check-for-update-interval=604800 \
52-
--overscroll-history-navigation=0 \
53-
--password-store=basic \
54-
"$URL"
43+
CHROMIUM_BIN="$bin"
44+
break
5545
fi
5646
done
5747

58-
echo "[BEVO] No chromium binary found (tried: chromium-browser, chromium)" >&2
59-
exit 1
48+
if [[ -z "$CHROMIUM_BIN" ]]; then
49+
echo "[BEVO] No chromium binary found (tried: chromium-browser, chromium)" >&2
50+
exit 1
51+
fi
52+
53+
# Supervise the kiosk: a renderer/GPU crash or OOM kill must NOT leave a blank
54+
# dash for the rest of an endurance run. dashd and the static server already
55+
# auto-restart via systemd, but the browser did not until this loop — XDG
56+
# autostart launches it once and never again. Relaunch on exit with a short
57+
# backoff. `|| true` keeps `set -e` from aborting the loop on a nonzero exit.
58+
while true; do
59+
"$CHROMIUM_BIN" \
60+
--kiosk \
61+
--incognito \
62+
--ozone-platform=wayland \
63+
--noerrdialogs \
64+
--disable-restore-session-state \
65+
--disable-infobars \
66+
--disable-translate \
67+
--disable-features=TranslateUI \
68+
--check-for-update-interval=604800 \
69+
--overscroll-history-navigation=0 \
70+
--password-store=basic \
71+
"$URL" || true
72+
echo "[BEVO] Chromium kiosk exited; relaunching in 2s" >&2
73+
sleep 2
74+
done

BEVO/dashd/frontend/src/hooks/useDemoData.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,15 @@ export function useDemoData(enabled: boolean): DashMessage | null {
3636
lastLap: 78.45,
3737
currentLapStart: Date.now(),
3838
lapTarget: 75 + Math.random() * 8,
39+
40+
// Endurance pacing demo state. Mirrors what dashd computes on-car so the
41+
// bar + lap card render in demo mode. lapTrigger is bumped each lap.
42+
lapTrigger: 0,
43+
lapEnergyWh: 0,
44+
lapBudgetWh: 0,
45+
lastLapNum: null as number | null,
46+
lastLapTimeS: null as number | null,
47+
lastLapEnergyWh: null as number | null,
3948
});
4049

4150
useEffect(() => {
@@ -85,6 +94,14 @@ export function useDemoData(enabled: boolean): DashMessage | null {
8594
s.speed = Math.max(0, Math.min(s.speed, 99));
8695
const clampedPower = Math.min(Math.max(newPower, -80), 80);
8796

97+
// Endurance power budget for the energy-bar demo (kW). The bar
98+
// drifts green/red as the simulated power runs under/over this.
99+
const targetPower = 32;
100+
// Integrate per-lap energy + budget at the 100 ms tick (Wh = kW*dt/3.6),
101+
// mirroring dashd's on-car integration.
102+
a.lapEnergyWh += (clampedPower * 0.1) / 3.6;
103+
a.lapBudgetWh += (targetPower * 0.1) / 3.6;
104+
88105
// Accumulate derived values
89106
a.charge = Math.max(0, a.charge - (newPower > 0 ? 0.005 : -0.001));
90107

@@ -131,6 +148,13 @@ export function useDemoData(enabled: boolean): DashMessage | null {
131148
if (elapsed >= a.lapTarget) {
132149
a.lastLap = elapsed;
133150
if (elapsed < a.bestLap) a.bestLap = elapsed;
151+
a.lapTrigger += 1; // fire the on-dash lap card + per-lap reset
152+
// Snapshot the just-finished lap, then reset the integrators.
153+
a.lastLapNum = a.lapTrigger;
154+
a.lastLapTimeS = elapsed;
155+
a.lastLapEnergyWh = a.lapEnergyWh;
156+
a.lapEnergyWh = 0;
157+
a.lapBudgetWh = 0;
134158
a.currentLapStart = Date.now();
135159
a.lapTarget = 75 + Math.random() * 8;
136160
elapsed = 0;
@@ -235,6 +259,18 @@ export function useDemoData(enabled: boolean): DashMessage | null {
235259
lastLapTime: a.lastLap,
236260
currentLapTime,
237261
lapDeltaRate,
262+
targetPower,
263+
targetPowerStale: false,
264+
lapTrigger: a.lapTrigger,
265+
},
266+
pacing: {
267+
lapEnergyWh: a.lapEnergyWh,
268+
budgetDeltaWh: a.lapEnergyWh - a.lapBudgetWh,
269+
lapElapsedS: currentLapTime,
270+
lapNumber: a.lapTrigger + 1,
271+
lastLapNumber: a.lastLapNum,
272+
lastLapTimeS: a.lastLapTimeS,
273+
lastLapEnergyWh: a.lastLapEnergyWh,
238274
},
239275
});
240276
}, 100);
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { useEffect, useRef, useState } from 'react';
2+
import { DashMessage } from '../types/DashData';
3+
4+
// Endurance energy pacing. The integration + lap detection now live on-car in
5+
// dashd (BEVO/dashd/main.rs), so this hook is a thin consumer: it reads the
6+
// authoritative pacing snapshot and only owns the transient full-screen lap
7+
// card (show on each newly-completed lap, auto-clear after a few seconds).
8+
//
9+
// Why on-car: a chromium reload mid-endurance would otherwise zero the lap
10+
// integrator, and the trackside mirror reads the exact same numbers dashd
11+
// publishes to `lhre/dash/state`.
12+
13+
// How long the full-screen lap card stays up after a lap completes.
14+
export const LAP_CARD_MS = 5000;
15+
16+
export interface LapSummary {
17+
lapNumber: number; // the lap that just finished
18+
timeS: number; // its duration in seconds
19+
energyWh: number; // net energy used over the lap
20+
}
21+
22+
export interface EnergyPacing {
23+
lapEnergyWh: number;
24+
lapElapsedS: number;
25+
budgetDeltaWh: number | null; // >0 over budget (red), <0 under (green), null until a target is set
26+
targetPowerKw: number | null;
27+
lapNumber: number; // 1-based lap in progress
28+
lapCard: LapSummary | null; // the most recent completed lap, shown briefly
29+
}
30+
31+
export function useEnergyPacing(data: DashMessage | null): EnergyPacing {
32+
const [lapCard, setLapCard] = useState<LapSummary | null>(null);
33+
// Highest completed-lap number we've already reacted to. Seeded on first
34+
// sight so a reload mid-session doesn't pop a card for an old lap.
35+
const lastShownLap = useRef<number | null>(null);
36+
37+
const pacing = data?.pacing;
38+
const lastLapNumber = pacing?.lastLapNumber ?? null;
39+
const lastLapTimeS = pacing?.lastLapTimeS ?? null;
40+
const lastLapEnergyWh = pacing?.lastLapEnergyWh ?? null;
41+
42+
useEffect(() => {
43+
if (lastLapNumber === null) return;
44+
// First observation just sets the baseline (no card).
45+
if (lastShownLap.current === null) {
46+
lastShownLap.current = lastLapNumber;
47+
return;
48+
}
49+
if (lastLapNumber > lastShownLap.current) {
50+
lastShownLap.current = lastLapNumber;
51+
setLapCard({
52+
lapNumber: lastLapNumber,
53+
timeS: lastLapTimeS ?? 0,
54+
energyWh: lastLapEnergyWh ?? 0,
55+
});
56+
}
57+
}, [lastLapNumber, lastLapTimeS, lastLapEnergyWh]);
58+
59+
// Auto-clear the card after LAP_CARD_MS.
60+
useEffect(() => {
61+
if (!lapCard) return;
62+
const id = window.setTimeout(() => setLapCard(null), LAP_CARD_MS);
63+
return () => window.clearTimeout(id);
64+
}, [lapCard]);
65+
66+
return {
67+
lapEnergyWh: pacing?.lapEnergyWh ?? 0,
68+
lapElapsedS: pacing?.lapElapsedS ?? 0,
69+
budgetDeltaWh: pacing?.budgetDeltaWh ?? null,
70+
targetPowerKw: data?.mqtt.targetPower ?? null,
71+
lapNumber: pacing?.lapNumber ?? 1,
72+
lapCard,
73+
};
74+
}

0 commit comments

Comments
 (0)