diff --git a/FASTapi/__pycache__/endurance_can.cpython-312.pyc b/FASTapi/__pycache__/endurance_can.cpython-312.pyc new file mode 100644 index 0000000..5f5285c Binary files /dev/null and b/FASTapi/__pycache__/endurance_can.cpython-312.pyc differ diff --git a/FASTapi/__pycache__/graphViewer.cpython-312.pyc b/FASTapi/__pycache__/graphViewer.cpython-312.pyc new file mode 100644 index 0000000..7210f8a Binary files /dev/null and b/FASTapi/__pycache__/graphViewer.cpython-312.pyc differ diff --git a/FASTapi/__pycache__/logger.cpython-312.pyc b/FASTapi/__pycache__/logger.cpython-312.pyc new file mode 100644 index 0000000..1a8f801 Binary files /dev/null and b/FASTapi/__pycache__/logger.cpython-312.pyc differ diff --git a/FASTapi/__pycache__/main.cpython-312.pyc b/FASTapi/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..8eb82ac Binary files /dev/null and b/FASTapi/__pycache__/main.cpython-312.pyc differ diff --git a/FASTapi/endurance_can.py b/FASTapi/endurance_can.py new file mode 100644 index 0000000..370e5b9 --- /dev/null +++ b/FASTapi/endurance_can.py @@ -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) diff --git a/FASTapi/graphViewer.py b/FASTapi/graphViewer.py new file mode 100644 index 0000000..d078208 --- /dev/null +++ b/FASTapi/graphViewer.py @@ -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)) diff --git a/FASTapi/logger.py b/FASTapi/logger.py new file mode 100644 index 0000000..458938f --- /dev/null +++ b/FASTapi/logger.py @@ -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() diff --git a/FASTapi/main.py b/FASTapi/main.py new file mode 100644 index 0000000..4596236 --- /dev/null +++ b/FASTapi/main.py @@ -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) diff --git a/FASTapi/static/main.js b/FASTapi/static/main.js new file mode 100644 index 0000000..eb1cce5 --- /dev/null +++ b/FASTapi/static/main.js @@ -0,0 +1,105 @@ + diff --git a/FASTapi/static/styles.css b/FASTapi/static/styles.css new file mode 100644 index 0000000..c7aaf48 --- /dev/null +++ b/FASTapi/static/styles.css @@ -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; +} diff --git a/FASTapi/templates/graphs.html b/FASTapi/templates/graphs.html new file mode 100644 index 0000000..9281cbc --- /dev/null +++ b/FASTapi/templates/graphs.html @@ -0,0 +1,92 @@ + + +
+ +| Status | +Node ID | +Serial No. | +Setpoint | +Feedback | +Drift | +
|---|