wenxiangchao
4 天以前 b544d5cccd76735e034c8055918a50927b437474
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
<!doctype html>
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>仪器调用程序结构与类调用说明</title>
<style>
:root{--navy:#112e45;--blue:#176b87;--ink:#1b2e3b;--muted:#506576;--line:#d9e2e8;--bg:#edf2f4;--green:#e4f3ec;--amber:#fff3d8;--red:#fbe9e7}*{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);font:15px/1.7 "Segoe UI","Microsoft YaHei",sans-serif}.layout{display:grid;grid-template-columns:245px minmax(0,1fr);min-height:100vh}.side{position:sticky;top:0;height:100vh;overflow:auto;background:var(--navy);color:#edf5f8;padding:22px 16px}.brand{font-weight:800;font-size:17px}.side small{display:block;color:#aec5d0;margin:5px 0 18px}.side a{display:block;color:#d5e6ec;text-decoration:none;padding:6px 9px;border-radius:6px;font-size:13px}.side a:hover{background:#1b435d}main{max-width:1250px;width:100%;margin:auto;padding:26px 32px 55px}.hero{color:white;background:linear-gradient(125deg,#112e45,#176b87);padding:32px 36px;border-radius:16px}.hero h1{font-size:32px;line-height:1.25;margin:8px 0}.hero p{color:#d7eaf0;max-width:850px}.pill{display:inline-block;border:1px solid #71a3b5;border-radius:18px;padding:2px 10px;margin:4px 5px 0 0;font-size:12px}.toolbar{display:flex;gap:9px;margin:13px 0;align-items:center}.toolbar input{flex:1;min-width:200px;padding:10px;border:1px solid var(--line);border-radius:8px;font:inherit}.toolbar button{border:0;background:var(--navy);color:white;padding:10px 13px;border-radius:8px}.count{font-size:12px;color:var(--muted)}section{background:white;border:1px solid var(--line);border-radius:13px;padding:24px 28px;margin:15px 0;scroll-margin-top:14px}h2{margin:0 0 13px;color:var(--navy);font-size:22px}h3{color:#17536d;font-size:17px;margin:20px 0 7px}p{margin:8px 0}.tag{display:inline-block;padding:1px 7px;border-radius:5px;font-size:11px;font-weight:bold}.fact{background:var(--green);color:#247455}.target{background:var(--amber);color:#946313}.risk{background:var(--red);color:#9d3939}.note{padding:12px 14px;border-left:4px solid var(--blue);background:#edf7f9;border-radius:0 8px 8px 0;margin:12px 0}.warn{border-left-color:#c7871e;background:#fff8e8}.danger{border-left-color:#b74444;background:#fff0ee}table{border-collapse:collapse;width:100%;font-size:13px;margin:10px 0 16px}th,td{text-align:left;vertical-align:top;border-bottom:1px solid var(--line);padding:8px 9px}th{background:#edf3f6;color:#24495d}tr:hover td{background:#f8fbfc}code{background:#edf2f4;color:#183e55;padding:1px 4px;border-radius:3px;font:12px Consolas,monospace;overflow-wrap:anywhere}.path{font:12px Consolas,monospace;color:#435c6a;overflow-wrap:anywhere}.call{display:grid;grid-template-columns:34px 1fr;gap:10px;padding:11px 7px;border-bottom:1px solid var(--line)}.call:last-child{border:0}.num{width:28px;height:28px;border-radius:50%;display:grid;place-items:center;background:#d8eef2;color:#12526b;font-weight:bold}.call strong{color:#173f55}.call p{font-size:13px;color:var(--muted);margin:2px 0}.cols{display:grid;grid-template-columns:1fr 1fr;gap:12px}.card{border:1px solid var(--line);border-radius:9px;padding:15px;background:#fbfdfe}.card h3{margin:0 0 6px}@media(max-width:820px){.layout{display:block}.side{position:static;height:auto}.side nav{display:grid;grid-template-columns:1fr 1fr}main{padding:14px}.hero{padding:23px}.hero h1{font-size:27px}section{padding:18px}.cols{grid-template-columns:1fr}table{display:block;overflow:auto}}@media print{body{background:white}.layout{display:block}.side,.toolbar{display:none}.hero{print-color-adjust:exact}.hero,section{break-inside:avoid}}
</style></head><body><div class="layout">
<aside class="side"><div class="brand">TESTPLATFORM<br>仪器调用结构</div><small>类职责 · 调用顺序 · 生命周期</small><nav>
<a href="#summary">01 结论与边界</a><a href="#layers">02 类职责分层</a><a href="#can">03 CAN 单次调用</a><a href="#lease">04 作用域租约</a><a href="#power">05 电源 Slot 调用</a><a href="#broker">06 Broker 调用</a><a href="#startup">07 启动与连接</a><a href="#result">08 结果与错误</a><a href="#gaps">09 待完善项</a><a href="#sources">10 源码定位</a></nav></aside>
<main><header class="hero"><div>TapProject / Aging.Infrastructure / DeviceBroker</div><h1>仪器调用程序结构<br>与类调用说明</h1><p>根据源码梳理测试动作如何到达驱动、驱动如何访问物理通道、结果怎样返回;区分当前实现与 V3.1 目标设计。</p><span class="pill">D:\TestPlatform\TapProject</span><span class="pill">离线 HTML</span><span class="pill">2026-11-09</span><span class="pill">不使用 OpenTAP 原生 Instrument</span></header>
<div class="toolbar"><input id="filter" type="search" placeholder="筛选本页文字:例如 CanBusService、租约、Broker"><button id="reset">清除筛选</button><span id="count" class="count">10 个章节</span></div>
<section id="summary"><h2>01 结论与阅读边界</h2><p>仪器访问主链路:<strong>测试动作 → 类型化服务/自动化门面 → 资源授权与操作租约 → DeviceService → DeviceManager → IDeviceDriver → Transport → 仪器或 DeviceBroker</strong>。返回结果沿反方向逐层包装,最终由测试步骤保存结果并生成 Verdict。</p><div class="note"><b>OpenTAP 接入约束:</b>本系统不实现原生 <code>Instrument</code>。未来仪器类 StepType 只继承 <code>TestStep</code>,在 <code>Run()</code> 调用系统服务。该接入点属于目标方案;当前 TapProject 可直接追踪的自动化示例主要位于 IntegrationTest UI,不能据此认为生产 OpenTAP 插件已接通。</div><div class="note warn"><b>连接与租约不同:</b>步骤租约是短期资源授权,物理连接由唯一 I/O 宿主持有。V3.1 规定驱动注册后连接、HostReady 前就绪检查;当前代码仍有首次 I/O 才连接的路径。</div><p><span class="tag fact">代码事实</span> 源码已有。 <span class="tag target">V3.1 目标</span> 设计要求。 <span class="tag risk">缺口</span> 尚需实现或验证。</p></section>
<section id="layers"><h2>02 分层与类职责</h2><table><thead><tr><th>层</th><th>类 / 接口</th><th>职责与调用关系</th><th>状态</th></tr></thead><tbody>
<tr><td>测试入口</td><td><code>TestStep</code></td><td>保存逻辑设备/Slot、通道和工艺参数;调用服务、发布结果、设置 Verdict。不连接物理设备,不创建厂家 SDK。</td><td><span class="tag target">计划接入</span></td></tr>
<tr><td>类型化 API</td><td><code>ICanBusService / CanBusService</code></td><td>校验 CAN 帧、编码命令、解析读回。单次调用走 Facade;多操作可借用 Lease。</td><td><span class="tag fact">已有实现</span></td></tr>
<tr><td>自动化门面</td><td><code>IAutoDeviceFacade / AutoDeviceFacade</code></td><td>单次 Reserve→Execute→Release,或 Acquire 后由 Lease.Dispose 释放预约。</td><td><span class="tag fact">已有实现</span></td></tr>
<tr><td>Slot 服务</td><td><code>PowerSupplyService</code>、<code>SlotDeviceMappingResolver</code></td><td>按 Slot+Capability 解析端点/资源键,完成电源设定、测量、状态查询。</td><td><span class="tag fact">已有实现</span></td></tr>
<tr><td>设备执行服务</td><td><code>IDeviceService / DeviceService</code></td><td>查驱动、校验 Owner、取得单次操作租约、调用 Driver 并包装结果。</td><td><span class="tag fact">已有实现</span></td></tr>
<tr><td>资源授权</td><td><code>IResourceManager</code>、Owner Token、Operation Lease</td><td>Owner Reservation 覆盖步骤作用域;Operation Lease 覆盖一次 I/O 片段,两者不可混为一谈。</td><td><span class="tag fact">已有接口</span></td></tr>
<tr><td>驱动管理</td><td><code>IDeviceManager / DeviceManager</code></td><td>按 DeviceId 保存 Driver、缓存状态、提供连接/断开 API。<code>RegisterDriver()</code> 只注册,不自动 Connect。</td><td><span class="tag fact">已有实现</span></td></tr>
<tr><td>驱动创建/协议</td><td><code>DriverFactory</code>、<code>IDeviceDriver</code>、<code>CanDeviceDriver</code>、SCPI/ModBus/S7 adapters</td><td>按 transport 创建驱动并封装协议交互;Serial 分支尚未实现。</td><td><span class="tag fact">部分实现</span></td></tr>
<tr><td>传输与宿主</td><td><code>ICanTransport</code>、<code>NodeChannelTransport</code>、<code>BrokerRpcTransport</code>、<code>BrokerServer</code></td><td>区分 Local 通道与 Broker IPC;Broker 宿主持有其配置下真实驱动。</td><td><span class="tag fact">已有实现</span></td></tr></tbody></table><div class="note">依赖方向:StepType → 稳定服务 API/client → 服务实现 → 资源/设备管理契约 → Driver 契约 → Transport。上层不要 new 厂家 SDK、直连 IP/串口或依赖 Broker 私有实现。</div></section>
<section id="can"><h2>03 CAN 单次调用:每条操作单独预约</h2><p>示例:<code>ICanBusService.SendFrameAsync(deviceId, frameId, data)</code>。</p>
<div class="call"><div class="num">1</div><div><strong>TestStep 或当前测试 UI</strong><p>提供逻辑 deviceId、CAN ID 和数据字节。TestStep 是预期接入点;当前可查到的自动化调用入口主要在 IntegrationTest 的 AutoRunBtn_Click。</p></div></div>
<div class="call"><div class="num">2</div><div><strong>CanBusService.SendFrameAsync(deviceId,...)</strong><p>校验帧 ID/长度,编码为 SEND 命令,调用 AutoDeviceFacade.ExecuteAsync。</p></div></div>
<div class="call"><div class="num">3</div><div><strong>AutoDeviceFacade.ExecuteAsync()</strong><p>创建 Owner,按当前实现申请 <code>ResourceKey(deviceId, Generic)</code>;委托 DeviceService,finally 中释放预约。</p></div></div>
<div class="call"><div class="num">4</div><div><strong>DeviceService.ExecuteCommandAsync()</strong><p>通过 DeviceManager 找 Driver,验证 Generic 资源 Owner,取得一次 Operation Lease,然后调用驱动。</p></div></div>
<div class="call"><div class="num">5</div><div><strong>IDeviceDriver → CanDeviceDriver → ICanTransport</strong><p>当前 CAN Driver 在 Send/Query 前运行 EnsureConnectedAsync;未连接时第一次 I/O 会建连。</p></div></div>
<div class="call"><div class="num">6</div><div><strong>NodeChannelTransport 或 BrokerRpcTransport → 返回</strong><p>Local 走 NodeChannelTransport;Broker 走命名管道 RPC 到 DeviceBroker。结果回到 DeviceService,再由 CanBusService 转为 bool 或帧 DTO。</p></div></div><div class="note warn">单次调用会释放资源预约,不代表调用 DeviceManager.Disconnect。CAN driver/transport 可继续持有已建立的连接;首次 I/O 按需建连是现状,不是 V3.1 启动建连目标。</div></section>
<section id="lease"><h2>04 作用域租约:一个 Step 内多次操作</h2><ol><li><code>AutoDeviceFacade.AcquireAsync(deviceId, stepName)</code> 预约资源并返回 <code>IAutoDeviceLease</code>。</li><li>调用 <code>CanBusService.SendFrameAsync(lease,...)</code>、<code>ReceiveFrameAsync(lease)</code> 等重载。</li><li>CanBusService 委托 Lease.Execute/Query;AutoDeviceLease 进入 DeviceService,每次 I/O 再取得 Operation Lease。</li><li>步骤结束 Dispose Lease,释放外层预约;不会等价调用 Driver.Disconnect。</li></ol><div class="note">外层 Reservation 表示步骤当前获准使用设备;Operation Lease 表示具体命令/响应片段正在执行。共享物理会话安全串行还依赖资源管理器/SessionGate 的实际锁范围。</div></section>
<section id="power"><h2>05 电源 Slot 调用:逻辑位置映射到设备</h2><ol><li><code>PowerSupplyService.ConfigureAsync(slotId, command, owner, ct)</code> 接收 Slot、目标值和 Owner。</li><li><code>SlotDeviceMappingResolver.ResolveEndpoint(slotId, PowerSupply)</code> 解析设备、通道和资源键。</li><li>PowerSupplyService 验证资源授权并取得 Operation Lease。</li><li>从 <code>DeviceManager.GetDriver(endpoint.DeviceId)</code> 取 Driver,发 SCPI 设置并查询读回。</li><li>返回 <code>PowerOperationResult</code>:实测值、原始响应、错误码、耗时。</li></ol><div class="note warn">CAN Facade 当前申请 <code>deviceId:Generic</code>;电源服务按 Slot 映射资源键授权,可能细化到通道。Step 不应自行猜测 Owner/ResourceKey;需统一 typed API 或通道级 lease 合约。</div></section>
<section id="broker"><h2>06 Broker 调用链:客户端代理不持有实物</h2><div class="cols"><div class="card"><h3>客户端侧</h3><p>Bootstrapper 为 Broker 配置创建设备代理 Driver,注入 BrokerRpcTransport;业务层仍调用同一类型化 API。</p></div><div class="card"><h3>Broker 宿主</h3><p>BrokerServer 持有真实配置和真实 Driver,在设备 gate 中执行请求。物理 socket/串口/SDK 会话只归唯一 I/O 宿主。</p></div></div><div class="call"><div class="num">1</div><div><strong>BrokerRpcTransport</strong><p>把 SEND/QUERY 编成操作码和 payload,携带 DeviceId、client instance、配置 Hash。</p></div></div><div class="call"><div class="num">2</div><div><strong>BrokerClientChannel</strong><p>建立/复用命名管道,Hello 握手,用 CorrelationId 关联请求响应并设置等待期限。</p></div></div><div class="call"><div class="num">3</div><div><strong>BrokerServer</strong><p>处理 CONNECT/SEND/QUERY/STATUS;按需创建真实 Driver、连接并执行。</p></div></div><div class="call"><div class="num">4</div><div><strong>真实 Driver → 物理 Transport → 仪器</strong><p>Broker 返回状态与读回,代理清理传输封装后交回服务层。</p></div></div><div class="note danger">Broker 不可达时,TestStep 不可自行切换到 Local 直连同一仪器,否则可能形成同一个 PhysicalSessionKey 的双主控制。写命令已提交但超时,应视为 Unknown,先读回/恢复,不盲目重发。</div></section>
<section id="startup"><h2>07 启动与连接生命周期:接口存在不代表自动执行</h2><table><thead><tr><th>环节</th><th>当前源码事实</th><th>V3.1 目标 / 差距</th></tr></thead><tbody>
<tr><td>基础设施装配</td><td>Bootstrapper 创建 ResourceManager、DeviceManager、DeviceService、PowerSupplyService、AutoFacade、CanBusService。</td><td>已搜索到的 Bootstrapper 调用点在 Plugin.IntegrationTest;未据此确认生产主程序启动已接入。</td></tr>
<tr><td>Local 注册</td><td><code>LoadDevicesFromConfig()</code> 读取映射但创建本地 Driver 仍为 TODO。</td><td>按生效配置读取驱动定义、核对唯一宿主后注册。</td></tr>
<tr><td>Driver 注册</td><td>DeviceManager.RegisterDriver 写字典/初始状态/事件订阅,不执行 Connect。</td><td>注册后由宿主连接本宿主设备并检查 Ready。</td></tr>
<tr><td>CAN 连接</td><td>CanDeviceDriver 首次 Send/Query 时 EnsureConnectedAsync。</td><td>HostReady 前建连并健康检查,不让首次测试动作承担连接。</td></tr>
<tr><td>Broker 连接</td><td>客户端注册代理;BrokerServer 在 CONNECT 或首次操作时可创建/连接真实 Driver。</td><td>真实物理连接留在 Broker 宿主,代理只做 IPC。</td></tr>
<tr><td>SCPI 连接</td><td>当前 ScpiDriverAdapter.ConnectAsync 会发送 <code>*RST</code>。</td><td>自动连接前拆出 Reset;Connect 只建链并进行无副作用探测。</td></tr>
<tr><td>步骤结束</td><td>Dispose 释放资源预约,不等于 DeviceManager.Disconnect。</td><td>连接由宿主持有,步骤结束不断开。</td></tr>
<tr><td>关闭/重启</td><td>DeviceManager 有单设备 Connect/Disconnect;未见完整统一生命周期协调。</td><td>关闭需排空、安全处理、读回后断开;重启进入恢复门禁,再发布 HostReady。</td></tr></tbody></table><div class="note">建议 DeviceManager 管理驱动实例和单设备连接;宿主级 DeviceService 或独立连接协调器负责注册后批量连接、汇总 Connected/Ready、失败隔离、有序关闭。不要在每个 Run() 中 Connect/Disconnect。</div></section>
<section id="result"><h2>08 结果与错误边界</h2><table><thead><tr><th>层</th><th>返回值</th><th>判定边界</th></tr></thead><tbody><tr><td>Driver</td><td>原始响应、状态、异常/连接事件。</td><td>不是 DUT Pass/Fail。</td></tr><tr><td>DeviceService</td><td><code>DeviceCommandResult</code>:成功、读回、错误码/信息、延迟。</td><td>命令交换成功不一定等于工艺合格。</td></tr><tr><td>CanBusService</td><td>发送 bool、接收 CanFrameData。</td><td>生产 Step 应保留可诊断错误,避免只有 bool。</td></tr><tr><td>PowerSupplyService</td><td>PowerOperationResult:实测值、原始响应、错误、延迟。</td><td>由业务步骤按限值判定。</td></tr><tr><td>TestStep</td><td><span class="tag target">目标</span>记录输入/读数/单位/限值/耗时并设 Verdict。</td><td>设备故障通常为执行 Error;测量超限才是工艺 Fail;结果 Unknown 要恢复,不盲目重发。</td></tr></tbody></table></section>
<section id="gaps"><h2>09 建议优先完善的接口边界</h2><ol><li>给 TestStep 提供稳定 client API 与 DTO;如 OpenTAP Engine 与 Aging.Infrastructure 框架不兼容,用版本化 IPC。</li><li>把 Bootstrapper 接入生产组合根,加载完整配置、驱动定义及 Local/Broker 归属。</li><li>实现注册后连接协调、健康检查、HostReady 门禁、有限重试和故障隔离。</li><li>统一 CAN、电源等服务的 Owner、Slot、Channel、ResourceKey 申请方式。</li><li>把 SCPI <code>*RST</code>、输出设置、安全动作从 Connect 拆为显式受控操作。</li><li>确认共享 PhysicalSession 上的操作锁覆盖完整 I/O 片段,并定义超时/Unknown 语义。</li></ol><div class="note danger">证据边界:本文为源码静态阅读和 V3.1 设计对照,没有运行仪器、模拟器或整站应用。自动连接、HostReady 和恢复门禁均标为目标,不是已验收事实。</div></section>
<section id="sources"><h2>10 源码定位</h2><table><thead><tr><th>类型</th><th>文件</th><th>查看内容</th></tr></thead><tbody>
<tr><td>启动/注册</td><td class="path">D:\TestPlatform\TapProject\Aging.Infrastructure\InfrastructureBootstrapper.cs</td><td>Initialize、LoadDevicesFromConfig、RegisterBrokeredDrivers、RegisterDevice</td></tr>
<tr><td>门面/租约</td><td class="path">D:\TestPlatform\TapProject\Aging.Infrastructure\Services\AutoDeviceFacade.cs</td><td>Execute/Query、Acquire、AutoDeviceLease.Dispose</td></tr>
<tr><td>CAN API</td><td class="path">D:\TestPlatform\TapProject\Aging.Infrastructure\Services\CanBusService.cs</td><td>类型化调用、命令编码、租约重载</td></tr>
<tr><td>执行/管理</td><td class="path">D:\TestPlatform\TapProject\Aging.Infrastructure\Services\DeviceService.cs<br>D:\TestPlatform\TapProject\Aging.Infrastructure\Core\DeviceManager.cs</td><td>权限验证、操作租约、Driver 注册与连接状态</td></tr>
<tr><td>电源</td><td class="path">D:\TestPlatform\TapProject\Aging.Infrastructure\Services\PowerSupplyService.cs<br>D:\TestPlatform\TapProject\Aging.Infrastructure\Services\SlotDeviceMappingResolver.cs</td><td>Slot 映射和电源动作</td></tr>
<tr><td>驱动/传输</td><td class="path">D:\TestPlatform\TapProject\Aging.Infrastructure\Drivers\DriverFactory.cs<br>D:\TestPlatform\TapProject\Aging.Infrastructure\Drivers\CanDeviceDriver.cs<br>D:\TestPlatform\TapProject\Aging.Infrastructure\Drivers\BrokerRpcTransport.cs<br>D:\TestPlatform\TapProject\Aging.Infrastructure\Broker\BrokerClientChannel.cs</td><td>Driver 创建、按需建连、Broker RPC</td></tr>
<tr><td>Broker 宿主</td><td class="path">D:\TestPlatform\TapProject\DeviceBroker\BrokerServer.cs</td><td>真实 Driver 创建、连接和 I/O</td></tr>
<tr><td>当前自动化示例</td><td class="path">D:\TestPlatform\TapProject\Plugin.IntegrationTest\MainWindow.xaml.cs</td><td>InitializeAutoServices、RegisterCanDriver、AutoRunBtn_Click</td></tr>
<tr><td>目标设计</td><td class="path">D:\06-产品开发\18-舱驾控制器老化设备\Local + DeviceBroker 混合架构详细设计说明书 V3.1.html</td><td>第 23 章:启动连接、HostReady、恢复、关闭与 SCPI</td></tr></tbody></table></section>
<footer style="text-align:center;color:#657987;font-size:12px;padding:17px">仪器调用程序结构与类调用说明 · 本地源码静态梳理 · 2026-11-09</footer></main></div>
<script>const f=document.getElementById('filter'),ss=[...document.querySelectorAll('main section')],c=document.getElementById('count');function go(){let q=f.value.trim().toLowerCase(),n=0;for(const s of ss){let ok=!q||s.innerText.toLowerCase().includes(q);s.hidden=!ok;if(ok)n++}c.textContent=n+' / '+ss.length+' 个章节'}f.addEventListener('input',go);document.getElementById('reset').onclick=()=>{f.value='';go();f.focus()};</script></body></html>