wenxiangchao
3 天以前 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
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
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
<!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/&lt;包名&gt;/</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($&quot;VOLT {Voltage:F2}&quot;);
        PowerSupply.Write(&quot;OUTP ON&quot;);
        <span class="kwd">var</span> readback = PowerSupply.QueryDouble(&quot;MEAS:VOLT?&quot;);
        <span class="kwd">if</span> (Math.Abs(readback - Voltage) &gt; 0.5)
            Upwards.Find(Voltage, readback).Fail(&quot;电压偏差过大&quot;);
    }
}</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/&lt;name&gt;/<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/&lt;包名&gt;/</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>&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;Package Name="MyCompany.CanDriver"
         xmlns="http://opentap.io/schemas/package"
         Version="$(GitVersion)"          <span class="cmt">&lt;!-- MSBuild 风格变量注入 --&gt;</span>
         OS="$(Platform)" Architecture="$(Architecture)"&gt;
 
  &lt;Description&gt;CAN 总线仪器驱动插件(PCAN/虚拟总线)&lt;/Description&gt;
  &lt;Status&gt;Released&lt;/Status&gt;
  &lt;Organisation&gt;我司&lt;/Organisation&gt;
  &lt;SourceUrl&gt;https://git.internal/CanDriver&lt;/SourceUrl&gt;
  &lt;SourceLicense&gt;Proprietary&lt;/SourceLicense&gt;
  &lt;Owner&gt;MyCompany&lt;/Owner&gt;
 
  <span class="cmt">&lt;!-- ① 变量与条件(MSBuild 表达式)--&gt;</span>
  &lt;Variables&gt;
    &lt;ArchitectureCondition Condition="$(Architecture) == ''"&gt;x64&lt;/Architecture&gt;
    &lt;PlatformCondition Condition="$(Platform) == ''"&gt;Windows&lt;/Platform&gt;
  &lt;/Variables&gt;
 
  <span class="cmt">&lt;!-- ② 依赖:指向其他"包"(版本范围/必需/推荐)--&gt;</span>
  &lt;Dependencies&gt;
    &lt;PackageDependency Name="OpenTAP" Version="9.31.0" Action="Required"/&gt;
    &lt;PackageDependency Name="Keysight.SCPI" Version="9.x" Action="Optional"/&gt;
  &lt;/Dependencies&gt;
 
  <span class="cmt">&lt;!-- ③ 文件:安装路径 / 源路径 / 元数据操作 --&gt;</span>
  &lt;Files&gt;
    <span class="cmt">&lt;!-- 插件 DLL 放进包隔离目录 --&gt;</span>
    &lt;File Path="Packages/MyCompany.CanDriver/CanDriverPlugin.dll"&gt;
      &lt;SetAssemblyInfo Attributes="Version"/&gt;   <span class="cmt">&lt;!-- 自动写入程序集版本号 --&gt;</span>
    &lt;/File&gt;
    &lt;File Path="Packages/MyCompany.CanDriver/peak_pcans.dll" SourcePath="native/pcans.dll"/&gt;
    &lt;File Path="Docs/**" SourcePath="doc/"/&gt;                    <span class="cmt">&lt;!-- glob --&gt;</span>
  &lt;/Files&gt;
 
  <span class="cmt">&lt;!-- ④ 平台差异化 --&gt;</span>
  &lt;Files Condition="$(Platform) == Linux"&gt;
    &lt;File Path="Packages/MyCompany.CanDriver/libpcans.so"/&gt;
  &lt;/Files&gt;
&lt;/Package&gt;</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/&lt;name&gt;/</code> 目录约定基本可避免。</li>
    <li><strong>OpenTAP 内核版本兼容</strong>:所有包都依赖 <code>OpenTAP</code> 包本身;内核升级后旧包按 <code>&lt;PackageDependency Version="9.31.0"&gt;</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/&lt;name&gt;/</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>