IoT Pipeline

IoT Pipeline

DashboardDevicesOnboardingCompareErrorsLogsTest Plan⚙️ Test SettingsDecoding GuideGuide
Logout
Dashboard / Guide
Overview Pages & Navigation Data Flow Device Config (CAN map) Test Plan Verification ⚡ Which cases auto-detect Motion events (T05-T08) Why others stay manual Test Plan Flow

What this dashboard is

This dashboard is the operator view into the ALT IoT pipeline: every packet a device sends over MQTT, how it gets decoded, and what the pipeline produces from it (events, log uploads, commands). It's read-mostly except for device onboarding, sending commands, and running manual test verification.

Pages & Navigation

Dashboard
Home. Fleet-wide stats, recent devices, recent packets, recent decoded events. Start here to spot anything flowing wrong right now.
Devices
All registered devices, status, packet/event counts. Click a device for its detail page.
Device Detail
(open from Devices or Recent Packets) Live ping status, an Alerts card (every active fault/warning condition with its actual current reading — e.g. "BMS Fault — code 3, warning 512", not just a red chip), packet flags, latest decoded telemetry, a Device Config card (the live CAN signal map plus packet decoder configs and the last config pushed to the device, with a one-click Download config (JSON) for the firmware/IoT team — see Device Config for the format and why it is JSON, not a DBC), log transfers, command history, and the Verification card for this device's latest test run.
Onboarding
Register a new device (IMEI, device type, numeric ID) and issue its MQTT/HMAC credentials.
Compare
Side-by-side: our pipeline's latest decode vs. the separate calculated-redis reference, field by field — for spotting decode drift.
Errors
Dead-letter queue — packets that failed to decode (usually a missing/mismatched decoder config) and why.
Logs
SD-card log file uploads from devices — chunked HTTP parts + MQTT manifest, assembly/CRC/gunzip status, download the reassembled CSV.
Test Plan
New. Manual verification tracker for the IoT/firmware team's test sheet — auto-detects the cases it can, tracks the rest. See below.
Packet / Event Detail
(open from any table row) Full raw payload hex, decoded fields, integrity status (CRC/HMAC), and the normalized event it produced.

Data Flow

How a byte on the device becomes a row you can click on this dashboard:

Device MQTT publish RabbitMQ MQTT plugin Go Decoder header + config lookup raw_iot_packets every packet, decoded or not normalized_iot_events decoded, on success iot.decode.dlq on decode failure This Dashboard iot-control-service reads Postgres + Redis cache

Every packet lands in raw_iot_packets whether it decodes or not (so DLQ debugging has the original bytes). A successful decode also writes a normalized_iot_events row with clean, typed fields — that's what most of this dashboard reads from.

Device Config & CAN signal map — why JSON, not a DBC

The Device Config card on Device Detail lets you download, as one JSON file, everything the backend uses to decode and configure that device right now. It is the same data the decoder worker reads at runtime, so the file always reflects what is actually live — not a copy that can drift.

Why we don't use a DBC file

A DBC is a standalone CAN-database format maintained outside the system. Our CAN signal map lives in the backend as a versioned JSON config instead, because the same config also drives things a DBC can't express: which CAN IDs the firmware must send in every bundle (completeness check), where each decoded signal lands in the normalized event (battery.soc etc.), and composite signals computed from several frames. Keeping one source of truth avoids maintaining a DBC and the JSON in parallel, and a DBC→JSON round trip would lose that extra information. So the JSON is the signal definition; there is no separate DBC export.

What is in the download

SectionMeaning
deviceIMEI, device type, firmware, protocol and schema version the config was resolved for.
canDecoderConfigThe active CAN signal map (highest configVersion for the device type). Fields listed below.
otherActiveCanDecoderConfigsOther active CAN config rows for the same device type (different protocol/schema), listed only by version so you can spot a mismatch.
packetDecodingConfigsActive decoders for the non-CAN packet types (battery summary, location, etc.): strategy, endianness, field layout.
latestSyncedDeviceConfigThe last config version pushed to the device, with ACK status and time. null means nothing has been pushed yet — the device is running firmware defaults and the backend decoders above.

