仪器注册与硬件调试 · 软件逻辑与时序梳理

依据《Local + DeviceBroker 混合架构详细设计说明书 V3.0》与当前代码实况整理(含近期缺陷修复后的最终逻辑)

范围:Plugin.DeviceConfig(仪器仪表注册) · Plugin.DeviceDebug(硬件调试) · Aging.Infrastructure / Aging.ResourceContracts(基础设施与契约) · DeviceBroker.exe(共享仪器宿主) · Plugin.IntegrationTest(集成测试环境)

状态:现行实现基线 2026-11-09net472 / WPF / SQLite / Named Pipes两进程仲裁域:local-main + broker-b01
目录: 1 进程与数据拓扑2 启动初始化时序3 仪器注册逻辑4 驱动分派与配置哈希5 调试页选设备时序6 维护模式状态机与租约7 报文收发全链路8 异常与自愈机制9 关键代码索引10 已知限制

1 进程与数据拓扑

┌───────────────────────── 测试/操作员工作站(单机) ─────────────────────────┐
│                                                                            │
│  Plugin.IntegrationTest.exe(主程序进程,一切 UI 与业务调用方的宿主)      │
│  ├─ Plugin.DeviceConfig   仪器仪表注册页(编辑草稿/发布门禁/版本管理)      │
│  ├─ Plugin.DeviceDebug    硬件调试页(会话状态机/维护申请/CAN·电源·PLC面板)│
│  ├─ Aging.Infrastructure  DeviceManager / DeviceService / AutoDeviceFacade  │
│  │   ├─ Local 设备 → 直驱驱动(CanDriverAdapter 等,物理会话在本进程)      │
│  │   └─ Broker 设备 → BrokeredCanDriver 代理驱动(不持物理会话)         │
│  └─ LayeredResourceManager ──┐(进程内缓存 grants + 委托 DbLeaseStore)     │
│                               │                                            │
│        命名管道 RPC           │        ┌───────────────────────────────────┤
│   aging.brokerId.v1(帧协议) │        │  DeviceBroker.exe(共享仪器宿主进程)│
│        ◄──────────────────────┴────────┤  BrokerServer:三重关卡+SessionGate │
│                                        │  DbLeaseStore(broker-b01) 读仲裁库 │
│                                        │  CanDriverAdapter → 物理会话唯一持有│
│                                        └───────────────────────────────────┘
│  数据层(本机 SQLite,禁止网络盘):                                        │
│   data.db  配置库:DeviceDefinition(含执行位置5列)/DeviceChannel/Slot映射/版本 │
│   arb.db   仲裁库:resource_state/lease_history/recovery_checkpoint/authority_meta │
│   debug.db  调试会话库:debug_session(状态机持久化)+操作日志              │
└────────────────────────────────────────────────────────────────────────────┘

Local 直连适合已分配给唯一执行进程、可独立连接的设备;物理会话在客户端进程内。
Broker 代理适合跨进程共享设备:物理会话由 DeviceBroker.exe 唯一持有,客户端只做 RPC 委托。

注:CAN 总线模拟器(CanSimulator)是进程内虚拟总线——主程序与宿主各自持有一套 CAN_BUS_1 实例,跨进程不可见;真实硬件(物理卡/总线)天然共享,无此限制。测试环境用同宿主的 CAN-004 作为 CAN-003 的对端。

