Created fast api repo for endurance testbench

This commit is contained in:
VineetaGupta 2025-09-01 11:12:47 +02:00
parent 97f1e57233
commit eb917a1a30
12 changed files with 732 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

159
FASTapi/endurance_can.py Normal file
View File

@ -0,0 +1,159 @@
# Real CANopen data manager for Endurance Test Bench HMI
import canopen
import time
class EnduranceDataManager:
def __init__(self):
self.motor_data = {} # {node_id: {setpoint, feedback}}
self.pu_data = {"setpoint": {}, "feedback": {}, "flowmeter": {}, "pressure": {}, "pump": {}}
self.connected = False
self.network = None
self.nodes = {}
self.serial_numbers = {}
def connect_to_can(self):
try:
self.network = canopen.Network()
self.network.connect(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000)
self._init_pu_node()
self._init_motor_nodes()
self.connected = True
except Exception as e:
print(f"[CAN ERROR] Could not connect to CAN: {e}")
self.connected = False
def disconnect(self):
if self.network:
self.network.disconnect()
self.connected = False
self.motor_data.clear()
self.pu_data = {"setpoint": {}, "feedback": {}, "flowmeter": {}, "pressure": {}, "pump": {}}
self.nodes.clear()
def send_test_command(self, value: int):
try:
if self.valve_node:
self.valve_node.sdo[0x2007].raw = value
print(f"[TEST COMMAND] Sent value {value} to 0x2007")
else:
print("[TEST COMMAND] Valve node not initialized")
except Exception as e:
print(f"[TEST COMMAND ERROR] {e}")
def _init_pu_node(self):
try:
node = canopen.RemoteNode(1, r'C:\Users\vineetagupta\Documents\NorthStar_Bitbucket\EnduranceTestBench\EnduranceTestBench\coappl\enduranceTestBench.eds')
self.network.add_node(node)
node.nmt.state = 'OPERATIONAL'
node.tpdo.read()
self.nodes[1] = node
tpdo_cobs = {
0x284: 'setpoint',
0x285: 'setpoint',
0x286: 'setpoint',
0x281: 'flowmeter',
0x282: 'pressure',
0x283: 'pump'
}
for cob_id, key in tpdo_cobs.items():
tpdo_num = self._get_tpdo_number_by_cob_id(node, cob_id)
if tpdo_num is not None:
node.tpdo[tpdo_num].enabled = True
def make_pu_cb(k):
def cb(map_obj):
for var in map_obj:
sid = f"0x{var.subindex:02X}"
self.pu_data[k][sid] = var.raw
return cb
node.tpdo[tpdo_num].add_callback(make_pu_cb(key))
except Exception as e:
print(f"PU node error: {e}")
def _get_tpdo_number_by_cob_id(self, node, cob_id):
for i in range(1, 9):
if i in node.tpdo:
if node.tpdo[i].cob_id == cob_id:
return i
return None
def get_valve_data(self):
result = {}
for i in range(5, 25):
sid = f"0x{i - 4:02X}"
setpoint = self.pu_data["setpoint"].get(sid, 0)
feedback = self.motor_data.get(i, {}).get("feedback", 0)
drift = abs(setpoint - feedback)
serial = self.serial_numbers.get(i)
status = "UNKNOWN"
try:
if i in self.nodes:
status = self.nodes[i].nmt.state or "UNKNOWN"
except:
pass
if drift >= 5 and drift <= 10:
drift_display = f"⚠️ { drift}"
elif drift >= 10:
drift_display = f"{ drift}"
else:
drift_display = f"{drift}"
result[i] = {
"node_id": f"{i}",
"setpoint": setpoint,
"feedback": feedback,
"drift": drift_display,
"status": status,
"serial": serial
}
return result
def _init_motor_nodes(self):
for node_id in range(5, 6):
try:
motor_node = canopen.RemoteNode(
node_id,
r'C:\Users\vineetagupta\Documents\NorthStar_Bitbucket\MotorBoard\MotorValveBoard\coappl\motorcontrollervalve.eds'
)
self.network.add_node(motor_node)
motor_node.nmt.state = 'OPERATIONAL'
motor_node.tpdo.read()
motor_node.tpdo[1].enabled = True
def make_cb(nid):
def cb(map_obj):
for var in map_obj:
if var.index == 0x2004 and var.subindex == 0x0:
self.motor_data[nid] = {"feedback": var.raw}
return cb
motor_node.tpdo[1].add_callback(make_cb(node_id))
self.nodes[node_id] = motor_node
if not hasattr(self, 'serial_numbers'):
self.serial_numbers = {}
if node_id not in self.serial_numbers:
try:
serial = motor_node.sdo[0x1018][4].raw
self.serial_numbers[node_id] = serial
except Exception as e:
print(f"[Serial Read Error] Node {node_id}: {e}")
self.serial_numbers[node_id] = "Unknown"
except Exception as e:
print(f"[Motor Node {node_id}] Error: {e}")
def get_flow_data(self):
return self.pu_data["flowmeter"]
def get_pressure_data(self):
return self.pu_data["pressure"]
def get_pump_rpm(self):
return self.pu_data["pump"].get("0x00", 0)

