73d4477f1eccefaca853c2cace38f6fde4a85621
21
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
83153a0971 |
Send each CAM with its position vector, a rotating pseudonym and GNSS time
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. |
||
|
|
5ec3619cbe |
Stop retaining detected manoeuvres; the CAM rate bump is their only consumer
The detector runs to raise the CAM transmit rate through a manoeuvre. Nothing else read its output once the UI was removed, so keeping the rows was storing data with no reader on the chance it would one day be analysed. Drops the detected_events table in schema v5, deletes DetectedEventEntity and the DAO and repository methods behind it, removes the insertEvent call from the recording service, and removes the per-event rows and their five columns (event_type, confidence, peak_accel, peak_gyro, duration_ms) from the trip CSV along with the events parameter threaded through buildTripCsv and shareTripCsv. A detected manoeuvre now lives for the length of one onDetectedEvent call. MIGRATION_1_2 still creates the table: a v1 install upgrades 1-2-3-4-5 and so creates it before v5 drops it. Removing it from the earlier migration would break that path for anyone who has not upgraded yet. trips.eventCount is kept. Dropping a SQLite column means recreating the table and copying every recorded ride across, which is real risk for one unused integer; the service still writes an accurate count and the CSV header still reports it. It is the only thing left about detected manoeuvres. This closes off the route to the false-positive measurement that 11.3 flags as missing, so 11.3 now says that outright rather than pointing at an export that no longer carries the data. Docs 11.3/11.4, the user guide, the README and the traceability matrix updated to match. 55 tests, 0 failures. |
||
|
|
1ad123a6f8 |
Make the event detector a CAM rate input, not a ride-stats readout
The detector's only live consumer is the CAM transmit-rate policy: every emitted event calls CamTransmitLoop.onDetectedEvent, raising the beacon rate from 1 Hz to the elevated rate for five seconds so nearby stations get denser updates through a manoeuvre. Counting one's own braking events is not a goal of this project, so the display is gone and the detector stays: the live per-type counters and their notification text, the event pins and detail sheet on the trip review map, and the event chip on the history card. Events are still persisted and exported to CSV, which is the only route to the tuning measurement section 11.3 says is missing. Fix two defects found while documenting the detector. TripRecordingService overrode nine of DetectionConfig's twelve parameters in its constructor, so the tests validated the Phase A defaults while the phone ran something materially less sensitive. The tuned values are now the defaults and the override is deleted; the numbers moved location, not value, so detector sensitivity is unchanged. EventDetectorTest now sets only windowSize and the sustained-frame counts and inherits every signal threshold, which cannot drift again. That was not a free change and makes the same point from the other side: at the real thresholds the old stimuli triggered nothing. Accel alternating 3.5/0.5 gives a std dev of 1.5 and never clears 1.8, and the moderate-braking case used a 0.8 m/s drop that never clears 1.0. Those stimuli are re-derived against the real values. brakingHighConfidenceRate was documented as a rate but has always been compared against the peak cumulative drop from the onset speed, which grows with episode length, so HIGH was assigned more readily than the name implied. Renamed to brakingHighConfidencePeakDrop rather than changing the comparison: "lost more than 1.5 m/s in one episode" is coherent, whereas a rate off a 1 Hz speed signal sampled at 50 Hz spikes on a near-zero divisor early in an episode. Output is unchanged, so the existing confidence assertions stay evidence instead of being re-baselined. Docs 11.3/11.4 updated in place, including the correction of a claim that detected events do not reach the V2X side; the rate-bump path already existed when that was written. 55 tests, 0 failures. |
||
|
|
034ef22336 |
Decode raw v2x/rx on the CiT One path, and stop tracking our own CAM pings
The Use Case app's v2x-uca/output/json topics are a rate-limited and lossy view: traffic the OBU's radio actually heard, the ESP32's CAM pinger among it, never reached the app. The raw v2x/rx topics carry everything, as RecvV2XMessage protobuf with the ITS-G5 PDU in one bytes field (CI-CiT MQTT API section 2.4). RecvV2xMessage is a minimal protobuf wire-format reader for the three fields needed: btpHeader type and destination port, the GeoNetworking destination-area radius, and the payload. Hand-written for the same reason the ASN.1 codecs are, rather than adding protoc and the protobuf Gradle plugin and vendoring a third-party .proto into this repository. Field numbers are pinned by a byte fixture written out by hand from the encoding rules, not generated by our own encoder. Raw payloads now travel as bytes rather than String. The previous UTF-8 round trip replaced every byte that is not valid UTF-8, leaving a payload that still looked plausible in a log and decoded to nothing. CAM, DENM and SPATEM from both transports now meet in shared handlers, so everything downstream is transport-agnostic. SPATEM works on the CiT One path for the first time, and DENM gains its relevance radius there. Where both sources describe the same event the decoded one wins: remote CAMs from the processed topic are suppressed while the raw topic is live, and DENMs dedup on ETSI's actionID with the decoded list last. The processed topics stay subscribed as a fallback for an OBU whose configuration does not publish the raw ones. Two defects found while testing this: CamPinger transmits under a fixed bench station id, deliberately distinct from the persisted one, but the self-heard filter only knew the persisted id. Every ping therefore came back through the ESP32's promiscuous receive as a remote road user sitting exactly on top of the ego position, moving at the ego's own speed and heading, and was handed to the detection engine as a collision partner for itself. The rule now lives in OwnStationIds, covers both ids, and has tests, so a third transmit path cannot reintroduce the same gap quietly. Self-heard frames are now counted and reported on the pinger card instead of being discarded. That round trip is the only direct evidence the serial link, the ESP32's transmit path and its receive path all work, which is what the bench pinger exists to demonstrate. Also: the stationType warning banner no longer shows in ESP32-C5 mode. It reads a value from the CiT One's obu_gnss topic, which that hardware never publishes, so it stayed on screen reporting on an OBU that was no longer in use. |
||
|
|
ebe1c9edfd |
Dashboard: surface the nearest hazard and the next signal change
Two cards below the status cards, each shown only when there is something to show and each opening the V2X screen when tapped. Hazard: cause name, distance, and a count of the others behind it, ranked closest first. A hazard whose distance cannot be resolved, because there is no fix yet, sorts last rather than being dropped. Traffic light: the intersection changing soonest, its leading phase with a countdown, and every signal group as a colour-coded chip. The countdown runs on its own 500 ms clock rather than on SPATEM arrivals, so it cannot freeze mid-count and keep claiming a light is about to change after the RSU stops transmitting. Signals are ranked by time-to-change rather than by distance because SPATEM carries no position at all. Placing an intersection needs MAPEM geometry, which nothing on air is currently sending. Also fixes bottom-nav taps. One rule now applies to every tab: a tap lands on that tab's own screen, popping back to it when it is still on the stack so the gesture behaves exactly like Back or a back swipe. saveState/restoreState are gone, since on this flat graph a restored back stack brought back the sub-screen the rider was on instead of the tab root, which is the opposite of what the tap asked for. Tabs also now stay lit on the screens that belong to them. |
||
|
|
081347f31b |
Tapping the active bottom-nav tab returns to that tab's root screen
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.
|
||
|
|
cc35994e68 |
Fix four EventDetectorTest cases that never passed
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. |
||
|
|
d0701ccea4 |
Show roadside units in the station list; log ESP32 drop counters
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. |
||
|
|
b2b57fa39e |
Vendor the ASN.1 modules the codecs are verified against; untrack IDE churn
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. |
||
|
|
eb6150260b |
Fix NPE crash on the V2X map when messages arrive during teardown
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.
|
||
|
|
a5ad3dcc5d |
SPATEM receive, RSU CAM decode, and two ASN.1 encoding fixes
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. |
||
|
|
0ccb867228 |
DENM over-the-air receive on the ESP32-C5 path
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. |
||
|
|
f1770e11dd |
ESP32 link indicators, trip export with V2X + RSSI, trip/CSV cleanup
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. |
||
|
|
f507a8a9fd |
Fix UPER encoding of CurvatureCalculationMode; verified on hardware
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.
|
||
|
|
b91eb460dc |
Phase 03: CAM decode coverage, real sensor data in TX, V2X monitor for ESP32 path
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. |
||
|
|
64fb78590e |
Phase 03: ESP32-C5 serial link hardening + bench diagnostics
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. |
||
|
|
33c4ec5998 |
Phase 03: real CAM UPER codec + ESP32-C5 TX/RX serial link
- 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. |
||
|
|
842d362c8f | initial commit | ||
|
|
7b8a8cbb32 |
feat: CAM-based use case detection engine, decouple DENM from sensor triggers
Added: - UseCaseDetectionEngine: IMA-B, IMA-S, RTW-B, LTW-B, SMVA/BCW-B via per-station CAM history, yaw-rate/heading-trend turn detection, Info/Awareness/Warning alerts - Ego state from obu_gnss (~4Hz, yaw rate) with phone GPS fallback when stale - Human-readable alert narratives + technical detail in V2X Monitor - Settings > Use Case Alerts per-use-case toggles Fixed: - DENM no longer auto-fires on braking/stopping; manual-only (antenna/RSU test) - Removed unwired CamBuilder.kt (dead code, contradicted CAM-only architecture) - TripRecordingService no longer depends on MQTT/OBU connectivity |
||
|
|
dbd2fc780b |
fix: prevent LazyColumn crash on duplicate timestamps in V2X topic detail view
|
||
|
|
ae17d4fcba | Initial Commit |