The README still described the project as it was before Phase 03. Several
claims had become actively wrong rather than merely dated, and the last one
is the kind a reviewer would catch:
- "no ASN.1 encoding in the app" - the app hand-encodes and decodes CAM,
DENM and SPATEM bit by bit on the ESP32-C5 path. domain/asn1/ is now the
highest-risk code in the project, and the README denied it existed.
- The phase table listed Phase 03 as Bluetooth BLE. Phase 03 is the
ESP32-C5; Bluetooth is not implemented and the requirements leave it open.
- "communicates with the OBU exclusively via the consider it MQTT API v6"
is true of one of the two hardware paths.
- The feature list claimed MAP and CPM display. There is no MAPEM decoder
and nothing in the tree supports CPM, so both claims are dropped rather
than carried forward.
- DENM transmission was listed as a headline feature with no indication that
it is a manual antenna and range test tool, CiT One only, and deliberately
never triggered by a detected event or a use case alert. That decoupling is
the project's central architectural rule and the README implied the
opposite.
- The architecture tree predated domain/asn1, domain/usecase, domain/cam,
data/cam, the serial transport, obu-firmware/ and asn1/.
Added: the project goal, the two hardware paths and the point where they
converge, a verification section, a documentation index, and a status
section.
The convergence point is worth stating in the README rather than only in the
technical document, because it is what makes the second OBU a drop-in rather
than a fork: both paths normalise into the domain Cam type at
CamUseCaseRepository, and everything above it is shared and
transport-agnostic.
The status section says plainly that requirement 11.6 is not met, that
messages are not signed, and that the detection thresholds are untuned
estimates. Someone arriving at this repository should learn that from the
front page rather than from page forty of a Word document.
Word creates ~$<name>.docx beside a document while it is open and removes it on
close. docs/~$crOBU-User-Guide.docx was showing as untracked while the user guide
was being edited, which is exactly the kind of transient file a git add -A sweeps
in by accident.
Tapping Settings while on Settings > Connection appeared to do nothing. The tab
navigated to its own route, but restoreState = true then restored that tab's
saved back stack, putting the sub-screen straight back on top. The only way out
was the back button or a back swipe.
When the tap targets the tab already in use and the current destination is deeper
inside it, pop back to the tab's own screen instead of navigating. Only entries
above the tab root are removed, so Back and back-swipe behave exactly as before -
both routes out of a sub-screen now work.
The tab also stayed unhighlighted while any sub-screen was open, because selected
compared the current route for equality with the tab's route. Ownership is now
derived from the existing route naming convention, so settings/connection belongs
to Settings and trip_review/{tripId} belongs to Trips. A new settings/* screen is
picked up automatically; a sub-screen named outside its tab's prefix would need a
line in ownsRoute.
Two documents, wiki-style so they import cleanly: short titled sections,
tables over prose where the content is comparative, and cross-references
between sections rather than a narrative that has to be read start to end.
MicrOBU-User-Guide.docx is for riders. Features, setup for both hardware
paths, what each screen shows, what the five alerts mean in plain language,
troubleshooting. No ASN.1, no BTP ports, no bit widths anywhere in it. The
alert descriptions and the link states are taken from values/strings.xml so
the guide and the interface use the same words.
MicrOBU-Technical-Documentation.docx is for supervisors and stakeholders.
Architecture, the message path end to end, the serial protocol, the codecs
and how they are verified, the full requirements matrix, the bench results,
and the decisions. Section 15 is fourteen decisions written as chosen /
alternative / reasoning / cost accepted, because the alternative is the part
a supervisor asks about and it was previously recorded only in commit
messages.
Nothing is re-derived. Every measurement is cited from
05-obu-bench-test-2026-08-25.md, 04-transmit-setup.md, asn1/README.md or
the commit history. Where a claim has no evidence it is marked as unverified
rather than asserted:
- 11.6 Phase A success criteria is stated as not met, in its own subsection.
The bench proves reception; it cannot prove the use case with two moving
stations because nothing on the bench moves. This is the central claim of
the project and it needs a real ride.
- The tx_custom.c bypass is described as the least defensible component in
the system and load bearing, with the reverse-engineered struct layouts
and the skipped sanity checking spelled out.
- The 5900 MHz transmit story is marked a mitigation for a hypothesis, not a
diagnosis, and the isolation test that would settle it is named as not run.
- The detection thresholds are presented as untuned engineering estimates in
both documents, since presenting them as validated is the easiest and most
damaging overstatement available here.
Twenty image placeholders, none of them filled. Each is a shaded block
carrying a caption and a "Must show" line. For the three screenshots that
exist the line names the bench session and section they came from; for the
four diagrams that do not exist yet it is a full drawing spec, so the two
hardware paths, the message path, the frame layout and the verification loop
can be drawn from the document without re-reading the source.
references.bib collects the standards as BibTeX for a later publication:
EN 302 637-2/3, TS 103 301, SAE J2735, TS 102 894-2, EN 302 636-4-1 and
-5-1, TS 103 248, TS 103 097, IEEE 802.11 OCB, plus the C2C-CC white paper
and the vendored parser provenance.
Also tracks two documents the new ones cite that had never been committed:
docs/01-requirements-traceability.md and 04-transmit-setup.md. Section 18.3
lists them as repository sources, which would have been a dangling reference
otherwise.
Not included, deliberately: the two consider it PDFs cited in section 18.2.
They are third-party vendor documentation and redistributing them is a
licensing decision, not a documentation one.
Still missing: the German user guide. values-de/strings.xml already fixes the
terminology for it.
These have been red since the initial commit. All four failed for the same
reason, and in every case the test was wrong rather than the detector.
EventDetector requires two conditions to hold on the SAME frame, and one of them
is a rolling-window statistic that takes time to respond. The tests ignored that
lag, so they described situations the detector cannot see - and could not have
seen at any point in its history.
Braking (3 tests). Each filled the accel window with a CONSTANT value, then
stepped the GPS speed down. The speed drop is therefore true on exactly one
frame, and on that frame the accel std dev is still ~0.75 against a 1.2
threshold, because the window is full of the constant. By the time the window
recovers (frame 4), prevSpeedMps has caught up and the drop is 0. The two
conditions never coincide and no BRAKING is possible. Constant accelerometer
output right up to the instant of a brake is not physical either: the IMU is
sampled continuously while GPS speed lags, so the shaking precedes the reported
drop. The tests now establish that variability during the cruise phase.
Stopping. The second episode ran for stoppingFrames + 5. But stopping requires
the accel std dev to be BELOW a threshold, and the window still held the five
moving samples from the acceleration burst between episodes. Those take 8 frames
to drain far enough for the std dev to fall under 0.15, leaving 17 of the 21
frames the event needs. The first episode is unaffected because the window starts
empty. The episode is now long enough to cover the settling time.
Verified by simulating the detector's exact arithmetic against both the old and
new inputs before touching the file: the old profiles produce no events, the new
ones produce BRAKING/HIGH, BRAKING/HIGH, BRAKING/MEDIUM and two STOPPING.
No production code changed - the detector behaves consistently and defensibly.
Assertions are unchanged; only the stimulus is now something a bicycle could
actually produce. Suite is 41 tests, 0 failures.
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.
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.
asn1/
Three tests assert exact bytes - CamEncodeGoldenTest, DenmAirReceiveTest and
SpatemUperCodecTest - and their expected values came from asn1tools compiled
against ETSI modules that existed only as an untracked working copy on one
machine. A golden-byte fixture nobody else can regenerate is a fixture nobody
can safely touch, so the modules are now in the repo.
Only the seven .asn files those tests need are copied, 576 KB of a 4.2 MB
checkout; the upstream Rust parser is not used by this project at all. Verified
sufficient in isolation: copied into an empty directory, all three specs compile
and reproduce the committed golden CAM bytes byte-identically.
Source is consider it GmbH's C-ITS-Parser (github.com/consider-it/C-ITS-Parser)
at f457426, MIT licensed - LICENSE is retained alongside as that requires. The
schemas themselves are ETSI's standard definitions; upstream's contribution is
assembling them into a compilable set. asn1/README.md records the provenance,
which module pairs with which message, and the rule that matters: never
regenerate a golden fixture from this project's own encoder, because sharing a
mistake between encoder and decoder is exactly the failure these files exist to
catch.
Doc references in the codecs and tests now point at asn1/ instead of the
untracked checkout, and C-ITS-Parser/ is gitignored so the working copy beside
the project is never picked up.
Untracked local state
- .idea/deploymentTargetSelector.xml rewrites itself on every deploy, so it has
been showing as modified in essentially every commit. Along with
deviceManager.xml, appInsightsSettings.xml and studiobot.xml it is per-machine
state, not project configuration.
- obu-firmware/sdkconfig.old is ESP-IDF build output - it is the previous
sdkconfig, rewritten on every build. sdkconfig.defaults remains tracked, since
that is the configuration actually chosen.
All five stay on disk; only the tracking is removed. Also ignores
.claude/settings.local.json, which is per-machine, while leaving the skills
beside it committable as project knowledge.
osmdroid's MapView.onDetach() permanently tears the view down: afterwards its
MapViewRepository holds a null MapView, so constructing a Marker against it
throws NullPointerException from inside InfoWindow's constructor.
It was being called from a DisposableEffect keyed on the lifecycle owner, which
disposes independently of the AndroidView that owns the map. The update block
could therefore still run against an already-detached MapView and rebuild its
markers:
java.lang.NullPointerException: Attempt to invoke virtual method
'MapViewRepository MapView.getRepository()' on a null object reference
at org.osmdroid.views.overlay.Marker.<init>(Marker.java:116)
at V2xLiveMapViewKt...(V2xLiveMapView.kt:119)
The crash is dated 2026-08-17 18:19, ten minutes after DENM reception went live
on the device. The defect was always there, but every incoming message
recomposes this view, so going from occasional updates to one per second made
the window easy to land in - and SPATEM at ~2 Hz makes it easier still.
Moves the teardown to AndroidView's onRelease, which is the callback that means
"this View is gone" and after which Compose guarantees no further update.
SPATEM over the air
- gn_unwrap.c accepts BTP-B port 2004 alongside 2001/2002. The serial protocol
already carries the port in its V2X_RX prefix, so nothing else changed there.
Note the crossover that makes this easy to get wrong: SPATEM is port 2004 but
messageID 4, while MAPEM is port 2003 and messageID 5.
- SpatemUperCodec decodes SPAT down to per-signal-group phase and timing. The
bit layout was validated by replaying 79,042 real SPATEMs - the whole
2026-03-18 drive across 7+ RSUs plus the bench trigger - against asn1tools
using the ETSI modules. All 79,042 matched on every field, none hit an
unsupported branch. Two traps are pinned by tests: TimeChangeDetails is the
one SEQUENCE here that is NOT extensible (5 optional bits, no extension bit),
and maneuverAssistList cannot be skipped when present - it is variable-length,
so it has to be walked to find where the next movement starts.
- The V2X list shows one row per intersection with each signal group coloured by
phase and a countdown where the RSU supplies timing. TimeMark wraps hourly, so
the countdown corrects for it; without that it reads hugely negative once an
hour, precisely when someone is watching it.
- Entries expire after 15 s, much shorter than DENM's window: a traffic light
that stopped updating is not "still green".
Size caveat, deliberately deferred: SERIAL_LINK_MAX_PAYLOAD is still 512, so a
SPATEM over ~498 bytes is counted as an oversize drop. The bench RSU sends 58
bytes and is unaffected, but real road RSUs measured 555 median / 1243 max, so
roughly 70% would not arrive. Raising the cap also requires enlarging
RX_FRAME_MAX_LEN and moving rx_item_t off the WiFi driver's callback stack,
where it would otherwise overflow.
RSU CAM decode
- HighFrequencyContainer is a CHOICE, and a roadside unit picks
rsuContainerHighFrequency, which carries no kinematics at all. The decoder
bailed on that branch, so every RSU CAM was dropped - including the bench RSU,
which sends CAM and SPATEM from the same station id. It now decodes for
position and stationType.
- RSU CAMs are kept out of UseCaseDetectionEngine. They arrive as a permanently
stationary station at a fixed point, which is exactly the shape the
stopped-vehicle and intersection-movement use cases match, and would raise a
standing false alert for as long as the RSU was in range.
CAM transmit: yawRateConfidence
- YawRateConfidence has nine enumerands (0..8), so UPER needs 4 bits and
"unavailable" is 8. The encoder wrote 3 bits with value 7 - one bit short and
the wrong symbol - shifting every field after yawRate for any standards-strict
receiver. The decoder read 3 bits too, so phone and ESP32 agreed with each
other and with nothing else.
- This is the third instance of that exact failure mode in this project, after
CurvatureCalculationMode and the GeoNetworking reserved bytes. A round-trip
test through our own decoder structurally cannot catch it, so CamEncodeGolden
Test asserts the bytes asn1tools produces instead: it decoded this encoder's
output and re-encoded it byte-identically. Confirmed on air afterwards - 26 of
our own CAMs captured back off the OBU's receiver, all 26 accepted, where the
same decoder rejected them before.
DENM
- Hazards now expire 60 s after their last repetition. This needs a clock, not
just a filter: both source flows only emit when a DENM arrives, so a sender
that drives away or loses power would never trigger a recompute and its hazard
would stay on screen indefinitely.
- The MQTT path was dropping every DENM for two independent reasons, both found
by checking the payload against CI-CiT-MQTT_API_Documentation-v6 listing 2.6
rather than guessing: the station id key is originatingStationId, and
eventPosition IS a GeoJSON Point rather than an object containing one. Also
parses termination (presence is the signal), sequenceNumber, stationType and
the RFC3339 detectionTime. Note roadSideUnit is 15, not 12 - the enumeration
has a gap after tram(11).
V2X screen
- The decoded CAM/DENM list now renders on the CiT One path too; it was gated to
the ESP32-C5 path and CiT One fell through to the raw MQTT topic list. Those
topics move to their own tab, hidden on the ESP32-C5 path where there is no
broker.
Testing
- Adds org.json as a test-only dependency: the android.jar stub throws
"not mocked" on every JSONObject call, which made the MQTT payload parsers
untestable off-device.
- 23 V2X tests pass. EventDetectorTest's 4 failures are pre-existing and
untouched by this change.
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.
UI:
- Top bar shows a USB glyph reflecting the serial link on the ESP32-C5 path,
instead of a Wi-Fi glyph driven by an MQTT state that is permanently
disconnected there.
- Recording screen's OBU stream row follows whichever transport the selected
hardware uses. It previously read as offline throughout a recording that was
actively beaconing CAMs.
- Live map uses distinct markers: a centred dot for own position (a fact about
the viewer, not a tracked object) and a teardrop pin for remote stations,
tinted by alert severity. Both were osmdroid's identical default pin before,
and severity required tapping a marker to read its label.
- Sensor Monitor moves out of the bottom nav to Settings > Developer. A live
phone-sensor feed is a bench tool; the Dashboard already reports GNSS/IMU
health. Screen and route are unchanged, just not in the rider's way.
Detection engine on the ESP32 path:
- Seed our own StationID from the persisted value CamTransmitLoop transmits.
It was null here (the CiT One learns it from v2x/rx/obu_gnss, which doesn't
exist on this path), so the self-heard-TX filter never fired: the ESP32 runs
promiscuous for raw TX to work at all, hears our own CAMs back off the air,
and they were tracked as a remote station - a ghost vehicle on top of the ego
position, fed to the engine as a collision partner for itself.
RSSI:
- The firmware has always sent per-frame RSSI in byte 0 of every CAM_RX frame;
the app discarded it. Now carried on Cam, persisted per V2X message, shown per
station in the received-CAM list, and exported. Null for own CAMs and the
whole CiT One path, neither of which has a measurement.
Trips, CSV and export (schema v3 -> v4):
- trips.sessionId links a trip to the CSV session recorded alongside it. The two
are written by independent subsystems that the Recording button happens to
start together; without the link, deleting a trip orphaned its CSV forever.
Timestamp matching was rejected - close recordings would delete the wrong file.
- Deleting a trip now removes the CSV file AND its sessions row, so it stops
appearing in the Session Log pointing at nothing.
- Per-trip combined CSV export: GPS track, detected events, V2X messages (with
RSSI) and the raw sensor samples in one file, keyed by a leading type column.
One file rather than a zip of tables because the point of the export is
correlating those streams, and splitting them pushes the join downstream.
- Export takes the Activity context. Sharing from the ViewModel's Application
context threw AndroidRuntimeException on startActivity - this crashed on the
first tap of the share button.
Trips recorded before v4 have a null sessionId, so their exports omit raw sensor
rows and their CSVs still need clearing by hand once.
CurvatureCalculationMode is the one extensible ENUMERATED in CAM:
ENUMERATED {yawRateUsed(0), yawRateNotUsed(1), unavailable(2), ...}
UPER encodes an extensible ENUMERATED as an extension bit followed by the root
index - 1 + 2 = 3 bits. All three of our encoders wrote only the 2-bit index,
shifting yawRate and the entire low-frequency container one bit early for any
standards-compliant receiver.
It went unnoticed because every end of this project shared the mistake: the
Kotlin codec was ported bit-for-bit from cam.c, so phone and ESP32 agreed
perfectly with each other and with nothing else. Confirmed against the ETSI
ASN.1 in the C-ITS-Parser checkout, where rasn marks this type - and only this
type - #[non_exhaustive].
Fixed in all three copies of the encoder (app CamUperCodec.kt,
obu-firmware/main/cam.c, obu-cam-transmistter/main/cam.c) plus the decoder,
which now rejects rather than misreads a set extension bit. Frame size is
unchanged at 43 bytes. Transmitter reflashed and the phone decodes its CAMs.
Also in this change:
- serial_link: skip send_frame entirely when no USB host is attached, and raise
the tx mutex timeout above the worst-case hold. With the phone unplugged every
write blocked its full timeout while holding the lock, so forwarded CAM_RX
traffic starved the 1 Hz heartbeat - observed as "tx mutex timeout, dropping
frame" on the console, and it would have tripped the phone's link watchdog.
Verified gone on hardware.
- Log decoded and failed CAMs in CamUseCaseRepository. "The app shows nothing"
had two indistinguishable causes; a silent `?: return` made this bug much
harder to find than it needed to be.
- Remove the ESP32 send-only/send-and-receive toggle. Reception can't be
disabled in firmware (raw TX only works while promiscuous), so it was an
app-side filter pretending to be a radio control.
- V2X monitor follows the serial link state on the ESP32 path instead of MQTT,
which is permanently disconnected there; CAM intake is gated on the link being
up, and engine state is cleared when it drops.
- About screen: 0.5.0, Phase 03.
- Track obu-cam-transmistter, the bench CAM transmitter. Its cam.c is compiled
(unlike obu-firmware's reference copy) and must stay bit-identical to the other
two - this commit is what that coupling costs when it's broken.
- Document the two-toolchain split: this project builds on IDF 5.5.4, obu-firmware
on the pinned 6.1. Exporting both in one shell fails confusingly.
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.
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.