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 <noreply@anthropic.com>
This commit is contained in:
caoqianming 2026-03-27 10:30:43 +08:00
parent 1354f89204
commit 4227747852
6 changed files with 40 additions and 5 deletions

View File

@ -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 = (

View File

@ -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,
}
}
}

View File

@ -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));

View File

@ -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" })

View File

@ -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();

View File

@ -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();