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.
This commit is contained in:
Ashin Walpola
2026-08-17 18:42:48 +02:00
parent f1770e11dd
commit 0ccb867228
19 changed files with 989 additions and 188 deletions
+10 -3
View File
@@ -7,9 +7,9 @@ int geonet_wrap_shb(const uint8_t *its_payload, int its_len,
uint16_t btp_dest_port,
uint8_t *out, size_t out_len)
{
// GN Basic Header (4) + GN Common Header (8) + SHB source LPV (24)
// GN Basic Header (4) + GN Common Header (8) + SHB extended header (28)
// + BTP-B header (4) + ITS payload
int total = 4 + 8 + 24 + 4 + its_len;
int total = 4 + 8 + 28 + 4 + its_len;
if ((size_t)total > out_len) {
return -1;
}
@@ -43,7 +43,7 @@ int geonet_wrap_shb(const uint8_t *its_payload, int its_len,
*p++ = 1; // max hop limit = 1, matches basic header RHL (SHB single-hop)
*p++ = 0x00; // reserved
// ---- SHB extended header: Source Long Position Vector (24 bytes) ----
// ---- SHB extended header: Source Position Vector (24) + Reserved (4) = 28 bytes ----
// (clause 9.5.2). GN_ADDR (8 bytes) is itself structured, not a raw
// pseudonym (clause 9.5.1): bit0 M-flag(0=auto-derived), bits1-5 ITS-S
// type (5-bit), bits6-15 reserved(=0), then octets2-7 = MID, which is
@@ -71,6 +71,13 @@ int geonet_wrap_shb(const uint8_t *its_payload, int its_len,
*p++ = 0x00; *p++ = 0x00;
// Heading (16 bits, 0.1 degree units): 0 = due north / unavailable
*p++ = 0x00; *p++ = 0x00;
// Reserved (4 bytes) - clause 9.8.4: the SHB extended header is the 24-byte Source Position
// Vector FOLLOWED BY a 4-byte reserved field (media-dependent data), 28 bytes in total. These
// four bytes were missing, which is why a standards-compliant receiver read our CAM payload's
// first two bytes (0x02 0x02 = protocolVersion/messageID) as the BTP destination port and saw
// 514 instead of 2001 - confirmed against live air capture, 2026-08-13. Our own gn_unwrap.c
// had the identical off-by-four, so ESP32<->ESP32 worked and nothing else did.
*p++ = 0x00; *p++ = 0x00; *p++ = 0x00; *p++ = 0x00;
// ---- BTP-B header (4 bytes) ----
*p++ = (uint8_t)(btp_dest_port >> 8);