V3.1 结构:CAN 模拟器独立 TCP 服务 + 统一 CanDeviceDriver + 多设备并发调试 + 总线应答器 + 维护租约心跳。
本报告描述最新代码的运行逻辑,与仓库实际实现同步(2026-11-09)。
系统由 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 驱动只剩一个类。
| 层 | 文件 | 职责 |
|---|---|---|
| 模拟器类库 | 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) | |
| 模拟器服务 UI | CanSimulatorServer.App\App.cs · MainWindow.cs | WPF 监控窗口(节点树/帧表/统计/应答器复选框) |
| 驱动层 Aging.Infrastructure | Drivers\CanDeviceDriver.cs | 唯一 CAN 驱动:命令语义、接收缓冲、事件、连接状态机 |
| Drivers\ICanTransport.cs · NodeChannelTransport.cs · BrokerRpcTransport.cs | 传输后端抽象与两实现(TCP 直连 / 管道 RPC) | |
| Drivers\DriverFactory.cs | CAN 统一产出 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 显示 |
① 用户手动启动 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 就绪,能拿到仲裁服务
全系统只有一个 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)。
4 字节大端长度前缀 + UTF8 文本行(SimulatorFraming)。读侧循环读满头部与帧体,天然处理粘包/半包;长度非法抛 InvalidDataException;EOF 返回 null 触发断线感知。
| 方向 | 消息 | 含义 |
|---|---|---|
| C→S | HELLO:{bus}:{nodeIndex}:{tag} | 握手并挂载 (bus,nodeIndex),激活节点、开始收帧 |
SEND:{frameId}:{data} | 发帧(命令串透传) | |
QUERY:{命令串} | RECV/RECV_ALL/STATUS/AUTO_RESPONSE/BUS_ECHO/FILTER/LOG/BUS_STATUS… | |
DISCONNECT | 注销本连接节点 | |
| S→C | OK:{payload} / ERR:{code}:{msg} | 对上一条请求的响应(恰好一行,保序) |
EV:RECV:{id}:{data}:{ts}:{src} | 总线路由推送帧(主动推送,src 可为节点号或 RESPONDER) | |
EV:STATE:… | 预留扩展 |
读循环(单线程): 逐行读请求 → Process() → 响应经"写锁"发出 → FlushHeldPushes() 命令处理期间路由产生的 EV:RECV 先进 _heldPushes(满 1000 丢最老), 响应写完后再补发 → 保证客户端"首行必为响应、推送随后"。 空闲期推送:写锁串行即发即走。
错误码:ERR:NODE_BUSY(同 bus 同 nodeIndex 被占)、ERR:NOT_HANDSHAKED、ERR:UNKNOWN_CMD、ERR:BAD_FRAME。
两种"自动回复"能力,语义不同、互不干扰:
| 能力 | 触发者 | 回复给谁 | 开关 |
|---|---|---|---|
| 节点自动响应 | 接收节点(收到帧) | 其它节点 + 自身邮箱 | 通道面板"自动响应"(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 回该连接 → 发送者收到"回显"
DeviceDebug(壳,SimpleMaskControl 插件入口)
├─ 顶栏:设备下拉(仅列未打开设备) + [加入调试] + [关闭当前页] + 刷新
└─ TabControl
└─ DeviceDebugTab × N(每个 = 一个设备,独立会话/租约/面板/日志)
├─ 信息栏(设备ID/型号/连接/版本/预约/会话状态)
├─ [进入维护控制] [退出维护]
├─ panelContainer:LoadPanel(kind)
│ CanDevice → CanDebugPanel(多通道壳) → CanChannelView(单通道)
│ PowerSupply / PLC → 对应面板
└─ 操作日志(本会话)
规则:一会一设备——加入时若设备已开则聚焦其 Tab;下拉过滤掉已开设备;关闭 Tab 调 CloseSessionSilently() 释放会话与租约,设备重新回到可选。CAN 通道内部再套一层 CanDebugPanel TabControl,可对同设备开多通道。
Dispatcher.BeginInvoke(异步)。曾因用同步 Invoke,在读循环线程推送骤增时与"UI 线程阻塞等 RPC 响应"形成环形等待 → 开应答器即冻结(已修)。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"会话不存在" |
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 行
面板 → 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
服务端路由/应答器产生帧 → 目标节点 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)
| 不变量 | 含义 | 落点 |
|---|---|---|
| INV-01 | 无租约禁止 I/O | BrokerServer 栅栏: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 自动回收 |
| 关注点 | 位置 |
|---|---|
| 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 |
本报告与代码同步于 2026-11-09;实现变更请同步更新本文件。