refactor(protocol): route meshtastic node actions through runtime

This commit is contained in:
liu weikai
2026-06-14 07:38:55 +08:00
parent 21f90ebb8a
commit bfab72e4ed
7 changed files with 313 additions and 36 deletions
+33 -1
View File
@@ -11,6 +11,19 @@ set(TRAIL_MATE_REPO_ROOT
include("${TRAIL_MATE_REPO_ROOT}/cmake/TrailMateUxPacks.cmake")
trailmate_add_ui_lvgl_ux_packs(trailmate_ui_lvgl_ux_packs)
set(TRAIL_MATE_MESHTASTIC_GENERATED_SOURCES
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/generated/meshtastic/admin.pb.cpp"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/generated/meshtastic/channel.pb.cpp"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/generated/meshtastic/config.pb.cpp"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/generated/meshtastic/connection_status.pb.cpp"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/generated/meshtastic/device_ui.pb.cpp"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/generated/meshtastic/localonly.pb.cpp"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/generated/meshtastic/mesh.pb.cpp"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/generated/meshtastic/module_config.pb.cpp"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/generated/meshtastic/portnums.pb.cpp"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/generated/meshtastic/telemetry.pb.cpp"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/generated/meshtastic/xmodem.pb.cpp")
add_library(trailmate_linux_sim_shell
src/linux_sim_app_shell.cpp
src/linux_sim_runtime_entry.cpp
@@ -120,7 +133,10 @@ if(BUILD_TESTING)
COMMAND trailmate_mesh_adapter_router_core_smoke)
add_executable(trailmate_protocol_effect_executor_smoke
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/tests/test_protocol_effect_executor.cpp")
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/tests/test_protocol_effect_executor.cpp"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/third_party/nanopb/pb_common.c"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/third_party/nanopb/pb_encode.c"
${TRAIL_MATE_MESHTASTIC_GENERATED_SOURCES})
target_include_directories(trailmate_protocol_effect_executor_smoke
PRIVATE
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/include"
@@ -131,6 +147,22 @@ if(BUILD_TESTING)
add_test(NAME trailmate_protocol_effect_executor_smoke
COMMAND trailmate_protocol_effect_executor_smoke)
add_executable(trailmate_meshtastic_runtime_smoke
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/tests/test_meshtastic_runtime.cpp"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/third_party/nanopb/pb_common.c"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/third_party/nanopb/pb_decode.c"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/third_party/nanopb/pb_encode.c"
${TRAIL_MATE_MESHTASTIC_GENERATED_SOURCES})
target_include_directories(trailmate_meshtastic_runtime_smoke
PRIVATE
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/include"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/generated"
"${TRAIL_MATE_REPO_ROOT}/modules/core_chat/third_party/nanopb")
target_compile_features(trailmate_meshtastic_runtime_smoke
PRIVATE cxx_std_17)
add_test(NAME trailmate_meshtastic_runtime_smoke
COMMAND trailmate_meshtastic_runtime_smoke)
add_executable(trailmate_meshcore_protocol_strategy_smoke
"${TRAIL_MATE_REPO_ROOT}/modules/core_mesh/tests/test_meshcore_protocol_strategy.cpp"
"${TRAIL_MATE_REPO_ROOT}/modules/core_mesh/src/protocol/meshcore/meshcore_protocol_strategy.cpp"
+6 -3
View File
@@ -17,8 +17,8 @@ Primary root cause:
| --- | --- | --- | --- |
| Meshtastic NodeInfo peer reannounce | Shared policy in working tree | Medium | ESP32 and nRF now call the same reannounce gate; platform adapters still own the actual queue/send IO. |
| Meshtastic broadcast `want_response` | Shared policy in working tree | Medium | ESP32 and nRF now share the app-data destination/ACK/response decision; broadcast air ACK is suppressed while request response intent is preserved. |
| Meshtastic request/reply core | Mostly shared | Medium | NodeInfo/Position reply gating, NodeInfo/Position payload construction, TraceRoute reply gating, TraceRoute payload mutation, and TraceRoute/Position action lifecycle tracking are shared; reply radio send still lives in adapters/UI. |
| Meshtastic duplicated policy ownership | Mostly shared | Medium | App-data send intent, NodeInfo reannounce gate, NodeInfo/Position reply gates, NodeInfo self-announcement packet construction, Position payload construction, TraceRoute reply gate, TraceRoute payload mutation, TraceRoute/Position result lifecycle, and PKI/NO_CHANNEL resync decisions now live in shared runtime/policy. Radio IO and local data sources still live in adapters. |
| Meshtastic request/reply core | Mostly shared | Medium | NodeInfo/Position reply gating, NodeInfo/Position payload construction, TraceRoute/Position outgoing request packet decisions, TraceRoute reply gating, TraceRoute payload mutation, and TraceRoute/Position action lifecycle tracking are shared; radio send still lives in adapters/UI executors. |
| Meshtastic duplicated policy ownership | Mostly shared | Medium | App-data send intent, NodeInfo reannounce gate, NodeInfo/Position reply gates, NodeInfo self-announcement packet construction, Position payload construction, TraceRoute/Position outgoing request packet decisions, TraceRoute reply gate, TraceRoute payload mutation, TraceRoute/Position result lifecycle, and PKI/NO_CHANNEL resync decisions now live in shared runtime/policy. Radio IO and local data sources still live in adapters. |
| MeshCore NodeInfo query/reply | Shared runtime/effects | Medium | ESP32 and nRF now route `requestNodeInfo()` through `MeshCoreRuntime` and shared control payload codecs; platform adapters still own packet IO/projection. |
| MeshCore trace | Shared lifecycle, platform-limited routing | Medium | ESP32 and nRF use native `PAYLOAD_TYPE_TRACE` and shared completion/timeout policy; nRF still uses a minimal one-hop hash route. |
| MeshCore app-data ACK/capability | Shared lifecycle | Medium | ESP32 and nRF now declare ACK tracking only when runtime pending/completion handling is wired. ACK frame scheduling remains adapter IO. |
@@ -189,9 +189,12 @@ Current state:
- Shared `MeshtasticAppActionRuntime` now owns outgoing TraceRoute and Position Exchange
lifecycle interpretation: pending, routing delivered where available, routing error,
matching app response completed, and timeout.
- `MeshtasticRuntime::prepareOutgoing(...)` now maps nRF mono UI `TraceRouteIntent` and
`ExchangePositionIntent` into `SendPacketEffect`, so the UI no longer constructs
`TRACEROUTE_APP` / `POSITION_APP` packets or protobuf payloads directly.
- Mono UI observes `ChatService::IncomingDataObserver` instead of polling the adapter queue
directly, so TraceRoute result state does not steal app-data from BLE/other consumers.
- Adapter send mechanics still exist on ESP32 and nRF.
- Adapter/UI executor send mechanics still exist on ESP32 and nRF.
Residual risk:
@@ -321,6 +321,18 @@ It does not own:
- BLE phone protocol transport;
- memory placement or ISR details.
Current C++17 migration state:
- Meshtastic TraceRoute and Position Exchange outgoing user actions can now enter
`MeshtasticRuntime::prepareOutgoing(...)` as `TraceRouteIntent` / `ExchangePositionIntent`.
The runtime chooses Meshtastic portnum, request id fallback, ACK/response flags, and protobuf payload shape,
then emits `SendPacketEffect`.
- nRF mono UI executes those `SendPacketEffect`s through its platform mesh adapter and keeps UI-only lifecycle
projection in `MeshtasticAppActionRuntime`; it no longer constructs `TRACEROUTE_APP` /
`POSITION_APP` packets directly.
- Platform adapters still own physical radio send, local GPS source selection, BLE projection, queueing, and
adapter-side incoming packet execution until those can be represented as runtime effects/state.
### Meshtastic PKI Resync State
PKI resync must be a State object, not scattered if branches.
@@ -2,8 +2,11 @@
#include "chat/runtime/protocol_runtime.h"
#include "meshtastic/mesh.pb.h"
#include "meshtastic/portnums.pb.h"
#include "pb_encode.h"
#include <cstdint>
#include <type_traits>
namespace chat::runtime
{
@@ -86,10 +89,25 @@ class MeshtasticPkiResyncState
class MeshtasticRuntime final : public IProtocolRuntime
{
public:
ProtocolEffects prepareOutgoing(const ProtocolIntent&,
const RuntimeContext&) override
ProtocolEffects prepareOutgoing(const ProtocolIntent& intent,
const RuntimeContext& context) override
{
return ProtocolEffects{};
ProtocolEffects effects{};
std::visit(
[&effects, &context](const auto& item)
{
using Intent = std::decay_t<decltype(item)>;
if constexpr (std::is_same_v<Intent, TraceRouteIntent>)
{
resolveTraceRoute(item, context, effects);
}
else if constexpr (std::is_same_v<Intent, ExchangePositionIntent>)
{
resolveExchangePosition(item, context, effects);
}
},
intent);
return effects;
}
ProtocolEffects handleIncoming(const IncomingPacket&,
@@ -115,6 +133,113 @@ class MeshtasticRuntime final : public IProtocolRuntime
}
private:
static constexpr uint32_t kTraceRouteRequestSalt = 0x4D545254UL;
static constexpr uint32_t kPositionRequestSalt = 0x4D54504FUL;
static NodeId normalizePeer(NodeId peer)
{
return peer == 0xFFFFFFFFUL ? 0 : peer;
}
static MessageId makeRequestId(MessageId requested,
NodeId peer,
const RuntimeContext& context,
uint32_t salt)
{
if (requested != 0)
{
return requested;
}
MessageId id = context.now_ms ^ context.self_node ^ peer ^ salt;
return id == 0 ? 1 : id;
}
static EmitActionResultEffect buildFailedAction(ProtocolActionKind action,
NodeId peer,
MessageId request_id,
int32_t detail)
{
EmitActionResultEffect failed{};
failed.protocol = MeshProtocol::Meshtastic;
failed.action = action;
failed.state = ProtocolActionState::Failed;
failed.peer = peer;
failed.request_id = request_id;
failed.detail = detail;
return failed;
}
static void resolveTraceRoute(const TraceRouteIntent& intent,
const RuntimeContext& context,
ProtocolEffects& effects)
{
const NodeId peer = normalizePeer(intent.peer);
const MessageId request_id = makeRequestId(intent.request_id,
peer,
context,
kTraceRouteRequestSalt);
if (peer == 0 || peer == context.self_node)
{
effects.add(buildFailedAction(ProtocolActionKind::TraceRoute,
peer,
request_id,
-1));
return;
}
meshtastic_RouteDiscovery route = meshtastic_RouteDiscovery_init_zero;
uint8_t route_buf[96] = {};
pb_ostream_t stream = pb_ostream_from_buffer(route_buf, sizeof(route_buf));
if (!pb_encode(&stream, meshtastic_RouteDiscovery_fields, &route))
{
effects.add(buildFailedAction(ProtocolActionKind::TraceRoute,
peer,
request_id,
-2));
return;
}
SendPacketEffect packet{};
packet.protocol = MeshProtocol::Meshtastic;
packet.channel = intent.channel;
packet.dest = peer;
packet.portnum = meshtastic_PortNum_TRACEROUTE_APP;
packet.request_id = request_id;
packet.want_ack = true;
packet.want_response = true;
packet.payload.assign(route_buf, route_buf + stream.bytes_written);
effects.add(std::move(packet));
}
static void resolveExchangePosition(const ExchangePositionIntent& intent,
const RuntimeContext& context,
ProtocolEffects& effects)
{
const NodeId peer = normalizePeer(intent.peer);
const MessageId request_id = makeRequestId(intent.request_id,
peer,
context,
kPositionRequestSalt);
if (peer == 0 || peer == context.self_node)
{
effects.add(buildFailedAction(ProtocolActionKind::ExchangePosition,
peer,
request_id,
-1));
return;
}
SendPacketEffect packet{};
packet.protocol = MeshProtocol::Meshtastic;
packet.channel = intent.channel;
packet.dest = peer;
packet.portnum = meshtastic_PortNum_POSITION_APP;
packet.request_id = request_id;
packet.want_ack = false;
packet.want_response = true;
effects.add(std::move(packet));
}
MeshtasticPkiResyncState pki_resync_{};
};
@@ -55,6 +55,7 @@ struct RequestNodeInfoIntent
struct TraceRouteIntent
{
ChannelId channel = ChannelId::PRIMARY;
NodeId peer = 0;
MessageId request_id = 0;
uint32_t auth = 0;
@@ -64,7 +65,9 @@ struct TraceRouteIntent
struct ExchangePositionIntent
{
ChannelId channel = ChannelId::PRIMARY;
NodeId peer = 0;
MessageId request_id = 0;
};
struct DiscoverIntent
@@ -1,5 +1,7 @@
#include "chat/runtime/meshtastic_runtime.h"
#include "pb_decode.h"
#include <cassert>
namespace
@@ -18,14 +20,86 @@ int main()
{
using chat::ChannelId;
using chat::MeshProtocol;
using chat::runtime::EmitActionResultEffect;
using chat::runtime::ExchangePositionIntent;
using chat::runtime::ForgetPeerKeyEffect;
using chat::runtime::MeshtasticPkiResyncCause;
using chat::runtime::MeshtasticPkiResyncInput;
using chat::runtime::MeshtasticRuntime;
using chat::runtime::ProtocolActionKind;
using chat::runtime::ProtocolActionState;
using chat::runtime::RuntimeContext;
using chat::runtime::SendNodeInfoEffect;
using chat::runtime::SendPacketEffect;
using chat::runtime::SendRoutingErrorEffect;
using chat::runtime::TraceRouteIntent;
MeshtasticRuntime runtime;
RuntimeContext context{};
context.protocol = MeshProtocol::Meshtastic;
context.self_node = 0x11111111UL;
context.now_ms = 0x20240614UL;
{
TraceRouteIntent intent{};
intent.channel = ChannelId::SECONDARY;
intent.peer = 0x22222222UL;
intent.request_id = 0x01020304UL;
intent.timeout_ms = 9000;
const auto effects = runtime.prepareOutgoing(intent, context);
assert(effects.items.size() == 1);
const auto* packet = effectAt<SendPacketEffect>(effects, 0);
assert(packet);
assert(packet->protocol == MeshProtocol::Meshtastic);
assert(packet->channel == intent.channel);
assert(packet->dest == intent.peer);
assert(packet->portnum == meshtastic_PortNum_TRACEROUTE_APP);
assert(packet->request_id == intent.request_id);
assert(packet->want_ack);
assert(packet->want_response);
meshtastic_RouteDiscovery decoded = meshtastic_RouteDiscovery_init_zero;
pb_istream_t stream = pb_istream_from_buffer(packet->payload.data(),
packet->payload.size());
assert(pb_decode(&stream, meshtastic_RouteDiscovery_fields, &decoded));
}
{
ExchangePositionIntent intent{};
intent.channel = ChannelId::PRIMARY;
intent.peer = 0x33333333UL;
intent.request_id = 0x05060708UL;
const auto effects = runtime.prepareOutgoing(intent, context);
assert(effects.items.size() == 1);
const auto* packet = effectAt<SendPacketEffect>(effects, 0);
assert(packet);
assert(packet->protocol == MeshProtocol::Meshtastic);
assert(packet->channel == intent.channel);
assert(packet->dest == intent.peer);
assert(packet->portnum == meshtastic_PortNum_POSITION_APP);
assert(packet->request_id == intent.request_id);
assert(!packet->want_ack);
assert(packet->want_response);
assert(packet->payload.empty());
}
{
TraceRouteIntent intent{};
intent.peer = context.self_node;
intent.request_id = 0x0A0B0C0DUL;
const auto effects = runtime.prepareOutgoing(intent, context);
assert(effects.items.size() == 1);
const auto* failed = effectAt<EmitActionResultEffect>(effects, 0);
assert(failed);
assert(failed->protocol == MeshProtocol::Meshtastic);
assert(failed->action == ProtocolActionKind::TraceRoute);
assert(failed->state == ProtocolActionState::Failed);
assert(failed->peer == context.self_node);
assert(failed->request_id == intent.request_id);
}
{
MeshtasticPkiResyncInput input{};
+57 -29
View File
@@ -6,12 +6,11 @@
#include "chat/infra/meshcore/mc_region_presets.h"
#include "chat/infra/meshtastic/mt_protocol_helpers.h"
#include "chat/infra/meshtastic/mt_region.h"
#include "chat/runtime/meshtastic_runtime.h"
#include "chat/runtime/self_identity_policy.h"
#include "chat/usecase/chat_service.h"
#include "chat/usecase/contact_service.h"
#include "generated/meshtastic/mesh.pb.h"
#include "generated/meshtastic/portnums.pb.h"
#include "pb_encode.h"
#include "platform/ui/screen_runtime.h"
#include "ui/mono/assets/trailmate_sleep_logo.h"
#include "ui/mono/screens/screensaver_layout.h"
@@ -176,6 +175,33 @@ uint16_t normalizedMeshtasticChannelNum(uint16_t channel_num)
return std::min<uint16_t>(channel_num, kMeshtasticChannelNumMax);
}
const chat::runtime::SendPacketEffect* firstSendPacketEffect(
const chat::runtime::ProtocolEffects& effects)
{
for (const auto& effect : effects.items)
{
if (const auto* packet = std::get_if<chat::runtime::SendPacketEffect>(&effect))
{
return packet;
}
}
return nullptr;
}
bool executeSendPacketEffect(chat::IMeshAdapter& mesh,
const chat::runtime::SendPacketEffect& packet)
{
const uint8_t* payload = packet.payload.empty() ? nullptr : packet.payload.data();
return mesh.sendAppData(packet.channel,
packet.portnum,
payload,
packet.payload.size(),
packet.dest,
packet.want_ack,
packet.request_id,
packet.want_response);
}
void sanitizeMeshtasticChannelNum(app::AppConfig& cfg)
{
cfg.meshtastic_config.channel_num = normalizedMeshtasticChannelNum(cfg.meshtastic_config.channel_num);
@@ -7388,25 +7414,21 @@ void Runtime::executeNodeAction()
showTransientPopup("TRACE ROUTE", "UNAVAILABLE");
return;
}
meshtastic_RouteDiscovery route = meshtastic_RouteDiscovery_init_zero;
uint8_t route_buf[96] = {};
pb_ostream_t stream = pb_ostream_from_buffer(route_buf, sizeof(route_buf));
if (!pb_encode(&stream, meshtastic_RouteDiscovery_fields, &route))
{
appendBootLog("trace encode err");
showTransientPopup("TRACE ROUTE", "ENCODE FAILED");
return;
}
chat::runtime::MeshtasticRuntime protocol_runtime{};
chat::runtime::RuntimeContext context{};
context.protocol = chat::MeshProtocol::Meshtastic;
context.self_node = mesh->getNodeId();
context.now_ms = nowMs();
const chat::MessageId request_id = nextMeshtasticActionRequestId(node->node_id);
const bool ok = mesh->sendAppData(chat::ChannelId::PRIMARY,
meshtastic_PortNum_TRACEROUTE_APP,
route_buf,
stream.bytes_written,
node->node_id,
true,
request_id,
true);
chat::runtime::TraceRouteIntent intent{};
intent.channel = chat::ChannelId::PRIMARY;
intent.peer = node->node_id;
intent.request_id = nextMeshtasticActionRequestId(node->node_id);
const auto effects = protocol_runtime.prepareOutgoing(intent, context);
const auto* packet = firstSendPacketEffect(effects);
const chat::MessageId request_id = packet ? packet->request_id : intent.request_id;
const bool ok = packet && executeSendPacketEffect(*mesh, *packet);
if (ok)
{
meshtastic_action_runtime_.startTraceRoute(request_id, node->node_id, nowMs());
@@ -7458,15 +7480,21 @@ void Runtime::requestNodePositionExchange()
return;
}
const chat::MessageId request_id = nextMeshtasticActionRequestId(node->node_id);
const bool ok = mesh->sendAppData(chat::ChannelId::PRIMARY,
meshtastic_PortNum_POSITION_APP,
nullptr,
0,
node->node_id,
false,
request_id,
true);
chat::runtime::MeshtasticRuntime protocol_runtime{};
chat::runtime::RuntimeContext context{};
context.protocol = chat::MeshProtocol::Meshtastic;
context.self_node = mesh->getNodeId();
context.now_ms = nowMs();
chat::runtime::ExchangePositionIntent intent{};
intent.channel = chat::ChannelId::PRIMARY;
intent.peer = node->node_id;
intent.request_id = nextMeshtasticActionRequestId(node->node_id);
const auto effects = protocol_runtime.prepareOutgoing(intent, context);
const auto* packet = firstSendPacketEffect(effects);
const chat::MessageId request_id = packet ? packet->request_id : intent.request_id;
const bool ok = packet && executeSendPacketEffect(*mesh, *packet);
if (ok)
{
meshtastic_action_runtime_.startPositionExchange(request_id, node->node_id, nowMs());