SPATEM over the air - gn_unwrap.c accepts BTP-B port 2004 alongside 2001/2002. The serial protocol already carries the port in its V2X_RX prefix, so nothing else changed there. Note the crossover that makes this easy to get wrong: SPATEM is port 2004 but messageID 4, while MAPEM is port 2003 and messageID 5. - SpatemUperCodec decodes SPAT down to per-signal-group phase and timing. The bit layout was validated by replaying 79,042 real SPATEMs - the whole 2026-03-18 drive across 7+ RSUs plus the bench trigger - against asn1tools using the ETSI modules. All 79,042 matched on every field, none hit an unsupported branch. Two traps are pinned by tests: TimeChangeDetails is the one SEQUENCE here that is NOT extensible (5 optional bits, no extension bit), and maneuverAssistList cannot be skipped when present - it is variable-length, so it has to be walked to find where the next movement starts. - The V2X list shows one row per intersection with each signal group coloured by phase and a countdown where the RSU supplies timing. TimeMark wraps hourly, so the countdown corrects for it; without that it reads hugely negative once an hour, precisely when someone is watching it. - Entries expire after 15 s, much shorter than DENM's window: a traffic light that stopped updating is not "still green". Size caveat, deliberately deferred: SERIAL_LINK_MAX_PAYLOAD is still 512, so a SPATEM over ~498 bytes is counted as an oversize drop. The bench RSU sends 58 bytes and is unaffected, but real road RSUs measured 555 median / 1243 max, so roughly 70% would not arrive. Raising the cap also requires enlarging RX_FRAME_MAX_LEN and moving rx_item_t off the WiFi driver's callback stack, where it would otherwise overflow. RSU CAM decode - HighFrequencyContainer is a CHOICE, and a roadside unit picks rsuContainerHighFrequency, which carries no kinematics at all. The decoder bailed on that branch, so every RSU CAM was dropped - including the bench RSU, which sends CAM and SPATEM from the same station id. It now decodes for position and stationType. - RSU CAMs are kept out of UseCaseDetectionEngine. They arrive as a permanently stationary station at a fixed point, which is exactly the shape the stopped-vehicle and intersection-movement use cases match, and would raise a standing false alert for as long as the RSU was in range. CAM transmit: yawRateConfidence - YawRateConfidence has nine enumerands (0..8), so UPER needs 4 bits and "unavailable" is 8. The encoder wrote 3 bits with value 7 - one bit short and the wrong symbol - shifting every field after yawRate for any standards-strict receiver. The decoder read 3 bits too, so phone and ESP32 agreed with each other and with nothing else. - This is the third instance of that exact failure mode in this project, after CurvatureCalculationMode and the GeoNetworking reserved bytes. A round-trip test through our own decoder structurally cannot catch it, so CamEncodeGolden Test asserts the bytes asn1tools produces instead: it decoded this encoder's output and re-encoded it byte-identically. Confirmed on air afterwards - 26 of our own CAMs captured back off the OBU's receiver, all 26 accepted, where the same decoder rejected them before. DENM - Hazards now expire 60 s after their last repetition. This needs a clock, not just a filter: both source flows only emit when a DENM arrives, so a sender that drives away or loses power would never trigger a recompute and its hazard would stay on screen indefinitely. - The MQTT path was dropping every DENM for two independent reasons, both found by checking the payload against CI-CiT-MQTT_API_Documentation-v6 listing 2.6 rather than guessing: the station id key is originatingStationId, and eventPosition IS a GeoJSON Point rather than an object containing one. Also parses termination (presence is the signal), sequenceNumber, stationType and the RFC3339 detectionTime. Note roadSideUnit is 15, not 12 - the enumeration has a gap after tram(11). V2X screen - The decoded CAM/DENM list now renders on the CiT One path too; it was gated to the ESP32-C5 path and CiT One fell through to the raw MQTT topic list. Those topics move to their own tab, hidden on the ESP32-C5 path where there is no broker. Testing - Adds org.json as a test-only dependency: the android.jar stub throws "not mocked" on every JSONObject call, which made the MQTT payload parsers untestable off-device. - 23 V2X tests pass. EventDetectorTest's 4 failures are pre-existing and untouched by this change.
76 lines
4.3 KiB
C
76 lines
4.3 KiB
C
#ifndef GN_UNWRAP_H
|
|
#define GN_UNWRAP_H
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#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 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
|