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:
@@ -154,8 +154,15 @@ static void tx_radio_task(void *arg)
|
||||
// same pattern as the TX side and as the reference sniffer firmware (cmd_sniffer.c's
|
||||
// queue_packet), this just copies the frame and queues it; gn_unwrap_cam() and the serial write
|
||||
// both happen in rx_forward_task instead.
|
||||
// Capture buffer per queued frame. 800 bytes because real traffic is much larger than our own
|
||||
// TX: a CiT One CAM measures 286-355 bytes on air and its GeoBroadcast DENM measures 528
|
||||
// (measured 2026-08-17). The previous 400 silently truncated every DENM mid-payload, which no
|
||||
// amount of correct unwrapping downstream could have recovered from. Raise this before adding
|
||||
// MAPEM, which is larger again.
|
||||
#define RX_FRAME_MAX_LEN 800
|
||||
|
||||
typedef struct {
|
||||
uint8_t data[400]; // generous vs. our own ~300-byte TX frames; longer frames are truncated
|
||||
uint8_t data[RX_FRAME_MAX_LEN];
|
||||
int len;
|
||||
int8_t rssi;
|
||||
} rx_item_t;
|
||||
@@ -200,13 +207,17 @@ static void rx_forward_task(void *arg)
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint8_t *cam = NULL;
|
||||
int cam_len = 0;
|
||||
// Most promiscuously-captured frames are NOT CAM (management/control frames, other
|
||||
// ITS-G5 traffic types, our own loopback if the driver echoes it) - gn_unwrap_cam
|
||||
// returning false here is the common case, not an error.
|
||||
if (gn_unwrap_cam(item.data, item.len, &cam, &cam_len)) {
|
||||
serial_link_send_cam_rx(item.rssi, cam, cam_len);
|
||||
// Most promiscuously-captured frames are NOT ITS traffic we handle (management/control
|
||||
// frames, other message types, our own loopback if the driver echoes it) - gn_unwrap_its
|
||||
// returning false here is the common case, not an error, so it isn't logged per frame.
|
||||
gn_rx_t rx;
|
||||
if (gn_unwrap_its(item.data, item.len, &rx)) {
|
||||
serial_link_send_v2x_rx(rx.btp_dest_port, item.rssi,
|
||||
rx.has_geo_area,
|
||||
rx.geo_area_lat_tenmicrodeg,
|
||||
rx.geo_area_lon_tenmicrodeg,
|
||||
rx.geo_area_distance_a_m,
|
||||
rx.payload, rx.payload_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user