Compare commits
4 Commits
dd6cc73cf0
...
c8d892ced3
| Author | SHA1 | Date | |
|---|---|---|---|
| c8d892ced3 | |||
| cf82c3f6ec | |||
| 11e8fa6d37 | |||
| 5df448841c |
File diff suppressed because it is too large
Load Diff
272
main.py
272
main.py
|
|
@ -21,6 +21,7 @@ import numpy as np
|
|||
import aiohttp
|
||||
import httpx
|
||||
import time
|
||||
from patient_skid_functions import handle_patient_skid_for_idle, set_patient_skid_users
|
||||
|
||||
from serial_manager import SerialConfig, SerialStore, SerialReader
|
||||
from protocol_decoder import decode_frames
|
||||
|
|
@ -61,6 +62,15 @@ latest_setpoints: Dict[str, Any] = {
|
|||
}
|
||||
|
||||
active_PUs: list[int] = []
|
||||
VALID_STATES = {
|
||||
"IDLE",
|
||||
"PRE-PRODUCTION",
|
||||
"PRODUCTION",
|
||||
"FIRST_START",
|
||||
"THERMALLOOPCLEANING",
|
||||
"DISINFECTION",
|
||||
"SLEEP",
|
||||
}
|
||||
|
||||
# Dictionary to hold running tasks
|
||||
tasks: dict[str, asyncio.Task] = {}
|
||||
|
|
@ -262,57 +272,68 @@ def is_connected():
|
|||
return {"connected": can_backend.connected}
|
||||
|
||||
# PU CONTROL
|
||||
@app.post("/command/{state}/pu/{pu_number}")
|
||||
def send_command(state: str, pu_number: int, ploop_setpoint: float = Query(...), qperm_setpoint: float = Query(...)):
|
||||
VALID_STATES = {
|
||||
"IDLE",
|
||||
"PRE-PRODUCTION",
|
||||
"PRODUCTION",
|
||||
"FIRST_START",
|
||||
"THERMALLOOPCLEANING",
|
||||
"DISINFECTION",
|
||||
"SLEEP",
|
||||
}
|
||||
|
||||
# --- helpers.py (or in the same file if you want) ---
|
||||
|
||||
|
||||
def validate_state(state: str) -> str:
|
||||
"""Normalize and validate the requested state."""
|
||||
state = state.upper()
|
||||
|
||||
if state not in VALID_STATES:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid state '{state}'")
|
||||
return state
|
||||
|
||||
logging.info(f"Sending state '{state}' to PU {pu_number}")
|
||||
|
||||
pu_number = [pu_number] if pu_number !=3 else [1,2] # Temporary way of starting two pus
|
||||
def expand_pu_number(pu_number: int) -> list[int]:
|
||||
"""Temporary rule: if PU = 3 → run on [1, 2]."""
|
||||
return [pu_number] if pu_number != 3 else [1, 2]
|
||||
|
||||
|
||||
|
||||
def send_command_to_pu(
|
||||
pu: int, state: str, ploop_setpoint: float, qperm_setpoint: float
|
||||
) -> dict:
|
||||
"""Send a state command + update setpoints for one PU."""
|
||||
state = validate_state(state)
|
||||
|
||||
if state == "IDLE":
|
||||
set_patient_skid_users(0)
|
||||
url = f"http://192.168.1.28:8000/stop_test"
|
||||
response = httpx.get(url, timeout=1.0)
|
||||
logging.info(f"Stopping test on Patient Skid: {response.status_code}")
|
||||
handle_patient_skid_for_idle()
|
||||
update_setpoints(ploop_setpoint, qperm_setpoint, pu)
|
||||
can_backend.send_state_command(state, pu, ploop_setpoint, qperm_setpoint)
|
||||
current_state = can_backend.read_current_state(pu)
|
||||
|
||||
url = f"http://192.168.1.28:8000/close_valves"
|
||||
response = httpx.get(url, timeout=1.0)
|
||||
logging.info(f"Closing valves on Patient Skid: {response.status_code}")
|
||||
return {
|
||||
"pu": pu,
|
||||
"command": state,
|
||||
"ploop_setpoint": ploop_setpoint,
|
||||
"qperm_setpoint": qperm_setpoint,
|
||||
"current_state": current_state,
|
||||
}
|
||||
|
||||
@app.post("/command/{state}/pu/{pu_number}")
|
||||
def send_command_endpoint(
|
||||
state: str,
|
||||
pu_number: int,
|
||||
ploop_setpoint: float = Query(...),
|
||||
qperm_setpoint: float = Query(...),
|
||||
):
|
||||
logging.info(f"Sending state '{state}' to PU {pu_number}")
|
||||
|
||||
pus = expand_pu_number(pu_number)
|
||||
|
||||
try:
|
||||
for pu in pu_number:
|
||||
update_setpoints(ploop_setpoint, qperm_setpoint, pu)
|
||||
results = []
|
||||
for pu in pus:
|
||||
result = send_command_to_pu(pu, state, ploop_setpoint, qperm_setpoint)
|
||||
results.append(result)
|
||||
|
||||
can_backend.send_state_command(state, pu, ploop_setpoint, qperm_setpoint)
|
||||
current_state = can_backend.read_current_state(pu)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"command": state,
|
||||
"pu": pu,
|
||||
"ploop_setpoint": ploop_setpoint,
|
||||
"qperm_setpoint": qperm_setpoint,
|
||||
"current_state": current_state,
|
||||
}
|
||||
return {"status": "success", "results": results}
|
||||
|
||||
except Exception as e:
|
||||
logging.error(str(e))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
## MONITORING
|
||||
@app.get("/api/pu_status")
|
||||
def get_pu_status():
|
||||
|
|
@ -325,11 +346,11 @@ def get_pu_status():
|
|||
logging.debug(f"[PU STATUS] {states}")
|
||||
|
||||
if states["PU1"] == "SYSTEM_MODE_READY":
|
||||
send_command(state="PRODUCTION", pu_number = 1, ploop_setpoint = latest_setpoints["PU_1"]["Ploop_sp"] , qperm_setpoint=latest_setpoints["PU_1"]["Qperm_sp"])
|
||||
send_command_to_pu(state="PRODUCTION", pu_number = 1, ploop_setpoint = latest_setpoints["PU_1"]["Ploop_sp"] , qperm_setpoint=latest_setpoints["PU_1"]["Qperm_sp"])
|
||||
if states["PU2"] == "SYSTEM_MODE_READY":
|
||||
send_command(state="PRODUCTION", pu_number = 2, ploop_setpoint = latest_setpoints["PU_2"]["Ploop_sp"] , qperm_setpoint=latest_setpoints["PU_2"]["Qperm_sp"])
|
||||
send_command_to_pu(state="PRODUCTION", pu_number = 2, ploop_setpoint = latest_setpoints["PU_2"]["Ploop_sp"] , qperm_setpoint=latest_setpoints["PU_2"]["Qperm_sp"])
|
||||
if states["PU3"] == "SYSTEM_MODE_READY":
|
||||
send_command(state="PRODUCTION", pu_number = 3, ploop_setpoint = latest_setpoints["PU_3"]["Ploop_sp"] , qperm_setpoint=latest_setpoints["PU_3"]["Qperm_sp"])
|
||||
send_command_to_pu(state="PRODUCTION", pu_number = 3, ploop_setpoint = latest_setpoints["PU_3"]["Ploop_sp"] , qperm_setpoint=latest_setpoints["PU_3"]["Qperm_sp"])
|
||||
|
||||
|
||||
active_PUs = [
|
||||
|
|
@ -362,12 +383,13 @@ async def get_monitor_data():
|
|||
return latest_data
|
||||
|
||||
# LOCAL RECORDER
|
||||
@app.post("/start_recording")
|
||||
async def start_recording():
|
||||
# --- internal helpers (not endpoints) ---
|
||||
async def start_recording_internal():
|
||||
global recording_flag, recording_task, recording_file, recording_writer
|
||||
|
||||
if recording_flag:
|
||||
raise HTTPException(status_code=400, detail="Already recording.")
|
||||
logging.warning("Recording already in progress.")
|
||||
return None
|
||||
|
||||
now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"recording_{now}.csv"
|
||||
|
|
@ -377,11 +399,11 @@ async def start_recording():
|
|||
recording_file = open(filepath, "w", newline="")
|
||||
fieldnames_common = ["timestamp", "pu", "QSkid"]
|
||||
fieldnames_DS = list(format_DS_data({}).keys())
|
||||
fieldnames_DS.pop(0) # removing extra timestamp
|
||||
fieldnames_DS.pop(0)
|
||||
fieldnames_PUs = list(format_PU_data({}).keys())
|
||||
fieldnames_PUs.pop(0) # removing extra timestamp
|
||||
fieldnames_PUs.pop(0)
|
||||
|
||||
fieldnames = fieldnames_common + fieldnames_DS + fieldnames_PUs
|
||||
fieldnames = fieldnames_common + fieldnames_DS + fieldnames_PUs + ["Qperm_sp", "Ploop_sp"]
|
||||
|
||||
recording_writer = csv.DictWriter(recording_file, fieldnames=fieldnames)
|
||||
recording_writer.writeheader()
|
||||
|
|
@ -389,14 +411,15 @@ async def start_recording():
|
|||
recording_flag = True
|
||||
recording_task = asyncio.create_task(record_data_loop())
|
||||
logging.info(f"[RECORDING STARTED] File: {filepath}")
|
||||
return {"status": "recording started", "file": filename}
|
||||
return filename
|
||||
|
||||
@app.post("/stop_recording")
|
||||
async def stop_recording():
|
||||
|
||||
async def stop_recording_internal():
|
||||
global recording_flag, recording_task, recording_file
|
||||
|
||||
if not recording_flag:
|
||||
raise HTTPException(status_code=400, detail="Not recording.")
|
||||
logging.warning("No active recording to stop.")
|
||||
return False
|
||||
|
||||
recording_flag = False
|
||||
if recording_task:
|
||||
|
|
@ -408,8 +431,30 @@ async def stop_recording():
|
|||
recording_file = None
|
||||
|
||||
logging.info("[RECORDING STOPPED]")
|
||||
return True
|
||||
|
||||
|
||||
# --- API endpoints ---
|
||||
@app.post("/start_recording")
|
||||
async def start_recording():
|
||||
filename = await start_recording_internal()
|
||||
if not filename:
|
||||
raise HTTPException(status_code=400, detail="Already recording.")
|
||||
return {"status": "recording started", "file": filename}
|
||||
|
||||
@app.post("/stop_recording")
|
||||
async def stop_recording():
|
||||
success = await stop_recording_internal()
|
||||
if not success:
|
||||
raise HTTPException(status_code=400, detail="Not recording.")
|
||||
return {"status": "recording stopped"}
|
||||
|
||||
@app.get("/is_recording")
|
||||
async def is_recording():
|
||||
"""Return True if recording is on, False otherwise"""
|
||||
return JSONResponse(content={"recording": recording_flag})
|
||||
|
||||
|
||||
async def record_data_loop():
|
||||
global recording_writer, recording_file, write_buffer, last_flush_time
|
||||
|
||||
|
|
@ -437,14 +482,8 @@ async def send_command_with_delay(
|
|||
):
|
||||
await asyncio.sleep(delay_s)
|
||||
logging.info(f"[AUTO TEST] Sending {state} to PU{pu} after {delay_s}s")
|
||||
|
||||
url = f"http://127.0.0.1:8080/command/{state}/pu/{pu}?ploop_setpoint={ploop_setpoint}&qperm_setpoint={qperm_setpoint}"
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.post(url)
|
||||
response.raise_for_status()
|
||||
logging.info(f"[AUTO TEST] Command {state} sent successfully to PU{pu}: {response.json()}")
|
||||
return response.json()
|
||||
result = send_command_to_pu(pu, state, ploop_setpoint, qperm_setpoint)
|
||||
except Exception as e:
|
||||
logging.error(f"[AUTO TEST] Failed to send {state} to PU{pu}: {e}")
|
||||
return {"status": "error", "detail": str(e)}
|
||||
|
|
@ -454,67 +493,76 @@ async def set_patients_with_delay(count: int, delay_s: int):
|
|||
logging.info(f"[AUTO TEST] Sending {count} patients to patient skid after {delay_s}s")
|
||||
set_patient_skid_users(count)
|
||||
|
||||
@router.post("/test/auto/1")
|
||||
async def auto_test_pu1(ploop_setpoint: float = Query(0.0)):
|
||||
pu = 1
|
||||
logging.info("[AUTO TEST] Starting automatic test for 1 PU")
|
||||
from fastapi import Query, Path
|
||||
|
||||
# Cancel existing task if still running
|
||||
if "pu1" in tasks and not tasks["pu1"].done():
|
||||
tasks["pu1"].cancel()
|
||||
logging.info("[AUTO TEST] PU1 Cancelled")
|
||||
@router.post("/test/auto/{pu_number}")
|
||||
async def auto_test(pu_number: int ):
|
||||
"""
|
||||
Start automatic test for PU1 or PU2.
|
||||
"""
|
||||
global tasks
|
||||
|
||||
task = asyncio.create_task(run_auto_test_pu1(pu, ploop_setpoint))
|
||||
tasks["pu1"] = task
|
||||
return {"status": "started", "pu": pu}
|
||||
logging.info(f"[AUTO TEST] Starting automatic test for PU{pu_number}")
|
||||
|
||||
@router.post("/test/auto/2")
|
||||
async def auto_test_pu2(ploop_setpoint: float = Query(0.0)):
|
||||
logging.info("[AUTO TEST] Starting automatic test for 2 PUs")
|
||||
key = f"pu{pu_number}"
|
||||
if key in tasks and not tasks[key].done():
|
||||
tasks[key].cancel()
|
||||
logging.info(f"[AUTO TEST] PU{pu_number} Cancelled")
|
||||
|
||||
if "pu2" in tasks and not tasks["pu2"].done():
|
||||
tasks["pu2"].cancel()
|
||||
logging.info("[AUTO TEST] PU2 Cancelled")
|
||||
await start_recording_internal()
|
||||
logging.info("[AUTO TEST] Recorder started")
|
||||
if pu_number == 1:
|
||||
task = asyncio.create_task(run_auto_test_1())
|
||||
result = {"status": "started", "pu": 1}
|
||||
elif pu_number == 2:
|
||||
task = asyncio.create_task(run_auto_test_2())
|
||||
result = {"status": "started", "pu": [2]}
|
||||
elif pu_number == 3:
|
||||
task = asyncio.create_task(run_auto_test_3())
|
||||
result = {"status": "started", "pu": [2]}
|
||||
else:
|
||||
return {"status": "error", "message": "Invalid PU number"}
|
||||
|
||||
tasks[key] = task
|
||||
return result
|
||||
|
||||
task = asyncio.create_task(run_auto_test_pu2(ploop_setpoint))
|
||||
tasks["pu2"] = task
|
||||
return {"status": "started", "pu": [1, 2]}
|
||||
|
||||
@router.post("/test/auto/stop/{pu}")
|
||||
async def stop_auto_test(pu: int):
|
||||
global tasks
|
||||
key = f"pu{pu}"
|
||||
logging.info(f"[AUTO TEST] Stopping {pu}")
|
||||
|
||||
await stop_recording_internal()
|
||||
logging.info("[AUTO TEST] Recorder stopped")
|
||||
if key in tasks and not tasks[key].done():
|
||||
tasks[key].cancel()
|
||||
await send_command_with_delay("IDLE", pu =pu, delay_s=0)
|
||||
logging.info(f"[AUTO TEST] {key} STOPPED")
|
||||
logging.info(f"[AUTO TEST] Test of {key} canceled and PU stopped")
|
||||
return {"status": "stopped", "pu": pu}
|
||||
|
||||
logging.info(f"[AUTO TEST] Stopping {pu} No test Runining")
|
||||
return {"status": "no task running", "pu": pu}
|
||||
|
||||
async def run_auto_test_pu1(pu: int, ploop_setpoint: float):
|
||||
async def run_auto_test_1(pu: int = 1):
|
||||
try:
|
||||
await send_command_with_delay("PRE-PRODUCTION", pu = pu, delay_s=0, ploop_setpoint=ploop_setpoint, qperm_setpoint=1200.0)
|
||||
print("SENDING PRE PROD at ", time.time())
|
||||
await set_patients_with_delay(5, delay_s=10)
|
||||
print("SENDING set_patients_with_delay ", time.time())
|
||||
|
||||
await set_patients_with_delay(10, delay_s=15)
|
||||
print("SENDING set_patients_with_delay ", time.time())
|
||||
|
||||
await send_command_with_delay("IDLE", pu =pu, delay_s=60, ploop_setpoint=ploop_setpoint, qperm_setpoint=1200.0)
|
||||
await send_command_with_delay("PRE-PRODUCTION", pu = pu, delay_s=0, ploop_setpoint=2.5, qperm_setpoint=1200.0)
|
||||
await set_patients_with_delay(5, delay_s=190)
|
||||
await set_patients_with_delay(10, delay_s=20)
|
||||
await set_patients_with_delay(0, delay_s=20)
|
||||
await send_command_with_delay("IDLE", pu =pu, delay_s=20, ploop_setpoint=2.5, qperm_setpoint=1200.0)
|
||||
logging.info("[AUTO TEST] Finished PU1 test")
|
||||
|
||||
await stop_recording_internal()
|
||||
logging.info("[AUTO TEST] Recorder stopped")
|
||||
except asyncio.CancelledError:
|
||||
logging.info(f"[AUTO TEST] PU 1 task cancelled")
|
||||
# optional cleanup
|
||||
raise
|
||||
|
||||
async def run_auto_test_pu2(ploop_setpoint: float):
|
||||
async def run_auto_test_2():
|
||||
ploop_setpoint = 2.5
|
||||
try:
|
||||
# Step 1: Run PU1 test
|
||||
# await run_auto_test_pu1(1, ploop_setpoint)
|
||||
# await run_auto_test_1(1, ploop_setpoint)
|
||||
|
||||
# Step 2: PU2 sequence
|
||||
await send_command_with_delay("PRE-PRODUCTION", pu=2, delay_s=0, ploop_setpoint=ploop_setpoint, qperm_setpoint=1200.0)
|
||||
|
|
@ -529,11 +577,23 @@ async def run_auto_test_pu2(ploop_setpoint: float):
|
|||
# optional cleanup
|
||||
raise
|
||||
|
||||
@router.post("/test/auto/3")
|
||||
async def auto_test_pu3():
|
||||
# Call the function for PU3 auto test
|
||||
logging.info("Start auto test of 3 PU")
|
||||
return {"status": "started", "pu": 3}
|
||||
async def run_auto_test_3(ploop_setpoint: float):
|
||||
try:
|
||||
# Step 1: Run PU1 test
|
||||
# await run_auto_test_1()
|
||||
|
||||
# Step 2: PU2 sequence
|
||||
await send_command_with_delay("PRE-PRODUCTION", pu=2, delay_s=0, ploop_setpoint=ploop_setpoint, qperm_setpoint=1200.0)
|
||||
await set_patients_with_delay(15, delay_s=60)
|
||||
await set_patients_with_delay(0, delay_s=60)
|
||||
await send_command_with_delay("IDLE", pu=2, delay_s=60, ploop_setpoint=ploop_setpoint, qperm_setpoint=1200.0)
|
||||
await send_command_with_delay("IDLE", pu=1, delay_s=60, ploop_setpoint=ploop_setpoint, qperm_setpoint=1200.0)
|
||||
logging.info("[AUTO TEST] Finished PU1 + PU2 test")
|
||||
except asyncio.CancelledError:
|
||||
logging.info(f"[AUTO TEST] PU 2 task cancelled")
|
||||
# optional cleanup
|
||||
raise
|
||||
|
||||
|
||||
# PATIENT SKID HELPERS
|
||||
async def update_latest_flow():
|
||||
|
|
@ -550,30 +610,6 @@ async def update_latest_flow():
|
|||
logging.error(f"Error fetching flow: {e}")
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
def stop_patient_skid():
|
||||
try:
|
||||
url = f"http://192.168.1.28:8000/stop_test"
|
||||
response = httpx.get(url, timeout=5.0)
|
||||
|
||||
if response.status_code == 200:
|
||||
return {"status": "success", "detail": response.json()}
|
||||
else:
|
||||
raise HTTPException(status_code=502, detail=f"Remote server error: {response.text}")
|
||||
except httpx.RequestError as e:
|
||||
raise HTTPException(status_code=500, detail=f"Request to external server failed: {str(e)}")
|
||||
|
||||
def set_patient_skid_users(count: int = 1):
|
||||
try:
|
||||
url = f"http://192.168.1.28:8000/set_users/{count}"
|
||||
response = httpx.get(url, timeout=5.0)
|
||||
|
||||
if response.status_code == 200:
|
||||
return {"status": "success", "detail": response.json()}
|
||||
else:
|
||||
raise HTTPException(status_code=502, detail=f"Remote server error: {response.text}")
|
||||
except httpx.RequestError as e:
|
||||
raise HTTPException(status_code=500, detail=f"Request to external server failed: {str(e)}")
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
30
patient_skid_functions.py
Normal file
30
patient_skid_functions.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import httpx
|
||||
import logging
|
||||
|
||||
def handle_patient_skid_for_idle() -> None:
|
||||
"""Send the special commands to patient skid when entering IDLE."""
|
||||
try:
|
||||
url = "http://192.168.1.28:8000/stop_test"
|
||||
response = httpx.get(url, timeout=1.0)
|
||||
logging.info(f"Stopping test on Patient Skid: {response.status_code}")
|
||||
|
||||
url = "http://192.168.1.28:8000/close_valves"
|
||||
response = httpx.get(url, timeout=1.0)
|
||||
logging.info(f"Closing valves on Patient Skid: {response.status_code}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error handling patient skid for IDLE: {e}")
|
||||
raise
|
||||
|
||||
def set_patient_skid_users(count: int = 0):
|
||||
try:
|
||||
url = f"http://192.168.1.28:8000/set_users/{count}"
|
||||
response = httpx.get(url, timeout=5.0)
|
||||
|
||||
response_2 = httpx.get("http://192.168.1.28:8000/start_defined_test", timeout=5.0)
|
||||
|
||||
if response.status_code == 200:
|
||||
return {"status": "success", "detail": response.json()}
|
||||
else:
|
||||
raise HTTPException(status_code=502, detail=f"Remote server error: {response.text}")
|
||||
except httpx.RequestError as e:
|
||||
raise HTTPException(status_code=500, detail=f"Request to external server failed: {str(e)}")
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
|
@ -17,6 +18,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #1e1e1e;
|
||||
padding: 10px 20px;
|
||||
|
|
@ -24,12 +26,14 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.connect-button {
|
||||
background-color: #ff4444;
|
||||
color: white;
|
||||
|
|
@ -42,9 +46,11 @@
|
|||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.connected {
|
||||
background-color: #00C851;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
|
@ -52,17 +58,21 @@
|
|||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.left-panel, .right-panel {
|
||||
|
||||
.left-panel,
|
||||
.right-panel {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.left-panel {
|
||||
background-color: #1e1e1e;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mode-block {
|
||||
background-color: #333;
|
||||
padding: 15px;
|
||||
|
|
@ -71,10 +81,12 @@
|
|||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pu-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mode-block button {
|
||||
background-color: #4285F4;
|
||||
color: white;
|
||||
|
|
@ -86,39 +98,49 @@
|
|||
transition: background-color 0.3s;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mode-block button:hover {
|
||||
background-color: #3367d6;
|
||||
}
|
||||
|
||||
.mode-block button.active {
|
||||
background-color: #00C851;
|
||||
}
|
||||
|
||||
.mode-block button.in-progress {
|
||||
background-color: #ffcc00;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.mode-block button.ready {
|
||||
background-color: #00C851;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mode-block button.disabled {
|
||||
background-color: #777;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.in-progress {
|
||||
background-color: yellow !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.ready {
|
||||
background-color: orange !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.production {
|
||||
background-color: green !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.pu-status {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pu-item {
|
||||
background-color: #333;
|
||||
padding: 10px;
|
||||
|
|
@ -128,17 +150,20 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monitor-block {
|
||||
background-color: #333;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.monitor-values {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.monitor-value {
|
||||
background-color: #444;
|
||||
padding: 10px;
|
||||
|
|
@ -146,6 +171,7 @@
|
|||
border-radius: 5px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
background-color: #1e1e1e;
|
||||
padding: 10px;
|
||||
|
|
@ -153,12 +179,14 @@
|
|||
color: #fff;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.slider-container label {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.slider-values {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -167,10 +195,12 @@
|
|||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.slider-values span#currentValue {
|
||||
font-weight: bold;
|
||||
color: #00bfff;
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
|
|
@ -180,6 +210,7 @@
|
|||
appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb,
|
||||
.slider::-moz-range-thumb {
|
||||
height: 18px;
|
||||
|
|
@ -188,6 +219,7 @@
|
|||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monitor-link {
|
||||
color: white;
|
||||
background-color: #007bff;
|
||||
|
|
@ -197,14 +229,17 @@
|
|||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.monitor-link:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.monitor-pu-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.monitor-pu-buttons a {
|
||||
color: white;
|
||||
background-color: #007bff;
|
||||
|
|
@ -214,14 +249,17 @@
|
|||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.monitor-pu-buttons a:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.button-group button {
|
||||
padding: 8px 16px;
|
||||
font-size: 1rem;
|
||||
|
|
@ -231,16 +269,19 @@
|
|||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button-group button:hover {
|
||||
background-color: #005f6b;
|
||||
}
|
||||
|
||||
.auto-running {
|
||||
background-color: #ffcc00 !important; /* yellow */
|
||||
background-color: #ffcc00 !important;
|
||||
/* yellow */
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Hydraulic Machine Control</h1>
|
||||
|
|
@ -274,23 +315,32 @@
|
|||
<div class="left-panel">
|
||||
<div class="mode-block">
|
||||
<div class="pu-buttons">
|
||||
<button onclick="sendCommand('IDLE', 1, this)" data-action="IDLE" data-pu="1"><i class="fas fa-power-off"></i> IDLE PU 1</button>
|
||||
<button onclick="sendCommand('IDLE', 2, this)" data-action="IDLE" data-pu="2"><i class="fas fa-power-off"></i> IDLE PU 2</button>
|
||||
<button onclick="sendCommand('IDLE', 3, this)" data-action="IDLE" data-pu="3"><i class="fas fa-power-off"></i> IDLE BOTH</button>
|
||||
<button onclick="sendCommand('IDLE', 1, this)" data-action="IDLE" data-pu="1"><i
|
||||
class="fas fa-power-off"></i> IDLE PU 1</button>
|
||||
<button onclick="sendCommand('IDLE', 2, this)" data-action="IDLE" data-pu="2"><i
|
||||
class="fas fa-power-off"></i> IDLE PU 2</button>
|
||||
<button onclick="sendCommand('IDLE', 3, this)" data-action="IDLE" data-pu="3"><i
|
||||
class="fas fa-power-off"></i> IDLE BOTH</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mode-block">
|
||||
<div class="pu-buttons">
|
||||
<button onclick="sendCommand('PRE-PRODUCTION', 1, this)" data-action="PRE-PRODUCTION" data-pu="1"><i class="fas fa-play"></i> PRE-PROD PU 1</button>
|
||||
<button onclick="sendCommand('PRE-PRODUCTION', 2, this)" data-action="PRE-PRODUCTION" data-pu="2"><i class="fas fa-play"></i> PRE-PROD PU 2</button>
|
||||
<button onclick="sendCommand('PRE-PRODUCTION', 3, this)" data-action="PRE-PRODUCTION" data-pu="3"><i class="fas fa-play"></i> PRE-PROD BOTH</button>
|
||||
<button onclick="sendCommand('PRE-PRODUCTION', 1, this)" data-action="PRE-PRODUCTION" data-pu="1"><i
|
||||
class="fas fa-play"></i> PRE-PROD PU 1</button>
|
||||
<button onclick="sendCommand('PRE-PRODUCTION', 2, this)" data-action="PRE-PRODUCTION" data-pu="2"><i
|
||||
class="fas fa-play"></i> PRE-PROD PU 2</button>
|
||||
<button onclick="sendCommand('PRE-PRODUCTION', 3, this)" data-action="PRE-PRODUCTION" data-pu="3"><i
|
||||
class="fas fa-play"></i> PRE-PROD BOTH</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mode-block">
|
||||
<div class="pu-buttons">
|
||||
<button onclick="sendCommand('FIRST_START', 1, this)" data-action="FIRST_START" data-pu="1"><i class="fas fa-power-off"></i> FIRST START PU 1</button>
|
||||
<button onclick="sendCommand('FIRST_START', 2, this)" data-action="FIRST_START" data-pu="2"><i class="fas fa-power-off"></i> FIRST START PU 2</button>
|
||||
<button onclick="sendCommand('FIRST_START', 3, this)" data-action="FIRST_START" data-pu="3"><i class="fas fa-power-off"></i> FIRST START BOTH</button>
|
||||
<button onclick="sendCommand('FIRST_START', 1, this)" data-action="FIRST_START" data-pu="1"><i
|
||||
class="fas fa-power-off"></i> FIRST START PU 1</button>
|
||||
<button onclick="sendCommand('FIRST_START', 2, this)" data-action="FIRST_START" data-pu="2"><i
|
||||
class="fas fa-power-off"></i> FIRST START PU 2</button>
|
||||
<button onclick="sendCommand('FIRST_START', 3, this)" data-action="FIRST_START" data-pu="3"><i
|
||||
class="fas fa-power-off"></i> FIRST START BOTH</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="slider-container">
|
||||
|
|
@ -300,7 +350,8 @@
|
|||
<span id="currentValue">2.5</span>
|
||||
<span id="maxValue">3.5</span>
|
||||
</div>
|
||||
<input type="range" min="0.5" max="3.5" step="0.1" value="2.5" id="ploopSetpoint" class="slider" oninput="updatePloopSetpoint(this.value)">
|
||||
<input type="range" min="0.5" max="3.5" step="0.1" value="2.5" id="ploopSetpoint" class="slider"
|
||||
oninput="updatePloopSetpoint(this.value)">
|
||||
</div>
|
||||
|
||||
<div class="slider-container">
|
||||
|
|
@ -310,14 +361,14 @@
|
|||
<span id="qpermCurrent">1200</span>
|
||||
<span id="qpermMax">1400</span>
|
||||
</div>
|
||||
<input type="range" min="1200" max="1400" step="50" value="1200"
|
||||
id="qpermSetpoint" class="slider"
|
||||
oninput="updateQpermSetpoint(this.value)">
|
||||
<input type="range" min="1200" max="1400" step="50" value="1200" id="qpermSetpoint" class="slider"
|
||||
oninput="updateQpermSetpoint(this.value)">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mode-block">
|
||||
<button onclick="sendCommand('ThermalLoopCleaning', 0, this)"><i class="fas fa-fire"></i> Thermal Loop Cleaning</button>
|
||||
<button onclick="sendCommand('ThermalLoopCleaning', 0, this)"><i class="fas fa-fire"></i> Thermal Loop
|
||||
Cleaning</button>
|
||||
</div>
|
||||
<div class="pu-status">
|
||||
<div class="pu-item"><span>PU 1: </span><span id="pu1-status">Offline</span></div>
|
||||
|
|
@ -401,6 +452,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function toggleConnection() {
|
||||
const response = await fetch('/connect_toggle', { method: 'POST' });
|
||||
const data = await response.json();
|
||||
|
|
@ -413,20 +466,37 @@
|
|||
try {
|
||||
if (!isRecording) {
|
||||
await fetch('/start_recording', { method: 'POST' });
|
||||
button.innerHTML = '<i class="fas fa-stop-circle"></i> Stop Recording';
|
||||
button.classList.add('connected');
|
||||
} else {
|
||||
await fetch('/stop_recording', { method: 'POST' });
|
||||
button.innerHTML = '<i class="fas fa-circle"></i> Start Recording';
|
||||
button.classList.remove('connected');
|
||||
}
|
||||
isRecording = !isRecording;
|
||||
await getRecordingStatus(); // ✅ refresh button state
|
||||
} catch (error) {
|
||||
console.error('Recording toggle failed:', error);
|
||||
alert('Failed to toggle recording. Check connection.');
|
||||
}
|
||||
}
|
||||
|
||||
async function getRecordingStatus() {
|
||||
try {
|
||||
const response = await fetch('/is_recording', { method: 'GET' });
|
||||
const data = await response.json();
|
||||
const button = document.getElementById('recordButton');
|
||||
isRecording = data.recording;
|
||||
|
||||
if (isRecording) {
|
||||
button.innerHTML = '<i class="fas fa-stop-circle"></i> Stop Recording';
|
||||
button.classList.add('connected'); // green
|
||||
button.style.backgroundColor = '#00C851'; // ✅ Green when active
|
||||
} else {
|
||||
button.innerHTML = '<i class="fas fa-circle"></i> Start Recording';
|
||||
button.classList.remove('connected');
|
||||
button.style.backgroundColor = '#ff4444'; // ✅ Red when off
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching recording status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendCommand(state, puNumber, buttonEl) {
|
||||
const ploopSetpoint = document.getElementById('ploopSetpoint').value;
|
||||
const qpermSetpoint = document.getElementById('qpermSetpoint').value;
|
||||
|
|
@ -436,42 +506,42 @@
|
|||
btn.classList.remove('in-progress', 'ready', 'production');
|
||||
});
|
||||
if (state === 'PRE-PRODUCTION') {
|
||||
buttonEl.classList.add('in-progress');
|
||||
buttonEl.textContent = `Waiting... PU ${puNumber}`;
|
||||
buttonEl.disabled = true;
|
||||
|
||||
const checkReady = async () => {
|
||||
const res = await fetch(`/api/pu_status`);
|
||||
const states = await res.json();
|
||||
const currentState = states[`PU${puNumber}`];
|
||||
|
||||
if (currentState === 'SYSTEM_MODE_READY') {
|
||||
buttonEl.classList.remove('in-progress');
|
||||
buttonEl.classList.add('ready');
|
||||
buttonEl.textContent = `START PRODUCTION PU ${puNumber}`;
|
||||
buttonEl.disabled = false;
|
||||
|
||||
buttonEl.onclick = async () => {
|
||||
await sendCommand("PRODUCTION", puNumber, buttonEl);
|
||||
buttonEl.classList.remove('ready');
|
||||
buttonEl.classList.add('production');
|
||||
buttonEl.textContent = `PRODUCTION ON PU ${puNumber}`;
|
||||
buttonEl.classList.add('in-progress');
|
||||
buttonEl.textContent = `Waiting... PU ${puNumber}`;
|
||||
buttonEl.disabled = true;
|
||||
};
|
||||
}
|
||||
else if (currentState === 'SYSTEM_MODE_PRODUCTION') {
|
||||
// ✅ Directly update if already in production
|
||||
buttonEl.classList.remove('in-progress');
|
||||
buttonEl.classList.add('production');
|
||||
buttonEl.textContent = `PRODUCTION ON PU ${puNumber}`;
|
||||
buttonEl.disabled = true;
|
||||
}
|
||||
else {
|
||||
setTimeout(checkReady, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
checkReady();
|
||||
const checkReady = async () => {
|
||||
const res = await fetch(`/api/pu_status`);
|
||||
const states = await res.json();
|
||||
const currentState = states[`PU${puNumber}`];
|
||||
|
||||
if (currentState === 'SYSTEM_MODE_READY') {
|
||||
buttonEl.classList.remove('in-progress');
|
||||
buttonEl.classList.add('ready');
|
||||
buttonEl.textContent = `START PRODUCTION PU ${puNumber}`;
|
||||
buttonEl.disabled = false;
|
||||
|
||||
buttonEl.onclick = async () => {
|
||||
await sendCommand("PRODUCTION", puNumber, buttonEl);
|
||||
buttonEl.classList.remove('ready');
|
||||
buttonEl.classList.add('production');
|
||||
buttonEl.textContent = `PRODUCTION ON PU ${puNumber}`;
|
||||
buttonEl.disabled = true;
|
||||
};
|
||||
}
|
||||
else if (currentState === 'SYSTEM_MODE_PRODUCTION') {
|
||||
// ✅ Directly update if already in production
|
||||
buttonEl.classList.remove('in-progress');
|
||||
buttonEl.classList.add('production');
|
||||
buttonEl.textContent = `PRODUCTION ON PU ${puNumber}`;
|
||||
buttonEl.disabled = true;
|
||||
}
|
||||
else {
|
||||
setTimeout(checkReady, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
checkReady();
|
||||
|
||||
} else if (state === 'PRODUCTION') {
|
||||
// ✅ Handles initial load case
|
||||
|
|
@ -628,6 +698,9 @@
|
|||
getConnectionStatus();
|
||||
setInterval(fetchMonitorData, 1000);
|
||||
fetchMonitorData();
|
||||
setInterval(getRecordingStatus, 1000);
|
||||
getRecordingStatus();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Reference in New Issue
Block a user