From 422774785210e30d637953c0f3ee1d27ce8fc621 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Fri, 27 Mar 2026 10:30:43 +0800 Subject: [PATCH] feat(control): stop auto-control and disable buttons when REM goes local - Add `rem_local: bool` to UnitRuntime; set true when any equipment's REM signal is false with good quality - Engine check_fault_comm: stop auto-control and fire AutoControlStopped when any equipment switches to local mode - Block start-auto when rem_local (backend + error message) - Frontend: disable Start Auto button in units/ops views when rem_local - Frontend: disable equipment Start/Stop buttons in config view when unit's rem_local is true Co-Authored-By: Claude Sonnet 4.6 --- src/control/engine.rs | 18 ++++++++++++++++++ src/control/runtime.rs | 3 +++ src/handler/control.rs | 5 ++++- web/js/equipment.js | 7 +++++++ web/js/ops.js | 6 ++++-- web/js/units.js | 6 ++++-- 6 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/control/engine.rs b/src/control/engine.rs index 1824460..e2d5de0 100644 --- a/src/control/engine.rs +++ b/src/control/engine.rs @@ -317,6 +317,15 @@ async fn check_fault_comm( None }; + // REM local: any equipment with a rem point that is explicitly false (local mode) with good quality. + let any_rem_local = all_roles.iter().any(|(_, roles)| { + roles + .get("rem") + .and_then(|rp| monitor.get(&rp.point_id)) + .map(|m| !super::monitor_value_as_bool(m) && m.quality == PointQuality::Good) + .unwrap_or(false) + }); + drop(monitor); let prev_comm = runtime.comm_locked; @@ -324,9 +333,11 @@ async fn check_fault_comm( let prev_fault_locked = runtime.fault_locked; let prev_auto = runtime.auto_enabled; let prev_ack = runtime.manual_ack_required; + let prev_rem_local = runtime.rem_local; runtime.comm_locked = any_bad; runtime.flt_active = any_flt; + runtime.rem_local = any_rem_local; if !prev_comm && runtime.comm_locked { let _ = state.event_manager.send(AppEvent::CommLocked { unit_id: unit.id }); @@ -351,11 +362,18 @@ async fn check_fault_comm( } } + // Stop auto-control when any equipment switches to local mode. + if any_rem_local && runtime.auto_enabled { + runtime.auto_enabled = false; + let _ = state.event_manager.send(AppEvent::AutoControlStopped { unit_id: unit.id }); + } + runtime.comm_locked != prev_comm || runtime.flt_active != prev_flt || runtime.fault_locked != prev_fault_locked || runtime.auto_enabled != prev_auto || runtime.manual_ack_required != prev_ack + || runtime.rem_local != prev_rem_local } type EquipMaps = ( diff --git a/src/control/runtime.rs b/src/control/runtime.rs index 80e710e..6a9d26a 100644 --- a/src/control/runtime.rs +++ b/src/control/runtime.rs @@ -25,6 +25,8 @@ pub struct UnitRuntime { pub flt_active: bool, pub comm_locked: bool, pub manual_ack_required: bool, + /// True when any equipment in the unit has REM=false (local mode) with good signal quality. + pub rem_local: bool, } impl UnitRuntime { @@ -39,6 +41,7 @@ impl UnitRuntime { flt_active: false, comm_locked: false, manual_ack_required: false, + rem_local: false, } } } diff --git a/src/handler/control.rs b/src/handler/control.rs index 2a84987..ab85448 100644 --- a/src/handler/control.rs +++ b/src/handler/control.rs @@ -36,7 +36,7 @@ fn validate_unit_timing_order( } fn auto_control_start_blocked(runtime: &crate::control::runtime::UnitRuntime) -> bool { - runtime.fault_locked || runtime.comm_locked || runtime.manual_ack_required + runtime.fault_locked || runtime.comm_locked || runtime.manual_ack_required || runtime.rem_local } #[derive(Debug, Deserialize, Validate)] @@ -543,6 +543,8 @@ pub async fn start_auto_unit( "Unit is fault locked, cannot start auto control" } else if runtime.comm_locked { "Unit communication is locked, cannot start auto control" + } else if runtime.rem_local { + "Equipment is in local mode (REM off), cannot start auto control" } else { "Fault acknowledgement required before starting auto control" }; @@ -740,6 +742,7 @@ mod tests { flt_active: false, comm_locked: true, manual_ack_required: false, + rem_local: false, }; assert!(auto_control_start_blocked(&runtime)); diff --git a/web/js/equipment.js b/web/js/equipment.js index 336842d..e6e8d14 100644 --- a/web/js/equipment.js +++ b/web/js/equipment.js @@ -208,9 +208,14 @@ export function renderEquipments() { actionRow.append(editBtn, deleteBtn); if (equipment.kind === "coal_feeder" || equipment.kind === "distributor") { + const unitRuntime = equipment.unit_id ? state.runtimes.get(equipment.unit_id) : null; + const remLocal = unitRuntime?.rem_local ?? false; + const startBtn = document.createElement("button"); startBtn.className = "secondary"; startBtn.textContent = "Start"; + startBtn.disabled = remLocal; + startBtn.title = remLocal ? "设备处于本地模式(REM关)" : ""; startBtn.addEventListener("click", (e) => { e.stopPropagation(); apiFetch(`/api/control/equipment/${equipment.id}/start`, { method: "POST" }) @@ -220,6 +225,8 @@ export function renderEquipments() { const stopBtn = document.createElement("button"); stopBtn.className = "danger"; stopBtn.textContent = "Stop"; + stopBtn.disabled = remLocal; + stopBtn.title = remLocal ? "设备处于本地模式(REM关)" : ""; stopBtn.addEventListener("click", (e) => { e.stopPropagation(); apiFetch(`/api/control/equipment/${equipment.id}/stop`, { method: "POST" }) diff --git a/web/js/ops.js b/web/js/ops.js index a93d069..83fe9b4 100644 --- a/web/js/ops.js +++ b/web/js/ops.js @@ -55,13 +55,15 @@ export function renderOpsUnits() { const actions = item.querySelector(".ops-unit-item-actions"); const isAutoOn = runtime?.auto_enabled; - const startBlocked = !isAutoOn && (runtime?.fault_locked || runtime?.manual_ack_required); + const startBlocked = !isAutoOn && (runtime?.fault_locked || runtime?.manual_ack_required || runtime?.rem_local); const autoBtn = document.createElement("button"); autoBtn.className = isAutoOn ? "danger" : "secondary"; autoBtn.textContent = isAutoOn ? "Stop Auto" : "Start Auto"; autoBtn.disabled = startBlocked; autoBtn.title = startBlocked - ? (runtime?.fault_locked ? "设备故障中,无法启动自动控制" : "需人工确认故障后才可启动自动控制") + ? (runtime?.fault_locked ? "设备故障中,无法启动自动控制" + : runtime?.rem_local ? "设备处于本地模式(REM关),无法启动自动控制" + : "需人工确认故障后才可启动自动控制") : (isAutoOn ? "停止自动控制" : "启动自动控制"); autoBtn.addEventListener("click", (e) => { e.stopPropagation(); diff --git a/web/js/units.js b/web/js/units.js index 6c87bd0..3de467d 100644 --- a/web/js/units.js +++ b/web/js/units.js @@ -151,13 +151,15 @@ export function renderUnits() { actions.append(editBtn, deleteBtn); const isAutoOn = runtime?.auto_enabled; - const startBlocked = !isAutoOn && (runtime?.fault_locked || runtime?.manual_ack_required); + const startBlocked = !isAutoOn && (runtime?.fault_locked || runtime?.manual_ack_required || runtime?.rem_local); const autoBtn = document.createElement("button"); autoBtn.className = isAutoOn ? "danger" : "secondary"; autoBtn.textContent = isAutoOn ? "Stop Auto" : "Start Auto"; autoBtn.disabled = startBlocked; autoBtn.title = startBlocked - ? (runtime?.fault_locked ? "设备故障中,无法启动自动控制" : "需人工确认故障后才可启动自动控制") + ? (runtime?.fault_locked ? "设备故障中,无法启动自动控制" + : runtime?.rem_local ? "设备处于本地模式(REM关),无法启动自动控制" + : "需人工确认故障后才可启动自动控制") : (isAutoOn ? "停止自动控制" : "启动自动控制"); autoBtn.addEventListener("click", (e) => { e.stopPropagation();