<!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>
|