#ifndef GN_UNWRAP_H #define GN_UNWRAP_H #include #include #include // 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 the ITS payload (a UPER // message) plus the metadata the phone needs to know what it received. // // ---- Supported GeoNetworking header types -------------------------------------------------- // Two shapes, chosen by the Common Header's HeaderType, with DIFFERENT extended-header lengths: // // 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. // // 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. // // 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. // // ---- Accepted BTP-B ports (ETSI TS 103 248) ------------------------------------------------ // 2001 (CAM), 2002 (DENM) and 2004 (SPATEM). MAPEM (2003) 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). // // SPATEM size caveat: SERIAL_LINK_MAX_PAYLOAD is 512, so a SPATEM whose UPER exceeds 498 bytes is // counted as an oversize drop rather than forwarded. The bench RSU trigger emits ~58-byte SPATEMs // and is unaffected, but real road RSUs measured 555 bytes median and 1243 max (2026-03-18 drive, // 79k messages), i.e. roughly 70% would be dropped. Raising the cap is deliberately deferred: it // also requires enlarging RX_FRAME_MAX_LEN and moving rx_item_t off the WiFi callback stack, // which at that size would overflow it. // // ---- 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