zjchina.net

Drogon · ROS 2 · 独立开发笔记

ADR-00082026-10-15

孪生层补 DES 内核:事件为真源、帧为投影

拍板:zj

背景

docs/LET_TIME_SEE.md §5.3(智能工厂轨道)把验收标准写死了一条硬要求:任一产线状态 可由台账日志重放还原。把孪生层现有件底盘点一遍,四条(三条腿 + 一个内核)里只到位两条半:

  • 已到位:数据面契约 zjchina_core::twin(Entity / EntityState / WorldSnapshot / TelemetrySample / BindingTable / TimelineFrame)、 内容地址 zjchina-ledger::twin::frame_cid(sha256 + canonical 信封,时间不进哈希)、 单帧时间插值回放 twin::replay_at(纯函数),以及一条 ROS2 sensor_msgs/JointState 手写解析(twin/feed.rs,零 ROS SDK)。
  • 缺失:①数据源腿(真 ROS2 桥 / rosbag 回放)、②时间轴腿(帧落台账 + 时间窗查询)、 ③仿真内核腿——框架完全没有。

③ 之所以是真缺口,是因为「由台账重放还原」只解决了看历史;没有仿真内核, 就没有「不看现场也能先把产线跑起来」的能力:新工位布局、缓冲位分配、瓶颈搬迁、 停机率对产出的影响,全靠真机试错。而这恰是无锡美的双高端工厂(出处见 docs/LET_TIME_SEE.md)现场最贵的一类问题。

本决策要定的是三件事:DES 内核放哪、它的真源是什么、以及它与既有孪生契约的边界。

选项

  • A. 新开独立 crate zjchina-sim。 概念最干净(引擎不是契约)。但 ADR-0006 已确立 「契约不跨节点边界、不为契约开新 crate」;而本内核产出物就是 WorldSnapshot, 是纯逻辑(CSR wasm / SSR native 双端可跑)、零外部依赖。为一个只依赖 zjchina-core 的纯算法模块开 crate,换来的是拆分成本而非隔离收益。
  • B. 引擎与契约都放 zjchina-core/src/sim/(采纳)。 与 twin/ 同构:契约 DTO
    • 纯函数同处 core(twin/bind.rs::apply_sample、twin/timeline.rs::replay_at 已是先例)。 逃生舱留好——引擎与 DTO 分文件,将来真要独立成 crate,成本 = 一个 Cargo.toml。
  • C. 复用 twin 的 Binding/BindingTable 把仿真当「遥测源」。 概念重复:Binding 是「外部遥测字段 → 实体属性」的解析器,而仿真内部的属性是引擎自己算出来的, 硬套一层绑定表只是把常量搬进配置文件。不采纳(但拓扑/身份复用 Entity/Scene, 见「拍板」)。

证据

  • twin/ 的既有约定可直接继承:apply_sample 的文档注释已明确「全局 seq / prev_cid 由追加方管理,不在本函数」——仿真的帧同样只出内容,盖章交给 ledger 侧。
  • frame_canonical 信封含 seq 与 prev(zjchina-ledger/src/twin.rs,其测试断言 「seq 属于内容」)。故 CID 对帧是位置指纹而非纯内容指纹;周期采样不会撞 CID, 但也不能被误读为内容指纹——这决定了本轮采用周期采样 + 单一赋值点,把 「变化触发帧 + 心跳帧」推后,届时要先让「无变化」成为内容的一部分。
  • timeline.rs::interpolate 只遍历前一帧的实体(该函数对 b 帧新增实体不补位): 所以不能造瞬态在制品实体,否则插值回放会「丢件」。在制品只能作为聚合标量 (wip)挂在工位上。这是既有代码反向约束设计的一个实例。
  • 确定性是回放与 CID 的前提:跨端浮点(Box–Muller 的 sin/cos、指数的 ln) 在 wasm 与 macOS libm 间可能差 1 ULP,取整后事件序分叉。故时间量一律整数毫秒、 抽样即取整,且 PRNG 自持(splitmix64),不引 rand——PRNG 算法一变「同种子同结果」 的全量历史结果就变,自持才可控。这条同时满足 AGENTS.md 依赖准入(零新增依赖)。
  • 消费侧样板已存在:/Users/zjchina/factorywatch(独立仓,汽车总装 11 工位看板) 的 fw-render/model.rs 自带 State{Run,Idle,Maint,Fault} / Line{station,takt,output, progress,state} / Board{kpis,lines,alarms,agvs,devices} 与自研 xorshift32, 注释明确写着「将来接真源时字段对齐 zjchina_core::twin 即可」——本内核的投影层 就是为这条对接准备的验证环。
  • OEE 的窗口口径必须排除暖机:availability 与 performance 的分母若含 warmup_ms,起步空线会被算成「停机」。故指标窗口 = horizon_ms − warmup_ms。

