Compare commits

..

4 Commits

4 changed files with 314 additions and 2635 deletions

File diff suppressed because it is too large Load Diff

260
main.py
View File

@ -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}")
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}")
try:
for pu in pu_number:
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)
return {
"status": "success",
"command": state,
"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:
results = []
for pu in pus:
result = send_command_to_pu(pu, state, ploop_setpoint, qperm_setpoint)
results.append(result)
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
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>
<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"
<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;
@ -628,6 +698,9 @@
getConnectionStatus();
setInterval(fetchMonitorData, 1000);
fetchMonitorData();
setInterval(getRecordingStatus, 1000);
getRecordingStatus();
</script>
</body>
</html>