Keep the colleague's microbu-esp32c5 tree in this repository
obu-firmware builds against vanetza-idf from microbu-esp32c5/external, but that tree was gitignored, so a clone of this repository could not build the firmware it ships. It is now committed here as ordinary files in its own folder, microbu-esp32c5/: the colleague's commit cf4b99f plus the V2X2MAP bridge's signature verification (--trust) used on the bench. Nothing is fetched from or pushed to the colleague's repository; this repository and its remotes carry everything. The folder's own .gitignore keeps build output, downloaded components and private key material out, as it did there; the committed file set is identical to that repository's tracked files. The ESP32-C5 is still flashed from obu-firmware/, which only takes vanetza-idf from microbu-esp32c5/, so the two stay separate folders. FLASHING.md says how to take a newer version of the colleague's tree (copy it over the folder, rebuild, test, commit).
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user