2 启动初始化时序(MainWindow.InitializePlugins)

 1. DeviceConfig.InitFinish          → 建库/迁移 data.db(PRAGMA 检查→ALTER 补执行位置5列)
 2. SeedPresetCanDevices()           → 预置 CAN-001/002/003(必须先于基础设施:Bootstrapper 扫库决定驱动注册)
 3. InitializeAutoServices()
    3.1 InfrastructureBootstrapper.Initialize(data.db 路径)
        a. DbLeaseStore(arb.db, "local-main")
        b. ReconcileAfterBoot          ← 启动对账:SweepExpired + ApproveRecovery 回收上次进程泄漏租约
        c. LayeredResourceManager 注册进 UtilContainer("ResourceManager"/"LeaseCoordinator")
        d. DeviceManager / DeviceService / PowerSupplyService / AutoDeviceFacade / CanBusService
        e. RegisterBrokeredDrivers   ← 扫 data.db:ExecutionMode='Broker' 的设备注册
                                           BrokeredCanDriver(管道=aging.{BrokerId}.v1, 携启动时 configHash)
    3.2 从 UtilContainer 拉取 IAutoDeviceFacade / ICanBusService,注册 Local CAN 直驱
    3.3 TryStartDeviceBroker()       ← 若存在 Broker 设备:Process.Start DeviceBroker.exe
                                           --db data.db --arb arb.db --broker broker-b01
 4. DeviceDebug.InitFinish           → 从 UtilContainer 取 DeviceManager/DeviceService/ResourceManager
                                       (必须在 3 之后!倒置会取到 null,仲裁/驱动复用链路全部静默降级)
 5. ForceRefreshForTest()×2          → 两插件列表刷新
 Window_Closing:Kill DeviceBroker 子进程(其物理会话随进程消亡,租约靠 TTL+启动对账收口)
✅ 修复记录:原时序中 DeviceDebug.InitFinish 早于 Bootstrapper.Initialize,导致调试页取到 null 服务——维护仲裁被绕过、Broker 设备连接状态恒"未连接"、面板退化为本地直驱。现已调整为"建库→预置→基础设施→调试插件"。

3 仪器注册逻辑(Plugin.DeviceConfig)

3.1 编辑弹窗(DeviceEditModal)字段与联动

区块字段规则
设备信息DeviceId新增可编辑、编辑只读;保存时查重
Kind / DriverType类型联动驱动建议(CanDevice→CAN,PowerSupply→SCPI…)
TransportTcp/Serial/Visa/SDK/Can
Endpoint按 Transport 校验格式并更新提示(CAN:总线:节点)
通道列表ChannelId 唯一,能力/电压/电流/SessionKey
执行位置
(V3.0 §05)
ExecutionMode单选 Local(默认)/Broker;与仲裁域双向联动
AuthorityId必填下拉,候选来自 V3AuthorityCatalog(appSettings V3.AuthorityIds,未配置回退 local-main;broker-b01)
BrokerId仅 Broker 显示;候选 V3.BrokerIds;门禁要求 BrokerId==AuthorityId
OwnerProcessId只读=当前进程 PID(MVP 占位);不入配置哈希(运行期信息)
PhysicalSessionKey仅 Broker;候选为端点规范化的写死字符串:can/{bus}/main、tcp/{ip:port}/main、serial/{COM}/main、{deviceId}/main、已存通道 SessionKey
访问策略AccessPolicy只读派生不落库:Local→"独占直连·本进程持有物理会话";Broker→"共享仲裁·Broker SessionGate 代理"

联动规则:选 Broker → 仲裁域自动切到首个 Broker 候选;选 local- 域 → 自动切回 Local。XAML 初始化期事件加控件空引用守卫。

3.2 数据模型与持久化

  • DeviceDefinition 表 13 列 = 原 8 列 + 执行位置 5 列;旧库经 PRAGMA table_info 检查后 ALTER TABLE ADD COLUMN 迁移,存量数据默认 Local/空AuthorityId。
  • 实体默认值 ExecutionMode=Local;读取端 HasColumn 容错缺列。
  • 批量生成(BatchGenerateModal)走同一 SaveDevice,新列取默认。

3.3 版本状态机(发布工作流 §05)

Draft ──保存草稿──► (重算 hash 落库)
Draft ──发布(PublishVersion)──► ① ValidatePublishGate() 拦截
                               ② 通过→重算 hash→status=Published
Published ──应用(Apply)──► Effective(旧 Effective 退回 Published)

3.4 发布门禁校验项(ValidatePublishGate)

  1. AuthorityId 必填且在配置枚举内(存量空值 → 提示"旧数据请重新保存")
  2. Local 设备的域必须 local- 前缀
  3. Broker 设备:BrokerId 必填、在枚举内、且与 AuthorityId 一致(域一致)
  4. Broker 设备:PhysicalSessionKey 必填
  5. 同一 PhysicalSessionKey 只允许一个宿主域("同一会话只能有一个宿主")
