仪器注册与仪器调试 · 系统架构说明(V3.0 重梳理版)

基于《Local + DeviceBroker 混合架构详细设计说明书 V3.0》对现有系统的一次完整重构叙述 —— 含 TestStep 仪器使用规范 · 2026-11-03
一、总定位:一句话读新架构 二、五域分层模型 三、仪器注册(DeviceConfig)的角色重塑 四、仪器调试(DeviceDebug)的角色重塑 五、进程拓扑与责任边界 六、接口契约演化映射(现有代码 → V3.0) 七、三条核心数据流 八、保持不变的部分(架构连续性资产) 九、TestStep 仪器使用规范 十、结论

一、总定位:一句话读新架构

配置域生产"不可变真理",仲裁域守护"谁有权碰",宿主域执行"谁来动手",业务域决定"该不该判 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
域解决什么核心产物不变量
① 配置域物理世界的数字孪生 + 生效仪式不可变快照 + HashINV-04 启动后冻结 Recipe/Config/Driver/DBC/UDS/映射 Hash
② 仲裁域谁有权在这台仪器上发命令LeaseGrant(每资源一代)INV-02 预约授权与命令串行化分层
③ 执行宿主域物理句柄独占 + 原子片段SessionGate + Task JournalINV-01 同一 PhysicalSessionKey 只有一个 I/O 宿主
④ 业务执行域工艺编排与合格判定Verdict / DUTContextINV-05 写操作在实际宿主入口校验,客户端仅提示
⑤ 数据域事后追溯与容量治理四库分离 + OutboxINV-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配置视图、维护写权申请、只读监控不持有生产设备的物理连接
WorkerLocal 设备宿主 + 本空位组步骤引擎 + 异步等待(120 空位 ≠ 120 线程)不加载共享设备的 SDK
Broker共享会话设备唯一 I/O 宿主、Task 有界执行、周期任务、订阅遥测不做 DUT 合格判定、不装整条 Recipe、不管 MES 事务(不是第二套测试引擎)
Supervisor拉起、限频防快速重启循环、epoch 管理不执行业务命令
边界铁律:

六、接口契约演化映射(现有代码 → 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,决定续跑/重测。

八、保持不变的部分(架构连续性资产)

  1. 插件化 UI 骨架:SimpleMaskControl / PluginLoader / MVVM 全部保留,三个视图仍是热插拔插件(注意现状命名对齐:实际为 Plugin.DeviceConfig.DeviceConfig 等,且 DeviceMonitorView 需新建);
  2. SQLite 技术选型(本机盘 + WAL + 短事务 + busy_timeout),但数据库边界重划;
  3. 调试会话"状态机 + 审计"设计思想——V3.0 是加状态,不是推翻;
  4. Draft/Published/Effective 版本理念——升级为不可变快照 + 哈希对账;
  5. 全自动门面 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@100msTestStep 内 while+SleepPeriodicTx 模板驻留 Broker,随 owner 租约存续/到期/失效;禁止重连后补发旧帧
老化期间监控总线信号120 空位轮询线程Acquisition 订阅 + 事件游标增量拉取;数据 Gap 带质量标记 Stale/Disconnected
UDS 会话保活步骤定时器发 TesterPresentProtocolSession:会话归一调度,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 跨线程处释放守卫 MutexAbandonedMutex / 非法释放守卫归宿主专用线程,步骤只操作租约
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 → 恢复编排 → 配置发布体系),详见《多进程仪器资源方案评估》。本文与以下文档配套阅读: