Commit Graph
6 Commits
Author SHA1 Message Date
Ashin Walpola 73d4477f1e Move the capture tooling into the repo
live_capture.py and dump_pcap.py were untracked files inside the third-party
its-g5-receiver-firmware checkout, so the tools every on-air measurement depends
on were versioned nowhere and would vanish with a fresh clone of that project.
They are ours rather than that project's, so they now live in capture/, with the
setup notes rewritten as its README: which port the sniffer speaks on and why,
how to capture, how to check a capture, and how to flash the sniffer board.

live_capture.py carries the fix made on 2026-09-14. The sniffer's console
converts LF to CRLF on its way out, and that applies to every 0x0a byte of the
binary pcap stream, not only to log text, so every inserted CR shifts the rest
of the stream. A 787 KB capture parsed cleanly for only 82 of about 2000
records, and DENMs turned up on nonsense BTP ports because their payloads carry
0x0a often. undo_crlf() reverses it on the raw stream before any framing, which
is exact; afterwards a capture parsed to EOF and DENMs read as port 2002.
Captures taken before that date are truncated at their first corrupted record,
so anything measured from them is worth re-checking.

capture/recordings/ is gitignored, since captures are data rather than source.
The host tests now read both that directory and the older one in the receiver
checkout, so no capture has to be moved while it is being written.

dump_pcap.py reads the same console and still needs the same treatment; that is
recorded in TODO.md.
2026-09-14 13:38:38 +02:00
Ashin Walpola 277c6b20f4 Record the on-air verification and how to run the bench beacon
Both firmware fixes are now confirmed over the air, with the sniffer board
capturing and asn1tools judging the result.

GN lifetime: our station transmits 0x05 (1 s), the value both bench stations
use, where the August captures show 0x83 (3200 s) for the same frames. 397 CAMs
from station 999999 decode and re-encode byte-identically, so the whole
transmit chain is right on the wire, not only in the host tests.

yawRateConfidence: the bench beacon, flashed to a spare board, sends CAMs that
decode and re-encode byte-identically as well (72 of 72 from station
0x0BADC0DE). NOTES.md now says how to flash that beacon and how to check what
it sends, including that it shares the phone pinger's MAC and the two are told
apart by station ID.

The new firmware also runs on the OBU with the phone attached: CAM, DENM and
SPATEM from the bench stations all keep decoding in the app now that messages
are cut to the length their header declares.

Signed reception stays open. The CiT One transmits unsigned and nothing else
here signs, so it needs real roadside traffic, the CiT One switched to signed
mode, or a replay firmware on a spare board.

Two bench facts worth not rediscovering are recorded too: opening COM3's
console resets the OBU and drops the phone's USB link, and every capture taken
before today is truncated at its first corrupted record, because the receiver's
console inserts a CR before every 0x0a byte of the binary pcap stream.
live_capture.py now undoes that, a change that lives in the receiver repo and
is not part of this commit.
2026-09-14 13:33:47 +02:00
Ashin Walpola 1baae2c5f6 Encode yawRateConfidence in 4 bits in the firmware CAM encoders
YawRateConfidence has nine enumerands, degSec-000-01(0) to
unavailable(8) (cdd_1_3_1_1.asn), so UPER needs 4 bits and
"unavailable" is 8. Both firmware copies of cam.c wrote 3 bits with
value 7, which is also the wrong symbol (outOfRange), and every field
after it shifted by one bit. The app's CamUperCodec fixed the same line
on 2026-08-20; these two copies were missed.

obu-cam-transmistter compiles its cam.c, so a board running that bench
beacon sent CAMs no standards-compliant station could decode.
obu-firmware's copy is reference only - it is not in SRCS, since the
phone encodes the CAM - and is kept in step because the app's encoder
was ported from it. The production OBU runs obu-firmware and was never
affected.

Every other field width was compared against CamUperCodec.kt and
matches. Checked with asn1tools against asn1/cam_1_4_1.asn and
cdd_1_3_1_1.asn: the CAM both fixed copies emit decodes with every
expected value and re-encodes byte-identically, while the version before
this change fails on yawRateConfidence. obu-cam-transmistter builds on
IDF 5.5.4.
2026-09-11 20:19:40 +02:00
Ashin Walpola 8871708a98 Send the GeoNetworking lifetime as 1 s, not 3200 s
geonet_wrap_shb wrote lifetime 0x83, commented as about 60 s. The field
holds the multiplier in its upper six bits and the base in the lower two
(50 ms, 1 s, 10 s, 100 s), so 0x83 is 32 x 100 s = 3200 s. That is over
the 600 s itsGnMaxPacketLifetime a sender may use at all; vanetza
refuses to send such a packet. The byte arrived with the Phase 03 commit
as a placeholder and was never checked against the encoding.

For a single-hop CAM this is non-compliance rather than a functional
fault: nothing stores or forwards an SHB packet, so no receiver acts on
the value, and no dropped CAM was ever traced to it.

0x05 (1 x 1 s) is what every other station in
its-g5-receiver-firmware/recordings sends its CAMs with; the recorded
GeoBroadcast DENMs use 0x79 (30 s). Changed in obu-cam-transmistter's
copy as well. Both firmwares build (IDF 6.1 and 5.5.4), and the
disassembled geonet_wrap_shb of each stores 0x05. Not yet seen on air:
the production OBU still runs the 2026-09-10 build, and the on-air
check is listed in TODO.md.
2026-09-11 20:19:39 +02:00
Ashin Walpola 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.
2026-08-17 18:42:48 +02:00
Ashin Walpola 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.
2026-08-11 14:50:35 +02:00