Files
MicrOBU/microbu-esp32c5/firmware/main/link_protocol.hpp
T

358 lines
15 KiB
C++
Raw Normal View History

#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,
};
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