# CAN 模拟器独立 TCP 服务进程 — 设计规格 日期:2026-11-09 状态:已与用户逐节确认 关联代码基线:V3.0 混合架构(Local + DeviceBroker),进程内 CanSimulator ## 1. 问题与目标 **现状问题**:`CanDriverAdapter` 用进程内 static 字典 `_sharedBuses` 管理总线实例, 主程序(CAN-001/002)与 DeviceBroker 宿主(CAN-003/004)各自持有一套同名但隔离的 CAN_BUS_1 实例,跨进程不可见,只能靠"同宿主对端设备"变通验证收发。 **目标**:将 CAN 模拟器改造为独立 TCP 服务进程(总线权威),模仿物理 CAN 分析仪行为, 实现跨进程多通道并发测试;同时把 CAN 驱动收敛为**唯一类** `CanDeviceDriver`, Local/Broker 差异下沉为驱动内部传输后端,自动化程序只面向 `IDeviceDriver` 编程。 ## 2. 已确认的关键决策 | 决策点 | 结论 | |---|---| | 协议取向 | 命令串直传(SEND:/RECV_ALL/AUTO_RESPONSE:ON 等现有语义原样传输) | | 分帧方式 | 4 字节大端长度 + UTF8 文本(与 BrokerIpc 同惯用法) | | 服务端形态 | 独立 WPF 进程 CanSimulatorServer.exe(带 UI) | | 地址来源 | 全局 App.config(CanSimulator.Host / CanSimulator.Port) | | 模式取舍 | 全面切 TCP,驱动删除进程内总线分支,无回退开关 | | 服务端实现 | 复用现有 CanBusSimulator/CanNode 路由与自动响应逻辑,零重写 | | 会话模型 | 方案 A:请求-响应(保序)+ 服务端主动推送 EV:RECV(双工) | | 驱动收敛 | 唯一 CAN 驱动类 CanDeviceDriver,内部 ICanTransport 双实现 | | 面板改造 | 抽 CanChannelView + CanDebugPanel 壳化为 TabControl 多通道 | | 启动方式 | 模拟器服务纯手动启动,主程序与 Broker 均不拉起 | ## 3. 总体架构 ``` ┌───────────────────────── CanSimulatorServer.exe(新建,WPF) ─────────────────────────┐ │ TcpListener(默认 127.0.0.1:9000) │ │ ├─ TcpAcceptor:每连接一个 ConnectionSession │ │ ├─ CanBusManager:懒创建 CanBusSimulator;(bus,nodeIndex) 唯一性仲裁; │ │ │ 节点 FrameReceived → 会话推送 的胶水 │ │ └─ UI:节点树 | 实时帧表 | 总线启停/统计 | 日志 │ └────────────────────────────────────────────────────────────────────────────────────────┘ ▲ TCP(NodeChannelTransport) ▲ TCP │ │ ┌──────┴──────────────┐ ┌──────────┴──────────────┐ │ 主程序 │ 命名管道RPC │ DeviceBroker.exe │ │ CAN-001/002 │ ◄────────────► │ CAN-003/004 │ │ CanDeviceDriver │ BrokerRpc │ CanDeviceDriver │ │ (NodeChannelTransport)│ Transport │ (NodeChannelTransport) │ └──────────────────────┘ └─────────────────────────┘ ``` - 全系统只有一份"节点通道驱动"代码:Broker 模式下宿主进程内的物理会话同样是 `CanDeviceDriver(NodeChannelTransport)`。 - 仲裁链路完全不动:BrokerServer 三重关卡(白名单→configHash→租约栅栏)、 SessionGate、维护租约、只读豁免均保留在 BrokerRpcTransport 委托路径上。 - 自动化程序经 `DeviceManager.GetDriver(deviceId)` 拿到的永远是 `CanDeviceDriver`, ExecutionMode 在驱动构造时读一次,上层零感知。 ## 4. TCP 线协议 分帧:4 字节大端长度 + UTF8 文本行;文本按冒号分段。 ### 4.1 客户端 → 服务端 | 消息 | 说明 | |---|---| | `HELLO:{bus}:{nodeIndex}:{clientTag}` | 握手并挂载节点;确认后节点激活、开始接收总线帧 | | `SEND:{frameId}:{data}` | 发送帧(现行命令串透发) | | `QUERY:{命令串}` | RECV / RECV_ALL / STATUS / AUTO_RESPONSE:ON / FILTER:id:mask / LOG 等透发 | | `DISCONNECT` | 注销本连接节点 | ### 4.2 服务端 → 客户端 | 消息 | 说明 | |---|---| | `OK:{payload}` / `ERR:{code}:{message}` | 对上一条请求的响应(恰好一行,保序) | | `EV:RECV:{frameId}:{data}:{HH:mm:ss.fff}:{srcNodeIndex}` | 总线路由推送的接收帧(主动推送,无请求关联) | | `EV:STATE:{connected\|lost}` | 对端上下线通知,MVP 仅定义格式不发送 | ### 4.3 节点身份模型 一个总线节点 = **(busName, nodeIndex)**,服务端以 nodeIndex 为总线上唯一寻址 (与真实 CAN 节点地址语义一致)。同一 nodeIndex 被其他连接占用时握手返回 `ERR:NODE_BUSY` 拒绝挂载;连接断开自动释放。两个设备配置同 (bus,node) 属配置错误, 靠错误文案引导修改端点,不做自动避让。 ### 4.4 错误与边界 - 未知命令 / 格式非法 → `ERR:UNKNOWN_CMD` / `ERR:BAD_FRAME`,不断连接 - `HELLO` 前发其它命令 → `ERR:NOT_HANDSHAKED` - 服务端不可达 → `ConnectAsync` 抛连接异常,文案含"请先启动 CAN 模拟器服务 {Host}:{Port}" ## 5. 驱动层:统一 CanDeviceDriver ### 5.1 类型划分(Aging.Infrastructure) ``` IDeviceDriver(现有契约,不变) ▲ CanDeviceDriver 全系统唯一 CAN 驱动类(由 CanDriverAdapter 更名重构) ├─ 命令串解析、_recvBuffer、RECV/RECV_ALL 组装、DriverFrameReceived 事件、 │ STATUS/统计、连接状态机、Dispose └─ ICanTransport _transport(构造时按设备 ExecutionMode 选择) ICanTransport(内部接口) ├─ ConnectAsync / TransceiveAsync(cmd)→响应行 / Dispose ├─ event FramePushed(EV:RECV 行到达;NodeChannel 为 TCP 直推, │ BrokerRpc 为随宿主响应捎带到达,见 §5.4) ├─ event TransportBroken │ ├─ NodeChannelTransport TCP 直连服务端;读循环分流响应/推送; │ 断线指数退避重连(0.5s 起、上限 5s)、重连自动重发 HELLO └─ BrokerRpcTransport 现 BrokeredCanDriver 的 RPC 逻辑原样迁入 (Hello 握手 / SubmitTask / ResultBody 态→异常映射不变: RejectedStaleLease→LEASE_STALE、ConfigMismatch、 Unknown→TimeoutException 禁止假定未执行) ``` ### 5.2 注册与分派 - `DriverFactory.CreateDriver("CAN", …)` 统一返回 `CanDeviceDriver`;工厂读 data.db 该设备 `ExecutionMode/BrokerId` 选择传输后端。 - `InfrastructureBootstrapper.RegisterBrokeredDrivers` 取消——Broker 设备与 Local 设备 走同一条 `LoadDevicesFromConfig` 注册路径。 - 删除:`BrokeredCanDriver` 类(逻辑入 BrokerRpcTransport)、`CanDriverAdapter._sharedBuses` 进程内分支。 ### 5.3 上层兼容 - `DeviceManager.GetDriver` 契约、DeviceService/CanBusService/AutoDeviceFacade 自动化链、 DeviceDebug 的 `ExistingDriver` 复用逻辑均不改。 - 按 `GetType().Name` 区分驱动的日志/断言改为按传输后端类型区分。 - 面板 `DrainBrokerMailbox`(RECV_ALL 1s 轮询) 降级为兜底(重连窗口期补显示), 帧来源标注 `BUS@tcp` / `BUS@broker`。 ### 5.4 Broker 捎带推送 宿主内驱动的 NodeChannelTransport 把 EV:RECV 追加到 `pendingPushes` 列表(有界 1000, 满丢最老);BrokerServer 响应 QUERY/SEND 时在 readback 末尾追加 `|PUSHED:n` + 推送行; BrokerRpcTransport 解析后回调 FramePushed 并从宿主列表移除(一次性消费,不丢不重)。 空闲无请求时推送滞留宿主,下一次任意请求捎带——Broker 路径实时性仍为请求驱动, 与现状一致,面板 1s RECV_ALL 兜底保证最终显示。 ## 6. 服务端工程 CanSimulatorServer - 新目录 `CanSimulatorServer\`,SDK 风格 WPF(`net472;UseWPF=true`), 引用 CanSimulator 类库 + log4net,**不引用 Aging.Infrastructure**; 加入 `WPFProject\TapProject.sln`。 - `App.config`:`ServerPort`(默认 9000)、`ListenOnAllInterfaces`(默认 false)。 - 结构:`TcpAcceptor` / `CanBusManager` / `ConnectionSession`(每连接 MPSC 有界发送队列 1000 帧,满丢最老+计数)/ `MainWindow`(节点树、环形 2000 行帧表 TX/RX 分色、 每总线启停与清空、统计、日志;Dispatcher 100ms 批量刷新)。 - 服务端**必须引用 CanSimulator 类库**(CanBusSimulator/CanNode/CanFrame 即路由引擎, 复用其路由、自动响应、滤波器、总线日志,零重写)。 - 帧去重:TCP 模式下帧进入客户端驱动仅经推送事件一条路,驱动不重复入节点邮箱, 避免历史上 buffer+邮箱双报问题重现。 ## 7. 调试面板多通道 ``` CanDebugPanel(类名/入口不变,壳化为 TabControl) ├─ 工具条:[+ 添加通道] [关闭当前通道];Tab 头 "CAN-001 @CAN_BUS_1:0" └─ TabControl → CanChannelView × N(新 UserControl,承载现单通道全部 UI) ``` - CanChannelView:现有面板四区(连接控制/发送/接收记录/状态统计)整体抽出, x:Name 控件归 View 内部,每 Tab 独立控件树。 - 通道两入口: 1. DeviceDebug 选设备 → 打开/聚焦该设备通道页(重复选择只聚焦不重建), 注入 `CanDeviceDriver` 后总线/节点输入只读(设备表权威); 2. 手动添加通道 → 小对话框(总线/节点编号/后端 Local-TCP),自建驱动连接。 - 每 View 独立 100ms PollTimer;维护模式按钮语义分两类:注入驱动→委托 DebugSessionManager 租约状态机(Token 缓存复用等现有逻辑不动); 自建通道→维持模拟器本地直连语义(与自动化无关,不引入仲裁旁路)。 - 主程序 Tab3"CAN 模拟器"宿主容器改用同一壳类,MainWindow 无需大改。 ## 8. 配置、启动与错误处理 | 键 | 位置 | 默认 | |---|---|---| | `CanSimulator.Host` | 主程序 + DeviceBroker App.config | 127.0.0.1 | | `CanSimulator.Port` | 同上 | 9000 | | `ServerPort` / `ListenOnAllInterfaces` | 服务端 App.config | 9000 / false | `UseTcpMode` 取消;Endpoint 格式 `CAN_BUS_1:0` 不变;data.db 零迁移。 启动顺序:用户手起服务 → 主程序(照常拉起 DeviceBroker)→ CAN 按需连接; 主程序退出只关自己的连接,服务常驻。 | 故障 | 行为 | |---|---| | 服务不可达(连接时) | 抛连接异常含启动提示文案 | | 中途断线 | IsConnected=false → 指数退避重连 → HELLO 重发;期间发送快速失败 NOT_CONNECTED;ConnectionChanged 上抛 | | 在途请求撞断线 | TimeoutException,语义等同 Unknown(INV-06),禁止自动重试 SEND | | 节点被占 | ERR:NODE_BUSY,提示修改设备端点 | | 推送队列满 | 丢最老+UI 计数;客户端 1s RECV_ALL 兜底补账 | | Broker 管道断/宿主死 | 沿用现异常;宿主内 TCP 重连对客户端透明 | ## 9. 测试与验收 - **存量改造**:依赖进程内总线的用例(BrokerE2ETests CAN 收发断言、CanBusService 相关) 改为测试 fixture 在测试进程内 new `CanBusManager` + `TcpListener`(随机端口) 托管服务核心(无 UI 复用层),与被测驱动走真实 TCP 回环,无需人工起服务。 - **协议单测**:SimulatorFraming 粘包/半包/超长拒绝;HELLO 状态机;NODE_BUSY 仲裁。 - **驱动单测**:重连后 HELLO 重发;在途超时语义;EV:RECV→事件链路。 - **手动验收清单**: 1. 启动 CanSimulatorServer.exe; 2. 主程序 CAN-001/002 经 TCP 挂载; 3. DeviceBroker 启动,CAN-003/004 经宿主 RPC 中转走 TCP; 4. 调试面板同屏打开 CAN-001 与 CAN-003 两个通道页; 5. CAN-001 发帧 → CAN-003 收到; 6. CAN-003 发帧 → CAN-001 收到; 7. 双通道并发收发互不阻塞。 ## 10. 构建约束与已知风险 - 构建须用 VS 完整版 MSBuild(解决方案 `WPFProject\TapProject.sln`), 命令行不加 `/p:Platform=x64`(老式工程 XAML 编译链路限制)。 - 风险 1:服务端 TCP 核心与 UI 的引用边界——已定:服务端必须引用 CanSimulator 类库, TcpAcceptor/CanBusManager 放服务端工程内,CanSimulator 保持无 TCP 依赖的纯净类库。 - 风险 2:全面切 TCP 后服务未启动时 Local CAN 设备完全不可用——已定, 以清晰错误文案与测试 fixture 自托管缓解。 - 风险 3:Broker 路径实时性仍为请求驱动(捎带模型)——与现状一致,验收标准不因此提高。