Compare commits

...

4 Commits

4 changed files with 314 additions and 2635 deletions

File diff suppressed because it is too large Load Diff

272
main.py
View File

@ -21,6 +21,7 @@ import numpy as np
import aiohttp import aiohttp
import httpx import httpx
import time import time
from patient_skid_functions import handle_patient_skid_for_idle, set_patient_skid_users
from serial_manager import SerialConfig, SerialStore, SerialReader from serial_manager import SerialConfig, SerialStore, SerialReader
from protocol_decoder import decode_frames from protocol_decoder import decode_frames
@ -61,6 +62,15 @@ latest_setpoints: Dict[str, Any] = {
} }
active_PUs: list[int] = [] active_PUs: list[int] = []
VALID_STATES = {
"IDLE",
"PRE-PRODUCTION",
"PRODUCTION",
"FIRST_START",
"THERMALLOOPCLEANING",
"DISINFECTION",
"SLEEP",
}
# Dictionary to hold running tasks # Dictionary to hold running tasks
tasks: dict[str, asyncio.Task] = {} tasks: dict[str, asyncio.Task] = {}
@ -262,57 +272,68 @@ def is_connected():
return {"connected": can_backend.connected} return {"connected": can_backend.connected}
# PU CONTROL # 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() state = state.upper()
if state not in VALID_STATES: if state not in VALID_STATES:
raise HTTPException(status_code=400, detail=f"Invalid state '{state}'") 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": if state == "IDLE":
set_patient_skid_users(0) handle_patient_skid_for_idle()
url = f"http://192.168.1.28:8000/stop_test" update_setpoints(ploop_setpoint, qperm_setpoint, pu)
response = httpx.get(url, timeout=1.0) can_backend.send_state_command(state, pu, ploop_setpoint, qperm_setpoint)
logging.info(f"Stopping test on Patient Skid: {response.status_code}") current_state = can_backend.read_current_state(pu)
url = f"http://192.168.1.28:8000/close_valves" return {
response = httpx.get(url, timeout=1.0) "pu": pu,
logging.info(f"Closing valves on Patient Skid: {response.status_code}") "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: try:
for pu in pu_number: results = []
update_setpoints(ploop_setpoint, qperm_setpoint, pu) 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) return {"status": "success", "results": results}
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,
}
except Exception as e: except Exception as e:
logging.error(str(e)) logging.error(str(e))
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
## MONITORING ## MONITORING
@app.get("/api/pu_status") @app.get("/api/pu_status")
def get_pu_status(): def get_pu_status():
@ -325,11 +346,11 @@ def get_pu_status():
logging.debug(f"[PU STATUS] {states}") logging.debug(f"[PU STATUS] {states}")
if states["PU1"] == "SYSTEM_MODE_READY": 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": 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": 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 = [ active_PUs = [
@ -362,12 +383,13 @@ async def get_monitor_data():
return latest_data return latest_data
# LOCAL RECORDER # LOCAL RECORDER
@app.post("/start_recording") # --- internal helpers (not endpoints) ---
async def start_recording(): async def start_recording_internal():
global recording_flag, recording_task, recording_file, recording_writer global recording_flag, recording_task, recording_file, recording_writer
if recording_flag: 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") now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"recording_{now}.csv" filename = f"recording_{now}.csv"
@ -377,11 +399,11 @@ async def start_recording():
recording_file = open(filepath, "w", newline="") recording_file = open(filepath, "w", newline="")
fieldnames_common = ["timestamp", "pu", "QSkid"] fieldnames_common = ["timestamp", "pu", "QSkid"]
fieldnames_DS = list(format_DS_data({}).keys()) 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 = 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 = csv.DictWriter(recording_file, fieldnames=fieldnames)
recording_writer.writeheader() recording_writer.writeheader()
@ -389,14 +411,15 @@ async def start_recording():
recording_flag = True recording_flag = True
recording_task = asyncio.create_task(record_data_loop()) recording_task = asyncio.create_task(record_data_loop())
logging.info(f"[RECORDING STARTED] File: {filepath}") 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 global recording_flag, recording_task, recording_file
if not recording_flag: if not recording_flag:
raise HTTPException(status_code=400, detail="Not recording.") logging.warning("No active recording to stop.")
return False
recording_flag = False recording_flag = False
if recording_task: if recording_task:
@ -408,8 +431,30 @@ async def stop_recording():
recording_file = None recording_file = None
logging.info("[RECORDING STOPPED]") 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"} 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(): async def record_data_loop():
global recording_writer, recording_file, write_buffer, last_flush_time 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) await asyncio.sleep(delay_s)
logging.info(f"[AUTO TEST] Sending {state} to PU{pu} after {delay_s}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: try:
async with httpx.AsyncClient(timeout=5.0) as client: result = send_command_to_pu(pu, state, ploop_setpoint, qperm_setpoint)
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()
except Exception as e: except Exception as e:
logging.error(f"[AUTO TEST] Failed to send {state} to PU{pu}: {e}") logging.error(f"[AUTO TEST] Failed to send {state} to PU{pu}: {e}")
return {"status": "error", "detail": str(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") logging.info(f"[AUTO TEST] Sending {count} patients to patient skid after {delay_s}s")
set_patient_skid_users(count) set_patient_skid_users(count)
@router.post("/test/auto/1") from fastapi import Query, Path
async def auto_test_pu1(ploop_setpoint: float = Query(0.0)):
pu = 1
logging.info("[AUTO TEST] Starting automatic test for 1 PU")
# Cancel existing task if still running @router.post("/test/auto/{pu_number}")
if "pu1" in tasks and not tasks["pu1"].done(): async def auto_test(pu_number: int ):
tasks["pu1"].cancel() """
logging.info("[AUTO TEST] PU1 Cancelled") Start automatic test for PU1 or PU2.
"""
global tasks
task = asyncio.create_task(run_auto_test_pu1(pu, ploop_setpoint)) logging.info(f"[AUTO TEST] Starting automatic test for PU{pu_number}")
tasks["pu1"] = task
return {"status": "started", "pu": pu}
@router.post("/test/auto/2") key = f"pu{pu_number}"
async def auto_test_pu2(ploop_setpoint: float = Query(0.0)): if key in tasks and not tasks[key].done():
logging.info("[AUTO TEST] Starting automatic test for 2 PUs") tasks[key].cancel()
logging.info(f"[AUTO TEST] PU{pu_number} Cancelled")
if "pu2" in tasks and not tasks["pu2"].done(): await start_recording_internal()
tasks["pu2"].cancel() logging.info("[AUTO TEST] Recorder started")
logging.info("[AUTO TEST] PU2 Cancelled") 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}") @router.post("/test/auto/stop/{pu}")
async def stop_auto_test(pu: int): async def stop_auto_test(pu: int):
global tasks
key = f"pu{pu}" key = f"pu{pu}"
logging.info(f"[AUTO TEST] Stopping {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(): if key in tasks and not tasks[key].done():
tasks[key].cancel() tasks[key].cancel()
await send_command_with_delay("IDLE", pu =pu, delay_s=0) 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} return {"status": "stopped", "pu": pu}
logging.info(f"[AUTO TEST] Stopping {pu} No test Runining") logging.info(f"[AUTO TEST] Stopping {pu} No test Runining")
return {"status": "no task running", "pu": pu} 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: try:
await send_command_with_delay("PRE-PRODUCTION", pu = pu, delay_s=0, 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)
print("SENDING PRE PROD at ", time.time()) await set_patients_with_delay(5, delay_s=190)
await set_patients_with_delay(5, delay_s=10) await set_patients_with_delay(10, delay_s=20)
print("SENDING set_patients_with_delay ", time.time()) 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)
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)
logging.info("[AUTO TEST] Finished PU1 test") logging.info("[AUTO TEST] Finished PU1 test")
await stop_recording_internal()
logging.info("[AUTO TEST] Recorder stopped")
except asyncio.CancelledError: except asyncio.CancelledError:
logging.info(f"[AUTO TEST] PU 1 task cancelled") logging.info(f"[AUTO TEST] PU 1 task cancelled")
# optional cleanup
raise raise
async def run_auto_test_pu2(ploop_setpoint: float): async def run_auto_test_2():
ploop_setpoint = 2.5
try: try:
# Step 1: Run PU1 test # 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 # 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 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 # optional cleanup
raise raise
@router.post("/test/auto/3") async def run_auto_test_3(ploop_setpoint: float):
async def auto_test_pu3(): try:
# Call the function for PU3 auto test # Step 1: Run PU1 test
logging.info("Start auto test of 3 PU") # await run_auto_test_1()
return {"status": "started", "pu": 3}
# 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 # PATIENT SKID HELPERS
async def update_latest_flow(): async def update_latest_flow():
@ -550,30 +610,6 @@ async def update_latest_flow():
logging.error(f"Error fetching flow: {e}") logging.error(f"Error fetching flow: {e}")
await asyncio.sleep(1.0) 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) app.include_router(router)
if __name__ == "__main__": if __name__ == "__main__":

