diff --git a/main.py b/main.py index 9bf5901..41ae2b5 100644 --- a/main.py +++ b/main.py @@ -61,6 +61,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 +271,82 @@ 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] - if state == "IDLE": - set_patient_skid_users(0) - url = f"http://192.168.1.28:8000/stop_test" + +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 = f"http://192.168.1.28:8000/close_valves" + 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 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": + 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 { + "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 +359,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 +396,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 +412,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 +424,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 +444,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 +495,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 +506,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()) - 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 = 1): +async def run_auto_test_1(pu: int = 1): try: 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=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 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 +590,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(): @@ -562,11 +635,13 @@ def stop_patient_skid(): 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): +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: diff --git a/templates/control.html b/templates/control.html index 96dc0f9..b9f3275 100644 --- a/templates/control.html +++ b/templates/control.html @@ -1,5 +1,6 @@ + @@ -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; } - +

Hydraulic Machine Control

@@ -274,23 +315,32 @@
- - - + + +
- - - + + +
- - - + + +
@@ -300,7 +350,8 @@ 2.5 3.5
- +
@@ -310,14 +361,14 @@ 1200 1400
- +
- +
PU 1: Offline
@@ -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 = ' Stop Recording'; - button.classList.add('connected'); } else { await fetch('/stop_recording', { method: 'POST' }); - button.innerHTML = ' 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 = ' Stop Recording'; + button.classList.add('connected'); // green + button.style.backgroundColor = '#00C851'; // ✅ Green when active + } else { + button.innerHTML = ' 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(); - + + \ No newline at end of file