Adds mock CAN class for test and new layout, and Ploop setpoitn setting

This commit is contained in:
Etienne Chassaing 2025-07-08 13:54:10 +02:00
parent f411d715d8
commit f25bd813bc
4 changed files with 186 additions and 133 deletions

35
MockCAN.py Normal file
View File

@ -0,0 +1,35 @@
import numpy as np
PUs_states = [{"PU_MODE": "OFF","ploop_setpoint":0.0},{"PU_MODE": "OFF","ploop_setpoint":0.0},{"PU_MODE": "OFF","ploop_setpoint":0.0}] # data[i] is PU_i dictionnary
# Placeholder for CAN backend
class CANBackend:
def __init__(self):
self.connected = False
def connect(self, node_id: int, eds_path: str) -> bool:
# Placeholder for connection logic
self.connected = True
return True
def shutdown(self):
self.connected = False
def read_current_state(self,pu_number: int):
# Placeholder for reading mode command
return PUs_states[pu_number-1]
def send_thermal_loop_cleaning(self, mode: str):
# Placeholder for thermal loop cleaning
pass
def send_state_command(self, state: str, pu_number : int, ploop_setpoint : float):
# Placeholder for sending mode command
PUs_states[pu_number-1] = {"PU_MODE": state, "ploop_setpoint":ploop_setpoint}
def get_latest_data(self):
# Placeholder for getting latest TPDO data
return {"FM2" : round(1000*np.random.random(),1),"PS1" : round(10*np.random.random(),2)}

View File

@ -56,8 +56,14 @@ class CANBackend:
def get_latest_data(self): def get_latest_data(self):
with self.lock: with self.lock:
return self.latest_data.copy() return self.latest_data.copy()
def read_current_state(self,pu_number: int):
# Placeholder for reading mode command
#TODO : CODE IT
pass
def send_state_command(self, state: str, pu_number: int): def send_state_command(self, state: str, pu_number: int, ploop_setpoint:float):
#TODO : link ploop_setpoint
if not self.connected: if not self.connected:
raise RuntimeError("CAN not connected") raise RuntimeError("CAN not connected")

42
main.py
View File

@ -1,8 +1,12 @@
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException, Query
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
import logging import logging
from classCAN import CANBackend # Your real backend import platform
if platform.system() in ['Darwin']: # macOS or Windows
from MockCAN import CANBackend
else :
from classCAN import CANBackend # Your real backend
app = FastAPI() app = FastAPI()
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
@ -29,50 +33,26 @@ def connect_toggle():
return {"connected": True} return {"connected": True}
@app.post("/command/{state}/pu/{pu_number}") @app.post("/command/{state}/pu/{pu_number}")
def send_command(state: str, pu_number: int): def send_command(state: str, pu_number: int, ploop_setpoint: float = Query(...)):
"""Send a state command to a specific PU.""" """Send a state command to a specific PU."""
logging.info(f"Sending state '{state}' to PU {pu_number}") logging.info(f"Sending state '{state}' to PU {pu_number} with ploop_setpoint {ploop_setpoint}")
try: try:
can_backend.send_state_command(state.upper(), pu_number) can_backend.send_state_command(state.upper(), pu_number, ploop_setpoint)
return {"status": "success", "command": state.upper(), "pu": pu_number} return {"status": "success", "command": state.upper(), "pu": pu_number, "ploop_setpoint": ploop_setpoint}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@app.get("/monitor") @app.get("/monitor")
def get_monitor_data(): def get_monitor_data():
data = can_backend.get_latest_data() data = can_backend.get_latest_data()
print(f"[MONITOR] Latest SDO: {data}") print(f"[MONITOR] Latest SDO: {data}")
return { return {
"Qperm": [0.0, data.get("FM2", 0.0), 0.0], "Qperm": [data.get("FM2", 0.0), data.get("FM2", 0.0), 0.0],
"Pdilute": [data.get("PS1", 0.0), 0.0, 0.0], "Pdilute": [data.get("PS1", 0.0), 0.0, 0.0],
"Conductivity": [0.0, 0.0, 0.0], "Conductivity": [0.0, 0.0, 0.0],
"Pro": [0.0, 0.0, 0.0], "Pro": [0.0, 0.0, 0.0],
} }
#
# @app.get("/monitor")
# def get_monitor_values():
# """
# Returns current machine monitoring data.
# Expected structure:
# Q_perm: [Q1, Q2, Q3]
# Pdilute: [P1, P2, P3]
# Conductivity: [C1, C2, C3]
# Pro: [PR1, PR2, PR3]
# """
# try:
# return {
# "Q_perm": can_backend.get_q_perm(),
# "Pdilute": can_backend.get_pdilute(),
# "Conductivity": can_backend.get_conductivity(),
# "Pro": can_backend.get_pro(),
# }
# except Exception as e:
# raise HTTPException(status_code=500, detail=str(e))
@app.get("/can_status") @app.get("/can_status")
def can_status(): def can_status():
"""Return current CAN connection status.""" """Return current CAN connection status."""

View File

@ -13,12 +13,15 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
background-color: #f4f4f9; background-color: #121212;
color: white;
} }
.header { .header {
background-color: #333; background-color: #1e1e1e;
padding: 10px; padding: 10px 20px;
text-align: center; display: flex;
justify-content: space-between;
align-items: center;
} }
.connect-button { .connect-button {
background-color: #ff4444; background-color: #ff4444;
@ -28,7 +31,9 @@
font-size: 16px; font-size: 16px;
cursor: pointer; cursor: pointer;
border-radius: 5px; border-radius: 5px;
transition: background-color 0.3s; display: flex;
align-items: center;
gap: 10px;
} }
.connected { .connected {
background-color: #00C851; background-color: #00C851;
@ -43,132 +48,169 @@
overflow-y: auto; overflow-y: auto;
} }
.left-panel { .left-panel {
background-color: #1e1e1e;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 15px; gap: 10px;
background-color: #e9ecef;
} }
.mode-block { .mode-block {
border: 1px solid #ccc; background-color: #333;
padding: 15px; padding: 15px;
border-radius: 5px; border-radius: 5px;
background-color: #fff; display: flex;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); flex-direction: column;
transition: background-color 0.3s; gap: 10px;
} }
.mode-block.active { .pu-buttons {
background-color: #a5d6a7; /* Light green background for active mode */ display: flex;
} gap: 10px;
.mode-block h2 {
margin-top: 0;
color: #333;
} }
.mode-block button { .mode-block button {
background-color: #4285F4; background-color: #4285F4;
color: white; color: white;
border: none; border: none;
padding: 10px; padding: 10px;
margin: 5px 0;
cursor: pointer; cursor: pointer;
border-radius: 5px; border-radius: 5px;
width: 100%;
font-size: 14px; font-size: 14px;
transition: background-color 0.3s; transition: background-color 0.3s;
flex: 1;
} }
.mode-block button:hover { .mode-block button:hover {
background-color: #3367d6; background-color: #3367d6;
} }
.mode-block button i { .mode-block button.active {
margin-right: 8px; background-color: #00C851;
}
.pu-status {
margin-top: 20px;
}
.pu-item {
background-color: #333;
padding: 10px;
border-radius: 5px;
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
} }
.monitor-block { .monitor-block {
border: 1px solid #ccc; background-color: #333;
padding: 15px; padding: 15px;
border-radius: 5px; border-radius: 5px;
background-color: #fff;
margin-bottom: 15px; margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
} }
.monitor-block h2 { .monitor-block h2 {
margin-top: 0; margin-top: 0;
color: #333; display: flex;
align-items: center;
gap: 10px;
} }
.monitor-values { .monitor-values {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 10px; gap: 10px;
margin-top: 10px;
} }
.monitor-value { .monitor-value {
border: 1px solid #ddd; background-color: #444;
padding: 10px; padding: 10px;
text-align: center; text-align: center;
border-radius: 5px; border-radius: 5px;
background-color: #f8f9fa; }
.slider-container {
margin-top: 10px;
}
.slider-container label {
display: block;
margin-bottom: 5px;
}
.slider-container input {
width: 100%;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="header"> <div class="header">
<h1>Hydraulic Machine Control</h1>
<button id="connectButton" class="connect-button" onclick="toggleConnection()"> <button id="connectButton" class="connect-button" onclick="toggleConnection()">
<i class="fas fa-plug"></i> Disconnected <i class="fas fa-power-off"></i> Connect
</button> </button>
</div> </div>
<div class="container"> <div class="container">
<div class="left-panel"> <div class="left-panel">
<div class="mode-block" id="idleMode"> <div class="mode-block">
<h2><i class="fas fa-pause-circle"></i> IDLE Mode</h2> <div class="pu-buttons">
<button onclick="triggerPU('IDLE', 1)"><i class="fas fa-power-off"></i> PU 1</button> <button onclick="sendCommand('IDLE', 1, this)"><i class="fas fa-power-off"></i> IDLE PU 1</button>
<button onclick="triggerPU('IDLE', 2)"><i class="fas fa-power-off"></i> PU 2</button> <button onclick="sendCommand('IDLE', 2, this)"><i class="fas fa-power-off"></i> IDLE PU 2</button>
<button onclick="triggerPU('IDLE', 3)"><i class="fas fa-power-off"></i> PU 3</button> <button onclick="sendCommand('IDLE', 3, this)"><i class="fas fa-power-off"></i> IDLE PU 3</button>
</div> </div>
<div class="mode-block" id="prodMode">
<h2><i class="fas fa-play-circle"></i> PRODUCTION Mode</h2>
<button onclick="triggerPU('PRODUCTION', 1)"><i class="fas fa-power-off"></i> PU 1</button>
<button onclick="triggerPU('PRODUCTION', 2)"><i class="fas fa-power-off"></i> PU 2</button>
<button onclick="triggerPU('PRODUCTION', 3)"><i class="fas fa-power-off"></i> PU 3</button>
</div>
<div class="mode-block" id="firstStartMode">
<h2><i class="fas fa-sync-alt"></i> First Start</h2>
<button onclick="triggerPU('FIRST_START', 1)"><i class="fas fa-power-off"></i> PU 1</button>
<button onclick="triggerPU('FIRST_START', 2)"><i class="fas fa-power-off"></i> PU 2</button>
<button onclick="triggerPU('FIRST_START', 3)"><i class="fas fa-power-off"></i> PU 3</button>
</div> </div>
<div class="mode-block"> <div class="mode-block">
<h2><i class="fas fa-broom"></i> Thermal Loop Cleaning</h2> <div class="pu-buttons">
<button onclick="thermalLoopCleaning('IDLE')"><i class="fas fa-broom"></i> Clean</button> <button onclick="sendCommand('PROD', 1, this)"><i class="fas fa-power-off"></i> PROD PU 1</button>
<button onclick="sendCommand('PROD', 2, this)"><i class="fas fa-power-off"></i> PROD PU 2</button>
<button onclick="sendCommand('PROD', 3, this)"><i class="fas fa-power-off"></i> PROD PU 3</button>
</div>
</div>
<div class="mode-block">
<div class="pu-buttons">
<button onclick="sendCommand('FIRST_START', 1, this)"><i class="fas fa-power-off"></i> FIRST START PU 1</button>
<button onclick="sendCommand('FIRST_START', 2, this)"><i class="fas fa-power-off"></i> FIRST START PU 2</button>
<button onclick="sendCommand('FIRST_START', 3, this)"><i class="fas fa-power-off"></i> FIRST START PU 3</button>
</div>
</div>
<div class="slider-container">
<label for="ploopSetpoint">P-loop Setpoint (bars):</label>
<input type="range" min="0.5" max="3.5" step="0.1" value="1.0" id="ploopSetpoint" onchange="updatePloopSetpoint(this.value)">
<span id="ploopValue">1.0</span>
</div>
<div class="mode-block">
<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>
<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> </div>
</div> </div>
<div class="right-panel"> <div class="right-panel">
<div class="monitor-block"> <div class="monitor-block">
<h2><i class="fas fa-tachometer-alt"></i> Q perm</h2> <h2><i class="fas fa-tint"></i> Q Perm</h2>
<div class="monitor-values" id="Qperm"> <div class="monitor-values" id="Qperm">
<div class="monitor-value">Q1: 0.0</div> <div class="monitor-value">#1<br>0.0 L/h</div>
<div class="monitor-value">Q2: 0.0</div> <div class="monitor-value">#2<br>0.0 L/h</div>
<div class="monitor-value">Q3: 0.0</div> <div class="monitor-value">#3<br>0.0 L/h</div>
</div> </div>
</div> </div>
<div class="monitor-block"> <div class="monitor-block">
<h2><i class="fas fa-water"></i> Pdilute</h2> <h2><i class="fas fa-water"></i> P Dilute</h2>
<div class="monitor-values" id="Pdilute"> <div class="monitor-values" id="Pdilute">
<div class="monitor-value">P1: 0.0</div> <div class="monitor-value">#1<br>0.0 bar</div>
<div class="monitor-value">P2: 0.0</div> <div class="monitor-value">#2<br>0.0 bar</div>
<div class="monitor-value">P3: 0.0</div> <div class="monitor-value">#3<br>0.0 bar</div>
</div> </div>
</div> </div>
<div class="monitor-block"> <div class="monitor-block">
<h2><i class="fas fa-bolt"></i> Conductivity</h2> <h2><i class="fas fa-bolt"></i> Conductivity</h2>
<div class="monitor-values" id="Conductivity"> <div class="monitor-values" id="Conductivity">
<div class="monitor-value">C1: 0.0</div> <div class="monitor-value">#1<br>0.0 µS/cm</div>
<div class="monitor-value">C2: 0.0</div> <div class="monitor-value">#2<br>0.0 µS/cm</div>
<div class="monitor-value">C3: 0.0</div> <div class="monitor-value">#3<br>0.0 µS/cm</div>
</div> </div>
</div> </div>
<div class="monitor-block"> <div class="monitor-block">
<h2><i class="fas fa-flask"></i> Pro</h2> <h2><i class="fas fa-thermometer-half"></i> Pro</h2>
<div class="monitor-values" id="Pro"> <div class="monitor-values" id="Pro">
<div class="monitor-value">PR1: 0.0</div> <div class="monitor-value">#1<br>0.0 units</div>
<div class="monitor-value">PR2: 0.0</div> <div class="monitor-value">#2<br>0.0 units</div>
<div class="monitor-value">PR3: 0.0</div> <div class="monitor-value">#3<br>0.0 units</div>
</div> </div>
</div> </div>
</div> </div>
@ -183,65 +225,55 @@
const connectButton = document.getElementById('connectButton'); const connectButton = document.getElementById('connectButton');
if (data.connected) { if (data.connected) {
connectButton.classList.add('connected'); connectButton.classList.add('connected');
connectButton.innerHTML = '<i class="fas fa-plug"></i> Connected'; connectButton.innerHTML = '<i class="fas fa-power-off"></i> Disconnect';
} else { } else {
connectButton.classList.remove('connected'); connectButton.classList.remove('connected');
connectButton.innerHTML = '<i class="fas fa-plug"></i> Disconnected'; connectButton.innerHTML = '<i class="fas fa-power-off"></i> Connect';
} }
} }
async function triggerPU(state, puNumber) { async function sendCommand(state, puNumber, buttonEl) {
const response = await fetch(`/command/${state}/pu/${puNumber}`, { const ploopSetpoint = document.getElementById('ploopSetpoint').value;
method: 'POST' const response = await fetch(`/command/${state}/pu/${puNumber}?ploop_setpoint=${ploopSetpoint}`, {
}); method: 'POST'
const data = await response.json(); });
console.log(data);
// Highlight the active mode block based on the state if (!response.ok) {
document.querySelectorAll('.mode-block').forEach(block => { console.error('Failed to send command');
block.classList.remove('active'); return;
}); }
// Map state to lowercase div ID suffix const data = await response.json();
const stateToId = { console.log(data);
"IDLE": "idle",
"PRODUCTION": "prod",
"FIRST_START": "firstStart"
};
const blockId = stateToId[state]; // Reset all buttons
if (blockId) { document.querySelectorAll('.mode-block button').forEach(button => {
const block = document.getElementById(`${blockId}Mode`); button.classList.remove('active');
if (block) { });
block.classList.add('active');
}
}
}
// Set the clicked button to active
buttonEl.classList.add('active');
// Update PU status
document.getElementById(`pu${puNumber}-status`).textContent = state;
}
async function thermalLoopCleaning(mode) {
const response = await fetch(`/mode/${mode}/thermal_loop`, {
method: 'POST'
});
const data = await response.json();
console.log(data);
}
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();
updateMonitorValues('Qperm', data.Qperm); updateMonitorValues('Qperm', data.Qperm, 'L/h');
updateMonitorValues('Pdilute', data.Pdilute); updateMonitorValues('Pdilute', data.Pdilute, 'bar');
updateMonitorValues('Conductivity', data.Conductivity); updateMonitorValues('Conductivity', data.Conductivity, 'µS/cm');
updateMonitorValues('Pro', data.Pro); updateMonitorValues('Pro', data.Pro, 'units');
} }
function updateMonitorValues(id, values) { 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');
valueElements.forEach((element, index) => { valueElements.forEach((element, index) => {
if (index < values.length) { if (index < values.length) {
element.textContent = `${id.charAt(0)}${index + 1}: ${values[index]}`; element.innerHTML = `#${index + 1}<br>${values[index]} ${unit}`;
} }
}); });
} }