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:
@@ -5,35 +5,64 @@
|
||||
#include <stdbool.h>
|
||||
|
||||
// Inverse of geonet_wrap_shb() + dot11p_build_frame(): takes a raw 802.11 frame as delivered by
|
||||
// the WiFi driver's promiscuous RX callback and strips 802.11 header -> LLC/SNAP -> GeoNetworking
|
||||
// Basic/Common/extended header -> BTP-B header, leaving just the ITS payload (CAM UPER bytes)
|
||||
// and the sender's station id (GN_ADDR MID).
|
||||
// the WiFi driver's promiscuous RX callback and strips 802.11 header -> LLC/SNAP ->
|
||||
// GeoNetworking Basic/Common/extended header -> BTP-B header, leaving the ITS payload (a UPER
|
||||
// message) plus the metadata the phone needs to know what it received.
|
||||
//
|
||||
// Deliberately narrow, matching what this project actually transmits: only handles the
|
||||
// Single-Hop-Broadcast (TSB, HeaderType=5/Subtype=0) extended header shape, same as
|
||||
// geonet_wrap_shb() builds - the same "best-tested decode path" rationale documented there.
|
||||
// A real receiver would also need GeoBroadcast (HeaderType=4, used by DENM dissemination in
|
||||
// real deployments) and possibly Beacon/GeoUnicast - out of scope for now since nothing this
|
||||
// project talks to sends those. Extend header_type handling here if that changes.
|
||||
// ---- Supported GeoNetworking header types --------------------------------------------------
|
||||
// Two shapes, chosen by the Common Header's HeaderType, with DIFFERENT extended-header lengths:
|
||||
//
|
||||
// Only accepts BTP-B destination port 2001 (CAM, per ETSI TS 103 248) - other ports (e.g. 2002
|
||||
// DENM) are silently rejected since the phone-side decoder only understands CAM right now.
|
||||
// TSB/SINGLE_HOP (HT=5, HST=0) - 28 bytes: Source Position Vector (24) + Reserved (4).
|
||||
// What CAM uses, and what geonet_wrap_shb() builds.
|
||||
// GEOBROADCAST (HT=4) - 44 bytes: SeqNum (2) + Reserved (2) + SO PV (24) +
|
||||
// GeoArea lat (4) + lon (4) + DistanceA (2) + DistanceB (2) + Angle (2) + Reserved (2).
|
||||
// What DENM uses in practice - real RSUs and OBUs disseminate DENM by GeoBroadcast so it
|
||||
// can be forwarded across an area, not by single-hop broadcast.
|
||||
//
|
||||
// Returns true and fills *out_cam / *out_cam_len (pointing INTO the input frame buffer, not a
|
||||
// copy - valid only as long as `frame` is) if this was a well-formed, CAM-carrying SHB frame
|
||||
// this project can decode. Returns false otherwise (wrong ethertype, wrong header type, wrong
|
||||
// BTP port, truncated, or FCS/promiscuous-capture garbage - all common and expected on an
|
||||
// open-air capture, not logged as errors by the caller).
|
||||
// Both lengths are measured facts, not spec-table guesses: verified against live air capture on
|
||||
// 2026-08-17 (its-g5-receiver-firmware/recordings/capture_20260817_171055.pcap) by locating the
|
||||
// BTP port and ItsPduHeader and checking they agree. An earlier version of this file used 24 for
|
||||
// the SHB case, four bytes short, which read the BTP port out of the Reserved field and silently
|
||||
// dropped EVERY real CAM. Do not "simplify" these constants without re-measuring.
|
||||
//
|
||||
// No station id is extracted here on purpose: CAM's own ItsPduHeader.stationID (the first real
|
||||
// field inside the UPER payload this function hands back, per cam.c) is already the meaningful
|
||||
// application-level identifier - the Kotlin-side decoder reads it from there. The GN_ADDR MID
|
||||
// this frame also carries is a separate, link-layer-only pseudonym; extracting and forwarding
|
||||
// it too would just be a second, easily-confused "station id" for no benefit here.
|
||||
// Beacon, GeoUnicast, GeoAnycast and multi-hop TSB are still rejected - nothing this project
|
||||
// talks to sends them, and each has its own extended-header length that would need measuring.
|
||||
//
|
||||
// RSSI is NOT extracted here either - it comes from the promiscuous callback's own packet
|
||||
// metadata (wifi_pkt_rx_ctrl_t.rssi in main.c), not from anything inside the frame bytes.
|
||||
bool gn_unwrap_cam(const uint8_t *frame, int frame_len,
|
||||
const uint8_t **out_cam, int *out_cam_len);
|
||||
// ---- Accepted BTP-B ports (ETSI TS 103 248) ------------------------------------------------
|
||||
// 2001 (CAM) and 2002 (DENM). MAPEM (2003), SPATEM (2004) and the rest are deliberately not
|
||||
// accepted yet: the phone has no decoder for them, so forwarding would just burn serial
|
||||
// bandwidth. Adding one is a one-line change here plus a decoder on the phone - the serial
|
||||
// protocol itself is already generic (see SERIAL_MSG_V2X_RX in serial_link.h).
|
||||
//
|
||||
// ---- What is NOT handled -------------------------------------------------------------------
|
||||
// Secured packets (GN Basic Header NextHeader=2, i.e. ETSI TS 103 097 signed messages). The
|
||||
// units on this bench run with ItsGnSecurity=0 so everything observed is unsecured; a secured
|
||||
// packet is rejected rather than mis-parsed.
|
||||
//
|
||||
// No FCS/CRC check: the WiFi driver has already validated and stripped it.
|
||||
typedef struct {
|
||||
// BTP-B destination port, identifying the service: 2001 = CAM, 2002 = DENM.
|
||||
uint16_t btp_dest_port;
|
||||
|
||||
// ITS payload (UPER message bytes). Points INTO the caller's `frame` buffer - NOT a copy, so
|
||||
// it is only valid while `frame` is.
|
||||
const uint8_t *payload;
|
||||
int payload_len;
|
||||
|
||||
// GeoBroadcast destination area, when this frame carried one (GEOBROADCAST only; false for
|
||||
// TSB/SHB). This is the hazard's relevance area - for a DENM it says "this warning applies
|
||||
// within DistanceA metres of this point", which is materially more useful on a map than the
|
||||
// originator's own position.
|
||||
bool has_geo_area;
|
||||
int32_t geo_area_lat_tenmicrodeg;
|
||||
int32_t geo_area_lon_tenmicrodeg;
|
||||
uint16_t geo_area_distance_a_m;
|
||||
} gn_rx_t;
|
||||
|
||||
// Returns true and fills *out if this was a well-formed, supported ITS frame. Returns false
|
||||
// otherwise (wrong ethertype, secured, unsupported header type, unaccepted BTP port, truncated,
|
||||
// or promiscuous-capture garbage) - all common and expected on an open-air capture, so the caller
|
||||
// should treat false as "not for us", not as an error worth logging per frame.
|
||||
bool gn_unwrap_its(const uint8_t *frame, int frame_len, gn_rx_t *out);
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user