任一失败 → 弹窗列出错误(最多显示 8 条)并阻止发布;冲突检测(IP/Slot/越界)仍走 DetectConflicts。

4 驱动分派与配置哈希(INV-04)

                    注册页保存设备(含执行位置) ──► data.db
                                  │
 Bootstrapper 启动扫描             ▼
   ExecutionMode='Local'  ──► DriverFactory.CreateDriver ──► 直驱(本进程物理会话)
   ExecutionMode='Broker' ──► BrokeredCanDriver(deviceId, aging.{BrokerId}.v1, hash₀)
                                  │  hash₀ = DeviceConfigHash.Compute(data.db)
                                  │  规则与 DeviceConfigModel.ComputeConfigHash 完全一致:
                                  │  DEVICE(id|kind|driver|transport|endpoint|mode|authority|broker|sessionKey)
                                  │  + CHANNEL + SLOT 三段拼接 SHA256;OwnerProcessId 不参与
                                  ▼ 每次 RPC 携带
                     宿主出队时与自己重算的生效 hash 对账 → 不一致 RejectedConfigMismatch

5 调试页选设备时序(OnDeviceSelected)

设备下拉变更
 1. LoadDeviceInfo(deviceId)              ← 读 data.db:Kind/Endpoint/EffectiveVersion
 2. OpenSession(deviceId)                 ← DebugSessionManager
      a. GetActiveSessionByDevice → 命中且在本进程内存 → 复用返回
      b. 命中但仅存在于 debug.db(进程重启遗留死会话)→ 强制回收为 Closed 再新建
      c. 新建 session(State=ReadOnly,写 debug_session 表 + 入内存缓存)
 3. DeviceManager.ConnectAsync(deviceId)
      Local  直驱 ConnectAsync:注册总线节点/建立 TCP…
      Broker BrokeredCanDriver → RPC CONNECT(无租约也允许:仅登记宿主物理会话,无总线输出)
      异常→仅连接状态显示"连接失败",不抛出
 4. UpdateButtonStates()                  ← "进入维护"仅 ReadOnly 时可用
 5. LoadPanel(kind)
      CAN 面板:ExistingDriver = DeviceManager.GetDriver(deviceId)
        ├─ 非 null(Broker 设备)→ 面板复用该驱动,禁止本地直驱(不 Dispose 外部驱动)
        └─ null(未注册)→ 降级本地直驱(模拟器语义,owns=true)
      面板连接后:DispatcherTimer 100ms 轮询启动(UpdateStats + 代理驱动邮箱抽取)
 顶部"连接状态"与面板内"状态"为两个指标:前者=DeviceManager 通路,后者=面板实际使用的驱动

6 维护模式状态机与租约时序

6.1 会话状态机

             RequestMaintenance ✔            ActivateMaintenance
  ReadOnly ────────────────────► AcquiringMaintenance ────────────► MaintenanceActive
     ▲                               │失败(占用/超时)                     │ CloseSession
     │回收死会话(Closed)             ▼                                   ▼
     └────────────────────────  保持 ReadOnly                     Closing → Closed
                                                                    (释放维护租约)

6.2 进入维护(含跨进程仲裁)

"进入维护" → RequestMaintenance(sessionId)
 1. 会话在内存缓存?不在→debug.db 恢复兜底;仍无→拒绝
 2. State==ReadOnly?否则拒绝(按钮已按此置灰)
 3. 取/建会话→令牌缓存 _sessionOwnerTokens[sessionId]
    (ResourceOwnerToken.TokenId 是随机 GUID,必须复用同一实例,
      否则 LayeredResourceManager 按 TokenId 索引的 grants 释放时找不回)
 4. ResourceManager.ReserveAsync(token, key=deviceId)
      → DbLeaseStore.Acquire(arb.db BEGIN IMMEDIATE 单事务)
        ├ Acquired/Reused → Active 租约(TTL 300s,Generation+1)
        ├ ConflictHeld    → 抛 ResourceConflictException → 拒绝(提示含占用者/机器/PID)
        └ RecoveryBlocked → 拒绝(过期待回收,见 §8)
 5. State=AcquiringMaintenance → ActivateMaintenance → MaintenanceActive
 6. LoadPanel 重建 → 面板 IsMaintenanceMode=true(写控件解锁)

