Separates into 3 monitor pages

This commit is contained in:
Etienne Chassaing 2025-07-15 14:56:52 +02:00
parent f3cd47432c
commit 656ae95f19
4 changed files with 142 additions and 109 deletions

View File

@ -29,26 +29,51 @@ class CANBackend:
def get_latest_data(self, pu_number=1, data={}): 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 { if pu_number ==1:
"FM2": round(1000 * np.random.random(), 1), return {
"FM3": round(1000 * np.random.random(), 1), "FM2": 1080,
"FM4": round(1000 * np.random.random(), 1), "FM3": 1080,
"FM5": round(1000 * np.random.random(), 1), "FM4": 1080,
"PS1": round(10 * np.random.random(), 2), "FM5": 1080,
"PS2": round(10 * np.random.random(), 2), "PS1": 6.2,
"PS3": round(10 * np.random.random(), 2), "PS2": 6.2,
"PS4": round(10 * np.random.random(), 2), "PS3": 6.2,
"Cond": 1* np.random.random(), "PS4": 6.2,
"MV02": round(100 * np.random.random(), 2), "Cond": 1* np.random.random(),
"MV02_sp": round(100 * np.random.random(), 2), "MV02": round(100 * np.random.random(), 2),
"MV03": round(100 * np.random.random(), 2), "MV02_sp": round(100 * np.random.random(), 2),
"MV03_sp": round(100 * np.random.random(), 2), "MV03": round(100 * np.random.random(), 2),
"MV05": round(100 * np.random.random(), 2), "MV03_sp": round(100 * np.random.random(), 2),
"MV05_sp": round(100 * np.random.random(), 2), "MV05": round(100 * np.random.random(), 2),
"MV06": round(100 * np.random.random(), 2), "MV05_sp": round(100 * np.random.random(), 2),
"MV06_sp": round(100 * np.random.random(), 2), "MV06": round(100 * np.random.random(), 2),
"MV07": round(100 * np.random.random(), 2), "MV06_sp": round(100 * np.random.random(), 2),
"MV07_sp": round(100 * np.random.random(), 2), "MV07": round(100 * np.random.random(), 2),
"MV08": round(100 * np.random.random(), 2), "MV07_sp": round(100 * np.random.random(), 2),
"MV08_sp": round(100 * np.random.random(), 2), "MV08": round(100 * np.random.random(), 2),
} "MV08_sp": round(100 * np.random.random(), 2),
}
else :
return {
"FM2": round(1000 * np.random.random(), 1),
"FM3": round(1000 * np.random.random(), 1),
"FM4": round(1000 * np.random.random(), 1),
"FM5": round(1000 * np.random.random(), 1),
"PS1": round(10 * np.random.random(), 2),
"PS2": round(10 * np.random.random(), 2),
"PS3": round(10 * np.random.random(), 2),
"PS4": round(10 * np.random.random(), 2),
"Cond": 1* np.random.random(),
"MV02": round(100 * np.random.random(), 2),
"MV02_sp": round(100 * np.random.random(), 2),
"MV03": round(100 * np.random.random(), 2),
"MV03_sp": round(100 * np.random.random(), 2),
"MV05": round(100 * np.random.random(), 2),
"MV05_sp": round(100 * np.random.random(), 2),
"MV06": round(100 * np.random.random(), 2),
"MV06_sp": round(100 * np.random.random(), 2),
"MV07": round(100 * np.random.random(), 2),
"MV07_sp": round(100 * np.random.random(), 2),
"MV08": round(100 * np.random.random(), 2),
"MV08_sp": round(100 * np.random.random(), 2),
}

15
main.py
View File

