<!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>
|