83ccf335bb3cefe1821dfbf365365c66a9bff0a8
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
ae17d4fcba | Initial Commit |