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:
@@ -0,0 +1,266 @@
|
||||
#pragma once
|
||||
// The ESP32-C5 half of the VRU ITS-S: BTP-B, GeoNetworking, the SN-SAP
|
||||
// security entity with the station's provisioned credentials, and the ITS-G5 access adapter.
|
||||
// Everything here runs on one task (the station task).
|
||||
#include "c5_radio.hpp"
|
||||
#include "link_protocol.hpp"
|
||||
#include <vanetza_idf/stack.hpp>
|
||||
#include <vanetza_idf/security.hpp>
|
||||
#include <vanetza_idf/credentials.hpp>
|
||||
#include <vanetza_idf/backend_mbedtls.hpp>
|
||||
#include <vanetza/common/manual_runtime.hpp>
|
||||
#include <vanetza/common/position_provider.hpp>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
namespace microbu {
|
||||
|
||||
#if CONFIG_MICROBU_TEST_CHANNEL
|
||||
/// Records the software lower tester collects (test channel, test firmware only).
|
||||
struct TestRecord { std::uint8_t kind; link::Bytes bytes; };
|
||||
#endif
|
||||
|
||||
/// @note All public members except the on_*() callback setters must be called from the station task.
|
||||
class Station final : public vanetza_idf::Access, public vanetza::PositionProvider {
|
||||
public:
|
||||
using Indication = std::function<void(const link::BtpDataIndication&)>;
|
||||
using IdEvent = std::function<void(const link::IdChangeEvent&)>;
|
||||
using Disseminated = std::function<void()>;
|
||||
using Received = std::function<void()>;
|
||||
/// MicrOBU: body of one link V2X_RX message (see link_protocol.hpp).
|
||||
using RawIts = std::function<void(const link::Bytes&)>;
|
||||
|
||||
/// @brief Constructs an unconfigured station (call configure() before use).
|
||||
Station();
|
||||
/// @brief Tears down the stack, security entity and radio in dependency order.
|
||||
~Station() override;
|
||||
|
||||
// ---- link primitives (station task) ----
|
||||
/// @brief (Re)builds the stack and security entity for a new station configuration.
|
||||
/// @param config Requested configuration.
|
||||
/// @param detail Receives the assigned GN address, identifier, and ticket count.
|
||||
/// @return Result code.
|
||||
link::Code configure(const link::StationConfigure& config, link::Bytes& detail);
|
||||
|
||||
/// @brief Applies a position/time fix from the phone and advances the ITS clock.
|
||||
/// @param fix Position/time fix to apply.
|
||||
/// @return Result code.
|
||||
link::Code poti(const link::PotiUpdate& fix);
|
||||
|
||||
/// @brief Submits a BTP data request to the stack.
|
||||
/// @param request Request to submit.
|
||||
/// @return Result code.
|
||||
link::Code btp_request(const link::BtpDataRequest& request);
|
||||
|
||||
/// @brief Decodes and stores a credential bundle, rebuilding the security entity if configured.
|
||||
/// @param bundle Raw credential bundle bytes.
|
||||
/// @param report Receives the applied root/authority/ticket counts.
|
||||
/// @return Result code.
|
||||
link::Code provision(const link::Bytes& bundle, link::ApplyReport& report);
|
||||
|
||||
/// @brief Erases stored credentials and rebuilds if security is configured.
|
||||
/// @return Result code.
|
||||
link::Code erase_credentials();
|
||||
|
||||
/// @brief Subscribes to pseudonym-change events.
|
||||
/// @param subscriber_data Opaque data echoed back with events for this subscription.
|
||||
/// @param subscription Receives the assigned subscription handle.
|
||||
/// @return Result code.
|
||||
link::Code subscribe(const link::Bytes& subscriber_data, std::uint64_t& subscription);
|
||||
|
||||
/// @brief Cancels a pseudonym-change subscription.
|
||||
/// @param subscription Handle returned by subscribe().
|
||||
/// @return Result code.
|
||||
link::Code unsubscribe(std::uint64_t subscription);
|
||||
|
||||
/// @brief Resolves a pending PREPARE/COMMIT identity-change event.
|
||||
/// @param subscription Handle the event was delivered for.
|
||||
/// @param return_code Caller's response (accept/reject) to the pending event.
|
||||
/// @return Result code.
|
||||
link::Code event_response(std::uint64_t subscription, bool return_code);
|
||||
|
||||
/// @brief Triggers an immediate pseudonym change.
|
||||
/// @return Result code.
|
||||
link::Code trigger();
|
||||
|
||||
/// @brief Locks the current pseudonym for the given duration.
|
||||
/// @param seconds Lock duration in seconds.
|
||||
/// @param handle Receives the assigned lock handle.
|
||||
/// @return Result code.
|
||||
link::Code lock(std::uint8_t seconds, std::uint64_t& handle);
|
||||
|
||||
/// @brief Releases a pseudonym lock.
|
||||
/// @param handle Handle returned by lock().
|
||||
/// @return Result code.
|
||||
link::Code unlock(std::uint64_t handle);
|
||||
|
||||
/// @return Current station status snapshot.
|
||||
link::Status status();
|
||||
|
||||
/// @brief Advances the ITS clock, runs timers, polls the radio; call every few milliseconds.
|
||||
void tick();
|
||||
|
||||
/// @brief Registers the callback invoked when a BTP data indication arrives.
|
||||
/// @param f Callback to invoke; replaces any previously registered callback.
|
||||
void on_indication(Indication f) { indication_ = std::move(f); }
|
||||
|
||||
/// @brief Registers the callback invoked when a pseudonym-change event fires.
|
||||
/// @param f Callback to invoke; replaces any previously registered callback.
|
||||
void on_id_event(IdEvent f) { id_event_ = std::move(f); }
|
||||
|
||||
/// @brief Registers the callback invoked after a frame is disseminated.
|
||||
/// @param f Callback to invoke; replaces any previously registered callback.
|
||||
void on_disseminated(Disseminated f) { disseminated_ = std::move(f); }
|
||||
|
||||
/// @brief Registers the callback invoked after a frame is received.
|
||||
/// @param f Callback to invoke; replaces any previously registered callback.
|
||||
void on_received(Received f) { received_ = std::move(f); }
|
||||
|
||||
/// @brief MicrOBU: registers the callback for every ITS message heard on air, unwrapped by
|
||||
/// gn_unwrap.c before (and independently of) the stack's security checks. The stack drops
|
||||
/// what it cannot verify (itsGnSnDecapResultHandling is STRICT in vanetza-idf): unsigned
|
||||
/// traffic, and anything signed under a root other than the provisioned demo root, i.e. every
|
||||
/// RSU. This path is how those still reach the phone, exactly as with the previous firmware.
|
||||
/// @param f Callback to invoke with a V2X_RX body; replaces any previously registered callback.
|
||||
void on_raw_its(RawIts f) { raw_its_ = std::move(f); }
|
||||
|
||||
#if CONFIG_MICROBU_TEST_CHANNEL
|
||||
// ---- test channel (software lower tester; not part of the phone interface -- see test_channel.hpp) ----
|
||||
enum class Mirror : std::uint8_t { off = 0, mirror = 1, divert = 2 };
|
||||
/// @brief Sets whether requests/indications are mirrored to, or diverted through, the test channel.
|
||||
/// @param mode Off, mirror (copy) or divert (intercept) mode.
|
||||
void test_mirror(Mirror mode);
|
||||
|
||||
/// @brief Injects a raw GN PDU as if received over the air.
|
||||
/// @param source Source MAC address to report for the injected frame.
|
||||
/// @param destination Destination MAC address to report for the injected frame.
|
||||
/// @param gnpdu Raw GeoNetworking PDU bytes.
|
||||
/// @return Result code.
|
||||
link::Code test_inject(const vanetza::MacAddress& source, const vanetza::MacAddress& destination, link::Bytes gnpdu);
|
||||
/// @brief Submits a raw GeoNetworking request bypassing BTP.
|
||||
/// @param traffic_class GeoNetworking traffic class to submit with.
|
||||
/// @param payload Raw payload bytes.
|
||||
/// @return Result code.
|
||||
link::Code test_gn_request(std::uint8_t traffic_class, link::Bytes payload);
|
||||
/// @brief Pops queued mirror/divert records fitting into budget octets (3 per record header); the rest stays queued.
|
||||
/// @param overflow Receives true if records were dropped because the queue budget was exceeded.
|
||||
/// @param budget Maximum number of octets of records to drain.
|
||||
/// @return Drained records.
|
||||
std::deque<TestRecord> test_drain(bool& overflow, std::size_t budget);
|
||||
/// @brief Clears mirror mode and any queued records.
|
||||
void test_reset();
|
||||
/// @brief Starts the radio if needed and sends a burst of raw test frames.
|
||||
/// @param channel ITS-G5 channel number to transmit on.
|
||||
/// @param power_dbm Transmit power in dBm.
|
||||
/// @param mcs 802.11p modulation and coding scheme index.
|
||||
/// @param count Number of frames to send.
|
||||
/// @param interval_ms Interval between frames in milliseconds.
|
||||
/// @param payload_len Length of each frame's payload in bytes.
|
||||
/// @return Result code.
|
||||
link::Code test_radio_burst(std::uint16_t channel, double power_dbm, unsigned mcs,
|
||||
unsigned count, unsigned interval_ms, std::size_t payload_len);
|
||||
/// @brief Starts the radio if needed and samples the CCA state for duration_ms.
|
||||
/// @param duration_ms Sampling window duration in milliseconds.
|
||||
/// @param detail Receives the sampling statistics.
|
||||
/// @return Result code.
|
||||
link::Code test_cca_sample(unsigned duration_ms, link::Bytes& detail);
|
||||
#endif
|
||||
|
||||
// vanetza_idf::Access
|
||||
/// @brief Submits a frame to the radio (through DCC_ACC when enabled), mirroring/diverting for the test channel.
|
||||
/// @param request Frame and transmit parameters from the access layer.
|
||||
/// @return Result code.
|
||||
vanetza_idf::Result request(vanetza_idf::AlDataRequest request) override;
|
||||
|
||||
// vanetza::PositionProvider
|
||||
/// @return The most recently applied position fix.
|
||||
const vanetza::PositionFix& position_fix() override { return fix_; }
|
||||
|
||||
private:
|
||||
struct Security;
|
||||
struct Clock {
|
||||
bool synchronised = false;
|
||||
std::int64_t base_its_us = 0; // ITS time at base_esp_us
|
||||
std::int64_t base_esp_us = 0;
|
||||
std::int64_t now_us() const;
|
||||
};
|
||||
|
||||
/// @brief (Re)builds the stack and security entity from config_, stored credentials and the clock.
|
||||
/// @return Result code.
|
||||
link::Code build();
|
||||
|
||||
/// @brief Destroys the stack, security entity and runtime in dependency order.
|
||||
void teardown();
|
||||
|
||||
/// @brief Maps a station configuration onto the GeoNetworking MIB.
|
||||
/// @param mib MIB to populate.
|
||||
/// @param config Station configuration to map from.
|
||||
void apply_mib(vanetza_idf::StackConfig& mib, const link::StationConfigure& config) const;
|
||||
|
||||
/// @brief Re-subscribes existing pseudonym-change handles to a freshly built security entity.
|
||||
void resubscribe_id_change();
|
||||
|
||||
/// @brief Pushes the current position fix into the stack, clamped to not precede the station clock.
|
||||
void apply_position();
|
||||
|
||||
/// @brief Samples the hardware CCA counters at the DCC cadence and feeds both DCC entities.
|
||||
void sample_dcc_channel_load();
|
||||
|
||||
#if CONFIG_MICROBU_TEST_CHANNEL
|
||||
/// @brief Queues bytes for the test channel, subject to the mirror-buffer budget.
|
||||
/// @param kind Record kind tag.
|
||||
/// @param bytes Record payload bytes.
|
||||
void record(std::uint8_t kind, link::Bytes bytes);
|
||||
#endif
|
||||
|
||||
/// @brief MicrOBU: unwraps one raw received frame with gn_unwrap.c and hands it to raw_its_.
|
||||
/// @param frame Complete 802.11 frame as captured (FCS included; gn_unwrap reads declared lengths).
|
||||
/// @param rssi Received signal strength, dBm.
|
||||
void forward_raw(const vanetza::ByteBuffer& frame, int rssi);
|
||||
|
||||
/// @brief MicrOBU: transmits an unsecured BTP-B/SHB request the way the previous firmware did,
|
||||
/// with geonet.c's GN header and the Source Position Vector of the last PoTi, through request().
|
||||
/// @param request The phone's request (already checked: configured, clock and fix present).
|
||||
/// @return Result code.
|
||||
link::Code unsecured_request(const link::BtpDataRequest& request);
|
||||
|
||||
/// @brief Translates and forwards a stack BTP indication to the link service and test channel.
|
||||
/// @param indication Indication received from the stack.
|
||||
void deliver(vanetza_idf::BtpIndication indication);
|
||||
|
||||
link::Status counters_;
|
||||
|
||||
std::optional<link::StationConfigure> config_;
|
||||
Clock clock_;
|
||||
vanetza::PositionFix fix_;
|
||||
bool have_fix_ = false;
|
||||
link::PotiUpdate last_poti_; // MicrOBU: the GN Source Position Vector of unsecured_request()
|
||||
std::uint32_t raw_forwarded_ = 0, raw_oversize_ = 0;
|
||||
std::unique_ptr<vanetza::ManualRuntime> runtime_;
|
||||
std::unique_ptr<Security> security_;
|
||||
std::unique_ptr<vanetza_idf::Stack> stack_;
|
||||
std::unique_ptr<C5Radio> radio_;
|
||||
std::unique_ptr<vanetza_idf::AccessStack> access_stack_; // DCC_ACC gate in front of radio_
|
||||
vanetza_idf::AlDataRequest radio_parameters_;
|
||||
std::optional<CcaCounters> dcc_cca_last_; // previous read_cca_counters() sample, for the 100 ms LCBR delta
|
||||
std::int64_t dcc_last_sample_its_us_ = 0;
|
||||
Indication indication_;
|
||||
IdEvent id_event_;
|
||||
Disseminated disseminated_;
|
||||
Received received_;
|
||||
RawIts raw_its_;
|
||||
std::map<std::uint64_t, std::shared_ptr<vanetza_idf::security::IdChangeResponder>> pending_responders_;
|
||||
std::map<std::uint64_t, std::uint64_t> link_subscriptions_; // subscription -> service handle (identity)
|
||||
bool rebuilding_ = false;
|
||||
#if CONFIG_MICROBU_TEST_CHANNEL
|
||||
Mirror mirror_ = Mirror::off;
|
||||
std::deque<TestRecord> records_;
|
||||
std::size_t record_bytes_ = 0;
|
||||
bool overflow_ = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace microbu
|
||||
Reference in New Issue
Block a user