Bench test on 2026-08-25 against live RSU and CiT One traffic found that 611 RSU CAMs decoded correctly and none of them were ever displayed. Excluding RSUs from UseCaseDetectionEngine - correct, since a permanently stationary station at a fixed point trips the stopped-vehicle use case for as long as it is in range - also removed them from the map and station list, because remotePositions is the engine's own map. RSUs are now tracked in a separate rsuStations flow and merged with the engine's road users for display only. Expiry is clock-driven for the same reason as hazards and signals: an RSU going out of range simply stops transmitting, and no further emission would arrive to recompute the list. Cleared on link-down alongside engine.reset(), so a stale RSU cannot outlive an unplug. The kinematics line is suppressed for them. An RSU's CAM uses rsuContainerHighFrequency, which carries no kinematics at all, so the zeroes in the model are placeholders - printing "0.0 km/h - heading 0" would assert a stationary vehicle pointing due north. Also logs the ESP32's STATUS heartbeat counters whenever one changes. They previously reached only the CAM Pinger card, so a bench run captured through logcat had no record of whether the firmware dropped anything. Logged on change rather than per beat: the interesting event is a drop appearing, and a once-per-second line would bury it. Test report in 05-obu-bench-test-2026-08-25.md.
245 lines
11 KiB
Markdown
245 lines
11 KiB
Markdown
# ESP32-C5 OBU firmware — bench test against live ITS-G5 traffic
|
||
|
||
**Date:** 2026-08-25
|
||
**Firmware:** `obu-firmware` @ commit `b2b57fa`, flashed to COM3 (CH343 UART bridge, 921600 baud)
|
||
**App:** `app-debug.apk`, installed 14:33:45, Pixel 9 Pro on the C5's native USB-C port
|
||
**Verdict:** the receive path works against all three live message types with zero failures.
|
||
Two blocking items remain before real-world use, both known and both outside what this bench can
|
||
exercise. See [Readiness](#readiness).
|
||
|
||
## Test environment
|
||
|
||
| role | device | notes |
|
||
|---|---|---|
|
||
| Device under test | ESP32-C5 OBU, COM3 | our firmware; phone attached to its native USB port |
|
||
| Traffic source | RSU, broker `192.168.3.202` | transmits SPATEM + CAM |
|
||
| Traffic source | CiT One OBU, broker `192.168.3.201` | transmits DENM + CAM |
|
||
| Independent witness | both brokers' `v2x/rx/*` topics | each hears the *other* device |
|
||
|
||
Both brokers publish raw UPER (protobuf-wrapped, field 3), so their counts are directly comparable
|
||
with what the OBU forwarded. That is what makes this a cross-check rather than a self-report: the
|
||
OBU's output is measured against two receivers that share none of its code.
|
||
|
||
Station IDs observed today (`3983312873` RSU, `3257224191` CiT One) differ from those seen this
|
||
morning (`968482441`, `2880458775`). **Station IDs rotate**, so nothing may treat one as a durable
|
||
identity for a physical unit.
|
||
|
||
## T1 — Message type coverage and rate
|
||
|
||
305-second continuous capture, phone logcat.
|
||
|
||
| type | frames | rate | stations |
|
||
|---|---|---|---|
|
||
| CAM | 1244 | 4.08/s | 2 |
|
||
| SPATEM | 978 | 3.20/s | 1 |
|
||
| DENM | 646 | 2.12/s | 3 |
|
||
| **total** | **2868** | **9.40/s** | |
|
||
|
||
Per station, with received signal strength:
|
||
|
||
| type | station | frames | RSSI min/median/max |
|
||
|---|---|---|---|
|
||
| SPATEM | 3983312873 | 978 | −65 / −60 / −48 dBm |
|
||
| CAM | 3257224191 | 633 | −58 / −52 / −48 dBm |
|
||
| DENM | 3257224191 | 616 | −65 / −58 / −48 dBm |
|
||
| CAM | 3983312873 | 611 | −65 / −62 / −60 dBm |
|
||
| DENM | 2908440021 | 20 | −64 / −61 / −53 dBm |
|
||
| DENM | 1220851972 | 10 | −63 / −52 / −49 dBm |
|
||
|
||
All three types decoded concurrently, from both transmitters plus two additional DENM sources that
|
||
happened to be on air. **PASS.**
|
||
|
||
## T2 — Decode integrity
|
||
|
||
Over the same window:
|
||
|
||
| check | result |
|
||
|---|---|
|
||
| Decode failures (CAM / DENM / SPATEM) | **0** |
|
||
| Unexpected BTP ports from firmware | **0** |
|
||
| USB I/O errors | **0** |
|
||
| Device detach events | **0** |
|
||
| Fatal exceptions | **0** |
|
||
|
||
Zero decode failures across 2868 frames. Since the decoders return null rather than guessing
|
||
whenever an extension bit or unsupported optional appears, a zero here means every frame matched
|
||
the bit layouts exactly — not that failures were being swallowed. **PASS.**
|
||
|
||
## T3 — Cross-check against independent receivers
|
||
|
||
60-second simultaneous capture from both brokers, counting the same UPER the OBU sees.
|
||
|
||
| stream | independent witness | our OBU |
|
||
|---|---|---|
|
||
| RSU SPATEM (3983312873) | 2.00/s | 3.20/s |
|
||
| RSU CAM (3983312873) | 2.00/s | 2.00/s |
|
||
| CiT One CAM (3257224191) | 2.05/s | 2.08/s |
|
||
| CiT One DENM (3257224191) | 1.00/s | 2.02/s |
|
||
|
||
CAM matches on both transmitters. SPATEM and DENM read high — explained in T4, not a defect.
|
||
|
||
## T4 — Duplicate transmission (finding, not a fault)
|
||
|
||
The DENM and SPATEM discrepancies above are **real duplicate transmissions**, not double-counting
|
||
in our firmware. Inter-arrival analysis of the captured stream:
|
||
|
||
| type | gaps < 150 ms | median of those | identical content, different RSSI |
|
||
|---|---|---|---|
|
||
| SPATEM | 50% (485/977) | 7 ms | **480 / 485** |
|
||
| DENM | 50% (308/615) | 5 ms | **282 / 308** |
|
||
| CAM | 2% (11/632) | 100 ms | 1 / 11 |
|
||
|
||
Each SPATEM and DENM goes out **twice, ~5–7 ms apart, with different RSSI** — two antennas. CAM is
|
||
sent once. This also reconciles the broker figures: the CiT One reports one copy on `v2x/rx/*` and
|
||
the other on `v2x/rx-red/*` ("red" = redundant), and only the primary was counted.
|
||
|
||
Our firmware is a promiscuous receiver, so forwarding both copies is correct behaviour. The app
|
||
deduplicates downstream — DENM on ETSI actionID, SPATEM on intersection key — so the UI shows one
|
||
entry per event. The cost is serial bandwidth: **38% of the bytes carried are duplicate copies.**
|
||
|
||
## T5 — Serial link load
|
||
|
||
Measured over the 305 s window, using UPER sizes taken from the brokers:
|
||
|
||
- **9.40 frames/s, ~1550 B/s (12.4 kbit/s)**
|
||
- Largest frame: DENM at 402 B UPER → 416 B payload (81% of the 512 B cap, 96 B headroom)
|
||
- On the wire that frame is 423 B, 41% of the 1024 B TX ring
|
||
- Suppressing duplicate copies would cut this to ~961 B/s (7.7 kbit/s)
|
||
|
||
No frame in this session exceeded the payload cap.
|
||
|
||
## T6 — Firmware drop counters
|
||
|
||
Read directly off the app at 14:45, after the capture:
|
||
|
||
```
|
||
ESP32: tx fail 0 · oversize 0 · crc err 0
|
||
```
|
||
|
||
These are free-running totals **since firmware boot**, so all three being zero covers the whole
|
||
session, not just the test window — no oversize drops, no `esp_wifi_80211_tx` failures, no CRC
|
||
errors at any point since the C5 was flashed. **PASS.**
|
||
|
||
The app also now logs these counters whenever one changes (added for this test; they previously
|
||
reached only the UI), so a future bench run captured through logcat records drops as they happen.
|
||
|
||
## T7 — End-to-end UI verification
|
||
|
||
Screenshot at 14:45 confirms the full chain reaches the display:
|
||
|
||
| element | shown |
|
||
|---|---|
|
||
| Link state | CONNECTED |
|
||
| Hazard (DENM) | `stationaryVehicle · station 3257224191`, 30 m, 1000 m radius, −63 dBm |
|
||
| Signals (SPATEM) | `Intersection -1/23 · station 3983312873`, SG1 red / SG2 amber, −63 dBm |
|
||
| Station (CAM) | `Station 3257224191 · Car`, 1.4 km/h, heading 19°, 25 m, −50 dBm |
|
||
|
||
Signal-group colouring, the DENM relevance radius from the GeoNetworking header, and per-station
|
||
RSSI all render correctly.
|
||
|
||
### Finding: the RSU's CAM decodes but is never displayed
|
||
|
||
The station list reads **"1 station(s) in range"** — only the CiT One (`3257224191`). The RSU
|
||
(`3983312873`) is absent, despite **611 of its CAMs decoding successfully** during the capture.
|
||
|
||
Cause: RSU CAMs are deliberately excluded from `UseCaseDetectionEngine` (a permanently stationary
|
||
station at a fixed point otherwise trips the stopped-vehicle use case continuously) — but
|
||
`remoteCamPositions`, which feeds both the station list and the map, is populated *by that engine*.
|
||
So the exclusion removes them from the display as well as from detection.
|
||
|
||
The RSU is not entirely invisible: it appears in the SPAT section as the intersection's station. But
|
||
its CAM-reported position is dropped on the floor. This is a defect introduced with the RSU CAM
|
||
decode fix earlier today, not a firmware problem — the firmware forwarded all 611 correctly.
|
||
|
||
**Fixed and re-verified the same session.** RSU CAMs are now tracked in a separate
|
||
`rsuStations` flow in the repository, merged with the engine's road users for display only, with a
|
||
15 s staleness window and a clear on link-down. Screenshot at 15:01 confirms:
|
||
|
||
```
|
||
2 station(s) in range - latest CAM per station
|
||
Station 2199514753 · Car 0.8 km/h · heading 192° 28 m -52 dBm
|
||
Station 440624502 · Roadside unit roadside unit - no kinematics reported
|
||
46 m -61 dBm
|
||
```
|
||
|
||
The kinematics line is suppressed for RSUs: their CAM carries none, so the zeroes in the model are
|
||
placeholders and printing "0.0 km/h · heading 0" would assert a stationary vehicle facing north.
|
||
The same station also drives the SPAT row, so the two views agree.
|
||
|
||
## T8 — Station ID rotation
|
||
|
||
Station IDs rotated **twice within one session**:
|
||
|
||
| time | RSU | CiT One |
|
||
|---|---|---|
|
||
| ~09:00 | 968482441 | 2880458775 |
|
||
| 14:45 | 3983312873 | 3257224191 |
|
||
| 15:01 | 440624502 | 2199514753 |
|
||
|
||
That is a rotation inside 16 minutes. Consequences for the app, none of them currently handled:
|
||
|
||
- **DENM dedup keys on ETSI actionID**, which contains the originating station ID. A hazard that
|
||
outlives a rotation will appear as a second, independent pin rather than an update of the first.
|
||
Both then persist until the 60 s TTL expires them.
|
||
- **The station list and map key on station ID**, so a rotation shows the same physical vehicle
|
||
twice for up to the 15 s window.
|
||
- **SPATEM is unaffected**, because it dedups on the intersection reference (`region/id`), which is
|
||
a property of the junction rather than the sender. That is the right key and it survives rotation.
|
||
|
||
Nothing here is a firmware issue, and pseudonym rotation is the intended privacy behaviour of the
|
||
transmitters. But any future logic that assumes a station ID identifies a physical unit over time
|
||
will be wrong.
|
||
|
||
## Readiness
|
||
|
||
### Working
|
||
|
||
- All three received message types decode correctly from live over-the-air traffic
|
||
- Concurrent multi-station, multi-type reception with no interference between streams
|
||
- Sustained 5-minute run with no link drop, no I/O error, no crash
|
||
- RSSI plausible and discriminating between transmitters (−48 to −65 dBm at bench distance)
|
||
- No frame exceeded the serial payload cap under this traffic mix
|
||
|
||
### Blocking for real-world use
|
||
|
||
1. **Serial payload cap (512 B).** The bench RSU sends 58-byte SPATEMs, but the 2026-03-18 drive
|
||
measured real road RSUs at 555 B median and 1243 B max — **roughly 70% would be dropped as
|
||
oversize**. Raising `SERIAL_LINK_MAX_PAYLOAD` and `RX_FRAME_MAX_LEN` to ~1536 is required, and
|
||
forces item 2.
|
||
2. **`rx_item_t` on the WiFi driver's callback stack** (`main.c`). At the current 800 B it is a
|
||
latent risk on a ~3.5 KB stack; at 1536 B it is a guaranteed overflow. Must be moved off the
|
||
stack as part of the same change.
|
||
|
||
### Defect found and fixed during this test
|
||
|
||
- **RSU CAM positions were decoded but never displayed** (see T7). App-side; the firmware forwarded
|
||
all 611 correctly. Fixed and re-verified in the same session.
|
||
|
||
### Open, found by this test
|
||
|
||
- **Station ID rotation** (see T8) fragments DENM and CAM identity across a rotation. Not yet
|
||
handled.
|
||
|
||
### Untested here
|
||
|
||
- **Link recovery** — unplug/replug and USB permission re-grant were not exercised; needs physical
|
||
intervention.
|
||
- **Sustained load at road rates.** This bench ran at 9.4 frames/s. The drive data implies 24–32
|
||
frames/s with frames 3× larger, where the TX-mutex interaction (400 ms worst-case hold vs the
|
||
1 Hz heartbeat and the phone's 3-beat dead-link timeout) becomes the thing to watch.
|
||
- **The link's actual ceiling**, which has never been saturated and so is unmeasured.
|
||
- **MAPEM** — nothing on air is transmitting it; no decoder written.
|
||
- **Secured messages** — 75 frames with GN `NextHeader=2` appeared in earlier pcaps; these are
|
||
rejected by design. The bench runs with `ItsGnSecurity = 0`.
|
||
|
||
### Recommendation
|
||
|
||
Ready for continued bench and short-range field work as it stands. **Not ready for a road drive
|
||
past real RSUs** until items 1 and 2 land, because the failure there is silent: oversize SPATEMs are
|
||
counted and dropped, so the symptom is "the intersection never appears" rather than an error.
|
||
|
||
## Reproducing
|
||
|
||
Capture: `adb logcat -d` filtered on `CamUseCaseRepo` while subscribed to `v2x/rx/#` on both
|
||
brokers. Decode cross-checks use `asn1tools` with the modules in `asn1/` — see `asn1/README.md`.
|