OpenTAP Instrument + 插件包机制分析报告
针对 TestPlatform Aging Software(.NET Framework 4.7.2 + OpenTAP 9.31.0)驱动热插拔改造的技术调研 · 2026-11-03
1. OpenTAP 插件体系总览
OpenTAP 的核心哲学是 “一切皆插件(Everything is a Plugin)” 。测试步骤、仪器驱动、被测件(DUT)、结果监听器、甚至 CLI 命令本身,都是继承自某个抽象基类的 C# 类。引擎启动时通过 PluginSearcher 扫描安装目录下所有程序集,构建插件类型图谱,供序列化器与编辑器(Editor)动态发现使用。
graph TB
subgraph 扫描["启动时插件发现 (PluginSearcher)"]
A["扫描安装根目录 *.dll"] --> B["扫描 Packages/** 子目录"]
B --> C["反射提取继承自插件基类的类型"]
C --> D["构建类型缓存 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
关键组件
组件 所在 DLL 职责
Engine(序列化/序列执行) OpenTap.dllTestPlan 执行、插件对象生命周期、XML 序列化
PluginSearcher OpenTap.dll(内置 Mono.Cecil)不加载 CLR 类型即可解析程序集的插件元数据(快、支持延迟加载)
PackageManager / Image OpenTap.Package.dll包安装、依赖解析、下载、卸载
CLI tap.dll / tap.exetap package install / run / plan 等命令入口
基础步骤包 OpenTap.Plugins.BasicSteps.dllDelay、Loop、If、Scpi 仪器控制等通用 TestStep
安装目录布局(实测官方 OpenTAP 包):
核心引擎 DLL(tap.dll、OpenTap.dll、OpenTap.Package.dll)放在根目录;
每个第三方包的文件被隔离到自己的子目录 Packages/<包名>/(如 Packages/OpenTAP/OpenTap.Plugins.BasicSteps.dll),卸载包时按清单删除这些文件,天然避免文件冲突。
2. Instrument 抽象深入解析
类层次
classDiagram
class IResource {
<
>
+string Name
+bool Enabled
+Open()
+Close()
}
class Resource {
<>
+string Name
+virtual bool Enabled
+virtual Open()
+virtual Close()
}
class IInstrument { <> }
class Instrument {
<>
Instrument() 默认Name= INST
}
class ScpiInstrument {
+IdnQuery
+Query/Write
+Connection 类型
+Open/Close 实现
}
class Dut {
<>
被测件模型
}
IResource <|-- Resource
IResource <.. IInstrument
IInstrument <|.. Instrument
Resource <|-- Instrument
Instrument <|-- ScpiInstrument
Resource <|-- Dut
Resource(抽象基类) :统一"台架资源"模型 —— 名称唯一、可启用/禁用、Open()/Close() 生命周期。Instrument 与 DUT 都继承它。
Instrument : Resource, IInstrument :源码仅 ~20 行(验证自 GitHub opentap/opentap Engine/Instrument.cs),构造函数默认 Name="INST"。派生类重写 Open/Close 完成硬件连接。
ScpiInstrument (SCPI 插件包提供):最常用的仪器基类,封装 SCPI 协议栈:
IDN 查询(*IDN? 自动识别厂商/型号/固件)
多传输层:VISA(Keysight/NI)、Raw TCP/Socket、Serial、USB-TMC、GPIB、HiSLIP
分层协议模型(Layer):可在 Message/Resource 层插入日志、超时、重试策略
连接配置持久化 :9.23+ 引入 BenchStore/ResourceSettings 体系,将仪器的地址、超时、别名从测试序列中剥离成台架级配置(对应旧版 InstrumentSettings.xml 文件)。
与你们 IDeviceDriver 的关键差异: OpenTAP 的 Instrument 对象同时承担"驱动 + 连接配置 + 生命周期"三种角色,且由序列化器在反序列化 TestPlan 时自动按类型名从插件图谱创建实例 。你们的 CanDriverAdapter(deviceId, busName, nodeIndex) 则是工厂显式构造,配置来自 SQLite 的 DeviceDefinition 表。
3. 测试步骤如何引用仪器
// OpenTAP 风格的 TestStep 引用仪器(编辑器自动渲染为下拉框)
public class PowerOnStep : TestStep
{
// 任何暴露 Instrument 属性的 TestStep,编辑器显示可选仪器列表;
// 列表来源 = PluginSearcher 扫描出的所有非抽象 Instrument 子类实例(来自 settings store)
public ScpiInstrument PowerSupply { get ; set ; }
public double Voltage { get ; set ; } = 12.0;
public override void Run()
{
using var _ = SynchronizationManager.RequestLock(PowerSupply, TimeSpan.FromSeconds(10));
PowerSupply.Write($"VOLT {Voltage:F2}");
PowerSupply.Write("OUTP ON");
var readback = PowerSupply.QueryDouble("MEAS:VOLT?");
if (Math.Abs(readback - Voltage) > 0.5)
Upwards.Find(Voltage, readback).Fail("电压偏差过大");
}
}
机制 说明
属性类型即选择器 public Instrument X 属性在编辑器/CLI 中渲染成仪器下拉框;[ResourceProperty] 等 Attribute 可限制候选类型(只列 CAN / 只列电源)
自动 Open/Close TestPlanRun 开始时引擎对所有 Enabled 的 Instrument 调 Open()(懒打开),结束后 Close();步骤代码不关心连接细节
并发保护 SynchronizationManager.RequestLock(resource, timeout) 提供进程内对同一仪器的互斥锁;跨进程互斥 OpenTAP 不提供 ,需外部仲裁(见 §9)
序列化 TestPlan XML 里保存的是仪器 Name 引用(非实例),运行时按名字解析到 settings store 中配置的实例
4. 包(Package)机制全景
包文件本质: .opentap 文件就是一个 ZIP 压缩包 ,根目录含 package.xml 清单,其余为载荷文件(DLL、配置、文档等)。清单声明了包名、语义化版本、平台约束、依赖关系和文件部署布局。
graph LR
subgraph 包文件
Z[".opentap (ZIP)"] --> PX["package.xml 清单"]
Z --> PL["载荷文件"]
end
subgraph 仓库
REPO["packages.opentap.io (官方 NuGet 风格仓库)"]
PRIV["私有仓库 tap repo add"]
end
PLAT["Developer's Platform (桌面版包浏览器/CI)"] --> REPO
REPO -->|tap package install| IMG["Image = 安装目录 状态 + 已装包清单"]
PRIV -->|同协议| IMG
PX --> IMG
IMG -->|部署到| FILES["根目录 + Packages/<name>/ + Dependencies/"]
IMG -->|卸载| DEL["按清单删除文件 (不影响其他包)"]
核心 CLI 命令
# 列出可安装 / 已安装
tap package list
tap package list -i
# 安装(可指定仓库、版本、通道)
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
# 从源码工程构建包
tap package create package.xml
# 管理私有仓库源
tap repo add http://192.168.1.50/opentap-repo
tap repo list
# 修复 / 序列化 / 会话
tap package recover
tap package serialize-package / deserialize-package
特性 机制
包隔离安装 包文件部署到 Packages/<包名>/,同名 DLL 不同版本可共存(引擎按包清单加载)
平台/架构区分 OS="Windows|Linux|MacOS"、Architecture="x64|x86|arm",清单内可按条件包含不同原生依赖(官方包用 $(Architecture) 变量切分 libgit2 的 .dll/.so/.dylib)
安装钩子 PackageActionExtensions 可在 pre/post install 执行外部命令(官方包用 chmod +x tap 修 Linux 权限)
通道(Channels) release / candidate / alpha,同一包可按不同通道发布;CI 场景锁定通道即可控制更新节奏
自动更新 tap package install --force 或桌面版包浏览器;版本比较使用严格 SemVer 2.0
5. package.xml 清单文件详解
以下骨架提炼自 官方 OpenTAP 包的真实 package.xml (GitHub opentap/opentap@main):
<?xml version="1.0" encoding="UTF-8"?>
<Package Name="MyCompany.CanDriver"
xmlns="http://opentap.io/schemas/package"
Version="$(GitVersion)" <!-- MSBuild 风格变量注入 -->
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>
<!-- ① 变量与条件(MSBuild 表达式)-->
<Variables>
<ArchitectureCondition Condition="$(Architecture) == ''">x64</Architecture>
<PlatformCondition Condition="$(Platform) == ''">Windows</Platform>
</Variables>
<!-- ② 依赖:指向其他"包"(版本范围/必需/推荐)-->
<Dependencies>
<PackageDependency Name="OpenTAP" Version="9.31.0" Action="Required"/>
<PackageDependency Name="Keysight.SCPI" Version="9.x" Action="Optional"/>
</Dependencies>
<!-- ③ 文件:安装路径 / 源路径 / 元数据操作 -->
<Files>
<!-- 插件 DLL 放进包隔离目录 -->
<File Path="Packages/MyCompany.CanDriver/CanDriverPlugin.dll">
<SetAssemblyInfo Attributes="Version"/> <!-- 自动写入程序集版本号 -->
</File>
<File Path="Packages/MyCompany.CanDriver/peak_pcans.dll" SourcePath="native/pcans.dll"/>
<File Path="Docs/**" SourcePath="doc/"/> <!-- glob -->
</Files>
<!-- ④ 平台差异化 -->
<Files Condition="$(Platform) == Linux">
<File Path="Packages/MyCompany.CanDriver/libpcans.so"/>
</Files>
</Package>
元素 作用
Package 根属性Name/Version/OS/Architecture,支持 $(变量) + Condition
Dependencies / PackageDependency声明对其他包的最小版本要求;Action = Required / Recommended / Optional
PluginDependencies声明运行时 .NET 程序集依赖(非包级),用于加载顺序与冲突提示
Files / File部署布局:Path(目标)+ SourcePath(源),可带 Action="Install|WriteCode"、SetAssemblyInfo(写版本/公司元数据)、SignCertificate( Authenticode 签名)
PackageActionExtensions安装前后执行外部可执行/脚本(如权限修复、驱动安装器)
Variables/Condition一份清单跨平台复用,构建时按变量裁剪文件集
TypeTags给插件类打标签,编辑器可按 tag 过滤(如 "PowerSupply" tag 只显示电源)
6. 依赖解析与版本模型
SemVer 2.0 :包版本严格遵循 MAJOR.MINOR.PATCH + 预发布标识(-alpha.1)。
解析器 :OpenTap.Package 内置求解器,处理 安装 A → A 依赖 B ≥2.0 → 已装 B 1.3 → 生成"升级 B"计划;多包交叉依赖用回溯搜索求最小变更集。
降级保护 :install 默认拒绝降级已安装包,除非 --force。
冲突处理 :同一文件被两个包声明 → 后者安装报错提示 ownership 冲突;通过 Packages/<name>/ 目录约定基本可避免。
OpenTAP 内核版本兼容 :所有包都依赖 OpenTAP 包本身;内核升级后旧包按 <PackageDependency Version="9.31.0"> 的最小版本匹配保持兼容,9.x 保持 ABI 承诺。
7. 驱动插件开发-分发-安装工作流
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,新仪器自动出现在下拉列表
开发环境依赖
NuGet 包:OpenTap、OpenTap.Package(提供 SDK 引用,编译期)
VS 扩展:OpenTAP Project Templates (含 Instrument 插件项目模板)
调试方式:把编译产物 DLL 直接拷到安装目录 Packages/xxx/,启动 tap ui(桌面版)或你们自己的编辑器宿主调试
注意:插件 SDK 是 netstandard2.0/net472 双目标 ,与你们 .NET Framework 4.7.2 主程序兼容
8. Image API:安装目录状态管理
OpenTAP 9.15+ 引入 OpenTap.Package.Image 概念,把"安装目录当前状态"抽象成对象:
// 程序化管理一个 OpenTAP 安装目录(CLI 底层就是它)
var image = new Image("C:/Program Files/OpenTAP" );
image.InstallPackage("MyCompany.CanDriver" , "latest" , "release" );
image.QueryAvailable("CAN" ); // 列出候选包
image.UninstallPackage("OldDriver" ); // 按清单精确删除
对你们的价值:未来如果做"HMI 内嵌驱动市场",可以直接调 Image API 实现界面里点几下完成驱动升级 ,无需运维介入。
9. 与 TapProject 现有架构的映射与差距
9.1 概念对照表
TapProject 现有 OpenTAP 对应物 匹配度
IDeviceDriver + DriverFactory switch Instrument 子类 + PluginSearcher 反射发现 ✅ 直接替换
DeviceConfig(SQLite DeviceDefinition) BenchStore / ResourceSettings(XML settings store) ⚠️ 需要双向同步适配器
IResourceManager 预约 + ResourceOwnerToken 租约 SynchronizationManager(仅进程内锁) ❌ OpenTAP 没有跨进程/跨会话仲裁,必须保留你们的
IAutoDeviceFacade + IAutoDeviceLease(Per-Step) TestStep 属性绑定 Instrument + RequestLock ✅ 语义等价,租约更细
HMI 插件 PluginLoader(WPF UI) 无对应(OpenTAP 不管 UI) — 保持独立
驱动分发=拷 DLL+重编译 .opentap 包 + tap package install + 私有仓库 ✅ 本次改造核心收益
9.2 桥接设计:双轨并行
graph TB
subgraph 统一层["Aging.Infrastructure(保留)"]
F["IAutoDeviceFacade"]
L["IAutoDeviceLease"]
RM["IResourceManager(跨会话仲裁)"]
end
subgraph 适配层["新增:Driver 适配"]
REG["DriverRegistry 反射扫描 Drivers/*.dll (阶段一,自研轻量)"]
BRIDGE["TapInstrumentBridge OpenTAP Instrument ↔ IDeviceDriver (阶段二,可选)"]
end
subgraph 驱动生态["两种驱动形态并存"]
D1["自研驱动 IDeviceDriverProvider → 打 .opentap 包分发"]
D2["第三方 OpenTAP 仪器包 Keysight/NationalInstruments → tap package install 直接用"]
end
F --> RM
F --> REG
REG --> D1
BRIDGE --> D2
BRIDGE --> REG
9.3 三个关键差距与对策
差距 影响 对策
OpenTAP 无生产/维护互斥仲裁 全自动与手动调试可能撞车(正是你们做租约的动机) 保留 IResourceManager,在 Instrument.Open() 里调用 Reserve;Close() 里 Release
仪器配置双源(SQLite vs settings store) 注册页改端点、序列里用旧地址 以 DeviceConfig 为唯一真源,写一个导出器生成 InstrumentSettings.xml;或反向导入
你们的 HMI 宿主 ≠ OpenTAP Editor 仪器下拉框在 WPF 里不会自动出现 你们的 DeviceEditModal 从 DriverRegistry/PluginManager 枚举类型填充 ComboBox(已有 ForceRefreshForTest 模式,同理)
10. 落地路线建议与成本评估
阶段 做什么 工作量 收益
阶段 0(已完成)
AutoDeviceFacade + Lease + 类型化 CAN 服务
—
调用体验已解决
阶段 1
自研 IDeviceDriverProvider + DriverRegistry 反射扫描 Drivers/*.dll; CAN 驱动拆成独立项目(丢 DLL 免编译主程序)
~2-3 人日
解决"加驱动必须重编译"的 80% 痛点;技术栈无引入
阶段 2
给每个驱动工程加 package.xml,用 tap package create 产出 .opentap;搭内网私有仓库(文件服务器即可)
~2 人日
版本化分发、依赖检查、现场 tap package install 一键装
阶段 3(可选)
TapInstrumentBridge:新真硬件仪器直接写 Instrument 子类走 OpenTAP 生态;桥接回 IDeviceDriver 复用租约
~5-10 人日
可直接复用 Keysight NI-DMM、Pickering 开关等数百个现成官方包,省去自研 SCPI 驱动
阶段 4(远期)
HMI 内嵌"驱动管理器"页面调 Image API 做界面级升级
~3 人日
运维自动化
风险提示:
阶段 1 反射加载若用 Assembly.LoadFrom 会锁文件导致无法热替换;建议 AssemblyLoadContext(.NET Core 风格,net472 无此 API → 退化为 AppDomain 隔离或接受"重启生效")。
OpenTAP 9.31 的 netstandard2.0 插件 SDK 在 net472 运行正常,但 不要假设 其包管理器会管理 net472 主程序——它管理的是 OpenTAP 安装目录;你们主程序若要利用 .opentap,需要把引擎装成一个"嵌入式 OpenTAP 目录"。
私有包仓库无鉴权时注意内网隔离;官方仓库地址可 tap repo remove 禁用外网访问。
11. 结论
OpenTAP 的 Instrument 是"资源型插件" :继承 Instrument : Resource 即自动获得发现、序列化绑定、生命周期托管;SCPI 场景有开箱即用的 ScpiInstrument。
插件包(.opentap)是分发单元而非引用单元 :ZIP + package.xml,依赖解析用 SemVer,平台差异化靠条件变量;安装到 Packages/<name>/ 隔离目录,卸载精确按清单。
"免重编译加驱动"两条路都能走 :轻量自研 DriverRegistry(阶段 1,立刻见效),或拥抱 OpenTAP 包生态(阶段 2/3,长期收益大,可白嫖厂商官方驱动)。
你们的 IResourceManager 租约体系是 OpenTAP 缺失的能力 ,改造中应保留并前移到 Instrument.Open()/Close() 钩子,而不是被替代。
推荐组合 :阶段 1 先行(2-3 人日解决主痛点)→ 阶段 2 上私有仓库(再 2 人日)→ 按真实硬件需求决定是否进阶段 3。
OpenTAP Instrument + 插件包机制分析 · TapProject 驱动热插拔调研 · 2026-11-03
参考:opentap.io 文档、github.com/opentap/opentap(package.xml / Engine/Instrument.cs 源码实测)