docs: CAN 模拟器独立 TCP 服务进程设计规格(统一 CanDeviceDriver + 双工推送协议)
1个文件已添加
222 ■■■■■ 已修改文件
docs/superpowers/specs/2026-11-09-can-simulator-tcp-server-design.md 222 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/superpowers/specs/2026-11-09-can-simulator-tcp-server-design.md
New file
@@ -0,0 +1,222 @@
# 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 路径实时性仍为请求驱动(捎带模型)——与现状一致,验收标准不因此提高。