99
FASTapi/graphViewer.py Normal file
View File

@ -0,0 +1,99 @@
import os
import csv
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
from datetime import datetime
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
LOGS_DIR = "logs"
class GraphViewer(tk.Toplevel):
def __init__(self, master=None):
super().__init__(master)
self.title("Valve Graph Viewer")
self.geometry("1000x600")
self._create_widgets()
self._populate_dates()
def _create_widgets(self):
self.date_label = ttk.Label(self, text="Select Date:")
self.date_label.pack()
self.date_cb = ttk.Combobox(self, state="readonly")
self.date_cb.pack()
self.node_label = ttk.Label(self, text="Select Node:")
self.node_label.pack()
self.node_cb = ttk.Combobox(self, state="readonly", values=[f"Node{nid:02}" for nid in range(5, 25)])
self.node_cb.pack()
self.plot_btn = ttk.Button(self, text="Plot Graph", command=self._plot_graph)
self.plot_btn.pack(pady=10)
self.canvas_frame = ttk.Frame(self)
self.canvas_frame.pack(fill="both", expand=True)
def _populate_dates(self):
if not os.path.exists(LOGS_DIR):
return
dates = [d for d in os.listdir(LOGS_DIR) if os.path.isdir(os.path.join(LOGS_DIR, d))]
self.date_cb['values'] = sorted(dates)
if dates:
self.date_cb.current(0)
self.node_cb.current(0)
def _plot_graph(self):
date = self.date_cb.get()
node = self.node_cb.get()
filepath = os.path.join(LOGS_DIR, date, f"{node}.csv")
if not os.path.exists(filepath):
messagebox.showerror("File Not Found", f"No data for {node} on {date}.")
return
timestamps = []
setpoints = []
feedbacks = []
flows = []
pressures = []
with open(filepath, newline='') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
try:
timestamps.append(datetime.strptime(row["Timestamp"], "%Y-%m-%d %H:%M:%S"))
setpoints.append(float(row["Setpoint"]))
feedbacks.append(float(row["Feedback"]))
flows.append(float(row["Flow (L/h)"]))
pressures.append(float(row["Pressure (bar)"]))
except Exception as e:
print(f"[PARSE ERROR] {e}")
self._draw_plot(timestamps, setpoints, feedbacks, flows, pressures)
def _draw_plot(self, t, sp, fb, fl, pr):
for widget in self.canvas_frame.winfo_children():
widget.destroy()
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(t, sp, label="Setpoint")
ax.plot(t, fb, label="Feedback")
ax.plot(t, fl, label="Flow (L/h)")
ax.plot(t, pr, label="Pressure (bar)")
ax.set_title("Valve Performance")
ax.set_xlabel("Time")
ax.set_ylabel("Values")
ax.legend()
ax.grid(True)
canvas = FigureCanvasTkAgg(fig, master=self.canvas_frame)
canvas.draw()
canvas.get_tk_widget().pack(fill="both", expand=True)
# Usage:
# from graph_viewer import GraphViewer
# btn.config(command=lambda: GraphViewer(root))

47
FASTapi/logger.py Normal file
View File