6.3 退出维护

"退出维护" → CloseSession
 1. State=MaintenanceActive?→ Closing
 2. ReleaseReservationAsync(复用进入时的同一令牌)
      → DbLeaseStore.Release → Phase=0(Free),lease_history 记 Released
 3. Closed + closed_at 落库、移出内存缓存、清令牌缓存
 校验点:arb.db resource_state 应变回 Free —— 泄漏租约曾导致
 "第二次进入维护永久失败",根因与修复见 §8/§10

6.4 维护与写权限矩阵(宿主侧执行语义)

操作租约要求原因
CONNECT / DISCONNECT豁免仅登记/释放宿主物理会话,无总线输出
RECV / RECV_ALL / LOG / STATUS豁免纯读邮箱/状态,被动监控支持调试页只读监听
SEND(帧发送)必须 Active总线写,INV-01 禁止无租约直接 I/O
AUTO_RESPONSE / FILTER / PROMISCUOUS必须 Active有副作用的 QUERY,改变节点行为
✅ 因此正确操作顺序即您指出的:先进入维护模式 → 才能打开自动响应/发送;只读"看帧"不需要维护。

7 报文收发全链路时序(Broker 设备)

7.1 发送(维护模式下,CAN-003 → 总线)

面板"发送" → BrokeredCanDriver.SendCommandAsync("SEND:0x123:55 66")
 └─ BrokerClientChannel.EnsureConnectedAsync     ← 惰性:管道断开自动重连+重握手
     └─ Hello → 宿主回 {ServerEpoch, ConfigHash, BrokerId, ProtocolVersion}
        协议主版本不符 → 断开拒绝
 └─ SubmitTask(帧=4字节大端长度+UTF8 JSON 封套;RequestId 关联、deadline 超时)
     Body={DeviceId, OpCode=SEND, Payload, ResourceKey=deviceId, ClientInstanceId}
          ▼ ─────────── DeviceBroker.exe ───────────
     ①白名单+域内设备过滤          非本域/未知op → Failed
     ②configHash 对账(INV-04)    不一致 → RejectedConfigMismatch("先 Apply 再 I/O")
     ③栅栏再校验(INV-01/03)      RecoveryRequired→拒绝;无 Active 租约→RejectedStaleLease
     ④SessionGate(按 PhysicalSessionKey 串行原子片段)
        队列等待 5s 超时 → Failed RESOURCE_BUSY(确定未执行,可安全重试)
     ⑤CanDriverAdapter.SendFrame(宿主进程物理/模拟总线)
     ⑥ResultBody{State, Readback, ServerEpoch} 同管道返回
          ▼ ─────────── 客户端 ───────────
     Succeeded→返回设备响应串 | RejectedStaleLease→抛 LEASE_STALE
     ConfigMismatch→抛异常 | Unknown(超时/片段中异常)→TimeoutException 禁止假定未执行(INV-06)

7.2 接收(总线帧 → 面板显示)

总线广播 → 宿主内 node(CAN-003) 收帧 → RxCount++,帧入 _recvBuffer + 节点邮箱
客户端面板 DispatcherTimer(100ms) 每 10 tick(≈1s):
   DrainBrokerMailbox → RPC QUERY "RECV_ALL"(只读,无需租约)
     → 解析 "RECV_ALL:n:id:data[:ts]|…" → AddReceivedFrame(RX, 来源=BUS@broker)
     RPC 失败(宿主不可达等)静默跳过,下一秒重试,不弹错误
(Local 直驱设备无需轮询:DriverFrameReceived 事件即时显示)
对端模拟:真实硬件同一总线天然互通;模拟器场景用同宿主的 CAN-004 作对端验证。
✅ 修复记录:① 代理驱动无帧事件且面板从不查邮箱 → "只有发送没有接收",现增加 RECV_ALL 只读轮询;② 宿主 HandleRecvAll 曾把同一帧经 buffer+邮箱双路上报(RX 重复行),已改为 buffer 优先、邮箱仅兜底。