CAN config fields (canDecoderConfig.configJson)

FieldMeaning
expectedCanIdsCAN IDs the firmware must include in every can_frame_bundle. Missing ones mark the bundle PARTIAL. Uppercase hex, no 0x prefix.
signals[]Single-frame signals. canId, startBit (0–63), bitLength, endian (LITTLE_ENDIAN/BIG_ENDIAN), signed, then value = raw × scale + offset. signalName is the label, targetPath is where it lands in the normalized event, unit is optional.
derivedSignals[]Composite signals built from several CAN IDs: requiredCanIds (all must be in the same bundle, else MISSING_DEPENDENCY), inputs[] (same bit layout as above plus an alias), and formula — a registered PRESET_* name, never a free expression. An unknown preset gives UNKNOWN_FORMULA and PARTIAL; it never goes to the DLQ.

What the firmware / IoT team needs to know

  • Firmware only decides which CAN IDs to collect and how often; all bit offsets, scaling and formulas are backend-only and are never sent to the device or returned by any device-facing API.
  • To change a signal (bit position, scale, new signal), send the change request with CAN ID, start bit, length, endian, signed, scale/offset and target field; the backend ships it as a new configVersion — the old one stays in history.
  • If the download shows canDecoderConfig: null, no active config exists for that device type and CAN bundles for it will not decode.

Test Plan Verification — what & why

The IoT/firmware team verifies each build by hand against a physical device, using a fixed sheet of ~64 test cases. Most of those checks can be answered from data we already have — so instead of the tester manually watching Recent Packets and writing PASS/FAIL in a spreadsheet, this feature watches the same data and fills it in live.

Not everything can be auto-checked — cases that need comparing to an external ground truth (a BMS screen, phone GPS, CAN analyzer) or that need a raw tool (RSSI, FOTA) stay manual by design. Auto-detection only marks PASS when the signal is fully internal and objective — it will never claim something is correct that it can't actually verify.

How to use it

1. Open Test Plan, pick the device, optionally tag the firmware build, click Start Run.
2. Do your normal manual testing on the device — auto-detectable cases (marked ⚡) turn green on their own as matching data arrives, usually within 15 seconds.
3. Fill in the rest by hand — status, note, and (optionally) which packet/event proves it. For cases where it helps (SOC, speed, cell voltages/temps, GPS position, odometer, etc.) a blue Live now strip shows the device's most recent reading right on the case row, so you can compare against your reference (BMS, GPS, speedometer) without switching tabs.
4. Click Stop Run when done. Runs auto-expire after 24h if you forget.
5. Every past run stays in history per device — useful for comparing pass/fail across firmware builds.

⚡ Which cases auto-detect, and why only some

22 of the ~64 sheet cases carry the ⚡ icon. That's not a bug or a coincidence — a case only gets auto-detection when its pass signal is something this pipeline already stores as data, fully on its own, with no outside comparison needed. Most cases on the sheet aren't like that: they ask you to compare a decoded value against a phone's GPS, a BMS screen, a CAN analyzer, RSSI on a signal meter, or an FOTA tool — things the dashboard has no way to read. Those stay manual by design, forever, no matter how "visible" the underlying number is on the dashboard, because a false auto-PASS on something we can't actually verify would be worse than no automation at all. The full breakdown of exactly which cases those are, and what each one specifically needs, is in Why some cases stay manual below.

On the Test Plan run page, a case without ⚡ simply has no automatic path — you always set its status by hand. A case with ⚡ starts the run as ⚡ Watching (still PENDING, but the evaluator is actively checking it every 15 seconds) and flips itself to PASS with the AUTO tag the moment the real signal shows up in the data — you still don't have to do anything except perform the actual test action on the device. You can always override any case by hand at any time; a manual edit is never overwritten by the evaluator.

