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:
@@ -12,22 +12,47 @@
|
||||
|
||||
#define GN_BASIC_HEADER_LEN (4)
|
||||
#define GN_COMMON_HEADER_LEN (8)
|
||||
#define GN_SHB_EXT_HEADER_LEN (24) // Source Long Position Vector, geonet.c's SHB shape
|
||||
#define BTP_B_HEADER_LEN (4)
|
||||
|
||||
// Extended-header lengths per GeoNetworking header type - see gn_unwrap.h for why these exact
|
||||
// numbers, and why they must not be assumed equal.
|
||||
#define GN_SHB_EXT_HEADER_LEN (28) // SO PV (24) + Reserved (4)
|
||||
#define GN_GBC_EXT_HEADER_LEN (44) // SN(2) + Rsvd(2) + SO PV(24) + area(12) + Rsvd(4)
|
||||
|
||||
// Offsets of the destination-area fields within the GBC extended header.
|
||||
#define GBC_AREA_LAT_OFFSET (28)
|
||||
#define GBC_AREA_LON_OFFSET (32)
|
||||
#define GBC_AREA_DIST_A_OFFSET (36)
|
||||
|
||||
#define GN_HEADER_TYPE_GBC (4) // GeoBroadcast
|
||||
#define GN_HEADER_TYPE_TSB (5) // Topologically-Scoped Broadcast
|
||||
#define GN_HEADER_SUBTYPE_SINGLE_HOP (0)
|
||||
|
||||
#define GN_NEXT_HEADER_COMMON (1) // unsecured; 2 would be a secured packet
|
||||
#define GN_COMMON_NEXT_HEADER_BTP_B (2)
|
||||
|
||||
#define BTP_DEST_PORT_CAM (2001) // ETSI TS 103 248
|
||||
#define BTP_DEST_PORT_DENM (2002)
|
||||
|
||||
static const uint8_t s_llc_snap_prefix[6] = {0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00};
|
||||
|
||||
bool gn_unwrap_cam(const uint8_t *frame, int frame_len,
|
||||
const uint8_t **out_cam, int *out_cam_len)
|
||||
static int32_t be32(const uint8_t *p)
|
||||
{
|
||||
if (!frame || frame_len < IEEE80211_HEADER_LEN) {
|
||||
return (int32_t)(((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
|
||||
((uint32_t)p[2] << 8) | (uint32_t)p[3]);
|
||||
}
|
||||
|
||||
static uint16_t be16(const uint8_t *p)
|
||||
{
|
||||
return (uint16_t)(((uint16_t)p[0] << 8) | (uint16_t)p[1]);
|
||||
}
|
||||
|
||||
bool gn_unwrap_its(const uint8_t *frame, int frame_len, gn_rx_t *out)
|
||||
{
|
||||
if (!frame || !out || frame_len < IEEE80211_HEADER_LEN) {
|
||||
return false;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
uint8_t fc0 = frame[0];
|
||||
uint8_t fc1 = frame[1];
|
||||
@@ -36,8 +61,8 @@ bool gn_unwrap_cam(const uint8_t *frame, int frame_len,
|
||||
bool to_ds = fc1 & 0x01;
|
||||
bool from_ds = fc1 & 0x02;
|
||||
|
||||
// Only plain broadcast Data frames, no WDS - matches what dot11p_build_frame ever produces
|
||||
// (and what real ITS-G5 hardware sends).
|
||||
// Only plain broadcast Data frames, no WDS. Both QoS Data (what real ITS-G5 hardware sends,
|
||||
// 26-byte header) and non-QoS Data (24-byte, what our own TX currently builds) are accepted.
|
||||
if (type != IEEE80211_FC_TYPE_DATA || (to_ds && from_ds)) {
|
||||
return false;
|
||||
}
|
||||
@@ -53,17 +78,22 @@ bool gn_unwrap_cam(const uint8_t *frame, int frame_len,
|
||||
if (memcmp(frame + offset, s_llc_snap_prefix, sizeof(s_llc_snap_prefix)) != 0) {
|
||||
return false;
|
||||
}
|
||||
uint16_t ethertype = ((uint16_t)frame[offset + 6] << 8) | frame[offset + 7];
|
||||
if (ethertype != GN_ETHERTYPE) {
|
||||
if (be16(frame + offset + 6) != GN_ETHERTYPE) {
|
||||
return false;
|
||||
}
|
||||
offset += LLC_SNAP_HEADER_LEN;
|
||||
|
||||
// ---- GN Basic Header (4 bytes) ---- nothing here we need to validate for our purposes;
|
||||
// just skip it. (version/NextHeader in byte0, lifetime in byte2, RHL in byte3.)
|
||||
// ---- GN Basic Header (4 bytes) ----
|
||||
if (frame_len < offset + GN_BASIC_HEADER_LEN) {
|
||||
return false;
|
||||
}
|
||||
// NextHeader distinguishes an unsecured packet (1 = Common Header follows) from a secured one
|
||||
// (2 = a TS 103 097 SecuredMessage follows, with the Common Header buried inside it at a
|
||||
// variable offset). Checking this rather than blindly skipping means a secured packet is
|
||||
// rejected cleanly instead of having its security envelope misread as a Common Header.
|
||||
if ((frame[offset] & 0x0F) != GN_NEXT_HEADER_COMMON) {
|
||||
return false;
|
||||
}
|
||||
offset += GN_BASIC_HEADER_LEN;
|
||||
|
||||
// ---- GN Common Header (8 bytes) ----
|
||||
@@ -73,42 +103,54 @@ bool gn_unwrap_cam(const uint8_t *frame, int frame_len,
|
||||
uint8_t next_header = (frame[offset + 0] >> 4) & 0x0F;
|
||||
uint8_t header_type = (frame[offset + 1] >> 4) & 0x0F;
|
||||
uint8_t header_subtype = frame[offset + 1] & 0x0F;
|
||||
if (next_header != 2 /* BTP-B */) {
|
||||
return false;
|
||||
}
|
||||
if (header_type != GN_HEADER_TYPE_TSB || header_subtype != GN_HEADER_SUBTYPE_SINGLE_HOP) {
|
||||
// Not a single-hop-broadcast frame - e.g. GeoBroadcast (DENM-style dissemination) or
|
||||
// something this project doesn't transmit/expect. Not an error, just not for us yet -
|
||||
// see gn_unwrap.h's note on scope.
|
||||
if (next_header != GN_COMMON_NEXT_HEADER_BTP_B) {
|
||||
return false;
|
||||
}
|
||||
offset += GN_COMMON_HEADER_LEN;
|
||||
|
||||
// ---- SHB extended header (24 bytes) ---- skip straight past it, we don't need the
|
||||
// sender's claimed position/speed/heading here (the CAM payload has its own, more precise
|
||||
// versions of those same fields).
|
||||
if (frame_len < offset + GN_SHB_EXT_HEADER_LEN) {
|
||||
// ---- Extended header: length depends on the header type ----
|
||||
int ext_len;
|
||||
bool is_gbc = false;
|
||||
if (header_type == GN_HEADER_TYPE_TSB && header_subtype == GN_HEADER_SUBTYPE_SINGLE_HOP) {
|
||||
ext_len = GN_SHB_EXT_HEADER_LEN;
|
||||
} else if (header_type == GN_HEADER_TYPE_GBC) {
|
||||
// Subtype selects the area shape (0 circle, 1 rectangle, 2 ellipse). All three carry the
|
||||
// same field layout - DistanceB and Angle are simply unused for a circle - so the length
|
||||
// is the same and we don't need to branch on it.
|
||||
ext_len = GN_GBC_EXT_HEADER_LEN;
|
||||
is_gbc = true;
|
||||
} else {
|
||||
return false; // Beacon / GeoUnicast / GeoAnycast / multi-hop TSB - see header comment
|
||||
}
|
||||
if (frame_len < offset + ext_len) {
|
||||
return false;
|
||||
}
|
||||
offset += GN_SHB_EXT_HEADER_LEN;
|
||||
if (is_gbc) {
|
||||
out->has_geo_area = true;
|
||||
out->geo_area_lat_tenmicrodeg = be32(frame + offset + GBC_AREA_LAT_OFFSET);
|
||||
out->geo_area_lon_tenmicrodeg = be32(frame + offset + GBC_AREA_LON_OFFSET);
|
||||
out->geo_area_distance_a_m = be16(frame + offset + GBC_AREA_DIST_A_OFFSET);
|
||||
}
|
||||
offset += ext_len;
|
||||
|
||||
// ---- BTP-B header (4 bytes) ----
|
||||
if (frame_len < offset + BTP_B_HEADER_LEN) {
|
||||
return false;
|
||||
}
|
||||
uint16_t dest_port = ((uint16_t)frame[offset + 0] << 8) | frame[offset + 1];
|
||||
if (dest_port != BTP_DEST_PORT_CAM) {
|
||||
return false; // e.g. DENM (2002) - not decoded by this project yet
|
||||
uint16_t dest_port = be16(frame + offset);
|
||||
if (dest_port != BTP_DEST_PORT_CAM && dest_port != BTP_DEST_PORT_DENM) {
|
||||
return false;
|
||||
}
|
||||
offset += BTP_B_HEADER_LEN;
|
||||
|
||||
// ---- Whatever's left is the CAM UPER payload ----
|
||||
int cam_len = frame_len - offset;
|
||||
if (cam_len <= 0) {
|
||||
// ---- Whatever's left is the ITS UPER payload ----
|
||||
int payload_len = frame_len - offset;
|
||||
if (payload_len <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*out_cam = frame + offset;
|
||||
*out_cam_len = cam_len;
|
||||
out->btp_dest_port = dest_port;
|
||||
out->payload = frame + offset;
|
||||
out->payload_len = payload_len;
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user