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.
187 lines
8.7 KiB
C++
187 lines
8.7 KiB
C++
#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
|