仪器调试平台 · 程序架构解析报告

V3.1 结构:CAN 模拟器独立 TCP 服务 + 统一 CanDeviceDriver + 多设备并发调试 + 总线应答器 + 维护租约心跳。
本报告描述最新代码的运行逻辑,与仓库实际实现同步(2026-11-09)。

目录

1 进程与数据拓扑

系统由 3 类进程 + 1 个服务进程 组成,CAN 总线世界由独立服务进程统一托管:

┌───────────────────────────────── 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    复用原路由/自动响应/总线日志 + 总线级应答器 EchoResponderToSender                    │
│  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 状态机持久化 + 操作日志)

Local 直连客户端进程内 CanDeviceDriver 经 NodeChannelTransport 直接 TCP 连模拟器。
Broker 代理客户端 CanDeviceDriver 经 BrokerRpcTransport 走命名管道委托宿主,宿主内再用 CanDeviceDriver(NodeChannel) 连模拟器。
服务CanSimulatorServer 是唯一的总线世界权威,所有进程的同名总线其实是同一个进程内的同一总线对象,因此跨进程设备天然互通。

与旧结构的关键差异:不再有"进程内 static 总线字典",跨进程隔离问题从根上消除;CAN 驱动只剩一个类。

2 工程与关键文件清单

层文件职责
模拟器类库CanSimulator\CanBusSimulator.cs · CanNode.cs · CanFrame.cs · SimulatorFraming.cs虚拟总线/节点/帧;新增 TCP 分帧编解码(4B 大端长度+UTF8)
模拟器服务端
CanSimulatorServer
CanSimulatorServer.Core\TcpSimulatorHost.cs监听/接受连接/会话登记;port=0 随机端口(测试回环)
CanSimulatorServer.Core\ServerSession.cs单连接协议状态机、保序响应、EV 推送、命令期推送暂存门
CanSimulatorServer.Core\CanBusManager.cs总线注册表 + (bus,node) 唯一仲裁 + 节点↔会话胶水
CanSimulatorServer.Core\CanCommandExecutor.cs命令串执行器(含 BUS_ECHO 应答器、AUTO_RESPONSE、FILTER、LOG、STATUS)
模拟器服务 UICanSimulatorServer.App\App.cs · MainWindow.csWPF 监控窗口(节点树/帧表/统计/应答器复选框)
驱动层
Aging.Infrastructure
Drivers\CanDeviceDriver.cs唯一 CAN 驱动:命令语义、接收缓冲、事件、连接状态机
Drivers\ICanTransport.cs · NodeChannelTransport.cs · BrokerRpcTransport.cs传输后端抽象与两实现(TCP 直连 / 管道 RPC)
Drivers\DriverFactory.csCAN 统一产出 CanDeviceDriver
InfrastructureBootstrapper.cs装配服务、Broker 设备注册代理驱动、启动对账
Broker 宿主DeviceBroker\BrokerServer.cs三重关卡 + SessionGate + 捎带推送 AppendPushes
调试插件
Plugin.DeviceDebug
DeviceDebug.xaml(.cs)多设备壳:设备下拉(过滤已开)+加入/关闭+TabControl
DeviceDebugTab.xaml(.cs)单设备工作区:独立会话+面板+操作日志+维护按钮
Model\DebugSessionManager.cs会话状态机 + 维护租约心跳续期 + 令牌缓存复用
Panels\CanDebugPanel.xaml(.cs)CAN 多通道壳(TabControl)
Panels\CanChannelView.xaml(.cs)单通道视图(连接/发送/接收/统计),BeginInvoke 显示

3 启动初始化时序

① 用户手动启动 CanSimulatorServer.exe(主程序与 Broker 都不拉起它)
   └ TcpSimulatorHost.StartAsync(9000) 监听;UI 就绪;应答器默认 OFF