30
patient_skid_functions.py Normal file
View 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)}")

View File

@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -17,6 +18,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.header { .header {
background-color: #1e1e1e; background-color: #1e1e1e;
padding: 10px 20px; padding: 10px 20px;
@ -24,12 +26,14 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.header-row { .header-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
margin-bottom: 5px; margin-bottom: 5px;
} }
.connect-button { .connect-button {
background-color: #ff4444; background-color: #ff4444;
color: white; color: white;
@ -42,9 +46,11 @@
align-items: center; align-items: center;
gap: 10px; gap: 10px;
} }
.connected { .connected {
background-color: #00C851; background-color: #00C851;
} }
.container { .container {
display: flex; display: flex;
flex: 1; flex: 1;
@ -52,17 +58,21 @@
overflow-x: hidden; overflow-x: hidden;
box-sizing: border-box; box-sizing: border-box;
} }
.left-panel, .right-panel {
.left-panel,
.right-panel {
flex: 1; flex: 1;
padding: 20px; padding: 20px;
overflow-y: auto; overflow-y: auto;
} }
.left-panel { .left-panel {
background-color: #1e1e1e; background-color: #1e1e1e;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
.mode-block { .mode-block {
background-color: #333; background-color: #333;
padding: 15px; padding: 15px;
@ -71,10 +81,12 @@
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
.pu-buttons { .pu-buttons {
display: flex; display: flex;
gap: 10px; gap: 10px;
} }
.mode-block button { .mode-block button {
background-color: #4285F4; background-color: #4285F4;
color: white; color: white;
@ -86,39 +98,49 @@
transition: background-color 0.3s; transition: background-color 0.3s;
flex: 1; flex: 1;
} }
.mode-block button:hover { .mode-block button:hover {
background-color: #3367d6; background-color: #3367d6;
} }
.mode-block button.active { .mode-block button.active {
background-color: #00C851; background-color: #00C851;
} }
.mode-block button.in-progress { .mode-block button.in-progress {
background-color: #ffcc00; background-color: #ffcc00;
color: #000; color: #000;
} }
.mode-block button.ready { .mode-block button.ready {
background-color: #00C851; background-color: #00C851;
color: #fff; color: #fff;
} }
.mode-block button.disabled { .mode-block button.disabled {
background-color: #777; background-color: #777;
cursor: not-allowed; cursor: not-allowed;
} }
.in-progress { .in-progress {
background-color: yellow !important; background-color: yellow !important;
color: black !important; color: black !important;
} }
.ready { .ready {
background-color: orange !important; background-color: orange !important;
color: black !important; color: black !important;
} }
.production { .production {
background-color: green !important; background-color: green !important;
color: white !important; color: white !important;
} }
.pu-status { .pu-status {
margin-top: 20px; margin-top: 20px;
} }
.pu-item { .pu-item {
background-color: #333; background-color: #333;
padding: 10px; padding: 10px;
@ -128,17 +150,20 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.monitor-block { .monitor-block {
background-color: #333; background-color: #333;
padding: 15px; padding: 15px;
border-radius: 5px; border-radius: 5px;
margin-bottom: 15px; margin-bottom: 15px;
} }
.monitor-values { .monitor-values {
display: flex; display: flex;
gap: 10px; gap: 10px;
margin-top: 10px; margin-top: 10px;
} }
.monitor-value { .monitor-value {
background-color: #444; background-color: #444;
padding: 10px; padding: 10px;
@ -146,6 +171,7 @@
border-radius: 5px; border-radius: 5px;
flex: 1; flex: 1;
} }
.slider-container { .slider-container {
background-color: #1e1e1e; background-color: #1e1e1e;
padding: 10px; padding: 10px;
@ -153,12 +179,14 @@
color: #fff; color: #fff;
width: 95%; width: 95%;
} }
.slider-container label { .slider-container label {
font-size: 1.2rem; font-size: 1.2rem;
font-weight: bold; font-weight: bold;
margin-bottom: 10px; margin-bottom: 10px;
display: block; display: block;
} }
.slider-values { .slider-values {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -167,10 +195,12 @@
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
} }
.slider-values span#currentValue { .slider-values span#currentValue {
font-weight: bold; font-weight: bold;
color: #00bfff; color: #00bfff;
} }
.slider { .slider {
width: 100%; width: 100%;
height: 8px; height: 8px;
@ -180,6 +210,7 @@
appearance: none; appearance: none;
cursor: pointer; cursor: pointer;
} }
.slider::-webkit-slider-thumb, .slider::-webkit-slider-thumb,
.slider::-moz-range-thumb { .slider::-moz-range-thumb {
height: 18px; height: 18px;
@ -188,6 +219,7 @@
border-radius: 50%; border-radius: 50%;
cursor: pointer; cursor: pointer;
} }
.monitor-link { .monitor-link {
color: white; color: white;
background-color: #007bff; background-color: #007bff;
@ -197,14 +229,17 @@
font-weight: bold; font-weight: bold;
font-size: 12px; font-size: 12px;
} }
.monitor-link:hover { .monitor-link:hover {
background-color: #0056b3; background-color: #0056b3;
} }
.monitor-pu-buttons { .monitor-pu-buttons {
display: flex; display: flex;
gap: 5px; gap: 5px;
margin: 10px; margin: 10px;
} }
.monitor-pu-buttons a { .monitor-pu-buttons a {
color: white; color: white;
background-color: #007bff; background-color: #007bff;
@ -214,14 +249,17 @@
font-weight: bold; font-weight: bold;
font-size: 12px; font-size: 12px;
} }
.monitor-pu-buttons a:hover { .monitor-pu-buttons a:hover {
background-color: #0056b3; background-color: #0056b3;
} }
.button-group { .button-group {
margin-top: 10px; margin-top: 10px;
display: flex; display: flex;
justify-content: space-around; justify-content: space-around;
} }
.button-group button { .button-group button {
padding: 8px 16px; padding: 8px 16px;
font-size: 1rem; font-size: 1rem;
@ -231,16 +269,19 @@
border: none; border: none;
cursor: pointer; cursor: pointer;
} }
.button-group button:hover { .button-group button:hover {
background-color: #005f6b; background-color: #005f6b;
} }
.auto-running { .auto-running {
background-color: #ffcc00 !important; /* yellow */ background-color: #ffcc00 !important;
/* yellow */
color: black !important; color: black !important;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="header"> <div class="header">
<h1>Hydraulic Machine Control</h1> <h1>Hydraulic Machine Control</h1>
@ -274,23 +315,32 @@
<div class="left-panel"> <div class="left-panel">
<div class="mode-block"> <div class="mode-block">
<div class="pu-buttons"> <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', 1, this)" data-action="IDLE" data-pu="1"><i
<button onclick="sendCommand('IDLE', 2, this)" data-action="IDLE" data-pu="2"><i class="fas fa-power-off"></i> IDLE PU 2</button> class="fas fa-power-off"></i> IDLE PU 1</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', 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> </div>
<div class="mode-block"> <div class="mode-block">
<div class="pu-buttons"> <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', 1, this)" data-action="PRE-PRODUCTION" data-pu="1"><i
<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> class="fas fa-play"></i> PRE-PROD PU 1</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', 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> </div>
<div class="mode-block"> <div class="mode-block">
<div class="pu-buttons"> <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', 1, this)" data-action="FIRST_START" data-pu="1"><i
<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> class="fas fa-power-off"></i> FIRST START PU 1</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', 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> </div>
<div class="slider-container"> <div class="slider-container">
@ -300,7 +350,8 @@
<span id="currentValue">2.5</span> <span id="currentValue">2.5</span>
<span id="maxValue">3.5</span> <span id="maxValue">3.5</span>
</div> </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>
<div class="slider-container"> <div class="slider-container">
@ -310,14 +361,14 @@
<span id="qpermCurrent">1200</span> <span id="qpermCurrent">1200</span>
<span id="qpermMax">1400</span> <span id="qpermMax">1400</span>
</div> </div>
<input type="range" min="1200" max="1400" step="50" value="1200" <input type="range" min="1200" max="1400" step="50" value="1200" id="qpermSetpoint" class="slider"
id="qpermSetpoint" class="slider" oninput="updateQpermSetpoint(this.value)">
oninput="updateQpermSetpoint(this.value)">
</div> </div>
<div class="mode-block"> <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>
<div class="pu-status"> <div class="pu-status">
<div class="pu-item"><span>PU 1: </span><span id="pu1-status">Offline</span></div> <div class="pu-item"><span>PU 1: </span><span id="pu1-status">Offline</span></div>
@ -401,6 +452,8 @@
} }
} }
async function toggleConnection() { async function toggleConnection() {
const response = await fetch('/connect_toggle', { method: 'POST' }); const response = await fetch('/connect_toggle', { method: 'POST' });
const data = await response.json(); const data = await response.json();
@ -413,20 +466,37 @@
try { try {
if (!isRecording) { if (!isRecording) {
await fetch('/start_recording', { method: 'POST' }); await fetch('/start_recording', { method: 'POST' });
button.innerHTML = '<i class="fas fa-stop-circle"></i> Stop Recording';
button.classList.add('connected');
} else { } else {
await fetch('/stop_recording', { method: 'POST' }); 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) { } catch (error) {
console.error('Recording toggle failed:', error); console.error('Recording toggle failed:', error);
alert('Failed to toggle recording. Check connection.'); 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) { async function sendCommand(state, puNumber, buttonEl) {
const ploopSetpoint = document.getElementById('ploopSetpoint').value; const ploopSetpoint = document.getElementById('ploopSetpoint').value;
const qpermSetpoint = document.getElementById('qpermSetpoint').value; const qpermSetpoint = document.getElementById('qpermSetpoint').value;
@ -436,42 +506,42 @@
btn.classList.remove('in-progress', 'ready', 'production'); btn.classList.remove('in-progress', 'ready', 'production');
}); });
if (state === 'PRE-PRODUCTION') { if (state === 'PRE-PRODUCTION') {
buttonEl.classList.add('in-progress'); buttonEl.classList.add('in-progress');
buttonEl.textContent = `Waiting... PU ${puNumber}`; 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.disabled = true; 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') { } else if (state === 'PRODUCTION') {
// ✅ Handles initial load case // ✅ Handles initial load case
@ -628,6 +698,9 @@
getConnectionStatus(); getConnectionStatus();
setInterval(fetchMonitorData, 1000); setInterval(fetchMonitorData, 1000);
fetchMonitorData(); fetchMonitorData();
setInterval(getRecordingStatus, 1000);
getRecordingStatus();
</script> </script>
</body> </body>
</html> </html>