From eb917a1a304813037f7d71fe0fbb52b56cbebf86 Mon Sep 17 00:00:00 2001 From: VineetaGupta Date: Mon, 1 Sep 2025 11:12:47 +0200 Subject: [PATCH] Created fast api repo for endurance testbench --- .../__pycache__/endurance_can.cpython-312.pyc | Bin 0 -> 8902 bytes .../__pycache__/graphViewer.cpython-312.pyc | Bin 0 -> 6547 bytes FASTapi/__pycache__/logger.cpython-312.pyc | Bin 0 -> 3390 bytes FASTapi/__pycache__/main.cpython-312.pyc | Bin 0 -> 3908 bytes FASTapi/endurance_can.py | 159 ++++++++++++++++++ FASTapi/graphViewer.py | 99 +++++++++++ FASTapi/logger.py | 47 ++++++ FASTapi/main.py | 68 ++++++++ FASTapi/static/main.js | 105 ++++++++++++ FASTapi/static/styles.css | 22 +++ FASTapi/templates/graphs.html | 92 ++++++++++ FASTapi/templates/index.html | 140 +++++++++++++++ 12 files changed, 732 insertions(+) create mode 100644 FASTapi/__pycache__/endurance_can.cpython-312.pyc create mode 100644 FASTapi/__pycache__/graphViewer.cpython-312.pyc create mode 100644 FASTapi/__pycache__/logger.cpython-312.pyc create mode 100644 FASTapi/__pycache__/main.cpython-312.pyc create mode 100644 FASTapi/endurance_can.py create mode 100644 FASTapi/graphViewer.py create mode 100644 FASTapi/logger.py create mode 100644 FASTapi/main.py create mode 100644 FASTapi/static/main.js create mode 100644 FASTapi/static/styles.css create mode 100644 FASTapi/templates/graphs.html create mode 100644 FASTapi/templates/index.html diff --git a/FASTapi/__pycache__/endurance_can.cpython-312.pyc b/FASTapi/__pycache__/endurance_can.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5f5285c52807b2bd5fd6f4cd1ed98420c5c0527e GIT binary patch literal 8902 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 Zn%*>1wEaJ*tp72*&d>)pD5Qk<{{b?=rFsAW literal 0 HcmV?d00001 diff --git a/FASTapi/__pycache__/graphViewer.cpython-312.pyc b/FASTapi/__pycache__/graphViewer.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7210f8a7d28f92746fedb38bb529f0e60f4c849c GIT binary patch literal 6547 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 literal 0 HcmV?d00001 diff --git a/FASTapi/__pycache__/logger.cpython-312.pyc b/FASTapi/__pycache__/logger.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a8f801d74323c6f6eb1317530ca095632a2e09c GIT binary patch literal 3390 zcmcf^ZA=^4@x5IyUK@X4zRgGC5FD_MIDF(vkCI%tK!D&ti`~1#1-XuQF?Rf=w@XZr zT_H79Oj_xPsw+WJ1gR>8D0hAL5bzOA}DxOD#&*mYNUBiOFZf_?MTW#2jeolh7~B9i-~vd26r7+qc%8ioDKG~g%7Oqua1Gd~ zVPBq!#PQ-z>rFBqJ{kMgOKr ztWcIxB@>XNud`y6!Z~=ty+F}XeB+8{iUCNf4p=Z@3+k6Fuu4{6Yk?7~l^ugM3bsE7 z>-TX56Avj^f>P`!i$qdY@(5b;$bl(Q#A!a%D$)>$q{igY3&uBbB z6K(r^&cu~|SoV-yK|J0BNxk?+4hJCEqG0=TLa$g#8WK8GmM{cQ?5`@AA8_TrEL1%N zua-){geIX)s1l5yQLN%_BmXJULh!RctMJ7?3fh1G;}is!a3jFnzfRMaKpY+gVym)i zBS6(jqu8O)Tt&io2iz^(EuzbTr13|_Yxy}2;~;0pRyjOlvR*oF4Ij617VAKl^+K1` zIm0TfZ!|63Qvum!_dEt4z#;6<0>1m>xR#S}jtUm~kKzgL0U!Z6ct>@Y;tKWq3%CGr zBy~7G`Xf3HAE+d%by}iZ+b<&0xP~7t(+;sx;? zKH{4kJJ;iMd1HYw+?)M8vd6CNX}bM1QQsLvY` zPTMi-z39a^Q9XY*WalJt0SbB$~#6@0MemWqCp6C^c z6?icg4Maq_TqJga=ef&X-xWzU9OZaiOzkr&yHB_(L9Ya`raHpML%39IjHGelCmNq^ zI3}^qycLN(mp3q@J%k;cm{`EeWUO8I6Th2vm?X3PKl9A36VTXbR-(b?u%%Gd@4u=T;=)t zBDEyx2~BEUE-lGKNaY}MELxZfOAIb$KaMjhNUE4uoRm~SoDE6$PLH$CLwx8R9F^Fx z_X^JikRWNWH}Cd&278^zK%6L&6Nz=BF+L*EBvD0i6cBL)Y;ML*YWk*qd`t{PBN7us zIM=pv)J(MX#B(`=9vp{MOulxDtw_mTyl0;gi7! z<_T~TBWoh^DxzAl4LcDC@p5p*0l|Y0&_fs!oIu|u%aBLpYXFhECVzW*Jz+ogU%8e7 zDILp8r9NR9fS!lV{@^gFYY_{(3l4H@aZtKp}ol0HE*3>VxEVeAqWNO+|Ls|2YEpzLJ zxiw>MPo2+})ozv9Hp*<7vNuw_*{Z`^Rc#wpZJDZ$)W8=-pr$@k=lC7-$h&6xZSiKE zBV%@~O>LN6sq@?QmW;*qM`q2t{=LzUi#IK^X$v?v)cNl&c)89?#1pEb^7G0`Vo~*JUY4FHjt?uoM*Qi8t*sVYr1d0 zXHQpW8oJgGbk@T!lD>CHJCx1>PB-Klh}nAzOWD$+&3z zq&d7<9m$s0ER5V5SsqUhtWK_J*Ik#_8-1JQ+?N`noHOy6i3IRv$v)@nFLa>9{5gR5 zKy;>g$FgaeUN3Fj(pfij)~}z_n7=2q;WPcH@6G-$@cV`y*l%YZAJt;wv7PDf)I9D` z6TC|^P)o1ThJi|Yt&+j`1N6p%n3s~?_`D@v+-bupnKEJIqXK-%hyS?~|4 zfjRcs00fNwC+BxD57;!Q6HkbIPayJa$gKh$Bbc3llLWLAK#U>s_#y`Zq$TgM-Qg%V z72;2$3z#I2h(NwyaF`H21=^=T^Awc)1=N39RkIGtpWuWXnIHYBV^)(jR?O;lRaR(} Gt^E_`S`>-^ literal 0 HcmV?d00001 diff --git a/FASTapi/__pycache__/main.cpython-312.pyc b/FASTapi/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8eb82aca1e703214e78ca45cf7a992a8653aaf8a GIT binary patch literal 3908 zcmc&%O>7&-6`olx|0#;1C{YykL)MWk){x``PGT#z8#}HWON~-Q%Au?PcEMdst8jOj z*`*y5J;-%{=;)s6Lk~I>Xa%W&F9mYUJr}YhDt8eh4blS5jjDR6ed>F&T#BY8G?%n1 z@a^n--~4=U-g`6pyss}r;Q8Ip|G56SpOCNd!+m`1#-S|`@@pa!nU;t~^HdY^0>y8k zvbr*2-mIHti<=|dumpWQ#hj%?rn~xEcBS+T!0N^8?QR7N{ea^c!$Tmg66j4tn?m^k4^_=t3t? zKo51$!(Hf+6VS;HdZY`T0`#j6t8OW$I_S|ZbovBzx`Q6;LXVxWlJO3Dq6*`N#+Wz;H4=ajk<)|HAwj@g}-cU=536RL!EARaHJ;f{=x~bTah2?jb+%Ew4FR1#4 z^va5&l}nPPm`k8b{7{!GOwxy}nx1AyESSQK7% z%;oNSWruRRiR;Kbd;hKZ)%Q(>nX9)|T~RD)tx~q6)$2yFqA9v%t}ZF-il)NbVAeXg zp3QXJH?smea!t{T>#H}eF0V*sbybs8eYSkp7MWpKED2NK&zu195AyF#^0#neKRi_r zPi>bT{q!?`?flz~@XeZd^XVZ*z%Ns2TJ4{Mf#&!Q%*e1oMKE+8L~)1RfJWP49fw-& zIsmRi$M!sKmhVwF&K>9N2unNE_7@G^0>U{DgQ`pH08U-d)|ee#Q>=DCpl*j8;=pVe z1@ht|4GnV?M4M&uw}Ji4>-Eg*jm*Vnc)ljiw;4`mX*J3j-YOY){wD+(uB*+9jYHe+ zLrOS*0U5DL(6I~(PU;l4P@w(=Qm@rBuQf6kn&FEz@#0gK1xz*036&w@%?eZgPoxHr zl)HvwiA^Cf6iDs`V)OOPd?RzI8NOT-FSm*HIm9Yjd4`p>B6RCKozz_bRc|E9^xMUv zO8124qY+Zs_P|^WBkH+Li*SChlmeQ2_6+JQVJPrWHPPq>;`Y#9V1N2@efn}^`Ukbl zo6Ydmnt1i8$@49H%FQ=k`(~g*TF!!0@72Jzp{3b){OohaD9=Q#$21+eEi1{Lgqz^99n>2JhkIKP*2qV40A z+_XikH+H~HP+KfRDCfL<^PM_+3P#}R$pg99R{;!vUas)0#JiDs1;i%#myZml_v5*G zJlBYy{USE9A3I%-oo>XYzetSjCuZx3*+$|V*H6`BQ;pb}gP=F!YZ0&4$EzM3@M}ye zubbc1(HwS$KRTP)AW|Jd4*V46b=MA?>rjy$EW><5e#af1+9UO|WbM_PjqpNETzJYx z0A~jZ1w4EUg=zrb^HRB7^`D#Nf9eAWyp^2C7S|2a0@uS?Zu^*0G9)>dwf(o?mIa3{ z!_Z<_80^8eY8DJ|^|^(YEr!u!54aZheq%EHx5PO$Vz@`qOL$u=6rjjf^qv@d7PzDta;2nPVedi@+!_in z_&)Ry;jE*AnFKFFYr)Y5O&{D;5CodV&f_g?wF0;-ZgrN`w!*SHR^$iV1`Y%t z^>kKvUqRtL1;Vm SDDkQMXYs9;hlry*q5lIE`zr1L literal 0 HcmV?d00001 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 @@ + + + + + Live Valve Graph + + + +
+

Live Valve Graph - Node 05

+ + +
+ + + + + + diff --git a/FASTapi/templates/index.html b/FASTapi/templates/index.html new file mode 100644 index 0000000..da4471d --- /dev/null +++ b/FASTapi/templates/index.html @@ -0,0 +1,140 @@ + + + + + Endurance Test Bench + + + + + +
+
+
+
+ + +
+ +
+
+ +
+
+

Valve Overview

+
+ + + + + + + + + + + + + + +
StatusNode IDSerial No.SetpointFeedbackDrift
+
+
+ +
+
System Feedback
+ +
+
Pressure Sensor
+
    +
  • Pressure 1: 0 bar
  • +
  • Pressure 2: 0 bar
  • +
  • Pressure 3: 0 bar
  • +
  • Pressure 4: 0 bar
  • +
+
+ +
+
Flowmeter
+
    +
  • Flowmeter 1: 0 L/h
  • +
  • Flowmeter 2: 0 L/h
  • +
  • Flowmeter 3: 0 L/h
  • +
  • Flowmeter 4: 0 L/h
  • +
+
+ +
+
Pump RPM
+
+

0

+
+
+
+
+
+ + + + + + +