feat(sim): simulate RUN signal feedback when SIMULATE_PLC=true
After a successful start/stop command, write run=true/false directly into the point monitor cache and broadcast PointNewValue via WebSocket. Gated by SIMULATE_PLC=true env var; real OPC-UA values override it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
36cfe9ecfc
commit
989a0286e9
|
|
@ -6,6 +6,9 @@ pub struct AppConfig {
|
||||||
pub server_host: String,
|
pub server_host: String,
|
||||||
pub server_port: u16,
|
pub server_port: u16,
|
||||||
pub write_api_key: Option<String>,
|
pub write_api_key: Option<String>,
|
||||||
|
/// When true, simulate RUN signal feedback after start/stop commands.
|
||||||
|
/// Set SIMULATE_PLC=true in .env for use with OPC UA proxy simulators.
|
||||||
|
pub simulate_plc: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -22,11 +25,16 @@ impl AppConfig {
|
||||||
.ok()
|
.ok()
|
||||||
.or_else(|| env::var("WRITE_KEY").ok());
|
.or_else(|| env::var("WRITE_KEY").ok());
|
||||||
|
|
||||||
|
let simulate_plc = env::var("SIMULATE_PLC")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_lowercase() == "true";
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
database_url,
|
database_url,
|
||||||
server_host,
|
server_host,
|
||||||
server_port,
|
server_port,
|
||||||
write_api_key,
|
write_api_key,
|
||||||
|
simulate_plc,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,86 @@ pub async fn stop_equipment(
|
||||||
send_equipment_command(state, equipment_id, ControlAction::Stop).await
|
send_equipment_command(state, equipment_id, ControlAction::Stop).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn simulate_run_feedback(state: &AppState, equipment_id: Uuid, action: ControlAction) {
|
||||||
|
let role_points =
|
||||||
|
match crate::service::get_equipment_role_points(&state.pool, equipment_id).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("simulate_run_feedback: db error: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let run_point = match role_points.iter().find(|p| p.signal_role == "run") {
|
||||||
|
Some(p) => p.clone(),
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let run_on = matches!(action, ControlAction::Start);
|
||||||
|
|
||||||
|
let (value, value_type, value_text) = {
|
||||||
|
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) => (
|
||||||
|
crate::telemetry::DataValue::Int(if run_on { 1 } else { 0 }),
|
||||||
|
Some(crate::telemetry::ValueType::Int),
|
||||||
|
Some(if run_on { "1" } else { "0" }.to_string()),
|
||||||
|
),
|
||||||
|
Some(crate::telemetry::ValueType::UInt) => (
|
||||||
|
crate::telemetry::DataValue::UInt(if run_on { 1 } else { 0 }),
|
||||||
|
Some(crate::telemetry::ValueType::UInt),
|
||||||
|
Some(if run_on { "1" } else { "0" }.to_string()),
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
crate::telemetry::DataValue::Bool(run_on),
|
||||||
|
Some(crate::telemetry::ValueType::Bool),
|
||||||
|
Some(run_on.to_string()),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let monitor = crate::telemetry::PointMonitorInfo {
|
||||||
|
protocol: "simulation".to_string(),
|
||||||
|
source_id: Uuid::nil(),
|
||||||
|
point_id: run_point.point_id,
|
||||||
|
client_handle: 0,
|
||||||
|
scan_mode: crate::model::ScanMode::Poll,
|
||||||
|
timestamp: Some(chrono::Utc::now()),
|
||||||
|
quality: crate::telemetry::PointQuality::Good,
|
||||||
|
value: Some(value),
|
||||||
|
value_type,
|
||||||
|
value_text,
|
||||||
|
old_value: None,
|
||||||
|
old_timestamp: None,
|
||||||
|
value_changed: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = state
|
||||||
|
.connection_manager
|
||||||
|
.update_point_monitor_data(monitor.clone())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::warn!("simulate_run_feedback: cache update failed: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = state
|
||||||
|
.ws_manager
|
||||||
|
.send_to_public(crate::websocket::WsMessage::PointNewValue(monitor))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"simulate_run_feedback: equipment={} run={}",
|
||||||
|
equipment_id,
|
||||||
|
run_on
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_equipment_command(
|
async fn send_equipment_command(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
equipment_id: Uuid,
|
equipment_id: Uuid,
|
||||||
|
|
@ -80,6 +160,10 @@ async fn send_equipment_command(
|
||||||
.await
|
.await
|
||||||
.map_err(|e| ApiErr::Internal(e, None))?;
|
.map_err(|e| ApiErr::Internal(e, None))?;
|
||||||
|
|
||||||
|
if state.config.simulate_plc {
|
||||||
|
simulate_run_feedback(&state, equipment_id, action).await;
|
||||||
|
}
|
||||||
|
|
||||||
let event = match action {
|
let event = match action {
|
||||||
ControlAction::Start => crate::event::AppEvent::EquipmentStartCommandSent {
|
ControlAction::Start => crate::event::AppEvent::EquipmentStartCommandSent {
|
||||||
equipment_id,
|
equipment_id,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue