Removed feedvalve logic (obsolete) and documented the backend class

This commit is contained in:
AzureAD\AniketSaha 2025-07-25 11:59:57 +02:00
parent 3db1f96489
commit feb48c6a19
3 changed files with 65 additions and 66 deletions

View File

@ -5,24 +5,38 @@ import time
import os
class CANBackend:
"""
CANBackend handles CANopen communication with two Process Units (PU1 and PU2).
It listens for TPDOs, tracks real-time data, and sends SDO control commands
such as setting system modes and setpoints.
"""
def __init__(self, eds_file=None):
"""
Initialize the CAN backend.
:param eds_file: Optional path to the EDS file to use for the master node.
"""
self.network = None
self.master_node = None
self.master_node_id = 0x16
self.master_node_id = 0x16 # Docking board node ID
self.nodes = {}
self.connected = False
self.lock = threading.Lock()
self.latest_data = {
1: {}, # PU1
2: {}, # PU2
1: {}, # PU1 data
2: {}, # PU2 data
}
if eds_file is None:
self.eds_path = os.path.join(os.path.dirname(__file__), "eds_file", "dockingBoard_0.eds")
else:
self.eds_path = eds_file
# Default EDS file path
self.eds_path = eds_file if eds_file else os.path.join(os.path.dirname(__file__), "eds_file", "dockingBoard_0.eds")
def connect(self):
"""
Connects to the CAN network and sets up the master node.
:return: True if successful, False otherwise.
"""
try:
self.network = canopen.Network()
self.network.connect(channel='can0', bustype='socketcan')
@ -32,7 +46,7 @@ class CANBackend:
self.master_node.nmt.state = 'OPERATIONAL'
self.nodes[0] = self.master_node
# Start TPDO listener thread
# Start background listener for TPDOs
self.listener_active = True
self.bus = can.interface.Bus(channel='can0', bustype='socketcan')
self.listener_thread = threading.Thread(target=self._can_listener_loop, daemon=True)
@ -46,6 +60,9 @@ class CANBackend:
return False
def shutdown(self):
"""
Cleanly shuts down the CAN backend and listener.
"""
self.listener_active = False
if self.network:
self.network.disconnect()
@ -55,6 +72,10 @@ class CANBackend:
self.connected = False
def _can_listener_loop(self):
"""
Background thread to listen for CAN TPDO messages.
Updates the internal state for PU1 and PU2 based on COB-ID.
"""
while self.listener_active:
msg = self.bus.recv(1.0)
if msg is None:
@ -64,8 +85,8 @@ class CANBackend:
cob_id = msg.arbitration_id
data = msg.data
with self.lock: # Ensure thread safety
with self.lock:
# ========== PU1 COB-IDs ==========
if cob_id == 0x2A6 and len(data) >= 8:
self.latest_data[1].update({
"FM1": int.from_bytes(data[0:2], 'little') / 100.0 * 60.0,
@ -81,7 +102,6 @@ class CANBackend:
"PS3": int.from_bytes(data[4:6], 'little') / 1000.0,
})
elif cob_id == 0x2A8 and len(data) >= 8:
self.latest_data[1].update({
"MV02_sp": int.from_bytes(data[0:2], 'little') / 100.0,
@ -107,13 +127,13 @@ class CANBackend:
"MV06": data[4],
"MV07": data[5],
"MV08": data[6],
"MV09": data[7], # if used
"MV09": data[7],
})
elif cob_id == 0x2AB and len(data) >= 1:
self.latest_data[1]["PU1_STATE"] = data[0]
# ======== PU2 COB IDs ===============
# ========== PU2 COB-IDs ==========
elif cob_id == 0x2AD and len(data) >= 8:
self.latest_data[2].update({
"FM1": int.from_bytes(data[0:2], 'little') / 100.0 * 60.0,
@ -129,7 +149,6 @@ class CANBackend:
"PS3": int.from_bytes(data[4:6], 'little') / 1000.0,
})
elif cob_id == 0x2AF and len(data) >= 8:
self.latest_data[2].update({
"MV02_sp": int.from_bytes(data[0:2], 'little') / 100.0,
@ -155,7 +174,7 @@ class CANBackend:
"MV06": data[4],
"MV07": data[5],
"MV08": data[6],
"MV09": data[7], # if used
"MV09": data[7],
})
elif cob_id == 0x2B2 and len(data) >= 1:
@ -164,16 +183,33 @@ class CANBackend:
except Exception as e:
print(f"[TPDO PARSE ERROR] {e}")
def get_latest_data(self, pu_number: int):
"""
Retrieve the latest real-time data for the given PU.
:param pu_number: 1 or 2
:return: Dictionary of flow, pressure, valve data
"""
with self.lock:
return self.latest_data.get(pu_number, {}).copy()
def read_current_state(self, pu_number: int):
"""
Get the system mode (decoded string) of the given PU.
:param pu_number: 1 or 2
:return: State name or "Offline"
"""
state_val = self.latest_data.get(pu_number, {}).get(f"PU{pu_number}_STATE")
return self.decode_state(state_val) if state_val is not None else "Offline"
def decode_state(self, state_val: int) -> str:
"""
Convert system state integer to human-readable label.
:param state_val: Integer value from TPDO
:return: String state name
"""
state_map = {
0: "SYSTEM_MODE_INIT",
1: "SYSTEM_MODE_OFF",
@ -195,6 +231,13 @@ class CANBackend:
return state_map.get(state_val, f"UNKNOWN({state_val})")
def send_state_command(self, state: str, pu_number: int, ploop_setpoint: float):
"""
Send the PU state and pressure loop setpoint to the master node.
:param state: State string (e.g., "PRODUCTION")
:param pu_number: PU1 or PU2
:param ploop_setpoint: Float setpoint in bar (will be scaled)
"""
if not self.connected:
raise RuntimeError("CAN not connected")
@ -229,6 +272,12 @@ class CANBackend:
raise
def send_thermal_loop_cleaning(self, mode: str, pu_number: int):
"""
Activate or deactivate thermal loop cleaning mode.
:param mode: "IDLE" or "ACTIVE"
:param pu_number: PU1 or PU2
"""
if not self.connected:
raise RuntimeError("CAN not connected")

19
main.py
View File

@ -17,7 +17,6 @@ from typing import Optional, Dict, Any
from fastapi import Query
import asyncio
import datetime
from valveBackend import ValveBackend
import csv
from collections import deque
import numpy as np
@ -39,9 +38,6 @@ app.add_middleware(SessionMiddleware, secret_key="your_super_secret_key")
router = APIRouter()
templates = Jinja2Templates(directory="templates")
can_backend = CANBackend()
valve_backend = ValveBackend(
eds_file="/home/hmi/Desktop/HMI/eds_file/inletvalveboard.eds"
)
# Serve static files (HTML, JS, CSS)
app.mount("/static", StaticFiles(directory="static"), name="static")
@ -175,10 +171,6 @@ def connect_toggle():
return {"connected": False}
else:
success = can_backend.connect()
try:
valve_backend.connect()
except Exception as e:
print(f"Connection error : {e}")
if not success:
raise HTTPException(status_code=500, detail="Connection failed.")
@ -267,17 +259,6 @@ def can_status():
return {"connected": can_backend.connected}
@app.post("/command/feed_valve")
def feedvalve_control(MV01_opening: int = Query(...)):
"""Control MV01 feed valve"""
global DEFAULT_FEED_VALVE
DEFAULT_FEED_VALVE = MV01_opening
valve_backend.send_command(MV01_opening)
logging.info(f"Feed valve opening to {MV01_opening}")
return {"status": "ok"}
# LOCAL RECORDER
@app.post("/start_recording")
async def start_recording():

View File

@ -192,23 +192,6 @@
.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;
}
.monitor-pu-buttons {
display: flex;
gap: 10px;
@ -313,11 +296,6 @@
<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 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 class="button-group">
<button onclick="runAutoTest(1)">Automatic Test PU1</button>
<button onclick="runAutoTest(2)">Automatic Test PU2</button>
@ -475,15 +453,6 @@
// });
}
async function setFeedValve(opening, buttonEl) {
await fetch(`/command/feed_valve?MV01_opening=${opening}`, {method: 'POST'});
document.querySelectorAll('.feed-valve-buttons button').forEach(btn => {
btn.classList.remove('active');
});
buttonEl.classList.add('active');
}
async function fetchPUStatus() {
const response = await fetch("/api/pu_status");
const data = await response.json();