plc_control/src/handler/control.rs

509 lines
15 KiB
Rust

use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use serde::Deserialize;
use serde_json::json;
use uuid::Uuid;
use validator::Validate;
use crate::{
control::validator::{validate_manual_control, ControlAction},
util::{
pagination::{PaginatedResponse, PaginationParams},
response::ApiErr,
},
AppState,
};
#[derive(Debug, Deserialize, Validate)]
pub struct GetUnitListQuery {
#[validate(length(min = 1, max = 100))]
pub keyword: Option<String>,
#[serde(flatten)]
pub pagination: PaginationParams,
}
pub async fn get_unit_list(
State(state): State<AppState>,
Query(query): Query<GetUnitListQuery>,
) -> Result<impl IntoResponse, ApiErr> {
query.validate()?;
let total = crate::service::get_units_count(&state.pool, query.keyword.as_deref()).await?;
let data = crate::service::get_units_paginated(
&state.pool,
query.keyword.as_deref(),
query.pagination.page_size,
query.pagination.offset(),
)
.await?;
Ok(Json(PaginatedResponse::new(
data,
total,
query.pagination.page,
query.pagination.page_size,
)))
}
pub async fn start_equipment(
State(state): State<AppState>,
Path(equipment_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiErr> {
send_equipment_command(state, equipment_id, ControlAction::Start).await
}
pub async fn stop_equipment(
State(state): State<AppState>,
Path(equipment_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiErr> {
send_equipment_command(state, equipment_id, ControlAction::Stop).await
}
async fn send_equipment_command(
state: AppState,
equipment_id: Uuid,
action: ControlAction,
) -> Result<impl IntoResponse, ApiErr> {
let context = validate_manual_control(&state, equipment_id, action).await?;
let pulse_ms = 300u64;
crate::control::command::send_pulse_command(
&state.connection_manager,
context.command_point.point_id,
context.command_value_type.as_ref(),
pulse_ms,
)
.await
.map_err(|e| ApiErr::Internal(e, None))?;
if state.config.simulate_plc {
crate::control::command::simulate_run_feedback(
&state,
equipment_id,
matches!(action, ControlAction::Start),
)
.await;
}
let event = match action {
ControlAction::Start => crate::event::AppEvent::EquipmentStartCommandSent {
equipment_id,
unit_id: context.unit_id,
point_id: context.command_point.point_id,
},
ControlAction::Stop => crate::event::AppEvent::EquipmentStopCommandSent {
equipment_id,
unit_id: context.unit_id,
point_id: context.command_point.point_id,
},
};
let _ = state.event_manager.send(event);
Ok(Json(json!({
"ok_msg": format!("Equipment {} command sent", action.as_str()),
"equipment_id": equipment_id,
"unit_id": context.unit_id,
"command_role": context.command_point.signal_role,
"command_point_id": context.command_point.point_id,
"pulse_ms": pulse_ms
})))
}
pub async fn get_unit(
State(state): State<AppState>,
Path(unit_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiErr> {
match crate::service::get_unit_by_id(&state.pool, unit_id).await? {
Some(unit) => Ok(Json(unit)),
None => Err(ApiErr::NotFound("Unit not found".to_string(), None)),
}
}
#[derive(serde::Serialize)]
pub struct PointDetail {
#[serde(flatten)]
pub point: crate::model::Point,
pub point_monitor: Option<crate::telemetry::PointMonitorInfo>,
}
#[derive(serde::Serialize)]
pub struct EquipmentDetail {
#[serde(flatten)]
pub equipment: crate::model::Equipment,
pub points: Vec<PointDetail>,
}
#[derive(serde::Serialize)]
pub struct UnitDetail {
#[serde(flatten)]
pub unit: crate::model::ControlUnit,
pub equipments: Vec<EquipmentDetail>,
}
pub async fn get_unit_detail(
State(state): State<AppState>,
Path(unit_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiErr> {
let unit = crate::service::get_unit_by_id(&state.pool, unit_id)
.await?
.ok_or_else(|| ApiErr::NotFound("Unit not found".to_string(), None))?;
let equipments = crate::service::get_equipment_by_unit_id(&state.pool, unit_id).await?;
let equipment_ids: Vec<Uuid> = equipments.iter().map(|e| e.id).collect();
let all_points = crate::service::get_points_by_equipment_ids(&state.pool, &equipment_ids).await?;
let monitor_guard = state
.connection_manager
.get_point_monitor_data_read_guard()
.await;
let equipments = equipments
.into_iter()
.map(|eq| {
let points = all_points
.iter()
.filter(|p| p.equipment_id == Some(eq.id))
.map(|p| PointDetail {
point_monitor: monitor_guard.get(&p.id).cloned(),
point: p.clone(),
})
.collect();
EquipmentDetail { equipment: eq, points }
})
.collect();
Ok(Json(UnitDetail { unit, equipments }))
}
#[derive(Debug, Deserialize, Validate)]
pub struct CreateUnitReq {
#[validate(length(min = 1, max = 100))]
pub code: String,
#[validate(length(min = 1, max = 100))]
pub name: String,
pub description: Option<String>,
pub enabled: Option<bool>,
#[validate(range(min = 0))]
pub run_time_sec: Option<i32>,
#[validate(range(min = 0))]
pub stop_time_sec: Option<i32>,
#[validate(range(min = 0))]
pub acc_time_sec: Option<i32>,
#[validate(range(min = 0))]
pub bl_time_sec: Option<i32>,
pub require_manual_ack_after_fault: Option<bool>,
}
pub async fn create_unit(
State(state): State<AppState>,
Json(payload): Json<CreateUnitReq>,
) -> Result<impl IntoResponse, ApiErr> {
payload.validate()?;
if crate::service::get_unit_by_code(&state.pool, &payload.code)
.await?
.is_some()
{
return Err(ApiErr::BadRequest(
"Unit code already exists".to_string(),
None,
));
}
let unit_id = crate::service::create_unit(
&state.pool,
crate::service::CreateUnitParams {
code: &payload.code,
name: &payload.name,
description: payload.description.as_deref(),
enabled: payload.enabled.unwrap_or(true),
run_time_sec: payload.run_time_sec.unwrap_or(0),
stop_time_sec: payload.stop_time_sec.unwrap_or(0),
acc_time_sec: payload.acc_time_sec.unwrap_or(0),
bl_time_sec: payload.bl_time_sec.unwrap_or(0),
require_manual_ack_after_fault: payload
.require_manual_ack_after_fault
.unwrap_or(true),
},
)
.await?;
Ok((
StatusCode::CREATED,
Json(serde_json::json!({
"id": unit_id,
"ok_msg": "Unit created successfully"
})),
))
}
#[derive(Debug, Deserialize, Validate)]
pub struct UpdateUnitReq {
#[validate(length(min = 1, max = 100))]
pub code: Option<String>,
#[validate(length(min = 1, max = 100))]
pub name: Option<String>,
pub description: Option<String>,
pub enabled: Option<bool>,
#[validate(range(min = 0))]
pub run_time_sec: Option<i32>,
#[validate(range(min = 0))]
pub stop_time_sec: Option<i32>,
#[validate(range(min = 0))]
pub acc_time_sec: Option<i32>,
#[validate(range(min = 0))]
pub bl_time_sec: Option<i32>,
pub require_manual_ack_after_fault: Option<bool>,
}
pub async fn update_unit(
State(state): State<AppState>,
Path(unit_id): Path<Uuid>,
Json(payload): Json<UpdateUnitReq>,
) -> Result<impl IntoResponse, ApiErr> {
payload.validate()?;
if crate::service::get_unit_by_id(&state.pool, unit_id)
.await?
.is_none()
{
return Err(ApiErr::NotFound("Unit not found".to_string(), None));
}
if let Some(code) = payload.code.as_deref() {
let duplicate = crate::service::get_unit_by_code(&state.pool, code).await?;
if duplicate.as_ref().is_some_and(|item| item.id != unit_id) {
return Err(ApiErr::BadRequest(
"Unit code already exists".to_string(),
None,
));
}
}
if payload.code.is_none()
&& payload.name.is_none()
&& payload.description.is_none()
&& payload.enabled.is_none()
&& payload.run_time_sec.is_none()
&& payload.stop_time_sec.is_none()
&& payload.acc_time_sec.is_none()
&& payload.bl_time_sec.is_none()
&& payload.require_manual_ack_after_fault.is_none()
{
return Ok(Json(serde_json::json!({"ok_msg": "No fields to update"})));
}
crate::service::update_unit(
&state.pool,
unit_id,
crate::service::UpdateUnitParams {
code: payload.code.as_deref(),
name: payload.name.as_deref(),
description: payload.description.as_deref(),
enabled: payload.enabled,
run_time_sec: payload.run_time_sec,
stop_time_sec: payload.stop_time_sec,
acc_time_sec: payload.acc_time_sec,
bl_time_sec: payload.bl_time_sec,
require_manual_ack_after_fault: payload.require_manual_ack_after_fault,
},
)
.await?;
Ok(Json(serde_json::json!({
"ok_msg": "Unit updated successfully"
})))
}
pub async fn delete_unit(
State(state): State<AppState>,
Path(unit_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiErr> {
let deleted = crate::service::delete_unit(&state.pool, unit_id).await?;
if !deleted {
return Err(ApiErr::NotFound("Unit not found".to_string(), None));
}
Ok(StatusCode::NO_CONTENT)
}
#[derive(Debug, Deserialize, Validate)]
pub struct GetEventListQuery {
pub unit_id: Option<Uuid>,
#[validate(length(min = 1, max = 100))]
pub event_type: Option<String>,
#[serde(flatten)]
pub pagination: PaginationParams,
}
pub async fn get_event_list(
State(state): State<AppState>,
Query(query): Query<GetEventListQuery>,
) -> Result<impl IntoResponse, ApiErr> {
query.validate()?;
let total = crate::service::get_events_count(
&state.pool,
query.unit_id,
query.event_type.as_deref(),
)
.await?;
let data = crate::service::get_events_paginated(
&state.pool,
query.unit_id,
query.event_type.as_deref(),
query.pagination.page_size,
query.pagination.offset(),
)
.await?;
Ok(Json(PaginatedResponse::new(
data,
total,
query.pagination.page,
query.pagination.page_size,
)))
}
pub async fn start_auto_unit(
State(state): State<AppState>,
Path(unit_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiErr> {
let unit = crate::service::get_unit_by_id(&state.pool, unit_id)
.await?
.ok_or_else(|| ApiErr::NotFound("Unit not found".to_string(), None))?;
if !unit.enabled {
return Err(ApiErr::BadRequest("Unit is disabled".to_string(), None));
}
let mut runtime = state.control_runtime.get_or_init(unit_id).await;
runtime.auto_enabled = true;
runtime.state = crate::control::runtime::UnitRuntimeState::Stopped;
runtime.current_stop_elapsed_sec = 0;
state.control_runtime.upsert(runtime).await;
let _ = state.event_manager.send(crate::event::AppEvent::AutoControlStarted { unit_id });
Ok(Json(json!({ "ok_msg": "Auto control started", "unit_id": unit_id })))
}
pub async fn stop_auto_unit(
State(state): State<AppState>,
Path(unit_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiErr> {
crate::service::get_unit_by_id(&state.pool, unit_id)
.await?
.ok_or_else(|| ApiErr::NotFound("Unit not found".to_string(), None))?;
let mut runtime = state.control_runtime.get_or_init(unit_id).await;
runtime.auto_enabled = false;
state.control_runtime.upsert(runtime).await;
let _ = state.event_manager.send(crate::event::AppEvent::AutoControlStopped { unit_id });
Ok(Json(json!({ "ok_msg": "Auto control stopped", "unit_id": unit_id })))
}
pub async fn batch_start_auto(
State(state): State<AppState>,
) -> Result<impl IntoResponse, ApiErr> {
let units = crate::service::get_all_enabled_units(&state.pool).await?;
let mut started = Vec::new();
let mut skipped = Vec::new();
for unit in units {
let mut runtime = state.control_runtime.get_or_init(unit.id).await;
if runtime.auto_enabled {
skipped.push(unit.id);
continue;
}
if runtime.fault_locked || runtime.comm_locked {
skipped.push(unit.id);
continue;
}
runtime.auto_enabled = true;
runtime.state = crate::control::runtime::UnitRuntimeState::Stopped;
runtime.current_stop_elapsed_sec = 0;
state.control_runtime.upsert(runtime).await;
let _ = state
.event_manager
.send(crate::event::AppEvent::AutoControlStarted { unit_id: unit.id });
started.push(unit.id);
}
Ok(Json(json!({ "started": started, "skipped": skipped })))
}
pub async fn batch_stop_auto(
State(state): State<AppState>,
) -> Result<impl IntoResponse, ApiErr> {
let units = crate::service::get_all_enabled_units(&state.pool).await?;
let mut stopped = Vec::new();
for unit in units {
let mut runtime = state.control_runtime.get_or_init(unit.id).await;
if !runtime.auto_enabled {
continue;
}
runtime.auto_enabled = false;
state.control_runtime.upsert(runtime).await;
let _ = state
.event_manager
.send(crate::event::AppEvent::AutoControlStopped { unit_id: unit.id });
stopped.push(unit.id);
}
Ok(Json(json!({ "stopped": stopped })))
}
pub async fn ack_fault_unit(
State(state): State<AppState>,
Path(unit_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiErr> {
crate::service::get_unit_by_id(&state.pool, unit_id)
.await?
.ok_or_else(|| ApiErr::NotFound("Unit not found".to_string(), None))?;
let mut runtime = state.control_runtime.get_or_init(unit_id).await;
if !runtime.fault_locked {
return Err(ApiErr::BadRequest(
"Unit is not fault locked".to_string(),
Some(json!({ "unit_id": unit_id })),
));
}
if runtime.flt_active {
return Err(ApiErr::BadRequest(
"FLT is still active, cannot acknowledge".to_string(),
Some(json!({ "unit_id": unit_id })),
));
}
runtime.fault_locked = false;
runtime.manual_ack_required = false;
runtime.state = crate::control::runtime::UnitRuntimeState::Stopped;
state.control_runtime.upsert(runtime).await;
let _ = state.event_manager.send(crate::event::AppEvent::FaultAcked { unit_id });
Ok(Json(json!({ "ok_msg": "Fault acknowledged", "unit_id": unit_id })))
}
pub async fn get_unit_runtime(
State(state): State<AppState>,
Path(unit_id): Path<Uuid>,
) -> Result<impl IntoResponse, ApiErr> {
crate::service::get_unit_by_id(&state.pool, unit_id)
.await?
.ok_or_else(|| ApiErr::NotFound("Unit not found".to_string(), None))?;
let runtime = state.control_runtime.get_or_init(unit_id).await;
Ok(Json(runtime))
}