@ -0,0 +1,47 @@
import csv
import os
import threading
from datetime import datetime
LOG_INTERVAL_SECONDS = 0.5
VALVE_IDS = range(5, 25)
BASE_LOG_DIR = "logs"
def start_per_valve_logger(data_mgr):
def log_data():
threading.Timer(LOG_INTERVAL_SECONDS, log_data).start()
try:
now = datetime.now()
timestamp_str = now.strftime("%Y-%m-%d %H:%M:%S")
date_folder = now.strftime("%Y-%m-%d")
valve_data = data_mgr.get_valve_data()
flow = data_mgr.get_flow_data()
pressure = data_mgr.get_pressure_data()
# Aggregate flow and pressure
total_flow = sum(flow.get(f"0x{i:02X}", 0) for i in range(1, 5)) / 100.0
total_pressure = sum(pressure.get(f"0x{i:02X}", 0) for i in range(1, 4)) / 100.0
# Create daily folder
log_dir = os.path.join(BASE_LOG_DIR, date_folder)
os.makedirs(log_dir, exist_ok=True)
for node_id in VALVE_IDS:
valve = valve_data.get(node_id, {})
setpoint = valve.get("setpoint", 0)
feedback = valve.get("feedback", 0)
filepath = os.path.join(log_dir, f"Node{node_id:02}.csv")
is_new_file = not os.path.exists(filepath)
with open(filepath, "a", newline="") as f:
writer = csv.writer(f)
if is_new_file:
writer.writerow(["Timestamp", "Setpoint", "Feedback", "Flow (L/h)", "Pressure (bar)"])
writer.writerow([timestamp_str, setpoint, feedback, total_flow, total_pressure])
except Exception as e:
print(f"[LOG ERROR] {e}")
log_data()

68
FASTapi/main.py Normal file
View File

@ -0,0 +1,68 @@
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
import uvicorn
from endurance_can import EnduranceDataManager
from logger import start_per_valve_logger
app = FastAPI()
# Static and templates
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
# Global CAN manager
data_mgr = EnduranceDataManager()
data_mgr.connect_to_can()
start_per_valve_logger(data_mgr)
@app.get("/", response_class=HTMLResponse)
async def root(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.get("/api/valve-data")
async def get_valve_data():
return JSONResponse(content=data_mgr.get_valve_data())
@app.get("/api/flow-data")
async def get_flow_data():
return JSONResponse(content=data_mgr.get_flow_data())
@app.get("/api/pressure-data")
async def get_pressure_data():
return JSONResponse(content=data_mgr.get_pressure_data())
@app.get("/api/pump-rpm")
async def get_pump_rpm():
return JSONResponse(content={"rpm": data_mgr.get_pump_rpm()})
@app.post("/api/start-test")
async def start_test():
data_mgr.send_test_command(1)
return {"status": "started"}
@app.post("/api/stop-test")
async def stop_test():
data_mgr.send_test_command(0)
return {"status": "stopped"}
@app.get("/data")
def get_data():
return {
"valves": data_mgr.get_valve_data(),
"flow": data_mgr.get_flow_data(),
"pressure": data_mgr.get_pressure_data(),
"pump": data_mgr.get_pump_rpm()
}
@app.get("/graphs", response_class=HTMLResponse)
async def show_graphs(request: Request):
return templates.TemplateResponse("graphs.html", {"request": request})
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True)

105
FASTapi/static/main.js Normal file
View File

