Adds feed valve button + reformatting

This commit is contained in:
Etienne Chassaing 2025-07-15 13:51:05 +02:00
parent e21bfa26f6
commit 1545a01350
3 changed files with 154 additions and 145 deletions

View File

@ -28,7 +28,7 @@ class CANBackend:
# Placeholder for sending mode command # Placeholder for sending mode command
PUs_states[pu_number-1] = {"PU_MODE": state, "ploop_setpoint":ploop_setpoint} PUs_states[pu_number-1] = {"PU_MODE": state, "ploop_setpoint":ploop_setpoint}
def get_latest_data(self): def get_latest_data(self, pu_number=1, data={}):
# Simulate getting the latest data with random values # Simulate getting the latest data with random values
return { return {
"FM2": round(1000 * np.random.random(), 1), "FM2": round(1000 * np.random.random(), 1),

59
main.py
View File

@ -5,19 +5,22 @@ import logging
import os import os
from fastapi import Request, APIRouter from fastapi import Request, APIRouter
import platform import platform
from fastapi.templating import Jinja2Templates # pip install fastapi uvicorn jinja2 python-multipart passlib from fastapi.templating import (
Jinja2Templates,
) # pip install fastapi uvicorn jinja2 python-multipart passlib
from starlette.middleware.sessions import SessionMiddleware from starlette.middleware.sessions import SessionMiddleware
from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.exceptions import HTTPException as StarletteHTTPException
from starlette.status import HTTP_302_FOUND from starlette.status import HTTP_302_FOUND
import json import json
from pathlib import Path from pathlib import Path
from typing import Optional
from fastapi import Query
if platform.system() in ["Darwin"]: # macOS or Windows
if platform.system() in ['Darwin']: # macOS or Windows
from MockCAN import CANBackend from MockCAN import CANBackend
else : else:
from classCAN import CANBackend # Your real backend from classCAN import CANBackend # Your real backend
app = FastAPI() app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="your_super_secret_key") app.add_middleware(SessionMiddleware, secret_key="your_super_secret_key")
router = APIRouter() router = APIRouter()
@ -53,12 +56,15 @@ PASSWORD = CREDENTIALS["password"]
# ======== LOGIN & SESSION HANDLING ======== # ======== LOGIN & SESSION HANDLING ========
def require_login(request: Request): def require_login(request: Request):
user = request.session.get("user") user = request.session.get("user")
if user != USERNAME: if user != USERNAME:
# raise 302 to trigger redirection manually (FastAPI doesn't support redirects from Depends directly) # raise 302 to trigger redirection manually (FastAPI doesn't support redirects from Depends directly)
raise StarletteHTTPException(status_code=302, detail="Redirect to login") raise StarletteHTTPException(status_code=302, detail="Redirect to login")
return user return user
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
def login_form(request: Request): def login_form(request: Request):
return templates.TemplateResponse("login.html", {"request": request}) return templates.TemplateResponse("login.html", {"request": request})
@ -69,7 +75,9 @@ def login(request: Request, username: str = Form(...), password: str = Form(...)
if username == USERNAME and password == PASSWORD: if username == USERNAME and password == PASSWORD:
request.session["user"] = username request.session["user"] = username
return RedirectResponse("/control", status_code=HTTP_302_FOUND) return RedirectResponse("/control", status_code=HTTP_302_FOUND)
return templates.TemplateResponse("login.html", {"request": request, "error": "Invalid credentials.json"}) return templates.TemplateResponse(
"login.html", {"request": request, "error": "Invalid credentials.json"}
)
@app.get("/logout") @app.get("/logout")
@ -80,19 +88,23 @@ def logout(request: Request):
# ======== PROTECTED INTERFACE ======== # ======== PROTECTED INTERFACE ========
@app.get("/control", response_class=HTMLResponse) @app.get("/control", response_class=HTMLResponse)
def control_page(request: Request): def control_page(request: Request):
if request.session.get("user") != USERNAME: if request.session.get("user") != USERNAME:
return RedirectResponse("/", status_code=HTTP_302_FOUND) return RedirectResponse("/", status_code=HTTP_302_FOUND)
return templates.TemplateResponse("control.html", {"request": request}) return templates.TemplateResponse("control.html", {"request": request})
@app.get("/monitor-page", response_class=HTMLResponse) @app.get("/monitor-page", response_class=HTMLResponse)
def monitor_page(request: Request): def monitor_page(request: Request):
with open("static/monitor.html") as f: with open("static/monitor.html") as f:
return HTMLResponse(f.read()) return HTMLResponse(f.read())
# ======== CAN + BACKEND ROUTES ======== # ======== CAN + BACKEND ROUTES ========
@app.post("/connect_toggle") @app.post("/connect_toggle")
def connect_toggle(): def connect_toggle():
logging.info("Toggling CAN connection...") logging.info("Toggling CAN connection...")
@ -109,8 +121,13 @@ def connect_toggle():
@app.post("/command/{state}/pu/{pu_number}") @app.post("/command/{state}/pu/{pu_number}")
def send_command(state: str, pu_number: int, ploop_setpoint: float = Query(...)): def send_command(state: str, pu_number: int, ploop_setpoint: float = Query(...)):
VALID_STATES = { VALID_STATES = {
"IDLE", "PRE-PRODUCTION", "PRODUCTION", "FIRST_START", "IDLE",
"THERMALLOOPCLEANING", "DISINFECTION", "SLEEP" "PRE-PRODUCTION",
"PRODUCTION",
"FIRST_START",
"THERMALLOOPCLEANING",
"DISINFECTION",
"SLEEP",
} }
state = state.upper() state = state.upper()
@ -128,7 +145,7 @@ def send_command(state: str, pu_number: int, ploop_setpoint: float = Query(...))
"command": state, "command": state,
"pu": pu_number, "pu": pu_number,
"ploop_setpoint": ploop_setpoint, "ploop_setpoint": ploop_setpoint,
"current_state": current_state "current_state": current_state,
} }
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@ -145,9 +162,6 @@ def get_pu_status():
return JSONResponse(content=states) return JSONResponse(content=states)
from typing import Optional
from fastapi import Query
@app.get("/monitor") @app.get("/monitor")
def get_monitor_data(pu_number: Optional[int] = Query(None)): def get_monitor_data(pu_number: Optional[int] = Query(None)):
def format_data(data): def format_data(data):
@ -156,30 +170,22 @@ def get_monitor_data(pu_number: Optional[int] = Query(None)):
"Qdilute": data.get("FM2", 0.0), "Qdilute": data.get("FM2", 0.0),
"Qdrain": data.get("FM3", 0.0), "Qdrain": data.get("FM3", 0.0),
"Qrecirc": data.get("FM4", 0.0), "Qrecirc": data.get("FM4", 0.0),
"Pro": data.get("PS1", 0.0), "Pro": data.get("PS1", 0.0),
"Pdilute": data.get("PS2", 0.0), "Pdilute": data.get("PS2", 0.0),
"Prentate": data.get("PS3", 0.0), "Prentate": data.get("PS3", 0.0),
"Conductivity": data.get("Cond", 0.0), "Conductivity": data.get("Cond", 0.0),
"MV02": data.get("MV02", 0.0), "MV02": data.get("MV02", 0.0),
"MV02_sp": data.get("MV02_sp", 0.0), "MV02_sp": data.get("MV02_sp", 0.0),
"MV03": data.get("MV03", 0.0), "MV03": data.get("MV03", 0.0),
"MV03_sp": data.get("MV03_sp", 0.0), "MV03_sp": data.get("MV03_sp", 0.0),
"MV05": data.get("MV05", 0.0), "MV05": data.get("MV05", 0.0),
"MV05_sp": data.get("MV05_sp", 0.0), "MV05_sp": data.get("MV05_sp", 0.0),
"MV06": data.get("MV06", 0.0), "MV06": data.get("MV06", 0.0),
"MV06_sp": data.get("MV06_sp", 0.0), "MV06_sp": data.get("MV06_sp", 0.0),
"MV07": data.get("MV07", 0.0), "MV07": data.get("MV07", 0.0),
"MV07_sp": data.get("MV07_sp", 0.0), "MV07_sp": data.get("MV07_sp", 0.0),
"MV08": data.get("MV08", 0.0), "MV08": data.get("MV08", 0.0),
"MV08_sp": data.get("MV08_sp", 0.0) "MV08_sp": data.get("MV08_sp", 0.0),
} }
if pu_number is not None: if pu_number is not None:
@ -193,7 +199,7 @@ def get_monitor_data(pu_number: Optional[int] = Query(None)):
print(f"[MONITOR] PU{pu}: {data}") print(f"[MONITOR] PU{pu}: {data}")
all_data[f"PU_{pu}"] = format_data(data) all_data[f"PU_{pu}"] = format_data(data)
return all_data return all_data
@app.get("/can_status") @app.get("/can_status")
def can_status(): def can_status():
@ -201,10 +207,19 @@ def can_status():
return {"connected": can_backend.connected} return {"connected": can_backend.connected}
@app.post("/command/feed_valve")
def can_status(MV01_opening: int = Query(...)):
"""Control MV01 feed valve"""
# can_backend.send_command(MV01_opening) # TODO: TODO
logging.info(f"Feed valve opening to {MV01_opening}")
return {"status": "ok"}
app.include_router(router) app.include_router(router)
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run( uvicorn.run(
"main:app", "main:app",
host="127.0.0.1", host="127.0.0.1",

View File

@ -10,14 +10,13 @@
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-x: hidden; /* ✅ prevent horizontal scroll */ overflow-x: hidden;
height: 100vh; height: 100vh;
background-color: #121212; background-color: #121212;
color: white; color: white;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.header { .header {
background-color: #1e1e1e; background-color: #1e1e1e;
padding: 10px 20px; padding: 10px 20px;
@ -45,7 +44,6 @@
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;
@ -77,8 +75,8 @@
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 { background-color: #00C851; } .mode-block button.active { background-color: #00C851; }
.mode-block button.in-progress { background-color: #ffcc00; color: #000; } .mode-block button.in-progress { background-color: #ffcc00; color: #000; }
@ -88,17 +86,14 @@
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 { margin-top: 20px; } .pu-status { margin-top: 20px; }
.pu-item { .pu-item {
background-color: #333; background-color: #333;
@ -148,7 +143,6 @@
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;
@ -178,6 +172,23 @@
font-weight: bold; font-weight: bold;
} }
.monitor-link:hover { background-color: #0056b3; } .monitor-link:hover { background-color: #0056b3; }
.feed-valve-buttons {
display: flex;
gap: 10px;
margin-top: 10px;
}
.feed-valve-buttons button {
flex: 1;
padding: 10px;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #444;
color: white;
}
.feed-valve-buttons button.active {
background-color: #00C851;
}
</style> </style>
</head> </head>
<body> <body>
@ -230,6 +241,11 @@
<div class="pu-item"><span>PU 2: </span><span id="pu2-status">Offline</span></div> <div class="pu-item"><span>PU 2: </span><span id="pu2-status">Offline</span></div>
<div class="pu-item"><span>PU 3: </span><span id="pu3-status">Offline</span></div> <div class="pu-item"><span>PU 3: </span><span id="pu3-status">Offline</span></div>
</div> </div>
<div class="feed-valve-buttons">
<button onclick="setFeedValve(0, this)">Feed Valve 0%</button>
<button onclick="setFeedValve(50, this)">Feed Valve 50%</button>
<button onclick="setFeedValve(100, this)">Feed Valve 100%</button>
</div>
</div> </div>
<div class="right-panel"> <div class="right-panel">
<div class="monitor-block"> <div class="monitor-block">
@ -266,48 +282,39 @@
</div> </div>
</div> </div>
</div> </div>
<script> <script>
function updatePloopSetpoint(value) { function updatePloopSetpoint(value) {
document.getElementById('currentValue').textContent = value; document.getElementById('currentValue').textContent = value;
} }
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();
const connectButton = document.getElementById('connectButton'); const connectButton = document.getElementById('connectButton');
connectButton.classList.toggle('connected', data.connected); connectButton.classList.toggle('connected', data.connected);
connectButton.innerHTML = `<i class="fas fa-power-off"></i> ${data.connected ? 'Disconnect' : 'Connect'}`; connectButton.innerHTML = `<i class="fas fa-power-off"></i> ${data.connected ? 'Disconnect' : 'Connect'}`;
} }
async function sendCommand(state, puNumber, buttonEl) { async function sendCommand(state, puNumber, buttonEl) {
const ploopSetpoint = document.getElementById('ploopSetpoint').value; const ploopSetpoint = document.getElementById('ploopSetpoint').value;
// Send command to backend
await fetch(`/command/${state}/pu/${puNumber}?ploop_setpoint=${ploopSetpoint}`, { method: 'POST' }); await fetch(`/command/${state}/pu/${puNumber}?ploop_setpoint=${ploopSetpoint}`, { method: 'POST' });
// Reset button styles
document.querySelectorAll('button').forEach(btn => { document.querySelectorAll('button').forEach(btn => {
btn.classList.remove('in-progress', 'ready', 'production'); btn.classList.remove('in-progress', 'ready', 'production');
}); });
// Handle PRE-PRODUCTION sequence
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; buttonEl.disabled = true;
const checkReady = async () => { const checkReady = async () => {
const res = await fetch(`/api/pu_status`); const res = await fetch(`/api/pu_status`);
const states = await res.json(); const states = await res.json();
const currentState = states[`PU${puNumber}`]; const currentState = states[`PU${puNumber}`];
if (currentState === 'SYSTEM_MODE_READY') { if (currentState === 'SYSTEM_MODE_READY') {
buttonEl.classList.remove('in-progress'); buttonEl.classList.remove('in-progress');
buttonEl.classList.add('ready'); buttonEl.classList.add('ready');
buttonEl.textContent = `START PRODUCTION PU ${puNumber}`; buttonEl.textContent = `START PRODUCTION PU ${puNumber}`;
buttonEl.disabled = false; buttonEl.disabled = false;
buttonEl.onclick = async () => { buttonEl.onclick = async () => {
await sendCommand("PRODUCTION", puNumber, buttonEl); await sendCommand("PRODUCTION", puNumber, buttonEl);
buttonEl.classList.remove('ready'); buttonEl.classList.remove('ready');
@ -319,19 +326,15 @@ function updatePloopSetpoint(value) {
setTimeout(checkReady, 1000); setTimeout(checkReady, 1000);
} }
}; };
checkReady(); checkReady();
} else if (state === 'PRODUCTION') { } else if (state === 'PRODUCTION') {
buttonEl.classList.add('production'); buttonEl.classList.add('production');
buttonEl.textContent = `PRODUCTION ON PU ${puNumber}`; buttonEl.textContent = `PRODUCTION ON PU ${puNumber}`;
} else if (state === 'IDLE' || state === 'FIRST_START') { } else if (state === 'IDLE' || state === 'FIRST_START') {
buttonEl.classList.remove('in-progress', 'ready', 'production'); buttonEl.classList.remove('in-progress', 'ready', 'production');
buttonEl.classList.add('production'); buttonEl.classList.add('production');
buttonEl.textContent = `${state.replace('_', ' ')} PU ${puNumber}`; buttonEl.textContent = `${state.replace('_', ' ')} PU ${puNumber}`;
// === Reset PRE-PROD button ===
const preProdBtn = document.querySelector(`button[data-action="PRE-PRODUCTION"][data-pu="${puNumber}"]`); const preProdBtn = document.querySelector(`button[data-action="PRE-PRODUCTION"][data-pu="${puNumber}"]`);
if (preProdBtn) { if (preProdBtn) {
preProdBtn.classList.remove('in-progress', 'ready', 'production'); preProdBtn.classList.remove('in-progress', 'ready', 'production');
@ -340,7 +343,6 @@ function updatePloopSetpoint(value) {
preProdBtn.onclick = () => sendCommand("PRE-PRODUCTION", puNumber, preProdBtn); preProdBtn.onclick = () => sendCommand("PRE-PRODUCTION", puNumber, preProdBtn);
} }
// === Reset IDLE button ===
const idleBtn = document.querySelector(`button[data-action="IDLE"][data-pu="${puNumber}"]`); const idleBtn = document.querySelector(`button[data-action="IDLE"][data-pu="${puNumber}"]`);
if (idleBtn && idleBtn !== buttonEl) { if (idleBtn && idleBtn !== buttonEl) {
idleBtn.classList.remove('in-progress', 'ready', 'production'); idleBtn.classList.remove('in-progress', 'ready', 'production');
@ -349,7 +351,6 @@ function updatePloopSetpoint(value) {
idleBtn.onclick = () => sendCommand("IDLE", puNumber, idleBtn); idleBtn.onclick = () => sendCommand("IDLE", puNumber, idleBtn);
} }
// === Reset FIRST START button ===
const firstStartBtn = document.querySelector(`button[data-action="FIRST_START"][data-pu="${puNumber}"]`); const firstStartBtn = document.querySelector(`button[data-action="FIRST_START"][data-pu="${puNumber}"]`);
if (firstStartBtn && firstStartBtn !== buttonEl) { if (firstStartBtn && firstStartBtn !== buttonEl) {
firstStartBtn.classList.remove('in-progress', 'ready', 'production'); firstStartBtn.classList.remove('in-progress', 'ready', 'production');
@ -360,105 +361,98 @@ function updatePloopSetpoint(value) {
} }
} }
async function fetchPUStatus() { async function setFeedValve(opening, buttonEl) {
const response = await fetch("/api/pu_status"); await fetch(`/command/feed_valve?MV01_opening=${opening}`, { method: 'POST' });
const data = await response.json(); document.querySelectorAll('.feed-valve-buttons button').forEach(btn => {
document.getElementById("pu1-status").textContent = data.PU1 || "Unknown"; btn.classList.remove('active');
document.getElementById("pu2-status").textContent = data.PU2 || "Unknown"; });
document.getElementById("pu3-status").textContent = data.PU3 || "Unknown"; buttonEl.classList.add('active');
}
async function fetchPUStatus() {
const response = await fetch("/api/pu_status");
const data = await response.json();
document.getElementById("pu1-status").textContent = data.PU1 || "Unknown";
document.getElementById("pu2-status").textContent = data.PU2 || "Unknown";
document.getElementById("pu3-status").textContent = data.PU3 || "Unknown";
}
fetchPUStatus();
setInterval(fetchPUStatus, 5000);
async function updateMonitorData() {
const response = await fetch('/monitor');
const data = await response.json();
for (const [puId, puData] of Object.entries(data)) {
const container = document.getElementById(puId);
if (!container) continue;
container.innerHTML = `
<h3>${puId}</h3>
<div class="monitor-value">Q_perm<br>${puData.Qperm.toFixed(1)} L/h</div>
<div class="monitor-value">Q_dilute<br>${puData.Qdilute.toFixed(1)} L/h</div>
<div class="monitor-value">Q_drain<br>${puData.Qdrain.toFixed(1)} L/h</div>
<div class="monitor-value">Q_recirc<br>${puData.Qrecirc.toFixed(1)} L/h</div>
<div class="monitor-value">P_ro<br>${puData.Pro.toFixed(1)} bar</div>
<div class="monitor-value">P_dilute<br>${puData.Pdilute.toFixed(1)} bar</div>
<div class="monitor-value">P_rentate<br>${puData.Prentate.toFixed(1)} bar</div>
<div class="monitor-value">Conductivity<br>${puData.Conductivity.toFixed(1)} µS/cm</div>
<div class="monitor-value">MV02<br>${puData.MV02.toFixed(1)} % (sp: ${puData.MV02_sp.toFixed(1)})</div>
<div class="monitor-value">MV03<br>${puData.MV03.toFixed(1)} % (sp: ${puData.MV03_sp.toFixed(1)})</div>
<div class="monitor-value">MV05<br>${puData.MV05.toFixed(1)} % (sp: ${puData.MV05_sp.toFixed(1)})</div>
<div class="monitor-value">MV06<br>${puData.MV06.toFixed(1)} % (sp: ${puData.MV06_sp.toFixed(1)})</div>
<div class="monitor-value">MV07<br>${puData.MV07.toFixed(1)} % (sp: ${puData.MV07_sp.toFixed(1)})</div>
<div class="monitor-value">MV08<br>${puData.MV08.toFixed(1)} % (sp: ${puData.MV08_sp.toFixed(1)})</div>
`;
} }
fetchPUStatus(); }
setInterval(fetchPUStatus, 5000);
async function updateMonitorData() { function updateMonitorValues(id, values, unit) {
const response = await fetch('/monitor'); const container = document.getElementById(id);
const data = await response.json(); // data = { PU_1: {...}, PU_2: {...}, PU_3: {...} } const valueElements = container.querySelectorAll('.monitor-value');
valueElements.forEach((element, index) => {
if (index < values.length) {
element.innerHTML = `#${index + 1}<br>${values[index]} ${unit}`;
}
});
}
for (const [puId, puData] of Object.entries(data)) { setInterval(updateMonitorData, 1000);
const container = document.getElementById(puId);
async function fetchMonitorData() {
try {
const puMap = {
"PU_1": 1,
"PU_2": 2,
"PU_3": 3
};
const fields = {
"Qperm": "L/h",
"Pdilute": "bar",
"Conductivity": "µS/cm",
"Pro": "bar"
};
const dataResponse = await fetch('/monitor');
const allData = await dataResponse.json();
for (const [fieldId, unit] of Object.entries(fields)) {
const container = document.getElementById(fieldId);
if (!container) continue; if (!container) continue;
container.innerHTML = `
<h3>${puId}</h3>
<div class="monitor-value">Q_perm<br>${puData.Qperm.toFixed(1)} L/h</div>
<div class="monitor-value">Q_dilute<br>${puData.Qdilute.toFixed(1)} L/h</div>
<div class="monitor-value">Q_drain<br>${puData.Qdrain.toFixed(1)} L/h</div>
<div class="monitor-value">Q_recirc<br>${puData.Qrecirc.toFixed(1)} L/h</div>
<div class="monitor-value">P_ro<br>${puData.Pro.toFixed(1)} bar</div>
<div class="monitor-value">P_dilute<br>${puData.Pdilute.toFixed(1)} bar</div>
<div class="monitor-value">P_rentate<br>${puData.Prentate.toFixed(1)} bar</div>
<div class="monitor-value">Conductivity<br>${puData.Conductivity.toFixed(1)} µS/cm</div>
<div class="monitor-value">MV02<br>${puData.MV02.toFixed(1)} % (sp: ${puData.MV02_sp.toFixed(1)})</div>
<div class="monitor-value">MV03<br>${puData.MV03.toFixed(1)} % (sp: ${puData.MV03_sp.toFixed(1)})</div>
<div class="monitor-value">MV05<br>${puData.MV05.toFixed(1)} % (sp: ${puData.MV05_sp.toFixed(1)})</div>
<div class="monitor-value">MV06<br>${puData.MV06.toFixed(1)} % (sp: ${puData.MV06_sp.toFixed(1)})</div>
<div class="monitor-value">MV07<br>${puData.MV07.toFixed(1)} % (sp: ${puData.MV07_sp.toFixed(1)})</div>
<div class="monitor-value">MV08<br>${puData.MV08.toFixed(1)} % (sp: ${puData.MV08_sp.toFixed(1)})</div>
`;
}
}
function updateMonitorValues(id, values, unit) {
const container = document.getElementById(id);
const valueElements = container.querySelectorAll('.monitor-value'); const valueElements = container.querySelectorAll('.monitor-value');
valueElements.forEach((element, index) => { let index = 0;
if (index < values.length) { for (const [puLabel, puData] of Object.entries(allData)) {
element.innerHTML = `#${index + 1}<br>${values[index]} ${unit}`; const value = puData[fieldId] ?? 0.0;
} if (valueElements[index]) {
}); valueElements[index].innerHTML = `#${index + 1}<br>${value.toFixed(1)} ${unit}`;
}
// Update monitor data every second
setInterval(updateMonitorData, 1000);
</script>
<script>
async function fetchMonitorData() {
try {
const puMap = {
"PU_1": 1,
"PU_2": 2,
"PU_3": 3
};
const fields = {
"Qperm": "L/h",
"Pdilute": "bar",
"Conductivity": "µS/cm",
"Pro": "bar"
};
const dataResponse = await fetch('/monitor');
const allData = await dataResponse.json(); // Returns {PU_1: {...}, PU_2: {...}, PU_3: {...}}
for (const [fieldId, unit] of Object.entries(fields)) {
const container = document.getElementById(fieldId);
if (!container) continue;
const valueElements = container.querySelectorAll('.monitor-value');
let index = 0;
for (const [puLabel, puData] of Object.entries(allData)) {
const value = puData[fieldId] ?? 0.0;
// Reuse DOM element if it exists
if (valueElements[index]) {
valueElements[index].innerHTML = `#${index + 1}<br>${value.toFixed(1)} ${unit}`;
}
index++;
} }
index++;
} }
} catch (error) {
console.error('Error fetching monitor data:', error);
} }
} catch (error) {
console.error('Error fetching monitor data:', error);
} }
}
setInterval(fetchMonitorData, 1000); setInterval(fetchMonitorData, 1000);
fetchMonitorData(); // initial load fetchMonitorData();
</script> </script>
</body> </body>
</html> </html>