#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 #include #include #include #include namespace microbu::link { using Bytes = std::vector; 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, }; 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 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 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(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(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