Add separate simulator UI
This commit is contained in:
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import date, datetime, timezone
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
@@ -13,9 +13,17 @@ from sqlalchemy.orm import Session
|
||||
from .config import get_settings
|
||||
from .database import Base, SessionLocal, engine, get_db
|
||||
from .models import Cycle, Downtime, DowntimeReason, Machine, MachineEvent, ProductionOrder, Scrap, ScrapReason
|
||||
from .schemas import DowntimeQualify, MachineConfigUpdate, ProductionOrderCreate, ScrapCreate
|
||||
from .schemas import (
|
||||
DowntimeQualify,
|
||||
MachineConfigUpdate,
|
||||
MachineEventIn,
|
||||
ProductionOrderCreate,
|
||||
ScrapCreate,
|
||||
SimulatorEventCreate,
|
||||
SimulatorScenarioCreate,
|
||||
)
|
||||
from .services.dashboard import build_dashboard_snapshot, serialize_machine_card, serialize_machine_config
|
||||
from .services.events import detect_missing_cycles, get_open_downtime, get_running_order
|
||||
from .services.events import detect_missing_cycles, get_open_downtime, get_running_order, handle_machine_event
|
||||
from .services.mqtt_listener import MqttListener
|
||||
from .services.oee import compute_daily_oee, compute_machine_oee
|
||||
from .services.realtime import hub
|
||||
@@ -259,6 +267,160 @@ def get_machine_events(machine_id: int, limit: int = Query(default=50, le=200),
|
||||
]
|
||||
|
||||
|
||||
def _get_machine_by_code_or_404(db: Session, machine_code: str) -> Machine:
|
||||
machine = db.execute(select(Machine).where(Machine.code == machine_code)).scalar_one_or_none()
|
||||
if machine is None:
|
||||
raise HTTPException(status_code=404, detail="Machine not found")
|
||||
return machine
|
||||
|
||||
|
||||
def _emit_simulator_event(db: Session, payload: SimulatorEventCreate) -> dict:
|
||||
_get_machine_by_code_or_404(db, payload.machine_id)
|
||||
incoming = MachineEventIn(
|
||||
machine_id=payload.machine_id,
|
||||
event_type=payload.event_type,
|
||||
timestamp=payload.timestamp or datetime.now(UTC),
|
||||
cycle_time_sec=payload.cycle_time_sec,
|
||||
source="simulator-ui",
|
||||
input_name=payload.input_name,
|
||||
input_value=payload.input_value,
|
||||
)
|
||||
result = handle_machine_event(db, incoming)
|
||||
if result["should_refresh"]:
|
||||
hub.notify({"type": "refresh", "source": "simulator-ui", **result})
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/api/simulator/events")
|
||||
def create_simulator_event(payload: SimulatorEventCreate, db: Session = Depends(get_db)) -> dict:
|
||||
return _emit_simulator_event(db, payload)
|
||||
|
||||
|
||||
@app.post("/api/simulator/scenarios")
|
||||
def run_simulator_scenario(payload: SimulatorScenarioCreate, db: Session = Depends(get_db)) -> dict:
|
||||
machine = _get_machine_by_code_or_404(db, payload.machine_id)
|
||||
now = datetime.now(UTC)
|
||||
cycle_time_sec = float(payload.cycle_time_sec or 20)
|
||||
events: list[SimulatorEventCreate] = []
|
||||
|
||||
if payload.scenario == "power_on":
|
||||
events.append(SimulatorEventCreate(machine_id=machine.code, event_type="machine_power_on", timestamp=now))
|
||||
elif payload.scenario == "power_off":
|
||||
events.append(SimulatorEventCreate(machine_id=machine.code, event_type="machine_power_off", timestamp=now))
|
||||
elif payload.scenario == "cycle_completed":
|
||||
events.append(
|
||||
SimulatorEventCreate(
|
||||
machine_id=machine.code,
|
||||
event_type="cycle_completed",
|
||||
timestamp=now,
|
||||
cycle_time_sec=cycle_time_sec,
|
||||
)
|
||||
)
|
||||
elif payload.scenario == "cycle_burst":
|
||||
first_event_time = now - timedelta(seconds=cycle_time_sec * max(payload.count - 1, 0))
|
||||
for index in range(payload.count):
|
||||
events.append(
|
||||
SimulatorEventCreate(
|
||||
machine_id=machine.code,
|
||||
event_type="cycle_completed",
|
||||
timestamp=first_event_time + timedelta(seconds=cycle_time_sec * index),
|
||||
cycle_time_sec=cycle_time_sec,
|
||||
)
|
||||
)
|
||||
elif payload.scenario == "cycle_signal_edge":
|
||||
current_value = machine.cycle_signal_edge != "falling"
|
||||
previous_value = not current_value
|
||||
events.extend(
|
||||
[
|
||||
SimulatorEventCreate(
|
||||
machine_id=machine.code,
|
||||
event_type="digital_input_changed",
|
||||
timestamp=now - timedelta(milliseconds=max(machine.debounce_ms + 100, 500)),
|
||||
input_name="cycle_signal",
|
||||
input_value=previous_value,
|
||||
),
|
||||
SimulatorEventCreate(
|
||||
machine_id=machine.code,
|
||||
event_type="digital_input_changed",
|
||||
timestamp=now,
|
||||
input_name="cycle_signal",
|
||||
input_value=current_value,
|
||||
cycle_time_sec=cycle_time_sec,
|
||||
),
|
||||
]
|
||||
)
|
||||
elif payload.scenario == "debounce_noise":
|
||||
current_value = machine.cycle_signal_edge != "falling"
|
||||
previous_value = not current_value
|
||||
events.extend(
|
||||
[
|
||||
SimulatorEventCreate(
|
||||
machine_id=machine.code,
|
||||
event_type="digital_input_changed",
|
||||
timestamp=now - timedelta(milliseconds=max(machine.debounce_ms - 50, 1)),
|
||||
input_name="cycle_signal",
|
||||
input_value=previous_value,
|
||||
),
|
||||
SimulatorEventCreate(
|
||||
machine_id=machine.code,
|
||||
event_type="digital_input_changed",
|
||||
timestamp=now,
|
||||
input_name="cycle_signal",
|
||||
input_value=current_value,
|
||||
cycle_time_sec=cycle_time_sec,
|
||||
),
|
||||
]
|
||||
)
|
||||
elif payload.scenario == "machine_stopped":
|
||||
events.append(SimulatorEventCreate(machine_id=machine.code, event_type="machine_stopped", timestamp=now))
|
||||
elif payload.scenario == "recover_with_cycle":
|
||||
events.append(
|
||||
SimulatorEventCreate(
|
||||
machine_id=machine.code,
|
||||
event_type="cycle_completed",
|
||||
timestamp=now,
|
||||
cycle_time_sec=cycle_time_sec,
|
||||
)
|
||||
)
|
||||
elif payload.scenario == "alarm_on":
|
||||
events.append(
|
||||
SimulatorEventCreate(
|
||||
machine_id=machine.code,
|
||||
event_type="digital_input_changed",
|
||||
timestamp=now,
|
||||
input_name="general_alarm",
|
||||
input_value=True,
|
||||
)
|
||||
)
|
||||
elif payload.scenario == "alarm_off":
|
||||
events.append(
|
||||
SimulatorEventCreate(
|
||||
machine_id=machine.code,
|
||||
event_type="digital_input_changed",
|
||||
timestamp=now,
|
||||
input_name="general_alarm",
|
||||
input_value=False,
|
||||
)
|
||||
)
|
||||
elif payload.scenario == "raw_input":
|
||||
if not payload.input_name:
|
||||
raise HTTPException(status_code=400, detail="input_name is required for raw_input")
|
||||
events.append(
|
||||
SimulatorEventCreate(
|
||||
machine_id=machine.code,
|
||||
event_type="digital_input_changed",
|
||||
timestamp=now,
|
||||
input_name=payload.input_name,
|
||||
input_value=payload.input_value,
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="Unsupported simulator scenario")
|
||||
|
||||
results = [_emit_simulator_event(db, event) for event in events]
|
||||
return {"scenario": payload.scenario, "machine_id": machine.code, "events": results}
|
||||
|
||||
|
||||
@app.get("/api/machines/{machine_id}/cycles")
|
||||
def get_machine_cycles(machine_id: int, limit: int = Query(default=100, le=500), db: Session = Depends(get_db)) -> list[dict]:
|
||||
rows = db.execute(
|
||||
|
||||
Reference in New Issue
Block a user