317 lines
13 KiB
Python
317 lines
13 KiB
Python
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
from typing import Optional, List
|
|
import logging
|
|
import math
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
logger = logging.getLogger("equipment_router")
|
|
router = APIRouter(prefix="/equipment", tags=["equipment"])
|
|
|
|
class EquipInfo(BaseModel):
|
|
equip_id: str
|
|
equip_name: str
|
|
equip_type: str
|
|
description: Optional[str] = ""
|
|
|
|
class ControlRequestM(BaseModel):
|
|
bit: Optional[int] = None
|
|
value: int
|
|
|
|
from .db_manager import (
|
|
get_all_equipment_from_db,
|
|
get_equipment_details_from_db,
|
|
get_all_machine_statuses_joined,
|
|
get_machine_status_detail_joined,
|
|
save_control_command
|
|
)
|
|
|
|
def update_m_write_in_db(equipment_id: int, payload: list):
|
|
try:
|
|
from database.connection import get_db_connection
|
|
import json
|
|
with get_db_connection() as conn:
|
|
with conn.cursor(dictionary=True) as cursor:
|
|
cursor.execute("SELECT m_write_raw FROM machine_status WHERE equipment_id = %s", (equipment_id,))
|
|
row = cursor.fetchone()
|
|
m_write = {}
|
|
if row and row.get("m_write_raw"):
|
|
try:
|
|
m_write = json.loads(row["m_write_raw"])
|
|
except:
|
|
pass
|
|
for item in payload:
|
|
if len(item) >= 2:
|
|
m_write[str(item[0])] = int(item[1])
|
|
cursor.execute(
|
|
"UPDATE machine_status SET m_write_raw = %s WHERE equipment_id = %s",
|
|
(json.dumps(m_write), equipment_id)
|
|
)
|
|
conn.commit()
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"[DB Error in update_m_write_in_db]: {e}")
|
|
return False
|
|
|
|
def update_d_write_in_db(equipment_id: int, payload: list):
|
|
try:
|
|
from database.connection import get_db_connection
|
|
with get_db_connection() as conn:
|
|
with conn.cursor() as cursor:
|
|
for item in payload:
|
|
if len(item) >= 2:
|
|
group_id = int(item[0])
|
|
val = float(item[1])
|
|
if 0 <= group_id <= 3:
|
|
cursor.execute(
|
|
f"UPDATE machine_status SET d_write_g{group_id} = %s WHERE equipment_id = %s",
|
|
(val, equipment_id)
|
|
)
|
|
conn.commit()
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"[DB Error in update_d_write_in_db]: {e}")
|
|
return False
|
|
|
|
def _get_line_prefix_by_id(equipment_id: int) -> str:
|
|
if equipment_id == 8:
|
|
return "line1"
|
|
return f"line{equipment_id}"
|
|
|
|
|
|
def _ensure_equipment_is_ready(equipment_id: int):
|
|
status = get_machine_status_detail_joined(equipment_id)
|
|
if not status:
|
|
raise HTTPException(status_code=404, detail="Machine status detail not found")
|
|
if status.get("sync_state") == "SYNCING" or status.get("device_state") != "ONLINE":
|
|
raise HTTPException(status_code=423, detail="Equipment initialization is in progress")
|
|
|
|
|
|
@router.get("", response_model=List[dict])
|
|
def list_equipment_root():
|
|
return get_all_equipment_from_db()
|
|
|
|
|
|
@router.get("/status/all", response_model=List[dict])
|
|
def list_machine_statuses():
|
|
return get_all_machine_statuses_joined()
|
|
|
|
|
|
@router.get("/monitoring/metrics", response_model=dict)
|
|
def get_performance_metrics():
|
|
from monitoring.metrics import get_metrics_summary
|
|
return get_metrics_summary()
|
|
|
|
|
|
@router.get("/{equipment_id}", response_model=dict)
|
|
def get_equipment_details(equipment_id: int):
|
|
details = get_equipment_details_from_db(equipment_id)
|
|
if not details:
|
|
raise HTTPException(status_code=404, detail="Equipment not found")
|
|
return details
|
|
|
|
|
|
@router.get("/{equipment_id}/status", response_model=dict)
|
|
def get_machine_status_detail(equipment_id: int):
|
|
status = get_machine_status_detail_joined(equipment_id)
|
|
if not status:
|
|
raise HTTPException(status_code=404, detail="Machine status detail not found")
|
|
return status
|
|
|
|
|
|
@router.post("/{equipment_id}/control/m", response_model=dict, status_code=202)
|
|
def send_momentary_control_m(equipment_id: int, payload: List[List[int]]):
|
|
from communication.mqtt_client import _client
|
|
import json
|
|
|
|
logger.info(f"[REST API WRITE M] Started. equipment_id: {equipment_id} | Payload: {payload}")
|
|
_ensure_equipment_is_ready(equipment_id)
|
|
|
|
if not payload:
|
|
raise HTTPException(status_code=400, detail="Empty payload not allowed")
|
|
|
|
seen_ids = set()
|
|
for item in payload:
|
|
if len(item) < 2:
|
|
raise HTTPException(status_code=400, detail="Invalid payload format: each item must be [id, value]")
|
|
bit_id, val = item[0], item[1]
|
|
if bit_id in seen_ids:
|
|
raise HTTPException(status_code=400, detail=f"Duplicate bit ID in payload: {bit_id}")
|
|
seen_ids.add(bit_id)
|
|
if not (0 <= bit_id <= 187):
|
|
raise HTTPException(status_code=400, detail=f"M-bit ID {bit_id} out of bounds (0~187)")
|
|
if val not in (0, 1):
|
|
raise HTTPException(status_code=400, detail=f"Invalid M-bit value {val} (must be 0 or 1)")
|
|
|
|
if _client is None or not _client.is_connected():
|
|
logger.error("[REST API WRITE M ERROR] MQTT client is offline")
|
|
raise HTTPException(status_code=503, detail="MQTT broker connection is offline")
|
|
|
|
command_id = str(uuid.uuid4())
|
|
|
|
db_ok = save_control_command(equipment_id, command_id, "m_write", payload, "ACCEPTED")
|
|
if not db_ok:
|
|
logger.error("[REST API WRITE M ERROR] Failed to save control command in database")
|
|
raise HTTPException(status_code=500, detail="Database transaction failed (Command not queued)")
|
|
|
|
ok = update_m_write_in_db(equipment_id, payload)
|
|
if not ok:
|
|
logger.error("[REST API WRITE M ERROR] Failed to update machine status write buffer in database")
|
|
from .db_manager import update_control_command_status
|
|
update_control_command_status(command_id, "FAILED")
|
|
raise HTTPException(status_code=500, detail="Database write buffer update failed")
|
|
|
|
prefix = _get_line_prefix_by_id(equipment_id)
|
|
topic = f"/{prefix}/m/out"
|
|
|
|
envelope = {
|
|
"command_id": command_id,
|
|
"type": "m_write",
|
|
"items": payload,
|
|
"requested_at": datetime.utcnow().isoformat()
|
|
}
|
|
|
|
try:
|
|
msg_payload = json.dumps(envelope)
|
|
result = _client.publish(topic, msg_payload, qos=1)
|
|
if result.rc != 0:
|
|
raise Exception(f"Paho MQTT publish returned error code: {result.rc}")
|
|
logger.info(f"[REST API WRITE M SUCCESS] Published to topic: {topic} | Payload: {msg_payload}")
|
|
except Exception as e:
|
|
logger.error(f"[REST API WRITE M MQTT ERROR] Publish failed: {e}", exc_info=True)
|
|
from .db_manager import update_control_command_status
|
|
update_control_command_status(command_id, "FAILED")
|
|
raise HTTPException(status_code=500, detail=f"MQTT publish failed: {e}")
|
|
|
|
return {
|
|
"status": "accepted",
|
|
"command_id": command_id,
|
|
"message": "Command queued, ACK pending"
|
|
}
|
|
|
|
|
|
@router.post("/{equipment_id}/control/d", response_model=dict, status_code=202)
|
|
def send_register_control_d(equipment_id: int, payload: List[List[int]]):
|
|
from communication.mqtt_client import _client
|
|
import json
|
|
|
|
logger.info(f"[REST API WRITE D] Started. equipment_id: {equipment_id} | Payload: {payload}")
|
|
_ensure_equipment_is_ready(equipment_id)
|
|
|
|
if not payload:
|
|
raise HTTPException(status_code=400, detail="Empty payload not allowed")
|
|
|
|
float_payload = []
|
|
seen_ids = set()
|
|
for item in payload:
|
|
if len(item) < 2:
|
|
raise HTTPException(status_code=400, detail="Invalid payload format: each item must be [id, value]")
|
|
group_id, val = int(item[0]), float(item[1])
|
|
if group_id in seen_ids:
|
|
raise HTTPException(status_code=400, detail=f"Duplicate group ID in payload: {group_id}")
|
|
seen_ids.add(group_id)
|
|
if not (0 <= group_id <= 3):
|
|
raise HTTPException(status_code=400, detail=f"D-group ID {group_id} out of bounds (0~3)")
|
|
if not math.isfinite(val):
|
|
raise HTTPException(status_code=400, detail=f"Invalid REAL value {val}")
|
|
float_payload.append([group_id, val])
|
|
|
|
if _client is None or not _client.is_connected():
|
|
logger.error("[REST API WRITE D ERROR] MQTT client is offline")
|
|
raise HTTPException(status_code=503, detail="MQTT broker connection is offline")
|
|
|
|
command_id = str(uuid.uuid4())
|
|
|
|
db_ok = save_control_command(equipment_id, command_id, "d_write", float_payload, "ACCEPTED")
|
|
if not db_ok:
|
|
logger.error("[REST API WRITE D ERROR] Failed to save control command in database")
|
|
raise HTTPException(status_code=500, detail="Database transaction failed (Command not queued)")
|
|
|
|
ok = update_d_write_in_db(equipment_id, float_payload)
|
|
if not ok:
|
|
logger.error("[REST API WRITE D ERROR] Failed to update machine status write buffer in database")
|
|
from .db_manager import update_control_command_status
|
|
update_control_command_status(command_id, "FAILED")
|
|
raise HTTPException(status_code=500, detail="Database write buffer update failed")
|
|
|
|
prefix = _get_line_prefix_by_id(equipment_id)
|
|
topic = f"/{prefix}/d/out"
|
|
|
|
envelope = {
|
|
"command_id": command_id,
|
|
"type": "d_write",
|
|
"items": float_payload,
|
|
"requested_at": datetime.utcnow().isoformat()
|
|
}
|
|
|
|
try:
|
|
msg_payload = json.dumps(envelope)
|
|
result = _client.publish(topic, msg_payload, qos=1)
|
|
if result.rc != 0:
|
|
raise Exception(f"Paho MQTT publish returned error code: {result.rc}")
|
|
logger.info(f"[REST API WRITE D SUCCESS] Published to topic: {topic} | Payload: {msg_payload}")
|
|
except Exception as e:
|
|
logger.error(f"[REST API WRITE D MQTT ERROR] Publish failed: {e}", exc_info=True)
|
|
from .db_manager import update_control_command_status
|
|
update_control_command_status(command_id, "FAILED")
|
|
raise HTTPException(status_code=500, detail=f"MQTT publish failed: {e}")
|
|
|
|
return {
|
|
"status": "accepted",
|
|
"command_id": command_id,
|
|
"message": "Command queued, ACK pending"
|
|
}
|
|
|
|
|
|
@router.post("/{equipment_id}/sync", response_model=dict)
|
|
def trigger_manual_sync(equipment_id: int):
|
|
from communication.mqtt_client import _client
|
|
import json
|
|
|
|
logger.info(f"[REST API SYNC] Manual Sync requested for equipment_id: {equipment_id}")
|
|
|
|
if _client is None or not _client.is_connected():
|
|
logger.error("[REST API SYNC ERROR] MQTT client is offline")
|
|
raise HTTPException(status_code=503, detail="MQTT broker connection is offline")
|
|
|
|
prefix = _get_line_prefix_by_id(equipment_id)
|
|
|
|
initial_m_write = json.dumps({str(i): 0 for i in range(188)})
|
|
try:
|
|
from database.connection import get_db_connection
|
|
with get_db_connection() as conn:
|
|
with conn.cursor() as cursor:
|
|
cursor.execute(
|
|
"UPDATE machine_status SET m_write_raw = %s, device_state = 'SYNCING' WHERE equipment_id = %s",
|
|
(initial_m_write, equipment_id)
|
|
)
|
|
conn.commit()
|
|
logger.info(f"[REST API SYNC] Successfully cleared database status values for equipment_id: {equipment_id}")
|
|
except Exception as dbe:
|
|
logger.error(f"[REST API SYNC ERROR] DB status reset failed: {dbe}", exc_info=True)
|
|
raise HTTPException(status_code=500, detail="Database reset transaction failed")
|
|
|
|
m_reset_payload = [[i, 0] for i in range(188)]
|
|
|
|
try:
|
|
result_m = _client.publish(f"/{prefix}/m/out", json.dumps(m_reset_payload), qos=1)
|
|
if result_m.rc != 0:
|
|
raise Exception(f"M output reset publish failed with rc: {result_m.rc}")
|
|
logger.info(f"[REST API SYNC] Published M output RESET payload to /{prefix}/m/out")
|
|
except Exception as e:
|
|
logger.error(f"[REST API SYNC ERROR] M reset payload publish failed: {e}", exc_info=True)
|
|
raise HTTPException(status_code=500, detail=f"MQTT reset publish failed: {e}")
|
|
|
|
cmd_topic = f"/{prefix}/cmd"
|
|
try:
|
|
result_cmd = _client.publish(cmd_topic, "sync", qos=1)
|
|
if result_cmd.rc != 0:
|
|
raise Exception(f"Sync command publish failed with rc: {result_cmd.rc}")
|
|
logger.info(f"[REST API SYNC SUCCESS] Published 'sync' command to topic: {cmd_topic}")
|
|
except Exception as e:
|
|
logger.error(f"[REST API SYNC ERROR] Sync command publish failed: {e}", exc_info=True)
|
|
raise HTTPException(status_code=500, detail=f"MQTT sync command publish failed: {e}")
|
|
|
|
return {"status": "success", "reset_topic_m": f"/{prefix}/m/out", "reset_topic_d": "", "cmd_topic": cmd_topic}
|