@ -0,0 +1,105 @@
<script>
const ctx = document.getElementById('liveChart').getContext('2d');
const nodeLabel = document.getElementById('selectedNodeLabel');
const nodeSelector = document.getElementById('nodeSelector');
let selectedNode = "5"; // Default node
const historyLimit = 300; // Keep up to 300 points in memory (5 min if 1/s)
const viewWindow = 60; // Show last 60 points on screen
let allData = {
labels: [],
setpoint: [],
feedback: [],
flow: [],
pressure: []
};
// Populate dropdown
for (let i = 5; i <= 24; i++) {
const option = document.createElement("option");
option.value = i.toString();
option.text = `Node ${i.toString().padStart(2, "0")}`;
if (i === 5) option.selected = true;
nodeSelector.appendChild(option);
}
nodeSelector.addEventListener("change", () => {
selectedNode = nodeSelector.value;
nodeLabel.textContent = selectedNode.padStart(2, "0");
// Reset history for this new node
allData = {
labels: [],
setpoint: [],
feedback: [],
flow: [],
pressure: []
};
chart.data.labels = [];
chart.data.datasets.forEach(ds => ds.data = []);
chart.update();
});
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'Setpoint', borderColor: 'blue', data: [], fill: false },
{ label: 'Feedback', borderColor: 'green', data: [], fill: false },
{ label: 'Flow (L/h)', borderColor: 'orange', data: [], fill: false },
{ label: 'Pressure (bar)', borderColor: 'red', data: [], fill: false }
]
},
options: {
responsive: true,
animation: false,
scales: {
x: { title: { display: true, text: 'Time' } },
y: { title: { display: true, text: 'Value' } }
}
}
});
function fetchLiveData() {
fetch("/data")
.then(res => res.json())
.then(data => {
const now = new Date().toLocaleTimeString();
const valve = data.valves?.[selectedNode] || {};
const flow = data.flow?.["0x01"] || 0;
const pressure = data.pressure?.["0x01"] || 0;
// Append to history arrays
allData.labels.push(now);
allData.setpoint.push(valve.setpoint || 0);
allData.feedback.push(valve.feedback || 0);
allData.flow.push(flow);
allData.pressure.push(pressure);
// Trim history if needed
if (allData.labels.length > historyLimit) {
allData.labels.shift();
allData.setpoint.shift();
allData.feedback.shift();
allData.flow.shift();
allData.pressure.shift();
}
// Extract last `viewWindow` points to show in graph
const start = Math.max(0, allData.labels.length - viewWindow);
chart.data.labels = allData.labels.slice(start);
chart.data.datasets[0].data = allData.setpoint.slice(start);
chart.data.datasets[1].data = allData.feedback.slice(start);
chart.data.datasets[2].data = allData.flow.slice(start);
chart.data.datasets[3].data = allData.pressure.slice(start);
chart.update();
});
}
setInterval(fetchLiveData, 1000);
</script>

22
FASTapi/static/styles.css Normal file
View File

@ -0,0 +1,22 @@
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
}
.status-operational {
background-color: green;
}
.status-stopped {
background-color: red;
}
.status-preop {
background-color: orange;
}
.status-unknown {
background-color: gray;
}

View File

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Live Valve Graph</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div style="text-align: center;">
<h3>Live Valve Graph - Node <span id="selectedNodeLabel">05</span></h3>
<label for="nodeSelector">Select Node:</label>
<select id="nodeSelector">
</select>
</div>
<canvas id="liveChart" width="1000" height="400"></canvas>
<script>
const ctx = document.getElementById('liveChart').getContext('2d');
const nodeLabel = document.getElementById('selectedNodeLabel');
const nodeSelector = document.getElementById('nodeSelector');
let selectedNode = "5";
// Add node options 05 to 24
for (let i = 5; i <= 24; i++) {
const option = document.createElement("option");
option.value = i.toString();
option.text = `Node ${i.toString().padStart(2, "0")}`;
if (i === 5) option.selected = true;
nodeSelector.appendChild(option);
}
nodeSelector.addEventListener("change", () => {
selectedNode = nodeSelector.value;
nodeLabel.textContent = selectedNode.padStart(2, "0");
// Clear chart
chart.data.labels = [];
chart.data.datasets.forEach(ds => ds.data = []);
chart.update();
});
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'Setpoint', borderColor: 'blue', data: [], fill: false },
{ label: 'Feedback', borderColor: 'green', data: [], fill: false },
{ label: 'Flow (L/h)', borderColor: 'orange', data: [], fill: false },
{ label: 'Pressure (bar)', borderColor: 'red', data: [], fill: false }
]
},
options: {
responsive: true,
animation: false,
scales: {
x: { title: { display: true, text: 'Time' } },
y: { title: { display: true, text: 'Value' } }
}
}
});
function fetchLiveData() {
fetch("/data")
.then(res => res.json())
.then(data => {
const now = new Date().toLocaleTimeString();
const valve = data.valves?.[selectedNode] || {};
const flow = data.flow?.["0x01"] || 0;
const pressure = data.pressure?.["0x01"] || 0;
chart.data.labels.push(now);
chart.data.datasets[0].data.push(valve.setpoint || 0);
chart.data.datasets[1].data.push(valve.feedback || 0);
chart.data.datasets[2].data.push(flow);
chart.data.datasets[3].data.push(pressure);
if (chart.data.labels.length > 50) {
chart.data.labels.shift();
chart.data.datasets.forEach(ds => ds.data.shift());
}
chart.update();
});
}
setInterval(fetchLiveData, 1000);
</script>
</body>
</html>

