Created fast api repo for endurance testbench
This commit is contained in:
parent
97f1e57233
commit
eb917a1a30
BIN
FASTapi/__pycache__/endurance_can.cpython-312.pyc
Normal file
BIN
FASTapi/__pycache__/endurance_can.cpython-312.pyc
Normal file
Binary file not shown.
BIN
FASTapi/__pycache__/graphViewer.cpython-312.pyc
Normal file
BIN
FASTapi/__pycache__/graphViewer.cpython-312.pyc
Normal file
Binary file not shown.
BIN
FASTapi/__pycache__/logger.cpython-312.pyc
Normal file
BIN
FASTapi/__pycache__/logger.cpython-312.pyc
Normal file
Binary file not shown.
BIN
FASTapi/__pycache__/main.cpython-312.pyc
Normal file
BIN
FASTapi/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
159
FASTapi/endurance_can.py
Normal file
159
FASTapi/endurance_can.py
Normal 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
99
FASTapi/graphViewer.py
Normal 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
47
FASTapi/logger.py
Normal 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
68
FASTapi/main.py
Normal 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
105
FASTapi/static/main.js
Normal 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
22
FASTapi/static/styles.css
Normal 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;
|
||||
}
|
||||
92
FASTapi/templates/graphs.html
Normal file
92
FASTapi/templates/graphs.html
Normal 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>
|
||||
140
FASTapi/templates/index.html
Normal file
140
FASTapi/templates/index.html
Normal 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>
|
||||
Loading…
Reference in New Issue
Block a user