From 3eeccfb268dd0f7cb870f70883721a0d6743c9b0 Mon Sep 17 00:00:00 2001 From: Ashin Walpola Date: Thu, 10 Sep 2026 14:47:30 +0200 Subject: [PATCH] Send CAMs under the phone's position vector, not bench placeholders Every field of the GeoNetworking Source Position Vector this firmware sent was a compile-time constant: the bench coordinates, speed 0, heading 0, TST 0, station type passengerCar and one fixed MAC. The CAM inside described a moving cyclist while the GN header around it described a car parked at the bench. SERIAL_MSG_CAM_TX_PV (0x05) puts a 24-byte prefix ahead of the CAM UPER: MAC, station type, PAI, TST, latitude, longitude, speed and heading, all values the phone already has when it builds the CAM and none of which this chip can know. geonet_wrap_shb now takes them as a gn_lpv_t, and tx_radio_task hands the same MAC to dot11p_build_frame, so the 802.11 source address and the GN_ADDR MID stay one address across a pseudonym change. Speed is clamped rather than masked, since an overflowing 15-bit value flips its sign bit and reads as travelling backwards. This reverses the Phase 03 decision that the firmware owns the pseudonym. A pseudonym only protects anyone if the MAC, the GN_ADDR and the CAM's stationID change together, and the phone owns the stationID. The heartbeat gains a capability byte (payload[7], bit0 = CAM_TX_PV), appended so an app reading the first 7 bytes is unaffected. The app sends 0x05 only once it sees that bit, so app and firmware can be updated in either order. CAM_TX (0x01) is still handled and falls back to the bench values, with the station type corrected to cyclist to match the CAM. Verified on air from the COM10 test board, decoded independently by the CiT One's gnHeader: 24 of 24 CAM_TX_PV frames matched the sent position vector field by field, and so did the CAM station ID. The legacy path delivered 23 of 24 frames with no field mismatches. Flashed on the COM3 OBU and its boot log is clean. Also corrects the SERIAL_LINK_MAX_PAYLOAD comment, which still named the 400-byte receive capture buffer as the ceiling on the RX path. That buffer is 800 bytes now, so the serial link is the ceiling, and larger payloads are dropped and counted there. --- obu-firmware/main/geonet.c | 55 +++++++++++-------- obu-firmware/main/geonet.h | 72 +++++++++++++++---------- obu-firmware/main/main.c | 96 ++++++++++++++++++++++++++------- obu-firmware/main/serial_link.c | 30 ++++++++--- obu-firmware/main/serial_link.h | 65 +++++++++++++++++----- 5 files changed, 228 insertions(+), 90 deletions(-) diff --git a/obu-firmware/main/geonet.c b/obu-firmware/main/geonet.c index 60ea6e0..e80ec36 100644 --- a/obu-firmware/main/geonet.c +++ b/obu-firmware/main/geonet.c @@ -1,9 +1,23 @@ #include "geonet.h" #include +// GeoNetworking is big-endian throughout, unlike this project's serial framing. +static void put_be16(uint8_t **p, uint16_t v) +{ + *(*p)++ = (uint8_t)(v >> 8); + *(*p)++ = (uint8_t)(v); +} + +static void put_be32(uint8_t **p, uint32_t v) +{ + *(*p)++ = (uint8_t)(v >> 24); + *(*p)++ = (uint8_t)(v >> 16); + *(*p)++ = (uint8_t)(v >> 8); + *(*p)++ = (uint8_t)(v); +} + int geonet_wrap_shb(const uint8_t *its_payload, int its_len, - const uint8_t mac[6], uint8_t station_type, - int32_t latitude_tenmicrodeg, int32_t longitude_tenmicrodeg, + const gn_lpv_t *lpv, uint16_t btp_dest_port, uint8_t *out, size_t out_len) { @@ -50,27 +64,24 @@ int geonet_wrap_shb(const uint8_t *its_payload, int its_len, // defined to BE the link-layer (802.11) address - so this must match // the source address dot11p_build_frame uses, not just "look similar." uint8_t gn_addr[8]; - gn_addr[0] = (uint8_t)((0 << 7) | ((station_type & 0x1F) << 2)); // M=0, ST=station_type, top 2 reserved bits=0 + gn_addr[0] = (uint8_t)((0 << 7) | ((lpv->station_type & 0x1F) << 2)); // M=0, ST=station_type, top 2 reserved bits=0 gn_addr[1] = 0x00; // remaining 8 reserved bits - memcpy(&gn_addr[2], mac, 6); // MID = link-layer address + memcpy(&gn_addr[2], lpv->mac, 6); // MID = link-layer address memcpy(p, gn_addr, 8); p += 8; - // Timestamp (4 bytes, ms since 2004-01-01 mod 2^32) - placeholder 0, - // same caveat as detectionTime in denm.c. - memset(p, 0, 4); p += 4; - // Latitude/Longitude (4+4 bytes, signed, big-endian, 1/10 microdegree) - - // fixed-width binary fields, not UPER bit-packed. - uint32_t lat_u = (uint32_t)latitude_tenmicrodeg; - *p++ = (uint8_t)(lat_u >> 24); *p++ = (uint8_t)(lat_u >> 16); - *p++ = (uint8_t)(lat_u >> 8); *p++ = (uint8_t)(lat_u); - uint32_t lon_u = (uint32_t)longitude_tenmicrodeg; - *p++ = (uint8_t)(lon_u >> 24); *p++ = (uint8_t)(lon_u >> 16); - *p++ = (uint8_t)(lon_u >> 8); *p++ = (uint8_t)(lon_u); - // PAI(1 bit) + Speed(15 bits), packed into 2 bytes: 0 = PAI false, - // speed 0 - which is actually correct semantics for a STATIONARY - // vehicle beacon, not just a placeholder. - *p++ = 0x00; *p++ = 0x00; - // Heading (16 bits, 0.1 degree units): 0 = due north / unavailable - *p++ = 0x00; *p++ = 0x00; + // TST (4 bytes): when the position below was acquired, ms, TimestampIts mod 2^32. + put_be32(&p, lpv->tst_ms); + // Latitude/Longitude (4+4 bytes, signed, 1/10 microdegree) - fixed-width binary fields, not + // UPER bit-packed like the CAM payload's own position. + put_be32(&p, (uint32_t)lpv->lat_tenmicrodeg); + put_be32(&p, (uint32_t)lpv->lon_tenmicrodeg); + // PAI (1 bit) + Speed (15 bits, signed, 0.01 m/s). Clamped, not masked: a 15-bit value that + // overflows wraps its sign bit and reads as travelling backwards at speed. + int32_t speed = lpv->speed_cms; + if (speed > 16383) speed = 16383; + if (speed < -16384) speed = -16384; + put_be16(&p, (uint16_t)(((lpv->pai ? 1u : 0u) << 15) | ((uint16_t)speed & 0x7FFFu))); + // Heading (16 bits, 0.1 degree from north, clockwise, 0..3599). + put_be16(&p, (uint16_t)(lpv->heading_decideg % 3600u)); // 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 @@ -84,7 +95,7 @@ int geonet_wrap_shb(const uint8_t *its_payload, int its_len, *p++ = (uint8_t)(btp_dest_port & 0xFF); *p++ = 0x00; *p++ = 0x00; // destination port info, unused for BTP-B - // ---- ITS payload (DENM UPER bytes) ---- + // ---- ITS payload (CAM UPER bytes from the phone) ---- memcpy(p, its_payload, its_len); p += its_len; diff --git a/obu-firmware/main/geonet.h b/obu-firmware/main/geonet.h index 22a5531..2c32215 100644 --- a/obu-firmware/main/geonet.h +++ b/obu-firmware/main/geonet.h @@ -1,43 +1,57 @@ #ifndef GEONET_H #define GEONET_H +#include #include #include -// Wraps an ITS application payload (e.g. from denm_encode) with a minimal -// GeoNetworking Basic Header + Common Header + Single-Hop-Broadcast -// extended header (HeaderType=TSB(5), HeaderSubtype=SINGLE_HOP(0), per -// ETSI EN 302 636-4-1 table 9), then prepends a BTP-B header addressed to -// the DENM service port (2002). +// The variable content of a GeoNetworking Long Position Vector (ETSI EN 302 636-4-1 clause +// 9.5.2): who the sender is and where it was. This is the Source Position Vector every +// GeoNetworking packet from this firmware carries. // -// `mac` is the 6-byte pseudonym/link-layer address - pass the SAME address -// you hand to dot11p_build_frame's src address, since GN_ADDR's MID field -// (the last 6 bytes of the 8-byte GN_ADDR) is defined to BE that -// link-layer address (EN 302 636-4-1 clause 9.5.1). `station_type` is the -// 5-bit ITS-S type from the same clause (5 = passengerCar) and gets packed -// into GN_ADDR alongside the address. +// Every field here used to be a compile-time constant: the bench coordinates, speed 0, heading 0, +// timestamp 0, passengerCar, and one fixed MAC. The phone never told the firmware where it was, +// so the GN layer described a stationary car parked at the bench while the CAM inside it +// described a moving cyclist somewhere else. The phone now supplies these values with each frame +// (SERIAL_MSG_CAM_TX_PV in serial_link.h) and this firmware only lays them out on the wire. +typedef struct { + // Pseudonym. Written into GN_ADDR's MID field here AND, by dot11p_build_frame, into the + // 802.11 source address. Clause 9.5.1 defines the MID as the link-layer address, so the two + // must be the same six bytes; taking both from this one field is what keeps them identical + // when the pseudonym rotates. + uint8_t mac[6]; + // ITS-S type, TS 102 894-2 StationType (2 = cyclist). Only the low 5 bits fit in GN_ADDR. + uint8_t station_type; + // Position Accuracy Indicator. + bool pai; + // TST: the moment lat/lon were acquired, in ms, as TimestampIts modulo 2^32. + uint32_t tst_ms; + // 1/10 microdegree, signed. + int32_t lat_tenmicrodeg; + int32_t lon_tenmicrodeg; + // 0.01 m/s. The wire field is 15-bit signed, so this is clamped to -16384..16383 on encode. + int16_t speed_cms; + // 0.1 degree from north, clockwise. Wrapped into 0..3599 on encode. + uint16_t heading_decideg; +} gn_lpv_t; + +// Wraps an ITS application payload (the CAM UPER bytes the phone built) in a GeoNetworking Basic +// Header + Common Header + Single-Hop-Broadcast extended header (HeaderType=TSB(5), +// HeaderSubtype=SINGLE_HOP(0), EN 302 636-4-1 table 9), then a BTP-B header addressed to +// `btp_dest_port`. // -// `latitude_tenmicrodeg`/`longitude_tenmicrodeg` go into the Source Long -// Position Vector (clause 9.5.2) as plain 32-bit signed big-endian fields - -// NOT UPER bit-packed like the DENM payload's position fields, this is a -// fixed-width binary protocol. Pass the SAME values you gave denm_encode's -// eventPosition, so the GN-layer position and the DENM's own claimed -// position agree. +// Single-hop broadcast is the correct packet type for CAM, which ETSI defines as never forwarded, +// so it has no destination area and no sequence number. A future DENM transmit path would need +// GeoBroadcast (HeaderType=4) instead, which this function does not build. // -// Deliberate simplification: real DENM dissemination normally uses -// GeoBroadcast (GBC, HeaderType=4) so RSUs/OBUs can forward it across an -// area - that needs a sequence number + geo-area fields this skeleton -// doesn't build yet. Single-hop broadcast is simpler and is the -// best-tested decode path in the receiver firmware you already have -// working (same extended header shape as CAM). Fine for a single-vehicle -// beacon; revisit if you need real multi-hop forwarding later. +// `lpv` supplies the Source Position Vector. Hand the SAME lpv->mac to dot11p_build_frame as its +// source address, or the GN and 802.11 layers will name two different senders. // -// `btp_dest_port` is the BTP-B destination port for the service being carried -// (ETSI TS 103 248): 2001 = CAM, 2002 = DENM, 2003 = MAPEM, 2004 = SPATEM, ... +// `btp_dest_port` is the BTP-B destination port (ETSI TS 103 248): 2001 = CAM, 2002 = DENM, +// 2003 = MAPEM, 2004 = SPATEM. // -// Returns bytes written, or -1 if out buffer too small. +// Returns bytes written, or -1 if the out buffer is too small. int geonet_wrap_shb(const uint8_t *its_payload, int its_len, - const uint8_t mac[6], uint8_t station_type, - int32_t latitude_tenmicrodeg, int32_t longitude_tenmicrodeg, + const gn_lpv_t *lpv, uint16_t btp_dest_port, uint8_t *out, size_t out_len); diff --git a/obu-firmware/main/main.c b/obu-firmware/main/main.c index 6b3d910..3263176 100644 --- a/obu-firmware/main/main.c +++ b/obu-firmware/main/main.c @@ -21,7 +21,9 @@ static const char *TAG = "obu-tx"; // Phase 03: CAM is no longer built on this chip. The phone fuses its own GNSS+IMU, UPER-encodes -// CAM itself, and hands the finished bytes down over serial_link (SERIAL_MSG_CAM_TX) - this +// CAM itself, and hands the finished bytes down over serial_link (SERIAL_MSG_CAM_TX_PV, together +// with the GeoNetworking position vector to send them under; plain SERIAL_MSG_CAM_TX from an app +// that predates it) - this // firmware's job on transmit shrinks to "GeoNetworking/BTP-wrap + 802.11-wrap + key the PA the // instant a CAM arrives." There is no on-chip transmit timer anymore; the phone's send cadence // (1 Hz baseline, faster near intersections/events - all decided app-side) IS the air cadence. @@ -41,26 +43,25 @@ static const char *TAG = "obu-tx"; #define TX_FREQ_MHZ 5900 // ---- CAM beacon profile (used for the GeoNetworking layer only now - see below) ---- -#define STATION_TYPE 5 // passengerCar (TS 102 894-2 StationType) - matches gn_addr's ST field +#define STATION_TYPE 2 // cyclist (TS 102 894-2 StationType), legacy CAM_TX path only - see legacy_lpv() #define BTP_PORT_CAM 2001 // BTP-B destination port for CAM (ETSI TS 103 248) -// Bench location, hardcoded since there's no GNSS module wired in yet and the unit is genuinely -// stationary here: 53°33'16.8"N 10°01'20.6"E, in 1/10-microdegree units. Used ONLY for the -// GeoNetworking Source Long Position Vector now (geonet_wrap_shb's own claimed position) - the -// CAM payload's own referencePosition comes from the phone's real GNSS and can legitimately -// differ from this bench placeholder until the GN layer is also given a real position source. -// TODO: feed this from the phone too (e.g. a lightweight position update piggybacked on -// SERIAL_MSG_CAM_TX, or a new small message type) instead of a fixed bench location. +// Bench location, 53°33'16.8"N 10°01'20.6"E, in 1/10-microdegree units. Used only by the legacy +// SERIAL_MSG_CAM_TX path (see legacy_lpv), which carries no position of its own. A current app +// sends SERIAL_MSG_CAM_TX_PV instead, and the GN Source Position Vector then comes from the +// phone's real fix, the same one the CAM payload's own referencePosition is built from. #define BENCH_LATITUDE_TENMICRODEG 535546667 #define BENCH_LONGITUDE_TENMICRODEG 100223889 -// Single source of truth for the pseudonym/link-layer address: used both as -// the 802.11 source MAC (Addr2) and as GN_ADDR's MID field, since the GN -// spec defines those as being the same address. Locally-administered bit -// set (0x02) per normal MAC convention. Fixed/non-rotating for now - real -// stacks rotate this every 5-15 min for privacy. Owned entirely by this firmware (not the -// phone) per the Phase 03 design decision - simplest given the phone never needs to know it. -static const uint8_t pseudonym_mac[6] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x01}; +// Link-layer address for the legacy SERIAL_MSG_CAM_TX path only. Locally-administered bit set +// (0x02), per normal MAC convention. +// +// This reverses the Phase 03 decision that the pseudonym is owned entirely by this firmware. That +// was simplest while the address never changed, but a pseudonym only protects anyone if the +// 802.11 address, the GN_ADDR MID and the CAM's stationID all change together, and the phone owns +// the stationID. One identity needs one owner, so with CAM_TX_PV the phone sends the address with +// every frame and rotates it, and this constant is only what the legacy path falls back to. +static const uint8_t LEGACY_MAC[6] = {0x02, 0x00, 0x00, 0x00, 0x00, 0x01}; // Undocumented libphy.a calls that push the radio into 802.11p OCB mode on // the 5.9 GHz ITS-G5 band. See docs/04-transmit-setup.md for source + what @@ -77,6 +78,7 @@ extern void phy_change_channel(int freq_mhz, int bw_mode, int sec_chan_offset, i typedef struct { uint8_t data[SERIAL_LINK_MAX_PAYLOAD]; int len; + gn_lpv_t lpv; // the Source Position Vector this CAM goes out under } cam_tx_item_t; static QueueHandle_t s_tx_queue; @@ -87,6 +89,23 @@ static QueueHandle_t s_tx_queue; // tx_radio_task below, off the UART parsing path entirely. xQueueSend with 0 timeout: if the // radio task is somehow behind, drop this CAM rather than stall UART frame parsing - the next // one is only ~1s (or less, at elevated rate) away regardless. +// Source Position Vector for the legacy SERIAL_MSG_CAM_TX path, which carries no position of its +// own. Everything here describes the bench, not the rider: a fixed point, standing still, at an +// unknown time, under a fixed address. That is exactly why the phone now sends CAM_TX_PV. Kept so +// an app that predates it still transmits what it always did, except that the station type now +// says cyclist to agree with the CAM inside. +static void legacy_lpv(gn_lpv_t *lpv) +{ + memcpy(lpv->mac, LEGACY_MAC, sizeof(lpv->mac)); + lpv->station_type = STATION_TYPE; + lpv->pai = false; + lpv->tst_ms = 0; + lpv->lat_tenmicrodeg = BENCH_LATITUDE_TENMICRODEG; + lpv->lon_tenmicrodeg = BENCH_LONGITUDE_TENMICRODEG; + lpv->speed_cms = 0; + lpv->heading_decideg = 0; +} + static void on_cam_tx_from_phone(const uint8_t *cam_uper, int cam_len) { if (cam_len <= 0 || cam_len > SERIAL_LINK_MAX_PAYLOAD) { @@ -96,6 +115,42 @@ static void on_cam_tx_from_phone(const uint8_t *cam_uper, int cam_len) cam_tx_item_t item; item.len = cam_len; memcpy(item.data, cam_uper, (size_t)cam_len); + legacy_lpv(&item.lpv); + if (xQueueSend(s_tx_queue, &item, 0) != pdTRUE) { + ESP_LOGW(TAG, "tx queue full, dropping CAM from phone"); + } +} + +static uint16_t le16(const uint8_t *p) +{ + return (uint16_t)(p[0] | (p[1] << 8)); +} + +static uint32_t le32(const uint8_t *p) +{ + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} + +// SERIAL_MSG_CAM_TX_PV: the phone's CAM plus the position vector to send it under. The prefix +// layout is documented at SERIAL_MSG_CAM_TX_PV in serial_link.h. Same speed constraint as +// on_cam_tx_from_phone: decode, queue, return. +static void on_cam_tx_pv_from_phone(const uint8_t *prefix, const uint8_t *cam_uper, int cam_len) +{ + if (cam_len <= 0 || cam_len > SERIAL_LINK_MAX_PAYLOAD) { + ESP_LOGW(TAG, "on_cam_tx_pv_from_phone: bad length %d", cam_len); + return; + } + cam_tx_item_t item; + item.len = cam_len; + memcpy(item.data, cam_uper, (size_t)cam_len); + memcpy(item.lpv.mac, prefix, sizeof(item.lpv.mac)); + item.lpv.station_type = prefix[6]; + item.lpv.pai = (prefix[7] & 0x01) != 0; + item.lpv.tst_ms = le32(prefix + 8); + item.lpv.lat_tenmicrodeg = (int32_t)le32(prefix + 12); + item.lpv.lon_tenmicrodeg = (int32_t)le32(prefix + 16); + item.lpv.speed_cms = (int16_t)le16(prefix + 20); + item.lpv.heading_decideg = le16(prefix + 22); if (xQueueSend(s_tx_queue, &item, 0) != pdTRUE) { ESP_LOGW(TAG, "tx queue full, dropping CAM from phone"); } @@ -116,8 +171,7 @@ static void tx_radio_task(void *arg) // singleton, created once in app_main. Both wrap functions bounds-check against the size // passed in and return <= 0 on overflow, so an oversized CAM is rejected, not written past. static uint8_t gn_payload[SERIAL_LINK_MAX_PAYLOAD + 64]; - int gn_len = geonet_wrap_shb(item.data, item.len, pseudonym_mac, STATION_TYPE, - BENCH_LATITUDE_TENMICRODEG, BENCH_LONGITUDE_TENMICRODEG, + int gn_len = geonet_wrap_shb(item.data, item.len, &item.lpv, BTP_PORT_CAM, gn_payload, sizeof(gn_payload)); if (gn_len <= 0) { ESP_LOGW(TAG, "geonet_wrap_shb failed (cam_len=%d)", item.len); @@ -125,7 +179,9 @@ static void tx_radio_task(void *arg) } static uint8_t frame[SERIAL_LINK_MAX_PAYLOAD + 192]; - int frame_len = dot11p_build_frame(gn_payload, gn_len, pseudonym_mac, frame, + // Source address from the same lpv the GN header was built from, so the 802.11 and + // GeoNetworking layers always name the same sender, including across a pseudonym change. + int frame_len = dot11p_build_frame(gn_payload, gn_len, item.lpv.mac, frame, sizeof(frame), false); if (frame_len <= 0) { ESP_LOGW(TAG, "dot11p_build_frame failed (gn_len=%d)", gn_len); @@ -339,7 +395,7 @@ void app_main(void) xTaskCreate(tx_radio_task, "tx_radio", 4096, NULL, 6, NULL); xTaskCreate(rx_forward_task, "rx_forward", 4096, NULL, 5, NULL); - serial_link_init(on_cam_tx_from_phone); + serial_link_init(on_cam_tx_from_phone, on_cam_tx_pv_from_phone); ESP_LOGW(TAG, "OCB @ %d MHz - TX/RX armed, driven by serial_link (no on-chip TX timer)", TX_FREQ_MHZ); diff --git a/obu-firmware/main/serial_link.c b/obu-firmware/main/serial_link.c index bf68bae..a1be9ba 100644 --- a/obu-firmware/main/serial_link.c +++ b/obu-firmware/main/serial_link.c @@ -13,6 +13,7 @@ static const char *TAG = "serial_link"; #define SYNC1 0x55 static serial_link_cam_tx_cb_t s_on_cam_tx; +static serial_link_cam_tx_pv_cb_t s_on_cam_tx_pv; // ---- Counters reported to the phone in every heartbeat (see SERIAL_MSG_STATUS in the header). // Saturating rather than wrapping: "65535 drops" reads as "lots and still going", whereas a wrap @@ -157,9 +158,9 @@ bool serial_link_send_v2x_rx(uint16_t btp_dest_port, int8_t rssi, bool serial_link_send_status(uint8_t status) { - // [status:1][oversize_drops:2 LE][tx_failures:2 LE][rx_crc_errors:2 LE] - keep in lockstep - // with EspLinkStatus.parse() in the app's SerialFrame.kt. - uint8_t payload[7]; + // [status:1][oversize_drops:2 LE][tx_failures:2 LE][rx_crc_errors:2 LE][capabilities:1] - + // keep in lockstep with EspLinkStatus.parse() in the app's SerialFrame.kt. + uint8_t payload[8]; payload[0] = status; payload[1] = (uint8_t)(s_oversize_drops & 0xFF); payload[2] = (uint8_t)((s_oversize_drops >> 8) & 0xFF); @@ -167,6 +168,9 @@ bool serial_link_send_status(uint8_t status) payload[4] = (uint8_t)((s_tx_failures >> 8) & 0xFF); payload[5] = (uint8_t)(s_rx_crc_errors & 0xFF); payload[6] = (uint8_t)((s_rx_crc_errors >> 8) & 0xFF); + // What this firmware accepts. The app reads it to decide whether it may send CAM_TX_PV, which + // is what lets a new app keep working against firmware that predates that message. + payload[7] = SERIAL_CAP_CAM_TX_PV; return send_frame(SERIAL_MSG_STATUS, payload, sizeof(payload)); } @@ -262,9 +266,19 @@ static void rx_task(void *arg) uint16_t crc_calc = crc16_ccitt_false(crc_buf, (size_t)(3 + len)); if (crc_calc == crc_recv) { - if (type == SERIAL_MSG_CAM_TX && s_on_cam_tx) { - s_on_cam_tx(payload, len); - } else if (type != SERIAL_MSG_CAM_TX) { + if (type == SERIAL_MSG_CAM_TX) { + if (s_on_cam_tx) s_on_cam_tx(payload, len); + } else if (type == SERIAL_MSG_CAM_TX_PV) { + // A frame that is all prefix has nothing to transmit. + if (len > SERIAL_CAM_TX_PV_PREFIX_LEN) { + if (s_on_cam_tx_pv) { + s_on_cam_tx_pv(payload, payload + SERIAL_CAM_TX_PV_PREFIX_LEN, + len - SERIAL_CAM_TX_PV_PREFIX_LEN); + } + } else { + ESP_LOGW(TAG, "rx: CAM_TX_PV of %u bytes carries no CAM, ignoring", len); + } + } else { ESP_LOGW(TAG, "rx: unexpected frame type 0x%02x from phone, ignoring", type); } } else { @@ -279,9 +293,11 @@ static void rx_task(void *arg) } } -void serial_link_init(serial_link_cam_tx_cb_t on_cam_tx) +void serial_link_init(serial_link_cam_tx_cb_t on_cam_tx, + serial_link_cam_tx_pv_cb_t on_cam_tx_pv) { s_on_cam_tx = on_cam_tx; + s_on_cam_tx_pv = on_cam_tx_pv; s_tx_mutex = xSemaphoreCreateMutex(); if (!s_tx_mutex) { diff --git a/obu-firmware/main/serial_link.h b/obu-firmware/main/serial_link.h index 7a6f555..87fa6fb 100644 --- a/obu-firmware/main/serial_link.h +++ b/obu-firmware/main/serial_link.h @@ -49,20 +49,52 @@ // 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: -// [status:1][oversize_drops:2 LE][tx_failures:2 LE][rx_crc_errors:2 LE] +// UsbSerialTransport.kt declares the link dead after 3 missed beats). Payload is 8 bytes: +// [status:1][oversize_drops:2 LE][tx_failures:2 LE][rx_crc_errors:2 LE][capabilities:1] // status 0 = ok. The counters are free-running totals since boot, saturating at 0xFFFF. +// capabilities is a bitmask of the SERIAL_CAP_* flags below. It was appended as byte 7 rather +// than inserted, so an app that predates it, and reads only the first 7 bytes, is unaffected. // They exist because the alternative - ESP_LOGW on the flashing port - is invisible to the // phone, which is the only thing watching during a bench session. Mirrored by EspLinkStatus // in the app's SerialFrame.kt. -#define SERIAL_MSG_CAM_TX 0x01 -#define SERIAL_MSG_CAM_RX 0x02 -#define SERIAL_MSG_STATUS 0x03 -#define SERIAL_MSG_V2X_RX 0x04 +#define SERIAL_MSG_CAM_TX 0x01 +#define SERIAL_MSG_CAM_RX 0x02 +#define SERIAL_MSG_STATUS 0x03 +#define SERIAL_MSG_V2X_RX 0x04 +#define SERIAL_MSG_CAM_TX_PV 0x05 // Size of the V2X_RX prefix documented above. Must match the app's SerialFrame.kt. #define SERIAL_V2X_RX_PREFIX_LEN 14 +// SERIAL_MSG_CAM_TX_PV (0x05), phone -> ESP32: a CAM together with the GeoNetworking Source +// Position Vector to transmit it under. Payload is a fixed 24-byte prefix, then the CAM UPER: +// +// [0..5] mac 6 bytes pseudonym: the 802.11 source address AND the GN_ADDR MID +// [6] station_type uint8 TS 102 894-2 StationType (2 = cyclist) +// [7] flags uint8 bit0: PAI, position accuracy indicator +// [8..11] tst uint32 LE ms at which lat/lon were acquired, TimestampIts mod 2^32 +// [12..15] lat int32 LE 1/10 microdegree +// [16..19] lon int32 LE 1/10 microdegree +// [20..21] speed int16 LE 0.01 m/s +// [22..23] heading uint16 LE 0.1 degree from north, clockwise, 0..3599 +// [24..] CAM UPER bytes +// +// Little-endian like the rest of this framing; geonet.c converts to GeoNetworking's big-endian. +// Every prefix field is something the phone already has when it builds the CAM, and none of it +// can be known on this chip, which has no GNSS and no clock source on the OCB channel. Before +// this message existed the GN header carried fixed placeholders instead (see main.c). +// +// A new type rather than a redefined CAM_TX, so app and firmware can be updated independently: +// - old app, new firmware: the app sends CAM_TX, which is handled exactly as before. +// - new app, old firmware: the app sends CAM_TX_PV only once the heartbeat advertises +// SERIAL_CAP_CAM_TX_PV, and an old heartbeat carries no such bit, so it stays on CAM_TX. +// Redefining CAM_TX would instead have double-wrapped every frame in one of those combinations +// and sent one with no GN header in the other, silently, since neither side checks versions. +#define SERIAL_CAM_TX_PV_PREFIX_LEN 24 + +// Capability bits, carried in byte 7 of the SERIAL_MSG_STATUS payload. +#define SERIAL_CAP_CAM_TX_PV 0x01 + // 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 // sizes for usb_serial_jtag_driver_install() (see serial_link.c) are sized generously relative @@ -83,16 +115,25 @@ // Raised from 160 to 512: 160 was reasoned from cam.c's 96-byte encode buffer, which only ever // described OUR OWN minimal CAM. A third-party CAM off the air carrying a path-history or // special-vehicle container comfortably exceeds it, and those stations would then never reach the -// phone at all. 512 clears any realistic CAM; the real upstream ceiling on the RX path is -// rx_item_t.data (400 bytes) in main.c, so nothing larger can get here anyway. +// phone at all. 512 clears any realistic CAM. Our own CAM is 43 bytes of UPER. +// +// This, not the radio side, is the ceiling on the RX path. main.c captures up to RX_FRAME_MAX_LEN +// (800) bytes per frame, sized for the CiT One's 528-byte DENM, so a larger ITS payload +// does arrive here. serial_link_send_v2x_rx() then drops anything above this minus its 14-byte +// prefix and counts it in the heartbeat's oversize-drop counter. #define SERIAL_LINK_MAX_PAYLOAD 512 // Initializes the USB Serial/JTAG driver and its background RX-framing and 1 Hz heartbeat tasks. -// Call once from app_main, after nvs/event loop init. `on_cam_tx` is invoked (from the RX task's -// context - keep it fast, it blocks the next frame's parsing) whenever a complete, checksummed -// SERIAL_MSG_CAM_TX frame arrives from the phone. +// Call once from app_main, after nvs/event loop init. Both callbacks run in the RX task's context, +// so keep them fast: they block the next frame's parsing. +// on_cam_tx a complete, checksummed SERIAL_MSG_CAM_TX frame: bare CAM UPER. +// on_cam_tx_pv a complete, checksummed SERIAL_MSG_CAM_TX_PV frame, already checked to carry at +// least one CAM byte after its prefix: the 24-byte prefix, then the CAM UPER. 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); +typedef void (*serial_link_cam_tx_pv_cb_t)(const uint8_t *prefix, + const uint8_t *cam_uper, int cam_len); +void serial_link_init(serial_link_cam_tx_cb_t on_cam_tx, + serial_link_cam_tx_pv_cb_t on_cam_tx_pv); // 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