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