Test Settings (firmware / dev team): the Test Settings page lets the firmware team change what the auto-detection uses without a code change: the event.type number for each motion case, optional minimum / maximum peak strength (raw counts), the telemetry cadence window and the packet-gap length. Values are read by the evaluator within about 45 seconds; an empty field keeps the default. The same page lists the details still missing from the firmware side (sensor scales, trigger thresholds, meaning of code 3, speed / heading fields, clock) and a free-text section for the dev team (new packet fields, CAN signal layout, new auto rules). Anything left empty stays shown as missing.
See all 64 cases at once: the Test Cases page lists every case grouped by area, with its auto rule and proof fields (or why it stays manual), how to verify it, and how often it has passed in past runs. Filter by Auto / Tool / Partial / Needs reference / Never passed.
How to verify an auto-PASS yourself: on the run page (or the case popup) click the Evidence / View proof link of the case. For event evidence it opens the Event page with a green Test Plan Proof banner on top: the rule, what the PASS proves, and a table of the exact fields (for example event.type needed = 2, this event = 2 ✓). The same fields are highlighted green in the Normalized table below. Cases proven by an arriving packet open the Packet page with the rule; log-transfer cases open the Logs page.

The 22 auto-detectable cases

CaseTitleWhat triggers the auto-PASS
T05Harsh accelerationA full_snapshot_bundle packet arrives in the run with event.type = 2 (strong +X acceleration). How motion events are detected
T06Harsh brakingA full_snapshot_bundle packet arrives in the run with event.type = 1 (strong -X acceleration). How motion events are detected
T07Sharp turnA full_snapshot_bundle packet arrives in the run with event.type = 4 (largest gyro-Z swing). How motion events are detected
T08Regular turnA full_snapshot_bundle packet arrives in the run with event.type = 5 (milder turn signature). How motion events are detected
T09Accident captureAn accident snapshot bundle is received during the run
T16OdometerOdometer reading increases between two telemetry packets in the run
T17Cold-start fixhasGps flips false → true and latitude/longitude become non-zero
T20SIM + registrationAny packet at all arrives from the device after Start Run (proves it registered)
T22Broker connect (TLS)Any packet arrives — since it only arrives at all over a live TLS/MQTT session
T23Telemetry bundle every 30sTwo consecutive telemetry packets land 25–35s apart
T25Heartbeat hbHeartbeat flag observed false, then later true, in the run
T26Motor immobilize immMotor-immobilize flag flips to true in the run
T27Battery immobilize immbBattery-immobilize flag flips to true in the run
T29State persists across rebootMotor-immobilize flag is true, a packet gap of 90s+ occurs (reboot), and it's still true right after
T30Log upload happy pathA log transfer for the device reaches status COMPLETE
T31Manifest correctnessA transfer's manifest is fully received — size, CRC, expected-rows all present
T32Chunk accountingA transfer's received part count matches its expected part count
T33Retry on failureA transfer for the device reaches status FAILED
T35Already completeThe same filename reaches COMPLETE twice in the run
T38Re-request after failA later transfer for a filename that previously failed shows up
T45Network dropA packet gap of 90s+ occurs, then packets resume
T46GPS losshasGps flips true → false while telemetry keeps arriving

T05–T08, T17, T29, T45 and T46 are new additions on top of the original 14 — added once we confirmed their pass signal is a plain internal state change (an event type arriving, or a flag flipping), not a threshold judgment call. Note what these 8 actually prove: that the event fired or the state changed — not that the physical trigger you applied was strong/precise enough (e.g. T05 proves a harsh-acceleration event arrived, not that your nudge was hard enough to be "harsh" by spec). Tune-the-threshold judgment calls like that stay something a human glances at.