② 启动 Plugin.IntegrationTest.exe → MainWindow.InitializePlugins(顺序敏感)
   1. DeviceConfig.InitFinish        → 建库/迁移 data.db
   2. SeedPresetCanDevices()         → 预置 CAN 设备(必须先于基础设施,Bootstrapper 扫库决定注册)
   3. InitializeAutoServices()
      InfrastructureBootstrapper.Initialize(data.db)
        a. DbLeaseStore(arb.db,"local-main")
        b. ReconcileAfterBoot       ← 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 就绪,能拿到仲裁服务
顺序要点:建库→预置设备→基础设施→Broker→DeviceDebug。若 DeviceDebug 早于基础设施,UtilContainer 取到 null 服务,维护仲裁会被静默绕过(历史缺陷,已修)。

4 设备注册与统一驱动分派

全系统只有一个 CAN 驱动类 CanDeviceDriver,Local/Broker 差异下沉到构造时选择的传输后端 ICanTransport,上层(自动化 DeviceManager.GetDriver、调试面板)完全无感知。

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 客户端
场景驱动如何产生物理会话在哪
Local 设备(CAN-001/002)面板/工厂 new CanDeviceDriver(NodeChannel)本进程→TCP 连模拟器
Broker 设备(CAN-003/004) 客户端Bootstrapper 注册 CreateBrokerProxy(BrokerRpc)宿主进程
Broker 设备 宿主侧BrokerServer.EnsureConnectedAsync→DriverFactory→CanDeviceDriver(NodeChannel)宿主→TCP 连模拟器

关键:全系统只有一份"节点通道驱动"代码——Broker 模式下宿主内部的物理会话同样是 CanDeviceDriver(NodeChannel)。

5 TCP 线协议与模拟器服务端

5.1 分帧

4 字节大端长度前缀 + UTF8 文本行(SimulatorFraming)。读侧循环读满头部与帧体,天然处理粘包/半包;长度非法抛 InvalidDataException;EOF 返回 null 触发断线感知。

5.2 消息类型

方向消息含义
C→SHELLO:{bus}:{nodeIndex}:{tag}握手并挂载 (bus,nodeIndex),激活节点、开始收帧
SEND:{frameId}:{data}发帧(命令串透传)
QUERY:{命令串}RECV/RECV_ALL/STATUS/AUTO_RESPONSE/BUS_ECHO/FILTER/LOG/BUS_STATUS…
DISCONNECT注销本连接节点
S→COK:{payload} / ERR:{code}:{msg}对上一条请求的响应(恰好一行,保序)
EV:RECV:{id}:{data}:{ts}:{src}总线路由推送帧(主动推送,src 可为节点号或 RESPONDER)
EV:STATE:…预留扩展

5.3 ServerSession 保序与暂存门

读循环(单线程): 逐行读请求 → Process() → 响应经"写锁"发出 → FlushHeldPushes()
命令处理期间路由产生的 EV:RECV 先进 _heldPushes(满 1000 丢最老),
响应写完后再补发 → 保证客户端"首行必为响应、推送随后"。
空闲期推送:写锁串行即发即走。

错误码:ERR:NODE_BUSY(同 bus 同 nodeIndex 被占)、ERR:NOT_HANDSHAKED、ERR:UNKNOWN_CMD、ERR:BAD_FRAME。

6 总线路由与应答器

两种"自动回复"能力,语义不同、互不干扰:

能力触发者回复给谁开关
节点自动响应接收节点(收到帧)其它节点 + 自身邮箱通道面板"自动响应"(AUTO_RESPONSE:ON,写节点属性)
总线应答器(新)总线本身发送者自己(同帧回显,src=RESPONDER)服务窗口复选框 / QUERY:BUS_ECHO:ON(写 CanBusSimulator.EchoResponderToSender)
OnNodeFrameSent(frame, src=node0):
  1) 路由到其它节点;每个接收节点若 AutoResponseEnabled → SendAutoResponse(回它收到的帧)
  2) 若 EchoResponderToSender:把 frame 复制、SourceNodeId="RESPONDER",直接投递回发送节点自身
     → 发送节点 FrameReceived → ServerSession 推送 EV:RECV 回该连接 → 发送者收到"回显"
