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 import os
class CANBackend: 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): 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.network = None
self.master_node = None self.master_node = None
self.master_node_id = 0x16 self.master_node_id = 0x16 # Docking board node ID
self.nodes = {} self.nodes = {}
self.connected = False self.connected = False
self.lock = threading.Lock() self.lock = threading.Lock()
self.latest_data = { self.latest_data = {
1: {}, # PU1 1: {}, # PU1 data
2: {}, # PU2 2: {}, # PU2 data
} }
if eds_file is None: # Default EDS file path
self.eds_path = os.path.join(os.path.dirname(__file__), "eds_file", "dockingBoard_0.eds") self.eds_path = eds_file if eds_file else os.path.join(os.path.dirname(__file__), "eds_file", "dockingBoard_0.eds")
else:
self.eds_path = eds_file
def connect(self): def connect(self):
"""
Connects to the CAN network and sets up the master node.
:return: True if successful, False otherwise.
"""
try: try:
self.network = canopen.Network() self.network = canopen.Network()
self.network.connect(channel='can0', bustype='socketcan') self.network.connect(channel='can0', bustype='socketcan')
@ -32,7 +46,7 @@ class CANBackend:
self.master_node.nmt.state = 'OPERATIONAL' self.master_node.nmt.state = 'OPERATIONAL'
self.nodes[0] = self.master_node self.nodes[0] = self.master_node
# Start TPDO listener thread # Start background listener for TPDOs
self.listener_active = True self.listener_active = True
self.bus = can.interface.Bus(channel='can0', bustype='socketcan') self.bus = can.interface.Bus(channel='can0', bustype='socketcan')
self.listener_thread = threading.Thread(target=self._can_listener_loop, daemon=True) self.listener_thread = threading.Thread(target=self._can_listener_loop, daemon=True)
@ -46,6 +60,9 @@ class CANBackend:
return False return False
def shutdown(self): def shutdown(self):
"""
Cleanly shuts down the CAN backend and listener.
"""
self.listener_active = False self.listener_active = False
if self.network: if self.network:
self.network.disconnect() self.network.disconnect()
@ -55,6 +72,10 @@ class CANBackend:
self.connected = False self.connected = False
def _can_listener_loop(self): 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: while self.listener_active:
msg = self.bus.recv(1.0) msg = self.bus.recv(1.0)
if msg is None: if msg is None:
@ -64,8 +85,8 @@ class CANBackend:
cob_id = msg.arbitration_id cob_id = msg.arbitration_id
data = msg.data data = msg.data
with self.lock: # Ensure thread safety with self.lock:
# ========== PU1 COB-IDs ==========
if cob_id == 0x2A6 and len(data) >= 8: if cob_id == 0x2A6 and len(data) >= 8:
self.latest_data[1].update({ self.latest_data[1].update({
"FM1": int.from_bytes(data[0:2], 'little') / 100.0 * 60.0, "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, "PS3": int.from_bytes(data[4:6], 'little') / 1000.0,
}) })
elif cob_id == 0x2A8 and len(data) >= 8: elif cob_id == 0x2A8 and len(data) >= 8:
self.latest_data[1].update({ self.latest_data[1].update({
"MV02_sp": int.from_bytes(data[0:2], 'little') / 100.0, "MV02_sp": int.from_bytes(data[0:2], 'little') / 100.0,
@ -107,13 +127,13 @@ class CANBackend:
"MV06": data[4], "MV06": data[4],
"MV07": data[5], "MV07": data[5],
"MV08": data[6], "MV08": data[6],
"MV09": data[7], # if used "MV09": data[7],
}) })
elif cob_id == 0x2AB and len(data) >= 1: elif cob_id == 0x2AB and len(data) >= 1:
self.latest_data[1]["PU1_STATE"] = data[0] self.latest_data[1]["PU1_STATE"] = data[0]
# ======== PU2 COB IDs =============== # ========== PU2 COB-IDs ==========
elif cob_id == 0x2AD and len(data) >= 8: elif cob_id == 0x2AD and len(data) >= 8:
self.latest_data[2].update({ self.latest_data[2].update({
"FM1": int.from_bytes(data[0:2], 'little') / 100.0 * 60.0, "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, "PS3": int.from_bytes(data[4:6], 'little') / 1000.0,
}) })
elif cob_id == 0x2AF and len(data) >= 8: elif cob_id == 0x2AF and len(data) >= 8:
self.latest_data[2].update({ self.latest_data[2].update({
"MV02_sp": int.from_bytes(data[0:2], 'little') / 100.0, "MV02_sp": int.from_bytes(data[0:2], 'little') / 100.0,
@ -155,7 +174,7 @@ class CANBackend:
"MV06": data[4], "MV06": data[4],
"MV07": data[5], "MV07": data[5],
"MV08": data[6], "MV08": data[6],
"MV09": data[7], # if used "MV09": data[7],
}) })
elif cob_id == 0x2B2 and len(data) >= 1: elif cob_id == 0x2B2 and len(data) >= 1:
@ -164,16 +183,33 @@ class CANBackend:
except Exception as e: except Exception as e:
print(f"[TPDO PARSE ERROR] {e}") print(f"[TPDO PARSE ERROR] {e}")
def get_latest_data(self, pu_number: int): 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: with self.lock:
return self.latest_data.get(pu_number, {}).copy() return self.latest_data.get(pu_number, {}).copy()
def read_current_state(self, pu_number: int): 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") 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" return self.decode_state(state_val) if state_val is not None else "Offline"
def decode_state(self, state_val: int) -> str: 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 = { state_map = {
0: "SYSTEM_MODE_INIT", 0: "SYSTEM_MODE_INIT",
1: "SYSTEM_MODE_OFF", 1: "SYSTEM_MODE_OFF",
@ -195,6 +231,13 @@ class CANBackend:
return state_map.get(state_val, f"UNKNOWN({state_val})") return state_map.get(state_val, f"UNKNOWN({state_val})")
def send_state_command(self, state: str, pu_number: int, ploop_setpoint: float): 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: if not self.connected:
raise RuntimeError("CAN not connected") raise RuntimeError("CAN not connected")
@ -229,6 +272,12 @@ class CANBackend:
raise raise
def send_thermal_loop_cleaning(self, mode: str, pu_number: int): 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: if not self.connected:
raise RuntimeError("CAN not 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 from fastapi import Query
import asyncio import asyncio
import datetime import datetime
from valveBackend import ValveBackend
import csv import csv
from collections import deque from collections import deque
import numpy as np import numpy as np
@ -39,9 +38,6 @@ app.add_middleware(SessionMiddleware, secret_key="your_super_secret_key")
router = APIRouter() router = APIRouter()
templates = Jinja2Templates(directory="templates") templates = Jinja2Templates(directory="templates")
can_backend = CANBackend() can_backend = CANBackend()
valve_backend = ValveBackend(
eds_file="/home/hmi/Desktop/HMI/eds_file/inletvalveboard.eds"
)
# 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")
@ -175,10 +171,6 @@ def connect_toggle():
return {"connected": False} return {"connected": False}
else: else:
success = can_backend.connect() success = can_backend.connect()
try:
valve_backend.connect()
except Exception as e:
print(f"Connection error : {e}")
if not success: if not success:
raise HTTPException(status_code=500, detail="Connection failed.") raise HTTPException(status_code=500, detail="Connection failed.")
@ -267,17 +259,6 @@ def can_status():
return {"connected": can_backend.connected} 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 # LOCAL RECORDER
@app.post("/start_recording") @app.post("/start_recording")
async def start_recording(): async def start_recording():

View File

@ -192,23 +192,6 @@
.monitor-link:hover { .monitor-link:hover {
background-color: #0056b3; 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 { .monitor-pu-buttons {
display: flex; display: flex;
gap: 10px; 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 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 class="button-group"> <div class="button-group">
<button onclick="runAutoTest(1)">Automatic Test PU1</button> <button onclick="runAutoTest(1)">Automatic Test PU1</button>
<button onclick="runAutoTest(2)">Automatic Test PU2</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() { 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();