Removed feedvalve logic (obsolete) and documented the backend class
This commit is contained in:
parent
3db1f96489
commit
feb48c6a19
81
classCAN.py
81
classCAN.py
|
|
@ -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
19
main.py
|
|
@ -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():
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user