wenxiangchao
5 天以前 559890c482f5973f06aafdfec9550fee5ba3c7f2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>仪器调试平台 程序架构解析报告(V3.1 · TCP 模拟器 + 统一驱动 + 多设备并发)</title>
<style>
:root{--bg:#f7f8fa;--card:#fff;--ink:#1f2430;--muted:#5b6472;--line:#e4e8ef;--accent:#2f6fed;--warn:#b7791f;--bad:#c0392b;--ok:#2e8b57;--code:#0d1117;}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.7 "Segoe UI","Microsoft YaHei",Arial,sans-serif}
.wrap{max-width:1080px;margin:0 auto;padding:32px 20px 80px}
h1{font-size:26px;margin:0 0 6px}
h2{font-size:20px;margin:34px 0 12px;padding-bottom:6px;border-bottom:2px solid var(--line)}
h3{font-size:16px;margin:20px 0 8px;color:var(--accent)}
.sub{color:var(--muted);margin:0 0 18px}
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px 18px;margin:14px 0;box-shadow:0 1px 2px rgba(0,0,0,.03)}
pre{background:var(--code);color:#e6edf3;border-radius:10px;padding:14px 16px;overflow:auto;font:13px/1.55 "Cascadia Code",Consolas,monospace;margin:10px 0}
code{font-family:"Cascadia Code",Consolas,monospace;background:#eef1f6;padding:1px 5px;border-radius:4px;font-size:13px}
pre code{background:none;padding:0;color:inherit}
table{border-collapse:collapse;width:100%;margin:10px 0;font-size:14px}
th,td{border:1px solid var(--line);padding:7px 10px;text-align:left;vertical-align:top}
th{background:#eef2f8}
.tag{display:inline-block;padding:1px 8px;border-radius:999px;font-size:12px;font-weight:600;margin-right:4px}
.t-local{background:#e6f0ff;color:#1b4fa0}
.t-broker{background:#fdeee0;color:#a35a12}
.t-server{background:#e7f6ec;color:#1f7a3d}
.ok{color:var(--ok);font-weight:600}
.bad{color:var(--bad);font-weight:600}
.warn{color:var(--warn);font-weight:600}
.small{font-size:13px;color:var(--muted)}
.toc{columns:2;font-size:14px}
.toc a{color:var(--accent);text-decoration:none}
.kv{color:#8b949e}
.hl{background:#fff6d6;border-left:4px solid var(--warn);padding:10px 12px;border-radius:6px;margin:10px 0}
.fx{background:#e8f5e9;border-left:4px solid var(--ok);padding:10px 12px;border-radius:6px;margin:10px 0}
</style>
</head>
<body>
<div class="wrap">
 
<h1>仪器调试平台 · 程序架构解析报告</h1>
<p class="sub">V3.1 结构:CAN 模拟器独立 TCP 服务 + 统一 <code>CanDeviceDriver</code> + 多设备并发调试 + 总线应答器 + 维护租约心跳。<br/>
本报告描述<b>最新代码</b>的运行逻辑,与仓库实际实现同步(2026-11-09)。</p>
 
<div class="card">
<b>目录</b>
<div class="toc">
<div><a href="#s1">1. 进程与数据拓扑</a></div>
<div><a href="#s2">2. 工程与关键文件清单</a></div>
<div><a href="#s3">3. 启动初始化时序</a></div>
<div><a href="#s4">4. 设备注册与统一驱动分派</a></div>
<div><a href="#s5">5. TCP 线协议与模拟器服务端</a></div>
<div><a href="#s6">6. 总线路由与应答器</a></div>
<div><a href="#s7">7. 调试页多标签多会话</a></div>
<div><a href="#s8">8. 维护会话状态机与租约心跳</a></div>
<div><a href="#s9">9. 报文收发全链路(Local / Broker)</a></div>
<div><a href="#s10">10. 错误处理与不变量</a></div>
<div><a href="#s11">11. 代码索引</a></div>
<div><a href="#s12">12. 已知限制</a></div>
</div>
</div>
 
<!-- ============================================================ -->
<h2 id="s1">1 进程与数据拓扑</h2>
<div class="card">
<p>系统由 <b>3 类进程 + 1 个服务进程</b> 组成,CAN 总线世界由独立服务进程统一托管:</p>
<pre>
┌───────────────────────────────── CanSimulatorServer.exe(总线权威,独立进程) ─────────────────────────────────┐
│  TcpListener 127.0.0.1:9000                                                                                      │
│   ├─ TcpSimulatorHost   接受连接,每连接一个 ServerSession                                                        │
│   ├─ ServerSession      HELLO 状态机 + 一问一答保序响应 + EV:RECV 推送(写锁串行、命令期暂存门)                 │
│   ├─ CanBusManager      按名懒建 CanBusSimulator;(bus,nodeIndex) 唯一性仲裁                                     │
│   ├─ CanCommandExecutor 命令串执行(SEND/QUERY/AUTO_RESPONSE/BUS_ECHO/FILTER/LOG/STATUS…)                       │
│   └─ CanBusSimulator    复用原路由/自动响应/总线日志 + <b>总线级应答器 EchoResponderToSender</b>                    │
│  UI:监听状态 · 节点树 · 实时帧表(TX/RX) · 统计 · 日志 · 应答器复选框                                              │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
        ▲ TCP 9000(NodeChannelTransport)              ▲ TCP 9000(宿主物理会话)
        │                                               │
┌───────┴───────────────┐   命名管道 RPC   ┌────────────┴─────────────┐
│ Plugin.IntegrationTest │ ◄──────────────► │  DeviceBroker.exe         │
│ (主程序,Local 设备)  │  aging.broker-   │ (Broker 共享设备宿主)   │
│  CAN-001/002 …         │  b01.v1          │  CAN-003/004 …            │
└────────────────────────┘                  └───────────────────────────┘
 
数据文件(%LOCALAPPDATA%\PluginIntegrationTest):
  data.db  设备定义(含 ExecutionMode=Local/Broker、BrokerId、Endpoint、PhysicalSessionKey)
  arb.db   跨进程租约仲裁(resource_state / lease_history / recovery_checkpoint / authority_meta)
  debug.db 调试会话(debug_session 状态机持久化 + 操作日志)
</pre>
<p><span class="tag t-local">Local 直连</span>客户端进程内 <code>CanDeviceDriver</code> 经 <code>NodeChannelTransport</code> 直接 TCP 连模拟器。<br/>
<span class="tag t-broker">Broker 代理</span>客户端 <code>CanDeviceDriver</code> 经 <code>BrokerRpcTransport</code> 走命名管道委托宿主,宿主内再用 <code>CanDeviceDriver(NodeChannel)</code> 连模拟器。<br/>
<span class="tag t-server">服务</span>CanSimulatorServer 是唯一的总线世界权威,所有进程的同名总线其实是<b>同一个进程内的同一总线对象</b>,因此跨进程设备天然互通。</p>
<p class="small">与旧结构的关键差异:不再有"进程内 static 总线字典",跨进程隔离问题从根上消除;CAN 驱动只剩一个类。</p>
</div>
 
<!-- ============================================================ -->
<h2 id="s2">2 工程与关键文件清单</h2>
<div class="card">
<table>
<tr><th>层</th><th>文件</th><th>职责</th></tr>
<tr><td>模拟器类库</td><td>CanSimulator\CanBusSimulator.cs · CanNode.cs · CanFrame.cs · <b>SimulatorFraming.cs</b></td><td>虚拟总线/节点/帧;新增 TCP 分帧编解码(4B 大端长度+UTF8)</td></tr>
<tr><td rowspan="4">模拟器服务端<br/>CanSimulatorServer</td><td>CanSimulatorServer.Core\TcpSimulatorHost.cs</td><td>监听/接受连接/会话登记;port=0 随机端口(测试回环)</td></tr>
<tr><td>CanSimulatorServer.Core\ServerSession.cs</td><td>单连接协议状态机、保序响应、EV 推送、命令期推送暂存门</td></tr>
<tr><td>CanSimulatorServer.Core\CanBusManager.cs</td><td>总线注册表 + (bus,node) 唯一仲裁 + 节点↔会话胶水</td></tr>
<tr><td>CanSimulatorServer.Core\CanCommandExecutor.cs</td><td>命令串执行器(含 BUS_ECHO 应答器、AUTO_RESPONSE、FILTER、LOG、STATUS)</td></tr>
<tr><td>模拟器服务 UI</td><td>CanSimulatorServer.App\App.cs · MainWindow.cs</td><td>WPF 监控窗口(节点树/帧表/统计/应答器复选框)</td></tr>
<tr><td rowspan="4">驱动层<br/>Aging.Infrastructure</td><td>Drivers\CanDeviceDriver.cs</td><td><b>唯一 CAN 驱动</b>:命令语义、接收缓冲、事件、连接状态机</td></tr>
<tr><td>Drivers\ICanTransport.cs · NodeChannelTransport.cs · BrokerRpcTransport.cs</td><td>传输后端抽象与两实现(TCP 直连 / 管道 RPC)</td></tr>
<tr><td>Drivers\DriverFactory.cs</td><td>CAN 统一产出 CanDeviceDriver</td></tr>
<tr><td>InfrastructureBootstrapper.cs</td><td>装配服务、Broker 设备注册代理驱动、启动对账</td></tr>
<tr><td>Broker 宿主</td><td>DeviceBroker\BrokerServer.cs</td><td>三重关卡 + SessionGate + 捎带推送 AppendPushes</td></tr>
<tr><td rowspan="5">调试插件<br/>Plugin.DeviceDebug</td><td>DeviceDebug.xaml(.cs)</td><td>多设备壳:设备下拉(过滤已开)+加入/关闭+TabControl</td></tr>
<tr><td>DeviceDebugTab.xaml(.cs)</td><td>单设备工作区:独立会话+面板+操作日志+维护按钮</td></tr>
<tr><td>Model\DebugSessionManager.cs</td><td>会话状态机 + <b>维护租约心跳续期</b> + 令牌缓存复用</td></tr>
<tr><td>Panels\CanDebugPanel.xaml(.cs)</td><td>CAN 多通道壳(TabControl)</td></tr>
<tr><td>Panels\CanChannelView.xaml(.cs)</td><td>单通道视图(连接/发送/接收/统计),BeginInvoke 显示</td></tr>
</table>
</div>
 
<!-- ============================================================ -->
<h2 id="s3">3 启动初始化时序</h2>
<div class="card">
<pre class="seq">
① 用户手动启动 CanSimulatorServer.exe(主程序与 Broker 都不拉起它)
   └ TcpSimulatorHost.StartAsync(9000) 监听;UI 就绪;应答器默认 OFF
 
② 启动 Plugin.IntegrationTest.exe → MainWindow.InitializePlugins(顺序敏感)
   1. DeviceConfig.InitFinish        → 建库/迁移 data.db
   2. SeedPresetCanDevices()         → 预置 CAN 设备(<b>必须先于基础设施</b>,Bootstrapper 扫库决定注册)
   3. InitializeAutoServices()
      InfrastructureBootstrapper.Initialize(data.db)
        a. DbLeaseStore(arb.db,"local-main")
        b. <u>ReconcileAfterBoot</u>       ← SweepExpired + ApproveRecovery 回收上次进程泄漏/过期租约
        c. LayeredResourceManager 注册进 UtilContainer("ResourceManager"/"LeaseCoordinator")
        d. DeviceManager/DeviceService/AutoDeviceFacade/CanBusService
        e. LoadDevicesFromConfig + RegisterBrokeredDrivers(Broker 设备→CanDeviceDriver.CreateBrokerProxy)
   4. 自动拉起 DeviceBroker.exe(存在 Broker 设备时)
      └ BrokerServer 构造时同样 ReconcileAfterBoot(broker:...)
   5. DeviceDebug.InitFinish          ← 此时 UtilContainer 就绪,能拿到仲裁服务
</pre>
<div class="hl"><b>顺序要点</b>:建库→预置设备→基础设施→Broker→DeviceDebug。若 DeviceDebug 早于基础设施,UtilContainer 取到 null 服务,维护仲裁会被静默绕过(历史缺陷,已修)。</div>
</div>
 
<!-- ============================================================ -->
<h2 id="s4">4 设备注册与统一驱动分派</h2>
<div class="card">
<p>全系统只有一个 CAN 驱动类 <code>CanDeviceDriver</code>,Local/Broker 差异下沉到构造时选择的传输后端 <code>ICanTransport</code>,上层(自动化 <code>DeviceManager.GetDriver</code>、调试面板)完全无感知。</p>
<pre>
IDeviceDriver(统一契约)
    ▲
CanDeviceDriver  ← 命令解析 / _recvBuffer / DriverFrameReceived 事件 / 连接状态机
 ├─ NodeChannelTransport   (Local)  TCP 直连模拟器:HELLO→命令一问一答→读循环分流响应/推送→断线重连
 └─ BrokerRpcTransport     (Broker) 命名管道委托宿主:SubmitBody RPC→ResultBody 态映射→解析 |PUSHED 捎带
 
分派来源:
 · DriverFactory.CreateDriver("CAN", endpoint) → new CanDeviceDriver(deviceId, "bus:node")   // Local / 宿主物理会话
 · InfrastructureBootstrapper.RegisterBrokeredDrivers → CanDeviceDriver.CreateBrokerProxy(...) // Broker 客户端
</pre>
<table>
<tr><th>场景</th><th>驱动如何产生</th><th>物理会话在哪</th></tr>
<tr><td>Local 设备(CAN-001/002)</td><td>面板/工厂 new CanDeviceDriver(NodeChannel)</td><td>本进程→TCP 连模拟器</td></tr>
<tr><td>Broker 设备(CAN-003/004) 客户端</td><td>Bootstrapper 注册 CreateBrokerProxy(BrokerRpc)</td><td>宿主进程</td></tr>
<tr><td>Broker 设备 宿主侧</td><td>BrokerServer.EnsureConnectedAsync→DriverFactory→CanDeviceDriver(NodeChannel)</td><td>宿主→TCP 连模拟器</td></tr>
</table>
<p class="small">关键:<b>全系统只有一份"节点通道驱动"代码</b>——Broker 模式下宿主内部的物理会话同样是 CanDeviceDriver(NodeChannel)。</p>
</div>
 
<!-- ============================================================ -->
<h2 id="s5">5 TCP 线协议与模拟器服务端</h2>
<div class="card">
<h3>5.1 分帧</h3>
<p>4 字节<b>大端</b>长度前缀 + UTF8 文本行(<code>SimulatorFraming</code>)。读侧循环读满头部与帧体,天然处理粘包/半包;长度非法抛 <code>InvalidDataException</code>;EOF 返回 null 触发断线感知。</p>
<h3>5.2 消息类型</h3>
<table>
<tr><th>方向</th><th>消息</th><th>含义</th></tr>
<tr><td rowspan="4">C→S</td><td><code>HELLO:{bus}:{nodeIndex}:{tag}</code></td><td>握手并挂载 (bus,nodeIndex),激活节点、开始收帧</td></tr>
<tr><td><code>SEND:{frameId}:{data}</code></td><td>发帧(命令串透传)</td></tr>
<tr><td><code>QUERY:{命令串}</code></td><td>RECV/RECV_ALL/STATUS/AUTO_RESPONSE/BUS_ECHO/FILTER/LOG/BUS_STATUS…</td></tr>
<tr><td><code>DISCONNECT</code></td><td>注销本连接节点</td></tr>
<tr><td rowspan="3">S→C</td><td><code>OK:{payload}</code> / <code>ERR:{code}:{msg}</code></td><td>对上一条请求的响应(恰好一行,保序)</td></tr>
<tr><td><code>EV:RECV:{id}:{data}:{ts}:{src}</code></td><td>总线路由推送帧(主动推送,src 可为节点号或 RESPONDER)</td></tr>
<tr><td><code>EV:STATE:…</code></td><td>预留扩展</td></tr>
</table>
<h3>5.3 ServerSession 保序与暂存门</h3>
<pre>
读循环(单线程): 逐行读请求 → Process() → 响应经"写锁"发出 → FlushHeldPushes()
命令处理期间路由产生的 EV:RECV 先进 _heldPushes(满 1000 丢最老),
响应写完后再补发 → 保证客户端"首行必为响应、推送随后"。
空闲期推送:写锁串行即发即走。
</pre>
<p class="small">错误码:<code>ERR:NODE_BUSY</code>(同 bus 同 nodeIndex 被占)、<code>ERR:NOT_HANDSHAKED</code>、<code>ERR:UNKNOWN_CMD</code>、<code>ERR:BAD_FRAME</code>。</p>
</div>
 
<!-- ============================================================ -->
<h2 id="s6">6 总线路由与应答器</h2>
<div class="card">
<p>两种"自动回复"能力,语义不同、互不干扰:</p>
<table>
<tr><th>能力</th><th>触发者</th><th>回复给谁</th><th>开关</th></tr>
<tr><td>节点自动响应</td><td><b>接收</b>节点(收到帧)</td><td>其它节点 + 自身邮箱</td><td>通道面板"自动响应"(<code>AUTO_RESPONSE:ON</code>,写节点属性)</td></tr>
<tr><td><b>总线应答器</b>(新)</td><td>总线本身</td><td><b>发送者自己</b>(同帧回显,src=RESPONDER)</td><td>服务窗口复选框 / <code>QUERY:BUS_ECHO:ON</code>(写 <code>CanBusSimulator.EchoResponderToSender</code>)</td></tr>
</table>
<pre>
OnNodeFrameSent(frame, src=node0):
  1) 路由到其它节点;每个接收节点若 AutoResponseEnabled → SendAutoResponse(回它收到的帧)
  2) 若 EchoResponderToSender:把 frame 复制、SourceNodeId="RESPONDER",直接投递回<b>发送节点自身</b>
     → 发送节点 FrameReceived → ServerSession 推送 EV:RECV 回该连接 → 发送者收到"回显"
</pre>
<div class="fx"><b>意义</b>:单客户端调试时也能"发一条收到一条回复",无需第二节点在线——这正是把 CAN-003 这类共享设备当作被测设备验证请求/响应的关键能力。</div>
</div>
 
<!-- ============================================================ -->
<h2 id="s7">7 调试页多标签多会话</h2>
<div class="card">
<pre>
DeviceDebug(壳,SimpleMaskControl 插件入口)
 ├─ 顶栏:设备下拉(仅列<b>未打开</b>设备) + [加入调试] + [关闭当前页] + 刷新
 └─ TabControl
     └─ DeviceDebugTab × N(每个 = 一个设备,独立会话/租约/面板/日志)
          ├─ 信息栏(设备ID/型号/连接/版本/预约/会话状态)
          ├─ [进入维护控制] [退出维护]
          ├─ panelContainer:LoadPanel(kind)
          │     CanDevice → CanDebugPanel(多通道壳) → CanChannelView(单通道)
          │     PowerSupply / PLC → 对应面板
          └─ 操作日志(本会话)
</pre>
<p>规则:<b>一会一设备</b>——加入时若设备已开则聚焦其 Tab;下拉过滤掉已开设备;关闭 Tab 调 <code>CloseSessionSilently()</code> 释放会话与租约,设备重新回到可选。CAN 通道内部再套一层 <code>CanDebugPanel</code> TabControl,可对同设备开多通道。</p>
<div class="hl"><b>接收显示线程模型</b>:CanChannelView 帧上屏用 <code>Dispatcher.BeginInvoke</code>(异步)。曾因用同步 <code>Invoke</code>,在读循环线程推送骤增时与"UI 线程阻塞等 RPC 响应"形成环形等待 → 开应答器即冻结(已修)。</div>
</div>
 
<!-- ============================================================ -->
<h2 id="s8">8 维护会话状态机与租约心跳</h2>
<div class="card">
<pre class="seq">
OpenSession(deviceId) → ReadOnly
   │ RequestMaintenance:ReserveAsync(复用缓存 ownerToken) 拿维护租约(TTL=300s)
   ▼
AcquiringMaintenance → ActivateMaintenance → <b>MaintenanceActive</b>
   │  └ 启动租约心跳 Timer:每 120s 对每个 MaintenanceActive 会话
   │        Coordinator.Renew(grant, 300) 续期,防 TTL 过期
   │ 退出维护 / 关闭 Tab:CloseSession
   ▼
Closing → Closed(ReleaseReservationAsync 用同一 ownerToken 释放租约;无维护会话则停心跳)
</pre>
<table>
<tr><th>机制</th><th>解决的问题</th></tr>
<tr><td>ownerToken 按 sessionId 缓存复用</td><td>租约按 TokenId(GUID) 索引;每次 new 会导致释放落空→租约泄漏→过期 RecoveryRequired 永久阻塞</td></tr>
<tr><td><b>维护租约心跳续期</b>(新)</td><td>长时间维护会话到 300s TTL 自然过期→Broker 栅栏 RECOVERY_REQUIRED 禁止 I/O;心跳每 120s 续期,永不触达过期线</td></tr>
<tr><td>启动 ReconcileAfterBoot</td><td>进程重启自动 SweepExpired+ApproveRecovery,清掉上次残留/过期租约(含已卡 RecoveryRequired 的)</td></tr>
<tr><td>死会话回收</td><td>DB 找回的未关闭会话强制标 Closed 再新建,避免 RequestMaintenance"会话不存在"</td></tr>
</table>
</div>
 
<!-- ============================================================ -->
<h2 id="s9">9 报文收发全链路</h2>
<div class="card">
<h3>9.1 发送 — Local 设备</h3>
<pre>
CanChannelView.发送 → CanDeviceDriver.SendCommandAsync("SEND:0x300:AA BB")
 → NodeChannelTransport.TransceiveAsync → TCP 写 "SEND:0x300:AA BB"
 → ServerSession 处理 → CanCommandExecutor.HandleSend → CanNode.SendFrame → CanBusSimulator 路由
 → 回 "OK:TX:0x300:2" → 驱动返回;面板加 TX 行
</pre>
<h3>9.2 发送 — Broker 设备(CAN-003)</h3>
<pre>
面板 → CanDeviceDriver(BrokerRpc).SendCommandAsync("SEND:…")
 → BrokerRpcTransport 映射 opCode=SEND,payload=整串 → SubmitBody 经命名管道
 → BrokerServer.ExecuteOneShotAsync:
     ① 白名单+域内设备  ② configHash 对账(INV-04)  ③ 租约栅栏(INV-01/03)  ④ SessionGate 串行
     ⑤ 宿主内 CanDeviceDriver(NodeChannel).SendCommandAsync → TCP → 模拟器路由
     ⑥ ResultBody.Readback 末尾 <b>AppendPushes</b>:把宿主驱动自上次以来收到的 EV:RECV
        以 "|PUSHED:n|EV:RECV…" 拼上 → 同管道返回
 → 客户端 BrokerRpcTransport 剥离 |PUSHED 段并触发 FramePushed
</pre>
<h3>9.3 接收 — 统一事件模型</h3>
<pre>
服务端路由/应答器产生帧 → 目标节点 FrameReceived → ServerSession 推送 EV:RECV
 ├─ Local:NodeChannel 读循环收到 EV:RECV → CanDeviceDriver.OnFramePushed
 │           → _recvBuffer + _pendingPushLines + <b>DriverFrameReceived</b> → 面板 BeginInvoke 上屏
 └─ Broker:宿主读循环收到 → 宿主驱动 _pendingPushLines 暂存
            客户端下一次 STATUS/SEND 轮询时,宿主 AppendPushes 捎带 → 客户端 FramePushed → 事件 → 上屏
(面板 PollTimer 每 100ms 调 QueryAsync("STATUS"):既刷新统计,也触发 Broker 捎带 drain)
</pre>
<div class="fx">结果:Local 与 Broker 设备共享同一套事件驱动接收,实时且无重复行;应答器开启后 Broker 设备也能"发一条收一条回显"(实测 50 次连续发送,每次 1 TX + 回显 RX,无错误无冻结)。</div>
</div>
 
<!-- ============================================================ -->
<h2 id="s10">10 错误处理与不变量</h2>
<div class="card">
<table>
<tr><th>不变量</th><th>含义</th><th>落点</th></tr>
<tr><td>INV-01</td><td>无租约禁止 I/O</td><td>BrokerServer 栅栏:SEND/写类 QUERY 需 Active 租约;CONNECT/DISCONNECT 与只读 drain(RECV/STATUS/LOG/BUS_STATUS) 豁免</td></tr>
<tr><td>INV-03</td><td>过期不自动让渡</td><td>租约过期→RecoveryRequired,拒绝 I/O,需 ApproveRecovery(心跳续期避免误触)</td></tr>
<tr><td>INV-04</td><td>配置哈希对账</td><td>RPC 携带 configHash,与宿主生效哈希不符→RejectedConfigMismatch(改配置须先 Apply)</td></tr>
<tr><td>INV-06</td><td>结果未知不得假定未执行</td><td>超时/在途异常→Unknown→客户端抛 TimeoutException,禁止自动重试 SEND</td></tr>
</table>
<table>
<tr><th>故障</th><th>行为</th></tr>
<tr><td>模拟器服务不可达</td><td>NodeChannel.ConnectAsync 抛含"请先启动 CanSimulatorServer.exe"的连接异常</td></tr>
<tr><td>中途断线</td><td>IsConnected=false→指数退避重连(0.5~5s)→重发 HELLO;期间发送快速失败 NOT_CONNECTED</td></tr>
<tr><td>同 (bus,node) 冲突</td><td>HELLO 返回 ERR:NODE_BUSY,驱动抛"节点编号 X 已被占用"(属配置错误,引导改端点)</td></tr>
<tr><td>推送暂存满</td><td>丢最老并计数;客户端 STATUS/RECV 兜底补账</td></tr>
<tr><td>维护租约过期</td><td>心跳续期避免;若进程崩溃遗留,重启 ReconcileAfterBoot 自动回收</td></tr>
</table>
</div>
 
<!-- ============================================================ -->
<h2 id="s11">11 代码索引</h2>
<div class="card">
<table>
<tr><th>关注点</th><th>位置</th></tr>
<tr><td>TCP 分帧</td><td>CanSimulator\SimulatorFraming.cs(Encode/Decode/ReadFrameAsync)</td></tr>
<tr><td>服务端会话/协议</td><td>CanSimulatorServer.Core\ServerSession.cs(Process/HandleHello/OnNodeFrame/FlushHeldPushes)</td></tr>
<tr><td>命令执行/应答器</td><td>CanSimulatorServer.Core\CanCommandExecutor.cs(HandleSend/HandleQuery/BUS_ECHO/FormatPush)</td></tr>
<tr><td>路由/自动响应/应答器</td><td>CanSimulator\CanBusSimulator.cs(OnNodeFrameSent/SendAutoResponse/EchoResponderToSender)</td></tr>
<tr><td>统一驱动</td><td>Aging.Infrastructure\Drivers\CanDeviceDriver.cs(Connect/Send/Query/OnFramePushed/DrainPendingPushLines)</td></tr>
<tr><td>TCP 后端</td><td>Drivers\NodeChannelTransport.cs(HandshakeAsync/ReadLoopAsync/重连)</td></tr>
<tr><td>RPC 后端</td><td>Drivers\BrokerRpcTransport.cs(TransceiveAsync/DrainPiggyback/ThrowForState)</td></tr>
<tr><td>Broker 捎带</td><td>DeviceBroker\BrokerServer.cs(ExecuteOneShotAsync/RunFragmentAsync/AppendPushes)</td></tr>
<tr><td>多设备壳/工作区</td><td>Plugin.DeviceDebug\DeviceDebug.xaml.cs · DeviceDebugTab.xaml.cs</td></tr>
<tr><td>会话状态机/心跳</td><td>Plugin.DeviceDebug\Model\DebugSessionManager.cs(RequestMaintenance/ActivateMaintenance/RenewActiveMaintenance)</td></tr>
<tr><td>通道显示</td><td>Panels\CanDebugPanel.xaml.cs(壳)· Panels\CanChannelView.xaml.cs(AddReceivedFrame/BeginInvoke)</td></tr>
<tr><td>装配/注册</td><td>Aging.Infrastructure\InfrastructureBootstrapper.cs(RegisterBrokeredDrivers)· Drivers\DriverFactory.cs</td></tr>
</table>
</div>
 
<!-- ============================================================ -->
<h2 id="s12">12 已知限制与后续</h2>
<div class="card">
<ul>
<li><b>Broker 接收实时性为"请求驱动"</b>:靠宿主 STATUS/SEND 轮询捎带 drain,非纯推送;面板 100ms 轮询保证最终一致。真实硬件无此层。</li>
<li><b>owner↔clientInstanceId 弱对齐</b>:同机 arb.db 仅存在性/相位校验,未做签名凭据(V3.0 §13 P2)。</li>
<li><b>应答器为总线级</b>:一次开关作用于该总线所有发送者;如需按节点/按帧ID 精细映射为后续项。</li>
<li><b>UI 待办</b>:临时诊断日志撤除;RecoveryRequired 人工审批 UI(现由启动对账代偿);App.Success/Warn 标题本地化。</li>
<li><b>构建</b>:WPF 链路须用 VS 完整版 MSBuild(不加 /p:Platform=x64);模拟器核心可 dotnet 构建/测试。</li>
</ul>
<p class="small">本报告与代码同步于 2026-11-09;实现变更请同步更新本文件。</p>
</div>
 
</div>
</body>
</html>