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
+130
View File
@@ -0,0 +1,130 @@
// Station's test-channel API (software lower tester; not part of the phone interface, see
// test_channel.hpp). Kept in its own translation unit so the productive station.cpp stays free
// of test-only code; compiles to nothing when CONFIG_MICROBU_TEST_CHANNEL is off.
#include "station.hpp"
#if CONFIG_MICROBU_TEST_CHANNEL
#include <vanetza_idf/nf_sap.hpp>
#include <vanetza_idf/sf_sap.hpp>
#include <esp_log.h>
#include "c5_radio.hpp"
namespace microbu {
using namespace vanetza_idf;
namespace gn = vanetza::geonet;
namespace {
const char* TAG = "station";
link::Code code(Result result) { return static_cast<link::Code>(result); }
}
void Station::test_mirror(Mirror mode) { mirror_ = mode; }
link::Code Station::test_inject(const vanetza::MacAddress& source, const vanetza::MacAddress& destination, link::Bytes gnpdu) {
if (!stack_) return link::Code::not_configured;
tick();
AlDataIndication indication;
indication.source = source;
indication.destination = destination;
indication.channel_number = radio_parameters_.channel_number;
indication.data = std::move(gnpdu);
return code(stack_->indicate(std::move(indication)));
}
link::Code Station::test_gn_request(std::uint8_t traffic_class, link::Bytes payload) {
if (!stack_) return link::Code::not_configured;
if (!clock_.synchronised || !have_fix_) return link::Code::rejected;
tick();
GnRequest request;
request.traffic_class = gn::TrafficClass(traffic_class);
request.data = std::move(payload);
return code(stack_->request(std::move(request)));
}
std::deque<TestRecord> Station::test_drain(bool& overflow, std::size_t budget) {
std::deque<TestRecord> out;
std::size_t used = 0;
while (!records_.empty() && used + records_.front().bytes.size() + 3 <= budget) {
used += records_.front().bytes.size() + 3;
record_bytes_ -= records_.front().bytes.size();
out.push_back(std::move(records_.front()));
records_.pop_front();
}
overflow = overflow_ && records_.empty();
if (records_.empty()) overflow_ = false;
return out;
}
void Station::test_reset() {
mirror_ = Mirror::off;
records_.clear();
record_bytes_ = 0;
overflow_ = false;
}
link::Code Station::test_radio_burst(std::uint16_t channel, double power_dbm, unsigned mcs,
unsigned count, unsigned interval_ms, std::size_t payload_len) {
if (!radio_) {
// Automatically start the radio for testing if not yet started
C5RadioConfig rc;
rc.channel_number = channel;
rc.transmit_power_dbm = power_dbm;
rc.laboratory_transmission = true;
radio_ = std::make_unique<C5Radio>(rc);
const auto error = radio_->start();
if (error != ESP_OK) {
ESP_LOGE(TAG, "radio start failed for test burst: %s", esp_err_to_name(error));
radio_.reset();
return link::Code::rejected;
}
}
const auto err = radio_->transmit_burst(channel, power_dbm, mcs, count, interval_ms, payload_len);
if (err == ESP_OK) {
counters_.radio_submitted += count;
if (disseminated_) disseminated_();
return link::Code::accepted;
}
if (err == ESP_ERR_INVALID_ARG) return link::Code::invalid_argument;
if (err == ESP_ERR_NO_MEM) return link::Code::resource_limit;
return link::Code::rejected;
}
link::Code Station::test_cca_sample(unsigned duration_ms, link::Bytes& detail) {
// Capped: this busy-loops with interrupts otherwise free, so keep it short enough that
// the task watchdog and the WiFi/BLE stack's own housekeeping are not starved for long.
duration_ms = std::min(duration_ms, 500u);
if (!radio_) {
C5RadioConfig rc; // default channel 180 / 10 dBm is fine: this probes PHY polling
rc.laboratory_transmission = true; // capability, not a specific channel's RF content
radio_ = std::make_unique<C5Radio>(rc);
const auto error = radio_->start();
if (error != ESP_OK) {
ESP_LOGE(TAG, "radio start failed for CCA sample: %s", esp_err_to_name(error));
radio_.reset();
return link::Code::rejected;
}
}
const auto result = radio_->sample_cca(duration_ms);
link::Writer w;
w.u32(result.samples);
w.u32(result.duration_us);
w.u32(result.min_delta_us);
w.u32(result.max_delta_us);
w.u32(result.busy_count);
w.i32(result.first_cca);
w.i32(result.last_cca);
w.i32(result.noise_floor_dbm);
w.i32(result.cca_total_cycles_delta);
w.i32(result.cca_busy_cycles_delta);
w.i32(result.cca_status);
detail = w.out;
return result.samples > 0 ? link::Code::accepted : link::Code::rejected;
}
void Station::record(std::uint8_t kind, link::Bytes bytes) {
if (mirror_ == Mirror::off) return;
if (record_bytes_ + bytes.size() > 8192 || records_.size() >= 32) { overflow_ = true; return; }
record_bytes_ += bytes.size();
records_.push_back(TestRecord {kind, std::move(bytes)});
}
} // namespace microbu
#endif // CONFIG_MICROBU_TEST_CHANNEL