260 lines
8.0 KiB
Rust
260 lines
8.0 KiB
Rust
use std::sync::Arc;
|
|
|
|
use plc_platform_core::{
|
|
event::{record_event, EventInsert, MetadataCache},
|
|
websocket::WebSocketManager,
|
|
};
|
|
use tokio::sync::mpsc;
|
|
use uuid::Uuid;
|
|
|
|
const CONTROL_EVENT_CHANNEL_CAPACITY: usize = 1024;
|
|
|
|
/// Operation-system business events.
|
|
///
|
|
/// Each variant maps to a row in the `event` table (via `record_event`) and
|
|
/// follows the `ops.*` namespace from design doc §8.1. Every record carries
|
|
/// `subject_type` + `subject_id` so the front-end can filter the timeline
|
|
/// for one segment / station without joining on event_type strings.
|
|
#[derive(Debug, Clone)]
|
|
pub enum AppEvent {
|
|
SegmentAutoStarted {
|
|
segment_id: Uuid,
|
|
},
|
|
SegmentAutoStopped {
|
|
segment_id: Uuid,
|
|
},
|
|
SegmentStepAdvanced {
|
|
segment_id: Uuid,
|
|
step_no: i32,
|
|
},
|
|
SegmentCompleted {
|
|
segment_id: Uuid,
|
|
},
|
|
SegmentBlocked {
|
|
segment_id: Uuid,
|
|
reason: String,
|
|
},
|
|
SegmentFaultLocked {
|
|
segment_id: Uuid,
|
|
message: String,
|
|
},
|
|
SegmentFaultAcked {
|
|
segment_id: Uuid,
|
|
},
|
|
SegmentCommLocked {
|
|
segment_id: Uuid,
|
|
},
|
|
SegmentCommRecovered {
|
|
segment_id: Uuid,
|
|
},
|
|
StationStateChanged {
|
|
station_id: Uuid,
|
|
presence: bool,
|
|
vacancy: bool,
|
|
},
|
|
AlarmActionTimeout {
|
|
segment_id: Uuid,
|
|
step_no: i32,
|
|
},
|
|
AlarmSignalConflict {
|
|
segment_id: Uuid,
|
|
message: String,
|
|
},
|
|
AlarmResourceBusy {
|
|
segment_id: Uuid,
|
|
resource_key: String,
|
|
},
|
|
}
|
|
|
|
pub struct EventManager {
|
|
sender: mpsc::Sender<AppEvent>,
|
|
}
|
|
|
|
impl EventManager {
|
|
pub fn new(
|
|
pool: sqlx::PgPool,
|
|
ws_manager: Option<Arc<WebSocketManager>>,
|
|
metadata: Arc<MetadataCache>,
|
|
) -> Self {
|
|
let (sender, mut receiver) = mpsc::channel::<AppEvent>(CONTROL_EVENT_CHANNEL_CAPACITY);
|
|
|
|
let pool_for_task = pool.clone();
|
|
let ws_for_task = ws_manager.clone();
|
|
tokio::spawn(async move {
|
|
while let Some(event) = receiver.recv().await {
|
|
handle_event(event, &pool_for_task, ws_for_task.as_ref(), &metadata).await;
|
|
}
|
|
});
|
|
|
|
Self { sender }
|
|
}
|
|
|
|
pub fn send(&self, event: AppEvent) -> Result<(), String> {
|
|
match self.sender.try_send(event) {
|
|
Ok(()) => Ok(()),
|
|
Err(mpsc::error::TrySendError::Closed(e)) => {
|
|
Err(format!("ops event channel closed ({e:?})"))
|
|
}
|
|
Err(mpsc::error::TrySendError::Full(e)) => Err(format!("ops event queue full ({e:?})")),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn segment_event(
|
|
event_type: &'static str,
|
|
level: &'static str,
|
|
segment_id: Uuid,
|
|
message: String,
|
|
payload: serde_json::Value,
|
|
) -> EventInsert {
|
|
EventInsert {
|
|
event_type,
|
|
level,
|
|
unit_id: None,
|
|
equipment_id: None,
|
|
source_id: None,
|
|
subject_type: Some("segment"),
|
|
subject_id: Some(segment_id),
|
|
message,
|
|
payload,
|
|
}
|
|
}
|
|
|
|
async fn handle_event(
|
|
event: AppEvent,
|
|
pool: &sqlx::PgPool,
|
|
ws_manager: Option<&Arc<WebSocketManager>>,
|
|
_metadata: &MetadataCache,
|
|
) {
|
|
let record: Option<EventInsert> = match &event {
|
|
AppEvent::SegmentAutoStarted { segment_id } => Some(segment_event(
|
|
"ops.segment.auto_started",
|
|
"info",
|
|
*segment_id,
|
|
format!("Segment {} auto control started", segment_id),
|
|
serde_json::json!({ "segment_id": segment_id }),
|
|
)),
|
|
AppEvent::SegmentAutoStopped { segment_id } => Some(segment_event(
|
|
"ops.segment.auto_stopped",
|
|
"info",
|
|
*segment_id,
|
|
format!("Segment {} auto control stopped", segment_id),
|
|
serde_json::json!({ "segment_id": segment_id }),
|
|
)),
|
|
AppEvent::SegmentStepAdvanced {
|
|
segment_id,
|
|
step_no,
|
|
} => Some(segment_event(
|
|
"ops.segment.step_advanced",
|
|
"info",
|
|
*segment_id,
|
|
format!("Segment {} advanced to step {}", segment_id, step_no),
|
|
serde_json::json!({ "segment_id": segment_id, "step_no": step_no }),
|
|
)),
|
|
AppEvent::SegmentCompleted { segment_id } => Some(segment_event(
|
|
"ops.segment.completed",
|
|
"info",
|
|
*segment_id,
|
|
format!("Segment {} completed", segment_id),
|
|
serde_json::json!({ "segment_id": segment_id }),
|
|
)),
|
|
AppEvent::SegmentBlocked { segment_id, reason } => Some(segment_event(
|
|
"ops.segment.blocked",
|
|
"warn",
|
|
*segment_id,
|
|
format!("Segment {} blocked: {}", segment_id, reason),
|
|
serde_json::json!({ "segment_id": segment_id, "reason": reason }),
|
|
)),
|
|
AppEvent::SegmentFaultLocked {
|
|
segment_id,
|
|
message,
|
|
} => Some(segment_event(
|
|
"ops.segment.fault_locked",
|
|
"error",
|
|
*segment_id,
|
|
format!("Segment {} fault locked: {}", segment_id, message),
|
|
serde_json::json!({ "segment_id": segment_id, "message": message }),
|
|
)),
|
|
AppEvent::SegmentFaultAcked { segment_id } => Some(segment_event(
|
|
"ops.segment.fault_acked",
|
|
"info",
|
|
*segment_id,
|
|
format!("Segment {} fault acknowledged", segment_id),
|
|
serde_json::json!({ "segment_id": segment_id }),
|
|
)),
|
|
AppEvent::SegmentCommLocked { segment_id } => Some(segment_event(
|
|
"ops.segment.comm_locked",
|
|
"warn",
|
|
*segment_id,
|
|
format!("Segment {} communication locked", segment_id),
|
|
serde_json::json!({ "segment_id": segment_id }),
|
|
)),
|
|
AppEvent::SegmentCommRecovered { segment_id } => Some(segment_event(
|
|
"ops.segment.comm_recovered",
|
|
"info",
|
|
*segment_id,
|
|
format!("Segment {} communication recovered", segment_id),
|
|
serde_json::json!({ "segment_id": segment_id }),
|
|
)),
|
|
AppEvent::StationStateChanged {
|
|
station_id,
|
|
presence,
|
|
vacancy,
|
|
} => Some(EventInsert {
|
|
event_type: "ops.station.state_changed",
|
|
level: "info",
|
|
unit_id: None,
|
|
equipment_id: None,
|
|
source_id: None,
|
|
subject_type: Some("station"),
|
|
subject_id: Some(*station_id),
|
|
message: format!(
|
|
"Station {} state changed (presence={}, vacancy={})",
|
|
station_id, presence, vacancy
|
|
),
|
|
payload: serde_json::json!({
|
|
"station_id": station_id,
|
|
"presence": presence,
|
|
"vacancy": vacancy
|
|
}),
|
|
}),
|
|
AppEvent::AlarmActionTimeout {
|
|
segment_id,
|
|
step_no,
|
|
} => Some(segment_event(
|
|
"ops.alarm.action_timeout",
|
|
"error",
|
|
*segment_id,
|
|
format!("Action timeout on segment {} step {}", segment_id, step_no),
|
|
serde_json::json!({ "segment_id": segment_id, "step_no": step_no }),
|
|
)),
|
|
AppEvent::AlarmSignalConflict {
|
|
segment_id,
|
|
message,
|
|
} => Some(segment_event(
|
|
"ops.alarm.signal_conflict",
|
|
"error",
|
|
*segment_id,
|
|
format!("Signal conflict on segment {}: {}", segment_id, message),
|
|
serde_json::json!({ "segment_id": segment_id, "message": message }),
|
|
)),
|
|
AppEvent::AlarmResourceBusy {
|
|
segment_id,
|
|
resource_key,
|
|
} => Some(segment_event(
|
|
"ops.alarm.resource_busy",
|
|
"warn",
|
|
*segment_id,
|
|
format!("Resource {} busy for segment {}", resource_key, segment_id),
|
|
serde_json::json!({
|
|
"segment_id": segment_id,
|
|
"resource_key": resource_key
|
|
}),
|
|
)),
|
|
};
|
|
|
|
if let Some(record) = record {
|
|
record_event(pool, ws_manager.map(Arc::as_ref), record).await;
|
|
}
|
|
}
|