拍板

拍板者 zj(AI 协作者 TRAE 参与现状盘点与选项对比,拍板权属作者)。日期:2026-10-15。

① 落点:zjchina-core/src/sim/(选项 B),不新开 crate;lib.rs 只加 pub mod sim;, 不做平铺 re-export(Entity/Scene 等价名称过泛),统一 zjchina_core::sim::* 前缀, 与 twin 的既有命名纪律一致。

② 真源与投影:事件为真源,帧为投影。

  • 真源 = SimEvent{ts_ms, seq, station_id, kind, product_id} 事件流,(ts_ms, seq) 构成 全序 → 同刻事件顺序确定(回放、逐步审计、逐步 diff 都靠它);
  • 投影 = 每 sample_period_ms 采一帧的 WorldSnapshot 序列;帧的 seq / prev_cid 留空,由追加方(ledger 侧)盖章后算 frame_cid;
  • 指标由相位区间累计 fold 得出:每站任一时刻只处于 Busy/Blocked/Starved/Down 之一,故 busy + blocked + starved + down == 指标窗口 是结构性事实,而不是一句断言。

③ 建模口径(写死,避免反复):

  • 缓冲按边:StationSpec::out_buffer = 本站与下游之间可存放的成品件数。「按站入口缓冲」 对首站无意义,出口缓冲才能表达「慢下游反压上游」(本来靠 in_buffer 建模的话, 末站积压这条最常见的整线反压根本表达不出来)。
  • 末站例外:末站的下游是出货口,出货视为即时消耗,故末站完工即刻下线、 其 out_buffer 不参与建模(P1 不建模出货节拍;等出货节拍成为真需求再引入, 届时末站缓冲才有意义)。
  • 故障日历式 MTBF(与是否在加工无关),加工中断后保留剩余工时(不重置)。
  • performance 可 >1(标称节拍抽样偏低时),不 clamp;blocked / starved 归 performance 损失,不归 availability。
  • 依赖准入:零新增依赖;时间量整数毫秒;LineSpec::validate() 拦坏输入(返 Err 不 panic)。

④ 通用化红线(不可越):仿真只做通用化场景验证,样例 LineSpec 用虚构的通用 工位名,不带任何真实工厂数据、节拍、涉密信息(仓库零工厂信息);web 层永不进控制 闭环,硬实时留在 DDS/PLC 域;工厂数据不出内网。数据源腿(rclrs/rosbag)与时间轴腿 (帧落台账 + 时间窗查询)本轮不做,与本内核互不阻塞——内核只依赖既有契约, 故二者可并行推进、独立回滚。

⑤ 冻结(P2 再评估):变化触发帧/心跳帧、返修回流(破串联假设,需重做状态机)、 AGV/物料(连续运动,不硬塞离散事件)、多线/并行工位/资源竞争、factory 台账 channel 的 DDL 迁移、C++ 侧消费、大屏页面与 SSE、spec_cid。

← 返回决策日志