Files
MicrOBU/obu-firmware/main/serial_link.h
T
Ashin Walpola 3eeccfb268 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.
2026-09-10 14:47:30 +02:00

160 lines
10 KiB
C

#ifndef SERIAL_LINK_H
#define SERIAL_LINK_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
// Binary framing for the phone <-> ESP32-C5 link (Phase 03). This runs over the ESP32-C5's
// native USB Serial/JTAG peripheral (driver/usb_serial_jtag.h) - the same physical USB-C port
// used for JTAG, exposed to the host as a fixed-VID/PID (0x303A/0x1001) USB CDC-ACM device.
// Deliberately NOT the same wire as the ESP-IDF console/ESP_LOG output, which stays on the
// OTHER USB-C port (the UART-bridge one, UART0, see sdkconfig CONFIG_ESP_CONSOLE_UART_NUM=0) -
// mixing binary frames with human-readable log text on one wire would corrupt both, and this
// way they're physically separate ports so there's no risk of that regardless.
//
// On the Android side, connect the phone (via USB-OTG) to the board's NATIVE USB-C port, not
// the UART-bridge/flashing port. The usb-serial-for-android library's default prober doesn't
// know Espressif's 0x303A/0x1001 VID/PID, so UsbSerialTransport.kt registers it manually via a
// custom ProbeTable pointed at CdcAcmSerialDriver - see that file's KDoc.
//
// Frame format (both directions, symmetric):
// [0xAA][0x55][type:1][length:2 LE][payload: length bytes][crc16:2 LE]
// CRC16 is CRC-16/CCITT-FALSE (poly 0x1021, init 0xFFFF, no reflect, no xorout), computed over
// type + length + payload only (not the two sync bytes). Same algorithm must be used on the
// Kotlin side (see app SerialFrame.kt) - frames that don't checksum are silently dropped.
//
// Direction / types:
// SERIAL_MSG_CAM_TX (0x01), phone -> ESP32: payload is a raw CAM UPER byte string, already
// 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: 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 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_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
// to SERIAL_LINK_MAX_PAYLOAD below.
#define SERIAL_LINK_USB_BUF_SIZE 1024
// Per-write block ceiling, and the mutex acquire timeout that must comfortably exceed the
// worst case of one frame (4 writes: sync, head, payload, crc). Keep that relationship if you
// change either number - a lock timeout below the max hold turns normal contention into
// dropped frames, which is how the heartbeat was being starved by forwarded CAM_RX traffic.
#define SERIAL_LINK_WRITE_TIMEOUT_MS 100
#define SERIAL_LINK_TX_LOCK_TIMEOUT_MS 600
// Max CAM payload this link will carry. MUST match SERIAL_LINK_MAX_PAYLOAD in the app's
// SerialFrame.kt - a mismatch means every frame above the smaller of the two is rejected by that
// side's "length exceeds max, resync" branch, silently.
//
// 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. 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. 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);
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
// 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.
bool serial_link_send_status(uint8_t status);
// Records a failed esp_wifi_80211_tx() so it shows up in the next heartbeat's tx_failures
// counter. Called from main.c's tx_radio_task - a CAM that reached the radio but didn't go out is
// otherwise indistinguishable, from the phone's side, from one that transmitted fine.
void serial_link_note_tx_failure(void);
#endif