refactor(web): remove dead code and translate plan docs to Chinese
- ops.js: remove unused `formatValue` import
- logs.js: remove `export` from internal-only `appendLog`
- state.js: fix stale comment ({ valueEl, qualityEl } → { dotEl })
- docs: rewrite both plan docs in Chinese; update dual-view-web plan to
reflect actual implementation (sigDotClass dots, loadAllEquipmentCards,
syncEquipmentButtonsForUnit, batch auto buttons in startOps)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
dd0e782450
commit
4ce91adf60
|
|
@ -1,38 +1,38 @@
|
||||||
# Control Engine Implementation Plan
|
# 控制引擎实现计划
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
> **适用于代理执行:** 必须使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务执行。步骤使用复选框(`- [ ]`)语法跟踪进度。
|
||||||
|
|
||||||
**Goal:** Implement the automated control engine for coal feeder / distributor units, including state machine, fault/comm protection, runtime API and frontend control panels.
|
**目标:** 实现投煤器 / 布料机单元的自动控制引擎,包括状态机、故障/通信保护、运行时 API 及前端控制面板。
|
||||||
|
|
||||||
**Architecture:** The engine spawns one async task per enabled unit (supervised by a 10s scanner). Each task drives the unit's state machine using `tokio::time::sleep_until` for phase timing and `tokio::sync::Notify` for instant wake-up when external state changes (auto enable/disable, fault ack). A 500ms fault-poll ticker runs inside each task's `wait_phase` helper so fault/comm status is still checked promptly during long phases. State is kept in `ControlRuntimeStore` (in-memory, never persisted). Frontend receives real-time updates via `WsMessage::UnitRuntimeChanged` — pushed **only on state transitions**, not every tick.
|
**架构:** 引擎为每个已启用的单元各启动一个异步任务(由10秒扫描器监督)。每个任务通过 `tokio::time::sleep_until` 控制阶段计时,通过 `tokio::sync::Notify` 在外部状态变化时(自动启停、故障确认)立即唤醒。状态保存在 `ControlRuntimeStore`(内存中,不持久化)。前端通过 `WsMessage::UnitRuntimeChanged` 实时接收更新——**仅在状态转换时推送**,不做周期性推送。
|
||||||
|
|
||||||
**Tech Stack:** Rust/Axum backend, sqlx/PostgreSQL, tokio async, vanilla JS ES modules frontend.
|
**技术栈:** Rust/Axum 后端、sqlx/PostgreSQL、tokio 异步、Vanilla JS ES 模块前端。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## File Map
|
## 文件清单
|
||||||
|
|
||||||
| File | Action | Responsibility |
|
| 文件 | 操作 | 职责 |
|
||||||
|------|--------|---------------|
|
|------|------|------|
|
||||||
| `src/control/runtime.rs` | ✅ Done | `UnitRuntime` struct + `ControlRuntimeStore` with `Notify` per unit |
|
| `src/control/runtime.rs` | ✅ 已完成 | `UnitRuntime` 结构体 + `ControlRuntimeStore`(含 `Notify`) |
|
||||||
| `src/control/command.rs` | ✅ Done | Shared `send_pulse_command()` helper |
|
| `src/control/command.rs` | ✅ 已完成 | 共享 `send_pulse_command()` 和 `simulate_run_feedback()` |
|
||||||
| `src/control/engine.rs` | ✅ Done | Supervisor + per-unit async tasks + `wait_phase` |
|
| `src/control/engine.rs` | ✅ 已完成 | 监督器 + 单元异步任务 + `wait_phase` |
|
||||||
| `src/control/validator.rs` | ✅ Done | Block manual commands when unit is fault/comm locked |
|
| `src/control/validator.rs` | ✅ 已完成 | 故障/通信锁定时阻断手动指令 |
|
||||||
| `src/control/mod.rs` | ✅ Done | Exposes `command`, `engine`, `runtime`, `validator` |
|
| `src/control/mod.rs` | ✅ 已完成 | 导出 `command`、`engine`、`runtime`、`validator` |
|
||||||
| `src/event.rs` | ✅ Done | 7 `AppEvent` variants; `UnitStateChanged` fires but is **not** persisted to DB |
|
| `src/event.rs` | ✅ 已完成 | 7个 `AppEvent` 变体;`UnitStateChanged` 触发但**不持久化到数据库** |
|
||||||
| `src/websocket.rs` | ✅ Done | `WsMessage::UnitRuntimeChanged` |
|
| `src/websocket.rs` | ✅ 已完成 | `WsMessage::UnitRuntimeChanged` |
|
||||||
| `src/service/control.rs` | ✅ Done | `get_all_enabled_units`, `get_equipment_by_unit_id` |
|
| `src/service/control.rs` | ✅ 已完成 | `get_all_enabled_units`、`get_equipment_by_unit_id` |
|
||||||
| `src/handler/control.rs` | ✅ Done | `start_auto`, `stop_auto`, `batch_start_auto`, `batch_stop_auto`, `ack_fault`, `get_unit_runtime`; calls `notify_unit` after every state change |
|
| `src/handler/control.rs` | ✅ 已完成 | `start_auto`、`stop_auto`、`batch_start_auto`、`batch_stop_auto`、`ack_fault`、`get_unit_runtime`;每次状态变更后调用 `notify_unit` |
|
||||||
| `src/main.rs` | ✅ Done | Routes for above endpoints |
|
| `src/main.rs` | ✅ 已完成 | 上述端点的路由注册 |
|
||||||
| `web/js/state.js` | ✅ Done | `runtimes: new Map()` |
|
| `web/js/state.js` | ✅ 已完成 | `runtimes: new Map()` |
|
||||||
| `web/js/units.js` | ✅ Done | Runtime state badge, Auto Start/Stop, Ack Fault; shows `display_acc_sec` |
|
| `web/js/units.js` | ✅ 已完成 | 运行时状态徽章、Auto Start/Stop、Ack Fault;显示 `display_acc_sec` |
|
||||||
| `web/js/ops.js` | ✅ Done | Ops panel unit cards show runtime badge + `display_acc_sec` |
|
| `web/js/ops.js` | ✅ 已完成 | 运维面板单元卡片显示运行时徽章与 `display_acc_sec` |
|
||||||
| `web/js/app.js` | ✅ Done | Handles `UnitRuntimeChanged` WS message |
|
| `web/js/app.js` | ✅ 已完成 | 处理 `UnitRuntimeChanged` WS 消息 |
|
||||||
| `web/styles.css` | ✅ Done | `.event-card { flex-shrink: 0 }` prevents text overlap under flex column |
|
| `web/styles.css` | ✅ 已完成 | `.event-card { flex-shrink: 0 }` 防止 flex 列表中文字重叠 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Current UnitRuntime Shape
|
## UnitRuntime 结构体(当前)
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// src/control/runtime.rs
|
// src/control/runtime.rs
|
||||||
|
|
@ -41,102 +41,102 @@ pub struct UnitRuntime {
|
||||||
pub unit_id: Uuid,
|
pub unit_id: Uuid,
|
||||||
pub state: UnitRuntimeState,
|
pub state: UnitRuntimeState,
|
||||||
pub auto_enabled: bool,
|
pub auto_enabled: bool,
|
||||||
pub accumulated_run_sec: i64, // internal accumulator (ms); do NOT display directly
|
pub accumulated_run_sec: i64, // 内部累加器(毫秒),不直接用于显示
|
||||||
pub display_acc_sec: i64, // snapshot at state-transition; use this for display
|
pub display_acc_sec: i64, // 状态转换时的快照,前端展示用此字段
|
||||||
pub fault_locked: bool,
|
pub fault_locked: bool,
|
||||||
pub flt_active: bool,
|
pub flt_active: bool,
|
||||||
pub comm_locked: bool,
|
pub comm_locked: bool,
|
||||||
pub manual_ack_required: bool,
|
pub manual_ack_required: bool,
|
||||||
}
|
}
|
||||||
// NOTE: elapsed-time fields (current_run_elapsed_sec, current_stop_elapsed_sec,
|
// 注意:elapsed 字段(current_run_elapsed_sec、current_stop_elapsed_sec、
|
||||||
// distributor_run_elapsed_sec, last_tick_at) were removed in the event-driven
|
// distributor_run_elapsed_sec、last_tick_at)已在事件驱动重构中移除。
|
||||||
// refactor. Timing is now managed entirely by tokio::time::sleep_until inside
|
// 计时完全由单元任务内部的 tokio::time::sleep_until 管理,请勿重新添加。
|
||||||
// the per-unit task. Do not re-add them.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`ControlRuntimeStore` adds:
|
`ControlRuntimeStore` 额外包含:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
notifiers: Arc<RwLock<HashMap<Uuid, Arc<Notify>>>>,
|
notifiers: Arc<RwLock<HashMap<Uuid, Arc<Notify>>>>,
|
||||||
|
|
||||||
// Methods:
|
// 方法:
|
||||||
pub async fn get_or_create_notify(&self, unit_id: Uuid) -> Arc<Notify>
|
pub async fn get_or_create_notify(&self, unit_id: Uuid) -> Arc<Notify>
|
||||||
pub async fn notify_unit(&self, unit_id: Uuid) // call from handlers after state changes
|
pub async fn notify_unit(&self, unit_id: Uuid) // 每次状态变更后调用
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Engine Architecture (event-driven, 2026-03-26)
|
## 引擎架构(事件驱动,2026-03-26)
|
||||||
|
|
||||||
```
|
```
|
||||||
start()
|
start()
|
||||||
└─ supervise() — interval 10s, spawns unit_task per enabled unit
|
└─ supervise() —— 10秒间隔,为每个启用单元启动 unit_task
|
||||||
|
|
||||||
unit_task(unit_id)
|
unit_task(unit_id)
|
||||||
├─ load_equipment_maps — once at task start (cached for task lifetime)
|
├─ load_equipment_maps —— 任务启动时加载一次(缓存至任务生命周期结束)
|
||||||
├─ fault_tick — interval 500ms, used inside wait_phase
|
├─ fault_tick —— 500ms 间隔,在 wait_phase 内部使用
|
||||||
└─ loop:
|
└─ loop:
|
||||||
├─ reload unit config (check still enabled)
|
├─ 重新加载单元配置(检查是否仍启用)
|
||||||
├─ check_fault_comm → push WS if changed
|
├─ check_fault_comm → 有变化则推送 WS
|
||||||
├─ if !auto || fault || comm → select!(fault_tick | notify), continue
|
├─ 若 !auto || fault || comm → select!(fault_tick | notify),continue
|
||||||
└─ match state:
|
└─ 按状态分支:
|
||||||
Stopped → wait_phase(stop_time_sec) → start feeder → state=Running → push WS
|
Stopped → wait_phase(stop_time_sec) → 启动给煤机 → 状态=Running → 推送 WS
|
||||||
Running → wait_phase(run_time_sec) → stop feeder → acc += run_time_sec
|
Running → wait_phase(run_time_sec) → 停止给煤机 → acc += run_time_sec
|
||||||
→ if acc >= acc_time_sec: start distributor, state=DistributorRunning
|
→ 若 acc >= acc_time_sec:启动布料机,状态=DistributorRunning
|
||||||
→ else: state=Stopped → push WS
|
→ 否则:状态=Stopped → 推送 WS
|
||||||
DistributorRunning → wait_phase(bl_time_sec) → stop distributor → acc=0 → state=Stopped → push WS
|
DistributorRunning → wait_phase(bl_time_sec) → 停止布料机 → acc=0 → 状态=Stopped → 推送 WS
|
||||||
FaultLocked|CommLocked → select!(fault_tick | notify)
|
FaultLocked|CommLocked → select!(fault_tick | notify)
|
||||||
|
|
||||||
wait_phase(secs):
|
wait_phase(secs):
|
||||||
deadline = now + secs
|
deadline = now + secs
|
||||||
loop:
|
loop:
|
||||||
select! { sleep_until(deadline) => return true
|
select! { sleep_until(deadline) => 返回 true(阶段正常完成)
|
||||||
fault_tick.tick() => re-check fault/comm; if interrupted return false
|
fault_tick.tick() => 重新检查故障/通信;若中断返回 false
|
||||||
notify.notified() => re-check fault/comm; if interrupted return false }
|
notify.notified() => 重新检查故障/通信;若中断返回 false }
|
||||||
```
|
```
|
||||||
|
|
||||||
**Key invariants:**
|
**关键不变量:**
|
||||||
- `accumulated_run_sec` is updated by **exactly** `run_time_sec * 1000` per completed cycle (no delta drift).
|
- `accumulated_run_sec` 每个完成周期**精确**增加 `run_time_sec * 1000`(无 delta 漂移)。
|
||||||
- `display_acc_sec` is a snapshot copied from `accumulated_run_sec` only at Running→Stopped or Running→DistributorRunning transitions. Frontend always reads `display_acc_sec`.
|
- `display_acc_sec` 仅在 Running→Stopped 或 Running→DistributorRunning 转换时从 `accumulated_run_sec` 复制快照,前端始终读取 `display_acc_sec`。
|
||||||
- WS is pushed **only** when something changes. No periodic push.
|
- WS **仅在状态变化时**推送,无周期性推送。
|
||||||
- `unit.state_changed` events are fired (for logging) but **not** written to the DB event table (too frequent).
|
- `unit.state_changed` 事件仅用于日志记录,**不写入**数据库事件表(频率过高)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task 1: Extend UnitRuntime — ✅ DONE
|
## 任务一:扩展 UnitRuntime ✅ 已完成
|
||||||
|
|
||||||
**Files:** `src/control/runtime.rs`
|
**文件:** `src/control/runtime.rs`
|
||||||
|
|
||||||
Fields as shown in "Current UnitRuntime Shape" above. `ControlRuntimeStore` includes the `notifiers` map with `get_or_create_notify` and `notify_unit` methods.
|
字段如上方"UnitRuntime 结构体"所示。`ControlRuntimeStore` 包含 `notifiers` 映射,提供 `get_or_create_notify` 和 `notify_unit` 方法。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task 2: Create shared pulse-command helper — ✅ DONE
|
## 任务二:创建共享脉冲指令辅助函数 ✅ 已完成
|
||||||
|
|
||||||
**Files:** `src/control/command.rs`, `src/control/mod.rs`, `src/handler/control.rs`
|
**文件:** `src/control/command.rs`、`src/control/mod.rs`、`src/handler/control.rs`
|
||||||
|
|
||||||
`send_pulse_command(connection_manager, point_id, value_type, pulse_ms)` writes high→delay→low.
|
`send_pulse_command(connection_manager, point_id, value_type, pulse_ms)` 写入高→延迟→低电平序列。
|
||||||
`simulate_run_feedback(state, eq_id, running)` writes a fake run-feedback value in simulate mode.
|
|
||||||
|
`simulate_run_feedback(state, eq_id, running)` 在模拟模式下写入虚拟运行反馈值(用于无真实 OPC UA 设备时的调试)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task 3: Add runtime-state checks to validator.rs — ✅ DONE
|
## 任务三:在 validator.rs 添加运行时状态检查 ✅ 已完成
|
||||||
|
|
||||||
**Files:** `src/control/validator.rs`
|
**文件:** `src/control/validator.rs`
|
||||||
|
|
||||||
After existing REM/FLT/quality checks in `validate_manual_control`:
|
在 `validate_manual_control` 的现有 REM/FLT/quality 检查之后添加:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
if let Some(unit_id) = equipment.unit_id {
|
if let Some(unit_id) = equipment.unit_id {
|
||||||
if let Some(runtime) = state.control_runtime.get(unit_id).await {
|
if let Some(runtime) = state.control_runtime.get(unit_id).await {
|
||||||
if runtime.auto_enabled {
|
if runtime.auto_enabled {
|
||||||
return Err(ApiErr::Forbidden("Auto control is active; disable auto first", ...));
|
return Err(ApiErr::Forbidden("自动控制已激活,请先停止自动控制", ...));
|
||||||
}
|
}
|
||||||
if runtime.comm_locked {
|
if runtime.comm_locked {
|
||||||
return Err(ApiErr::Forbidden("Unit communication is locked", ...));
|
return Err(ApiErr::Forbidden("单元通信已锁定", ...));
|
||||||
}
|
}
|
||||||
if runtime.fault_locked {
|
if runtime.fault_locked {
|
||||||
return Err(ApiErr::Forbidden("Unit is fault locked", ...));
|
return Err(ApiErr::Forbidden("单元处于故障锁定状态", ...));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -144,11 +144,11 @@ if let Some(unit_id) = equipment.unit_id {
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task 4: Extend AppEvent with business events — ✅ DONE
|
## 任务四:扩展 AppEvent 业务事件 ✅ 已完成
|
||||||
|
|
||||||
**Files:** `src/event.rs`
|
**文件:** `src/event.rs`
|
||||||
|
|
||||||
7 variants added:
|
新增 7 个变体:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
AutoControlStarted { unit_id: Uuid },
|
AutoControlStarted { unit_id: Uuid },
|
||||||
|
|
@ -160,23 +160,23 @@ CommRecovered { unit_id: Uuid },
|
||||||
UnitStateChanged { unit_id: Uuid, from_state: String, to_state: String },
|
UnitStateChanged { unit_id: Uuid, from_state: String, to_state: String },
|
||||||
```
|
```
|
||||||
|
|
||||||
**`persist_event_if_needed` mapping:**
|
**`persist_event_if_needed` 映射:**
|
||||||
|
|
||||||
| Variant | DB? | event_type |
|
| 变体 | 写库? | event_type |
|
||||||
|---------|-----|-----------|
|
|------|--------|-----------|
|
||||||
| `AutoControlStarted` | ✅ | `unit.auto_control_started` |
|
| `AutoControlStarted` | ✅ | `unit.auto_control_started` |
|
||||||
| `AutoControlStopped` | ✅ | `unit.auto_control_stopped` |
|
| `AutoControlStopped` | ✅ | `unit.auto_control_stopped` |
|
||||||
| `FaultLocked` | ✅ | `unit.fault_locked` (level: error) |
|
| `FaultLocked` | ✅ | `unit.fault_locked`(level: error)|
|
||||||
| `FaultAcked` | ✅ | `unit.fault_acked` |
|
| `FaultAcked` | ✅ | `unit.fault_acked` |
|
||||||
| `CommLocked` | ✅ | `unit.comm_locked` (level: warn) |
|
| `CommLocked` | ✅ | `unit.comm_locked`(level: warn)|
|
||||||
| `CommRecovered` | ✅ | `unit.comm_recovered` |
|
| `CommRecovered` | ✅ | `unit.comm_recovered` |
|
||||||
| `UnitStateChanged` | ❌ | — (too frequent; fires every cycle) |
|
| `UnitStateChanged` | ❌ | —(频率过高,每周期触发)|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task 5: Add WsMessage::UnitRuntimeChanged — ✅ DONE
|
## 任务五:添加 WsMessage::UnitRuntimeChanged ✅ 已完成
|
||||||
|
|
||||||
**Files:** `src/websocket.rs`
|
**文件:** `src/websocket.rs`
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
UnitRuntimeChanged(crate::control::runtime::UnitRuntime),
|
UnitRuntimeChanged(crate::control::runtime::UnitRuntime),
|
||||||
|
|
@ -184,9 +184,9 @@ UnitRuntimeChanged(crate::control::runtime::UnitRuntime),
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task 6: Add service helpers — ✅ DONE
|
## 任务六:添加 service 辅助函数 ✅ 已完成
|
||||||
|
|
||||||
**Files:** `src/service/control.rs`
|
**文件:** `src/service/control.rs`
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
pub async fn get_all_enabled_units(pool: &PgPool) -> Result<Vec<ControlUnit>, sqlx::Error>
|
pub async fn get_all_enabled_units(pool: &PgPool) -> Result<Vec<ControlUnit>, sqlx::Error>
|
||||||
|
|
@ -195,26 +195,26 @@ pub async fn get_equipment_by_unit_id(pool: &PgPool, unit_id: Uuid) -> Result<Ve
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task 7: Implement control/engine.rs — ✅ DONE (event-driven, 2026-03-26)
|
## 任务七:实现 control/engine.rs ✅ 已完成(事件驱动,2026-03-26)
|
||||||
|
|
||||||
**Files:** `src/control/engine.rs`
|
**文件:** `src/control/engine.rs`
|
||||||
|
|
||||||
See "Engine Architecture" section above for the full design.
|
完整设计参见上方"引擎架构"章节。
|
||||||
|
|
||||||
**Critical rules for future modifications:**
|
**后续修改的关键规则:**
|
||||||
- Never push `WsMessage::UnitRuntimeChanged` except at state transitions or fault/comm changes.
|
- 除状态转换或故障/通信变化外,不得推送 `WsMessage::UnitRuntimeChanged`。
|
||||||
- `wait_phase` must use `sleep_until(deadline)` not `sleep(duration)` — the deadline is fixed when the phase starts so that fault-tick re-checks don't restart the timer.
|
- `wait_phase` 必须使用 `sleep_until(deadline)` 而非 `sleep(duration)`——deadline 在阶段开始时固定,故障 tick 重检不会重置计时器。
|
||||||
- When handling `notify.notified()` inside `wait_phase`, always re-read runtime from store (the handler may have changed `auto_enabled`).
|
- 在 `wait_phase` 内处理 `notify.notified()` 时,必须从 store 重新读取运行时(handler 可能已修改 `auto_enabled`)。
|
||||||
- Equipment maps are loaded once per task invocation; if equipment config changes, the supervisor will restart the task on its next scan (≤10s delay).
|
- 设备映射在每次任务调用时加载一次;若设备配置变更,监督器将在下次扫描(≤10秒)时重启该任务。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task 8: New API endpoints — ✅ DONE
|
## 任务八:新增 API 端点 ✅ 已完成
|
||||||
|
|
||||||
**Files:** `src/handler/control.rs`, `src/main.rs`
|
**文件:** `src/handler/control.rs`、`src/main.rs`
|
||||||
|
|
||||||
| Method | Path | Handler |
|
| 方法 | 路径 | Handler |
|
||||||
|--------|------|---------|
|
|------|------|---------|
|
||||||
| POST | `/api/unit/:id/start-auto` | `start_auto_unit` |
|
| POST | `/api/unit/:id/start-auto` | `start_auto_unit` |
|
||||||
| POST | `/api/unit/:id/stop-auto` | `stop_auto_unit` |
|
| POST | `/api/unit/:id/stop-auto` | `stop_auto_unit` |
|
||||||
| POST | `/api/unit/:id/ack-fault` | `ack_fault_unit` |
|
| POST | `/api/unit/:id/ack-fault` | `ack_fault_unit` |
|
||||||
|
|
@ -222,50 +222,51 @@ See "Engine Architecture" section above for the full design.
|
||||||
| POST | `/api/unit/batch-stop-auto` | `batch_stop_auto` |
|
| POST | `/api/unit/batch-stop-auto` | `batch_stop_auto` |
|
||||||
| GET | `/api/unit/:id/runtime` | `get_unit_runtime` |
|
| GET | `/api/unit/:id/runtime` | `get_unit_runtime` |
|
||||||
|
|
||||||
**Notify contract:** every handler that modifies `auto_enabled` or `fault_locked` MUST call `state.control_runtime.notify_unit(unit_id).await` after upserting the runtime. This wakes the sleeping unit task immediately.
|
**Notify 规约:** 每个修改 `auto_enabled` 或 `fault_locked` 的 handler,在 upsert 运行时后**必须**调用 `state.control_runtime.notify_unit(unit_id).await`,以立即唤醒休眠中的单元任务。
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Pattern to follow in every auto/fault handler:
|
// 每个 auto/fault handler 必须遵循此模式:
|
||||||
state.control_runtime.upsert(runtime).await;
|
state.control_runtime.upsert(runtime).await;
|
||||||
state.control_runtime.notify_unit(unit_id).await; // ← must not be omitted
|
state.control_runtime.notify_unit(unit_id).await; // ← 不可省略
|
||||||
let _ = state.event_manager.send(AppEvent::...);
|
let _ = state.event_manager.send(AppEvent::...);
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task 9: Frontend runtime integration — ✅ DONE
|
## 任务九:前端运行时集成 ✅ 已完成
|
||||||
|
|
||||||
**Files:** `web/js/state.js`, `web/js/units.js`, `web/js/ops.js`, `web/js/app.js`
|
**文件:** `web/js/state.js`、`web/js/units.js`、`web/js/ops.js`、`web/js/app.js`
|
||||||
|
|
||||||
**WS handler in app.js:**
|
**app.js 中的 WS 处理器:**
|
||||||
```js
|
```js
|
||||||
case "UnitRuntimeChanged":
|
case "UnitRuntimeChanged":
|
||||||
state.runtimes.set(payload.data.unit_id, payload.data);
|
state.runtimes.set(payload.data.unit_id, payload.data);
|
||||||
renderUnits(); // re-renders unit cards with new badge/buttons
|
renderUnits(); // 重新渲染单元卡片(更新徽章和按钮)
|
||||||
renderOpsUnits();
|
renderOpsUnits(); // 更新运维视图单元列表
|
||||||
|
syncEquipmentButtonsForUnit(runtime.unit_id, runtime.auto_enabled);
|
||||||
break;
|
break;
|
||||||
```
|
```
|
||||||
|
|
||||||
**Display rule:** Always use `runtime.display_acc_sec` for Acc display, never `runtime.accumulated_run_sec`.
|
**显示规则:** 前端始终使用 `runtime.display_acc_sec` 显示累计时间,不使用 `runtime.accumulated_run_sec`。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// ✅ Correct
|
// ✅ 正确
|
||||||
`Acc ${Math.floor(runtime.display_acc_sec / 1000)}s`
|
`Acc ${Math.floor(runtime.display_acc_sec / 1000)}s`
|
||||||
|
|
||||||
// ❌ Wrong — shows mid-cycle jitter values
|
// ❌ 错误——会显示周期中途的抖动值
|
||||||
`Acc ${Math.floor(runtime.accumulated_run_sec / 1000)}s`
|
`Acc ${Math.floor(runtime.accumulated_run_sec / 1000)}s`
|
||||||
```
|
```
|
||||||
|
|
||||||
**Event list CSS:** `.event-card` must have `flex-shrink: 0` (in `web/styles.css`) to prevent card height compression and text overlap when the flex-column list grows.
|
**事件列表 CSS:** `.event-card` 必须设置 `flex-shrink: 0`(在 `web/styles.css` 中),防止 flex 列高度压缩导致文字重叠。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Task 10: Connect engine to AppState — ✅ DONE
|
## 任务十:将引擎接入 AppState ✅ 已完成
|
||||||
|
|
||||||
**Files:** `src/main.rs`
|
**文件:** `src/main.rs`
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let control_runtime = Arc::new(control::runtime::ControlRuntimeStore::new());
|
let control_runtime = Arc::new(control::runtime::ControlRuntimeStore::new());
|
||||||
// ... build AppState ...
|
// ... 构建 AppState ...
|
||||||
control::engine::start(state.clone(), control_runtime);
|
control::engine::start(state.clone(), control_runtime);
|
||||||
```
|
```
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -16,7 +16,7 @@ function parseLogLine(line) {
|
||||||
try { return JSON.parse(trimmed); } catch { return null; }
|
try { return JSON.parse(trimmed); } catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appendLog(line) {
|
function appendLog(line) {
|
||||||
if (!dom.logView) return;
|
if (!dom.logView) return;
|
||||||
const atBottom = dom.logView.scrollTop + dom.logView.clientHeight >= dom.logView.scrollHeight - 10;
|
const atBottom = dom.logView.scrollTop + dom.logView.clientHeight >= dom.logView.scrollHeight - 10;
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { apiFetch } from "./api.js";
|
import { apiFetch } from "./api.js";
|
||||||
import { dom } from "./dom.js";
|
import { dom } from "./dom.js";
|
||||||
import { formatValue } from "./points.js";
|
|
||||||
import { state } from "./state.js";
|
import { state } from "./state.js";
|
||||||
import { loadUnits } from "./units.js";
|
import { loadUnits } from "./units.js";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export const state = {
|
||||||
apiDocLoaded: false,
|
apiDocLoaded: false,
|
||||||
runtimes: new Map(), // unit_id -> UnitRuntime
|
runtimes: new Map(), // unit_id -> UnitRuntime
|
||||||
activeView: "ops", // "ops" | "config"
|
activeView: "ops", // "ops" | "config"
|
||||||
opsPointEls: new Map(), // point_id -> { valueEl, qualityEl }
|
opsPointEls: new Map(), // point_id -> { dotEl }
|
||||||
logSource: null,
|
logSource: null,
|
||||||
selectedOpsUnitId: null,
|
selectedOpsUnitId: null,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue