mirror of
https://github.com/vicliu624/trail-mate.git
synced 2026-09-01 16:48:21 +00:00
refactor(protocol): route meshcore discover through runtime
This commit is contained in:
@@ -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;
|
||||
|
||||
+1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user