Every field of the GeoNetworking Source Position Vector this firmware sent
was a compile-time constant: the bench coordinates, speed 0, heading 0,
TST 0, station type passengerCar and one fixed MAC. The CAM inside
described a moving cyclist while the GN header around it described a car
parked at the bench.
SERIAL_MSG_CAM_TX_PV (0x05) puts a 24-byte prefix ahead of the CAM UPER:
MAC, station type, PAI, TST, latitude, longitude, speed and heading, all
values the phone already has when it builds the CAM and none of which
this chip can know. geonet_wrap_shb now takes them as a gn_lpv_t, and
tx_radio_task hands the same MAC to dot11p_build_frame, so the 802.11
source address and the GN_ADDR MID stay one address across a pseudonym
change. Speed is clamped rather than masked, since an overflowing 15-bit
value flips its sign bit and reads as travelling backwards.
This reverses the Phase 03 decision that the firmware owns the
pseudonym. A pseudonym only protects anyone if the MAC, the GN_ADDR and
the CAM's stationID change together, and the phone owns the stationID.
The heartbeat gains a capability byte (payload[7], bit0 = CAM_TX_PV),
appended so an app reading the first 7 bytes is unaffected. The app sends
0x05 only once it sees that bit, so app and firmware can be updated in
either order. CAM_TX (0x01) is still handled and falls back to the bench
values, with the station type corrected to cyclist to match the CAM.
Verified on air from the COM10 test board, decoded independently by the
CiT One's gnHeader: 24 of 24 CAM_TX_PV frames matched the sent position
vector field by field, and so did the CAM station ID. The legacy path
delivered 23 of 24 frames with no field mismatches. Flashed on the COM3
OBU and its boot log is clean.
Also corrects the SERIAL_LINK_MAX_PAYLOAD comment, which still named the
400-byte receive capture buffer as the ceiling on the RX path. That
buffer is 800 bytes now, so the serial link is the ceiling, and larger
payloads are dropped and counted there.
rx_item_t is ~800 bytes at RX_FRAME_MAX_LEN, and wifi_promisc_rx_cb declared one
as a local. That callback runs on the WiFi driver's own task, already several
frames deep in the driver's call chain, on a stack of roughly 3.5 KB
(CONFIG_ESP_WIFI_TASK_STACK_SIZE, left at its default). Putting a fifth of that
stack into a single local is a stack-overflow risk that only appears under real
traffic - in front of an RSU rather than on the bench - and would present as a
random panic rather than anything pointing at its cause.
Both instances are now static: one in the callback, one in rx_forward_task. Safe
because each is touched by exactly one task, so there is no re-entrancy to guard
against; the same reasoning serial_link.c already uses for its static send
buffers. xQueueSend copies the struct out before returning, so reusing the
callback's buffer on the next frame is fine.
Firmware-only, no protocol change, so it does not require a matching app install.
Re-verified against live traffic after flashing: 1094 frames over 125 s with zero
decode failures, USB errors, detaches, crashes or mutex timeouts. SPATEM capture
rose from 3.20/s to 3.98/s against a theoretical maximum of 4.00/s, which is the
direction relieving stack pressure would produce, though RF geometry moves
between runs and this is not proof.
Report updated with T9, the accepted 512-byte ceiling, and the decision to drop
Phase B: the intersection use case is CAM-driven and needs none of it.
The firmware forwarded CAM only: gn_unwrap_cam accepted single-hop broadcast
(HT=5) and BTP port 2001, so every DENM was dropped before it reached the phone.
Real OBUs disseminate DENM by GeoBroadcast (HT=4), whose 44-byte extended header
also carries the hazard's relevance area - materially more useful on a map than
the sender's own position, since a sender may be relaying for someone else.
Firmware
- gn_unwrap_cam -> gn_unwrap_its: accepts GeoBroadcast alongside TSB/SHB, and
BTP ports 2001 and 2002, extracting the GeoBroadcast destination area. Both
extended-header lengths were measured against live air capture rather than
read off a spec table. Secured packets (Basic Header NextHeader=2) are
rejected rather than misparsed.
- SERIAL_MSG_CAM_RX (0x02) superseded by SERIAL_MSG_V2X_RX (0x04): a 14-byte
prefix carrying BTP port, RSSI and the destination area. Adding MAPEM later
needs a decoder on the phone but no protocol change. 0x02 stays reserved so
the numbering is not silently reused.
- Promiscuous RX capture buffer 400 -> 800 bytes. A real GeoBroadcast DENM is
around 500 bytes on air and was being truncated mid-payload, which no amount
of correct unwrapping downstream could have recovered from.
- geonet_wrap_shb, both firmwares: the SHB extended header is 28 bytes, not 24.
The Source Position Vector is followed by a 4-byte reserved field; without it
a standards-strict receiver reads the CAM payload's first two bytes as the BTP
destination port.
App
- DenmUperCodec: UPER decoder for the ManagementContainer and the
SituationContainer's eventType. ValidityDuration is 17 bits, not 16, and
ManagementContainer, SituationContainer and CauseCode each carry their own
extension bit - a single wrong bit made a real frame read causeCode 47
instead of 94.
- DenmEvent gains actionID (originatingStationID + sequenceNumber), stationType,
termination, detectionTime, relevance radius and RSSI. Dedup keys on actionID
where available, so a termination lands on the event it ends instead of
creating a second pin.
- denmEvents merges the MQTT and over-the-air sources and drops terminated
events. The V2X list view now shows hazards above the CAM stations; it
previously took no DENM parameter at all, so hazards reached the map but never
the list.
- DenmParser: the Use Case API sends causeCode as a string enum, so reading it
as an Int always yielded null.
Testing
- DenmAirReceiveTest covers the V2X_RX prefix and the decoder using real frames
from a live capture as fixtures. Expected values were cross-checked against
the ETSI ASN.1 modules via asn1tools, which agreed on all 1885 decodable
DENMs across the capture set, every field including detectionTime.
- Verified on hardware: a CiT One HLN-SV DENM decodes as cause 94/0 with a
1000 m relevance radius at 1 Hz alongside CAM, with no decode failures and no
unexpected BTP ports.
Also replaces em dashes with hyphens throughout the user-facing strings,
including the German translation.
Enumeration:
- Merge the library's stock probe table instead of replacing it, so adding
Espressif 0x303A/0x1001 doesn't drop every other supported device
- Select the ESP32-C5 by VID/PID rather than list position
- Log USB interface descriptors to distinguish CDC data from the JTAG interface
Lifecycle:
- Don't close the shared port in MqttViewModel.onCleared() - the foreground
recording service outlives the ViewModel and would beacon into a dead port
- Handle ACTION_USB_DEVICE_DETACHED so the UI stops reporting a stale link
- Implement the STATUS heartbeat on both sides (1 Hz) plus a phone-side watchdog
- Surface write failures and firmware drop counters on the CAM Pinger card
Protocol:
- Assert DTR/RTS on open (unverified on hardware - see FLASHING.md step 5)
- Raise SERIAL_LINK_MAX_PAYLOAD 160 -> 512 on both sides; real third-party CAMs
exceed 160 and were being silently dropped at the resync branch
- Move the enlarged buffers off task stacks; serialize send_frame with a mutex
Firmware and app must be updated together - a 512/160 mismatch fails silently.
- Firmware: rewrite obu-firmware TX loop to be serial-driven (no on-chip timer), add promiscuous RX + GeoNetworking/BTP unwrap (gn_unwrap.c), add binary UART framing to the phone (serial_link.c/.h). Drop local cam_encode() - CAM is now built on the phone.
- Kotlin: byte-exact UPER CAM encoder/decoder ported from cam.c (BitWriter/BitReader/CamUperCodec), matching SerialFrame codec, real UsbSerialTransport (usb-serial-for-android), CamTransmitLoop (1Hz base rate, event/geofence boost, ESP32-C5-only), wired into CamUseCaseRepository for RX and TripRecordingService for TX.
- Add V2X message retention: persist all CAM (own+remote) to Room while recording, drop otherwise (DB v2 -> v3 migration).
- Add jitpack repo + usb-serial-for-android dependency.
Fixes: UsbSerialTransport now uses SerialInputOutputManager.start()/stop() (this lib version manages its own thread internally) instead of manual Runnable/Thread submission, which didn't compile.