From db38c17a5e0918b6a90ee4609638a323d72cdada Mon Sep 17 00:00:00 2001 From: VineetaGupta Date: Mon, 14 Apr 2025 10:34:05 +0200 Subject: [PATCH] Added connect and disconnect button --- gui.py | 320 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 173 insertions(+), 147 deletions(-) diff --git a/gui.py b/gui.py index 81f88a8..683e7ad 100644 --- a/gui.py +++ b/gui.py @@ -1,181 +1,106 @@ import sys import time import canopen -from PyQt5.QtWidgets import QApplication, QWidget, QTableWidget, QTableWidgetItem, QVBoxLayout, QHBoxLayout, QLabel, QGroupBox, QFormLayout +from PyQt5.QtWidgets import ( + QApplication, QWidget, QTableWidget, QTableWidgetItem, QVBoxLayout, + QHBoxLayout, QLabel, QGroupBox, QFormLayout, QPushButton, QMessageBox, QHeaderView +) from PyQt5.QtCore import QTimer, Qt from PyQt5.QtGui import QColor -# Connect to CAN network -network = canopen.Network() -network.connect(channel='PCAN_USBBUS1', bustype='pcan', bitrate=250000) motorBoardEdsPath = r'C:\Users\vineetagupta\Documents\NorthStar-Motor-Valve-Board-Firmware\MotorValveBoard\coappl\motorcontrollervalve.eds' puBoardEdsPath = r'C:\Users\vineetagupta\Documents\NorthStar-Production-Unit-Board-Firmware\ProcessBoardV1\coappl\processBoard_1.eds' -nodes = {} -motorBoardTpdoData = {} # Store latest setpoint and feedback per node -PuBoardData = {} -def PUBoardNode(): - global PuBoardData - try: - PuBoardData = canopen.RemoteNode(0x1, puBoardEdsPath) - network.add_node(PuBoardData) - time.sleep(0.1) - - # Add to node list early for visibility - nodes[1] = PuBoardData - - # Now check if extra node is OPERATIONAL - if PuBoardData.nmt.state == 'OPERATIONAL': - PuBoardData.tpdo.read() - PuBoardData.tpdo[1].enabled = True - - def make_extra_callback(): - def cb(map_obj): - global PuBoardData - for var in map_obj: - # Flow meter data (0x6005, 0x01 to 0x04) - if var.index == 0x6005: - if var.subindex == 0x01: - PuBoardData['flowmeter']['0x01'] = var.raw - elif var.subindex == 0x02: - PuBoardData['flowmeter']['0x02'] = var.raw - elif var.subindex == 0x03: - PuBoardData['flowmeter']['0x03'] = var.raw - elif var.subindex == 0x04: - PuBoardData['flowmeter']['0x04'] = var.raw - - # Pressure sensor data (0x6006, 0x01 to 0x04) - elif var.index == 0x6006: - if var.subindex == 0x01: - PuBoardData['pressure']['0x01'] = var.raw - elif var.subindex == 0x02: - PuBoardData['pressure']['0x02'] = var.raw - elif var.subindex == 0x03: - PuBoardData['pressure']['0x03'] = var.raw - elif var.subindex == 0x04: - PuBoardData['pressure']['0x04'] = var.raw - - return cb - - PuBoardData.tpdo[1].add_callback(make_extra_callback()) - else: - pass - - except Exception as e: - print(f"Extra Node {1} error: {e}") - - -def motorNode(): - for node_id in range(5, 27): - try: - node = canopen.RemoteNode(node_id, motorBoardEdsPath) - network.add_node(node) - time.sleep(0.1) - - # Add to node list early so it's visible in GUI - nodes[node_id] = node - motorBoardTpdoData[node_id] = {'setpoint': '-', 'feedback': '-'} - - # Now check if node is OPERATIONAL - if node.nmt.state == 'OPERATIONAL': - node.tpdo.read() - node.tpdo[1].enabled = True - - def make_callback(nid): - def cb(map_obj): - for var in map_obj: - if var.index == 0x6002: - motorBoardTpdoData[nid]['setpoint'] = var.raw - elif var.index == 0x6004: - motorBoardTpdoData[nid]['feedback'] = var.raw - return cb - - node.tpdo[1].add_callback(make_callback(node_id)) - else: - pass - - except Exception as e: - print(f"Node {node_id} error: {e}") - - -# --------------------------- PyQt GUI ------------------------------- class NodeTableWindow(QWidget): def __init__(self, nodes): super().__init__() self.setWindowTitle("Valve Node Status") self.nodes = nodes - self.num_nodes = len(nodes) + self.motorBoardTpdoData = {} + self.PuBoardData = {} + self.connected = False + self.network = None + self.num_nodes = 0 self.init_ui() self.start_timer() def init_ui(self): - layout = QHBoxLayout(self) + layout = QVBoxLayout(self) - # Left side - Table + # Control bar with connect button + button_row = QHBoxLayout() + self.connect_btn = QPushButton("Connect to CAN", self) + self.disconnect_btn = QPushButton("Disconnect", self) + self.disconnect_btn.setEnabled(False) + + self.connect_btn.clicked.connect(self.handle_connect) + self.disconnect_btn.clicked.connect(self.handle_disconnect) + + button_row.addWidget(self.connect_btn) + button_row.addWidget(self.disconnect_btn) + layout.addLayout(button_row) + + # Main layout split: Table (left) and Details (right) + main_layout = QHBoxLayout() + + # Valve Table self.table_widget = QWidget(self) - table_layout = QVBoxLayout(self.table_widget) + table_layout = QVBoxLayout() - # Heading for the table - table_heading = QLabel("Valve Details", self) + # Valve Details Label + table_heading = QLabel("Valve Details") + table_heading.setAlignment(Qt.AlignLeft) table_layout.addWidget(table_heading) - # Table - self.table = QTableWidget(self.num_nodes, 4) + # Table Setup + self.table = QTableWidget(0, 4) self.table.setHorizontalHeaderLabels(["Node ID", "Setpoint", "Feedback", "Status"]) + self.table.horizontalHeader().setStretchLastSection(True) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + self.table.verticalHeader().setVisible(False) - # Dynamic height based on the number of nodes - self.table.setFixedHeight(30 * self.num_nodes) # 30px height per row - self.table.setFixedWidth(500) - - # Adjust column width to content - self.table.resizeColumnsToContents - - for row, node_id in enumerate(sorted(self.nodes.keys())): - item = QTableWidgetItem(str(node_id)) - item.setTextAlignment(Qt.AlignCenter) - self.table.setItem(row, 0, item) - + # Add table to layout table_layout.addWidget(self.table) + + # Apply layout to table_widget self.table_widget.setLayout(table_layout) - # Right side - Pump, Flowmeter, and Pressure Sensor Details + + # Details self.details_widget = QWidget(self) details_layout = QVBoxLayout(self.details_widget) - # Pump Details pump_group = QGroupBox("Pump Details", self) pump_layout = QFormLayout(pump_group) - pump_speed_label = QLabel("Speed: 0 RPM", self) # Example pump speed - pump_layout.addRow("Pump 1", pump_speed_label) - pump_layout.addRow("Pump 2", pump_speed_label) - pump_layout.addRow("Pump 3", pump_speed_label) - pump_layout.addRow("Pump 4", pump_speed_label) + self.pump_labels = [] + for i in range(1, 5): + label = QLabel("Speed: 0 RPM", self) + self.pump_labels.append(label) + pump_layout.addRow(f"Pump {i}", label) details_layout.addWidget(pump_group) - # Flow Meter Details flow_group = QGroupBox("Flow Meter Details", self) flow_layout = QFormLayout(flow_group) - self.flow_rate_label = QLabel("Flow Rate: 0 L/min", self) # Assign object name - self.flow_rate_label.setObjectName("Flow Rate") - flow_layout.addRow("Flow Rate", self.flow_rate_label) + self.flow_rate_label = [] + for i in range(1, 5): + label = QLabel("Flow: 0 L/h", self) + self.flow_rate_label.append(label) + flow_layout.addRow(f"Flowmeter {i}", label) details_layout.addWidget(flow_group) - # Pressure Sensor Details pressure_group = QGroupBox("Pressure Sensor Details", self) pressure_layout = QFormLayout(pressure_group) - self.pressure_value_label = QLabel("Pressure: 0 bar", self) # Assign object name - self.pressure_value_label.setObjectName("Pressure") + self.pressure_value_label = QLabel("Pressure: 0 bar", self) pressure_layout.addRow("Pressure", self.pressure_value_label) details_layout.addWidget(pressure_group) self.details_widget.setLayout(details_layout) - # Add both widgets (table and details) to the main layout - layout.addWidget(self.table_widget) - layout.addWidget(self.details_widget) - - self.setLayout(layout) + # Combine layouts + main_layout.addWidget(self.table_widget) + main_layout.addWidget(self.details_widget) + layout.addLayout(main_layout) # Set window size screen = QApplication.primaryScreen() @@ -186,56 +111,157 @@ class NodeTableWindow(QWidget): y = int((screen_geometry.height() - height) / 2) self.setGeometry(x, y, width, height) - def start_timer(self): self.timer = QTimer() self.timer.setInterval(500) self.timer.timeout.connect(self.update_table) self.timer.start() + def handle_connect(self): + try: + self.network = canopen.Network() + self.network.connect(channel='PCAN_USBBUS1', bustype='pcan', bitrate=250000) + + self.nodes = {} + self.motorBoardTpdoData = {} + self.PuBoardData = {} + + self.init_motor_nodes() + self.init_pu_node() + + self.connected = True + self.num_nodes = len(self.nodes) + self.table.setRowCount(self.num_nodes) + + for row, node_id in enumerate(sorted(self.nodes.keys())): + item = QTableWidgetItem(str(node_id)) + item.setTextAlignment(Qt.AlignCenter) + self.table.setItem(row, 0, item) + + self.connect_btn.setStyleSheet("background-color: lightgreen;") + self.disconnect_btn.setEnabled(True) + QMessageBox.information(self, "Connected", "Successfully connected to CAN and initialized nodes.") + + except Exception as e: + QMessageBox.critical(self, "Connection Error", f"Failed to connect:\n{e}") + self.connected = False + + + def handle_disconnect(self): + try: + if self.network: + self.network.disconnect() + + self.connected = False + self.nodes.clear() + self.motorBoardTpdoData.clear() + self.PuBoardData.clear() + + self.table.clearContents() + self.table.setRowCount(0) + + self.connect_btn.setEnabled(True) + self.disconnect_btn.setEnabled(False) + self.connect_btn.setStyleSheet("") # Resets to default style + + QMessageBox.information(self, "Disconnected", "CAN network disconnected.") + except Exception as e: + QMessageBox.warning(self, "Disconnect Error", f"Error while disconnecting:\n{e}") + + + def init_motor_nodes(self): + for node_id in range(5, 27): + try: + node = canopen.RemoteNode(node_id, motorBoardEdsPath) + self.network.add_node(node) + time.sleep(0.1) + + self.nodes[node_id] = node + self.motorBoardTpdoData[node_id] = {'setpoint': '-', 'feedback': '-'} + + if node.nmt.state == 'OPERATIONAL': + node.tpdo.read() + node.tpdo[1].enabled = True + + def make_callback(nid): + def cb(map_obj): + for var in map_obj: + if var.index == 0x6002: + self.motorBoardTpdoData[nid]['setpoint'] = var.raw + elif var.index == 0x6004: + self.motorBoardTpdoData[nid]['feedback'] = var.raw + return cb + + node.tpdo[1].add_callback(make_callback(node_id)) + except Exception as e: + print(f"Motor node {node_id} error: {e}") + + def init_pu_node(self): + try: + node = canopen.RemoteNode(0x1, puBoardEdsPath) + self.network.add_node(node) + time.sleep(0.1) + self.nodes[1] = node + self.PuBoardData = {'flowmeter': {}, 'pressure': {}} + + if node.nmt.state == 'OPERATIONAL': + node.tpdo.read() + node.tpdo[1].enabled = True + + def make_callback(): + def cb(map_obj): + for var in map_obj: + if var.index == 0x6005: + self.PuBoardData['flowmeter'][f'0x{var.subindex:02X}'] = var.raw + elif var.index == 0x6006: + self.PuBoardData['pressure'][f'0x{var.subindex:02X}'] = var.raw + return cb + + node.tpdo[1].add_callback(make_callback()) + except Exception as e: + print(f"PU node error: {e}") + def update_table(self): + if not self.connected: + return + for row, node_id in enumerate(sorted(self.nodes.keys())): node = self.nodes[node_id] - # Node status try: status = node.nmt.state except: status = "UNKNOWN" - # Color Node ID based on status node_item = self.table.item(row, 0) - node_item.setBackground(QColor("green") if status == "OPERATIONAL" else QColor("red")) + if node_item: + node_item.setBackground( + QColor("green") if status == "OPERATIONAL" else + QColor("yellow") if status == "PRE-OPERATIONAL" else + QColor("red") + ) - # Setpoint - set_val = str(motorBoardTpdoData.get(node_id, {}).get('setpoint', '-')) + set_val = str(self.motorBoardTpdoData.get(node_id, {}).get('setpoint', '-')) setpoint_item = QTableWidgetItem(set_val) setpoint_item.setTextAlignment(Qt.AlignCenter) self.table.setItem(row, 1, setpoint_item) - # Feedback - fb_val = str(motorBoardTpdoData.get(node_id, {}).get('feedback', '-')) + fb_val = str(self.motorBoardTpdoData.get(node_id, {}).get('feedback', '-')) feedback_item = QTableWidgetItem(fb_val) feedback_item.setTextAlignment(Qt.AlignCenter) self.table.setItem(row, 2, feedback_item) - # Status status_item = QTableWidgetItem(status) status_item.setTextAlignment(Qt.AlignCenter) self.table.setItem(row, 3, status_item) def main(): - # Initialize all the nodes - motorNode() - PUBoardNode() - - # Start the GUI app = QApplication(sys.argv) - window = NodeTableWindow(nodes) + window = NodeTableWindow({}) window.show() - sys.exit(app.exec_()) + if __name__ == "__main__": main()