Sign ITS messages on the ESP32-C5 with vanetza-idf, over USB or BLE

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.
This commit is contained in:
Ashin Walpola
2026-09-23 17:27:54 +02:00
parent 7285fa19b7
commit d2fd222a62
31 changed files with 4837 additions and 1022 deletions
+186
View File
@@ -0,0 +1,186 @@
#include "link_protocol.hpp"
namespace microbu::link {
bool encode(const Message& message, Bytes& out) {
if (header_size + message.body.size() > maximum_message) return false;
out.clear();
out.reserve(header_size + message.body.size());
out.push_back(static_cast<std::uint8_t>(message.header.opcode));
out.push_back(message.header.flags);
out.push_back(message.header.sequence & 0xFF);
out.push_back(message.header.sequence >> 8);
out.insert(out.end(), message.body.begin(), message.body.end());
return true;
}
bool decode(const Bytes& octets, Message& out) {
if (octets.size() < header_size || octets.size() > maximum_message) return false;
out.header.opcode = static_cast<Opcode>(octets[0]);
out.header.flags = octets[1];
out.header.sequence = octets[2] | (octets[3] << 8);
out.body.assign(octets.begin() + header_size, octets.end());
return true;
}
namespace {
void put_area(Writer& w, const DestinationArea& a) {
w.u8(a.shape); w.i32(a.latitude); w.i32(a.longitude); w.u16(a.distance_a); w.u16(a.distance_b); w.u16(a.angle);
}
DestinationArea get_area(Reader& r) {
DestinationArea a;
a.shape = r.u8(); a.latitude = r.i32(); a.longitude = r.i32(); a.distance_a = r.u16(); a.distance_b = r.u16(); a.angle = r.u16();
return a;
}
}
// STATION_CONFIGURE
Bytes encode(const StationConfigure& c) {
Writer w;
w.u8(c.station_type); w.u8(c.security); w.u8(c.address_configuration); w.bytes(c.mid, 6); w.u8(c.beaconing);
w.u16(c.channel_number); w.u8(c.transmit_power_dbm); w.u8(c.radio); w.u8(c.default_traffic_class); w.u8(c.default_lifetime);
return w.out;
}
bool decode(const Bytes& body, StationConfigure& c) {
Reader r(body);
c.station_type = r.u8(); c.security = r.u8(); c.address_configuration = r.u8(); r.bytes(c.mid, 6); c.beaconing = r.u8();
c.channel_number = r.u16(); c.transmit_power_dbm = r.u8(); c.radio = r.u8(); c.default_traffic_class = r.u8(); c.default_lifetime = r.u8();
return r.done() && c.security <= 1 && c.address_configuration <= 1 && c.beaconing <= 1 && c.radio <= 2;
}
// POTI_UPDATE
Bytes encode(const PotiUpdate& p) {
Writer w;
w.u64(p.timestamp_ms); w.i32(p.latitude); w.i32(p.longitude); w.u16(p.semi_major_cm); w.u16(p.semi_minor_cm);
w.u16(p.orientation_deci_degree); w.u8(p.flags); w.i32(p.altitude_cm); w.u16(p.speed_cm_s); w.u16(p.heading_deci_degree);
return w.out;
}
bool decode(const Bytes& body, PotiUpdate& p) {
Reader r(body);
p.timestamp_ms = r.u64(); p.latitude = r.i32(); p.longitude = r.i32(); p.semi_major_cm = r.u16(); p.semi_minor_cm = r.u16();
p.orientation_deci_degree = r.u16(); p.flags = r.u8(); p.altitude_cm = r.i32(); p.speed_cm_s = r.u16(); p.heading_deci_degree = r.u16();
return r.done();
}
// BTP_DATA_REQUEST
Bytes encode(const BtpDataRequest& q) {
Writer w;
w.u8(q.btp_type); w.u16(q.destination_port); w.u16(q.destination_port_info); w.u8(q.gn_packet_transport_type);
w.u8(q.gn_communication_profile); w.u8(q.gn_security_profile); w.u8(q.gn_traffic_class); w.u8(q.gn_maximum_packet_lifetime);
w.u8(q.gn_maximum_hop_limit); w.u16(q.gn_repetition_interval_ms); w.u16(q.gn_repetition_maximum_ms); w.u32(q.its_aid);
w.u8(static_cast<std::uint8_t>(q.permissions.size())); w.bytes(q.permissions);
w.u8(static_cast<std::uint8_t>(q.context.size())); w.bytes(q.context);
if (q.gn_packet_transport_type == 3) put_area(w, q.area.value_or(DestinationArea {}));
w.u16(static_cast<std::uint16_t>(q.fl_sdu.size())); w.bytes(q.fl_sdu);
return w.out;
}
bool decode(const Bytes& body, BtpDataRequest& q) {
Reader r(body);
q.btp_type = r.u8(); q.destination_port = r.u16(); q.destination_port_info = r.u16(); q.gn_packet_transport_type = r.u8();
q.gn_communication_profile = r.u8(); q.gn_security_profile = r.u8(); q.gn_traffic_class = r.u8(); q.gn_maximum_packet_lifetime = r.u8();
q.gn_maximum_hop_limit = r.u8(); q.gn_repetition_interval_ms = r.u16(); q.gn_repetition_maximum_ms = r.u16(); q.its_aid = r.u32();
q.permissions = r.bytes(r.u8());
q.context = r.bytes(r.u8());
q.area.reset();
if (q.gn_packet_transport_type == 3) q.area = get_area(r);
q.fl_sdu = r.bytes(r.u16());
return r.done() && q.btp_type <= 1 && q.permissions.size() <= 31;
}
// BTP_DATA_INDICATION
Bytes encode(const BtpDataIndication& i) {
Writer w;
w.u8(i.btp_type); w.u16(i.destination_port); w.u16(i.destination_port_info); w.u8(i.gn_packet_transport_type);
w.u8(i.gn_traffic_class); w.u8(i.gn_remaining_packet_lifetime); w.u8(i.gn_remaining_hop_limit);
w.bytes(i.source_gn_address, 8); w.u32(i.source_timestamp); w.i32(i.source_latitude); w.i32(i.source_longitude);
w.u8(i.security_report); w.u32(i.its_aid);
w.u8(static_cast<std::uint8_t>(i.permissions.size())); w.bytes(i.permissions);
w.u8(i.certificate_present ? 1 : 0); w.bytes(i.certificate_id, 8);
w.u8(i.area ? 1 : 0);
if (i.area) put_area(w, *i.area);
w.u16(static_cast<std::uint16_t>(i.received_fl_sdu.size())); w.bytes(i.received_fl_sdu);
return w.out;
}
bool decode(const Bytes& body, BtpDataIndication& i) {
Reader r(body);
i.btp_type = r.u8(); i.destination_port = r.u16(); i.destination_port_info = r.u16(); i.gn_packet_transport_type = r.u8();
i.gn_traffic_class = r.u8(); i.gn_remaining_packet_lifetime = r.u8(); i.gn_remaining_hop_limit = r.u8();
r.bytes(i.source_gn_address, 8); i.source_timestamp = r.u32(); i.source_latitude = r.i32(); i.source_longitude = r.i32();
i.security_report = r.u8(); i.its_aid = r.u32();
i.permissions = r.bytes(r.u8());
i.certificate_present = r.u8() != 0; r.bytes(i.certificate_id, 8);
i.area.reset();
if (r.u8()) i.area = get_area(r);
i.received_fl_sdu = r.bytes(r.u16());
return r.done();
}
// CREDENTIALS_PROVISION
Bytes encode(const CredentialsProvision& c) {
Writer w;
w.u16(c.total_length); w.u16(c.offset); w.u8(static_cast<std::uint8_t>(c.segment.size())); w.bytes(c.segment);
return w.out;
}
bool decode(const Bytes& body, CredentialsProvision& c) {
Reader r(body);
c.total_length = r.u16(); c.offset = r.u16(); c.segment = r.bytes(r.u8());
return r.done() && !c.segment.empty() && c.offset + c.segment.size() <= c.total_length;
}
// SF_IDCHANGE_EVENT
Bytes encode(const IdChangeEvent& e) {
Writer w;
w.u64(e.subscription); w.u8(e.command); w.bytes(e.id, 8);
w.u8(static_cast<std::uint8_t>(e.subscriber_data.size())); w.bytes(e.subscriber_data);
return w.out;
}
bool decode(const Bytes& body, IdChangeEvent& e) {
Reader r(body);
e.subscription = r.u64(); e.command = r.u8(); r.bytes(e.id, 8); e.subscriber_data = r.bytes(r.u8());
return r.done() && e.command <= 3;
}
Bytes encode(const IdChangeEventResponse& e) { Writer w; w.u64(e.subscription); w.u8(e.return_code); return w.out; }
bool decode(const Bytes& body, IdChangeEventResponse& e) {
Reader r(body);
e.subscription = r.u64(); e.return_code = r.u8();
return r.done() && e.return_code <= 1;
}
// STATUS
Bytes encode(const Status& s) {
Writer w;
w.u32(s.uptime_ms); w.u8(s.configured); w.bytes(s.gn_address, 8); w.bytes(s.identifier, 8); w.u8(s.change_pending); w.u8(s.tickets);
w.u32(s.signed_messages); w.u32(s.refused_no_ticket); w.u32(s.refused_change_pending); w.u32(s.refused_permission);
w.u32(s.sign_failed); w.u32(s.verified); w.u32(s.rejected);
w.u32(s.requests_accepted); w.u32(s.requests_refused); w.u32(s.indications);
w.u32(s.radio_submitted); w.u32(s.radio_failed); w.u32(s.radio_received); w.u32(s.radio_dropped);
w.u32(s.link_rx_frames); w.u32(s.link_crc_errors); w.u32(s.link_malformed); w.u32(s.poti_updates);
w.u64(s.its_time_ms);
return w.out;
}
bool decode(const Bytes& body, Status& s) {
Reader r(body);
s.uptime_ms = r.u32(); s.configured = r.u8(); r.bytes(s.gn_address, 8); r.bytes(s.identifier, 8); s.change_pending = r.u8(); s.tickets = r.u8();
s.signed_messages = r.u32(); s.refused_no_ticket = r.u32(); s.refused_change_pending = r.u32(); s.refused_permission = r.u32();
s.sign_failed = r.u32(); s.verified = r.u32(); s.rejected = r.u32();
s.requests_accepted = r.u32(); s.requests_refused = r.u32(); s.indications = r.u32();
s.radio_submitted = r.u32(); s.radio_failed = r.u32(); s.radio_received = r.u32(); s.radio_dropped = r.u32();
s.link_rx_frames = r.u32(); s.link_crc_errors = r.u32(); s.link_malformed = r.u32(); s.poti_updates = r.u32();
s.its_time_ms = r.u64();
return r.done();
}
// RESULT
Bytes encode(const Result& res) {
Writer w;
w.u8(static_cast<std::uint8_t>(res.code)); w.u8(static_cast<std::uint8_t>(res.detail.size())); w.bytes(res.detail);
return w.out;
}
bool decode(const Bytes& body, Result& res) {
Reader r(body);
res.code = static_cast<Code>(r.u8()); res.detail = r.bytes(r.u8());
return r.done();
}
} // namespace microbu::link