仪器注册与仪器调试 · 系统架构说明(V3.0 重梳理版)
基于《Local + DeviceBroker 混合架构详细设计说明书 V3.0》对现有系统的一次完整重构叙述 —— 含 TestStep 仪器使用规范 · 2026-11-03
一、总定位:一句话读新架构
配置域生产"不可变真理",仲裁域守护"谁有权碰",宿主域执行"谁来动手",业务域决定"该不该判 FAIL",数据域保证"事后能说清"。
仪器注册不再只是"增删改查表单",而是配置域的发布管线 ;仪器调试不再只是"连上发命令",而是仲裁域中一个特殊的写权限持有者(Maintenance Lease )。
与旧架构的根本区别: 旧架构里 DeviceConfig → data.db → DeviceDebug 是"两个模块共享一张表"的隐式契约;新架构里没有任何模块直接操作别人的库 ——注册产物是哈希锁定的快照,调试/生产消费的永远是快照 + 租约句柄,物理 I/O 永远只发生在唯一宿主内。
二、五域分层模型
graph TB
subgraph D1["① 配置域(设计时)"]
CFG["DeviceConfig 注册页 Draft → Validated → Published → Prepared → Effective → Retired"]
SNAP["config_snapshot(不可变+SHA256) resource_definition · slot_binding broker_task_template · effective_config"]
CFG --> SNAP
end
subgraph D2["② 仲裁域(运行时)"]
LS["LeaseStore arb.db(Local 域) broker-bxx.db(Broker 域)"]
RS["resource_state(generation/phase/epoch) parent_permit · lease_history"]
ARB["LeaseArbiter 优先级矩阵 · 让渡 · 抢占拒绝"]
LS --- RS
end
subgraph D3["③ 执行宿主域(物理 I/O)"]
WKR["Worker ×6(W01–W06) Local 设备唯一宿主 连接守卫 Mutex 专用线程"]
BRK["Broker ×3(B01–B03)+B00 SessionActor · TaskScheduler Journal/Outbox · Watchdog"]
DF["DriverFactory 分化 宿主驱动(载SDK) vs BrokerProxy(不载SDK)"]
end
subgraph D4["④ 业务执行域"]
TAP["OpenTAP TestPlan / Recipe 120 × DUTContext(异步计时) 逻辑能力绑定:空位+能力 不选 COM / IP / 进程"]
RUNS["RunService 中断窗口 · 续跑/重测决策"]
end
subgraph D5["⑤ 数据域"]
DB1[("aging_system.db 配置+审计索引")]
DB2[("arb.db / broker-bxx.db 各守单写者")]
DB3[("每 Run aging_data.db 采样分片归档")]
end
SNAP -->|"冻结 RunSnapshot 五元 Hash 对账"| TAP
TAP -->|"AcquireGroup"| ARB
ARB --> RS
TAP -->|"DeviceOperation Grants+IdemKey"| WKR & BRK
WKR & BRK --> DB2
TAP --> DB3
CFG --> DB1
域 解决什么 核心产物 不变量
① 配置域 物理世界的数字孪生 + 生效仪式 不可变快照 + Hash INV-04 启动后冻结 Recipe/Config/Driver/DBC/UDS/映射 Hash
② 仲裁域 谁有权在这台仪器上发命令 LeaseGrant(每资源一代) INV-02 预约授权与命令串行化分层
③ 执行宿主域 物理句柄独占 + 原子片段 SessionGate + Task Journal INV-01 同一 PhysicalSessionKey 只有一个 I/O 宿主
④ 业务执行域 工艺编排与合格判定 Verdict / DUTContext INV-05 写操作在实际宿主入口校验,客户端仅提示
⑤ 数据域 事后追溯与容量治理 四库分离 + Outbox INV-06 已发未回记 Unknown,禁止当未执行
三、仪器注册(DeviceConfig)的角色重塑
3.1 新旧职责对照
维度 旧模型(现状代码) V3.0 模型
本质 设备信息 CRUD 表单 配置发布管线 (Draft→Validated→Published→Prepared→Effective→Retired)
存储 DeviceDefinition 可变行,改了立即生效config_snapshot 不可变 ,(config_hash, resource_key) 复合主键,回滚也新建版本记录
版本管理 SaveDraft/Publish/Apply 改状态列发布产物 = JSON 快照 + SHA256;运行时冻结 RunSnapshot(Recipe/Driver/DBC/UDS/映射 五件套)
设备标识 Endpoint 字符串直写(CAN_BUS_1:0) EndpointRef (凭据/地址引用分离)+ PhysicalIdentity (SN 物理身份,防换机后身份漂移)
工位绑定 SlotDeviceMapping 一张映射表slot_binding(随快照版本化)+ 120 条映射由公式生成器产出并离线校验
冲突检测 DetectConflicts() 跑一次 SQL 分组发布门禁:身份去重、驱动位数、能力覆盖、Authority 一致、同一 PhysicalSessionKey 只允许一个 I/O 宿主(INV-01) ,不过审禁止发布
3.2 设备注册页需新增的字段(改造清单)
executionMode Local | Broker // 按物理拓扑决定,禁止运行时混用
authorityId broker-b01 / local-ipc01 // 所属仲裁域
brokerId + ownerProcessId // Broker 域归属
physicalSessionKey psu01/main // 同一物理连接标识(SessionGate 键)
physicalIdentity serial:TBD-PSU01 // SN 物理身份;TBD 禁止过发布门禁
accessPolicy Exclusive | ExclusiveChannelWithParentPermit
| TimeShared | SharedRead // 独占/分时的策略声明位置
safeStateProfile PSU-SAFE-001 // 恢复/交接时的安全态模板
leasePolicy TTL / Renew / Handover // 全局默认 + 域级覆盖
3.3 发布流程(替代现在的"应用"按钮)
flowchart LR
A[编辑草稿 Draft] --> B[离线校验 身份冲突/能力覆盖 限值/驱动依赖]
B --> C[Validated]
C --> D["维护窗口探测 连接/通道寻址/读回"]
D --> E["Published 不可变快照+Hash"]
E --> F["分发 Prepare 各宿主回执同 Hash"]
F --> G["停机窗口 Apply 停Run→撤写权→安全断连 →各宿主Apply+自检"]
G --> H{全部成功?}
H -->|是| I["更新 Effective 指针 Run 创建时复制快照"]
H -->|部分失败| J["整组 ApplyFailed 阻止 Run 启动 统一回旧版或新版"]
J -.-> F
关键语义变化: "生效"从一次 UPDATE 变成一场需要所有宿主回执确认 的两阶段仪式。旧稿 ApplyVersion() 那种单点改状态的逻辑不复存在;既有 Run 继续引用旧 Hash,不允许静默切版。
四、仪器调试(DeviceDebug)的角色重塑
4.1 最重要的拆分:监控 ≠ 写权限
graph TB
OLD["旧模型:选设备 → OpenSession(ReadOnly) → [RequestMaintenance → Activate] → 发命令 ⚠️ 监控和写权限混在同一个会话状态机里"]
OLD ==> NEW1["新模型 · 监控通路(常驻,无租约) 宿主状态缓存 / Telemetry Fanout → DeviceMonitorView 数据带 来源+质量+序列号+时间;Gap 必须显式呈现"]
OLD ==> NEW2["新模型 · 写权限通路(维护会话 = 申请 Maintenance 租约) 必须经过仲裁域:看占用 → Draining → 排空 → 安全确认 → Grant"]
4.2 维护会话状态机(对现有 DebugSessionState 的扩展)
stateDiagram-v2
[*] --> ViewOnly: 打开设备(仅监控)
ViewOnly --> Requested: 申请维护(填理由)
Requested --> Draining: Run协调安全暂停该空位
Draining --> MaintenanceActive: 排空+安全动作读回+旧句柄关闭\n+Mutex守卫移交+联锁手续确认
Draining --> RecoveryRequired: 交接超时(30s)
RecoveryRequired --> ViewOnly: 恢复核对完成\n(不能因"等了30秒"直接给维护者—INV-03)
MaintenanceActive --> Exiting: 退出维护
Exiting --> Released: 读回复核+恢复通信配置+报警基线
Released --> [*]: RunService显式恢复或新建Run
现有状态/方法 V3.0 去向
ReadOnly / OpenSession 拆为 ViewOnly(监控)+ Requested(写权申请)两个概念
RequestMaintenance(内存 Reserve) 进入仲裁域:显示占用者/Run/DUT/剩余租约/父子影响 → Draining
ActivateMaintenance 仅在"排空 + 安全动作读回 + 旧句柄/守卫确认"后签发维护 Grant
CloseSession(释放内存字典) 升级为"排空 I/O + 物理读回 + ReleaseAsync"三步,失败进 RecoveryRequired 而非假装成功
Faulted / RecoveryRequired 语义保留,但进入路径增加:交接超时、续期失败、释放失败
4.3 "强制释放"按钮的正确语义
旧直觉: Admin 点一下 → UPDATE/DELETE 租约行 → 设备归我。
V3.0: Force release 只能触发"隔离 + 恢复流程" ——先在物理层确认旧 I/O 已终止(旧进程句柄关闭、Named Mutex 可获取、设备输出读回),否则宁可保持 RecoveryRequired。多人作业、拔线、接线的许可不由软件 token 单独判定 (INV-07:软件租约不替代断电与硬件联锁)。
4.4 审计升级
debug_operation 保留,新增两列:lease_generation(哪一代持有者)、idempotency_key(重试去重,UNIQUE 部分索引)。任何一条历史指令可回答:谁(OwnerId+Principal)、以哪次授权(Grant 快照)、在哪个配置版本(ConfigHash)下、是否可能被重复执行过 。
五、进程拓扑与责任边界
graph TB
HMI["HMI 进程(工程师/操作员) DeviceConfigView ① · DeviceDebugView ② DeviceMonitorView ③ (新建)"]
W["Worker ×6(W01–W06,每20空位) Local 设备 I/O 宿主(连接守卫 Mutex 专用线程) DUTExecution 状态机 / 步骤引擎(OpenTAP) 独立续期任务(不占UI;失败立即阻断新操作)"]
B["Broker ×3(B01–B03)+ 可选 B00(全局共享域) 共享会话设备唯一 I/O 宿主 SessionActor(每真连接单队列,一次一段原子 I/O) Task执行 · Journal/Outbox单写器 · Recovery检查点"]
SUP["Supervisor 单实例守卫 · 限频拉起 · epoch 提升"]
DEV["物理设备 工位专用电源(Local) / 多通道PSU·CAN·温箱·PLC(Broker)"]
HMI -->|"配置发布/维护申请(申请只到仲裁域)"| W & B
W -->|"Local直连"| DEV
B -->|"唯一句柄"| DEV
SUP -.->|看护| W & B
进程 职责 明确不做
HMI 配置视图、维护写权申请、只读监控 不持有生产设备的物理连接
Worker Local 设备宿主 + 本空位组步骤引擎 + 异步等待(120 空位 ≠ 120 线程) 不加载共享设备的 SDK
Broker 共享会话设备唯一 I/O 宿主、Task 有界执行、周期任务、订阅遥测 不做 DUT 合格判定、不装整条 Recipe、不管 MES 事务 (不是第二套测试引擎)
Supervisor 拉起、限频防快速重启循环、epoch 管理 不执行业务命令
边界铁律:
Recipe/步骤引擎从不 直接选 COM 口/IP/进程——只写"空位 S024 + 能力 Power",由 SnapshotResolver + ResourceRouter 按冻结版本解析出资源集与宿主;
跨域多资源组申请 = 协调式预留(Prepare→Activate,任一失败 Abort),不承诺跨库原子 ;严格全有全无的动作必须落在同一 Broker 域或打包成一个 Task;
SQLite 一律本机磁盘,禁止 SMB/NAS 多机共写;连接显式 foreign_keys/busy_timeout;事务内禁做 I/O。
六、接口契约演化映射(现有代码 → V3.0)
现有实现(本仓库) V3.0 去向 变化性质
Core/ResourceManager.cs (内存 Reserve/Release/ValidateOwner)
旧单资源方法保留为过渡 Adapter;新增 IResourceCoordinator(AcquireGroup / RenewGroup / RequestHandover)
兼容扩展
Contracts/ResourceOwnerToken.cs (单一可变 Generation)
业务元数据保留;I/O 改用每资源独立 LeaseGrant (AuthorityId / Epoch / LeaseId / Generation / ExpiresUtcMs)
破坏性:一 owner 多资源不能共用 Generation
Services/AutoDeviceFacade.cs + IAutoDeviceLease(同步 Dispose 阻塞释放)
门面语义保留;Lease 注入独立续期任务 + LeaseLost 信号 + HandoverRequested;Dispose 改显式异步 Release(失败可查询、进恢复)
语义增强
Services/DeviceService.cs (deviceId+command+token 裸命令)
升级为 IDeviceExecutor:类型化 OpCode + Grants 集 + IdempotencyKey + Deadline;raw command 仅保留受控维护入口
生产路径重构
Drivers/DriverFactory.cs (静态 switch)
区分宿主驱动 (Local/Broker 内加载 SDK)与 BrokerProxy (客户端,不加载 SDK);配合 .opentap 包分发(见姊妹篇)
分发解耦
Plugin.DeviceDebug/Model/DebugSessionManager.cs
增 Draining / RecoveryRequired;恢复时不可重造 owner token ,必须使用持久化 Grant;监控与写权限分离
状态扩展
Plugin.DeviceConfig/Model/DeviceConfigModel.cs (DetectConflicts/SaveDevice)
冲突检测移入发布门禁(离线校验 + 维护窗口探测);可变表升级为不可变快照
位置前移
data.db (一库三用)+ debug.db
拆四库:aging_system.db / arb.db / broker-bxx.db / 每Run aging_data.db
最大的一次性工程:存储边界重组
ConnectionUtil/PipeClient.cs
V3.0 §13 认定其"响应不可达"(SendMessage 写后直接 return,读取为死代码)、serverName 未生效——修复后方可作为 IPC 候选基础
P0 前置修复项
建议: 契约(ILeaseScope / LeaseGrant / DeviceOperation / IDeviceExecutor / BrokerTaskRequest 等)抽出为独立类库 Aging.ResourceContracts ,避免 Broker / Worker / HMI 三端互带 WPF 依赖。
七、三条核心数据流
7.1 生产写命令(从步骤到物理 I/O 的完整一跳链)
sequenceDiagram
autonumber
participant STEP as TestStep (④业务域)
participant CO as IResourceCoordinator (②仲裁域)
participant EX as IDeviceExecutor (③宿主域)
participant SG as SessionGate/Actor (③宿主域)
participant DRV as 宿主驱动
participant DB as 运行库(Journal/Outbox)
STEP->>CO: AcquireGroup(资源集, OwnerKind=Production, ConfigHash)
CO->>DB: BEGIN IMMEDIATE 事务CAS(逐资源 Free→Active, generation+1) + parent_permit 检查 + lease_history
DB-->>CO: 提交成功
CO-->>STEP: ILeaseScope(Grants + 续期任务 + LeaseLost)
STEP->>EX: DeviceOperation(OpCode, Params, IdemKey, Grants, Deadline)
EX->>EX: 入队校验: grant集/epoch/ConfigHash/deadline/连接身份
EX->>SG: 出队持门: [选通道→写→读响应] 原子片段
SG->>DB: MarkRunning 先持久化(I/O 之前)
SG->>DRV: 物理执行
DRV-->>SG: 读回值
SG->>DB: 同事务提交 Task终态+事件+Outbox
SG-->>STEP: OperationResult(Readback, Quality)
Note over STEP: 判定 PASS/FAIL 在步骤/引擎(INV-05/T-3)
STEP->>CO: ReleaseAsync(排空→读回/交接→释放)
7.2 维护交接(摘要,对应文档 14.2 六步)
占用可见化(Run/DUT/剩余租约/父子影响)→ Run 协调安全暂停,资源转 Draining,拒新写、停周期 → 等在途片段结束 + 执行 SafeStateProfile 并读回(超时 → RecoveryRequired,不等于 自动给维护者)→ 旧 owner 排空释放 + 旧句柄关闭 + Mutex 守卫可获取 → 独立联锁/断电手续确认 → 签发维护 Grant,动作全量审计;退出维护反向复核后才交还生产。
7.3 Broker 重启恢复
取单实例守卫 → epoch+1 落盘 (旧 Grant/排队任务全部失效)→ 阻断普通写 → 设备身份验证 → 读回输出/通道/协议状态 → 按设备安全模板恢复(安全态 ≠ 统一"输出全关" :温箱、冷却、被测件各有要求)→ 恢复检查点与证据持久化(需人工审批则等待)→ Free/Ready → RunService 记录中断窗口与数据 Gap,决定续跑/重测。
八、保持不变的部分(架构连续性资产)
插件化 UI 骨架 :SimpleMaskControl / PluginLoader / MVVM 全部保留,三个视图仍是热插拔插件(注意现状命名对齐:实际为 Plugin.DeviceConfig.DeviceConfig 等,且 DeviceMonitorView 需新建);
SQLite 技术选型 (本机盘 + WAL + 短事务 + busy_timeout),但数据库边界重划;
调试会话"状态机 + 审计"设计思想 ——V3.0 是加状态,不是推翻;
Draft/Published/Effective 版本理念 ——升级为不可变快照 + 哈希对账;
全自动门面 B+C 模式 (作用域租约 + 类型化服务,本项目已实现)——正是 V3.0 ILeaseScope + DeviceOperation 的雏形。
九、TestStep 仪器使用规范(业务执行域使用视图)
前八章定义“系统与职责”,本章定义“使用者怎么写”——Recipe/TestStep 面向测试开发人员的使用契约 。
9.1 三条铁律(先立规矩)
# 铁律 原因
T-1
TestStep 永不 直接 new 驱动、填 COM 口、IP 地址、进程名
物理路由属于配置域(冻结快照);写死在步骤里 = 换台架全量改序列,且破坏 INV-01/04
T-2
TestStep 永不 自己调 ResourceManager.Reserve / 拼 LeaseGrant
租约生命周期归框架(Scope + 独立续期任务),步骤自己拿租约必然忘续期/忘释放
T-3
TestStep 永不 把 Broker 读回值直接当合格判定
“数据从哪来、新不新鲜”是宿主责任;“是否达到工艺条件”是步骤/引擎判定(§20.6 分权)
9.2 解析链:步骤声明逻辑能力,框架解析物理资源
flowchart LR
A["TestStep 声明 SlotId=S024, Capability=Power"] --> B["SnapshotResolver (Run启动时已冻结 RunSnapshot)"]
B --> C["slot_binding S024+Power → psu/psu03/ch/5 + 父 Permit psu/psu03"]
C --> D["ResourceRouter resource_definition: executionMode=Broker, authority=B01"]
D --> E1["LocalExecutor(W0x)"]
D --> E2["BrokerClient(B01)"]
style A fill:#E8F5E9
style E1 fill:#FFF8E1
style E2 fill:#FFF8E1
步骤作者面对的只有两个概念:空位号 + 能力名 。DUTContext 每空位独立保存 SN/步骤/计时/结果,120 个空位 ≠ 120 条线程 (等待用异步计时)。
9.3 三种使用模式
模式 B(首选):作用域租约 —— 一个步骤多次操作同一资源
// net472 无 await using,显式 ReleaseAsync + try/finally
public override async void Run()
{
var req = new AcquireRequest {
OwnerId = RunContext.StationId, // 如 "W02.Run-20261103-A"
OwnerKind = OwnerKind.Production,
ProcessInstanceId = RunContext.ProcessInstanceId, // boot_id+pid,防串台
ConfigHash = RunContext.RunSnapshot.ConfigHash,
ResourceKeys = new [] { "psu/psu03/ch/5" , "can/can01/ch/2" }, // 框架按序展开组申请
WaitTimeoutMs = 60_000
};
ILeaseScope scope = null ;
try
{
scope = await coordinator.AcquireGroupAsync (req, ct); // 同域:事务全有全无;跨域:协调式预留
// LeaseLost 信号:续期失败/被隔离 → 立即停止下发
var ioCts = CancellationTokenSource.CreateLinkedTokenSource (ct, scope.LeaseLost);
var psu = executorFactory.For (scope, "psu/psu03/ch/5" ); // Local/Broker 路由对步骤透明
await psu.SetVoltageAsync (12.0 , ctx.NextIdempotencyKey (), ioCts.Token);
await psu.OutputOn (ioCts.Token);
var v = await psu.ReadVoltage (ioCts.Token);
Upwards.Find (12.0 , v.Readback).Verdict = // ← 判定在引擎侧(T-3)
Math.Abs (v.Readback - 12.0 ) <= 0.5 ? Verdict.Pass : Verdict.Fail;
}
finally
{
// 显式异步释放:排空 I/O → 读回/安全交接 → 释放;
// 失败进恢复,绝不假装成功;终态保存用独立有界 token,不受调用方取消影响
if (scope != null )
await scope.ReleaseAsync (CancellationToken.None);
}
}
模式 C(常用封装):类型化服务 + 双签名
// 独立调用(框架内部走完 申请→执行→释放)——适合单命令步骤
await canBus.SendFrameAsync ("CAN-001" , 0x100 , data, ct);
// 借用作用域(推荐:与模式B组合,复用已持有租约)
await canBus.SendFrameAsync (scope.LeaseOf ("can/can01/ch/2" ), 0x100 , data, ct);
模式选择速查
场景 模式
单条查询类步骤(如扫一条 UDS DID) A 一次性
多命令原子工艺段(上电→设压→读数→关输出) B 作用域(+C 封装)
跨资源联动(同一 DUT 电源 + CAN) B 组申请(同域才承诺全有全无)
周期性激励 / 长时监控 B + §9.6 后台任务模板,不是循环 A
9.4 幂等键与超时重试规范
// 键的生成规则:全局唯一、可重放定位、重试不换内容只增 attempt 序号
string idemKey = $"{RunId}/{DutExecId}/{StepExecId}/attempt-{attempt}" ;
动作类型 超时后可自动重发? 依据
设置型(SetVoltage=12.0) ✅ 先 Query 对账再重发 幂等,重复执行结果同
触发/复位/烧写/计数型 ❌ 只能按 Unknown 走读回核对 文档 §11.2:跨执行与提交之间无硬件事务,不承诺 exactly-once
任意动作,结果未收到 先同键 Query/重提,Journal 有终态返回原结果 §20.4:绝不生成新键再触发一次
客户端重连必须保留原 ClientInstanceId 查询 ,不生成新键重发。
9.5 交接、续期与 LeaseLost:步骤作者的义务
框架职责 步骤职责
后台独立线程续期(TTL/3) 监听 scope.LeaseLost,联进所有下发的 CTS
Draining 通知(维护者申请进入) 只在【步骤边界】让渡,绝不在半条命令间放手
续期连续失败 2 次 → LeaseLost 立即停止下发新操作
Release 失败 如实上报,等待恢复,不“记日志后继续”
// 步骤收尾点检查(框架在 HandoverRequested 时置位)
if (scope.HandoverRequested) return ; // 本步骤正常结束,作用域退出时安全让渡
// 注意:长 Delay 应拆成可中断的异步等待片段,保证 30s handover 窗口内能到达边界
取消的三种语义 (框架已处理,作者只需知道):排队中取消 = 确定未执行;执行中取消 = 需驱动安全停止确认;响应等待取消 = 只是不等了,不能反推设备没动 ——结果仍走 Unknown/读回通道。
9.6 长周期任务:交给 Broker,步骤不养线程
需求 ❌ 错误做法 ✅ 正确做法(Task 类型)
CAN 周期发送 0x100@100ms TestStep 内 while+Sleep PeriodicTx 模板驻留 Broker,随 owner 租约存续/到期/失效;禁止重连后补发旧帧
老化期间监控总线信号 120 空位轮询线程 Acquisition 订阅 + 事件游标增量拉取;数据 Gap 带质量标记 Stale/Disconnected
UDS 会话保活 步骤定时器发 TesterPresent ProtocolSession :会话归一调度,TesterPresent 与业务动作同队
等 30 分钟 Thread.Sleep 占线程 异步计时留在执行引擎(Recipe 的长 Delay 不进 Broker ——Broker 只做有界设备动作)
// 步骤侧订阅形态:提交-查询-ACK,不驻留
var sub = await broker.SubscribeAsync (new { resource = "can/can01/ch/2" ,
signal = "Motor.Temp" , templateId = "can.acq.motor-temp" }, ct);
// 等待信号达标(判定仍在引擎)
var sample = await sub.WaitMatchingAsync (s => s.Value > 85.0 , TimeSpan.FromMinutes (30 ), ct);
if (sample == null ) Upwards.Find ().Fail ($"超时未达85℃,Gap={sub.GapCount}" );
await sub.UnsubscribeAsync (ct); // 取消并确认停采/解绑
结果消费遵循 Outbox:ACK 只在结果落盘成功后发送 ,步骤崩溃重启用同 IdempotencyKey 找回未 ACK 结果,不重跑设备动作。
9.7 完整示例:多工位 CAN 老化步骤(B + C + 订阅 拼成真实工艺)
[Display("CAN 通信老化" , Group = "Aging" )]
public class CanCommAgingStep : TestStep
{
[ResourceProperty("CAN" )] public string SlotId { get ; set ; } = "S001" ;
public int Cycles { get ; set ; } = 100 ;
public override async void Run()
{
var ctx = RunContext.Current; // Run/DUT/Step 关联ID
var auto = AgingRuntime.Coordinator; // IResourceCoordinator
var cans = AgingRuntime.CanBus; // ICanBusService(类型化)
var req = ctx.BuildAcquire (new [] { "can/" + ctx.SlotOf (SlotId) + "/ch/2" });
using var cts = Utils.LinkedWithVerdictCancel (Upwards);
ILeaseScope scope = await auto.AcquireGroupAsync (req, cts.Token);
try
{
var lease = scope.LeaseOf (ctx.ResourceOf (SlotId));
// ① 起周期激励(驻留 Broker,随租约失效自动停)
var periodic = await lease.StartTemplateAsync ("can.periodic.keepalive" ,
new { frameId = 0x3E0 , periodMs = 500 }, cts.Token);
// ② 订阅响应信号
var sub = await lease.SubscribeAsync ("can.acq.response-0x3E1" , cts.Token);
for (int i = 0 ; i < Cycles && !scope.HandoverRequested; i++)
{
await cans.SendFrameAsync (lease, 0x100 + (uint)i, ctx.Payload (i),
ctx.NextIdempotencyKey (), cts.Token);
var echo = await sub.WaitMatchingAsync (f => f.MatchRequest (0x100 + i),
TimeSpan.FromSeconds (2 ), cts.Token);
if (echo == null ) { Verdict = Verdict.Fail; break ; } // 判定在引擎
Report.Sample (i, echo); // 采样走结果通路
}
await periodic.StopAsync (cts.Token); // 周期任务显式停止并“确认停止”
await sub.UnsubscribeAsync (cts.Token);
}
finally
{
await scope.ReleaseAsync (CancellationToken.None); // 异步、可失败进恢复
}
}
}
9.8 步骤生命周期时序
OnPlanLoad 解析 Slot+Capability → 预检配置存在性(不占资源)
Run() 入口 DUTContext 状态检查 → AcquireGroup(排队等待=异步,不阻塞他位)
持租阶段 类型化操作/订阅 → 每命令带 Grant+IdemKey+Deadline → 读回质量核对
退出边界 HandoverRequested? → 让渡或续跑
finally 停后台任务(确认) → ReleaseAsync → 结果 Outbox ACK 后终态保存
异常路径 LeaseLost / Unknown → 停止下发 → 上报 → 交给恢复/RunService 决策(不自动判合否)
9.9 反模式清单(TestStep 侧汇总)
反模式 后果 正解
步骤里 new CanDriverAdapter(...) 绕过仲裁与快照,双宿主冲突(INV-01) 只经 Scope/Executor 路由
超时后换新 IdempotencyKey 重发 触发型动作执行两次 同键 Query 对账(§20.4)
在 await 跨线程处释放守卫 Mutex AbandonedMutex / 非法释放 守卫归宿主专用线程,步骤只操作租约
scope.Dispose() 同步阻塞 UI/线程池 排空等待卡死调度 显式 ReleaseAsync
用 Thread.Sleep 等老化时长 120 空位 = 120 线程 异步计时 + Broker Periodic/Acquisition
把 Broker 读回直接当 PASS 掩盖通信故障(数据质量≠工艺判定) 判定在步骤/引擎,Gap 必须计入
捕获 LeaseLost 后继续发命令 僵尸写入(栅栏只挡坏结果,挡不住你没查) LeaseLost 联进所有下发的 CTS
十、结论
新架构图的一句话读法: 仪器注册生产带哈希的配置真理 ,仪器调试申请带栅栏的临时写权 ,Worker/Broker 执行带原子片段的物理 I/O ,Recipe 只做空位→能力→判定 ,四本数据库各守各的单写者——任何一次“碰设备”都要同时回答:授权是谁(Grant)、配置是哪版(Hash)、物理状态信不信得过(读回) 。
落地顺序遵循 P0~P4 裁剪(PipeClient 修复与硬件拓扑冻结 → 单 Authority 租约内核 → Broker MVP → 恢复编排 → 配置发布体系),详见《多进程仪器资源方案评估》。本文与以下文档配套阅读:
仪器注册与仪器调试 · 系统架构说明 V3.0 重梳理版(含 TestStep 使用规范)· 2026-11-03
基准:《Local + DeviceBroker 混合架构详细设计说明书 V3.0》· 本文现状描述部分已对照真实代码核实