View File

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Endurance Test Bench</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.status-indicator {
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;
}
.status-unknown {
background-color: gray;
}
.valve-table-wrapper {
max-height: 500px;
overflow-y: auto;
}
</style>
</head>
<body class="bg-light text-dark">
<div class="container-fluid p-4">
<div class="row mb-4">
<div class="col-12 d-flex justify-content-between align-items-center flex-wrap">
<div class="mb-2 mb-md-0">
<button id="startTest" class="btn btn-outline-success me-2">✅ Start Test</button>
<button id="stopTest" class="btn btn-outline-danger">❌ Stop Test</button>
</div>
<button id="showGraphs" class="btn btn-outline-info">🖥 Show Graphs</button>
</div>
</div>
<div class="row gx-4">
<div class="col-lg-9 col-md-12 mb-4">
<h4 class="fw-bold mb-3">Valve Overview</h4>
<div class="table-responsive valve-table-wrapper">
<table class="table table-bordered align-middle text-center">
<thead class="table-primary text-white">
<tr>
<th>Status</th>
<th>Node ID</th>
<th>Serial No.</th>
<th>Setpoint</th>
<th>Feedback</th>
<th>Drift</th>
</tr>
</thead>
<tbody id="valve-table-body">
<!-- JS will populate this -->
</tbody>
</table>
</div>
</div>
<div class="col-lg-3 col-md-12">
<h5 class="fw-bold mb-3">System Feedback</h5>
<div class="card mb-3">
<div class="card-header bg-primary text-white fw-bold">Pressure Sensor</div>
<ul class="list-group list-group-flush" id="pressureList">
<li class="list-group-item">Pressure 1: 0 bar</li>
<li class="list-group-item">Pressure 2: 0 bar</li>
<li class="list-group-item">Pressure 3: 0 bar</li>
<li class="list-group-item">Pressure 4: 0 bar</li>
</ul>
</div>
<div class="card mb-3">
<div class="card-header bg-primary text-white fw-bold">Flowmeter</div>
<ul class="list-group list-group-flush" id="flowList">
<li class="list-group-item">Flowmeter 1: 0 L/h</li>
<li class="list-group-item">Flowmeter 2: 0 L/h</li>
<li class="list-group-item">Flowmeter 3: 0 L/h</li>
<li class="list-group-item">Flowmeter 4: 0 L/h</li>
</ul>
</div>
<div class="card">
<div class="card-header bg-primary text-white fw-bold">Pump RPM</div>
<div class="card-body">
<h3 class="card-title text-center" id="pumpRPM">0</h3>
</div>
</div>
</div>
</div>
</div>
<script>
function fetchAllData() {
fetch("/data")
.then(res => res.json())
.then(data => {
const tbody = document.getElementById("valve-table-body");
tbody.innerHTML = "";
for (let i = 5; i <= 24; i++) {
const valve = data.valves?.[i.toString()] || {
node_id: "--", serial: "--", setpoint: "--",
feedback: "--", drift: "--", status: "UNKNOWN"
};
const row = document.createElement("tr");
row.innerHTML = `
<td><span class="status-indicator status-${valve.status?.toLowerCase() || 'unknown'}"></span></td>
<td>${valve.node_id}</td>
<td>${valve.serial ?? "--"}</td>
<td>${valve.setpoint}</td>
<td>${valve.feedback}</td>
<td>${valve.drift}</td>
`;
tbody.appendChild(row);
}
const flowEls = document.querySelectorAll("#flowList li");
Object.values(data.flow || {}).forEach((val, i) => {
if (flowEls[i]) flowEls[i].textContent = `Flowmeter ${i + 1}: ${val} L/h`;
});
const pressureEls = document.querySelectorAll("#pressureList li");
Object.values(data.pressure || {}).forEach((val, i) => {
if (pressureEls[i]) pressureEls[i].textContent = `Pressure ${i + 1}: ${val} bar`;
});
document.getElementById("pumpRPM").textContent = data.pump ?? "0";
})
.catch(err => console.error("Failed to fetch /data", err));
}
setInterval(fetchAllData, 1000);
</script>
<script>
document.getElementById("showGraphs").addEventListener("click", () => {
window.open("/graphs", "_blank");
});
</script>
</body>
</html>