#include "station.hpp" #include #include #include #include #include #include #include #include #include #include #include "c5_radio.hpp" extern "C" { #include "geonet.h" #include "gn_unwrap.h" } #include #include #include #include namespace microbu { using namespace vanetza_idf; using vanetza::ByteBuffer; namespace gn = vanetza::geonet; namespace { const char* TAG = "station"; link::Code code(Result result) { return static_cast(result); } // Numbering of the link's "Packet transport type" field matches the TRANSP_CORE.request/.indication // service primitive parameter of ETSI TS 103 836-4-1 annex J.2/J.4 (0 GUC, 1 SHB, 2 TSB, 3 GBC, // 4 GAC); it does not reuse the HT wire encoding of clause 9.7.4 table 9, which numbers these // differently (GEOUNICAST=2, GEOANYCAST=3, GEOBROADCAST=4, TSB=5, no separate SHB value there). std::uint8_t transport_number(gn::TransportType t) { switch (t) { case gn::TransportType::GUC: return 0; case gn::TransportType::SHB: return 1; case gn::TransportType::TSB: return 2; case gn::TransportType::GBC: return 3; case gn::TransportType::GAC: return 4; } return 0; } std::optional transport_from(std::uint8_t n) { switch (n) { case 0: return gn::TransportType::GUC; case 1: return gn::TransportType::SHB; case 2: return gn::TransportType::TSB; case 3: return gn::TransportType::GBC; case 4: return gn::TransportType::GAC; } return std::nullopt; } // Wire DestinationArea -> vanetza gn::Area (GBC destination). gn::Area area_from(const link::DestinationArea& a) { gn::Area area; switch (a.shape) { case 0: { gn::Circle c; c.r = double(a.distance_a) * vanetza::units::si::meter; area.shape = c; break; } case 1: { gn::Rectangle r; r.a = double(a.distance_a) * vanetza::units::si::meter; r.b = double(a.distance_b) * vanetza::units::si::meter; area.shape = r; break; } default: { gn::Ellipse e; e.a = double(a.distance_a) * vanetza::units::si::meter; e.b = double(a.distance_b) * vanetza::units::si::meter; area.shape = e; break; } } area.position = gn::GeodeticPosition(a.latitude / 1.0e7 * vanetza::units::degree, a.longitude / 1.0e7 * vanetza::units::degree); area.angle = vanetza::units::Angle(double(a.angle) * vanetza::units::degree); return area; } // gn::Area -> wire DestinationArea (the inverse of area_from). struct AreaToLink : boost::static_visitor { const gn::Area& area; explicit AreaToLink(const gn::Area& a) : area(a) {} link::DestinationArea common() const { link::DestinationArea out; out.latitude = static_cast(area.position.latitude.value() * 1.0e7); out.longitude = static_cast(area.position.longitude.value() * 1.0e7); out.angle = static_cast(area.angle.value()); return out; } link::DestinationArea operator()(const gn::Circle& c) const { auto o = common(); o.shape = 0; o.distance_a = c.r.value(); return o; } link::DestinationArea operator()(const gn::Rectangle& r) const { auto o = common(); o.shape = 1; o.distance_a = r.a.value(); o.distance_b = r.b.value(); return o; } link::DestinationArea operator()(const gn::Ellipse& e) const { auto o = common(); o.shape = 2; o.distance_a = e.a.value(); o.distance_b = e.b.value(); return o; } }; // 0 unsecured (no security envelope), 1 + VerificationReport ordinal otherwise struct ReportToLink : boost::static_visitor { ReportToLink() = default; std::uint8_t operator()(boost::blank) const { return 0; } std::uint8_t operator()(vanetza::security::VerificationReport r) const { return 1 + static_cast(r); } }; } // Credentials, backend, trust configuration, ticket pool and the entity built on them. struct Station::Security { BackendMbedTls backend; security::TrustConfiguration trust; security::CertificatePool pool {backend}; std::unique_ptr entity; bool loaded = false; security::ApplyReport report; }; std::int64_t Station::Clock::now_us() const { return base_its_us + (esp_timer_get_time() - base_esp_us); } Station::Station() = default; Station::~Station() { teardown(); } void Station::teardown() { rebuilding_ = true; // the DEREG of TS 102 723-8 Figure 15 is ours, not the phone's stack_.reset(); // router timers before the runtime // AccessStack::Dcc's Limeric holds a reference to *runtime_ and calls into it from its // destructor (Runtime::cancel): it must be destroyed before runtime_ is. access_stack_.reset(); security_.reset(); runtime_.reset(); dcc_cca_last_.reset(); dcc_last_sample_its_us_ = 0; pending_responders_.clear(); rebuilding_ = false; } // TS 102 894-2 station config -> the GeoNetworking MIB (TS 103 836-4-1). void Station::apply_mib(StackConfig& config, const link::StationConfigure& c) const { config.mib.itsGnLocalGnAddr.mid(vanetza::MacAddress {c.mid[0], c.mid[1], c.mid[2], c.mid[3], c.mid[4], c.mid[5]}); config.mib.itsGnLocalGnAddr.station_type(static_cast(c.station_type & 0x1F)); config.mib.itsGnLocalGnAddr.is_manually_configured(c.address_configuration == 0); config.mib.itsGnLocalAddrConfMethod = c.address_configuration == 1 ? gn::AddrConfMethod::Anonymous : gn::AddrConfMethod::Auto; config.mib.itsGnSecurity = c.security == 1; config.mib.vanetzaDisableBeaconing = c.beaconing == 0; config.mib.itsGnDefaultTrafficClass = gn::TrafficClass(c.default_traffic_class); gn::Lifetime lifetime; lifetime.raw(c.default_lifetime); config.mib.itsGnDefaultPacketLifetime = lifetime; config.mib.itsGnMaxPacketLifetime = lifetime < config.mib.itsGnMaxPacketLifetime ? config.mib.itsGnMaxPacketLifetime : lifetime; config.radio_parameters.channel_number = c.channel_number; config.radio_parameters.transmit_power_dbm = c.transmit_power_dbm; } // Carry the phone's SF-SAP pseudonym-change subscriptions (TS 102 723-8 clause 6.3) // over to the identity manager of a freshly rebuilt security entity. void Station::resubscribe_id_change() { std::map renewed; for (const auto& [handle, old_service] : link_subscriptions_) { (void)old_service; renewed[handle] = security_->entity->id_change().subscribe( [this, handle](security::IdChangeCommand command, const security::Identifier& id, const ByteBuffer& data, std::shared_ptr responder) { link::IdChangeEvent event; event.subscription = handle; event.command = static_cast(command); std::copy(id.begin(), id.end(), event.id); event.subscriber_data = data; if (responder) pending_responders_[handle] = responder; if (id_event_ && !rebuilding_) id_event_(event); }); } link_subscriptions_ = renewed; } // (Re)build stack and security entity from config_, the stored credentials and the current clock. link::Code Station::build() { teardown(); const auto& c = *config_; runtime_ = std::make_unique(vanetza::Clock::time_point(std::chrono::microseconds(clock_.synchronised ? clock_.now_us() : 0))); StackConfig config; apply_mib(config, c); radio_parameters_ = config.radio_parameters; vanetza::security::SecurityEntity* entity = nullptr; if (config.mib.itsGnSecurity) { security_ = std::make_unique(); security::Credentials credentials; security::NvsCredentialStore store; const auto load = store.load(credentials); if (load == Result::accepted) { security_->report = security::apply(credentials, security_->trust, security_->pool); security_->loaded = security_->report.result == Result::accepted && !security_->pool.empty(); ESP_LOGI(TAG, "credentials from NVS: %u roots, %u authorities, %u tickets (result %d)", unsigned(security_->report.roots), unsigned(security_->report.authorities), unsigned(security_->report.tickets), int(security_->report.result)); } else if (load == Result::rejected) { ESP_LOGW(TAG, "no credentials in NVS: secured requests will be refused until provisioned"); } else { ESP_LOGE(TAG, "stored credentials do not decode (result %d)", int(load)); } security_->entity = std::make_unique(*runtime_, *this, security_->backend, security_->pool, security_->trust); entity = security_->entity.get(); resubscribe_id_change(); } stack_ = std::make_unique(config, *runtime_, *this, entity); stack_->on_receive([this](BtpIndication indication) { deliver(std::move(indication)); }); #if CONFIG_MICROBU_TEST_CHANNEL stack_->on_receive_gn([this](GnIndication indication) { record(3, std::move(indication.data)); }); #endif stack_->on_access_result([](Result) { /* counted in Station::request, the adapter itself */ }); // DCC_ACC gate in front of whatever radio_ currently is (rebuilt here rather than in // Station::configure() because it must reference the fresh runtime_, not a torn-down one -- // see the destruction-order comment in Station::teardown()). if (radio_) { access_stack_ = std::make_unique(*radio_); access_stack_->enable_dcc(*runtime_); stack_->report_tx_power(static_cast(std::lround(radio_parameters_.transmit_power_dbm))); } dcc_cca_last_.reset(); dcc_last_sample_its_us_ = clock_.synchronised ? clock_.now_us() : 0; if (have_fix_) apply_position(); return link::Code::accepted; } link::Code Station::configure(const link::StationConfigure& c, link::Bytes& detail) { const bool radio_changed = !config_ || config_->radio != c.radio || config_->channel_number != c.channel_number || config_->transmit_power_dbm != c.transmit_power_dbm; if (radio_changed) { radio_.reset(); if (c.radio != 0) { C5RadioConfig rc; rc.channel_number = c.channel_number; rc.transmit_power_dbm = c.transmit_power_dbm; rc.laboratory_transmission = c.radio == 2; radio_ = std::make_unique(rc); const auto error = radio_->start(); if (error != ESP_OK) { ESP_LOGE(TAG, "radio start failed: %s", esp_err_to_name(error)); radio_.reset(); return link::Code::rejected; } ESP_LOGI(TAG, "radio on channel %u, %s", unsigned(c.channel_number), c.radio == 2 ? "transmit and receive" : "receive only"); } } config_ = c; link_subscriptions_.clear(); // a configuration starts a phone session: earlier subscriptions are void counters_ = link::Status {}; // and the session's counters start at zero const auto result = build(); if (result != link::Code::accepted) return result; link::Writer w; ByteBuffer address; gn::serialize_into_buffer(stack_->address(), address); w.bytes(address); security::Identifier identifier {}; if (security_ && security_->entity) identifier = security_->entity->id_change().current_identifier(); w.bytes(identifier.data(), 8); w.u8(security_ && security_->loaded ? 1 : 0); w.u8(security_ ? static_cast(std::min(security_->pool.size(), 255)) : 0); detail = w.out; counters_.configured = 1; return link::Code::accepted; } void Station::apply_position() { if (!stack_ || !have_fix_) return; // never ahead of the station clock (Stack::update_position rejects that) const auto now = runtime_->now(); if (fix_.timestamp > now) fix_.timestamp = now; const auto result = stack_->update_position(fix_); if (result != Result::accepted) ESP_LOGW(TAG, "position rejected: %d", int(result)); } link::Code Station::poti(const link::PotiUpdate& p) { if (p.latitude < -900000000 || p.latitude > 900000000 || p.longitude < -1800000000 || p.longitude > 1800000000) return link::Code::invalid_argument; // ITS clock: the phone's PoTi time is the reference; the local esp_timer runs between updates. const std::int64_t its_us = static_cast(p.timestamp_ms) * 1000; const std::int64_t esp_us = esp_timer_get_time(); link::Code result = link::Code::accepted; if (!clock_.synchronised) { clock_ = Clock {true, its_us, esp_us}; if (config_) build(); // the runtime started at 0: restart it at real time } else { const std::int64_t drift = its_us - clock_.now_us(); if (drift >= 0) { clock_.base_its_us = its_us; clock_.base_esp_us = esp_us; // forward: step immediately } else if (drift > -1000000) { // small backward drift: hold the local clock, it catches up with the next updates } else { // the reference moved back by more than a second: restart the station at that time ESP_LOGW(TAG, "ITS time moved back by %lld ms, restarting the station", static_cast(-drift / 1000)); clock_ = Clock {true, its_us, esp_us}; if (config_) build(); result = link::Code::time_regression; } } fix_ = vanetza::PositionFix {}; fix_.timestamp = vanetza::Clock::time_point(std::chrono::microseconds(std::min(its_us, clock_.now_us()))); fix_.latitude = p.latitude / 1.0e7 * vanetza::units::degree; fix_.longitude = p.longitude / 1.0e7 * vanetza::units::degree; fix_.confidence.semi_major = p.semi_major_cm / 100.0 * vanetza::units::si::meter; fix_.confidence.semi_minor = p.semi_minor_cm / 100.0 * vanetza::units::si::meter; fix_.confidence.orientation = p.orientation_deci_degree / 10.0 * vanetza::units::true_north_degrees; fix_.speed = (p.has_speed() ? p.speed_cm_s / 100.0 : 0.0) * vanetza::units::si::meters_per_second; fix_.course = (p.has_heading() ? p.heading_deci_degree / 10.0 : 0.0) * vanetza::units::true_north_degrees; if (p.has_altitude()) fix_.altitude = vanetza::ConfidentQuantity(p.altitude_cm / 100.0 * vanetza::units::si::meter); have_fix_ = true; last_poti_ = p; ++counters_.poti_updates; tick(); apply_position(); return result; } link::Code Station::btp_request(const link::BtpDataRequest& q) { if (!stack_) return link::Code::not_configured; if (!clock_.synchronised || !have_fix_) { ++counters_.requests_refused; return link::Code::rejected; } NF_SAP::BTP_DATA_request request; request.fl_sdu = q.fl_sdu; request.length = q.fl_sdu.size(); request.btp_type = q.btp_type == 0 ? BtpType::a : BtpType::b; request.destination_port = q.destination_port; if (request.btp_type == BtpType::a) request.source_port = q.destination_port_info; else request.destination_port_info = q.destination_port_info; const auto transport = transport_from(q.gn_packet_transport_type); if (!transport) { ++counters_.requests_refused; return link::Code::invalid_argument; } request.gn_packet_transport_type = *transport; switch (q.gn_communication_profile) { case 0: request.gn_communication_profile = gn::CommunicationProfile::Unspecified; break; case 1: request.gn_communication_profile = gn::CommunicationProfile::ITS_G5; break; case 2: request.gn_communication_profile = gn::CommunicationProfile::LTE_V2X; break; default: ++counters_.requests_refused; return link::Code::invalid_argument; } // MicrOBU: the colleague's firmware refuses unsecured requests outright. Here the phone's // "Sign outgoing messages" setting decides, per request; 0 means the station's configuration. if (q.gn_security_profile > 2) { ++counters_.requests_refused; return link::Code::invalid_argument; } const bool secured = q.gn_security_profile == 2 || (q.gn_security_profile == 0 && config_->security == 1); if (!secured) return unsecured_request(q); request.gn_security_profile = NF_SAP::SecurityProfile::SECURED; request.gn_traffic_class = q.gn_traffic_class == 0xFF ? stack_->config().mib.itsGnDefaultTrafficClass : gn::TrafficClass(q.gn_traffic_class); if (q.gn_maximum_packet_lifetime != 0xFF) { gn::Lifetime l; l.raw(q.gn_maximum_packet_lifetime); request.gn_maximum_packet_lifetime = l; } if (q.gn_maximum_hop_limit) request.gn_maximum_hop_limit = q.gn_maximum_hop_limit; if (q.gn_repetition_interval_ms) { gn::DataRequest::Repetition repetition; repetition.interval = (q.gn_repetition_interval_ms / 1000.0) * vanetza::units::si::seconds; repetition.maximum = (q.gn_repetition_maximum_ms / 1000.0) * vanetza::units::si::seconds; request.gn_repetition = repetition; } if (*transport == gn::TransportType::GBC) { if (!q.area) { ++counters_.requests_refused; return link::Code::invalid_argument; } request.gn_destination_address = area_from(*q.area); } request.its_aid = q.its_aid; request.permissions = q.permissions; request.context_information = q.context; tick(); // the packet carries the current time and position ESP_LOGI(TAG, "heap before sign: free=%lu min=%lu dma=%lu stack_hwm=%u", (unsigned long)esp_get_free_heap_size(), (unsigned long)esp_get_minimum_free_heap_size(), (unsigned long)heap_caps_get_free_size(MALLOC_CAP_DMA), (unsigned)uxTaskGetStackHighWaterMark(nullptr)); Result result = Result::rejected; try { result = NF_SAP::BTP_DATA_request_submit(*stack_, std::move(request)); if (result == Result::accepted) ++counters_.requests_accepted; else ++counters_.requests_refused; } catch (const std::exception& e) { ESP_LOGE(TAG, "BTP_DATA_request_submit exception: %s", e.what()); ++counters_.requests_refused; return link::Code::rejected; } return code(result); } void Station::forward_raw(const ByteBuffer& frame, int rssi) { if (!raw_its_) return; // Most captured frames are not ITS traffic this handles; false is the common case, not an error. gn_rx_t rx; if (!gn_unwrap_its(frame.data(), static_cast(frame.size()), &rx)) return; constexpr std::size_t prefix = 14; if (rx.truncated || rx.payload_len <= 0 || static_cast(rx.payload_len) + prefix + link::header_size > link::maximum_message) { ++raw_oversize_; static std::int64_t last_report = 0; // one line per 10 s at most, the counter has the rest const auto now = esp_timer_get_time(); if (now - last_report > 10000000) { last_report = now; ESP_LOGW(TAG, "V2X_RX: port %u message of %d bytes does not fit a link message (%lu dropped so far)", unsigned(rx.btp_dest_port), rx.payload_len, (unsigned long)raw_oversize_); } return; } link::Writer w; w.u16(rx.btp_dest_port); w.u8(static_cast(static_cast(std::clamp(rssi, -128, 127)))); w.u8((rx.has_geo_area ? 0x01 : 0) | (rx.signed_unverified ? 0x02 : 0)); w.i32(rx.geo_area_lat_tenmicrodeg); w.i32(rx.geo_area_lon_tenmicrodeg); w.u16(rx.geo_area_distance_a_m); w.bytes(rx.payload, static_cast(rx.payload_len)); ++raw_forwarded_; raw_its_(w.out); } link::Code Station::unsecured_request(const link::BtpDataRequest& q) { // geonet.c builds exactly one packet shape: BTP-B inside a GN single-hop broadcast. That is // what CAM and VAM are; anything else still needs the stack, which here only signs. if (q.btp_type != 1 || q.gn_packet_transport_type != 1) { ++counters_.requests_refused; return link::Code::unsupported; } gn_lpv_t lpv {}; std::copy(std::begin(config_->mid), std::end(config_->mid), lpv.mac); lpv.station_type = config_->station_type; lpv.pai = last_poti_.pai(); lpv.tst_ms = static_cast(last_poti_.timestamp_ms); // TimestampIts mod 2^32 lpv.lat_tenmicrodeg = last_poti_.latitude; lpv.lon_tenmicrodeg = last_poti_.longitude; lpv.speed_cms = static_cast(last_poti_.has_speed() ? std::min(last_poti_.speed_cm_s, 16383) : 0); lpv.heading_decideg = last_poti_.has_heading() ? last_poti_.heading_deci_degree : 0; ByteBuffer pdu(q.fl_sdu.size() + 64); const int length = geonet_wrap_shb(q.fl_sdu.data(), static_cast(q.fl_sdu.size()), &lpv, q.destination_port, pdu.data(), pdu.size()); if (length <= 0) { ++counters_.requests_refused; return link::Code::invalid_argument; } pdu.resize(static_cast(length)); AlDataRequest frame = radio_parameters_; frame.source = vanetza::MacAddress {lpv.mac[0], lpv.mac[1], lpv.mac[2], lpv.mac[3], lpv.mac[4], lpv.mac[5]}; frame.destination = vanetza::cBroadcastMacAddress; frame.data = std::move(pdu); const auto result = request(std::move(frame)); if (result == Result::accepted) ++counters_.requests_accepted; else ++counters_.requests_refused; return code(result); } void Station::deliver(BtpIndication received) { auto indication = NF_SAP::BTP_DATA_indication_from(std::move(received)); ++counters_.indications; #if CONFIG_MICROBU_TEST_CHANNEL // The test channel sees every BTP indication as well (kind 2, the HIL SUT layout). if (mirror_ != Mirror::off) { link::Writer w; w.u8(indication.btp_type == BtpType::b ? 1 : 0); const auto port = indication.destination_port; const auto info = indication.source_port.value_or(indication.destination_port_info.value_or(0)); w.out.push_back(port >> 8); w.out.push_back(port & 0xFF); // big-endian like hil_sut.cpp w.out.push_back(info >> 8); w.out.push_back(info & 0xFF); w.bytes(indication.received_fl_sdu); record(2, w.out); } #endif if (!indication_) return; link::BtpDataIndication out; out.btp_type = indication.btp_type == BtpType::b ? 1 : 0; out.destination_port = indication.destination_port; out.destination_port_info = indication.source_port.value_or(indication.destination_port_info.value_or(0)); out.gn_packet_transport_type = transport_number(indication.gn.transport_type); out.gn_traffic_class = indication.gn.traffic_class.raw(); out.gn_remaining_packet_lifetime = indication.gn.remaining_packet_lifetime ? indication.gn.remaining_packet_lifetime->raw() : 0xFF; out.gn_remaining_hop_limit = indication.gn.remaining_hop_limit ? static_cast(std::min(*indication.gn.remaining_hop_limit, 254u)) : 0xFF; ByteBuffer address; gn::serialize_into_buffer(indication.gn.source_position.gn_addr, address); std::copy_n(address.begin(), std::min(address.size(), 8), out.source_gn_address); out.source_timestamp = indication.gn.source_position.timestamp.raw(); out.source_latitude = indication.gn.source_position.latitude.value(); out.source_longitude = indication.gn.source_position.longitude.value(); out.security_report = boost::apply_visitor(ReportToLink {}, indication.gn.security_report); out.its_aid = indication.gn.its_aid.value_or(0); if (indication.gn.permissions) out.permissions = *indication.gn.permissions; if (indication.gn.certificate_id) { out.certificate_present = true; std::copy(indication.gn.certificate_id->begin(), indication.gn.certificate_id->end(), out.certificate_id); } if (const auto* area = boost::get(&indication.gn.destination)) out.area = boost::apply_visitor(AreaToLink {*area}, area->shape); out.received_fl_sdu = std::move(indication.received_fl_sdu); indication_(out); } link::Code Station::provision(const link::Bytes& bundle, link::ApplyReport& report) { try { security::Credentials credentials; if (!security::decode(bundle, credentials) || credentials.roots.empty() || credentials.tickets.empty()) { return link::Code::invalid_argument; } security::NvsCredentialStore store; const auto saved = store.save(credentials); if (saved != Result::accepted) return code(saved); if (config_ && config_->security) { const auto result = build(); if (result != link::Code::accepted) return result; if (security_) { report.roots = security_->report.roots; report.authorities = security_->report.authorities; report.tickets = security_->report.tickets; ESP_LOGI(TAG, "credentials provisioned: %u roots, %u authorities, %u tickets", unsigned(report.roots), unsigned(report.authorities), unsigned(report.tickets)); } return link::Code::accepted; } report.roots = credentials.roots.size(); report.authorities = credentials.authorities.size(); report.tickets = credentials.tickets.size(); return link::Code::accepted; } catch (const std::bad_alloc&) { ESP_LOGE(TAG, "out of memory provisioning credentials"); return link::Code::resource_limit; } catch (const std::exception& e) { ESP_LOGE(TAG, "failed provisioning credentials: %s", e.what()); return link::Code::invalid_argument; } } link::Code Station::erase_credentials() { security::NvsCredentialStore store; const auto erased = store.erase(); if (erased != Result::accepted && erased != Result::rejected) return code(erased); if (config_ && config_->security) return build(); return link::Code::accepted; } link::Code Station::subscribe(const link::Bytes& subscriber_data, std::uint64_t& subscription) { if (!security_ || !security_->entity) return link::Code::security_unavailable; static std::uint64_t next_handle = 1; const auto handle = next_handle++; const auto service = security_->entity->id_change().subscribe( [this, handle](security::IdChangeCommand command, const security::Identifier& id, const ByteBuffer& data, std::shared_ptr responder) { link::IdChangeEvent event; event.subscription = handle; event.command = static_cast(command); std::copy(id.begin(), id.end(), event.id); event.subscriber_data = data; if (responder) pending_responders_[handle] = responder; if (id_event_ && !rebuilding_) id_event_(event); }, subscriber_data); link_subscriptions_[handle] = service; subscription = handle; return link::Code::accepted; } link::Code Station::unsubscribe(std::uint64_t subscription) { const auto it = link_subscriptions_.find(subscription); if (it == link_subscriptions_.end()) return link::Code::invalid_argument; Result result = Result::accepted; if (security_ && security_->entity) result = security_->entity->id_change().unsubscribe(it->second); link_subscriptions_.erase(it); pending_responders_.erase(subscription); return code(result); } link::Code Station::event_response(std::uint64_t subscription, bool return_code) { const auto it = pending_responders_.find(subscription); if (it == pending_responders_.end()) return link::Code::invalid_argument; it->second->respond(return_code); pending_responders_.erase(it); return link::Code::accepted; } link::Code Station::trigger() { if (!security_ || !security_->entity) return link::Code::security_unavailable; return code(security_->entity->id_change().trigger()); } link::Code Station::lock(std::uint8_t seconds, std::uint64_t& handle) { if (!security_ || !security_->entity) return link::Code::security_unavailable; handle = security_->entity->id_change().lock(seconds); return link::Code::accepted; } link::Code Station::unlock(std::uint64_t handle) { if (!security_ || !security_->entity) return link::Code::security_unavailable; return code(security_->entity->id_change().unlock(handle)); } void Station::tick() { // MicrOBU: the stack needs the ITS clock, which only the phone's first PoTi sets. The radio // does not: its queue (16 frames) must be drained and raw frames forwarded from the moment the // station is configured. The colleague's version returned early here, so a phone that had // configured the station but not yet sent a PoTi (no trip recording, no pinger) received // nothing and every frame on air overflowed the queue into radio_dropped. const bool running = stack_ && clock_.synchronised; if (running) { const auto now = vanetza::Clock::time_point(std::chrono::microseconds(clock_.now_us())); if (now > runtime_->now()) stack_->advance(now); } if (radio_) { radio_->poll([this, running](AlDataIndication indication) { ++counters_.radio_received; if (received_) received_(); if (running && stack_) stack_->indicate(std::move(indication)); }, [this](const ByteBuffer& frame, int rssi, std::uint32_t) { forward_raw(frame, rssi); }); counters_.radio_dropped = radio_->dropped_frames(); } if (running) sample_dcc_channel_load(); } // Sample the hardware LCBR counters at the T_Cbr cadence (TS 102 687 clause 5.4 / // TS 103 836-4-2 clause 5.2, both 100 ms) and feed both DCC entities. The first sample after // (re)build only establishes the baseline counter snapshot -- a CBR delta needs two samples. void Station::sample_dcc_channel_load() { if (!radio_ || !access_stack_) return; const auto its_us = clock_.now_us(); if (its_us - dcc_last_sample_its_us_ < 100000) return; const auto counters = radio_->read_cca_counters(); if (dcc_cca_last_) { const vanetza::dcc::ChannelLoad local_cbr(C5Radio::calculate_cbr(counters, *dcc_cca_last_)); stack_->report_local_channel_load(local_cbr); const auto global_cbr = stack_->global_channel_busy_ratio(); // Consume Release-2 CBR_G for DCC_ACC when available, else LCBR. access_stack_->report_channel_load(global_cbr ? *global_cbr : local_cbr); } dcc_cca_last_ = counters; dcc_last_sample_its_us_ = its_us; } link::Status Station::status() { link::Status s = counters_; s.uptime_ms = static_cast(esp_timer_get_time() / 1000); s.configured = stack_ ? 1 : 0; if (stack_) { ByteBuffer address; gn::serialize_into_buffer(stack_->address(), address); std::copy_n(address.begin(), std::min(address.size(), 8), s.gn_address); s.change_pending = stack_->identity_change_pending() ? 1 : 0; } if (security_ && security_->entity) { const auto id = security_->entity->id_change().current_identifier(); std::copy(id.begin(), id.end(), s.identifier); s.tickets = static_cast(std::min(security_->pool.size(), 255)); const auto& st = security_->entity->statistics(); s.signed_messages = st.signed_messages; s.refused_no_ticket = st.refused_no_ticket; s.refused_change_pending = st.refused_change_pending; s.refused_permission = st.refused_permission; s.sign_failed = st.failed; s.verified = st.verified; s.rejected = st.rejected_profile + st.rejected_signer + st.rejected_certificate + st.rejected_signature + st.rejected_time + st.replayed; } s.its_time_ms = clock_.synchronised ? static_cast(clock_.now_us() / 1000) : 0; return s; } // ---- access adapter: radio, mirrored or diverted to the software lower tester ---- Result Station::request(AlDataRequest request) { #if CONFIG_MICROBU_TEST_CHANNEL if (mirror_ != Mirror::off) record(1, request.data); if (mirror_ == Mirror::divert) { ++counters_.radio_submitted; return overflow_ ? Result::resource_limit : Result::accepted; } #endif if (radio_) { // Adaptive DCC_ACC gates here when access_stack_ has it enabled (build()); it falls // through to radio_->request() unchanged otherwise. const auto result = access_stack_ ? access_stack_->request(std::move(request)) : radio_->request(std::move(request)); if (result == Result::accepted) { ++counters_.radio_submitted; if (disseminated_) disseminated_(); } else { ++counters_.radio_failed; static std::int64_t last_report = 0; // one line per second, the counter has the rest const auto now = esp_timer_get_time(); if (now - last_report > 1000000) { last_report = now; ESP_LOGW(TAG, "radio refused a frame: result %d", int(result)); } } return result; } ++counters_.radio_failed; return Result::unsupported; // no radio configured and nothing diverting: the request has nowhere to go } } // namespace microbu