Everything else on the sheet (raw IMU numbers, RSSI, FOTA, GPS-vs-phone comparisons, BMS comparisons, etc.) is manual-only — see the next section for the full list and exactly why each one can't be auto.

How motion events (T05 to T08) reach the dashboard

Harsh acceleration, harsh braking and the two turn cases do not have a packet type of their own. When the device detects one, it sends a full_snapshot_bundle packet (a snapshot of the whole vehicle state taken at the moment of the event) with an event block on top: a numeric event.type, severity, duration, peak acceleration and gyro on each axis, and speed before/at/after. So in Recent Packets you will see a full_snapshot_bundle row, not a row named "harsh_accel". Open it, then open Detail and look at event.type.

event.typeCaseWhat the data looks likeConfidence
1T06 Harsh brakingNegative X peak in every observed event (median about -2400 raw units); the evaluator also requires a negative peakAccelXInferred
2T05 Harsh accelerationPositive X peak in every observed event (median about +2800 raw units); the evaluator also requires a positive peakAccelXInferred
4T07 Sharp turnLargest gyro-Z swing (about 315 raw units on average)Inferred
5T08 Regular turnSmaller gyro-Z swing (about 120 raw units on average) and a Y componentInferred
Why "Inferred": the firmware's list of event.type numbers is not part of the decoder config (the decoder stores the raw number), so the table above was derived from the peak-accel / peak-gyro pattern of about 18,000 events seen over 30 days. It matches the test sheet ("Nudge +X" for acceleration, "Nudge -X" for braking). Confirmation pending from the firmware side, mainly for 4 (sharp turn) vs 5 (regular turn); code 3 has not appeared in the data so far. If the wrong case turns green, set it by hand (a manual edit is never overwritten).

Why this was not working before

The first version of the evaluator looked for a packet whose type was literally harsh_accel, harsh_braking, sharp_turn or regular_turn. No device ever sends a packet with those names (nothing like it appeared in 30 days of data on any device), so these four cases stayed PENDING forever even when the device did fire the event. The evaluator now matches the numeric event.type inside full_snapshot_bundle instead.

Things to know when testing

- The event must arrive after you click Start Run; events from before the run do not count.
- Ordinary driving also produces these events (a normal turn gives type 5, a firm stop gives type 1), so a PASS means "the device reported this kind of event during the run", not "your specific nudge caused it". Check the timestamp and event.peakAccelX / event.peakGyroZ in Detail if it matters.
- Speed fields in the event block (speedBefore, speedAtTrigger, speedAfter) and headingChange currently look unpopulated by the firmware (speed never changes across the event, heading change is always 0), so do not use them to judge a test.

Why the other 42 cases stay manual

Every manual case falls into exactly one of three buckets, based on why it can't be auto-detected. This isn't "we haven't gotten to it yet" — these are structural: the dashboard genuinely doesn't have what it would need, for the reasons below. If firmware/decoder work ever changes that (e.g. a new event type starts carrying a comparable ground-truth value), the case can be revisited — but as things stand today, none of these 42 can safely auto-PASS.

1. Needs an outside reference to judge "correct" (12 cases)

The data is on the dashboard — but confirming it's right means comparing it to something outside the pipeline: a BMS screen, a phone's GPS, a known load, a known cell temperature. The dashboard only ever shows what the device itself reported. If the device's own number were wrong, the dashboard has no independent way to know — there's nothing to compare it against.

