<!DOCTYPE html>
|
<html lang="zh-CN">
|
<head>
|
<meta charset="UTF-8">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<title>OpenTAP Instrument + 插件包机制分析报告</title>
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
<style>
|
* { box-sizing: border-box; }
|
body {
|
font-family: -apple-system, "Segoe UI", "Microsoft YaHei", "PingFang SC", sans-serif;
|
max-width: 1240px;
|
margin: 0 auto;
|
padding: 32px 24px 80px;
|
background: #FAFAFA;
|
color: #212121;
|
line-height: 1.75;
|
}
|
h1 { font-size: 30px; color: #1565C0; border-bottom: 3px solid #1976D2; padding-bottom: 12px; }
|
h2 { font-size: 23px; color: #0D47A1; margin-top: 42px; padding-left: 12px; border-left: 4px solid #FF9800; }
|
h3 { font-size: 17px; color: #37474F; margin-top: 24px; }
|
.subtitle { color: #757575; font-size: 14px; margin-bottom: 24px; }
|
.card { background: white; border-radius: 8px; padding: 18px 22px; margin: 16px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
|
.highlight { background: #FFF8E1; border-left: 4px solid #FFC107; padding: 12px 16px; margin: 12px 0; border-radius: 4px; }
|
.info { background: #E3F2FD; border-left: 4px solid #2196F3; padding: 12px 16px; margin: 12px 0; border-radius: 4px; }
|
.success { background: #E8F5E9; border-left: 4px solid #4CAF50; padding: 12px 16px; margin: 12px 0; border-radius: 4px; }
|
.warn { background: #FBE9E7; border-left: 4px solid #FF5722; padding: 12px 16px; margin: 12px 0; border-radius: 4px; }
|
table { width: 100%; border-collapse: collapse; margin: 16px 0; background: white; border-radius: 6px; overflow: hidden; box-shadow: 0 1px 4px rgba(0,0,0,0.06); font-size: 14px; }
|
th { background: #1976D2; color: white; padding: 10px 14px; text-align: left; font-weight: 600; }
|
td { padding: 9px 14px; border-bottom: 1px solid #EEEEEE; vertical-align: top; }
|
tr:nth-child(even) td { background: #FAFAFA; }
|
tr:hover td { background: #E3F2FD; }
|
code { font-family: "Consolas", "Monaco", monospace; background: #F5F5F5; color: #C62828; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
pre { background: #263238; color: #ECEFF1; padding: 16px 20px; border-radius: 6px; overflow-x: auto; font-family: "Consolas", monospace; font-size: 13px; line-height: 1.55; }
|
pre code { background: transparent; color: inherit; padding: 0; font-size: 13px; }
|
.mermaid { background: white; padding: 20px; border-radius: 8px; margin: 20px 0; text-align: center; box-shadow: 0 2px 8px rgba(0,0,0,0.06); overflow-x: auto; }
|
ul { padding-left: 22px; }
|
li { margin: 4px 0; }
|
.toc { background: #F5F5F5; padding: 16px 24px; border-radius: 6px; margin: 20px 0; }
|
.toc a { color: #1976D2; text-decoration: none; display: block; padding: 3px 0; }
|
.toc a:hover { text-decoration: underline; }
|
.kwd { color: #FFCC80; } .str { color: #C5E1A5; . } .cmt { color: #546E7A; }
|
footer { margin-top: 60px; padding-top: 20px; border-top: 1px solid #E0E0E0; color: #757575; font-size: 12px; text-align: center; }
|
.tag { display:inline-block; background:#E1F5FE; color:#0277BD; border-radius:12px; padding:1px 10px; font-size:12px; margin:2px; }
|
</style>
|
</head>
|
<body>
|
|
<h1>OpenTAP Instrument + 插件包机制分析报告</h1>
|
<div class="subtitle">针对 TestPlatform Aging Software(.NET Framework 4.7.2 + OpenTAP 9.31.0)驱动热插拔改造的技术调研 · 2026-11-03</div>
|
|
<div class="toc">
|
<strong>目录</strong>
|
<a href="#s1">1. OpenTAP 插件体系总览</a>
|
<a href="#s2">2. Instrument 抽象深入解析</a>
|
<a href="#s3">3. 测试步骤如何引用仪器</a>
|
<a href="#s4">4. 包(Package)机制全景</a>
|
<a href="#s5">5. package.xml 清单文件详解</a>
|
<a href="#s6">6. 依赖解析与版本模型</a>
|
<a href="#s7">7. 驱动插件开发-分发-安装工作流</a>
|
<a href="#s8">8. Image API:安装目录状态管理</a>
|
<a href="#s9">9. 与 TapProject 现有架构的映射与差距</a>
|
<a href="#s10">10. 落地路线建议与成本评估</a>
|
<a href="#s11">11. 结论</a>
|
</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s1">1. OpenTAP 插件体系总览</h2>
|
|
<div class="card">
|
OpenTAP 的核心哲学是 <strong>“一切皆插件(Everything is a Plugin)”</strong>。测试步骤、仪器驱动、被测件(DUT)、结果监听器、甚至 CLI 命令本身,都是继承自某个抽象基类的 C# 类。引擎启动时通过 <code>PluginSearcher</code> 扫描安装目录下所有程序集,构建插件类型图谱,供序列化器与编辑器(Editor)动态发现使用。
|
</div>
|
|
<div class="mermaid">
|
graph TB
|
subgraph 扫描["启动时插件发现 (PluginSearcher)"]
|
A["扫描安装根目录 *.dll"] --> B["扫描 Packages/** 子目录"]
|
B --> C["反射提取继承自插件基类的类型"]
|
C --> D["构建类型缓存<br/>PluginManager 可查询"]
|
end
|
subgraph 基类["插件基类(OpenTap.dll / 插件包)"]
|
T1["TestStep"]
|
T2["Instrument : Resource"]
|
T3["Dut : Resource"]
|
T4["ResultListener"]
|
T5["SerializerPlugin"]
|
T6["ICliAction (tap 子命令)"]
|
end
|
D --> T1 & T2 & T3 & T4 & T5 & T6
|
</div>
|
|
<h3>关键组件</h3>
|
<table>
|
<tr><th>组件</th><th>所在 DLL</th><th>职责</th></tr>
|
<tr><td>Engine(序列化/序列执行)</td><td><code>OpenTap.dll</code></td><td>TestPlan 执行、插件对象生命周期、XML 序列化</td></tr>
|
<tr><td>PluginSearcher</td><td><code>OpenTap.dll</code>(内置 Mono.Cecil)</td><td>不加载 CLR 类型即可解析程序集的插件元数据(快、支持延迟加载)</td></tr>
|
<tr><td>PackageManager / Image</td><td><code>OpenTap.Package.dll</code></td><td>包安装、依赖解析、下载、卸载</td></tr>
|
<tr><td>CLI</td><td><code>tap.dll</code> / <code>tap.exe</code></td><td><code>tap package install / run / plan</code> 等命令入口</td></tr>
|
<tr><td>基础步骤包</td><td><code>OpenTap.Plugins.BasicSteps.dll</code></td><td>Delay、Loop、If、Scpi 仪器控制等通用 TestStep</td></tr>
|
</table>
|
|
<div class="info">
|
<strong>安装目录布局(实测官方 OpenTAP 包):</strong>
|
核心引擎 DLL(<code>tap.dll</code>、<code>OpenTap.dll</code>、<code>OpenTap.Package.dll</code>)放在根目录;
|
<strong>每个第三方包的文件被隔离到自己的子目录</strong> <code>Packages/<包名>/</code>(如 <code>Packages/OpenTAP/OpenTap.Plugins.BasicSteps.dll</code>),卸载包时按清单删除这些文件,天然避免文件冲突。
|
</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s2">2. Instrument 抽象深入解析</h2>
|
|
<h3>类层次</h3>
|
<div class="mermaid">
|
classDiagram
|
class IResource {
|
<<interface>>
|
+string Name
|
+bool Enabled
|
+Open()
|
+Close()
|
}
|
class Resource {
|
<<abstract>>
|
+string Name
|
+virtual bool Enabled
|
+virtual Open()
|
+virtual Close()
|
}
|
class IInstrument { <<interface>> }
|
class Instrument {
|
<<abstract>>
|
Instrument() 默认Name= INST
|
}
|
class ScpiInstrument {
|
+IdnQuery
|
+Query/Write
|
+Connection 类型
|
+Open/Close 实现
|
}
|
class Dut {
|
<<abstract>>
|
被测件模型
|
}
|
IResource <|-- Resource
|
IResource <.. IInstrument
|
IInstrument <|.. Instrument
|
Resource <|-- Instrument
|
Instrument <|-- ScpiInstrument
|
Resource <|-- Dut
|
</code></pre>
|
</div>
|
|
<ul>
|
<li><strong><code>Resource</code>(抽象基类)</strong>:统一"台架资源"模型 —— 名称唯一、可启用/禁用、<code>Open()</code>/<code>Close()</code> 生命周期。Instrument 与 DUT 都继承它。</li>
|
<li><strong><code>Instrument : Resource, IInstrument</code></strong>:源码仅 ~20 行(验证自 GitHub opentap/opentap Engine/Instrument.cs),构造函数默认 <code>Name="INST"</code>。派生类重写 <code>Open/Close</code> 完成硬件连接。</li>
|
<li><strong><code>ScpiInstrument</code></strong>(SCPI 插件包提供):最常用的仪器基类,封装 SCPI 协议栈:
|
<ul>
|
<li>IDN 查询(*IDN? 自动识别厂商/型号/固件)</li>
|
<li>多传输层:VISA(Keysight/NI)、Raw TCP/Socket、Serial、USB-TMC、GPIB、HiSLIP</li>
|
<li>分层协议模型(Layer):可在 Message/Resource 层插入日志、超时、重试策略</li>
|
</ul>
|
</li>
|
<li><strong>连接配置持久化</strong>:9.23+ 引入 <code>BenchStore</code>/<code>ResourceSettings</code> 体系,将仪器的地址、超时、别名从测试序列中剥离成台架级配置(对应旧版 <code>InstrumentSettings.xml</code> 文件)。</li>
|
</ul>
|
|
<div class="highlight">
|
<strong>与你们 <code>IDeviceDriver</code> 的关键差异:</strong>OpenTAP 的 Instrument 对象同时承担"驱动 + 连接配置 + 生命周期"三种角色,且由序列化器在反序列化 TestPlan 时<strong>自动按类型名从插件图谱创建实例</strong>。你们的 <code>CanDriverAdapter(deviceId, busName, nodeIndex)</code> 则是工厂显式构造,配置来自 SQLite 的 <code>DeviceDefinition</code> 表。
|
</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s3">3. 测试步骤如何引用仪器</h2>
|
|
<pre><code><span class="cmt">// OpenTAP 风格的 TestStep 引用仪器(编辑器自动渲染为下拉框)</span>
|
<span class="kwd">public class</span> PowerOnStep : TestStep
|
{
|
<span class="cmt">// 任何暴露 Instrument 属性的 TestStep,编辑器显示可选仪器列表;</span>
|
<span class="cmt">// 列表来源 = PluginSearcher 扫描出的所有非抽象 Instrument 子类实例(来自 settings store)</span>
|
<span class="kwd">public</span> ScpiInstrument PowerSupply { <span class="kwd">get</span>; <span class="kwd">set</span>; }
|
<span class="kwd">public double</span> Voltage { <span class="kwd">get</span>; <span class="kwd">set</span>; } = 12.0;
|
|
<span class="kwd">public override void</span> Run()
|
{
|
<span class="kwd">using</span> <span class="kwd">var</span> _ = SynchronizationManager.RequestLock(PowerSupply, TimeSpan.FromSeconds(10));
|
PowerSupply.Write($"VOLT {Voltage:F2}");
|
PowerSupply.Write("OUTP ON");
|
<span class="kwd">var</span> readback = PowerSupply.QueryDouble("MEAS:VOLT?");
|
<span class="kwd">if</span> (Math.Abs(readback - Voltage) > 0.5)
|
Upwards.Find(Voltage, readback).Fail("电压偏差过大");
|
}
|
}</code></pre>
|
|
<table>
|
<tr><th>机制</th><th>说明</th></tr>
|
<tr><td>属性类型即选择器</td><td><code>public Instrument X</code> 属性在编辑器/CLI 中渲染成仪器下拉框;[ResourceProperty] 等 Attribute 可限制候选类型(只列 CAN / 只列电源)</td></tr>
|
<tr><td>自动 Open/Close</td><td>TestPlanRun 开始时引擎对所有 Enabled 的 Instrument 调 <code>Open()</code>(懒打开),结束后 <code>Close()</code>;步骤代码不关心连接细节</td></tr>
|
<tr><td>并发保护</td><td><code>SynchronizationManager.RequestLock(resource, timeout)</code> 提供进程内对同一仪器的互斥锁;跨进程互斥 OpenTAP <strong>不提供</strong>,需外部仲裁(见 §9)</td></tr>
|
<tr><td>序列化</td><td>TestPlan XML 里保存的是仪器 <code>Name</code> 引用(非实例),运行时按名字解析到 settings store 中配置的实例</td></tr>
|
</table>
|
|
<!-- ============================================================ -->
|
<h2 id="s4">4. 包(Package)机制全景</h2>
|
|
<div class="card">
|
<strong>包文件本质:</strong><code>.opentap</code> 文件就是一个 <strong>ZIP 压缩包</strong>,根目录含 <code>package.xml</code> 清单,其余为载荷文件(DLL、配置、文档等)。清单声明了包名、语义化版本、平台约束、依赖关系和文件部署布局。
|
</div>
|
|
<div class="mermaid">
|
graph LR
|
subgraph 包文件
|
Z[".opentap (ZIP)"] --> PX["package.xml 清单"]
|
Z --> PL["载荷文件"]
|
end
|
subgraph 仓库
|
REPO["packages.opentap.io<br/>(官方 NuGet 风格仓库)"]
|
PRIV["私有仓库<br/>tap repo add"]
|
end
|
PLAT["Developer's Platform<br/>(桌面版包浏览器/CI)"] --> REPO
|
REPO -->|tap package install| IMG["Image = 安装目录<br/>状态 + 已装包清单"]
|
PRIV -->|同协议| IMG
|
PX --> IMG
|
IMG -->|部署到| FILES["根目录 + Packages/<name>/<br/>+ Dependencies/"]
|
IMG -->|卸载| DEL["按清单删除文件<br/>(不影响其他包)"]
|
</div>
|
|
<h3>核心 CLI 命令</h3>
|
<pre><code><span class="cmt"># 列出可安装 / 已安装</span>
|
tap package list
|
tap package list -i
|
|
<span class="cmt"># 安装(可指定仓库、版本、通道)</span>
|
tap package install "Developer's System CE" -y
|
tap package install MyCanDriver -v 2.1.0 -c candidate
|
tap package install Keysight.34461A -s https://packages.opentap.io
|
|
<span class="cmt"># 从源码工程构建包</span>
|
tap package create package.xml
|
|
<span class="cmt"># 管理私有仓库源</span>
|
tap repo add http://192.168.1.50/opentap-repo
|
tap repo list
|
|
<span class="cmt"># 修复 / 序列化 / 会话</span>
|
tap package recover
|
tap package serialize-package / deserialize-package</code></pre>
|
|
<table>
|
<tr><th>特性</th><th>机制</th></tr>
|
<tr><td>包隔离安装</td><td>包文件部署到 <code>Packages/<包名>/</code>,同名 DLL 不同版本可共存(引擎按包清单加载)</td></tr>
|
<tr><td>平台/架构区分</td><td><code>OS="Windows|Linux|MacOS"</code>、<code>Architecture="x64|x86|arm"</code>,清单内可按条件包含不同原生依赖(官方包用 <code>$(Architecture)</code> 变量切分 libgit2 的 .dll/.so/.dylib)</td></tr>
|
<tr><td>安装钩子</td><td><code>PackageActionExtensions</code> 可在 pre/post install 执行外部命令(官方包用 <code>chmod +x tap</code> 修 Linux 权限)</td></tr>
|
<tr><td>通道(Channels)</td><td>release / candidate / alpha,同一包可按不同通道发布;CI 场景锁定通道即可控制更新节奏</td></tr>
|
<tr><td>自动更新</td><td><code>tap package install --force</code> 或桌面版包浏览器;版本比较使用严格 SemVer 2.0</td></tr>
|
</table>
|
|
<!-- ============================================================ -->
|
<h2 id="s5">5. package.xml 清单文件详解</h2>
|
|
<p>以下骨架提炼自 <strong>官方 OpenTAP 包的真实 package.xml</strong>(GitHub opentap/opentap@main):</p>
|
|
<pre><code><?xml version="1.0" encoding="UTF-8"?>
|
<Package Name="MyCompany.CanDriver"
|
xmlns="http://opentap.io/schemas/package"
|
Version="$(GitVersion)" <span class="cmt"><!-- MSBuild 风格变量注入 --></span>
|
OS="$(Platform)" Architecture="$(Architecture)">
|
|
<Description>CAN 总线仪器驱动插件(PCAN/虚拟总线)</Description>
|
<Status>Released</Status>
|
<Organisation>我司</Organisation>
|
<SourceUrl>https://git.internal/CanDriver</SourceUrl>
|
<SourceLicense>Proprietary</SourceLicense>
|
<Owner>MyCompany</Owner>
|
|
<span class="cmt"><!-- ① 变量与条件(MSBuild 表达式)--></span>
|
<Variables>
|
<ArchitectureCondition Condition="$(Architecture) == ''">x64</Architecture>
|
<PlatformCondition Condition="$(Platform) == ''">Windows</Platform>
|
</Variables>
|
|
<span class="cmt"><!-- ② 依赖:指向其他"包"(版本范围/必需/推荐)--></span>
|
<Dependencies>
|
<PackageDependency Name="OpenTAP" Version="9.31.0" Action="Required"/>
|
<PackageDependency Name="Keysight.SCPI" Version="9.x" Action="Optional"/>
|
</Dependencies>
|
|
<span class="cmt"><!-- ③ 文件:安装路径 / 源路径 / 元数据操作 --></span>
|
<Files>
|
<span class="cmt"><!-- 插件 DLL 放进包隔离目录 --></span>
|
<File Path="Packages/MyCompany.CanDriver/CanDriverPlugin.dll">
|
<SetAssemblyInfo Attributes="Version"/> <span class="cmt"><!-- 自动写入程序集版本号 --></span>
|
</File>
|
<File Path="Packages/MyCompany.CanDriver/peak_pcans.dll" SourcePath="native/pcans.dll"/>
|
<File Path="Docs/**" SourcePath="doc/"/> <span class="cmt"><!-- glob --></span>
|
</Files>
|
|
<span class="cmt"><!-- ④ 平台差异化 --></span>
|
<Files Condition="$(Platform) == Linux">
|
<File Path="Packages/MyCompany.CanDriver/libpcans.so"/>
|
</Files>
|
</Package></code></pre>
|
|
<table>
|
<tr><th>元素</th><th>作用</th></tr>
|
<tr><td><code>Package</code> 根属性</td><td>Name/Version/OS/Architecture,支持 <code>$(变量)</code> + <code>Condition</code></td></tr>
|
<tr><td><code>Dependencies</code> / <code>PackageDependency</code></td><td>声明对其他包的最小版本要求;Action = Required / Recommended / Optional</td></tr>
|
<tr><td><code>PluginDependencies</code></td><td>声明运行时 .NET 程序集依赖(非包级),用于加载顺序与冲突提示</td></tr>
|
<tr><td><code>Files</code> / <code>File</code></td><td>部署布局:<code>Path</code>(目标)+ <code>SourcePath</code>(源),可带 <code>Action="Install|WriteCode"</code>、<code>SetAssemblyInfo</code>(写版本/公司元数据)、<code>SignCertificate</code>( Authenticode 签名)</td></tr>
|
<tr><td><code>PackageActionExtensions</code></td><td>安装前后执行外部可执行/脚本(如权限修复、驱动安装器)</td></tr>
|
<tr><td><code>Variables/Condition</code></td><td>一份清单跨平台复用,构建时按变量裁剪文件集</td></tr>
|
<tr><td><code>TypeTags</code></td><td>给插件类打标签,编辑器可按 tag 过滤(如 "PowerSupply" tag 只显示电源)</td></tr>
|
</table>
|
|
<!-- ============================================================ -->
|
<h2 id="s6">6. 依赖解析与版本模型</h2>
|
|
<ul>
|
<li><strong>SemVer 2.0</strong>:包版本严格遵循 <code>MAJOR.MINOR.PATCH</code> + 预发布标识(<code>-alpha.1</code>)。</li>
|
<li><strong>解析器</strong>:<code>OpenTap.Package</code> 内置求解器,处理 <code>安装 A → A 依赖 B ≥2.0 → 已装 B 1.3 → 生成"升级 B"计划</code>;多包交叉依赖用回溯搜索求最小变更集。</li>
|
<li><strong>降级保护</strong>:<code>install</code> 默认拒绝降级已安装包,除非 <code>--force</code>。</li>
|
<li><strong>冲突处理</strong>:同一文件被两个包声明 → 后者安装报错提示 ownership 冲突;通过 <code>Packages/<name>/</code> 目录约定基本可避免。</li>
|
<li><strong>OpenTAP 内核版本兼容</strong>:所有包都依赖 <code>OpenTAP</code> 包本身;内核升级后旧包按 <code><PackageDependency Version="9.31.0"></code> 的最小版本匹配保持兼容,9.x 保持 ABI 承诺。</li>
|
</ul>
|
|
<!-- ============================================================ -->
|
<h2 id="s7">7. 驱动插件开发-分发-安装工作流</h2>
|
|
<div class="mermaid">
|
sequenceDiagram
|
participant Dev as 驱动开发者
|
participant VS as VS / dotnet CLI
|
participant Tap as tap CLI
|
participant Repo as 包仓库
|
participant User as 现场工程师
|
|
Dev->>VS: NuGet 引用 OpenTap / OpenTap.Package SDK
|
Dev->>VS: 编写 `class MyCanInstrument : Instrument`
|
VS->>VS: dotnet build → 生成 DLL
|
Dev->>Tap: tap package create package.xml
|
Tap->>Tap: 校验清单 + ZIP 打包 → MyCan.opentap
|
Dev->>Repo: 上传到私有仓库 (HTTP 服务器/文件共享)
|
User->>Tap: tap repo add http://内网仓库
|
User->>Tap: tap package install MyCan -y
|
Tap->>Repo: 解析依赖 → 下载 .opentap
|
Tap->>Tap: 按清单部署到 Packages/MyCan/
|
Note over User: 重启编辑器/CLI,新仪器自动出现在下拉列表
|
</div>
|
|
<h3>开发环境依赖</h3>
|
<ul>
|
<li>NuGet 包:<code>OpenTap</code>、<code>OpenTap.Package</code>(提供 SDK 引用,编译期)</li>
|
<li>VS 扩展:<em>OpenTAP Project Templates</em>(含 Instrument 插件项目模板)</li>
|
<li>调试方式:把编译产物 DLL 直接拷到安装目录 <code>Packages/xxx/</code>,启动 <code>tap ui</code>(桌面版)或你们自己的编辑器宿主调试</li>
|
<li>注意:<strong>插件 SDK 是 netstandard2.0/net472 双目标</strong>,与你们 .NET Framework 4.7.2 主程序兼容</li>
|
</ul>
|
|
<!-- ============================================================ -->
|
<h2 id="s8">8. Image API:安装目录状态管理</h2>
|
|
<p>OpenTAP 9.15+ 引入 <code>OpenTap.Package.Image</code> 概念,把"安装目录当前状态"抽象成对象:</p>
|
|
<pre><code><span class="cmt">// 程序化管理一个 OpenTAP 安装目录(CLI 底层就是它)</span>
|
<span class="kwd">var</span> image = <span class="kwd">new</span> Image(<span class="str">"C:/Program Files/OpenTAP"</span>);
|
image.InstallPackage(<span class="str">"MyCompany.CanDriver"</span>, <span class="str">"latest"</span>, <span class="str">"release"</span>);
|
image.QueryAvailable(<span class="str">"CAN"</span>); <span class="cmt">// 列出候选包</span>
|
image.UninstallPackage(<span class="str">"OldDriver"</span>); <span class="cmt">// 按清单精确删除</span></code></pre>
|
|
<div class="info">对你们的价值:未来如果做"HMI 内嵌驱动市场",可以直接调 Image API 实现<strong>界面里点几下完成驱动升级</strong>,无需运维介入。</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s9">9. 与 TapProject 现有架构的映射与差距</h2>
|
|
<h3>9.1 概念对照表</h3>
|
<table>
|
<tr><th>TapProject 现有</th><th>OpenTAP 对应物</th><th>匹配度</th></tr>
|
<tr><td>IDeviceDriver + DriverFactory switch</td><td>Instrument 子类 + PluginSearcher 反射发现</td><td>✅ 直接替换</td></tr>
|
<tr><td>DeviceConfig(SQLite DeviceDefinition)</td><td>BenchStore / ResourceSettings(XML settings store)</td><td>⚠️ 需要双向同步适配器</td></tr>
|
<tr><td>IResourceManager 预约 + ResourceOwnerToken 租约</td><td>SynchronizationManager(仅进程内锁)</td><td>❌ OpenTAP 没有跨进程/跨会话仲裁,<strong>必须保留你们的</strong></td></tr>
|
<tr><td>IAutoDeviceFacade + IAutoDeviceLease(Per-Step)</td><td>TestStep 属性绑定 Instrument + RequestLock</td><td>✅ 语义等价,租约更细</td></tr>
|
<tr><td>HMI 插件 PluginLoader(WPF UI)</td><td>无对应(OpenTAP 不管 UI)</td><td>— 保持独立</td></tr>
|
<tr><td>驱动分发=拷 DLL+重编译</td><td>.opentap 包 + tap package install + 私有仓库</td><td>✅ 本次改造核心收益</td></tr>
|
</table>
|
|
<h3>9.2 桥接设计:双轨并行</h3>
|
<div class="mermaid">
|
graph TB
|
subgraph 统一层["Aging.Infrastructure(保留)"]
|
F["IAutoDeviceFacade"]
|
L["IAutoDeviceLease"]
|
RM["IResourceManager(跨会话仲裁)"]
|
end
|
subgraph 适配层["新增:Driver 适配"]
|
REG["DriverRegistry<br/>反射扫描 Drivers/*.dll<br/>(阶段一,自研轻量)"]
|
BRIDGE["TapInstrumentBridge<br/>OpenTAP Instrument ↔ IDeviceDriver<br/>(阶段二,可选)"]
|
end
|
subgraph 驱动生态["两种驱动形态并存"]
|
D1["自研驱动<br/>IDeviceDriverProvider<br/>→ 打 .opentap 包分发"]
|
D2["第三方 OpenTAP 仪器包<br/>Keysight/NationalInstruments<br/>→ tap package install 直接用"]
|
end
|
F --> RM
|
F --> REG
|
REG --> D1
|
BRIDGE --> D2
|
BRIDGE --> REG
|
</div>
|
|
<h3>9.3 三个关键差距与对策</h3>
|
<table>
|
<tr><th>差距</th><th>影响</th><th>对策</th></tr>
|
<tr><td>OpenTAP 无生产/维护互斥仲裁</td><td>全自动与手动调试可能撞车(正是你们做租约的动机)</td><td>保留 IResourceManager,在 Instrument.Open() 里调用 Reserve;Close() 里 Release</td></tr>
|
<tr><td>仪器配置双源(SQLite vs settings store)</td><td>注册页改端点、序列里用旧地址</td><td>以 DeviceConfig 为唯一真源,写一个导出器生成 <code>InstrumentSettings.xml</code>;或反向导入</td></tr>
|
<tr><td>你们的 HMI 宿主 ≠ OpenTAP Editor</td><td>仪器下拉框在 WPF 里不会自动出现</td><td>你们的 DeviceEditModal 从 DriverRegistry/PluginManager 枚举类型填充 ComboBox(已有 ForceRefreshForTest 模式,同理)</td></tr>
|
</table>
|
|
<!-- ============================================================ -->
|
<h2 id="s10">10. 落地路线建议与成本评估</h2>
|
|
<table>
|
<tr><th>阶段</th><th>做什么</th><th>工作量</th><th>收益</th></tr>
|
<tr>
|
<td><strong>阶段 0(已完成)</strong></td>
|
<td>AutoDeviceFacade + Lease + 类型化 CAN 服务</td>
|
<td>—</td>
|
<td>调用体验已解决</td>
|
</tr>
|
<tr>
|
<td><strong>阶段 1</strong></td>
|
<td>自研 <code>IDeviceDriverProvider</code> + <code>DriverRegistry</code> 反射扫描 <code>Drivers/*.dll</code>;<br>CAN 驱动拆成独立项目(丢 DLL 免编译主程序)</td>
|
<td>~2-3 人日</td>
|
<td>解决"加驱动必须重编译"的 80% 痛点;技术栈无引入</td>
|
</tr>
|
<tr>
|
<td><strong>阶段 2</strong></td>
|
<td>给每个驱动工程加 <code>package.xml</code>,用 <code>tap package create</code> 产出 <code>.opentap</code>;搭内网私有仓库(文件服务器即可)</td>
|
<td>~2 人日</td>
|
<td>版本化分发、依赖检查、现场 <code>tap package install</code> 一键装</td>
|
</tr>
|
<tr>
|
<td><strong>阶段 3(可选)</strong></td>
|
<td><code>TapInstrumentBridge</code>:新真硬件仪器直接写 <code>Instrument</code> 子类走 OpenTAP 生态;桥接回 IDeviceDriver 复用租约</td>
|
<td>~5-10 人日</td>
|
<td>可直接复用 Keysight NI-DMM、Pickering 开关等数百个现成官方包,<strong>省去自研 SCPI 驱动</strong></td>
|
</tr>
|
<tr>
|
<td><strong>阶段 4(远期)</strong></td>
|
<td>HMI 内嵌"驱动管理器"页面调 Image API 做界面级升级</td>
|
<td>~3 人日</td>
|
<td>运维自动化</td>
|
</tr>
|
</table>
|
|
<div class="warn">
|
<strong>风险提示:</strong>
|
<ul>
|
<li>阶段 1 反射加载若用 <code>Assembly.LoadFrom</code> 会锁文件导致无法热替换;建议 <code>AssemblyLoadContext</code>(.NET Core 风格,net472 无此 API → 退化为 <code>AppDomain</code> 隔离或接受"重启生效")。</li>
|
<li>OpenTAP 9.31 的 netstandard2.0 插件 SDK 在 net472 运行正常,但 <strong>不要假设</strong>其包管理器会管理 net472 主程序——它管理的是 OpenTAP 安装目录;你们主程序若要利用 .opentap,需要把引擎装成一个"嵌入式 OpenTAP 目录"。</li>
|
<li>私有包仓库无鉴权时注意内网隔离;官方仓库地址可 <code>tap repo remove</code> 禁用外网访问。</li>
|
</ul>
|
</div>
|
|
<!-- ============================================================ -->
|
<h2 id="s11">11. 结论</h2>
|
|
<div class="success">
|
<ol>
|
<li><strong>OpenTAP 的 Instrument 是"资源型插件"</strong>:继承 <code>Instrument : Resource</code> 即自动获得发现、序列化绑定、生命周期托管;SCPI 场景有开箱即用的 <code>ScpiInstrument</code>。</li>
|
<li><strong>插件包(.opentap)是分发单元而非引用单元</strong>:ZIP + package.xml,依赖解析用 SemVer,平台差异化靠条件变量;安装到 <code>Packages/<name>/</code> 隔离目录,卸载精确按清单。</li>
|
<li><strong>"免重编译加驱动"两条路都能走</strong>:轻量自研 DriverRegistry(阶段 1,立刻见效),或拥抱 OpenTAP 包生态(阶段 2/3,长期收益大,可白嫖厂商官方驱动)。</li>
|
<li><strong>你们的 IResourceManager 租约体系是 OpenTAP 缺失的能力</strong>,改造中应保留并前移到 Instrument.Open()/Close() 钩子,而不是被替代。</li>
|
<li><strong>推荐组合</strong>:阶段 1 先行(2-3 人日解决主痛点)→ 阶段 2 上私有仓库(再 2 人日)→ 按真实硬件需求决定是否进阶段 3。</li>
|
</ol>
|
</div>
|
|
<footer>
|
OpenTAP Instrument + 插件包机制分析 · TapProject 驱动热插拔调研 · 2026-11-03<br>
|
参考:opentap.io 文档、github.com/opentap/opentap(package.xml / Engine/Instrument.cs 源码实测)
|
</footer>
|
|
<script>
|
mermaid.initialize({
|
startOnLoad: true, theme: 'default', securityLevel: 'loose',
|
fontFamily: 'Segoe UI, Microsoft YaHei, sans-serif',
|
flowchart: { curve: 'basis', padding: 16 },
|
sequence: { showSequenceNumbers: false, actorMargin: 50 }
|
});
|
</script>
|
|
</body>
|
</html>
|