wenxiangchao
5 天以前 559890c482f5973f06aafdfec9550fee5ba3c7f2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>仪器注册与硬件调试 · 软件逻辑与时序梳理(V3.0 混合架构 · 2026-11-09)</title>
<style>
:root{--bg:#f6f7f9;--card:#fff;--ink:#24292f;--sub:#57606a;--line:#d0d7de;--blue:#0b5cad;--green:#1a7f37;--red:#cf222e;--orange:#bc4c00;--purple:#8250df;--code:#f0f2f5}
*{box-sizing:border-box}body{margin:0;font-family:"Microsoft YaHei","Segoe UI",sans-serif;background:var(--bg);color:var(--ink);line-height:1.7}
.wrap{max-width:1080px;margin:0 auto;padding:24px 20px 80px}
header{background:linear-gradient(135deg,#0b3d6e,#0b5cad 60%,#127fc4);color:#fff;border-radius:12px;padding:26px 30px;margin-bottom:22px}
header h1{margin:0 0 8px;font-size:24px}header p{margin:2px 0;opacity:.88;font-size:13.5px}
.badge{display:inline-block;background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.35);border-radius:20px;padding:1px 12px;font-size:12px;margin:6px 8px 0 0}
h2{font-size:19px;margin:34px 0 12px;padding-left:12px;border-left:5px solid var(--blue)}
h3{font-size:15.5px;margin:22px 0 8px;color:#0b3d6e}
.card{background:var(--card);border:1px solid var(--line);border-radius:10px;padding:16px 20px;margin:10px 0}
pre.seq{background:#0f1720;color:#cfe3f5;border-radius:8px;padding:14px 16px;overflow-x:auto;font-family:Consolas,monospace;font-size:12.5px;line-height:1.55}
pre.seq b{color:#7fd0ff}pre.seq i{color:#85e3a5;font-style:normal}pre.seq u{color:#ffc46b;text-decoration:none}pre.seq s{color:#ff9b9b;text-decoration:none}
table{border-collapse:collapse;width:100%;margin:10px 0;font-size:13px}
th{background:#eef3f8;text-align:left}th,td{border:1px solid var(--line);padding:6px 10px;vertical-align:top}
code{background:var(--code);border-radius:4px;padding:1px 6px;font-family:Consolas,monospace;font-size:12.5px}
.tag{display:inline-block;border-radius:4px;padding:0 8px;font-size:11.5px;font-weight:600;color:#fff;margin-right:6px;white-space:nowrap}
.t-local{background:var(--green)}.t-broker{background:var(--blue)}.t-warn{background:var(--orange)}.t-err{background:var(--red)}.t-core{background:var(--purple)}
.note{background:#fff8e6;border:1px solid #e8d48a;border-radius:8px;padding:10px 14px;font-size:13px;margin:10px 0}
.fixed{background:#e8f5ec;border:1px solid #9fd3ae;border-radius:8px;padding:10px 14px;font-size:13px;margin:10px 0}
ul,ol{margin:6px 0;padding-left:24px}li{margin:3px 0}
.small{font-size:12px;color:var(--sub)}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px}@media(max-width:820px){.grid2{grid-template-columns:1fr}}
.toc a{display:inline-block;margin:2px 10px 2px 0;font-size:13px;color:var(--blue);text-decoration:none}.toc a:hover{text-decoration:underline}
</style>
</head>
<body><div class="wrap">
 
<header>
<h1>仪器注册与硬件调试 · 软件逻辑与时序梳理</h1>
<p>依据《Local + DeviceBroker 混合架构详细设计说明书 V3.0》与当前代码实况整理(含近期缺陷修复后的最终逻辑)</p>
<p class="small">范围:Plugin.DeviceConfig(仪器仪表注册) · Plugin.DeviceDebug(硬件调试) · Aging.Infrastructure / Aging.ResourceContracts(基础设施与契约) · DeviceBroker.exe(共享仪器宿主) · Plugin.IntegrationTest(集成测试环境)</p>
<span class="badge">状态:现行实现基线 2026-11-09</span><span class="badge">net472 / WPF / SQLite / Named Pipes</span><span class="badge">两进程仲裁域:local-main + broker-b01</span>
</header>
 
<div class="card toc">
<b>目录</b>:
<a href="#s1">1 进程与数据拓扑</a><a href="#s2">2 启动初始化时序</a><a href="#s3">3 仪器注册逻辑</a><a href="#s4">4 驱动分派与配置哈希</a><a href="#s5">5 调试页选设备时序</a><a href="#s6">6 维护模式状态机与租约</a><a href="#s7">7 报文收发全链路</a><a href="#s8">8 异常与自愈机制</a><a href="#s9">9 关键代码索引</a><a href="#s10">10 已知限制</a>
</div>
 
<!-- ============================================================ -->
<h2 id="s1">1 进程与数据拓扑</h2>
<div class="card">
<pre class="seq">
┌───────────────────────── 测试/操作员工作站(单机) ─────────────────────────┐
│                                                                            │
│  <b>Plugin.IntegrationTest.exe</b>(主程序进程,一切 UI 与业务调用方的宿主)      │
│  ├─ Plugin.DeviceConfig   仪器仪表注册页(编辑草稿/发布门禁/版本管理)      │
│  ├─ Plugin.DeviceDebug    硬件调试页(会话状态机/维护申请/CAN·电源·PLC面板)│
│  ├─ Aging.Infrastructure  DeviceManager / DeviceService / AutoDeviceFacade  │
│  │   ├─ Local 设备 → 直驱驱动(CanDriverAdapter 等,物理会话在本进程)      │
│  │   └─ Broker 设备 → <b>BrokeredCanDriver 代理驱动</b>(不持物理会话)         │
│  └─ LayeredResourceManager ──┐(进程内缓存 grants + 委托 DbLeaseStore)     │
│                               │                                            │
│        命名管道 RPC           │        ┌───────────────────────────────────┤
│   aging.<u>brokerId</u>.v1(帧协议) │        │  <b>DeviceBroker.exe</b>(共享仪器宿主进程)│
│        ◄──────────────────────┴────────┤  BrokerServer:三重关卡+SessionGate │
│                                        │  DbLeaseStore(broker-b01) 读仲裁库 │
│                                        │  CanDriverAdapter → 物理会话唯一持有│
│                                        └───────────────────────────────────┘
│  数据层(本机 SQLite,禁止网络盘):                                        │
│   <i>data.db</i>  配置库:DeviceDefinition(含执行位置5列)/DeviceChannel/Slot映射/版本 │
│   <i>arb.db</i>   仲裁库:resource_state/lease_history/recovery_checkpoint/authority_meta │
│   <u>debug.db</u>  调试会话库:debug_session(状态机持久化)+操作日志              │
└────────────────────────────────────────────────────────────────────────────┘
</pre>
<p><span class="tag t-local">Local 直连</span>适合已分配给唯一执行进程、可独立连接的设备;物理会话在客户端进程内。<br>
<span class="tag t-broker">Broker 代理</span>适合跨进程共享设备:物理会话由 DeviceBroker.exe <b>唯一持有</b>,客户端只做 RPC 委托。</p>
<p class="small">注:CAN 总线模拟器(CanSimulator)是<b>进程内</b>虚拟总线——主程序与宿主各自持有一套 CAN_BUS_1 实例,跨进程不可见;真实硬件(物理卡/总线)天然共享,无此限制。测试环境用同宿主的 CAN-004 作为 CAN-003 的对端。</p>
</div>
 
<!-- ============================================================ -->
<h2 id="s2">2 启动初始化时序(MainWindow.InitializePlugins)</h2>
<div class="card">
<pre class="seq">
 1. DeviceConfig.InitFinish          → 建库/迁移 data.db(PRAGMA 检查→ALTER 补执行位置5列)
 2. SeedPresetCanDevices()           → 预置 CAN-001/002/003(<b>必须先于基础设施</b>:Bootstrapper 扫库决定驱动注册)
 3. InitializeAutoServices()
    3.1 InfrastructureBootstrapper.Initialize(data.db 路径)
        a. DbLeaseStore(arb.db, "local-main")
        b. <u>ReconcileAfterBoot</u>          ← 启动对账:SweepExpired + ApproveRecovery 回收上次进程泄漏租约
        c. LayeredResourceManager 注册进 UtilContainer("ResourceManager"/"LeaseCoordinator")
        d. DeviceManager / DeviceService / PowerSupplyService / AutoDeviceFacade / CanBusService
        e. <b>RegisterBrokeredDrivers</b>   ← 扫 data.db:ExecutionMode='Broker' 的设备注册
                                           BrokeredCanDriver(管道=aging.{BrokerId}.v1, 携启动时 configHash)
    3.2 从 UtilContainer 拉取 IAutoDeviceFacade / ICanBusService,注册 Local CAN 直驱
    3.3 <b>TryStartDeviceBroker()</b>       ← 若存在 Broker 设备:Process.Start DeviceBroker.exe
                                           --db data.db --arb arb.db --broker broker-b01
 4. DeviceDebug.InitFinish           → 从 UtilContainer 取 DeviceManager/DeviceService/ResourceManager
                                       <b>(必须在 3 之后!倒置会取到 null,仲裁/驱动复用链路全部静默降级)</b>
 5. ForceRefreshForTest()×2          → 两插件列表刷新
 Window_Closing:Kill DeviceBroker 子进程(其物理会话随进程消亡,租约靠 TTL+启动对账收口)
</pre>
<div class="fixed">✅ <b>修复记录</b>:原时序中 DeviceDebug.InitFinish 早于 Bootstrapper.Initialize,导致调试页取到 null 服务——维护仲裁被绕过、Broker 设备连接状态恒"未连接"、面板退化为本地直驱。现已调整为"建库→预置→基础设施→调试插件"。</div>
</div>
 
<!-- ============================================================ -->
<h2 id="s3">3 仪器注册逻辑(Plugin.DeviceConfig)</h2>
<div class="grid2">
<div class="card">
<h3>3.1 编辑弹窗(DeviceEditModal)字段与联动</h3>
<table>
<tr><th>区块</th><th>字段</th><th>规则</th></tr>
<tr><td rowspan="5">设备信息</td><td>DeviceId</td><td>新增可编辑、编辑只读;保存时查重</td></tr>
<tr><td>Kind / DriverType</td><td>类型联动驱动建议(CanDevice→CAN,PowerSupply→SCPI…)</td></tr>
<tr><td>Transport</td><td>Tcp/Serial/Visa/SDK/Can</td></tr>
<tr><td>Endpoint</td><td>按 Transport 校验格式并更新提示(CAN:总线:节点)</td></tr>
<tr><td>通道列表</td><td>ChannelId 唯一,能力/电压/电流/SessionKey</td></tr>
<tr><td rowspan="5">执行位置<br>(V3.0 §05)</td><td>ExecutionMode</td><td>单选 <span class="tag t-local">Local</span>(默认)/<span class="tag t-broker">Broker</span>;与仲裁域<b>双向联动</b></td></tr>
<tr><td>AuthorityId</td><td>必填下拉,候选来自 <code>V3AuthorityCatalog</code>(appSettings <code>V3.AuthorityIds</code>,未配置回退 <code>local-main;broker-b01</code>)</td></tr>
<tr><td>BrokerId</td><td>仅 Broker 显示;候选 <code>V3.BrokerIds</code>;门禁要求 BrokerId==AuthorityId</td></tr>
<tr><td>OwnerProcessId</td><td>只读=当前进程 PID(MVP 占位);<b>不入配置哈希</b>(运行期信息)</td></tr>
<tr><td>PhysicalSessionKey</td><td>仅 Broker;候选为端点规范化的写死字符串:<code>can/{bus}/main</code>、<code>tcp/{ip:port}/main</code>、<code>serial/{COM}/main</code>、<code>{deviceId}/main</code>、已存通道 SessionKey</td></tr>
<tr><td>访问策略</td><td>AccessPolicy</td><td><b>只读派生</b>不落库:Local→"独占直连·本进程持有物理会话";Broker→"共享仲裁·Broker SessionGate 代理"</td></tr>
</table>
<p class="small">联动规则:选 Broker → 仲裁域自动切到首个 Broker 候选;选 local- 域 → 自动切回 Local。XAML 初始化期事件加控件空引用守卫。</p>
</div>
<div class="card">
<h3>3.2 数据模型与持久化</h3>
<ul>
<li><code>DeviceDefinition</code> 表 13 列 = 原 8 列 + 执行位置 5 列;旧库经 <code>PRAGMA table_info</code> 检查后 <code>ALTER TABLE ADD COLUMN</code> 迁移,存量数据默认 <code>Local/空AuthorityId</code>。</li>
<li>实体默认值 <code>ExecutionMode=Local</code>;读取端 <code>HasColumn</code> 容错缺列。</li>
<li>批量生成(BatchGenerateModal)走同一 <code>SaveDevice</code>,新列取默认。</li>
</ul>
<h3>3.3 版本状态机(发布工作流 §05)</h3>
<pre class="seq">
Draft ──保存草稿──► (重算 hash 落库)
Draft ──发布(PublishVersion)──► ① ValidatePublishGate() 拦截
                               ② 通过→重算 hash→status=Published
Published ──应用(Apply)──► Effective(旧 Effective 退回 Published)
</pre>
<h3>3.4 发布门禁校验项(ValidatePublishGate)</h3>
<ol>
<li>AuthorityId 必填且在配置枚举内(存量空值 → 提示"旧数据请重新保存")</li>
<li>Local 设备的域必须 <code>local-</code> 前缀</li>
<li>Broker 设备:BrokerId 必填、在枚举内、且与 AuthorityId 一致(域一致)</li>
<li>Broker 设备:PhysicalSessionKey 必填</li>
<li>同一 PhysicalSessionKey 只允许一个宿主域("同一会话只能有一个宿主")</li>
</ol>
<div class="note">任一失败 → 弹窗列出错误(最多显示 8 条)并阻止发布;冲突检测(IP/Slot/越界)仍走 <code>DetectConflicts</code>。</div>
</div>
</div>
 
<!-- ============================================================ -->
<h2 id="s4">4 驱动分派与配置哈希(INV-04)</h2>
<div class="card">
<pre class="seq">
                    注册页保存设备(含执行位置) ──► data.db
                                  │
 Bootstrapper 启动扫描             ▼
   ExecutionMode='Local'  ──► DriverFactory.CreateDriver ──► 直驱(本进程物理会话)
   ExecutionMode='Broker' ──► <b>BrokeredCanDriver</b>(deviceId, aging.{BrokerId}.v1, hash₀)
                                  │  hash₀ = DeviceConfigHash.Compute(data.db)
                                  │  规则与 DeviceConfigModel.ComputeConfigHash 完全一致:
                                  │  DEVICE(id|kind|driver|transport|endpoint|<b>mode|authority|broker|sessionKey</b>)
                                  │  + CHANNEL + SLOT 三段拼接 SHA256;OwnerProcessId 不参与
                                  ▼ 每次 RPC 携带
                     宿主出队时与自己重算的生效 hash 对账 → 不一致 RejectedConfigMismatch
</pre>
<ul>
<li><b>分派点在驱动层</b>:<code>DeviceService</code> 不感知 Local/Broker——它只从 <code>DeviceManager.GetDriver(deviceId)</code> 取驱动。上层(自动化门面、调试面板)零改动。</li>
<li>配置哈希 = 快照指纹:Broker 用 <code>DeviceConfigHash.Compute</code>(与注册页同算法,缺表跳过空段)每 2s 惰性重算。</li>
</ul>
</div>
 
<!-- ============================================================ -->
<h2 id="s5">5 调试页选设备时序(OnDeviceSelected)</h2>
<div class="card">
<pre class="seq">
设备下拉变更
 1. LoadDeviceInfo(deviceId)              ← 读 data.db:Kind/Endpoint/EffectiveVersion
 2. OpenSession(deviceId)                 ← DebugSessionManager
      a. GetActiveSessionByDevice → 命中且<b>在本进程内存</b> → 复用返回
      b. 命中但仅存在于 debug.db(进程重启遗留死会话)→ <u>强制回收为 Closed</u> 再新建
      c. 新建 session(State=ReadOnly,写 debug_session 表 + 入内存缓存)
 3. DeviceManager.ConnectAsync(deviceId)
      <span class="tag t-local">Local</span>  直驱 ConnectAsync:注册总线节点/建立 TCP…
      <span class="tag t-broker">Broker</span> BrokeredCanDriver → RPC CONNECT(<b>无租约也允许</b>:仅登记宿主物理会话,无总线输出)
      异常→仅连接状态显示"连接失败",不抛出
 4. UpdateButtonStates()                  ← "进入维护"仅 ReadOnly 时可用
 5. LoadPanel(kind)
      CAN 面板:ExistingDriver = DeviceManager.GetDriver(deviceId)
        ├─ 非 null(Broker 设备)→ 面板复用该驱动,<b>禁止本地直驱</b>(不 Dispose 外部驱动)
        └─ null(未注册)→ 降级本地直驱(模拟器语义,owns=true)
      面板连接后:DispatcherTimer 100ms 轮询启动(UpdateStats + 代理驱动邮箱抽取)
 顶部"连接状态"与面板内"状态"为两个指标:前者=DeviceManager 通路,后者=面板实际使用的驱动
</pre>
</div>
 
<!-- ============================================================ -->
<h2 id="s6">6 维护模式状态机与租约时序</h2>
<div class="grid2">
<div class="card">
<h3>6.1 会话状态机</h3>
<pre class="seq">
             RequestMaintenance ✔            ActivateMaintenance
  ReadOnly ────────────────────► AcquiringMaintenance ────────────► <b>MaintenanceActive</b>
     ▲                               │失败(占用/超时)                     │ CloseSession
     │回收死会话(Closed)             ▼                                   ▼
     └────────────────────────  保持 ReadOnly                     Closing → Closed
                                                                    (释放维护租约)
</pre>
<h3>6.2 进入维护(含跨进程仲裁)</h3>
<pre class="seq">
"进入维护" → RequestMaintenance(sessionId)
 1. 会话在内存缓存?不在→debug.db 恢复兜底;仍无→拒绝
 2. State==ReadOnly?否则拒绝(按钮已按此置灰)
 3. <b>取/建会话→令牌缓存 _sessionOwnerTokens[sessionId]</b>
    (ResourceOwnerToken.TokenId 是随机 GUID,<u>必须复用同一实例</u>,
      否则 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(写控件解锁)
</pre>
</div>
<div class="card">
<h3>6.3 退出维护</h3>
<pre class="seq">
"退出维护" → CloseSession
 1. State=MaintenanceActive?→ Closing
 2. ReleaseReservationAsync(<b>复用进入时的同一令牌</b>)
      → DbLeaseStore.Release → Phase=0(Free),lease_history 记 Released
 3. Closed + closed_at 落库、移出内存缓存、清令牌缓存
 <b>校验点</b>:arb.db resource_state 应变回 Free —— 泄漏租约曾导致
 "第二次进入维护永久失败",根因与修复见 §8/§10
</pre>
<h3>6.4 维护与写权限矩阵(宿主侧执行语义)</h3>
<table>
<tr><th>操作</th><th>租约要求</th><th>原因</th></tr>
<tr><td>CONNECT / DISCONNECT</td><td><i>豁免</i></td><td>仅登记/释放宿主物理会话,无总线输出</td></tr>
<tr><td>RECV / RECV_ALL / LOG / STATUS</td><td><i>豁免</i></td><td>纯读邮箱/状态,被动监控支持调试页只读监听</td></tr>
<tr><td>SEND(帧发送)</td><td><b>必须 Active</b></td><td>总线写,INV-01 禁止无租约直接 I/O</td></tr>
<tr><td>AUTO_RESPONSE / FILTER / PROMISCUOUS</td><td><b>必须 Active</b></td><td>有副作用的 QUERY,改变节点行为</td></tr>
</table>
<div class="fixed">✅ 因此正确操作顺序即您指出的:<b>先进入维护模式 → 才能打开自动响应/发送</b>;只读"看帧"不需要维护。</div>
</div>
</div>
 
<!-- ============================================================ -->
<h2 id="s7">7 报文收发全链路时序(Broker 设备)</h2>
<div class="card">
<h3>7.1 发送(维护模式下,CAN-003 → 总线)</h3>
<pre class="seq">
面板"发送" → BrokeredCanDriver.SendCommandAsync("SEND:0x123:55 66")
 └─ BrokerClientChannel.EnsureConnectedAsync     ← 惰性:管道断开自动重连+重握手
     └─ <b>Hello</b> → 宿主回 {ServerEpoch, ConfigHash, BrokerId, ProtocolVersion}
        协议主版本不符 → 断开拒绝
 └─ <b>SubmitTask</b>(帧=4字节大端长度+UTF8 JSON 封套;RequestId 关联、deadline 超时)
     Body={DeviceId, OpCode=SEND, Payload, ResourceKey=deviceId, ClientInstanceId}
          ▼ ─────────── DeviceBroker.exe ───────────
     ①白名单+域内设备过滤          非本域/未知op → Failed
     ②<b>configHash 对账</b>(INV-04)    不一致 → RejectedConfigMismatch("先 Apply 再 I/O")
     ③<b>栅栏再校验</b>(INV-01/03)      RecoveryRequired→拒绝;无 Active 租约→RejectedStaleLease
     ④<b>SessionGate</b>(按 PhysicalSessionKey 串行原子片段)
        队列等待 5s 超时 → Failed RESOURCE_BUSY(确定未执行,可安全重试)
     ⑤CanDriverAdapter.SendFrame(宿主进程物理/模拟总线)
     ⑥ResultBody{State, Readback, ServerEpoch} 同管道返回
          ▼ ─────────── 客户端 ───────────
     Succeeded→返回设备响应串 | RejectedStaleLease→抛 LEASE_STALE
     ConfigMismatch→抛异常 | Unknown(超时/片段中异常)→<u>TimeoutException 禁止假定未执行(INV-06)</u>
</pre>
<h3>7.2 接收(总线帧 → 面板显示)</h3>
<pre class="seq">
总线广播 → 宿主内 node(CAN-003) 收帧 → RxCount++,帧入 _recvBuffer + 节点邮箱
客户端面板 DispatcherTimer(100ms) 每 10 tick(≈1s):
   DrainBrokerMailbox → RPC QUERY <b>"RECV_ALL"</b>(只读,<u>无需租约</u>)
     → 解析 "RECV_ALL:n:id:data[:ts]|…" → AddReceivedFrame(RX, 来源=<b>BUS@broker</b>)
     RPC 失败(宿主不可达等)静默跳过,下一秒重试,不弹错误
(Local 直驱设备无需轮询:DriverFrameReceived 事件即时显示)
对端模拟:真实硬件同一总线天然互通;模拟器场景用<b>同宿主的 CAN-004</b> 作对端验证。
</pre>
<div class="fixed">✅ <b>修复记录</b>:① 代理驱动无帧事件且面板从不查邮箱 → "只有发送没有接收",现增加 RECV_ALL 只读轮询;② 宿主 <code>HandleRecvAll</code> 曾把同一帧经 buffer+邮箱双路上报(RX 重复行),已改为 buffer 优先、邮箱仅兜底。</div>
</div>
 
<!-- ============================================================ -->
<h2 id="s8">8 异常与自愈机制</h2>
<div class="card">
<table>
<tr><th>场景</th><th>机制</th><th>表现/去向</th></tr>
<tr><td>进程异常退出(崩溃/被杀)</td><td>租约不再续期 → TTL(300s) 过期 → <b>RecoveryRequired</b>(INV-03 禁止自动移交)</td><td>下次 Acquire 返回 RecoveryBlocked → "无法进入维护"</td></tr>
<tr><td>宿主/主程序<b>重启</b></td><td><b>ReconcileAfterBoot</b>:SweepExpired(过期→待回收+历史)→ 逐个 ApproveRecovery(登记读回证据进 recovery_checkpoint,Phase=Free,Generation 不回退)</td><td>启动即自愈;证据可在 arb.db 审计</td></tr>
<tr><td>调试页维护租约泄漏</td><td>根因:进出维护各自 new Token(TokenId 随机 GUID)→ 释放按 TokenId 查找落空。修复:会话→令牌实例缓存复用</td><td>退出维护后 resource_state 必回 Free</td></tr>
<tr><td>debug.db 死会话残留</td><td>OpenSession 检出"DB 有、内存无"的未关闭会话 → 回收 Closed 再新建</td><td>不再出现"会话不存在"型拒绝</td></tr>
<tr><td>租约过期后使用(作用域租约)</td><td>AutoDeviceLease 心跳 8s Renew;连续 2 次失败 → LeaseLost 事件 + 后续调用抛 <b>LEASE_STALE</b>(熔断)</td><td>自动化步骤快速失败而非静默半执行</td></tr>
<tr><td>RPC 超时/断线</td><td>响应关联超时 → State=<b>Unknown</b>(INV-06);读循环断线感知 → 下次调用惰性重连重握手</td><td>面板轮询静默重试;显式 I/O 抛异常禁止重试假定未执行</td></tr>
<tr><td>配置版本错位(客户端快照旧)</td><td>宿主每次出队对账 configHash → RejectedConfigMismatch</td><td>强制"先 Apply 再 I/O"</td></tr>
</table>
</div>
 
<!-- ============================================================ -->
<h2 id="s9">9 关键代码索引</h2>
<div class="card">
<table>
<tr><th>职责</th><th>文件</th><th>关键成员</th></tr>
<tr><td>启动装配</td><td>Plugin.IntegrationTest/MainWindow.xaml.cs</td><td>InitializePlugins / SeedPresetCanDevices / InitializeAutoServices / TryStartDeviceBroker</td></tr>
<tr><td>注册 UI/联动/校验</td><td>Plugin.DeviceConfig/DeviceEditModal.xaml(.cs)</td><td>Open / ExecMode_Changed / CbAuthority_SelectionChanged / RefreshSessionKeyCandidates / SaveBtn_Click</td></tr>
<tr><td>候选枚举配置</td><td>Plugin.DeviceConfig/V3AuthorityCatalog.cs + Plugin.IntegrationTest/App.config</td><td>AuthorityIds / BrokerIds / LocalAuthorityId / IsLocalAuthority</td></tr>
<tr><td>发布门禁/哈希/迁移</td><td>Plugin.DeviceConfig/Model/DeviceConfigModel.cs</td><td>MigrateExecutionColumns / ValidatePublishGate / ComputeConfigHash / SaveDevice</td></tr>
<tr><td>实体</td><td>Plugin.DeviceConfig/Entity/DeviceDefinition.cs</td><td>ExecutionModeKind / AuthorityId / BrokerId / PhysicalSessionKey / AccessPolicy(派生)</td></tr>
<tr><td>服务装配</td><td>Aging.Infrastructure/InfrastructureBootstrapper.cs</td><td>Initialize / RegisterBrokeredDrivers</td></tr>
<tr><td>跨进程租约</td><td>Aging.Infrastructure/Core/DbLeaseStore.cs · LayeredResourceManager.cs</td><td>Acquire/Renew/Release/PhaseOf/SweepExpired/ApproveRecovery/<b>ReconcileAfterBoot</b></td></tr>
<tr><td>维护会话状态机</td><td>Plugin.DeviceDebug/Model/DebugSessionManager.cs</td><td>OpenSession(死会话回收) / RequestMaintenance(令牌复用) / ActivateMaintenance / CloseSession</td></tr>
<tr><td>调试页时序</td><td>Plugin.DeviceDebug/DeviceDebug.xaml.cs · Panels/CanDebugPanel.xaml.cs</td><td>OnDeviceSelected / LoadPanel(ExistingDriver 注入) / DoConnect / PollTimer_Tick / DrainBrokerMailbox</td></tr>
<tr><td>Broker 客户端</td><td>Aging.Infrastructure/Broker/BrokerClientChannel.cs · Drivers/BrokeredCanDriver.cs</td><td>EnsureConnectedAsync(Hello) / SendAsync(关联+超时) / SubmitAsync / ThrowForState</td></tr>
<tr><td>Broker 宿主</td><td>DeviceBroker/BrokerServer.cs · Program.cs</td><td>LoadDevices / ExecuteOneShotAsync(三重关卡) / SessionGate / RunFragmentAsync / ProbeReadback</td></tr>
<tr><td>线协议/契约</td><td>Aging.ResourceContracts/BrokerIpc.cs · IpcEnvelope.cs · DeviceOperationTypes.cs · LeaseTypes.cs</td><td>帧编解码 / SubmitBody·ResultBody / LeaseGrant·AcquireRequest</td></tr>
</table>
</div>
 
<!-- ============================================================ -->
<h2 id="s10">10 已知限制与简化(P2 计划项)</h2>
<div class="card">
<ul>
<li><b>owner↔clientInstanceId 弱对齐</b>:同机 arb.db 栅栏只做存在性/相位检查,未校验申请者身份与请求连接一一对应(待签名凭据/epoch token)。</li>
<li><b>模拟器跨进程总线</b>:CanSimulator 进程内实例互不相通,跨进程收发仅同宿主设备间可达;真实硬件无此限制。</li>
<li><b>推迟能力</b>:Draining 自动让渡、周期任务/订阅/ResultOutbox、父设备 Permit 多通道仲裁、Prepare/Activate 两段式跨域预留、发布预览差异。</li>
<li><b>UI 待办</b>:App.Success/App.Warn 标题未本地化;退出维护后设备选择器被重置;临时诊断日志(brokerrx_diag.log)待验证通过后撤除;RecoveryRequired 尚无人工审批 UI(当前由启动对账代偿)。</li>
<li><b>BrokerId/OwnerProcessId 语义</b>:OwnerProcessId 为 MVP 占位(当前进程 PID),Broker 常驻进程应为其宿主 PID 且随宿主重启更新——不入哈希的设计已预留。</li>
</ul>
<p class="small">本文档与代码实况同步于 2026-11-09;实现变更请同步更新本文件。</p>
</div>
 
</div></body>
</html>