#include "link_service.hpp" #include "simple_ble.hpp" #include "serial_link.hpp" #include #include namespace microbu { namespace { const char* TAG = "link"; } LinkService::LinkService(Station& station) : station_(station) { station_.on_indication([this](const link::BtpDataIndication& indication) { broadcast(link::Opcode::BTP_DATA_INDICATION, ++own_sequence_, link::encode(indication)); }); // MicrOBU: every ITS message heard on air, verified or not (Station::on_raw_its). station_.on_raw_its([this](const link::Bytes& body) { broadcast(link::Opcode::V2X_RX, ++own_sequence_, body); }); station_.on_id_event([this](const link::IdChangeEvent& event) { // PREPARE/COMMIT expect SF_IDCHANGE_EVENT_RESPONSE with this sequence broadcast(link::Opcode::SF_IDCHANGE_EVENT, ++own_sequence_, link::encode(event)); }); } void LinkService::send(LinkTransport transport, link::Opcode opcode, std::uint16_t sequence, const link::Bytes& body) { link::Bytes octets; link::Message message {{opcode, 0, sequence}, body}; if (!link::encode(message, octets)) { ESP_LOGE(TAG, "message 0x%02x too long (%u)", unsigned(opcode), unsigned(body.size())); return; } if (transport == LinkTransport::serial) serial::write(serial::FrameType::LINK, octets); else ble::write(octets); } void LinkService::broadcast(link::Opcode opcode, std::uint16_t sequence, const link::Bytes& body) { send(LinkTransport::serial, opcode, sequence, body); if (ble::connected()) send(LinkTransport::ble, opcode, sequence, body); } void LinkService::reply(LinkTransport transport, std::uint16_t sequence, link::Code code, const link::Bytes& detail) { send(transport, link::Opcode::RESULT, sequence, link::encode(link::Result {code, detail})); } link::Status LinkService::status() { auto value = station_.status(); const auto serial_counters = serial::counters(); const auto ble_counters = ble::counters(); value.link_rx_frames = serial_counters.frames + ble_counters.rx_messages; value.link_crc_errors = serial_counters.crc_errors; value.link_malformed += ble_counters.malformed; return value; } void LinkService::handle(const link::Bytes& octets, LinkTransport origin) { link::Message m; if (!link::decode(octets, m)) return; const auto seq = m.header.sequence; using link::Opcode; using link::Code; switch (m.header.opcode) { case Opcode::STATION_CONFIGURE: { link::StationConfigure c; if (!link::decode(m.body, c)) return reply(origin, seq, Code::malformed); link::Bytes detail; const auto result = station_.configure(c, detail); ESP_LOGI(TAG, "station configured: security %u, radio %u, result %d", unsigned(c.security), unsigned(c.radio), int(result)); return reply(origin, seq, result, detail); } case Opcode::POTI_UPDATE: { link::PotiUpdate p; if (!link::decode(m.body, p)) return reply(origin, seq, Code::malformed); const auto result = station_.poti(p); if (result != Code::accepted) reply(origin, seq, result); // accepted updates are silent return; } case Opcode::BTP_DATA_REQUEST: { link::BtpDataRequest q; if (!link::decode(m.body, q)) return reply(origin, seq, Code::malformed); return reply(origin, seq, station_.btp_request(q)); } case Opcode::CREDENTIALS_PROVISION: { link::CredentialsProvision c; if (!link::decode(m.body, c)) return reply(origin, seq, Code::malformed); if (c.offset == 0) { bundle_.clear(); bundle_total_ = c.total_length; } if (c.total_length != bundle_total_ || c.offset != bundle_.size() || c.total_length > 8192) { bundle_.clear(); return reply(origin, seq, Code::malformed); } bundle_.insert(bundle_.end(), c.segment.begin(), c.segment.end()); if (bundle_.size() < bundle_total_) return reply(origin, seq, Code::accepted); link::ApplyReport report; link::Code result = Code::accepted; try { result = station_.provision(bundle_, report); } catch (const std::exception& e) { ESP_LOGE(TAG, "provision exception: %s", e.what()); result = Code::invalid_argument; } bundle_.clear(); link::Bytes detail; detail.push_back(static_cast(report.roots)); detail.push_back(static_cast(report.authorities)); detail.push_back(static_cast(report.tickets)); return reply(origin, seq, result, detail); } case Opcode::CREDENTIALS_ERASE: return reply(origin, seq, station_.erase_credentials()); case Opcode::SF_IDCHANGE_SUBSCRIBE: { link::Reader r(m.body); const auto data = r.bytes(r.u8()); if (!r.done()) { ESP_LOGW(TAG, "SF_IDCHANGE_SUBSCRIBE malformed (len=%u)", (unsigned)m.body.size()); return reply(origin, seq, Code::malformed); } std::uint64_t handle = 0; const auto result = station_.subscribe(data, handle); ESP_LOGI(TAG, "SF_IDCHANGE_SUBSCRIBE handle=%llu result=%d", (unsigned long long)handle, int(result)); link::Writer w; w.u64(handle); return reply(origin, seq, result, result == Code::accepted ? w.out : link::Bytes {}); } case Opcode::SF_IDCHANGE_UNSUBSCRIBE: { link::Reader r(m.body); const auto handle = r.u64(); if (!r.done()) return reply(origin, seq, Code::malformed); return reply(origin, seq, station_.unsubscribe(handle)); } case Opcode::SF_IDCHANGE_EVENT_RESPONSE: { link::IdChangeEventResponse e; if (!link::decode(m.body, e)) return; // no reply defined for a response station_.event_response(e.subscription, e.return_code != 0); return; } case Opcode::SF_IDCHANGE_TRIGGER: return reply(origin, seq, station_.trigger()); case Opcode::SF_ID_LOCK: { link::Reader r(m.body); const auto seconds = r.u8(); if (!r.done()) return reply(origin, seq, Code::malformed); std::uint64_t handle = 0; const auto result = station_.lock(seconds, handle); link::Writer w; w.u64(handle); return reply(origin, seq, result, result == Code::accepted ? w.out : link::Bytes {}); } case Opcode::SF_ID_UNLOCK: { link::Reader r(m.body); const auto handle = r.u64(); if (!r.done()) return reply(origin, seq, Code::malformed); return reply(origin, seq, station_.unlock(handle)); } case Opcode::STATUS_REQUEST: return send(origin, link::Opcode::STATUS, seq, link::encode(status())); default: return reply(origin, seq, Code::unknown_opcode); } } void LinkService::tick() { const auto now = esp_timer_get_time(); if (now - last_status_us_ < 1000000) return; last_status_us_ = now; broadcast(link::Opcode::STATUS, ++own_sequence_, link::encode(status())); } } // namespace microbu