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
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>仪器调用架构与数据流转说明(V3.1)</title>
<style>
:root{--bg:#f7f8fa;--card:#fff;--ink:#1f2430;--muted:#5b6472;--line:#e4e8ef;--accent:#2f6fed;--ok:#2e8b57;--warn:#b7791f;--bad:#c0392b;--code:#0d1117;}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.7 "Segoe UI","Microsoft YaHei",Arial,sans-serif}
.wrap{max-width:1120px;margin:0 auto;padding:32px 20px 90px}
h1{font-size:25px;margin:0 0 6px}
h2{font-size:20px;margin:34px 0 12px;padding-bottom:6px;border-bottom:2px solid var(--line)}
h3{font-size:16px;margin:20px 0 8px;color:var(--accent)}
.sub{color:var(--muted);margin:0 0 18px}
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px 18px;margin:14px 0;box-shadow:0 1px 2px rgba(0,0,0,.03)}
pre{background:var(--code);color:#e6edf3;border-radius:10px;padding:14px 16px;overflow:auto;font:12.5px/1.55 "Cascadia Code",Consolas,monospace;margin:10px 0}
code{font-family:"Cascadia Code",Consolas,monospace;background:#eef1f6;padding:1px 5px;border-radius:4px;font-size:13px}
pre code{background:none;padding:0;color:inherit}
table{border-collapse:collapse;width:100%;margin:10px 0;font-size:14px}
th,td{border:1px solid var(--line);padding:7px 10px;text-align:left;vertical-align:top}
th{background:#eef2f8}
.tag{display:inline-block;padding:1px 8px;border-radius:999px;font-size:12px;font-weight:600;margin-right:4px}
.t-auto{background:#e6f0ff;color:#1b4fa0}
.t-dbg{background:#f3e8ff;color:#6b21a8}
.t-svc{background:#e7f6ec;color:#1f7a3d}
.t-dev{background:#fdeee0;color:#a35a12}
.ok{color:var(--ok);font-weight:600}.bad{color:var(--bad);font-weight:600}.warn{color:var(--warn);font-weight:600}
.small{font-size:13px;color:var(--muted)}
.toc{columns:2;font-size:14px}.toc a{color:var(--accent);text-decoration:none}
.note{background:#fff6d6;border-left:4px solid var(--warn);padding:10px 12px;border-radius:6px;margin:10px 0}
.fx{background:#e8f5e9;border-left:4px solid var(--ok);padding:10px 12px;border-radius:6px;margin:10px 0}
.layer{border-left:4px solid var(--accent);padding-left:12px;margin:8px 0}
</style>
</head>
<body>
<div class="wrap">
 
<h1>仪器调用架构与数据流转说明</h1>
<p class="sub">聚焦"一次仪器调用从入口到硬件、再回到调用方"的完整路径与类引用结构。V3.1(TCP 模拟器 + 统一 <code>CanDeviceDriver</code> + 多设备并发)。与代码同步 2026-11-09。</p>
 
<div class="card">
<b>目录</b>
<div class="toc">
<div><a href="#s1">1. 调用入口全景</a></div>
<div><a href="#s2">2. 分层类引用结构</a></div>
<div><a href="#s3">3. 自动化调用数据流转</a></div>
<div><a href="#s4">4. 调试调用数据流转</a></div>
<div><a href="#s5">5. 驱动→传输→服务端 逐跳</a></div>
<div><a href="#s6">6. 一次 CAN 发送完整时序</a></div>
<div><a href="#s7">7. 权限/租约在链路中的位置</a></div>
<div><a href="#s8">8. 关键数据结构一览</a></div>
<div><a href="#s9">9. 类引用关系总图</a></div>
</div>
</div>
 
<!-- ============================================================ -->
<h2 id="s1">1 调用入口全景</h2>
<div class="card">
<p>仪器调用只有<b>两类入口</b>,但都汇聚到同一套"服务层 → 驱动层 → 传输层",最终落到模拟器服务里的总线:</p>
<table>
<tr><th>入口</th><th>触发者</th><th>首跳 API</th><th>令牌类型</th></tr>
<tr><td><span class="tag t-auto">自动化</span>测试程序</td><td>TestPlan / 步骤脚本</td><td><code>ICanBusService</code> / <code>IAutoDeviceFacade</code></td><td>OwnerType.<b>Production</b></td></tr>
<tr><td><span class="tag t-dbg">调试</span>硬件调试页</td><td>人工(DeviceDebugTab)</td><td>面板直调驱动 或 <code>IDeviceService</code></td><td>OwnerType.<b>Maintenance</b></td></tr>
</table>
<p class="small">两条入口互斥于同一 arb.db 租约:某设备被生产预约时维护进不去,反之亦然(INV 由资源层栅栏保证)。</p>
</div>
 
<!-- ============================================================ -->
<h2 id="s2">2 分层类引用结构</h2>
<div class="card">
<pre>
【入口层】
  CanBusService ─┐                         DeviceDebugTab ─┐
  (类型化封装)    │  IAutoDeviceFacade                       │ (每设备一 Tab)
                 ▼                                           ▼
【门面/编排层】  AutoDeviceFacade ──► IDeviceService ◄── ExecuteViaDeviceService / 面板直调
                 (预约+释放编排)        (DeviceService:校验+租约+分派)
                                           │
【资源层】        IResourceManager ◄───────┤  (ValidateOwner / AcquireOperationAsync)
                 LayeredResourceManager ───┼─► ILeaseCoordinator ─► DbLeaseStore(arb.db)
                                           │
【设备/驱动层】   IDeviceManager.GetDriver(deviceId) ─► IDeviceDriver
                                           │
                       CanDeviceDriver(唯一 CAN 驱动,实现 IDeviceDriver)
                         ├─ ICanTransport
                         │    ├─ NodeChannelTransport ─► TCP ─► CanSimulatorServer
                         │    └─ BrokerRpcTransport   ─► 命名管道 ─► DeviceBroker
                         │                                   └►(宿主内) CanDeviceDriver(NodeChannel) ─► TCP ─► 模拟器
                         └─ 其它驱动:TcpRaw/Scpi/ModBus/S7(非 CAN 仪器)
 
【服务/总线层】   TcpSimulatorHost ─► ServerSession ─► CanCommandExecutor ─► CanBusSimulator/CanNode
</pre>
<p class="small">依赖方向自上而下;上层只依赖接口(IAutoDeviceFacade/IDeviceService/IResourceManager/IDeviceManager/IDeviceDriver),实现由 <code>InfrastructureBootstrapper</code> 装配进 <code>UtilContainer</code>。</p>
</div>
 
<!-- ============================================================ -->
<h2 id="s3">3 自动化调用数据流转</h2>
<div class="card">
<h3>3.1 一次性调用(OneShot)</h3>
<pre class="seq">
TestPlan 步骤
  → CanBusService.SendFrameAsync(deviceId, 0x300, data)
      · 校验帧(≤0x1FFFFFFF, ≤8字节) → 拼命令串 "SEND:0x300:AA BB"
  → AutoDeviceFacade.ExecuteAsync(deviceId, "SEND:…")
      ① ReserveCoreAsync: new ResourceOwnerToken(Production, auto_guid)
         ResourceManager.ReserveAsync(token, ResourceKey(deviceId,"Generic"))  ← 申请预约(写 arb.db, TTL)
      ② DeviceService.ExecuteCommandAsync(deviceId, cmd, token)
      ③ finally ReleaseCoreAsync → ReleaseReservationAsync  ← 释放预约
  → 返回 DeviceCommandResult{Success, Readback, LatencyMs}
</pre>
<h3>3.2 作用域租约(一步内多次调用)</h3>
<pre class="seq">
using (var lease = await AutoDeviceFacade.AcquireAsync(deviceId, "Step1"))
{
   // 内部 ReserveAsync + 若 LayeredResourceManager 则 lease.StartHeartbeat(Coordinator, grants)
   //   心跳:每 8s Coordinator.Renew(grant,30),连续失败≥2 → LeaseLost 事件
   await CanBusService.SendFrameAsync(lease, 0x100, d);   // lease.ExecuteAsync → DeviceService…
   var f   = await CanBusService.ReceiveFrameAsync(lease); // lease.QueryAsync("RECV")
}   // Dispose: 停心跳 + ReleaseReservationAsync
</pre>
<div class="note"><b>心跳差异</b>:自动化作用域租约自带 8s 心跳续期(<code>AutoDeviceLease.StartHeartbeat</code>);人工维护会话的心跳在 <code>DebugSessionManager</code>(120s),两条路径都要续期,否则到 TTL 过期触发 RECOVERY_REQUIRED。</div>
</div>
 
<!-- ============================================================ -->
<h2 id="s4">4 调试调用数据流转</h2>
<div class="card">
<p>硬件调试页每个设备一个 <code>DeviceDebugTab</code>(独立会话)。CAN 与 非CAN 的调用路径不同:</p>
<h3>4.1 CAN 面板(直调驱动,不经 DeviceService)</h3>
<pre class="seq">
DeviceDebugTab.LoadPanel("CanDevice")
  → CanDebugPanel(多通道壳) → CanChannelView(单通道, ExistingDriver=DeviceManager.GetDriver(deviceId))
  连接:DoConnect → 复用已注册 CanDeviceDriver(Broker 代理 或 Local NodeChannel)→ ConnectAsync
  发送:SendBtn_Click → _driver.SendCommandAsync("SEND:0x300:…")   ← 直接调驱动
  接收:驱动 DriverFrameReceived 事件 → AddReceivedFrame → Dispatcher.BeginInvoke 上屏
  维护门控:面板 IsMaintenanceMode(由会话状态驱动),进入维护经 DebugSessionManager
</pre>
<h3>4.2 电源/PLC 面板(经 DeviceService)</h3>
<pre class="seq">
PowerSupplyDebugPanel/PlcDebugPanel → OnCommandExecute(DebugCommand)
  → DeviceDebugTab.ExecuteViaDeviceService
     · new ResourceOwnerToken(Maintenance, sessionId)
     · BuildDeviceCommand(cmd) → 如 "VOLT …;CURR …;OUTP ON" / "WRITE:addr:TRUE"
     · DeviceService.ExecuteCommandAsync(deviceId, cmd, token)   ← 走服务层校验+租约+驱动
  → DebugOperation 落 debug.db + 操作日志
</pre>
<div class="note">CAN 走"面板直调驱动 + 会话级维护门"是为了低延迟实时收发;电源/PLC 走"服务层"以获得统一权限校验与操作审计。二者最终都经 <code>IDeviceDriver</code> 落到设备。</div>
</div>
 
<!-- ============================================================ -->
<h2 id="s5">5 驱动 → 传输 → 服务端 逐跳</h2>
<div class="card">
<p><code>CanDeviceDriver</code> 把命令交给选定的 <code>ICanTransport</code>,两条后端各自到总线的物理路径不同:</p>
<h3>5.1 Local(NodeChannelTransport)</h3>
<pre class="seq">
CanDeviceDriver.QueryAsync/SendCommandAsync
  → NodeChannelTransport.TransceiveAsync(line)
     · 首连 HandshakeAsync: 写 "HELLO:bus:node:tag" 直读 "OK:HELLO…"(读循环未起)
     · 之后 RequestLineAsync: _requestLock 串行 → 写命令帧 → 等 TCS(deadline 5s)
     · 后台 ReadLoopAsync: 分流 OK/ERR→完成 TCS;EV:RECV→FramePushed 事件
  → TCP 9000 → ServerSession → CanCommandExecutor → CanBusSimulator/CanNode
</pre>
<h3>5.2 Broker(BrokerRpcTransport)</h3>
<pre class="seq">
CanDeviceDriver(Broker).SendCommandAsync("SEND:…")
  → BrokerRpcTransport.TransceiveAsync
     · 映射 opCode(SEND/QUERY)+payload → SubmitBody → BrokerClientChannel(命名管道 RPC)
     · EnsureConnectedAsync(Hello 握手) → SubmitAsync(configHash 随信封)
  → DeviceBroker.BrokerServer.ExecuteOneShotAsync
     ① 白名单/域内  ② configHash 对账  ③ 租约栅栏  ④ SessionGate 串行
     ⑤ 宿主内 CanDeviceDriver(NodeChannel) → TCP → 模拟器路由
     ⑥ ResultBody.Readback 末尾 AppendPushes("|PUSHED:n|EV:RECV…")
  → 客户端 DrainPiggyback 剥离 PUSHED → FramePushed → 驱动缓冲+事件
</pre>
<p class="small">Broker 的接收靠"请求驱动捎带":客户端每次 STATUS/SEND 都会让宿主把自上次以来收到的推送带回;面板每 100ms 的 STATUS 轮询保证最终显示。</p>
</div>
 
<!-- ============================================================ -->
<h2 id="s6">6 一次 CAN 发送完整时序</h2>
<div class="card">
<h3>6.1 自动化 · Local 设备 CAN-001</h3>
<pre class="seq">
CanBusService.SendFrameAsync("CAN-001",0x300,[AA BB])
 → AutoDeviceFacade.ExecuteAsync → ReserveAsync(Production) → arb.db 写租约
 → DeviceService.ExecuteCommandAsync
     GetDriver("CAN-001")→CanDeviceDriver(NodeChannel)
     ValidateOwner→Renew(grant,1) 栅栏校验
     AcquireOperationAsync→OperationLease
     driver.SendCommandAsync("SEND:0x300:AA BB")
       → TCP HELLO/SEND → ServerSession → CanCommandExecutor.HandleSend
       → CanNode.SendFrame → CanBusSimulator 路由到其它节点(+应答器回显)
       → "OK:TX:0x300:2"
 → DeviceCommandResult{Success=true,Readback="OK:TX…"}
 → ReleaseReservationAsync → arb.db 释放
</pre>
<h3>6.2 调试 · Broker 设备 CAN-003</h3>
<pre class="seq">
CanChannelView.SendBtn → _driver(CanDeviceDriver/BrokerRpc).SendCommandAsync("SEND:0x300:AA BB")
 → BrokerRpcTransport → 命名管道 SubmitBody{SEND,"SEND:0x300:AA BB"}
 → BrokerServer 三重关卡+SessionGate → 宿主 CanDeviceDriver(NodeChannel) → TCP → 模拟器路由
 → ResultBody{Readback="OK:TX…" (|PUSHED 若有回显)} 回管道
 → 客户端 DrainPiggyback → 若含 PUSHED 则 FramePushed→DriverFrameReceived→面板 RX 行
(应答器开启时,模拟器把 0x300 回显给宿主节点→宿主暂存→本次/下次响应捎带回客户端)
</pre>
</div>
 
<!-- ============================================================ -->
<h2 id="s7">7 权限 / 租约在链路中的位置</h2>
<div class="card">
<pre>
入口            门面/编排           服务层(DeviceService)          资源层                驱动/传输/服务端
───────────────────────────────────────────────────────────────────────────────────────────────
自动化  ─► AutoDeviceFacade ─► ExecuteCommandAsync ─► ReserveAsync(预约,写租约)
              (Reserve/Release)   ValidateOwner(栅栏) ─► Renew(grant,1)
                                  AcquireOperationAsync ─► OperationLease
                                  driver.SendCommandAsync ─► (Local TCP / Broker RPC)
调试    ─► DebugSessionManager.RequestMaintenance ─► ReserveAsync(Maintenance) + 120s 心跳
              CAN: 面板直调 driver;电源/PLC: 经 DeviceService
Broker 宿主侧 ─► BrokerServer.ExecuteOneShotAsync 出队再校验:
              ① 白名单 ② configHash(INV-04) ③ PhaseOf/ProbeConflict(INV-01/03) ④ SessionGate
</pre>
<table>
<tr><th>校验点</th><th>方法</th><th>失败结果</th></tr>
<tr><td>预约归属</td><td>ResourceManager.ValidateOwner → Renew(grant,1)</td><td>PERMISSION_DENIED</td></tr>
<tr><td>操作互斥</td><td>AcquireOperationAsync → OperationLease</td><td>ResourceConflictException</td></tr>
<tr><td>Broker 出队栅栏</td><td>PhaseOf==Active 且有 holder</td><td>RejectedStaleLease / RECOVERY_REQUIRED</td></tr>
<tr><td>配置一致</td><td>configHash 对账</td><td>RejectedConfigMismatch(INV-04)</td></tr>
<tr><td>租约续期</td><td>Coordinator.Renew(grant,ttl)</td><td>失败≥2 → LeaseLost / 过期 RECOVERY_REQUIRED</td></tr>
</table>
</div>
 
<!-- ============================================================ -->
<h2 id="s8">8 关键数据结构一览</h2>
<div class="card">
<table>
<tr><th>类型</th><th>所在</th><th>作用</th></tr>
<tr><td><code>ResourceOwnerToken</code></td><td>Contracts</td><td>调用者身份(OwnerType+OwnerId+TokenId GUID);租约按 TokenId 索引,须复用同一实例</td></tr>
<tr><td><code>ResourceKey</code></td><td>Contracts</td><td>(deviceId,"Generic") 资源键,仲裁与预约的粒度</td></tr>
<tr><td><code>IOperationLease</code>/OperationLease</td><td>资源层</td><td>单次操作互斥租约(DeviceService 内 using)</td></tr>
<tr><td><code>IAutoDeviceLease</code>/AutoDeviceLease</td><td>AutoDeviceFacade</td><td>作用域租约 + 8s 心跳 + LeaseLost</td></tr>
<tr><td><code>DeviceCommandResult</code></td><td>Contracts</td><td>{Success,Readback,RawResponse,ErrorCode,LatencyMs}</td></tr>
<tr><td><code>DebugCommand</code>(+子类)</td><td>DeviceDebug.Entity</td><td>面板→服务的命令对象(PowerSet/CanSend/PlcWrite…)</td></tr>
<tr><td><code>DebugSession</code></td><td>DeviceDebug.Entity</td><td>会话状态机持久化(debug.db)</td></tr>
<tr><td><code>SubmitBody</code>/<code>ResultBody</code>/<code>IpcEnvelope</code></td><td>ResourceContracts.BrokerIpc</td><td>Broker RPC 线格式(设备/操作码/载荷/资源键/客户端实例;状态+读回+epoch)</td></tr>
<tr><td><code>LeaseGrant</code></td><td>ResourceContracts</td><td>arb.db 租约凭据(LeaseId/Generation/Epoch/Expires)</td></tr>
<tr><td>命令串 / <code>EV:RECV</code></td><td>SimulatorFraming 协议</td><td>SEND:/QUERY:/HELLO: 与推送行;4B 大端长度+UTF8 分帧</td></tr>
<tr><td><code>CanFrame</code>/<code>CanFrameDisplay</code></td><td>CanSimulator / DeviceDebug</td><td>总线帧模型 / 面板显示行</td></tr>
</table>
</div>
 
<!-- ============================================================ -->
<h2 id="s9">9 类引用关系总图</h2>
<div class="card">
<pre>
        [自动化]  CanBusService ──uses──► IAutoDeviceFacade(AutoDeviceFacade)
                                                   │ uses
        [调试] DeviceDebugTab ──┐                  ▼
                   │            │        IDeviceService(DeviceService)
                   │直调         │                  │ uses
                   ▼            ▼        ┌──────────┴───────────┐
        IDeviceDriver(CanDeviceDriver)   IResourceManager        IDeviceManager
                   │ holds               (LayeredResourceManager)│ GetDriver
                   ▼                          │ uses              ▼
             ICanTransport ──┐          ILeaseCoordinator   注册表: deviceId→CanDeviceDriver
              │              │              │                      │
   NodeChannelTransport  BrokerRpcTransport │              (Broker 设备=CreateBrokerProxy)
              │              │              ▼
          TCP 9000       命名管道      DbLeaseStore(arb.db)
              │              │
              ▼              ▼
   TcpSimulatorHost   BrokerServer ──(宿主内再建)──► CanDeviceDriver(NodeChannel) ──► TCP 9000
              └──────┬──────┘
                     ▼
        ServerSession → CanCommandExecutor → CanBusSimulator/CanNode
</pre>
<div class="fx"><b>一句话理解</b>:所有仪器调用都归一到 <code>IDeviceDriver</code>;CAN 的唯一实现 <code>CanDeviceDriver</code> 用 <code>ICanTransport</code> 屏蔽"本地 TCP 直连"与"Broker 管道委托"的差异;权限/租约在门面层(预约)与服务层(校验/操作租约)与 Broker 出队栅栏三处把关;数据最终以命令串/推送行经 TCP 落到模拟器服务的同一总线上。</div>
<p class="small">配套阅读:<a href="程序架构解析报告_V3.1_TCP模拟器与统一驱动.html">程序架构解析报告 V3.1</a>(进程拓扑/协议/应答器/多设备/心跳全景)。</p>
</div>
 
</div>
</body>
</html>