diff --git a/src/control/command.rs b/src/control/command.rs index 743a06b..06fbb3b 100644 --- a/src/control/command.rs +++ b/src/control/command.rs @@ -44,8 +44,13 @@ pub async fn send_pulse_command( } /// Simulate RUN signal feedback after a command when SIMULATE_PLC=true. -/// Looks up the equipment's "run" role point and writes a synthetic Good-quality -/// monitor entry, then broadcasts PointNewValue over WebSocket. +/// +/// Strategy: +/// 1. Try writing the desired value to the RUN point via the normal OPC UA write path. +/// If the proxy accepts the write, `write_point_values_batch` will emit a local +/// `PointNewValue` event that updates the cache and WebSocket automatically. +/// 2. If the write is rejected (proxy has no write target or returns an error), +/// fall back to directly patching the local monitor cache and broadcasting over WS. pub async fn simulate_run_feedback(state: &AppState, equipment_id: Uuid, run_on: bool) { let role_points = match crate::service::get_equipment_role_points(&state.pool, equipment_id).await { @@ -60,6 +65,57 @@ pub async fn simulate_run_feedback(state: &AppState, equipment_id: Uuid, run_on: None => return, }; + // Determine the write value based on the current known value_type for the point. + let write_json = { + let guard = state + .connection_manager + .get_point_monitor_data_read_guard() + .await; + match guard + .get(&run_point.point_id) + .and_then(|m| m.value_type.as_ref()) + { + Some(crate::telemetry::ValueType::Int) | Some(crate::telemetry::ValueType::UInt) => { + serde_json::json!(if run_on { 1 } else { 0 }) + } + _ => serde_json::json!(run_on), + } + }; + + // Try writing to the proxy server first. + let write_ok = match state + .connection_manager + .write_point_values_batch(crate::connection::BatchSetPointValueReq { + items: vec![crate::connection::SetPointValueReqItem { + point_id: run_point.point_id, + value: write_json, + }], + }) + .await + { + Ok(res) => res.success, + Err(e) => { + tracing::debug!("simulate_run_feedback: write attempt failed: {}", e); + false + } + }; + + if write_ok { + // write_point_values_batch already emitted PointNewValue; nothing more to do. + tracing::info!( + "simulate_run_feedback: wrote run={} for equipment={} via OPC UA", + run_on, + equipment_id + ); + return; + } + + // Fallback: patch the local cache and push over WebSocket. + tracing::debug!( + "simulate_run_feedback: OPC UA write rejected, falling back to cache patch for equipment={}", + equipment_id + ); + let (value, value_type, value_text) = { let guard = state .connection_manager @@ -118,9 +174,9 @@ pub async fn simulate_run_feedback(state: &AppState, equipment_id: Uuid, run_on: .await; tracing::info!( - "simulate_run_feedback: equipment={} run={}", - equipment_id, - run_on + "simulate_run_feedback: cache-patched run={} for equipment={}", + run_on, + equipment_id ); }