refactor(protocol): route meshcore discover through runtime

This commit is contained in:
liu weikai
2026-06-14 05:55:13 +08:00
parent f0d37d7d68
commit 1f3c86122f
11 changed files with 450 additions and 187 deletions
+7 -3
View File
@@ -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
@@ -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
@@ -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:
@@ -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;
@@ -28,6 +28,7 @@ struct ParsedPacket
constexpr size_t kMeshCoreTraceBasePayloadSize = 9;
constexpr uint8_t kMeshCorePayloadTypeTrace = 0x09;
constexpr uint8_t kMeshCorePayloadTypeControl = 0x0B;
struct DecodedTracePayload
{
@@ -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 <cstdio>
#include <cstdint>
#include <deque>
#include <string>
#include <type_traits>
#include <utility>
@@ -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<int32_t>(snr_qdb) * 10;
return static_cast<int16_t>((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<unsigned>(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<uint8_t>(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<uint8_t>(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)
@@ -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<uint8_t> public_key;
bool public_key_verified = false;
uint8_t hops = 0xFF;
MessageId tag = 0;
std::vector<uint8_t> payload;
};
using ProtocolEffect = std::variant<SendPacketEffect,
@@ -232,6 +249,7 @@ using ProtocolEffect = std::variant<SendPacketEffect,
SendRoutingErrorEffect,
SendTraceRouteEffect,
SendDiscoverRequestEffect,
SendDiscoverResponseEffect,
SendSelfAnnouncementEffect,
ForgetPeerKeyEffect,
RequestPeerNodeInfoEffect,
@@ -27,16 +27,20 @@ int main()
using chat::runtime::RequestNodeInfoIntent;
using chat::runtime::RuntimeContext;
using chat::runtime::SendDiscoverRequestEffect;
using chat::runtime::SendDiscoverResponseEffect;
using chat::runtime::SendNodeInfoEffect;
using chat::runtime::SendSelfAnnouncementEffect;
using chat::runtime::SendTraceRouteEffect;
using chat::runtime::TraceRouteIntent;
using chat::runtime::TxResult;
using chat::runtime::UpdatePeerRouteEffect;
MeshCoreRuntime runtime{};
RuntimeContext context{};
context.protocol = MeshProtocol::MeshCore;
context.self_node = 0x11111111UL;
context.meshcore_discover_node_type = chat::meshcore::kMeshCoreAdvertTypeChat;
context.meshcore_local_modified_epoch = 1781259000UL;
{
DiscoverIntent intent{};
@@ -94,6 +98,102 @@ int main()
assert(node_info->want_response);
}
{
chat::meshcore::MeshCoreDiscoverRequestBuildInfo request{};
request.type_filter = static_cast<uint8_t>(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<SendDiscoverResponseEffect>(effects, 0);
assert(response);
assert(response->protocol == MeshProtocol::MeshCore);
assert(response->tag == request.tag);
assert(response->prefix_only);
request.type_filter = static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(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<PublishNodeInfoEffect>(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<UpdatePeerRouteEffect>(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;
@@ -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<unsigned>(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<Effect, runtime::SendDiscoverResponseEffect>)
{
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<int8_t>(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<uint32_t>(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<uint32_t>(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<unsigned long>(item.tag),
item.prefix_only ? 1U : 0U,
static_cast<unsigned>(resp_len),
static_cast<unsigned long>(delay_ms));
}
}
}
}
}
else if constexpr (std::is_same_v<Effect, runtime::SendSelfAnnouncementEffect>)
{
if (item.protocol == MeshProtocol::MeshCore)
@@ -2167,11 +2225,47 @@ bool MeshCoreAdapter::executeProtocolEffect(const runtime::ProtocolEffect& effec
ts,
static_cast<uint8_t>(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<Effect, runtime::UpdatePeerRouteEffect>)
{
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<uint8_t>(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<Effect, runtime::EmitActionResultEffect>)
{
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<uint32_t>(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<uint32_t>(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<unsigned long>(req.tag),
static_cast<unsigned>(req.type_filter),
static_cast<unsigned long>(req.since),
req.prefix_only ? 1U : 0U,
static_cast<unsigned>(resp_len),
static_cast<unsigned long>(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<uint8_t>(parsed.path_len) : 0xFF;
const float snr = static_cast<float>(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<uint8_t>(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<unsigned>(parsed.route_type),
static_cast<unsigned>(parsed.path_len),
static_cast<unsigned long>(resp.tag),
static_cast<unsigned>(resp.node_type),
static_cast<int>(resp.snr_qdb),
static_cast<unsigned>(resp.pubkey[0]),
static_cast<unsigned>(resp.pubkey_len),
full_key ? 1U : 0U,
static_cast<unsigned long>(node));
return true;
}
MESHCORE_LOG("[MESHCORE] RX DISCOVER_CTRL unknown route=%u path=%u len=%u first=%02X\n",
static_cast<unsigned>(parsed.route_type),
static_cast<unsigned>(parsed.path_len),
static_cast<unsigned>(parsed.payload_len),
parsed.payload_len > 0 ? static_cast<unsigned>(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<int16_t>(std::lround(last_rx_snr_ * 10.0f));
}
if (std::isfinite(last_rx_rssi_))
{
packet.rx_meta.rssi_dbm_x10 = static_cast<int16_t>(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<unsigned>(parsed.route_type),
static_cast<unsigned>(parsed.path_len),
static_cast<unsigned>(parsed.payload_len),
parsed.payload_len > 0 ? static_cast<unsigned>(parsed.payload[0]) : 0U);
Event ev{};
ev.type = Event::Type::ControlData;
ev.peer_hash = parsed.payload_len > 0 ? parsed.payload[0] : 0;
@@ -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_{};
@@ -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<Effect, ::chat::runtime::SendDiscoverResponseEffect>)
{
ok = executeDiscoverResponseEffect(item);
}
else if constexpr (std::is_same_v<Effect, ::chat::runtime::SendSelfAnnouncementEffect>)
{
ok = executeSelfAnnouncementEffect(item);
}
else if constexpr (std::is_same_v<Effect, ::chat::runtime::PublishNodeInfoEffect>)
{
ok = false;
ok = item.protocol == ::chat::MeshProtocol::MeshCore;
}
else if constexpr (std::is_same_v<Effect, ::chat::runtime::UpdatePeerRouteEffect>)
{
ok = item.protocol == ::chat::MeshProtocol::MeshCore;
}
else if constexpr (std::is_same_v<Effect, ::chat::runtime::EmitActionResultEffect>)
{
@@ -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)