意义:单客户端调试时也能"发一条收到一条回复",无需第二节点在线——这正是把 CAN-003 这类共享设备当作被测设备验证请求/响应的关键能力。

7 调试页多标签多会话

DeviceDebug(壳,SimpleMaskControl 插件入口)
 ├─ 顶栏:设备下拉(仅列未打开设备) + [加入调试] + [关闭当前页] + 刷新
 └─ TabControl
     └─ DeviceDebugTab × N(每个 = 一个设备,独立会话/租约/面板/日志)
          ├─ 信息栏(设备ID/型号/连接/版本/预约/会话状态)
          ├─ [进入维护控制] [退出维护]
          ├─ panelContainer:LoadPanel(kind)
          │     CanDevice → CanDebugPanel(多通道壳) → CanChannelView(单通道)
          │     PowerSupply / PLC → 对应面板
          └─ 操作日志(本会话)

规则:一会一设备——加入时若设备已开则聚焦其 Tab;下拉过滤掉已开设备;关闭 Tab 调 CloseSessionSilently() 释放会话与租约,设备重新回到可选。CAN 通道内部再套一层 CanDebugPanel TabControl,可对同设备开多通道。

接收显示线程模型:CanChannelView 帧上屏用 Dispatcher.BeginInvoke(异步)。曾因用同步 Invoke,在读循环线程推送骤增时与"UI 线程阻塞等 RPC 响应"形成环形等待 → 开应答器即冻结(已修)。

8 维护会话状态机与租约心跳

OpenSession(deviceId) → ReadOnly
   │ RequestMaintenance:ReserveAsync(复用缓存 ownerToken) 拿维护租约(TTL=300s)
   ▼
AcquiringMaintenance → ActivateMaintenance → MaintenanceActive
   │  └ 启动租约心跳 Timer:每 120s 对每个 MaintenanceActive 会话
   │        Coordinator.Renew(grant, 300) 续期,防 TTL 过期
   │ 退出维护 / 关闭 Tab:CloseSession
   ▼
Closing → Closed(ReleaseReservationAsync 用同一 ownerToken 释放租约;无维护会话则停心跳)
机制解决的问题
ownerToken 按 sessionId 缓存复用租约按 TokenId(GUID) 索引;每次 new 会导致释放落空→租约泄漏→过期 RecoveryRequired 永久阻塞
维护租约心跳续期(新)长时间维护会话到 300s TTL 自然过期→Broker 栅栏 RECOVERY_REQUIRED 禁止 I/O;心跳每 120s 续期,永不触达过期线
启动 ReconcileAfterBoot进程重启自动 SweepExpired+ApproveRecovery,清掉上次残留/过期租约(含已卡 RecoveryRequired 的)
死会话回收DB 找回的未关闭会话强制标 Closed 再新建,避免 RequestMaintenance"会话不存在"

9 报文收发全链路

9.1 发送 — Local 设备

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 行

9.2 发送 — Broker 设备(CAN-003)

面板 → CanDeviceDriver(BrokerRpc).SendCommandAsync("SEND:…")
 → BrokerRpcTransport 映射 opCode=SEND,payload=整串 → SubmitBody 经命名管道
 → BrokerServer.ExecuteOneShotAsync:
     ① 白名单+域内设备  ② configHash 对账(INV-04)  ③ 租约栅栏(INV-01/03)  ④ SessionGate 串行
     ⑤ 宿主内 CanDeviceDriver(NodeChannel).SendCommandAsync → TCP → 模拟器路由
     ⑥ ResultBody.Readback 末尾 AppendPushes:把宿主驱动自上次以来收到的 EV:RECV
        以 "|PUSHED:n|EV:RECV…" 拼上 → 同管道返回
 → 客户端 BrokerRpcTransport 剥离 |PUSHED 段并触发 FramePushed

9.3 接收 — 统一事件模型

