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