The app side of the firmware's CAM_TX_PV message. Until now the phone
handed the ESP32 bare CAM bytes, so the GeoNetworking header around them
could only carry the firmware's bench placeholders.
GnPositionVector.fromCam builds the Source Position Vector from the same
Cam the UPER is encoded from, so the two layers cannot disagree about
where the rider is. Position is rounded exactly as CamUperCodec rounds
it, heading wraps into 0..3599, and non-finite values become 0. PAI is
set when Android's horizontal accuracy is at most 24.7 m, the 40 m
itsGnPaiInterval/2 threshold converted from a 95% to a 68% confidence
radius. UsbSerialTransport.sendCamTx sends 0x05 once the heartbeat
advertises the capability and 0x01 otherwise, so this build still
transmits against older firmware, and logs which path it is on.
Pseudonyms. The station ID used to be created once per install and never
changed, under a MAC that never changed either, so every CAM this phone
ever sent was linkable to every other. PseudonymManager now owns the
station ID and the MAC as one identity and replaces both together every
10 minutes, or immediately if the clock goes backwards. Both are
persisted in a single edit, so a crash cannot leave them mismatched.
MACs are locally administered unicast and can never equal the bench
ping's. CamTransmitLoop takes the current pseudonym per CAM, and the two
most recently retired IDs still count as ours, so a frame sent just
before a rotation is not taken for a stranger.
GNSS time. On 2026-09-10 the bench phone's clock was 24 minutes fast:
with no SIM and no internet time it had no automatic time source, and
every CAM went out stamped in the future. GnssTimeSource moves transmit
timestamps onto SystemClock.currentGnssTimeClock() and falls back to the
wall clock without a fix, logging which one is in use and the measured
error. ItsTime is now the single rule for both the CAM's
generationDeltaTime and the GN TST. Receive paths stay on the wall clock
so everything they stamp remains comparable.
The bench pinger keeps its fixed station 999999 and a fixed MAC, so a
ping stays recognisable in a capture. 999999 now counts as ours only
while this phone's pinger runs and for 5 s after it stops. The previous
rule treated it as ours unconditionally, which hid another phone's pings
on the same bench.
Leap seconds are an open question, recorded in ItsTime: TimestampIts may
be TAI-based, which would put it 5 s higher. 85 tests, 0 failures.
CAM codec:
- Stop rejecting CAMs carrying a specialVehicleContainer. It is declared last in
CamParameters, after everything this decoder reads, so buses / emergency
vehicles / road-works vehicles now decode for position and kinematics instead
of being dropped outright
- Drop the lowFrequencyContainer parse - it extracted nothing into Cam, and its
reads were only correct when no high-frequency optionals were present
- Document why the 7 optional-presence bits are consumed but not acted on: UPER
writes a SEQUENCE's presence bitmap up front but each field's value in
declaration order, and all seven are declared after yawRate
- Field widths and container ordering verified against the ETSI ASN.1 sources in
the C-ITS-Parser checkout, not from memory
Transmit path:
- Own StationID is now a persisted random 32-bit value instead of a hardcoded 0.
Receivers key on StationID to track a station across CAMs, so every unit
broadcasting 0 made two MicrOBUs indistinguishable - including to this app's
own detection engine
- Populate longitudinalAcceleration from successive GNSS speed samples. Not from
the accelerometer: CAM wants signed along-track acceleration, and the raw
sensor is device-frame with gravity in it. Null outside a usable sample gap
rather than a fabricated value
- CAM pinger builds from live GNSS/IMU via PhoneCamBuilder instead of beaconing a
hardcoded bench coordinate with speed and heading pinned to zero, so it now
exercises the sensor pipeline and not just the wire. Sends nothing without a
fix, and reports that rather than sitting at "Sent: 0"
V2X monitor:
- Received-CAM pane for the ESP32-C5 path, replacing the MQTT topic list that is
permanently empty there. One row per station rather than per message - CAMs
arrive at 1-10 Hz per station, so the pane is bounded by road users nearby, not
by traffic rate. Nearest first, tinted by active alert level
- DENM hazard pins on the live map as a warning triangle, drawn above vehicle
markers. CiT One path only: the ESP32 firmware forwards BTP-B port 2001 (CAM)
and drops port 2002 before it reaches the phone
DenmParser uses tolerant field-name matching - the Use Case API's DENM JSON
schema is not yet confirmed against real payloads.
- 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.