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);
+72 -30
View File
@@ -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;
}
+54 -25
View File
@@ -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
+19 -8
View File
@@ -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);
}
}
}
+33 -9
View File
@@ -112,23 +112,47 @@ static bool send_frame(uint8_t type, const uint8_t *payload, int len)
return wrote == (int)(sizeof(sync) + sizeof(head) + len + sizeof(crc_bytes));
}
bool serial_link_send_cam_rx(int8_t rssi, const uint8_t *cam_uper, int cam_len)
bool serial_link_send_v2x_rx(uint16_t btp_dest_port, int8_t rssi,
bool has_geo_area,
int32_t geo_area_lat_tenmicrodeg,
int32_t geo_area_lon_tenmicrodeg,
uint16_t geo_area_distance_a_m,
const uint8_t *uper, int uper_len)
{
if (cam_len < 0 || cam_len > SERIAL_LINK_MAX_PAYLOAD - 1) {
if (uper_len < 0 || uper_len > SERIAL_LINK_MAX_PAYLOAD - SERIAL_V2X_RX_PREFIX_LEN) {
// Counted, not just logged: this log line goes to the flashing port, which nobody is
// watching during a phone bench session - so the symptom would be "that station just
// never shows up in the app" with no visible cause.
bump(&s_oversize_drops);
ESP_LOGW(TAG, "send_cam_rx: cam_len too large (%d), total oversize drops %u",
cam_len, s_oversize_drops);
ESP_LOGW(TAG, "send_v2x_rx: port %u payload too large (%d), total oversize drops %u",
btp_dest_port, uper_len, s_oversize_drops);
return false;
}
// static, not stack (515 bytes at MAX_PAYLOAD 512); only rx_forward_task calls this, and
// static, not stack (526 bytes at MAX_PAYLOAD 512); only rx_forward_task calls this, and
// send_frame's mutex covers the handoff onto the wire.
static uint8_t s_cam_rx_payload[SERIAL_LINK_MAX_PAYLOAD];
s_cam_rx_payload[0] = (uint8_t)rssi;
memcpy(s_cam_rx_payload + 1, cam_uper, (size_t)cam_len);
return send_frame(SERIAL_MSG_CAM_RX, s_cam_rx_payload, 1 + cam_len);
static uint8_t s_v2x_payload[SERIAL_LINK_MAX_PAYLOAD];
// Little-endian prefix, layout documented in serial_link.h - keep in lockstep with the app's
// SerialFrame.kt.
s_v2x_payload[0] = (uint8_t)(btp_dest_port & 0xFF);
s_v2x_payload[1] = (uint8_t)((btp_dest_port >> 8) & 0xFF);
s_v2x_payload[2] = (uint8_t)rssi;
s_v2x_payload[3] = has_geo_area ? 0x01 : 0x00;
uint32_t lat = (uint32_t)geo_area_lat_tenmicrodeg;
uint32_t lon = (uint32_t)geo_area_lon_tenmicrodeg;
s_v2x_payload[4] = (uint8_t)(lat & 0xFF);
s_v2x_payload[5] = (uint8_t)((lat >> 8) & 0xFF);
s_v2x_payload[6] = (uint8_t)((lat >> 16) & 0xFF);
s_v2x_payload[7] = (uint8_t)((lat >> 24) & 0xFF);
s_v2x_payload[8] = (uint8_t)(lon & 0xFF);
s_v2x_payload[9] = (uint8_t)((lon >> 8) & 0xFF);
s_v2x_payload[10] = (uint8_t)((lon >> 16) & 0xFF);
s_v2x_payload[11] = (uint8_t)((lon >> 24) & 0xFF);
s_v2x_payload[12] = (uint8_t)(geo_area_distance_a_m & 0xFF);
s_v2x_payload[13] = (uint8_t)((geo_area_distance_a_m >> 8) & 0xFF);
if (uper_len > 0) memcpy(s_v2x_payload + SERIAL_V2X_RX_PREFIX_LEN, uper, (size_t)uper_len);
return send_frame(SERIAL_MSG_V2X_RX, s_v2x_payload, SERIAL_V2X_RX_PREFIX_LEN + uper_len);
}
bool serial_link_send_status(uint8_t status)
+34 -9
View File
@@ -28,11 +28,25 @@
// built by the phone (position/speed/heading/yaw rate baked in). On receipt the ESP32
// immediately GeoNetworking-wraps and transmits it - this IS the transmit clock now, there
// is no independent on-chip timer. See main.c's rx-driven tx path.
// SERIAL_MSG_CAM_RX (0x02), ESP32 -> phone: payload is [rssi:1 signed][CAM UPER bytes...] - a
// CAM received over the air, already stripped of its 802.11/LLC-SNAP/GeoNetworking/BTP-B
// framing by gn_unwrap.c. The phone never sees raw 802.11 frames. No station id is carried
// separately - CAM's own ItsPduHeader.stationID (the first field inside the UPER bytes) is
// already the meaningful identifier; see gn_unwrap.h for why a second one isn't added here.
// SERIAL_MSG_CAM_RX (0x02), ESP32 -> phone: SUPERSEDED by SERIAL_MSG_V2X_RX, no longer sent.
// The constant is kept so the numbering is not silently reused by a future message type.
// SERIAL_MSG_V2X_RX (0x04), ESP32 -> phone: any ITS message received over the air, already
// stripped of its 802.11/LLC-SNAP/GeoNetworking/BTP-B framing by gn_unwrap.c - the phone
// never sees raw 802.11 frames. Payload is a fixed 14-byte prefix followed by the UPER bytes:
//
// [0..1] btp_dest_port uint16 LE 2001 = CAM, 2002 = DENM (ETSI TS 103 248)
// [2] rssi int8 dBm, from the promiscuous RX metadata
// [3] flags uint8 bit0: geo area fields below are valid
// [4..7] geo_area_lat int32 LE 1/10 microdegree, GeoBroadcast destination area
// [8..11] geo_area_lon int32 LE 1/10 microdegree
// [12..13] geo_area_dist uint16 LE Distance A, metres (relevance radius for a circle)
// [14..] UPER message bytes
//
// All prefix fields are LITTLE-endian, matching this framing's own length field - note the
// GeoNetworking wire format they came from is big-endian, so gn_unwrap.c converts.
// Generic on purpose: adding MAPEM/SPATEM later needs a decoder on the phone and one port in
// gn_unwrap.c, but no change to this protocol. No station id is carried separately - each
// message's own ItsPduHeader.stationID is the meaningful identifier.
// SERIAL_MSG_STATUS (0x03), ESP32 -> phone: heartbeat + counters, sent at 1 Hz so the phone can
// distinguish "link idle" from "link dead" independent of CAM traffic (the app's watchdog in
// UsbSerialTransport.kt declares the link dead after 3 missed beats). Payload is 7 bytes:
@@ -44,6 +58,10 @@
#define SERIAL_MSG_CAM_TX 0x01
#define SERIAL_MSG_CAM_RX 0x02
#define SERIAL_MSG_STATUS 0x03
#define SERIAL_MSG_V2X_RX 0x04
// Size of the V2X_RX prefix documented above. Must match the app's SerialFrame.kt.
#define SERIAL_V2X_RX_PREFIX_LEN 14
// USB Serial/JTAG has no baud rate or GPIO pins to configure - it's a fixed on-chip USB device
// controller wired directly to the native USB-C port's D+/D- lines in silicon. RX/TX buffer
@@ -76,10 +94,17 @@
typedef void (*serial_link_cam_tx_cb_t)(const uint8_t *cam_uper, int cam_len);
void serial_link_init(serial_link_cam_tx_cb_t on_cam_tx);
// Sends a SERIAL_MSG_CAM_RX frame to the phone: rssi + the CAM UPER bytes gn_unwrap.c extracted
// from an over-the-air frame. Returns true if the frame was written to the UART (not an
// end-to-end ack - the phone may still drop it, e.g. serial buffer overrun).
bool serial_link_send_cam_rx(int8_t rssi, const uint8_t *cam_uper, int cam_len);
// Sends a SERIAL_MSG_V2X_RX frame: the metadata prefix plus the UPER bytes gn_unwrap.c extracted
// from an over-the-air frame. Pass has_geo_area=false and zeroes for the area fields when the
// source frame carried no destination area (i.e. it was single-hop broadcast, not GeoBroadcast).
// Returns true if the frame was written to the USB endpoint - not an end-to-end ack, the phone
// may still drop it.
bool serial_link_send_v2x_rx(uint16_t btp_dest_port, int8_t rssi,
bool has_geo_area,
int32_t geo_area_lat_tenmicrodeg,
int32_t geo_area_lon_tenmicrodeg,
uint16_t geo_area_distance_a_m,
const uint8_t *uper, int uper_len);
// Sends one SERIAL_MSG_STATUS heartbeat frame immediately (status byte + the current counters).
// Normally unnecessary to call by hand - serial_link_init() starts a task that does this at 1 Hz.