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}