服务端路由/应答器产生帧 → 目标节点 FrameReceived → ServerSession 推送 EV:RECV
 ├─ Local:NodeChannel 读循环收到 EV:RECV → CanDeviceDriver.OnFramePushed
 │           → _recvBuffer + _pendingPushLines + DriverFrameReceived → 面板 BeginInvoke 上屏
 └─ Broker:宿主读循环收到 → 宿主驱动 _pendingPushLines 暂存
            客户端下一次 STATUS/SEND 轮询时,宿主 AppendPushes 捎带 → 客户端 FramePushed → 事件 → 上屏
(面板 PollTimer 每 100ms 调 QueryAsync("STATUS"):既刷新统计,也触发 Broker 捎带 drain)
结果:Local 与 Broker 设备共享同一套事件驱动接收,实时且无重复行;应答器开启后 Broker 设备也能"发一条收一条回显"(实测 50 次连续发送,每次 1 TX + 回显 RX,无错误无冻结)。

10 错误处理与不变量

不变量含义落点
INV-01无租约禁止 I/OBrokerServer 栅栏:SEND/写类 QUERY 需 Active 租约;CONNECT/DISCONNECT 与只读 drain(RECV/STATUS/LOG/BUS_STATUS) 豁免
INV-03过期不自动让渡租约过期→RecoveryRequired,拒绝 I/O,需 ApproveRecovery(心跳续期避免误触)
INV-04配置哈希对账RPC 携带 configHash,与宿主生效哈希不符→RejectedConfigMismatch(改配置须先 Apply)
INV-06结果未知不得假定未执行超时/在途异常→Unknown→客户端抛 TimeoutException,禁止自动重试 SEND
故障行为
模拟器服务不可达NodeChannel.ConnectAsync 抛含"请先启动 CanSimulatorServer.exe"的连接异常
中途断线IsConnected=false→指数退避重连(0.5~5s)→重发 HELLO;期间发送快速失败 NOT_CONNECTED
同 (bus,node) 冲突HELLO 返回 ERR:NODE_BUSY,驱动抛"节点编号 X 已被占用"(属配置错误,引导改端点)
推送暂存满丢最老并计数;客户端 STATUS/RECV 兜底补账
维护租约过期心跳续期避免;若进程崩溃遗留,重启 ReconcileAfterBoot 自动回收

11 代码索引

关注点位置
TCP 分帧CanSimulator\SimulatorFraming.cs(Encode/Decode/ReadFrameAsync)
服务端会话/协议CanSimulatorServer.Core\ServerSession.cs(Process/HandleHello/OnNodeFrame/FlushHeldPushes)
命令执行/应答器CanSimulatorServer.Core\CanCommandExecutor.cs(HandleSend/HandleQuery/BUS_ECHO/FormatPush)
路由/自动响应/应答器CanSimulator\CanBusSimulator.cs(OnNodeFrameSent/SendAutoResponse/EchoResponderToSender)
统一驱动Aging.Infrastructure\Drivers\CanDeviceDriver.cs(Connect/Send/Query/OnFramePushed/DrainPendingPushLines)
TCP 后端Drivers\NodeChannelTransport.cs(HandshakeAsync/ReadLoopAsync/重连)
RPC 后端Drivers\BrokerRpcTransport.cs(TransceiveAsync/DrainPiggyback/ThrowForState)
Broker 捎带DeviceBroker\BrokerServer.cs(ExecuteOneShotAsync/RunFragmentAsync/AppendPushes)
多设备壳/工作区Plugin.DeviceDebug\DeviceDebug.xaml.cs · DeviceDebugTab.xaml.cs
会话状态机/心跳Plugin.DeviceDebug\Model\DebugSessionManager.cs(RequestMaintenance/ActivateMaintenance/RenewActiveMaintenance)
通道显示Panels\CanDebugPanel.xaml.cs(壳)· Panels\CanChannelView.xaml.cs(AddReceivedFrame/BeginInvoke)
装配/注册Aging.Infrastructure\InfrastructureBootstrapper.cs(RegisterBrokeredDrivers)· Drivers\DriverFactory.cs

12 已知限制与后续

本报告与代码同步于 2026-11-09;实现变更请同步更新本文件。