依据《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┌───────────────────────── 测试/操作员工作站(单机) ─────────────────────────┐ │ │ │ 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 的对端。
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+启动对账收口)
| 区块 | 字段 | 规则 |
|---|---|---|
| 设备信息 | DeviceId | 新增可编辑、编辑只读;保存时查重 |
| Kind / DriverType | 类型联动驱动建议(CanDevice→CAN,PowerSupply→SCPI…) | |
| Transport | Tcp/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 初始化期事件加控件空引用守卫。
DeviceDefinition 表 13 列 = 原 8 列 + 执行位置 5 列;旧库经 PRAGMA table_info 检查后 ALTER TABLE ADD COLUMN 迁移,存量数据默认 Local/空AuthorityId。ExecutionMode=Local;读取端 HasColumn 容错缺列。SaveDevice,新列取默认。
Draft ──保存草稿──► (重算 hash 落库)
Draft ──发布(PublishVersion)──► ① ValidatePublishGate() 拦截
② 通过→重算 hash→status=Published
Published ──应用(Apply)──► Effective(旧 Effective 退回 Published)
local- 前缀DetectConflicts。
注册页保存设备(含执行位置) ──► 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
DeviceService 不感知 Local/Broker——它只从 DeviceManager.GetDriver(deviceId) 取驱动。上层(自动化门面、调试面板)零改动。DeviceConfigHash.Compute(与注册页同算法,缺表跳过空段)每 2s 惰性重算。
设备下拉变更
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 通路,后者=面板实际使用的驱动
RequestMaintenance ✔ ActivateMaintenance
ReadOnly ────────────────────► AcquiringMaintenance ────────────► MaintenanceActive
▲ │失败(占用/超时) │ CloseSession
│回收死会话(Closed) ▼ ▼
└──────────────────────── 保持 ReadOnly Closing → Closed
(释放维护租约)
"进入维护" → 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(写控件解锁)
"退出维护" → 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
| 操作 | 租约要求 | 原因 |
|---|---|---|
| CONNECT / DISCONNECT | 豁免 | 仅登记/释放宿主物理会话,无总线输出 |
| RECV / RECV_ALL / LOG / STATUS | 豁免 | 纯读邮箱/状态,被动监控支持调试页只读监听 |
| SEND(帧发送) | 必须 Active | 总线写,INV-01 禁止无租约直接 I/O |
| AUTO_RESPONSE / FILTER / PROMISCUOUS | 必须 Active | 有副作用的 QUERY,改变节点行为 |
面板"发送" → 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)
总线广播 → 宿主内 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 作对端验证。
HandleRecvAll 曾把同一帧经 buffer+邮箱双路上报(RX 重复行),已改为 buffer 优先、邮箱仅兜底。| 场景 | 机制 | 表现/去向 |
|---|---|---|
| 进程异常退出(崩溃/被杀) | 租约不再续期 → 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" |
| 职责 | 文件 | 关键成员 |
|---|---|---|
| 启动装配 | Plugin.IntegrationTest/MainWindow.xaml.cs | InitializePlugins / SeedPresetCanDevices / InitializeAutoServices / TryStartDeviceBroker |
| 注册 UI/联动/校验 | Plugin.DeviceConfig/DeviceEditModal.xaml(.cs) | Open / ExecMode_Changed / CbAuthority_SelectionChanged / RefreshSessionKeyCandidates / SaveBtn_Click |
| 候选枚举配置 | Plugin.DeviceConfig/V3AuthorityCatalog.cs + Plugin.IntegrationTest/App.config | AuthorityIds / BrokerIds / LocalAuthorityId / IsLocalAuthority |
| 发布门禁/哈希/迁移 | Plugin.DeviceConfig/Model/DeviceConfigModel.cs | MigrateExecutionColumns / ValidatePublishGate / ComputeConfigHash / SaveDevice |
| 实体 | Plugin.DeviceConfig/Entity/DeviceDefinition.cs | ExecutionModeKind / AuthorityId / BrokerId / PhysicalSessionKey / AccessPolicy(派生) |
| 服务装配 | Aging.Infrastructure/InfrastructureBootstrapper.cs | Initialize / RegisterBrokeredDrivers |
| 跨进程租约 | Aging.Infrastructure/Core/DbLeaseStore.cs · LayeredResourceManager.cs | Acquire/Renew/Release/PhaseOf/SweepExpired/ApproveRecovery/ReconcileAfterBoot |
| 维护会话状态机 | Plugin.DeviceDebug/Model/DebugSessionManager.cs | OpenSession(死会话回收) / RequestMaintenance(令牌复用) / ActivateMaintenance / CloseSession |
| 调试页时序 | Plugin.DeviceDebug/DeviceDebug.xaml.cs · Panels/CanDebugPanel.xaml.cs | OnDeviceSelected / LoadPanel(ExistingDriver 注入) / DoConnect / PollTimer_Tick / DrainBrokerMailbox |
| Broker 客户端 | Aging.Infrastructure/Broker/BrokerClientChannel.cs · Drivers/BrokeredCanDriver.cs | EnsureConnectedAsync(Hello) / SendAsync(关联+超时) / SubmitAsync / ThrowForState |
| Broker 宿主 | DeviceBroker/BrokerServer.cs · Program.cs | LoadDevices / ExecuteOneShotAsync(三重关卡) / SessionGate / RunFragmentAsync / ProbeReadback |
| 线协议/契约 | Aging.ResourceContracts/BrokerIpc.cs · IpcEnvelope.cs · DeviceOperationTypes.cs · LeaseTypes.cs | 帧编解码 / SubmitBody·ResultBody / LeaseGrant·AcquireRequest |
本文档与代码实况同步于 2026-11-09;实现变更请同步更新本文件。