From 3467f203caa4dd48a05976cf90bfa5aab268999f Mon Sep 17 00:00:00 2001 From: caoqianming Date: Mon, 18 May 2026 21:38:33 +0800 Subject: [PATCH] Add operation-system engine design spec Spec covers station/segment/step/interlock domain model, segment state machine (Idle..ManualAckRequired), action templates including persistent commands, resource lease registry, ops.* event taxonomy, and the AppEvent WebSocket envelope. Stage plan includes P-1 core cleanup before ops work begins. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...26-05-18-operation-system-engine-design.md | 631 ++++++++++++++++++ 运转系统逻辑说明.doc | Bin 0 -> 119296 bytes 2 files changed, 631 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-18-operation-system-engine-design.md create mode 100644 运转系统逻辑说明.doc diff --git a/docs/superpowers/specs/2026-05-18-operation-system-engine-design.md b/docs/superpowers/specs/2026-05-18-operation-system-engine-design.md new file mode 100644 index 0000000..4a8c73b --- /dev/null +++ b/docs/superpowers/specs/2026-05-18-operation-system-engine-design.md @@ -0,0 +1,631 @@ +# 运转系统顺控引擎设计 + +日期:2026-05-18 + +参考来源: +- `运转系统逻辑说明.doc`(说明书 14 章) +- `docs/运转系统实现方案.md`(高层方案) +- `docs/superpowers/specs/2026-04-14-dual-app-shared-core-design.md`(双应用共享核心架构) +- 现有 `crates/app_feeder_distributor` 实现作为工程参考 + +## 1. 目标 + +在已经搭好的 `crates/app_operation_system` 骨架内,落地说明书中规定的整线自动控制能力: + +- 覆盖 8 个业务子系统:回车线、前端码车道、机械臂、摆渡车、1 号干燥/焙烧窑、2 号干燥/焙烧窑、窑尾下摆渡车、卸砖机位。 +- 引擎语义遵循说明书第 1.4 与 13 章:"顺序控制 + 联锁保护 + 检测信号闭环确认 + 异常停留人工恢复"。 +- 双窑线(1 号 / 2 号)采用同一套段模板,仅通过参数差异化,不写两套代码。 +- 复用 `plc_platform_core` 的接入层(OPC UA / 点位 / 设备 / 事件 / WebSocket / 日志)。 +- 不引入 `app_feeder_distributor` 的 `unit + run_time/stop_time/acc_time/bl_time` 业务模型。 + +非目标(首期): + +- 不做规则引擎或 DSL,只支持固定 `rule_kind` 联锁判定。 +- 不做高级排程(最大化吞吐、动态优化),只做基于空位/资源占用的放行决策。 +- 不做权限/审计/历史回放。 + +## 2. 设计结论 + +| 决策 | 选择 | 原因 | +| --- | --- | --- | +| 业务模型 | **station + segment + step + interlock** | 说明书是工位驱动的整线顺控,不是节拍式设备启停 | +| `unit` 表 | **不复用** | 语义不匹配;ops 自己建 `process_segment` | +| 引擎调度单位 | **段(segment)** | 每个 enabled segment 一个 tokio task,对齐 feeder 引擎结构 | +| 双窑线参数化 | **同一段模板 + line_code 区分实例** | 对齐说明书第 11 章 | +| 联锁配置 | **数据库表 + 固定 rule_kind 枚举** | 首期不引入表达式语言 | +| WebSocket 消息扩展 | **core 保持通用通道,ops 使用业务 payload 分支** | 避免 `plc_platform_core` 反向依赖 ops 领域类型;前端仍只连一处 | +| 报警 | **走 `event` 表 + `subject_type/subject_id` + `level=warn/error`** | 复用现有事件表,同时支持按段 / 工位查询 | +| 公共资源互斥 | **app 内部命名锁注册表 + 租约/恢复策略** | 摆渡车 / 机械臂 / 卸砖机位等共享资源,防止 task 异常退出后长期占锁 | + +## 3. 不沿用 feeder 模型的理由 + +`ControlUnit` 当前字段是 `run_time_sec / stop_time_sec / acc_time_sec / bl_time_sec`,语义是"运行 N 秒 → 停 M 秒 → 累计 K 秒后启动布料机 → 布料 L 秒"。 + +运转系统的核心动作完全不是这种节拍: +- 说明书 8.2 要求"码车位到车确认 → 输送机构停止",是检测信号驱动,不是定时。 +- 说明书 10.1 要求"开门 → 门开到位确认 → 顶车 → 前位确认 → 顶车后退 → 后位确认 → 关门 → 门关到位确认",是 8 步串行带闭环。 +- 说明书 13 章明确要求"动作完成不得仅靠时间,必须结合限位、检测或反馈信号"。 + +因此引擎需要换语义:**段(segment)状态机 + 步骤(step)顺序 + 每步等待闭环信号**。 + +## 4. 领域模型 + +### 4.1 实体一览 + +``` +source ──┐ + │ +point ───┼─→ equipment + │ + ├─→ station_signal ──→ station ──┐ + │ │ + └──────────────→ segment_step ──→ process_segment ──→ segment_runtime + │ │ + │ ├──→ segment_interlock + │ └──→ segment_resource + │ + └──→ action_kind (枚举) +``` + +`source / point / equipment` 沿用平台层定义,不改动。 + +信号边界: + +- `point.signal_role` 是设备信号角色,例如 `rem / flt / home / run / start_cmd / stop_cmd / open_cmd / close_cmd`。 +- `station_signal.signal_role` 是工位信号角色,例如 `presence / vacancy / arrived / allow_in / done / fault`。 +- 同一个 `point` 可以同时被设备角色和工位角色引用,但两者语义分开维护。 +- `vacancy` 可由独立点位绑定,也可由 `presence = false` 推导。首期通过 `station_signal.derived_from_role` 表达推导关系,避免现场必须额外提供空位点。 + +### 4.2 新增对象 + +#### 4.2.1 `station`(工位) + +表示流程中的一个位置或交接位。 + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | UUID | | +| `code` | TEXT UNIQUE | 例 `ST-DRY1-IN` | +| `name` | TEXT | 例 "1 号干燥窑进口位" | +| `line_code` | TEXT NULL | 例 `KILN_1` / `KILN_2` / `COMMON` | +| `segment_code` | TEXT NULL | 用于分组(前端码车 / 双窑线 / 窑尾) | +| `station_type` | TEXT | `load / dry_in / dry_step / dry_out / fire_in / fire_step / fire_out / transfer / unload / return` | +| `enabled` | BOOL | | +| `description` | TEXT NULL | | + +#### 4.2.2 `station_signal`(工位 ↔ 信号绑定) + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | UUID | | +| `station_id` | UUID FK | | +| `signal_role` | TEXT | `presence / vacancy / arrived / allow_in / done / fault` | +| `point_id` | UUID FK | 绑定到具体点位 | +| `derived_from_role` | TEXT NULL | 例 `presence`,表示由同工位其他角色反向推导 | +| `invert_value` | BOOL | 推导或读取时是否取反,默认 false | +| UNIQUE | (`station_id`, `signal_role`) | | + +#### 4.2.3 `process_segment`(流程段) + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | UUID | | +| `code` | TEXT UNIQUE | 例 `SEG-DRY1-INFEED` | +| `name` | TEXT | | +| `segment_type` | TEXT | `front_load / robot / front_release / front_transfer / kiln_infeed / kiln_step / kiln_outfeed / tail_transfer / tail_step / unload / return` | +| `line_code` | TEXT NULL | `KILN_1` / `KILN_2` / `COMMON` | +| `priority` | INT | 公共资源冲突时使用 | +| `enabled` | BOOL | | +| `mode` | TEXT | `auto / remote_manual / local_manual / disabled` | +| `require_manual_ack_after_fault` | BOOL | 故障解除后是否需要人工确认,默认 true | +| `description` | TEXT NULL | | + +模式语义: + +- `local_manual`:现场就地优先,软件不推进自动顺控;自动运行中检测到任一相关设备 `rem=false` 时,停止当前自动段并进入人工恢复路径。 +- `remote_manual`:允许通过软件发单步 / 单设备命令,但仍必须执行设备故障、通信质量、安全链和关键门位联锁。 +- `auto`:允许 supervisor 自动推进段状态机。 +- `disabled`:段任务不启动;已运行任务在下一次配置重载后退出。 + +#### 4.2.4 `segment_step`(段步骤) + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | UUID | | +| `segment_id` | UUID FK | | +| `step_no` | INT | 序号 | +| `step_code` | TEXT | 步骤代号 | +| `action_kind` | TEXT | 见下方动作模板表 | +| `target_equipment_id` | UUID NULL FK | 例如顶车机 | +| `target_station_id` | UUID NULL FK | 例如目标摆渡位 | +| `confirm_signal_role` | TEXT NULL | 等待哪个信号角色为真 | +| `confirm_point_id` | UUID NULL FK | 直接指定确认点位(覆盖 role) | +| `expected_value` | BOOL | 信号到位的期望值(默认 true) | +| `timeout_ms` | INT | 超时即报警转 Faulted | +| `command_role` | TEXT NULL | 设备命令角色,例 `start_cmd / open_cmd / forward_cmd` | +| `stop_command_role` | TEXT NULL | 到位或异常时需要发出的停止命令角色,例 `stop_cmd` | +| `pulse_ms` | INT NULL | 脉冲命令宽度;为空时按 action 默认值 | +| `hold_until_confirm` | BOOL | true 表示命令保持到确认信号或故障;false 表示脉冲后等待 | +| `cancel_on_fault` | BOOL | 故障 / 模式切换 / 通信异常时是否执行停止命令,默认 true | +| `next_step_no_on_success` | INT NULL | 成功后跳转;为空表示顺序进入下一 step | +| `next_step_no_on_failure` | INT NULL | 失败后跳转;首期通常为空并进入 Faulted | +| `on_timeout` | TEXT | `fault / retry / block`,首期默认 `fault` | +| `description` | TEXT NULL | | +| UNIQUE | (`segment_id`, `step_no`) | | + +`action_kind` 枚举(首期): + +| 值 | 含义 | +| --- | --- | +| `open_door` | 开门:向门机 `open_cmd` 发脉冲 | +| `close_door` | 关门 | +| `push_forward` | 顶车机前进 | +| `push_retract` | 顶车机后退复位 | +| `pull_run` | 拉引机拉车 | +| `pull_retract` | 拉引机复位 | +| `transfer_move_to` | 摆渡车移动到目标工位 | +| `step_once` | 节拍步进机执行一步 | +| `robot_permit` | 允许机械臂自动作业 | +| `robot_release` | 允许码车道放车 | +| `wait_signal` | 不发命令,仅等待 `confirm_*` | +| `pulse_cmd` | 通用脉冲命令(fallback) | + +动作执行策略: + +- 对 `open_door / close_door / robot_permit` 等短命令,默认 `pulse_ms=300`,命令发出后等待确认信号。 +- 对输送、顶车、拉引、步进等持续动作,默认 `hold_until_confirm=true`,到位后执行 `stop_command_role`。 +- 对故障、急停、通信质量异常、自动切就地等中断场景,若 `cancel_on_fault=true`,先发停止 / 复位命令,再进入 `Faulted` 或 `ManualAckRequired`。 + +#### 4.2.5 `segment_interlock`(段联锁) + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `id` | UUID | | +| `segment_id` | UUID FK | | +| `applies_to` | TEXT | `start_allow / start_deny / run_halt` | +| `rule_kind` | TEXT | 见下方 | +| `point_id` | UUID NULL FK | | +| `station_id` | UUID NULL FK | | +| `equipment_id` | UUID NULL FK | | +| `expected_value` | BOOL NULL | | +| `description` | TEXT NULL | | + +`rule_kind` 枚举(首期): + +- `point_eq` —— 指定 point 的值等于 `expected_value` +- `station_vacant` —— 工位空(绑定的 `vacancy` 信号 = true 且 `presence` = false) +- `station_occupied` —— 工位有车 +- `equipment_origin` —— 设备在原位(角色 `home`) +- `equipment_no_fault` —— 设备无故障(`flt` = false) +- `equipment_remote` —— 设备远程(`rem` = true) +- `safety_chain_ok` —— 安全链路正常 + +未来可扩展 `expression` 类型,但首期不引入。 + +#### 4.2.6 `segment_runtime`(段运行态,内存) + +不落库(与 feeder `UnitRuntime` 一致,重启重置): + +```rust +pub enum SegmentState { + Idle, + Checking, + Executing, + Confirming, + Resetting, + Completed, + Blocked, + Faulted, + ManualAckRequired, +} + +pub struct SegmentRuntime { + pub segment_id: Uuid, + pub state: SegmentState, + pub auto_enabled: bool, + pub current_step_no: Option, + pub step_started_at: Option>, + pub last_completed_at: Option>, + pub blocked_reason: Option, + pub fault_message: Option, + pub manual_ack_required: bool, + pub comm_locked: bool, + pub rem_local: bool, + pub held_resources: Vec, +} +``` + +#### 4.2.7 `segment_resource`(段资源声明) + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `segment_id` | UUID FK | | +| `resource_key` | TEXT | 例 `transfer_front / transfer_tail / robot_arm / unload_position / return_line` | +| UNIQUE | (`segment_id`, `resource_key`) | | + +#### 4.2.8 `event` 表归因扩展 + +现有 `event` 表保留 `unit_id / equipment_id / source_id`,为了支持 ops 按段、工位检索,新增通用归因字段: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `subject_type` | TEXT NULL | `segment / station / equipment / source / platform` | +| `subject_id` | UUID NULL | 对应业务对象 ID | + +ops 事件写入规则: + +- 段级事件:`subject_type='segment'`,`subject_id=segment_id`。 +- 工位状态事件:`subject_type='station'`,`subject_id=station_id`。 +- 设备动作事件:优先保留 `equipment_id`,同时可按上下文设置 `subject_type='segment'`。 + +### 4.3 双窑线参数化 + +不写两套硬编码逻辑。1 号与 2 号窑线的差异由: + +- `process_segment.line_code`(`KILN_1` / `KILN_2`) +- `segment_step.target_equipment_id` 与 `target_station_id`(指向各自的门机、顶车机、工位) +- `segment_interlock.point_id` / `station_id`(指向各自工位的检测点) + +承载。引擎读到的就是统一的 step 列表,与窑线无关。 + +## 5. 顺控引擎设计 + +### 5.1 结构(与 feeder 对齐) + +``` +crates/app_operation_system/src/ + app.rs // AppState 接入 segment_runtime + event_manager + resource_registry + router.rs + event.rs // AppEvent(ops.*) + control/ + mod.rs + engine.rs // supervisor + per-segment task + runtime.rs // SegmentRuntime / SegmentRuntimeStore + state.rs // SegmentState enum + step_executor.rs // 按 action_kind 调度 + interlock.rs // 通用允许/禁止/停机判定 + resource.rs // 摆渡车 / 机械臂 / 卸砖位 互斥 + simulate.rs // 开发态信号回灌 + handler/ + doc.rs (已存在) + station.rs // CRUD + 信号绑定 + segment.rs // CRUD + step / interlock 配置 + control.rs // 段启停 / 手动动作 / 故障确认 + runtime.rs // overview / segment detail / station detail +``` + +### 5.2 段状态机 + +对应说明书 13.6: + +| SegmentState | 含义 | 出口 | +| --- | --- | --- | +| `Idle` | 等待 auto 启动 | → `Checking` | +| `Checking` | 评估 `start_allow` / `start_deny` 联锁 | 通过 → `Executing`;否则 → `Blocked` | +| `Executing` | 已发出当前 step 的命令 | → `Confirming` | +| `Confirming` | 等待 `confirm_signal` 到位 | 收到 → 下一步;超时 → `Faulted` | +| `Resetting` | 等待执行机构复位(如顶车机后退) | → 下一步或 `Completed` | +| `Completed` | 段完成,输出完成信号 | 回 `Idle`(自动循环段) | +| `Blocked` | 允许条件不满足 | 条件再次满足 → `Checking` | +| `Faulted` | 故障或超时 | 故障解除 + 满足复位 → `ManualAckRequired` 或 `Idle` | +| `ManualAckRequired` | 等待人工确认 | API ack → `Idle` | + +### 5.3 段内执行循环 + +伪代码: + +``` +loop { + reload segment + steps + interlocks + run check_interlocks(state, run_halt) // 运行中停机检测 + match state { + Idle if auto_enabled => state = Checking, + Checking => { + if pass(start_allow) && !any(start_deny) { + step = first_step + state = Executing + } else { + blocked_reason = ... + state = Blocked + } + } + Executing => { + execute(step.action_kind, step.target_*) // 发命令 + state = Confirming + } + Confirming => { + wait_signal(step.confirm_*, step.timeout_ms) + on timeout → fault / retry / block by step.on_timeout + on ok → next_step_no_on_success or next step or Completed + } + Faulted => break and wait manual recovery + ... + } + notify or fault_tick +} +``` + +`wait_signal` 复用与 feeder `wait_phase` 类似的 `tokio::select! { sleep_until(deadline), notify, fault_tick }` 模式,但终止条件是"绑定信号到达期望值"而非时间到。 + +### 5.4 step_executor + +集中处理 `action_kind` 到具体写点动作: + +- 短命令类 `action_kind` 调 `plc_platform_core::control::command::send_pulse_command`。 +- 持续命令类 `action_kind` 先写 `command_role`,确认到位、超时或故障中断时按 `stop_command_role` 收尾。 +- `transfer_move_to`:写目标工位编号到摆渡车定位命令点位,等待 `arrived` 信号。 +- `wait_signal`:不发命令。 +- 各设备的 `start_cmd / stop_cmd / open_cmd / close_cmd` 信号角色复用 feeder 已有的 `signal_role` 命名空间,equipment 表无需新表结构。 + +命令执行前必须重新检查: + +- 设备 `rem=true` +- 设备 `flt=false` +- 命令点与确认点 `quality=Good` +- 当前段仍处于允许执行模式 +- 当前 step 仍是 runtime 中的 `current_step_no` + +## 6. 联锁与异常 + +### 6.1 联锁判定顺序(对齐说明书 8.1 / 13) + +1. 通信质量(任一绑定点 quality != Good) → `comm_locked` +2. 就地 / 远程状态(`rem=false`)→ 停止自动并转人工恢复 +3. 安全联锁 / 急停 → `Faulted` +4. 设备故障(`flt` = true) → `Faulted` +5. 门位联锁 +6. 机械臂联锁 +7. 工艺允许条件(空位 / 到位) +8. 普通顺控条件 + +高优先级不满足时低优先级不再判断。 + +### 6.2 通用允许检查(自动注入到每段) + +每段无论是否有显式 `segment_interlock`,引擎都执行以下通用检查(说明书 13.1): + +- 目标工位空位 +- 本工位有车或动作前提 +- 执行机构原位 +- 设备无故障 +- 设备处于远程 +- 信号质量正常 +- 段引用的资源未被占用 + +### 6.3 异常恢复(说明书 13.5) + +- 故障优先停止当前 step 的命令。 +- `Faulted` 保留 `current_step_no`,不跳步。 +- `remote_manual` 下允许人工执行复位动作,但复位动作仍执行安全、故障、门位和通信检查。 +- 故障物理消失后: + - 若 `require_manual_ack_after_fault`(默认 true) → `ManualAckRequired` + - 否则自动回 `Idle`。 +- `POST /api/control/segment/{id}/ack-fault` 用于人工确认。 + +## 7. 公共资源调度 + +说明书 3.3 / 3.4 指出:前端码车系统、窑尾摆渡、回车线、卸砖线为公共段,1 号 / 2 号窑线在此处汇合。 + +实现: + +```rust +pub struct ResourceRegistry { + inner: RwLock>, +} + +pub struct ResourceLease { + pub owner_segment_id: Uuid, + pub acquired_at: DateTime, + pub heartbeat_at: DateTime, +} +``` + +资源 key 示例:`transfer_front` / `transfer_tail` / `robot_arm` / `unload_position` / `return_line`。 + +段配置中以新表 `segment_resource(segment_id, resource_key)` 声明所需资源;段进入 `Executing` 前必须 `try_acquire`,进入 `Completed` 时 `release`。冲突时停留 `Blocked`,附 `blocked_reason = "resource_busy: transfer_front"`。 + +资源恢复策略: + +- 资源持有段每个状态循环刷新 `heartbeat_at`。 +- 若 owner task 已退出、段被禁用、或 owner 已回到 `Idle/Completed`,supervisor 可回收租约。 +- `Faulted` 时是否释放资源按资源类型决定:机械臂区、卸砖位等可释放;摆渡车正在载车时不释放,必须人工确认或到达安全位后释放。 +- 资源等待超时只报警和进入 `Blocked`,不抢占低优先级段。首期不做死锁自动解除。 + +## 8. 事件与 WebSocket + +### 8.1 业务事件命名空间 `ops.*` + +| event_type | level | +| --- | --- | +| `ops.segment.auto_started` | info | +| `ops.segment.auto_stopped` | info | +| `ops.segment.step_advanced` | info | +| `ops.segment.completed` | info | +| `ops.segment.blocked` | warn | +| `ops.segment.fault_locked` | error | +| `ops.segment.fault_acked` | info | +| `ops.segment.comm_locked` | warn | +| `ops.segment.comm_recovered` | info | +| `ops.station.state_changed` | info | +| `ops.alarm.action_timeout` | error | +| `ops.alarm.signal_conflict` | error | +| `ops.alarm.resource_busy` | warn | + +所有事件经 `record_event` 落 `event` 表(复用平台机制)。 + +### 8.2 WebSocket 消息扩展 + +不把 ops 的 `SegmentRuntime` 类型放进 core。`plc_platform_core::websocket::WsMessage` 增加一个通用业务消息分支,业务 payload 由 app crate 构造: + +```rust +pub enum WsMessage { + // 已有 ... + AppEvent(AppWsEvent), +} + +pub struct AppWsEvent { + pub app: String, // "operation-system" + pub event_type: String, // "segment_runtime_changed" / "station_state_changed" + pub data: serde_json::Value, +} +``` + +ops 侧约定: + +- `event_type="segment_runtime_changed"`:`data` 序列化 `SegmentRuntime`。 +- `event_type="station_state_changed"`:`data` 包含 `station_id / presence / vacancy / arrived / updated_at`。 +- feeder 前端忽略未知 `AppEvent` 或非本 app 的消息;ops 前端只处理 `app="operation-system"`。 + +> 这样仍保留单一 websocket 入口,但 core 不需要知道 ops 的领域模型。 + +## 9. API 设计 + +### 9.1 配置 API + +``` +GET /api/station +POST /api/station +GET /api/station/{id} +PUT /api/station/{id} +DELETE /api/station/{id} +POST /api/station/{id}/signal // 绑定信号 +DELETE /api/station/{id}/signal/{role} + +GET /api/segment +POST /api/segment +GET /api/segment/{id} +GET /api/segment/{id}/detail // 含 step / interlock / resource +PUT /api/segment/{id} +DELETE /api/segment/{id} +POST /api/segment/{id}/step +PUT /api/segment/{id}/step/{step_no} +DELETE /api/segment/{id}/step/{step_no} +POST /api/segment/{id}/interlock +DELETE /api/segment/{id}/interlock/{interlock_id} +``` + +### 9.2 控制 API + +``` +POST /api/control/segment/{id}/start-auto +POST /api/control/segment/{id}/stop-auto +POST /api/control/segment/{id}/reset // 强制回 Idle,仅在 Faulted/Blocked 状态可用 +POST /api/control/segment/{id}/ack-fault +POST /api/control/segment/{id}/manual-step // remote_manual 下单步执行 +POST /api/control/segment/batch-start-auto +POST /api/control/segment/batch-stop-auto + +POST /api/control/equipment/{id}/manual-action // remote_manual 下单设备动作,仍执行联锁 +``` + +### 9.3 运行态 API + +``` +GET /api/runtime/overview // 所有段 + 关键工位 + 报警计数 +GET /api/runtime/segment/{id} +GET /api/runtime/station/{id} +GET /api/event?type=ops.* +``` + +## 10. 前端 + +复用 `web/core` 的源码、点位、设备、事件、日志、文档抽屉。 + +`web/ops/` 增加: + +- 总览页:双窑线 + 公共段流程图(首版静态 SVG + 区域绑定段 / 工位状态) +- 段卡片列表:展示 `state / current_step / blocked_reason / fault_message` +- 工位状态视图:有车 / 空位 / 到位 +- 配置页:站点 / 段 / step / interlock 表格 + 表单 +- 手动操作:段启停 / 故障确认 / 复位 + +WebSocket 订阅 `AppEvent(app="operation-system")`,按 `event_type` 分发 `segment_runtime_changed` 和 `station_state_changed` 实时刷新。 + +## 11. 复用 vs 新增对照 + +| 模块 | 来源 | 用途 | +| --- | --- | --- | +| `plc_platform_core::connection` | 复用 | OPC UA 读写 | +| `plc_platform_core::control::command::send_pulse_command` | 复用 | 所有动作命令底层 | +| `plc_platform_core::event::record_event` + `EventInsert` | 复用 | 事件落库 | +| `plc_platform_core::event::MetadataCache` | 复用 + 扩展 | 通用化为按 `(table, id)` 查 code;feeder 用 unit/equipment,ops 加 station/segment | +| `plc_platform_core::websocket::WsMessage` | 重构 | 删除 `UnitRuntimeChanged`(feeder 业务),新增通用 `AppEvent(AppWsEvent)`;feeder 和 ops 都走 AppEvent | +| `plc_platform_core::handler::platform_routes` | 复用 | source / point / equipment / tag / page | +| `plc_platform_core::model::ControlUnit` | **迁出 core** | P-1 阶段下放到 feeder;语义本就是 feeder 业务 | +| `plc_platform_core::control::runtime::{UnitRuntime, ControlRuntimeStore}` | **迁出 core** | 同上,含 `DistributorRunning` 这种 feeder 专属状态 | +| `plc_platform_core::service::control` unit CRUD | **迁出 core** | 下放到 feeder;event 查询留 core | +| `app_feeder_distributor::control::*` | **不复用** | 结构参考 | + +> **P-1 阶段说明**:上表中的"迁出 core"是清理动作,发生在 P0 之前。详见 §12。 + +## 12. 阶段计划 + +| 阶段 | 目标 | 主要工作 | +| --- | --- | --- | +| **P-1 Core 业务清理** | core 不再持有 feeder 业务模型 | 把 `UnitRuntime / UnitRuntimeState / ControlRuntimeStore / ControlUnit / unit CRUD / WsMessage::UnitRuntimeChanged` 从 `plc_platform_core` 迁到 `app_feeder_distributor`;`WsMessage` 新增 `AppEvent(AppWsEvent)` 分支并删除 `UnitRuntimeChanged`;feeder 引用全部调整;前端 ws 客户端按 `app + event_type` 分发;`MetadataCache` 通用化为 `entity_code(table, id)`。零行为变更,feeder 通过现有 smoke test | +| **P0 骨架对齐** | `app_operation_system` 与 feeder 在依赖、AppState、bootstrap、tray、启动/退出链路对齐 | Cargo.toml 补依赖;AppState 加 `EventManager` + `SegmentRuntimeStore` + `ResourceRegistry`;启动接 `connect_all_enabled_sources`;启动 engine supervisor;退出时断开数据源 | +| **P1 数据库迁移 & 模型** | ops 配置表 + event 归因字段 + Rust model | 新 migration `2026-05-1x_create_operation_system.sql`;新增 `station / station_signal / process_segment / segment_step / segment_interlock / segment_resource`;扩展 `event.subject_type/subject_id`;`app_operation_system::model` 模块 | +| **P2 配置 API** | 站点 / 段 / step / interlock CRUD | `service::station / segment`;handler;router | +| **P3 引擎 MVP** | 跑通 1 个段端到端(前端码车位进车段,说明书 8.2) | `engine`、`step_executor`、`interlock`、`runtime`;通用 `AppEvent` WebSocket 推送 | +| **P4 动作模板补全** | 覆盖 8 章 + 10 章典型动作 | 各 `action_kind` 实现 + simulate 反馈 | +| **P5 双窑线段模板化** | 通过段配置实现 1 号 / 2 号窑线 4 段(进口 / 内前移 / 出口) | 段配置 seed;端到端跑通 | +| **P6 资源调度** | 公共段互斥 | `ResourceRegistry`;`segment_resource` 表;Blocked 路径完善 | +| **P7 公共段** | 摆渡车 / 卸砖 / 回车线 | 段实例 + 段间交接 | +| **P8 报警 & 异常恢复** | 超时报警、信号冲突、人工确认完整链路 | `AppEvent::Alarm*`;ack-fault API | +| **P9 前端监控页** | 段卡片 + 工位状态 + 流程图 | `web/ops/html` + JS | +| **P10 配置前端** | 段 / 工位 / 联锁可视化配置 | `web/ops/html` 表格表单 | + +每阶段都要求: + +- `cargo build -p app_operation_system` 通过 +- 至少 1 个单元测试或 smoke test +- 不破坏 `app_feeder_distributor` 编译 + +## 13. 风险与约束 + +### 13.1 主要风险 + +- **P-1 迁移破坏 feeder**:从 core 把 unit 模型迁到 feeder 时容易漏改 import 或 ws 客户端调用。要求迁移单独成 commit,feeder 启动 + 单元测试 + ws 推送链路逐项验证。 +- **现场 I/O 清单缺失**:说明书描述了逻辑关系但未明确每个工位 / 设备对应的具体点位。落地前必须补 I/O 对照表。 +- **段切分粒度**:段切得太细 → 状态机膨胀;切得太粗 → 段内步骤过多。首期建议按说明书章节级切(一节 = 一段)。 +- **WebSocket 领域边界**:不得把 `SegmentRuntime` 放入 core,否则 core 会反向依赖 ops 业务模型;采用通用 `AppEvent` payload。 +- **公共资源死锁**:例如摆渡车被段 A 占用、段 A 又等卸砖位空(被段 B 占用)。首期通过段优先级与超时报警缓解,不引入死锁检测。 +- **持续命令收尾**:输送、顶车、拉引等不是纯脉冲动作,必须在超时、故障和模式切换时明确停止命令。 + +### 13.2 约束 + +- 首期不做规则引擎,所有联锁靠固定 `rule_kind` 枚举。 +- 首期段 / step 改动不做热加载——supervisor 每 10s 重读配置,与 feeder 一致。 +- 首期 `segment_runtime` 不持久化,重启全部回 `Idle`。 +- 首期不做资源抢占;资源冲突只阻塞、报警和等待人工处理。 + +## 14. 验收标准 + +完成 P0–P5 后应达到: + +- 仓库新增 6 张 ops 业务配置表,并扩展 `event.subject_type/subject_id`,与 feeder 业务表互不干扰。 +- `app_operation_system` 可独立编译为 exe,可启动并连接 OPC UA 数据源。 +- 启动后具备 `EventManager`、`SegmentRuntimeStore`、`ResourceRegistry`、engine supervisor,退出时可断开数据源。 +- 至少 1 条段(例如 2 号干燥窑进口段,含 8 步)可通过配置驱动跑通: + - 自动启停 + - 步骤顺序推进 + - 闭环信号确认 + - 持续动作到位后停止命令 + - 故障停步 + 人工确认 +- WebSocket 通过 `AppEvent(app="operation-system")` 推送段运行态变化、工位状态变化。 +- 前端可见段卡片与当前步骤进度。 +- `event` 表能按 `ops.*` 和 `subject_type/subject_id` 查到全链路事件。 + +完成 P6–P10 后应达到: + +- 1 号 / 2 号窑线全部 6 段(进口 / 内前移 / 出口 × 2 窑)跑通。 +- 公共段(前端码车、摆渡车、窑尾、卸砖、回车)跑通。 +- 报警分类齐全(说明书 13.4 全部 10 类)。 +- 监控前端 + 配置前端可用。 + +## 15. 后续可演进项(非首期) + +- 联锁 `expression` 类型:引入简单布尔表达式语言,替代 `rule_kind` 枚举。 +- 段历史持久化:将每段每次完成 / 故障写入 `segment_run_history`,支持时间线回放。 +- 现场调试视图:模拟点位值、单步推进、跳步授权(带操作员签名)。 +- 公共能力下沉:若后续出现第三套类似业务,再把 segment 引擎抽到 `plc_platform_core::control::segment`。 diff --git a/运转系统逻辑说明.doc b/运转系统逻辑说明.doc new file mode 100644 index 0000000000000000000000000000000000000000..fa93b37939c3838571ac9009bfe8c6ef51892fd2 GIT binary patch literal 119296 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 literal 0 HcmV?d00001