CaseTitleWhat a tester needs to actually verify it
T10DebounceTrigger the same event twice fast and eyeball that only one row landed — the dashboard can't know a 2nd trigger was even attempted
T18Position accuracyA phone GPS or map, read at the same moment, to confirm the fix is within a few metres
DC02Decoded location vs mapA map showing the device's true physical location
DC03Hemisphere signA device known to be in the S/W hemisphere at test time — sign correctness depends on where the device actually is
DC05Stale-fix holdDeliberately blocking GPS and watching the last fix hold (not zero) with an aging updatedAt
DC06Pack current sign & scaleA known charge/discharge event to confirm sign and scale — a plausible-looking wrong value can't be told apart from a correct one
DC08SOC valueThe BMS's own SOC% reading at the same moment
DC09Cell voltage scaleA known reference cell voltage to compare the mV→V scaling against
DC10Cell temp negativeA cell actually cooled below 0°C, to confirm signed encoding doesn't wrap at 255
DC11Cumulative energy WhA known load run over time, to confirm the value moves plausibly (not just "isn't 0")
DC12Speed km/h scaleThe vehicle's true speed (speedometer/GPS ground speed) to compare the decoded scale against
DC13Odometer rawThe BMS's own odometer reading, to confirm no extra scaling/offset

2. Bench/tool-only — never becomes pipeline data (15 cases)

These checks happen on a bench, a debugger, or a CAN analyzer, before or entirely outside of anything that gets encoded into an MQTT packet. There's no event for the evaluator to watch, because the signal never reaches the pipeline at all.

CaseTitleTool needed
T01Accel scaling at restDecoder / debugger
T02Tilt / inclinometerDecoder / debugger
T03Gyro zero-rate at restDecoder / debugger
T04Gyro responseDecoder / debugger
T21Signal strength (RSSI)AT console (+CSQ / +CPSI)
T28Motor RPM mrpmCAN analyzer
T37Temp .gz cleanupSerial / SD read
T39FOTA firmware updatecmdStatus + BL UART + g_fotaStage
T40FOTA fallback / rollbackBL UART; fwVersion in packet 31
T41SMS alert deliveryThe phone / serial (SMS never touches MQTT)
T44SD card fullSerial / SD
T48RAM headroomCubeIDE .map (a build-time artifact)
DC01GPS coordinate FORMATAT console
DC15IMU axis-to-vehicle mappingDecoder / debugger
DC16Endianness & CRCDecoder (reference check)

3. Partially visible — full check needs another source (15 cases)

Some of the picture is on the dashboard, but a full pass/fail call needs a second source too — a decoder debug view of raw fields, a backend log, or a serial console — that the dashboard doesn't surface.

CaseTitleWhat else is needed
T11Restore PROD thresholdsCode review of evConfig — dashboard shows results, not the config
T12BMS pack telemetryDecoder debug view of raw packV/SOH fields
T13Cell voltages / tempsDecoder debug view of all 16 cells / 8 temps (dashboard shows a summary)
T14Motor telemetryDecoder debug view of throttle/gear/reverse fields
T15Power meterDecoder/Detail cross-check of the power meter math
T19Speed = motion truthDecoder view of gps.speed alongside vehicle speed
T24RTC time syncDecoder/Detail check that sync landed within tolerance
T34Bad API keyBackend log for the 401 (dashboard doesn't surface auth failures)
T36Missing fileSerial console
T42Power cycleSerial console check for corruption
T43SD card absentSerial console
T47Soak 24-48 hWatchdog log / .map — inherently a long, human-monitored run
DC04GPS speed valueDecoder view of the raw GPS speed field
DC07Pack voltage scaleDecoder/Detail check of the scale factor
DC14Epoch is UTCDecoder/Detail check that epoch isn't local time

The exact same text shown here is what you'll see on each case's own detail popup on the Test Plan page — click any case title to open it.

Test Plan Flow

Start Run (pick device) Run = ACTIVE 64 cases created as PENDING Evaluator (every 15s) scans new packets/events/log transfers Match found status=PASS, source=AUTO, evidence attached automatically Tool-only / no-signal cases tester fills status + note by hand from the run detail page Stop Run (manual) — or 24h auto-expire status=COMPLETED / EXPIRED, completedAt set Run appears in history pass/fail/N-A counts, linked from device page too
Active / in progress Auto-detected Needs manual input

Full rule table (which cases auto-detect and how) and the schema are documented in docs/13-test-plan-verification.md in the repo, for engineers extending this later.