From de1b1eb08037a1fc5d0d4c98ee8854bdbd52f0d2 Mon Sep 17 00:00:00 2001 From: liu weikai Date: Sun, 14 Jun 2026 10:26:49 +0800 Subject: [PATCH] docs(protocol): require facade boundary in runtime spec --- .../PROTOCOL_RUNTIME_DESIGN_SPEC.md | 223 ++++++++++++++++-- 1 file changed, 202 insertions(+), 21 deletions(-) diff --git a/docs/specification/PROTOCOL_RUNTIME_DESIGN_SPEC.md b/docs/specification/PROTOCOL_RUNTIME_DESIGN_SPEC.md index 15714860..03590b1f 100644 --- a/docs/specification/PROTOCOL_RUNTIME_DESIGN_SPEC.md +++ b/docs/specification/PROTOCOL_RUNTIME_DESIGN_SPEC.md @@ -48,6 +48,32 @@ Status date: 2026-06-14 本文只采用少数 GoF 23 设计模式作为主轴,避免“模式堆砌”。 +### Design Verdict + +本规格接受原始改造设想作为约束基线:Trail Mate 的协议层改造不是把若干 helper +抽到 shared 目录,而是建立一条清晰的用例入口到平台执行链路: + +```text +UI / ChatService + -> MeshProtocolFacade + -> ProtocolIntent + -> IProtocolRuntime strategy + -> Protocol State + -> ProtocolEffects + -> IProtocolEffectExecutor bridge + -> Platform adapters +``` + +其中 `Strategy + Command + State + Bridge + Adapter` 是主轴。如果这五个没有立住, +`Visitor / Chain of Responsibility / Facade / Factory / Builder` 都只能算局部工具,不能宣称 +协议 runtime 改造完成。 + +`Facade` 虽然不是前五个主轴之一,但它是上层用例入口边界,不能只存在于图里。规格中出现 +`MeshProtocolFacade` 时,表示必须有代码级对象或等价命名的 concrete boundary。该对象负责给 +UI / ChatService 暴露稳定用例 API,并隐藏 runtime 选择、effect 执行、TX 失败回灌和 action +result 投影的编排细节。若未来决定不用 `MeshProtocolFacade` 这个名字,必须先修改本规格, +明确新的等价代码对象和验收条件。 + ### Language Baseline 目标方向是现代 C++,长期可以提升到 C++20;但当前 ESP32 / nRF52 的 PlatformIO @@ -77,8 +103,8 @@ C++20,并考虑引入 concepts / `std::span` / 更强类型约束。 | --- | --- | --- | | Visitor | `std::variant` + `std::visit` 的 effect dispatch | 只用于 executor/test 处理 effects,不扩散到 UI | | Chain of Responsibility | incoming packet 分类管线:decrypt -> routing -> nodeinfo -> position -> trace -> text/appdata | handler 顺序必须写进 runtime 或 spec,不能形成隐式吞包黑洞 | -| Facade | `MeshProtocolFacade` 给 UI/ChatService 的入口 | 只做用例门面,不沉淀协议语义 | -| Abstract Factory / Factory Method | product composition 选择 runtime + executor + codec | 用静态对象/引用也可以,不强制动态分配 | +| Facade | `MeshProtocolFacade` 给 UI/ChatService 的入口 | 必须落成真实代码边界;只做用例门面,不沉淀协议语义 | +| Abstract Factory / Factory Method | product composition 选择 facade + runtime + executor + codec | 用静态对象/引用也可以,不强制动态分配 | | Builder | packet/control payload 构造 | 局部用于 codec/build request,不能替代 runtime | ### Patterns To Avoid As Main Axis @@ -118,6 +144,42 @@ Runtime 是协议真相所在: - `MeshCoreRuntime` 解释 MeshCore Intent、incoming frame、trace path、NodeInfo control、tick; - runtime 可以拥有 State,但不能拥有平台 IO。 +### Facade + +Facade 是 UI / ChatService 面对协议系统的稳定用例入口。它不是 protocol runtime 的别名,也不是 +adapter 的包装名。 + +`MeshProtocolFacade` 必须拥有或聚合: + +- 当前协议的 `IProtocolRuntime` strategy; +- 对应平台的 `IProtocolEffectExecutor` bridge; +- 构造 `RuntimeContext` 所需的 clock / self node / protocol facts provider; +- 将 `ProtocolEffects` 执行、记录、回灌 `TxResult`、并向上层返回 app-facing result 的编排逻辑。 + +Facade 必须暴露的是用例动作,而不是 wire 细节: + +- `startTraceRoute(peer)` +- `exchangePosition(peer)` +- `requestNodeInfo(peer, wantResponse)` +- `sendText(channel, peer, text)` +- `sharePosition(...)` +- `shareWaypoint(...)` +- `handleIncoming(packet)` +- `handleTxResult(result)` +- `tick(nowMs)` + +Facade 不得: + +- 直接选择 Meshtastic portnum 或 MeshCore payload type; +- 编码 protobuf / MeshCore frame; +- 决定 PKI resync、NodeInfo reply、TraceRoute completion、ACK timeout 等协议语义; +- 执行 radio/storage/BLE 细节; +- 变成新的 God Object 或 Mediator。 + +Facade 的职责是隔离上层与协议编排。上层不应该直接拼 `ProtocolIntent` 后调用 runtime, +也不应该直接遍历 `ProtocolEffects` 后调用 executor。过渡期允许少量平台 adapter 仍然这样做, +但每个 active UI / ChatService 入口必须有迁入 `MeshProtocolFacade` 的计划和测试。 + ### Effect Effect 是 runtime 要求外部世界发生的动作: @@ -154,6 +216,64 @@ Codec 只负责编解码: - `MeshCoreCodec`:frame/header/direct data/group data/control/trace; - codec 可以使用 Builder 风格的 build request,但不得保存业务状态。 +### Product Composition / Factory + +产品组合阶段负责把 `MeshProtocolFacade`、`IProtocolRuntime`、`IProtocolEffectExecutor`、codec 和平台 +facts provider 接起来。这里使用 Abstract Factory / Factory Method 的意图是避免每个 UI 或 adapter +手动拼装协议对象。 + +嵌入式平台不要求动态分配。允许使用静态对象和返回引用的工厂: + +```cpp +struct ProtocolRuntimeBundle +{ + MeshProtocolFacade* facade; + IProtocolRuntime* runtime; + IProtocolEffectExecutor* executor; +}; + +ProtocolRuntimeBundle protocolRuntimeFor(MeshProtocol protocol); +``` + +Factory 可以知道产品/平台能力,但不得决定协议语义。它选择“用哪套 runtime/executor”,不决定 +“该不该回复 NodeInfo”或“TraceRoute 何时完成”。 + +### Incoming Handler Chain + +Incoming packet 的分类可以局部使用 Chain of Responsibility,但顺序必须显式,不能形成隐式吞包。 +handler 可以解密、分类、更新状态、产生 effects,但必须使用统一返回语义: + +```cpp +enum class PacketHandling +{ + NotHandled, + HandledContinue, + HandledStop, + DropWithEffects, +}; +``` + +Meshtastic incoming chain 的概念顺序必须是: + +1. wire/Data 解码与 channel / PKI 解密事实建立; +2. routing app / routing error; +3. NodeInfo; +4. Position; +5. TraceRoute; +6. KeyVerification; +7. text / generic app-data projection。 + +MeshCore incoming chain 的概念顺序必须是: + +1. frame/header/direct/group 解码与 peer identity facts 建立; +2. ACK; +3. control: discover / NodeInfo; +4. trace; +5. text / app-data projection。 + +这些 handler 可以先以函数存在,也可以之后拆成对象;但顺序和吞包语义必须属于 runtime/codec +边界,不能散落在平台 adapter 的任意 if 分支里。 + ## Primary Class Model ```mermaid @@ -163,8 +283,11 @@ classDiagram +exchangePosition(NodeId peer) +requestNodeInfo(NodeId peer, bool wantResponse) +sendText(ChannelId channel, NodeId peer, string text) - +onIncomingBytes(bytes) - +tick(nowMs) + +sharePosition(PositionSnapshot position) + +shareWaypoint(WaypointSnapshot waypoint) + +handleIncoming(IncomingPacket packet) + +handleTxResult(TxResult result) + +tick(uint32_t nowMs) } class IProtocolRuntime { @@ -204,9 +327,16 @@ classDiagram class NrfProtocolExecutor class RecordingProtocolExecutor + class ProtocolRuntimeFactory { + +protocolRuntimeFor(MeshProtocol protocol) ProtocolRuntimeBundle + } + class MeshtasticCodec class MeshCoreCodec + ProtocolRuntimeFactory ..> MeshProtocolFacade + ProtocolRuntimeFactory ..> IProtocolRuntime + ProtocolRuntimeFactory ..> IProtocolEffectExecutor MeshProtocolFacade o-- IProtocolRuntime MeshProtocolFacade o-- IProtocolEffectExecutor IProtocolRuntime <|.. MeshtasticRuntime @@ -225,8 +355,11 @@ classDiagram ```mermaid flowchart LR - UI[UI / ChatService] -->|Command: ProtocolIntent| Facade[MeshProtocolFacade] - Facade -->|Strategy dispatch| Runtime[IProtocolRuntime] + UI[UI / ChatService] -->|Use-case command| Facade[MeshProtocolFacade] + PlatformIn[Platform packet source] -->|IncomingPacket / TxResult / tick| Facade + Facade -->|build ProtocolIntent| Intent[ProtocolIntent] + Intent -->|Strategy dispatch| Runtime[IProtocolRuntime] + Facade -->|direct incoming / tx / tick| Runtime Runtime --> MT[MeshtasticRuntime] Runtime --> MC[MeshCoreRuntime] MT -->|State| MTState[ACK / TraceRoute / Position / PKI states] @@ -235,7 +368,8 @@ flowchart LR MC -->|Codec dependency| MCCodec[MeshCoreCodec] MT -->|Effects| Effects[ProtocolEffects] MC -->|Effects| Effects - Effects -->|Visitor dispatch| Executor[IProtocolEffectExecutor] + Effects -->|returned to Facade| Facade + Facade -->|Visitor dispatch| Executor[IProtocolEffectExecutor] Executor -->|Bridge implementation| Esp[ESP executor] Executor -->|Bridge implementation| Nrf[nRF executor] Esp -->|Adapter| EspIO[ESP radio / storage / BLE] @@ -258,6 +392,7 @@ sequenceDiagram participant Platform as Platform IO User->>Facade: startTraceRoute(peer) + Facade->>Facade: build TraceRouteIntent + RuntimeContext Facade->>Runtime: prepareOutgoing(TraceRouteIntent) Runtime->>State: allocate request id and enter pending state Runtime-->>Facade: ProtocolEffects(SendPacket/SendTraceRoute) @@ -267,7 +402,13 @@ sequenceDiagram Codec-->>Executor: EncodedPacket Executor->>Platform: radio/storage/event operation Platform-->>Executor: ok/fail + Executor-->>Facade: EffectExecutionResult end + opt tx failure / tx metadata + Facade->>Runtime: handleTxResult(TxResult) + Runtime-->>Facade: ProtocolEffects(EmitActionResultEffect...) + end + Facade-->>User: ActionHandle / app-facing result ``` ### Incoming Packet @@ -276,15 +417,17 @@ sequenceDiagram sequenceDiagram autonumber participant Platform as Platform IO - participant Executor as Platform Adapter + participant Adapter as Platform Adapter + participant Facade as MeshProtocolFacade participant Runtime as IProtocolRuntime participant Chain as Incoming Handler Chain participant State as Protocol State - participant Facade as MeshProtocolFacade participant UI as UI/ChatService - Platform->>Executor: raw packet/frame - Executor->>Runtime: handleIncoming(IncomingPacket) + Platform->>Adapter: raw packet/frame + Adapter->>Adapter: decode/decrypt enough to form IncomingPacket facts + Adapter->>Facade: handleIncoming(IncomingPacket) + Facade->>Runtime: handleIncoming(IncomingPacket) Runtime->>Chain: decrypt/classify/route handlers alt NodeInfo request Chain->>State: apply NodeInfo rule @@ -296,8 +439,8 @@ sequenceDiagram Chain->>State: complete trace action State-->>Runtime: EmitActionResultEffect end - Runtime-->>Executor: ProtocolEffects - Executor-->>Facade: publish text/data/action result effects + Runtime-->>Facade: ProtocolEffects + Facade->>Adapter: execute platform effects through executor Facade-->>UI: stable app-facing result ``` @@ -479,19 +622,55 @@ This split intentionally leaves direct ACK burst scheduling and multi-ACK frame radio execution details. The question "is this local send still pending, completed, failed, or timed out" belongs to `MeshCoreRuntime`. +## Spec Conformance Criteria + +不能只因为 runtime helper 被抽出来就宣称改造完成。满足本规格必须同时满足以下条件: + +1. `IProtocolRuntime`、`MeshtasticRuntime`、`MeshCoreRuntime` 是真实代码对象,并有 shared tests 覆盖 + outgoing / incoming / tx result / tick。 +2. `ProtocolIntent` 是 UI / usecase 进入协议系统的命令模型;active UI / ChatService 入口不得直接制造 + portnum、payload type、protobuf bytes 或 MeshCore control frame。 +3. 跨时间协议会话必须由 runtime state 拥有。UI 和 adapter 不得独立维护 TraceRoute / Position + exchange / PKI resync / ACK completion 的状态转移。 +4. `IProtocolEffectExecutor` 或等价 executor bridge 是真实代码对象;平台只执行 `ProtocolEffect`,不决定 + 协议语义。 +5. `MeshProtocolFacade` 或明确命名的等价 facade 是真实代码对象。它必须是 UI / ChatService 面向协议系统的 + 稳定用例入口,负责选择 runtime、构造 intent、执行 effects、回灌 tx result、返回 app-facing result。 +6. Product factory / composition 不能散落在 UI 中。平台可以使用静态对象,但对象选择必须集中在产品组合层。 +7. Incoming handler chain 的顺序和吞包语义必须写进 runtime/codec 边界,不能只靠平台 adapter 巨型 if + 分支维持。 +8. 如果规格中的核心类名只存在于 Mermaid 图中,而代码里没有真实对象或等价声明,则该项未完成。 + +## Current Implementation Gaps + +截至本规格日期,runtime / effect 主干已经部分落地,但以下缺口仍然阻止本规格被判定为完成: + +1. `MeshProtocolFacade` 还没有真实代码对象。当前若干 active UI / platform path 仍直接构造 + `ProtocolIntent`、调用 `IProtocolRuntime`、遍历 `ProtocolEffects` 或手动处理 TX failure 回灌。 +2. Product composition 还没有统一的 factory / bundle 边界。平台代码各自持有 runtime / executor, + 组合逻辑仍可能继续散落。 +3. Incoming handler chain 尚未以 `PacketHandling` 或等价返回语义显式化。部分平台 adapter 仍保留 + 巨型 decode/classify 分支,虽然其中若干协议决策已经下沉到 runtime/policy。 +4. `RecordingProtocolExecutor` 已覆盖 effect 可记录性,但还缺少 `MeshProtocolFacade` 级别的 smoke, + 用来证明 UI / ChatService 可以只通过 facade 发起 action 并观察 app-facing result。 + ## Migration Rules 1. Add shared contracts first: Intent, Effect, Runtime, Executor. -2. Add test-only `RecordingProtocolExecutor` to prove effects are inspectable. -3. Move one state machine at a time: +2. Add a real Facade boundary before claiming UI / ChatService decoupling: + - first create `MeshProtocolFacade` or update this spec with the exact equivalent code object; + - facade must hide runtime selection and effect execution from active UI / ChatService paths; + - add smoke tests that fail if the facade object disappears. +3. Add test-only `RecordingProtocolExecutor` to prove effects are inspectable. +4. Move one state machine at a time: - Meshtastic PKI resync; - Meshtastic TraceRoute/Position action lifecycle; - Meshtastic NodeInfo/Position reply packet construction; - MeshCore NodeInfo control payload build/parse; - MeshCore trace action lifecycle. -4. Replace adapter logic with `executeEffect(...)` calls only after the shared state test exists. -5. Update `MeshCapabilities` when a protocol action becomes truly shared and executable. -6. Keep existing helper functions only as codec/building utilities, not as orchestration owners. +5. Replace adapter logic with `executeEffect(...)` calls only after the shared state test exists. +6. Update `MeshCapabilities` when a protocol action becomes truly shared and executable. +7. Keep existing helper functions only as codec/building utilities, not as orchestration owners. ## Review Checklist @@ -501,9 +680,11 @@ Before changing protocol code: 2. Name the GoF pattern involved and why it is being used. 3. If changing a platform adapter, verify the change is only executor/adapter work. 4. If changing protocol semantics, put the rule in shared runtime and add a shared test. -5. Update this spec when the pattern boundary changes. -6. Update `PROTOCOL_ADAPTER_DRIFT_AUDIT.md` when a drift item is resolved or accepted. -7. Run GitNexus impact analysis before edits and `detect-changes` before commit. +5. If the spec names a core class or boundary, verify the code contains that object or an explicitly documented + equivalent. Mermaid-only architecture does not count as implementation. +6. Update this spec when the pattern boundary changes. +7. Update `PROTOCOL_ADAPTER_DRIFT_AUDIT.md` when a drift item is resolved or accepted. +8. Run GitNexus impact analysis before edits and `detect-changes` before commit. ## Relationship To Other Specs