8 异常与自愈机制

场景机制表现/去向
进程异常退出(崩溃/被杀)租约不再续期 → TTL(300s) 过期 → RecoveryRequired(INV-03 禁止自动移交)下次 Acquire 返回 RecoveryBlocked → "无法进入维护"
宿主/主程序重启ReconcileAfterBoot:SweepExpired(过期→待回收+历史)→ 逐个 ApproveRecovery(登记读回证据进 recovery_checkpoint,Phase=Free,Generation 不回退)启动即自愈;证据可在 arb.db 审计
调试页维护租约泄漏根因:进出维护各自 new Token(TokenId 随机 GUID)→ 释放按 TokenId 查找落空。修复:会话→令牌实例缓存复用退出维护后 resource_state 必回 Free
debug.db 死会话残留OpenSession 检出"DB 有、内存无"的未关闭会话 → 回收 Closed 再新建不再出现"会话不存在"型拒绝
租约过期后使用(作用域租约)AutoDeviceLease 心跳 8s Renew;连续 2 次失败 → LeaseLost 事件 + 后续调用抛 LEASE_STALE(熔断)自动化步骤快速失败而非静默半执行
RPC 超时/断线响应关联超时 → State=Unknown(INV-06);读循环断线感知 → 下次调用惰性重连重握手面板轮询静默重试;显式 I/O 抛异常禁止重试假定未执行
配置版本错位(客户端快照旧)宿主每次出队对账 configHash → RejectedConfigMismatch强制"先 Apply 再 I/O"

9 关键代码索引

职责文件关键成员
启动装配Plugin.IntegrationTest/MainWindow.xaml.csInitializePlugins / SeedPresetCanDevices / InitializeAutoServices / TryStartDeviceBroker
注册 UI/联动/校验Plugin.DeviceConfig/DeviceEditModal.xaml(.cs)Open / ExecMode_Changed / CbAuthority_SelectionChanged / RefreshSessionKeyCandidates / SaveBtn_Click
候选枚举配置Plugin.DeviceConfig/V3AuthorityCatalog.cs + Plugin.IntegrationTest/App.configAuthorityIds / BrokerIds / LocalAuthorityId / IsLocalAuthority
发布门禁/哈希/迁移Plugin.DeviceConfig/Model/DeviceConfigModel.csMigrateExecutionColumns / ValidatePublishGate / ComputeConfigHash / SaveDevice
实体Plugin.DeviceConfig/Entity/DeviceDefinition.csExecutionModeKind / AuthorityId / BrokerId / PhysicalSessionKey / AccessPolicy(派生)
服务装配Aging.Infrastructure/InfrastructureBootstrapper.csInitialize / RegisterBrokeredDrivers
跨进程租约Aging.Infrastructure/Core/DbLeaseStore.cs · LayeredResourceManager.csAcquire/Renew/Release/PhaseOf/SweepExpired/ApproveRecovery/ReconcileAfterBoot
维护会话状态机Plugin.DeviceDebug/Model/DebugSessionManager.csOpenSession(死会话回收) / RequestMaintenance(令牌复用) / ActivateMaintenance / CloseSession
调试页时序Plugin.DeviceDebug/DeviceDebug.xaml.cs · Panels/CanDebugPanel.xaml.csOnDeviceSelected / LoadPanel(ExistingDriver 注入) / DoConnect / PollTimer_Tick / DrainBrokerMailbox
Broker 客户端Aging.Infrastructure/Broker/BrokerClientChannel.cs · Drivers/BrokeredCanDriver.csEnsureConnectedAsync(Hello) / SendAsync(关联+超时) / SubmitAsync / ThrowForState
Broker 宿主DeviceBroker/BrokerServer.cs · Program.csLoadDevices / ExecuteOneShotAsync(三重关卡) / SessionGate / RunFragmentAsync / ProbeReadback
线协议/契约Aging.ResourceContracts/BrokerIpc.cs · IpcEnvelope.cs · DeviceOperationTypes.cs · LeaseTypes.cs帧编解码 / SubmitBody·ResultBody / LeaseGrant·AcquireRequest

10 已知限制与简化(P2 计划项)

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