diff --git a/docs/audits/PROTOCOL_ADAPTER_DRIFT_AUDIT.md b/docs/audits/PROTOCOL_ADAPTER_DRIFT_AUDIT.md index d14b83f2..ad95a9a1 100644 --- a/docs/audits/PROTOCOL_ADAPTER_DRIFT_AUDIT.md +++ b/docs/audits/PROTOCOL_ADAPTER_DRIFT_AUDIT.md @@ -305,13 +305,17 @@ Current state: - ESP32 direct text/app-data sends now use `MeshCoreDirectRoutePolicy` for the shared decision table: missing peer pubkey -> discover/fail, selected route -> direct route path, no selected route -> flood, and preferred-channel secret derivation may fall back to the requested channel. +- Incoming MeshCore discover request/response decisions now run through `MeshCoreRuntime`: request filter/since + matching emits `SendDiscoverResponseEffect`, and responses emit `PublishNodeInfoEffect` plus + `UpdatePeerRouteEffect`. - MeshCore direct shared-secret expansion and nRF peer-key derivation now use `MeshCoreDirectSecretCore`; ESP32 keeps identity storage and peer pubkey lookup in the adapter, then delegates key expansion to the shared helper. Residual risk: -- Peer route storage, pubkey persistence, private identity storage, and frame transmission remain platform-owned. +- Peer route storage, pubkey persistence, private identity storage, discover response scheduling, and frame + transmission remain platform-owned. - nRF can derive identity/peer secrets for request payloads through the shared helper but does not implement the ESP32 direct route table. @@ -334,8 +338,8 @@ Residual risk: 1. Add parity tests that assert ESP32 and nRF adapters advertise the fine-grained capabilities they actually execute. -2. Move MeshCore peer route storage / pubkey persistence effects toward shared runtime state. -3. Move MeshCore direct route request/response side effects behind protocol runtime effects before exposing richer +2. Move MeshCore peer route storage / pubkey persistence state toward shared runtime ownership. +3. Move remaining MeshCore direct route request side effects behind protocol runtime effects before exposing richer MeshCore direct actions through UI. ## Guardrail diff --git a/docs/specification/PROTOCOL_ADAPTER_PARITY_SPEC.md b/docs/specification/PROTOCOL_ADAPTER_PARITY_SPEC.md index 751ea34e..293b735e 100644 --- a/docs/specification/PROTOCOL_ADAPTER_PARITY_SPEC.md +++ b/docs/specification/PROTOCOL_ADAPTER_PARITY_SPEC.md @@ -166,8 +166,10 @@ payload types, control frames, direct paths, and identity/key rules. Implementation ownership: - MeshCore payload helpers and protocol strategy already exist in shared modules; -- NodeInfo query/reply, trace, ACK tracking, route selection policy, and identity/key policy must - move toward shared MeshCore runtime core instead of parallel ESP32/nRF adapter copies. +- NodeInfo query/reply, discover request/response decisions, trace, ACK tracking, route selection policy, + and identity/key policy must live in shared MeshCore runtime core instead of parallel ESP32/nRF adapter + copies. Platform adapters may still own radio scheduling, route-cache persistence, and hardware identity + storage while they execute runtime effects. ## Capability Surface Requirements diff --git a/docs/specification/PROTOCOL_RUNTIME_DESIGN_SPEC.md b/docs/specification/PROTOCOL_RUNTIME_DESIGN_SPEC.md index 2f7de44c..5dca7f98 100644 --- a/docs/specification/PROTOCOL_RUNTIME_DESIGN_SPEC.md +++ b/docs/specification/PROTOCOL_RUNTIME_DESIGN_SPEC.md @@ -370,11 +370,14 @@ Current C++17 migration state: - Direct-route send decision for ESP32 MeshCore direct text/app-data now uses shared `MeshCoreDirectRoutePolicy`: missing peer pubkey triggers discover/failure, selected routes use direct path, and direct secret derivation may fall back from preferred route channel to requested channel. +- Incoming MeshCore discover request/response control payloads now flow through `MeshCoreRuntime`: filter/since + matching emits `SendDiscoverResponseEffect`, and discover responses emit `PublishNodeInfoEffect` plus + `UpdatePeerRouteEffect`. - MeshCore identity shared-secret expansion and nRF peer-key derivation now use shared `MeshCoreDirectSecretCore`; ESP32 still owns private-key storage and route pubkey lookup before delegating key expansion to the runtime helper. -- Peer route storage, pubkey persistence, private identity storage, and frame transmission are still platform - adapter responsibilities until they can be represented as runtime state/effects. +- Peer route storage, pubkey persistence, private identity storage, response scheduling, and frame transmission + are still platform adapter responsibilities until they can be represented as runtime state/effects. It does not pretend MeshCore is Meshtastic: diff --git a/modules/core_chat/include/chat/infra/meshcore/meshcore_payload_helpers.h b/modules/core_chat/include/chat/infra/meshcore/meshcore_payload_helpers.h index 2c27e445..e2b85f51 100644 --- a/modules/core_chat/include/chat/infra/meshcore/meshcore_payload_helpers.h +++ b/modules/core_chat/include/chat/infra/meshcore/meshcore_payload_helpers.h @@ -28,6 +28,7 @@ constexpr size_t kMeshCoreNodeInfoInfoPayloadSize = constexpr uint8_t kMeshCoreAdvertTypeChat = 0x01; constexpr uint8_t kMeshCoreAdvertTypeRepeater = 0x02; constexpr uint8_t kMeshCoreDiscoverTypeFilterAll = 0xFF; +constexpr size_t kMeshCorePubKeyPrefixSize = 8; constexpr size_t kMeshCoreDiscoverRequestBasePayloadSize = 6; constexpr size_t kMeshCoreDiscoverResponseBasePayloadSize = 6; diff --git a/modules/core_chat/include/chat/infra/meshcore/meshcore_protocol_helpers.h b/modules/core_chat/include/chat/infra/meshcore/meshcore_protocol_helpers.h index 63f4901c..b2d0fe84 100644 --- a/modules/core_chat/include/chat/infra/meshcore/meshcore_protocol_helpers.h +++ b/modules/core_chat/include/chat/infra/meshcore/meshcore_protocol_helpers.h @@ -28,6 +28,7 @@ struct ParsedPacket constexpr size_t kMeshCoreTraceBasePayloadSize = 9; constexpr uint8_t kMeshCorePayloadTypeTrace = 0x09; +constexpr uint8_t kMeshCorePayloadTypeControl = 0x0B; struct DecodedTracePayload { diff --git a/modules/core_chat/include/chat/runtime/meshcore_runtime.h b/modules/core_chat/include/chat/runtime/meshcore_runtime.h index c9494f5d..48184233 100644 --- a/modules/core_chat/include/chat/runtime/meshcore_runtime.h +++ b/modules/core_chat/include/chat/runtime/meshcore_runtime.h @@ -1,11 +1,14 @@ #pragma once #include "chat/infra/meshcore/meshcore_payload_helpers.h" +#include "chat/infra/meshcore/meshcore_identity_crypto.h" #include "chat/infra/meshcore/meshcore_protocol_helpers.h" #include "chat/runtime/protocol_runtime.h" +#include #include #include +#include #include #include @@ -64,6 +67,14 @@ class MeshCoreRuntime final : public IProtocolRuntime return effects; } + if (packet.payload_type == chat::meshcore::kMeshCorePayloadTypeControl && + !packet.payload.empty() && + (packet.payload[0] & 0x80U) != 0) + { + handleIncomingDiscoverControl(packet, context, effects); + return effects; + } + if (packet.portnum != chat::meshcore::kMeshCoreNodeInfoPortnum) { return effects; @@ -300,6 +311,7 @@ class MeshCoreRuntime final : public IProtocolRuntime static constexpr uint32_t kDefaultTraceTimeoutMs = 15000; static constexpr uint32_t kDefaultAppAckTimeoutMs = 15000; static constexpr uint32_t kDefaultDiscoverRxGuardMs = 5000; + static constexpr uint32_t kMinValidEpochSeconds = 1577836800UL; static constexpr size_t kMaxPendingAppAcks = 32; static NodeId normalizePeer(NodeId peer) @@ -330,6 +342,24 @@ class MeshCoreRuntime final : public IProtocolRuntime return tag == 0 ? 1 : tag; } + static bool isValidEpoch(uint32_t seconds) + { + return seconds >= kMinValidEpochSeconds; + } + + static int16_t snrQuarterDbToX10(int8_t snr_qdb) + { + const int32_t value = static_cast(snr_qdb) * 10; + return static_cast((value >= 0 ? value + 2 : value - 2) / 4); + } + + static std::string makeDiscoverFallbackName(uint8_t peer_hash) + { + char name[8] = {}; + std::snprintf(name, sizeof(name), "%02X", static_cast(peer_hash)); + return std::string{name}; + } + void resolveDiscover(const DiscoverIntent& intent, const RuntimeContext& context, ProtocolEffects& effects) @@ -364,6 +394,93 @@ class MeshCoreRuntime final : public IProtocolRuntime } } + void handleIncomingDiscoverControl(const IncomingPacket& packet, + const RuntimeContext& context, + ProtocolEffects& effects) + { + chat::meshcore::DecodedDiscoverRequest request{}; + if (chat::meshcore::decodeDiscoverRequest(packet.payload.data(), + packet.payload.size(), + &request)) + { + if (!chat::meshcore::discoverFilterMatchesType(request.type_filter, + context.meshcore_discover_node_type)) + { + return; + } + + if (request.since != 0 && + isValidEpoch(request.since) && + isValidEpoch(context.meshcore_local_modified_epoch) && + context.meshcore_local_modified_epoch < request.since) + { + return; + } + + SendDiscoverResponseEffect response{}; + response.protocol = MeshProtocol::MeshCore; + response.tag = request.tag; + response.prefix_only = request.prefix_only; + effects.add(response); + return; + } + + chat::meshcore::DecodedDiscoverResponse response{}; + if (!chat::meshcore::decodeDiscoverResponse(packet.payload.data(), + packet.payload.size(), + &response) || + response.pubkey_len == 0 || + response.pubkey == nullptr) + { + return; + } + + const uint8_t peer_hash = response.pubkey[0]; + if (peer_hash == static_cast(context.self_node & 0xFFU)) + { + return; + } + + const NodeId node = chat::meshcore::deriveNodeIdFromPubkey(response.pubkey, + response.pubkey_len); + if (node == 0) + { + return; + } + + const uint8_t hops = packet.path.size() <= 255 + ? static_cast(packet.path.size()) + : 0xFF; + auto rx_meta = packet.rx_meta; + rx_meta.hop_count = hops; + rx_meta.snr_db_x10 = snrQuarterDbToX10(response.snr_qdb); + + PublishNodeInfoEffect publish{}; + publish.protocol = MeshProtocol::MeshCore; + publish.channel = packet.channel; + publish.node_id = node; + publish.short_name = makeDiscoverFallbackName(peer_hash); + publish.long_name = publish.short_name; + publish.role = chat::meshcore::mapAdvertTypeToRole(response.node_type); + publish.hops = hops; + publish.rx_meta = rx_meta; + publish.has_public_key = response.pubkey_len == chat::meshcore::kMeshCorePubKeySize; + effects.add(std::move(publish)); + + UpdatePeerRouteEffect route{}; + route.protocol = MeshProtocol::MeshCore; + route.peer = node; + route.peer_hash = peer_hash; + route.hops = hops; + route.tag = response.tag; + route.payload.assign(packet.payload.begin(), packet.payload.end()); + if (response.pubkey_len == chat::meshcore::kMeshCorePubKeySize) + { + route.public_key.assign(response.pubkey, response.pubkey + response.pubkey_len); + } + effects.add(std::move(route)); + } + void resolveTraceRoute(const TraceRouteIntent& intent, const RuntimeContext& context, ProtocolEffects& effects) diff --git a/modules/core_chat/include/chat/runtime/protocol_runtime.h b/modules/core_chat/include/chat/runtime/protocol_runtime.h index 7dca22da..4e3043c8 100644 --- a/modules/core_chat/include/chat/runtime/protocol_runtime.h +++ b/modules/core_chat/include/chat/runtime/protocol_runtime.h @@ -36,6 +36,8 @@ struct RuntimeContext MeshProtocol protocol = MeshProtocol::Meshtastic; NodeId self_node = 0; uint32_t now_ms = 0; + uint8_t meshcore_discover_node_type = 0; + uint32_t meshcore_local_modified_epoch = 0; }; struct SendTextIntent @@ -162,6 +164,13 @@ struct SendDiscoverRequestEffect uint32_t rx_guard_ms = 5000; }; +struct SendDiscoverResponseEffect +{ + MeshProtocol protocol = MeshProtocol::MeshCore; + uint32_t tag = 0; + bool prefix_only = false; +}; + struct SendSelfAnnouncementEffect { MeshProtocol protocol = MeshProtocol::MeshCore; @@ -206,6 +215,8 @@ struct PublishNodeInfoEffect uint8_t role = 0; uint8_t hops = 0; RxMeta rx_meta{}; + bool has_public_key = false; + bool key_manually_verified = false; }; struct EmitActionResultEffect @@ -223,8 +234,14 @@ struct UpdatePeerRouteEffect { MeshProtocol protocol = MeshProtocol::MeshCore; NodeId peer = 0; + uint8_t peer_hash = 0; uint8_t next_hop = 0; ChannelId preferred_channel = ChannelId::PRIMARY; + std::vector public_key; + bool public_key_verified = false; + uint8_t hops = 0xFF; + MessageId tag = 0; + std::vector payload; }; using ProtocolEffect = std::variantwant_response); } + { + chat::meshcore::MeshCoreDiscoverRequestBuildInfo request{}; + request.type_filter = static_cast(1U << chat::meshcore::kMeshCoreAdvertTypeChat); + request.prefix_only = true; + request.tag = 0x24681357UL; + request.since = 1781258000UL; + + uint8_t payload[chat::meshcore::kMeshCoreDiscoverRequestBasePayloadSize + sizeof(uint32_t)] = {}; + size_t payload_len = 0; + assert(chat::meshcore::buildDiscoverRequestControlPayload(request, + payload, + sizeof(payload), + &payload_len)); + + IncomingPacket packet{}; + packet.protocol = MeshProtocol::MeshCore; + packet.payload_type = chat::meshcore::kMeshCorePayloadTypeControl; + packet.payload.assign(payload, payload + payload_len); + + const auto effects = runtime.handleIncoming(packet, context); + assert(effects.items.size() == 1); + const auto* response = effectAt(effects, 0); + assert(response); + assert(response->protocol == MeshProtocol::MeshCore); + assert(response->tag == request.tag); + assert(response->prefix_only); + + request.type_filter = static_cast(1U << chat::meshcore::kMeshCoreAdvertTypeRepeater); + assert(chat::meshcore::buildDiscoverRequestControlPayload(request, + payload, + sizeof(payload), + &payload_len)); + packet.payload.assign(payload, payload + payload_len); + assert(runtime.handleIncoming(packet, context).items.empty()); + + request.type_filter = static_cast(1U << chat::meshcore::kMeshCoreAdvertTypeChat); + request.since = 1781260000UL; + assert(chat::meshcore::buildDiscoverRequestControlPayload(request, + payload, + sizeof(payload), + &payload_len)); + packet.payload.assign(payload, payload + payload_len); + assert(runtime.handleIncoming(packet, context).items.empty()); + } + + { + uint8_t pubkey[chat::meshcore::kMeshCorePubKeySize] = {}; + for (size_t i = 0; i < sizeof(pubkey); ++i) + { + pubkey[i] = static_cast(0x42U + i); + } + + uint8_t payload[chat::meshcore::kMeshCoreDiscoverResponseBasePayloadSize + + chat::meshcore::kMeshCorePubKeySize] = {}; + size_t payload_len = 0; + assert(chat::meshcore::buildDiscoverResponseControlPayload( + chat::meshcore::kMeshCoreAdvertTypeRepeater, + 6, + 0xAABBCCDDUL, + pubkey, + sizeof(pubkey), + payload, + sizeof(payload), + &payload_len)); + + IncomingPacket packet{}; + packet.protocol = MeshProtocol::MeshCore; + packet.payload_type = chat::meshcore::kMeshCorePayloadTypeControl; + packet.payload.assign(payload, payload + payload_len); + packet.rx_meta.rssi_dbm_x10 = -840; + + const auto effects = runtime.handleIncoming(packet, context); + assert(effects.items.size() == 2); + const auto* publish = effectAt(effects, 0); + assert(publish); + assert(publish->protocol == MeshProtocol::MeshCore); + assert(publish->node_id == chat::meshcore::deriveNodeIdFromPubkey(pubkey, sizeof(pubkey))); + assert(publish->short_name == "42"); + assert(publish->long_name == "42"); + assert(publish->role == chat::meshcore::mapAdvertTypeToRole(chat::meshcore::kMeshCoreAdvertTypeRepeater)); + assert(publish->hops == 0); + assert(publish->rx_meta.snr_db_x10 == 15); + assert(publish->rx_meta.rssi_dbm_x10 == -840); + assert(publish->has_public_key); + + const auto* route = effectAt(effects, 1); + assert(route); + assert(route->protocol == MeshProtocol::MeshCore); + assert(route->peer == publish->node_id); + assert(route->peer_hash == 0x42); + assert(route->public_key.size() == sizeof(pubkey)); + assert(std::memcmp(route->public_key.data(), pubkey, sizeof(pubkey)) == 0); + assert(route->tag == 0xAABBCCDDUL); + assert(route->payload.size() == payload_len); + } + { uint8_t payload[chat::meshcore::kMeshCoreNodeInfoInfoPayloadSize] = {}; size_t payload_len = 0; diff --git a/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp b/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp index 795384bf..817197ed 100644 --- a/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp +++ b/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp @@ -55,7 +55,7 @@ constexpr uint8_t kPayloadTypeAdvert = 0x04; constexpr uint8_t kPayloadTypePath = 0x08; constexpr uint8_t kPayloadTypeTrace = chat::meshcore::kMeshCorePayloadTypeTrace; constexpr uint8_t kPayloadTypeMultipart = 0x0A; -constexpr uint8_t kPayloadTypeControl = 0x0B; +constexpr uint8_t kPayloadTypeControl = chat::meshcore::kMeshCorePayloadTypeControl; constexpr uint8_t kPayloadVer1 = 0x00; constexpr size_t kMeshcorePathHashSize = 1; constexpr size_t kMeshcoreMaxPathSize = 64; @@ -110,7 +110,6 @@ constexpr uint8_t kAdvertFlagHasFeature1 = 0x20; constexpr uint8_t kAdvertFlagHasFeature2 = 0x40; constexpr uint8_t kAdvertFlagHasName = 0x80; constexpr size_t kMeshcorePubKeySize = 32; -constexpr size_t kMeshcorePubKeyPrefixSize = 8; constexpr size_t kAdvertSignatureSize = 64; constexpr size_t kAdvertMinPayloadSize = kMeshcorePubKeySize + sizeof(uint32_t) + kAdvertSignatureSize; @@ -199,7 +198,6 @@ using chat::meshcore::decodeGroupAppPayload; using chat::meshcore::decodeNodeInfoControlPayload; using chat::meshcore::decodeTracePayload; using chat::meshcore::deriveNodeIdFromPubkey; -using chat::meshcore::discoverFilterMatchesType; using chat::meshcore::formatVerificationCode; using chat::meshcore::hasControlPrefix; using chat::meshcore::kMeshCoreDiscoverTypeFilterAll; @@ -267,15 +265,6 @@ bool buildPathPlain(const uint8_t* out_path, size_t out_path_len, return true; } -void formatMeshCoreFallbackShortName(uint8_t peer_hash, char* out, size_t out_len) -{ - if (!out || out_len == 0) - { - return; - } - std::snprintf(out, out_len, "%02X", static_cast(peer_hash)); -} - } // namespace MeshCoreAdapter::MeshCoreAdapter(LoraBoard& board) @@ -2043,6 +2032,10 @@ runtime::RuntimeContext MeshCoreAdapter::buildRuntimeContext() const context.protocol = MeshProtocol::MeshCore; context.self_node = node_id_; context.now_ms = millis(); + context.meshcore_discover_node_type = config_.meshcore_client_repeat + ? kAdvertTypeRepeater + : kAdvertTypeChat; + context.meshcore_local_modified_epoch = now_epoch_seconds(); return context; } @@ -2119,6 +2112,71 @@ bool MeshCoreAdapter::executeProtocolEffect(const runtime::ProtocolEffect& effec ok = sendDiscoverRequestLocalDetailed(item).ok; } } + else if constexpr (std::is_same_v) + { + if (item.protocol == MeshProtocol::MeshCore) + { + if (!identity_.isReady()) + { + MESHCORE_LOG("[MESHCORE] RX DISCOVER_REQ ignored (identity unavailable)\n"); + ok = true; + } + else + { + const uint8_t local_type = config_.meshcore_client_repeat + ? kAdvertTypeRepeater + : kAdvertTypeChat; + const size_t key_len = item.prefix_only ? chat::meshcore::kMeshCorePubKeyPrefixSize : kMeshcorePubKeySize; + uint8_t resp_payload[6 + kMeshcorePubKeySize] = {}; + size_t resp_len = 0; + ok = buildDiscoverResponseControlPayload(local_type, + std::isfinite(last_rx_snr_) + ? static_cast(std::lround(last_rx_snr_ * 4.0f)) + : 0, + item.tag, + identity_.publicKey(), + key_len, + resp_payload, + sizeof(resp_payload), + &resp_len); + if (ok) + { + uint8_t frame[kMeshcoreMaxFrameSize] = {}; + size_t frame_len = 0; + ok = buildFrameNoTransport(kRouteTypeDirect, kPayloadTypeControl, + nullptr, 0, + resp_payload, resp_len, + frame, sizeof(frame), &frame_len); + if (ok) + { + float air_ms_f = estimateLoRaAirtimeMs(frame_len, + config_.meshcore_bw_khz, + config_.meshcore_sf, + config_.meshcore_cr); + if (!std::isfinite(air_ms_f) || air_ms_f <= 0.0f) + { + air_ms_f = 50.0f; + } + uint32_t t_ms = static_cast(std::lround((air_ms_f * 52.0f / 50.0f) / 2.0f)); + if (t_ms == 0) + { + t_ms = 1; + } + const uint32_t delay_ms = static_cast(random(1, 5)) * t_ms * 4U; + if (config_.tx_enabled) + { + enqueueScheduled(frame, frame_len, delay_ms); + } + MESHCORE_LOG("[MESHCORE] RX DISCOVER_REQ tag=%08lX prefix=%u -> RESP len=%u delay=%lu\n", + static_cast(item.tag), + item.prefix_only ? 1U : 0U, + static_cast(resp_len), + static_cast(delay_ms)); + } + } + } + } + } else if constexpr (std::is_same_v) { if (item.protocol == MeshProtocol::MeshCore) @@ -2167,11 +2225,47 @@ bool MeshCoreAdapter::executeProtocolEffect(const runtime::ProtocolEffect& effec ts, static_cast(chat::contacts::NodeProtocolType::MeshCore), item.role, - item.hops), + item.hops, + 0, + 0xFF, + false, + nullptr, + false, + false, + item.has_public_key, + item.key_manually_verified), 0); ok = true; } } + else if constexpr (std::is_same_v) + { + if (item.protocol == MeshProtocol::MeshCore) + { + const uint32_t now_ms = millis(); + const uint8_t peer_hash = item.peer_hash != 0 + ? item.peer_hash + : static_cast(item.peer & 0xFFU); + if (peer_hash != 0) + { + rememberPeerNodeId(peer_hash, item.peer, now_ms); + if (item.public_key.size() == kMeshcorePubKeySize) + { + rememberPeerPubKey(item.public_key.data(), now_ms, item.public_key_verified); + } + + Event ev{}; + ev.type = Event::Type::ControlData; + ev.peer_hash = peer_hash; + ev.peer_node = item.peer; + ev.flags = item.hops; + ev.tag = item.tag; + ev.payload = item.payload; + pushEvent(std::move(ev)); + } + ok = true; + } + } else if constexpr (std::is_same_v) { if (item.protocol == MeshProtocol::MeshCore) @@ -3766,170 +3860,6 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, 0); }; - auto handleDiscoverControl = [&]() -> bool - { - if (parsed.payload_len == 0) - { - return false; - } - - DecodedDiscoverRequest req{}; - if (decodeDiscoverRequest(parsed.payload, parsed.payload_len, &req)) - { - const uint8_t local_type = config_.meshcore_client_repeat - ? kAdvertTypeRepeater - : kAdvertTypeChat; - if (!discoverFilterMatchesType(req.type_filter, local_type)) - { - return true; - } - - const uint32_t local_mod_ts = now_epoch_seconds(); - if (req.since != 0 && is_valid_epoch(req.since) && - is_valid_epoch(local_mod_ts) && local_mod_ts < req.since) - { - return true; - } - - if (!identity_.isReady()) - { - MESHCORE_LOG("[MESHCORE] RX DISCOVER_REQ ignored (identity unavailable)\n"); - return true; - } - - const size_t key_len = req.prefix_only ? kMeshcorePubKeyPrefixSize : kMeshcorePubKeySize; - uint8_t resp_payload[6 + kMeshcorePubKeySize] = {}; - size_t resp_len = 0; - if (!buildDiscoverResponseControlPayload(local_type, - quantizeSnrQuarterDb(), - req.tag, - identity_.publicKey(), - key_len, - resp_payload, - sizeof(resp_payload), - &resp_len)) - { - return true; - } - - uint8_t frame[kMeshcoreMaxFrameSize] = {}; - size_t frame_len = 0; - if (!buildFrameNoTransport(kRouteTypeDirect, kPayloadTypeControl, - nullptr, 0, - resp_payload, resp_len, - frame, sizeof(frame), &frame_len)) - { - return true; - } - - float air_ms_f = estimateLoRaAirtimeMs(frame_len, - config_.meshcore_bw_khz, - config_.meshcore_sf, - config_.meshcore_cr); - if (!std::isfinite(air_ms_f) || air_ms_f <= 0.0f) - { - air_ms_f = 50.0f; - } - // Align with upstream MeshCore getRetransmitDelay()*4: - // t = (airtime * 52 / 50) / 2; delay = random(0..4) * t * 4 - uint32_t t_ms = static_cast(std::lround((air_ms_f * 52.0f / 50.0f) / 2.0f)); - if (t_ms == 0) - { - t_ms = 1; - } - const uint32_t delay_ms = static_cast(random(1, 5)) * t_ms * 4U; - if (config_.tx_enabled) - { - enqueueScheduled(frame, frame_len, delay_ms); - } - - MESHCORE_LOG("[MESHCORE] RX DISCOVER_REQ tag=%08lX filter=%02X since=%lu prefix=%u -> RESP len=%u delay=%lu\n", - static_cast(req.tag), - static_cast(req.type_filter), - static_cast(req.since), - req.prefix_only ? 1U : 0U, - static_cast(resp_len), - static_cast(delay_ms)); - return true; - } - - DecodedDiscoverResponse resp{}; - if (decodeDiscoverResponse(parsed.payload, parsed.payload_len, &resp)) - { - if (resp.pubkey_len == 0 || !resp.pubkey) - { - return true; - } - if (resp.pubkey[0] == self_hash_) - { - return true; - } - - const NodeId node = deriveNodeIdFromPubkey(resp.pubkey, resp.pubkey_len); - const uint8_t hops = (parsed.path_len <= 255) ? static_cast(parsed.path_len) : 0xFF; - const float snr = static_cast(resp.snr_qdb) / 4.0f; - const float rssi = std::isfinite(last_rx_rssi_) ? last_rx_rssi_ : NAN; - const uint32_t ts = now_message_timestamp(); - const bool full_key = resp.pubkey_len == kMeshcorePubKeySize; - - rememberPeerNodeId(resp.pubkey[0], node, now_ms); - if (full_key) - { - rememberPeerPubKey(resp.pubkey, now_ms, false); - } - char fallback_short_name[8] = {}; - formatMeshCoreFallbackShortName(resp.pubkey[0], fallback_short_name, - sizeof(fallback_short_name)); - sys::EventBus::publish( - new sys::NodeInfoUpdateEvent(node, - fallback_short_name, - fallback_short_name, - snr, - rssi, - ts, - static_cast(chat::contacts::NodeProtocolType::MeshCore), - mapAdvertTypeToRole(resp.node_type), - hops, - 0, - 0xFF, - false, - nullptr, - false, - false, - full_key, - false), - 0); - - Event ev{}; - ev.type = Event::Type::ControlData; - ev.peer_hash = resp.pubkey[0]; - ev.peer_node = node; - ev.flags = hops; - ev.tag = resp.tag; - ev.payload.assign(parsed.payload, parsed.payload + parsed.payload_len); - pushEvent(std::move(ev)); - - MESHCORE_LOG("[MESHCORE] RX DISCOVER_RESP route=%u path=%u tag=%08lX type=%u snr_qdb=%d hash=%02X key_len=%u full_key=%u node=%08lX\n", - static_cast(parsed.route_type), - static_cast(parsed.path_len), - static_cast(resp.tag), - static_cast(resp.node_type), - static_cast(resp.snr_qdb), - static_cast(resp.pubkey[0]), - static_cast(resp.pubkey_len), - full_key ? 1U : 0U, - static_cast(node)); - return true; - } - - MESHCORE_LOG("[MESHCORE] RX DISCOVER_CTRL unknown route=%u path=%u len=%u first=%02X\n", - static_cast(parsed.route_type), - static_cast(parsed.path_len), - static_cast(parsed.payload_len), - parsed.payload_len > 0 ? static_cast(parsed.payload[0]) : 0U); - return false; - }; - // TRACE direct packets use path[] for accumulated SNR and route hashes live in payload. if (is_direct_route && parsed.payload_type == kPayloadTypeTrace && parsed.payload_len >= 9) { @@ -3997,8 +3927,33 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, { if (parsed.path_len == 0) { - if (!handleDiscoverControl()) + runtime::IncomingPacket packet{}; + packet.protocol = MeshProtocol::MeshCore; + packet.payload_type = kPayloadTypeControl; + packet.payload.assign(parsed.payload, parsed.payload + parsed.payload_len); + if (std::isfinite(last_rx_snr_)) { + packet.rx_meta.snr_db_x10 = static_cast(std::lround(last_rx_snr_ * 10.0f)); + } + if (std::isfinite(last_rx_rssi_)) + { + packet.rx_meta.rssi_dbm_x10 = static_cast(std::lround(last_rx_rssi_ * 10.0f)); + } + + executeProtocolEffects(protocol_runtime_.handleIncoming(packet, buildRuntimeContext())); + + DecodedDiscoverRequest discover_request{}; + DecodedDiscoverResponse discover_response{}; + const bool known_discover_control = + decodeDiscoverRequest(parsed.payload, parsed.payload_len, &discover_request) || + decodeDiscoverResponse(parsed.payload, parsed.payload_len, &discover_response); + if (!known_discover_control) + { + MESHCORE_LOG("[MESHCORE] RX DISCOVER_CTRL unknown route=%u path=%u len=%u first=%02X\n", + static_cast(parsed.route_type), + static_cast(parsed.path_len), + static_cast(parsed.payload_len), + parsed.payload_len > 0 ? static_cast(parsed.payload[0]) : 0U); Event ev{}; ev.type = Event::Type::ControlData; ev.peer_hash = parsed.payload_len > 0 ? parsed.payload[0] : 0; diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h index 35563a9a..5c19aab7 100644 --- a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h @@ -84,6 +84,7 @@ class MeshCoreRadioAdapter final : public ::chat::IMeshAdapter, public ::chat::m bool executeProtocolEffect(const ::chat::runtime::ProtocolEffect& effect); bool executeNodeInfoEffect(const ::chat::runtime::SendNodeInfoEffect& effect); bool executeDiscoverRequestEffect(const ::chat::runtime::SendDiscoverRequestEffect& effect); + bool executeDiscoverResponseEffect(const ::chat::runtime::SendDiscoverResponseEffect& effect); bool executeSelfAnnouncementEffect(const ::chat::runtime::SendSelfAnnouncementEffect& effect); ::chat::MeshConfig config_{}; diff --git a/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp b/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp index 08905749..1b6c7a9a 100644 --- a/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp +++ b/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp @@ -29,7 +29,7 @@ constexpr uint8_t kPayloadTypeAck = 0x03; constexpr uint8_t kPayloadTypeDirectData = 0x07; constexpr uint8_t kPayloadTypeGrpData = 0x06; constexpr uint8_t kPayloadTypeTrace = ::chat::meshcore::kMeshCorePayloadTypeTrace; -constexpr uint8_t kPayloadTypeControl = 0x0B; +constexpr uint8_t kPayloadTypeControl = ::chat::meshcore::kMeshCorePayloadTypeControl; constexpr uint8_t kPayloadTypeRawCustom = 0x0F; constexpr uint8_t kPayloadTypeAdvert = 0x04; constexpr uint8_t kDirectAppMagic0 = 0xDA; @@ -250,6 +250,8 @@ bool MeshCoreRadioAdapter::requestNodeInfo(::chat::NodeId dest, bool want_respon context.protocol = ::chat::MeshProtocol::MeshCore; context.self_node = node_id_; context.now_ms = millis(); + context.meshcore_discover_node_type = ::chat::meshcore::kMeshCoreAdvertTypeChat; + context.meshcore_local_modified_epoch = ::chat::now_epoch_seconds(); return context; } @@ -311,13 +313,21 @@ bool MeshCoreRadioAdapter::executeProtocolEffect(const ::chat::runtime::Protocol { ok = executeDiscoverRequestEffect(item); } + else if constexpr (std::is_same_v) + { + ok = executeDiscoverResponseEffect(item); + } else if constexpr (std::is_same_v) { ok = executeSelfAnnouncementEffect(item); } else if constexpr (std::is_same_v) { - ok = false; + ok = item.protocol == ::chat::MeshProtocol::MeshCore; + } + else if constexpr (std::is_same_v) + { + ok = item.protocol == ::chat::MeshProtocol::MeshCore; } else if constexpr (std::is_same_v) { @@ -418,6 +428,41 @@ bool MeshCoreRadioAdapter::executeDiscoverRequestEffect(const ::chat::runtime::S sendControlData(payload, payload_len); } +bool MeshCoreRadioAdapter::executeDiscoverResponseEffect(const ::chat::runtime::SendDiscoverResponseEffect& effect) +{ + if (effect.protocol != ::chat::MeshProtocol::MeshCore) + { + return false; + } + if (!config_.tx_enabled) + { + return true; + } + + ensureIdentityKeys(); + if (!keys_ready_) + { + return true; + } + + const size_t key_len = effect.prefix_only + ? ::chat::meshcore::kMeshCorePubKeyPrefixSize + : ::chat::meshcore::kMeshCorePubKeySize; + uint8_t payload[::chat::meshcore::kMeshCoreDiscoverResponseBasePayloadSize + + ::chat::meshcore::kMeshCorePubKeySize] = {}; + size_t payload_len = 0; + return ::chat::meshcore::buildDiscoverResponseControlPayload( + ::chat::meshcore::kMeshCoreAdvertTypeChat, + 0, + effect.tag, + public_key_, + key_len, + payload, + sizeof(payload), + &payload_len) && + sendControlData(payload, payload_len); +} + bool MeshCoreRadioAdapter::executeSelfAnnouncementEffect(const ::chat::runtime::SendSelfAnnouncementEffect& effect) { if (effect.protocol != ::chat::MeshProtocol::MeshCore || !config_.tx_enabled) @@ -515,6 +560,22 @@ void MeshCoreRadioAdapter::handleRawPacket(const uint8_t* data, size_t size) return; } + if (parsed.payload_type == kPayloadTypeControl && + parsed.payload_len > 0 && + (parsed.payload[0] & 0x80U) != 0) + { + ::chat::runtime::IncomingPacket packet{}; + packet.protocol = ::chat::MeshProtocol::MeshCore; + packet.payload_type = kPayloadTypeControl; + packet.payload.assign(parsed.payload, parsed.payload + parsed.payload_len); + if (parsed.path_len > 0) + { + packet.path.assign(parsed.path, parsed.path + parsed.path_len); + } + executeProtocolEffects(protocol_runtime_.handleIncoming(packet, buildRuntimeContext())); + return; + } + if (parsed.payload_type == kPayloadTypeAdvert) { if (parsed.payload_len < kAdvertMinPayloadSize)