@ -26,19 +26,9 @@ app.add_middleware(SessionMiddleware, secret_key="your_super_secret_key")
router = APIRouter() router = APIRouter()
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
can_backend = CANBackend() can_backend = CANBackend()
# Mount static files
app.mount("/static", StaticFiles(directory="static"), name="static")
can_backend = CANBackend()
# Serve static files (HTML, JS, CSS) # Serve static files (HTML, JS, CSS)
app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="static"), name="static")
# Jinja2 templates
templates = Jinja2Templates(directory="templates")
# CREDENTIALS # CREDENTIALS
@ -164,6 +154,7 @@ def get_pu_status():
@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)):
logging.info(f"[MONITOR DATA] {pu_number}")
def format_data(data): def format_data(data):
return { return {
"Qperm": data.get("FM2", 0.0), "Qperm": data.get("FM2", 0.0),
@ -192,8 +183,8 @@ def get_monitor_data(pu_number: Optional[int] = Query(None)):
} }
all_data = {} all_data = {}
for pu in [1, 2, 3]: for pu in [1, 2, 3]:
data = can_backend.get_latest_data(pu) data = can_backend.get_latest_data(pu_number=pu)
print(f"[MONITOR] PU{pu}: {data}") logging.info(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

View File

@ -28,12 +28,6 @@
h1 { h1 {
text-align: center; text-align: center;
} }
#puSelector {
display: block;
margin: 10px auto 20px auto;
font-size: 16px;
padding: 5px 10px;
}
#recordButton { #recordButton {
background-color: #ff4444; background-color: #ff4444;
color: white; color: white;
@ -47,13 +41,7 @@
</style> </style>
</head> </head>
<body> <body>
<h1>Live Monitoring Dashboard</h1> <h1 id="pageTitle">Live Monitoring Dashboard</h1>
<label for="puSelector" style="text-align:center; display:block;">Select PU:</label>
<select id="puSelector">
<option value="1">PU 1</option>
<option value="2">PU 2</option>
<option value="3">PU 3</option>
</select>
<button id="recordButton" onclick="toggleRecording()">Record</button> <button id="recordButton" onclick="toggleRecording()">Record</button>
<div class="plot-container"> <div class="plot-container">
<div id="flow-plot" class="large-plot"></div> <div id="flow-plot" class="large-plot"></div>
@ -66,6 +54,11 @@
<div id="mv08-plot" class="small-plot"></div> <div id="mv08-plot" class="small-plot"></div>
</div> </div>
<script> <script>
// Extract PU number from URL
const urlParams = new URLSearchParams(window.location.search);
const puNumber = urlParams.get('pu_number') || '1'; // Default to PU 1 if not specified
document.getElementById('pageTitle').textContent = `Live Monitoring Dashboard - PU ${puNumber}`;
let isRecording = false; let isRecording = false;
let recordedData = []; let recordedData = [];
let recordingInterval; let recordingInterval;
@ -78,7 +71,7 @@
recordButton.style.backgroundColor = '#ff0000'; recordButton.style.backgroundColor = '#ff0000';
recordButton.textContent = 'Stop Recording'; recordButton.textContent = 'Stop Recording';
recordedData = []; recordedData = [];
csvFileName = `monitoring_data_${new Date().toISOString().replace(/[:.]/g, '-')}.csv`; csvFileName = `monitoring_data_PU${puNumber}_${new Date().toISOString().replace(/[:.]/g, '-')}.csv`;
startRecording(); startRecording();
} else { } else {
isRecording = false; isRecording = false;
@ -90,28 +83,29 @@
function startRecording() { function startRecording() {
recordingInterval = setInterval(async () => { recordingInterval = setInterval(async () => {
const response = await fetch(`/monitor?pu_number=${document.getElementById('puSelector').value}`); const response = await fetch('/monitor');
if (!response.ok) { if (!response.ok) {
console.error(`HTTP error! status: ${response.status}`); console.error(`HTTP error! status: ${response.status}`);
return; return;
} }
const pu = await response.json(); const allData = await response.json();
const puData = allData[`PU_${puNumber}`];
recordedData.push({ recordedData.push({
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
Qperm: pu.Qperm, Qperm: puData.Qperm,
Qdilute: pu.Qdilute, Qdilute: puData.Qdilute,
Qdrain: pu.Qdrain, Qdrain: puData.Qdrain,
Qrecirc: pu.Qrecirc, Qrecirc: puData.Qrecirc,
Pro: pu.Pro, Pro: puData.Pro,
Pdilute: pu.Pdilute, Pdilute: puData.Pdilute,
Prentate: pu.Prentate, Prentate: puData.Prentate,
MV02: pu.MV02, MV02: puData.MV02,
MV03: pu.MV03, MV03: puData.MV03,
MV04: pu.MV04, MV04: puData.MV04,
MV05: pu.MV05, MV05: puData.MV05,
MV06: pu.MV06, MV06: puData.MV06,
MV07: pu.MV07, MV07: puData.MV07,
MV08: pu.MV08 MV08: puData.MV08
}); });
}, 100); }, 100);
} }
@ -124,7 +118,6 @@
recordedData.map(row => recordedData.map(row =>
`${row.timestamp},${row.Qperm},${row.Qdilute},${row.Qdrain},${row.Qrecirc},${row.Pro},${row.Pdilute},${row.Prentate},${row.MV02},${row.MV03},${row.MV04},${row.MV05},${row.MV06},${row.MV07},${row.MV08}` `${row.timestamp},${row.Qperm},${row.Qdilute},${row.Qdrain},${row.Qrecirc},${row.Pro},${row.Pdilute},${row.Prentate},${row.MV02},${row.MV03},${row.MV04},${row.MV05},${row.MV06},${row.MV07},${row.MV08}`
).join("\n"); ).join("\n");
const encodedUri = encodeURI(csvContent); const encodedUri = encodeURI(csvContent);
const link = document.createElement("a"); const link = document.createElement("a");
link.setAttribute("href", encodedUri); link.setAttribute("href", encodedUri);
@ -140,14 +133,8 @@
} }
}; };
// Existing plot initialization and update functions
const maxPoints = 100; const maxPoints = 100;
const time = () => new Date(); const time = () => new Date();
let selectedPU = 1;
document.getElementById("puSelector").addEventListener("change", function () {
selectedPU = parseInt(this.value);
});
function getLastMinuteRange() { function getLastMinuteRange() {
const now = new Date(); const now = new Date();
@ -157,28 +144,28 @@
async function updatePlots() { async function updatePlots() {
try { try {
const response = await fetch(`/monitor?pu_number=${selectedPU}`); const response = await fetch('/monitor');
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const pu = await response.json(); const allData = await response.json();
const puData = allData[`PU_${puNumber}`];
const t = time(); const t = time();
Plotly.extendTraces('flow-plot', { Plotly.extendTraces('flow-plot', {
x: [[t], [t], [t], [t]], x: [[t], [t], [t], [t]],
y: [[pu.Qperm], [pu.Qdilute], [pu.Qdrain], [pu.Qrecirc]] y: [[puData.Qperm], [puData.Qdilute], [puData.Qdrain], [puData.Qrecirc]]
}, [0, 1, 2, 3], maxPoints); }, [0, 1, 2, 3], maxPoints);
Plotly.extendTraces('pressure-plot', { Plotly.extendTraces('pressure-plot', {
x: [[t], [t], [t]], x: [[t], [t], [t]],
y: [[pu.Pro], [pu.Pdilute], [pu.Prentate]] y: [[puData.Pro], [puData.Pdilute], [puData.Prentate]]
}, [0, 1, 2], maxPoints); }, [0, 1, 2], maxPoints);
// // Plotly.extendTraces('mv02-plot', { x: [[t]], y: [[puData.MV02]] }, [0]);
// Plotly.extendTraces('mv02-plot', { x: [[t]], y: [[pu.MV02]] }, [0]); // Plotly.extendTraces('mv03-plot', { x: [[t]], y: [[puData.MV03]] }, [0]);
// Plotly.extendTraces('mv03-plot', { x: [[t]], y: [[pu.MV03]] }, [0]);
// Plotly.extendTraces('mv04-05-plot', { // Plotly.extendTraces('mv04-05-plot', {
// x: [[t], [t]], // x: [[t], [t]],
// y: [[pu.MV04], [pu.MV05]] // y: [[puData.MV04], [puData.MV05]]
// }, [0, 1]); // }, [0, 1]);
// Plotly.extendTraces('mv06-plot', { x: [[t]], y: [[pu.MV06]] }, [0]); // Plotly.extendTraces('mv06-plot', { x: [[t]], y: [[puData.MV06]] }, [0]);
// Plotly.extendTraces('mv07-plot', { x: [[t]], y: [[pu.MV07]] }, [0]); // Plotly.extendTraces('mv07-plot', { x: [[t]], y: [[puData.MV07]] }, [0]);
// Plotly.extendTraces('mv08-plot', { x: [[t]], y: [[pu.MV08]] }, [0]); // Plotly.extendTraces('mv08-plot', { x: [[t]], y: [[puData.MV08]] }, [0]);
const range = getLastMinuteRange(); const range = getLastMinuteRange();
const plotIds = [ const plotIds = [
@ -245,7 +232,7 @@
// }], { // }], {
// title: 'MV08 (%)', yaxis: { range: [0, 100] }, xaxis: { type: 'date' } // title: 'MV08 (%)', yaxis: { range: [0, 100] }, xaxis: { type: 'date' }
// }); // });
setInterval(updatePlots, 1000); setInterval(updatePlots, 250);
} }
window.onload = initPlots; window.onload = initPlots;

View File

@ -36,7 +36,9 @@
align-items: center; align-items: center;
gap: 10px; gap: 10px;
} }
.connected { background-color: #00C851; } .connected {
background-color: #00C851;
}
.container { .container {
display: flex; display: flex;
flex: 1; flex: 1;
@ -63,7 +65,10 @@
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
} }
.pu-buttons { display: flex; gap: 10px; } .pu-buttons {
display: flex;
gap: 10px;
}
.mode-block button { .mode-block button {
background-color: #4285F4; background-color: #4285F4;
color: white; color: white;
@ -78,10 +83,21 @@
.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 {
.mode-block button.in-progress { background-color: #ffcc00; color: #000; } background-color: #00C851;
.mode-block button.ready { background-color: #00C851; color: #fff; } }
.mode-block button.disabled { background-color: #777; cursor: not-allowed; } .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 { .in-progress {
background-color: yellow !important; background-color: yellow !important;
color: black !important; color: black !important;
@ -94,7 +110,9 @@
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;
padding: 10px; padding: 10px;
@ -171,7 +189,9 @@
text-decoration: none; text-decoration: none;
font-weight: bold; font-weight: bold;
} }
.monitor-link:hover { background-color: #0056b3; } .monitor-link:hover {
background-color: #0056b3;
}
.feed-valve-buttons { .feed-valve-buttons {
display: flex; display: flex;
gap: 10px; gap: 10px;
@ -189,14 +209,38 @@
.feed-valve-buttons button.active { .feed-valve-buttons button.active {
background-color: #00C851; background-color: #00C851;
} }
.monitor-pu-buttons {
display: flex;
gap: 10px;
margin: 10px;
}
.monitor-pu-buttons a {
color: white;
background-color: #007bff;
padding: 10px 15px;
border-radius: 5px;
text-decoration: none;
font-weight: bold;
}
.monitor-pu-buttons a:hover {
background-color: #0056b3;
}
</style> </style>
</head> </head>
<body> <body>
<div class="header"> <div class="header">
<h1>Hydraulic Machine Control</h1> <h1>Hydraulic Machine Control</h1>
<a href="/monitor-page" target="_blank" rel="noopener noreferrer" class="monitor-link"> <div class="monitor-pu-buttons">
<i class="fas fa-chart-line"></i> Monitor <a href="/monitor-page?pu_number=1" target="_blank" class="monitor-link">
</a> <i class="fas fa-chart-line"></i> Monitor PU 1
</a>
<a href="/monitor-page?pu_number=2" target="_blank" class="monitor-link">
<i class="fas fa-chart-line"></i> Monitor PU 2
</a>
<a href="/monitor-page?pu_number=3" target="_blank" class="monitor-link">
<i class="fas fa-chart-line"></i> Monitor PU 3
</a>
</div>
<button id="connectButton" class="connect-button" onclick="toggleConnection()"> <button id="connectButton" class="connect-button" onclick="toggleConnection()">
<i class="fas fa-power-off"></i> Connect <i class="fas fa-power-off"></i> Connect
</button> </button>
@ -286,7 +330,6 @@
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();
@ -294,14 +337,12 @@ async function toggleConnection() {
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;
await fetch(`/command/${state}/pu/${puNumber}?ploop_setpoint=${ploopSetpoint}`, { method: 'POST' }); await fetch(`/command/${state}/pu/${puNumber}?ploop_setpoint=${ploopSetpoint}`, { method: 'POST' });
document.querySelectorAll('button').forEach(btn => { document.querySelectorAll('button').forEach(btn => {
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}`;
@ -334,7 +375,6 @@ async function sendCommand(state, puNumber, buttonEl) {
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}`;
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');
@ -342,7 +382,6 @@ async function sendCommand(state, puNumber, buttonEl) {
preProdBtn.disabled = false; preProdBtn.disabled = false;
preProdBtn.onclick = () => sendCommand("PRE-PRODUCTION", puNumber, preProdBtn); preProdBtn.onclick = () => sendCommand("PRE-PRODUCTION", puNumber, preProdBtn);
} }
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');
@ -350,7 +389,6 @@ async function sendCommand(state, puNumber, buttonEl) {
idleBtn.disabled = false; idleBtn.disabled = false;
idleBtn.onclick = () => sendCommand("IDLE", puNumber, idleBtn); idleBtn.onclick = () => sendCommand("IDLE", puNumber, idleBtn);
} }
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,7 +398,6 @@ async function sendCommand(state, puNumber, buttonEl) {
} }
} }
} }
async function setFeedValve(opening, buttonEl) { async function setFeedValve(opening, buttonEl) {
await fetch(`/command/feed_valve?MV01_opening=${opening}`, { method: 'POST' }); await fetch(`/command/feed_valve?MV01_opening=${opening}`, { method: 'POST' });
document.querySelectorAll('.feed-valve-buttons button').forEach(btn => { document.querySelectorAll('.feed-valve-buttons button').forEach(btn => {
@ -368,7 +405,6 @@ async function setFeedValve(opening, buttonEl) {
}); });
buttonEl.classList.add('active'); buttonEl.classList.add('active');
} }
async function fetchPUStatus() { async function fetchPUStatus() {
const response = await fetch("/api/pu_status"); const response = await fetch("/api/pu_status");
const data = await response.json(); const data = await response.json();
@ -376,10 +412,8 @@ async function fetchPUStatus() {
document.getElementById("pu2-status").textContent = data.PU2 || "Unknown"; document.getElementById("pu2-status").textContent = data.PU2 || "Unknown";
document.getElementById("pu3-status").textContent = data.PU3 || "Unknown"; document.getElementById("pu3-status").textContent = data.PU3 || "Unknown";
} }
fetchPUStatus(); fetchPUStatus();
setInterval(fetchPUStatus, 5000); setInterval(fetchPUStatus, 5000);
async function updateMonitorData() { async function updateMonitorData() {
const response = await fetch('/monitor'); const response = await fetch('/monitor');
const data = await response.json(); const data = await response.json();
@ -405,7 +439,6 @@ async function updateMonitorData() {
`; `;
} }
} }
function updateMonitorValues(id, values, unit) { function updateMonitorValues(id, values, unit) {
const container = document.getElementById(id); const container = document.getElementById(id);
const valueElements = container.querySelectorAll('.monitor-value'); const valueElements = container.querySelectorAll('.monitor-value');
@ -415,9 +448,7 @@ function updateMonitorValues(id, values, unit) {
} }
}); });
} }
setInterval(updateMonitorData, 1000); setInterval(updateMonitorData, 1000);
async function fetchMonitorData() { async function fetchMonitorData() {
try { try {
const puMap = { const puMap = {
@ -450,7 +481,6 @@ async function fetchMonitorData() {
console.error('Error fetching monitor data:', error); console.error('Error fetching monitor data:', error);
} }
} }
setInterval(fetchMonitorData, 1000); setInterval(fetchMonitorData, 1000);
fetchMonitorData(); fetchMonitorData();
</script> </script>