obu-firmware is now a port of the colleague's standalone VRU station (microbu-esp32c5/firmware, kept beside this repository and gitignored): the vanetza-idf C-ITS stack with the TS 103 097 security entity, credentials in NVS, the station-link v1 protocol over the native USB port (frame type 0x10 in the existing 0xAA55 framing) and over a BLE GATT peripheral, and its ITS-G5 radio adapter. The phone still builds CAM and VAM; the board adds GeoNetworking/BTP and signs with the provisioned authorization ticket. The private key never leaves the board. Builds with ESP-IDF 6.0.2 only, which vanetza-idf pins for the radio's private driver ABI. The previous C firmware stays on disk unbuilt; a full-flash backup of the bench board is kept in firmware-backups/ (gitignored). Changed against the colleague's firmware, marked MicrOBU: in the sources: - Reception unchanged for the app. vanetza-idf drops what it cannot verify (unsigned traffic, every RSU), so each captured frame also goes through the previous gn_unwrap.c and reaches the phone as link opcode V2X_RX (0x85), whose body is the old SERIAL_MSG_V2X_RX payload. - Unsigned transmission still possible, with the previous geonet.c header; the phone chooses per message. - Console on UART0 (CH343 port); the native USB port carries only link frames. - BLE advertising pauses while the USB link is in use: BLE and ITS-G5 share one RF front end. - NVS 80 KB (app at 0x20000). At 24 KB, with Wi-Fi settings the previous firmware left behind, the BLE bond could not be stored and the phone had to pair on every connection. - Bench fixes: the radio queue is drained before the first PoTi (no RX and ~177 queue drops before); the station loop waited pdMS_TO_TICKS(5) = 0 ticks at 100 Hz and starved the idle task; the 2.4 KB RX capture buffer is off the Wi-Fi task stack; BLE notifications longer than the MTU are dropped instead of cut short, MTU 517; serial writes are skipped with no USB host. - Manual country policy and TX-power read-back from the previous radio setup; logs for BLE encryption changes and the number of stored bonds. Verified on the bench board (COM3) with the phone over USB and BLE: CAM and VAM, signed and unsigned, go out; reception of the sim car and the RSU's CAM/SPATEM/MAPEM continues; the board survives app restarts and reconnects. See docs/06-signed-its-vam-ble.md.
364 lines
16 KiB
C++
364 lines
16 KiB
C++
#pragma once
|
|
// micrOBU station-internal link, message layer version 1 (implementation/station-link/README.md).
|
|
// Transport independent: the same octets are one GATT attribute value later and one serial
|
|
// frame payload today. Little-endian integers; ETSI payloads inside stay opaque.
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace microbu::link {
|
|
|
|
using Bytes = std::vector<std::uint8_t>;
|
|
|
|
constexpr std::size_t maximum_message = 512;
|
|
constexpr std::size_t header_size = 4;
|
|
|
|
/// @brief The message opcode, which determines the body type.
|
|
enum class Opcode : std::uint8_t {
|
|
// phone -> ESP32-C5
|
|
STATION_CONFIGURE = 0x01,
|
|
POTI_UPDATE = 0x02,
|
|
BTP_DATA_REQUEST = 0x03,
|
|
CREDENTIALS_PROVISION = 0x04,
|
|
CREDENTIALS_ERASE = 0x05,
|
|
SF_IDCHANGE_SUBSCRIBE = 0x06,
|
|
SF_IDCHANGE_UNSUBSCRIBE = 0x07,
|
|
SF_IDCHANGE_EVENT_RESPONSE = 0x08,
|
|
SF_IDCHANGE_TRIGGER = 0x09,
|
|
SF_ID_LOCK = 0x0A,
|
|
SF_ID_UNLOCK = 0x0B,
|
|
STATUS_REQUEST = 0x0C,
|
|
// ESP32-C5 -> phone
|
|
RESULT = 0x80,
|
|
BTP_DATA_INDICATION = 0x81,
|
|
SF_IDCHANGE_EVENT = 0x82,
|
|
STATUS = 0x84,
|
|
// MicrOBU extension, not in the colleague's station-link v1: every ITS message heard on air,
|
|
// unwrapped by gn_unwrap.c whether or not the security entity could verify it. Body is the
|
|
// previous firmware's SERIAL_MSG_V2X_RX payload unchanged: [u16 btp_dest_port][i8 rssi]
|
|
// [u8 flags: bit0 geo area valid, bit1 signed but not verified][i32 area_lat][i32 area_lon]
|
|
// [u16 area_distance_a], then the UPER bytes. See obu-firmware/NOTES.md.
|
|
V2X_RX = 0x85,
|
|
};
|
|
|
|
constexpr std::uint8_t flag_fragment_first = 0x01;
|
|
constexpr std::uint8_t flag_fragment_more = 0x02;
|
|
|
|
// RESULT codes: vanetza_idf::Result values first (same numbering), then link codes.
|
|
enum class Code : std::uint8_t {
|
|
accepted = 0, invalid_argument = 1, unsupported = 2, wrong_entry_point = 3, security_unavailable = 4,
|
|
resource_limit = 5, rejected = 6, time_regression = 7, identity_change_pending = 8,
|
|
unknown_opcode = 0x10, malformed = 0x11, not_configured = 0x12, busy = 0x13, no_credentials = 0x14,
|
|
};
|
|
|
|
/// @brief The header of a link message.
|
|
struct Header {
|
|
Opcode opcode;
|
|
std::uint8_t flags = 0;
|
|
std::uint16_t sequence = 0;
|
|
};
|
|
|
|
/// @brief A complete link message.
|
|
struct Message {
|
|
Header header;
|
|
Bytes body;
|
|
};
|
|
|
|
/// @brief Serializes header + body.
|
|
/// @param message Message to serialize.
|
|
/// @param out Receives the serialized bytes.
|
|
/// @return false when the result would exceed maximum_message.
|
|
bool encode(const Message& message, Bytes& out);
|
|
/// @brief Parses header + body.
|
|
/// @param octets Raw bytes to parse.
|
|
/// @param out Receives the decoded message.
|
|
/// @return false when shorter than the header or longer than maximum_message.
|
|
bool decode(const Bytes& octets, Message& out);
|
|
|
|
// ---- bodies ------------------------------------------------------------------------------
|
|
|
|
// security, address_configuration, default_traffic_class and default_lifetime are the GN protocol
|
|
// constants itsGnSecurity, itsGnLocalAddrConfMethod, itsGnDefaultTrafficClass and
|
|
// itsGnDefaultPacketLifetime of ETSI TS 103 836-4-1 (GeoNetworking) annex H; channel_number,
|
|
// transmit_power_dbm and radio are ITS-G5 access-layer parameters (ETSI EN 303 797).
|
|
struct StationConfigure {
|
|
std::uint8_t station_type = 2; // TS 102 894-2 StationType (2 = cyclist)
|
|
std::uint8_t security = 1; // itsGnSecurity
|
|
std::uint8_t address_configuration = 1; // 0 AUTO (mid below), 1 ANONYMOUS (ticket digest)
|
|
std::uint8_t mid[6] = {2, 0, 0, 0, 0, 1};
|
|
std::uint8_t beaconing = 1;
|
|
std::uint16_t channel_number = 180;
|
|
std::uint8_t transmit_power_dbm = 10;
|
|
std::uint8_t radio = 0; // 0 off, 1 receive only, 2 transmit and receive
|
|
std::uint8_t default_traffic_class = 2;
|
|
std::uint8_t default_lifetime = 0x05; // 1 s (base One_Second, multiplier 1)
|
|
};
|
|
/// @param body Raw message body bytes.
|
|
/// @param out Receives the decoded configuration.
|
|
/// @return false if malformed.
|
|
bool decode(const Bytes& body, StationConfigure& out);
|
|
|
|
/// @param value Configuration to encode.
|
|
/// @return Encoded body bytes.
|
|
Bytes encode(const StationConfigure& value);
|
|
|
|
// Position/confidence-ellipse/speed/heading fields are the ETSI TS 102 894-2 (Common Data
|
|
// Dictionary) ReferencePosition / PosConfidenceEllipse data types; pai() mirrors the Position
|
|
// Accuracy Indicator field of the GeoNetworking Long Position Vector (ETSI TS 103 836-4-1
|
|
// clause 9.5.2.2, table 2).
|
|
struct PotiUpdate {
|
|
std::uint64_t timestamp_ms = 0; // TimestampIts
|
|
std::int32_t latitude = 0; // 1/10 microdegree
|
|
std::int32_t longitude = 0;
|
|
std::uint16_t semi_major_cm = 0;
|
|
std::uint16_t semi_minor_cm = 0;
|
|
std::uint16_t orientation_deci_degree = 0;
|
|
std::uint8_t flags = 0; // bit0 altitude, bit1 speed, bit2 heading, bit3 PAI
|
|
std::int32_t altitude_cm = 0;
|
|
std::uint16_t speed_cm_s = 0;
|
|
std::uint16_t heading_deci_degree = 0;
|
|
bool has_altitude() const { return flags & 1; }
|
|
bool has_speed() const { return flags & 2; }
|
|
bool has_heading() const { return flags & 4; }
|
|
bool pai() const { return flags & 8; }
|
|
};
|
|
|
|
/// @param body Raw message body bytes.
|
|
/// @param out Receives the decoded fix.
|
|
/// @return false if malformed.
|
|
bool decode(const Bytes& body, PotiUpdate& out);
|
|
|
|
/// @param value Fix to encode.
|
|
/// @return Encoded body bytes.
|
|
Bytes encode(const PotiUpdate& value);
|
|
|
|
|
|
/// @brief The message body of a BTP_DATA_REQUEST (ETSI TS 103 836-4-1 annex J.2) request.
|
|
// GeoArea per ETSI TS 103 899 "Geographical Area Definition". shape follows the GEOBROADCAST_*/
|
|
// GEOANYCAST_* header sub-type encoding of ETSI TS 103 836-4-1 (GeoNetworking) clause 9.7.4 table 9
|
|
// (0 circle, 1 rectangle, 2 ellipse); position/distance_a/distance_b/angle are the GeoArea fields
|
|
// carried in the GBC/GAC extended header per clause 9.8.5 table 36.
|
|
struct DestinationArea {
|
|
std::uint8_t shape = 0; // 0 circle, 1 rectangle, 2 ellipse
|
|
std::int32_t latitude = 0;
|
|
std::int32_t longitude = 0;
|
|
std::uint16_t distance_a = 0;
|
|
std::uint16_t distance_b = 0;
|
|
std::uint16_t angle = 0;
|
|
};
|
|
|
|
// btp_type/destination_port(_info) are the BTP-A/BTP-B header fields of ETSI TS 103 836-5-1 (Basic
|
|
// Transport Protocol); the gn_* fields mirror the TRANSP_CORE.request service primitive parameters
|
|
// of ETSI TS 103 836-4-1 (GeoNetworking) annex J.2 (Packet transport type, Traffic class, Maximum
|
|
// hop limit, Repetition interval/maximum, Security profile); its_aid follows the ITS-AID registry
|
|
// of ETSI TS 102 965.
|
|
struct BtpDataRequest {
|
|
std::uint8_t btp_type = 1;
|
|
std::uint16_t destination_port = 0;
|
|
std::uint16_t destination_port_info = 0; // or source_port for BTP-A
|
|
std::uint8_t gn_packet_transport_type = 1; // 0 GUC, 1 SHB, 2 TSB, 3 GBC, 4 GAC (TS 103 836-4-1 annex J.2)
|
|
std::uint8_t gn_communication_profile = 1;
|
|
std::uint8_t gn_security_profile = 0; // 0 not given, 1 unsecured, 2 secured
|
|
std::uint8_t gn_traffic_class = 0xFF; // 0xFF = station default
|
|
std::uint8_t gn_maximum_packet_lifetime = 0xFF; // 0xFF = station default
|
|
std::uint8_t gn_maximum_hop_limit = 0; // 0 = station default
|
|
std::uint16_t gn_repetition_interval_ms = 0;
|
|
std::uint16_t gn_repetition_maximum_ms = 0;
|
|
std::uint32_t its_aid = 0;
|
|
Bytes permissions;
|
|
Bytes context;
|
|
std::optional<DestinationArea> area; // present for GBC
|
|
Bytes fl_sdu;
|
|
};
|
|
|
|
/// @param body Raw message body bytes.
|
|
/// @param out Receives the decoded request.
|
|
/// @return false if malformed.
|
|
bool decode(const Bytes& body, BtpDataRequest& out);
|
|
|
|
/// @param value Request to encode.
|
|
/// @return Encoded body bytes.
|
|
Bytes encode(const BtpDataRequest& value);
|
|
|
|
|
|
|
|
// Mirrors the TRANSP_CORE.indication service primitive parameters of ETSI TS 103 836-4-1
|
|
// (GeoNetworking) annex J.4: source_gn_address/source_timestamp/source_latitude/source_longitude
|
|
// are the sender's Long Position Vector (clause 9.5.2), security_report/certificate_* the Security
|
|
// report/Certificate id parameters, its_aid/permissions the ITS-AID/Security permissions
|
|
// parameters.
|
|
struct BtpDataIndication {
|
|
std::uint8_t btp_type = 1;
|
|
std::uint16_t destination_port = 0;
|
|
std::uint16_t destination_port_info = 0;
|
|
std::uint8_t gn_packet_transport_type = 1; // 0 GUC, 1 SHB, 2 TSB, 3 GBC, 4 GAC (TS 103 836-4-1 annex J.4)
|
|
std::uint8_t gn_traffic_class = 0;
|
|
std::uint8_t gn_remaining_packet_lifetime = 0xFF;
|
|
std::uint8_t gn_remaining_hop_limit = 0xFF;
|
|
std::uint8_t source_gn_address[8] = {};
|
|
std::uint32_t source_timestamp = 0;
|
|
std::int32_t source_latitude = 0;
|
|
std::int32_t source_longitude = 0;
|
|
std::uint8_t security_report = 0; // 0 unsecured, 1 + VerificationReport ordinal
|
|
std::uint32_t its_aid = 0;
|
|
Bytes permissions;
|
|
bool certificate_present = false;
|
|
std::uint8_t certificate_id[8] = {};
|
|
std::optional<DestinationArea> area;
|
|
Bytes received_fl_sdu;
|
|
};
|
|
|
|
/// @param body Raw message body bytes.
|
|
/// @param out Receives the decoded indication.
|
|
/// @return false if malformed.
|
|
bool decode(const Bytes& body, BtpDataIndication& out);
|
|
|
|
/// @param value Indication to encode.
|
|
/// @return Encoded body bytes.
|
|
Bytes encode(const BtpDataIndication& value);
|
|
|
|
|
|
|
|
// Segmented upload of an ETSI TS 102 941 (Trust and Privacy Management) credential bundle
|
|
// (root CA / enrolment or authorization authority certificates, authorization tickets), whose
|
|
// certificate encoding is ETSI TS 103 097.
|
|
struct CredentialsProvision {
|
|
std::uint16_t total_length = 0;
|
|
std::uint16_t offset = 0;
|
|
Bytes segment;
|
|
};
|
|
|
|
/// @param body Raw message body bytes.
|
|
/// @param out Receives the decoded segment.
|
|
/// @return false if malformed.
|
|
/// @note One CredentialsProvision carries a single segment of a larger bundle; total_length
|
|
/// and offset let the caller reassemble the full bundle across several messages.
|
|
bool decode(const Bytes& body, CredentialsProvision& out);
|
|
|
|
/// @param value Segment to encode.
|
|
/// @return Encoded body bytes.
|
|
Bytes encode(const CredentialsProvision& value);
|
|
|
|
// Counts of ETSI TS 102 941 credentials (root CA / EA-AA certificates / authorization tickets)
|
|
// applied from a CredentialsProvision bundle.
|
|
struct ApplyReport { std::uint8_t roots = 0, authorities = 0, tickets = 0; };
|
|
|
|
|
|
|
|
// Mirrors the security entity's pseudonym-change handshake that the GN Core subscribes to via the
|
|
// SN-IDCHANGE-SUBSCRIBE/-EVENT/-UNSUBSCRIBE primitives at the CORE_SEC interface (ETSI
|
|
// TS 103 836-4-1 clause 10.2.1.4); the pseudonym/Authorization Ticket change itself is governed by
|
|
// ETSI TS 102 941. command is the link's own PREPARE/COMMIT/ABORT/DEREG handshake state, not an
|
|
// ETSI-defined field.
|
|
struct IdChangeEvent {
|
|
std::uint64_t subscription = 0;
|
|
std::uint8_t command = 0; // 0 PREPARE, 1 COMMIT, 2 ABORT, 3 DEREG
|
|
std::uint8_t id[8] = {};
|
|
Bytes subscriber_data;
|
|
};
|
|
|
|
/// @param body Raw message body bytes.
|
|
/// @param out Receives the decoded event.
|
|
/// @return false if malformed.
|
|
bool decode(const Bytes& body, IdChangeEvent& out);
|
|
|
|
/// @param value Event to encode.
|
|
/// @return Encoded body bytes.
|
|
Bytes encode(const IdChangeEvent& value);
|
|
|
|
// The link's encoding of the SN-IDCHANGE-EVENT.response primitive (ETSI TS 103 836-4-1
|
|
// clause 10.2.1.4), acknowledging an IdChangeEvent.
|
|
struct IdChangeEventResponse { std::uint64_t subscription = 0; std::uint8_t return_code = 0; };
|
|
/// @param body Raw message body bytes.
|
|
/// @param out Receives the decoded response.
|
|
/// @return false if malformed.
|
|
bool decode(const Bytes& body, IdChangeEventResponse& out);
|
|
/// @param value Response to encode.
|
|
/// @return Encoded body bytes.
|
|
Bytes encode(const IdChangeEventResponse& value);
|
|
|
|
struct Status {
|
|
std::uint32_t uptime_ms = 0;
|
|
std::uint8_t configured = 0;
|
|
std::uint8_t gn_address[8] = {};
|
|
std::uint8_t identifier[8] = {};
|
|
std::uint8_t change_pending = 0;
|
|
std::uint8_t tickets = 0;
|
|
std::uint32_t signed_messages = 0, refused_no_ticket = 0, refused_change_pending = 0, refused_permission = 0,
|
|
sign_failed = 0, verified = 0, rejected = 0;
|
|
std::uint32_t requests_accepted = 0, requests_refused = 0, indications = 0;
|
|
std::uint32_t radio_submitted = 0, radio_failed = 0, radio_received = 0, radio_dropped = 0;
|
|
std::uint32_t link_rx_frames = 0, link_crc_errors = 0, link_malformed = 0, poti_updates = 0;
|
|
std::uint64_t its_time_ms = 0;
|
|
};
|
|
/// @param body Raw message body bytes.
|
|
/// @param out Receives the decoded status.
|
|
/// @return false if malformed.
|
|
bool decode(const Bytes& body, Status& out);
|
|
/// @param value Status to encode.
|
|
/// @return Encoded body bytes.
|
|
Bytes encode(const Status& value);
|
|
|
|
struct Result {
|
|
Code code = Code::accepted;
|
|
Bytes detail;
|
|
};
|
|
/// @param body Raw message body bytes.
|
|
/// @param out Receives the decoded result.
|
|
/// @return false if malformed.
|
|
bool decode(const Bytes& body, Result& out);
|
|
/// @param value Result to encode.
|
|
/// @return Encoded body bytes.
|
|
Bytes encode(const Result& value);
|
|
|
|
// ---- little-endian helpers shared with the test channel ----------------------------------
|
|
/// @note All multi-byte values are written little-endian.
|
|
class Writer {
|
|
public:
|
|
/// @brief Output buffer thats apppended to
|
|
Bytes out;
|
|
/// @brief Appends a single byte to the output.
|
|
void u8(std::uint8_t v) { out.push_back(v); }
|
|
/// @brief Appends a 16-bit unsigned integer to the output.
|
|
void u16(std::uint16_t v) { out.push_back(v & 0xFF); out.push_back(v >> 8); }
|
|
/// @brief Appends a 32-bit unsigned integer to the output.
|
|
void u32(std::uint32_t v) { for (int i = 0; i < 4; ++i) out.push_back((v >> (8 * i)) & 0xFF); }
|
|
/// @brief Appends a 64-bit unsigned integer to the output.
|
|
void u64(std::uint64_t v) { for (int i = 0; i < 8; ++i) out.push_back((v >> (8 * i)) & 0xFF); }
|
|
/// @brief Appends a 32-bit signed integer to the output.
|
|
void i32(std::int32_t v) { u32(static_cast<std::uint32_t>(v)); }
|
|
/// @brief Appends a sequence of bytes to the output.
|
|
void bytes(const std::uint8_t* p, std::size_t n) { out.insert(out.end(), p, p + n); }
|
|
/// @brief Appends a sequence of bytes to the output.
|
|
void bytes(const Bytes& b) { out.insert(out.end(), b.begin(), b.end()); }
|
|
};
|
|
|
|
/// @note All multi-byte values are read little-endian. Once a read runs past the end of the
|
|
/// buffer, ok() becomes false and all further reads return zero/empty instead of throwing.
|
|
class Reader {
|
|
public:
|
|
/// @param b Buffer to read from; must outlive the Reader.
|
|
/// @param at Starting offset into b.
|
|
Reader(const Bytes& b, std::size_t at = 0) : b_(b), at_(at) {}
|
|
bool ok() const { return ok_; }
|
|
bool done() const { return ok_ && at_ == b_.size(); }
|
|
std::size_t remaining() const { return b_.size() - at_; }
|
|
std::uint8_t u8() { return need(1) ? b_[at_++] : 0; }
|
|
std::uint16_t u16() { if (!need(2)) return 0; std::uint16_t v = b_[at_] | (b_[at_ + 1] << 8); at_ += 2; return v; }
|
|
std::uint32_t u32() { if (!need(4)) return 0; std::uint32_t v = 0; for (int i = 3; i >= 0; --i) v = (v << 8) | b_[at_ + i]; at_ += 4; return v; }
|
|
std::uint64_t u64() { if (!need(8)) return 0; std::uint64_t v = 0; for (int i = 7; i >= 0; --i) v = (v << 8) | b_[at_ + i]; at_ += 8; return v; }
|
|
std::int32_t i32() { return static_cast<std::int32_t>(u32()); }
|
|
bool bytes(std::uint8_t* p, std::size_t n) { if (!need(n)) return false; for (std::size_t i = 0; i < n; ++i) p[i] = b_[at_ + i]; at_ += n; return true; }
|
|
Bytes bytes(std::size_t n) { Bytes r; if (need(n)) { r.assign(b_.begin() + at_, b_.begin() + at_ + n); at_ += n; } return r; }
|
|
Bytes rest() { Bytes r(b_.begin() + at_, b_.end()); at_ = b_.size(); return r; }
|
|
private:
|
|
bool need(std::size_t n) { if (!ok_ || at_ + n > b_.size()) { ok_ = false; return false; } return true; }
|
|
const Bytes& b_;
|
|
std::size_t at_;
|
|
bool ok_ = true;
|
|
};
|
|
|
|
} // namespace microbu::link
|