From 72846262486693037d99546b22928ccc232219bb Mon Sep 17 00:00:00 2001 From: Kirubakaran Date: Wed, 27 Aug 2025 03:53:05 +0530 Subject: [PATCH] Fix: Improve MQTT connection and error handling --- core/mqtt_client.py | 226 ++++++++++++++++++++++++++++++-------------- ui/main_window.py | 164 +++++++++++++++++++++++++------- ui/styles.py | 8 +- ui/widgets.py | 105 ++++++++------------ 4 files changed, 330 insertions(+), 173 deletions(-) diff --git a/core/mqtt_client.py b/core/mqtt_client.py index 4a11887..8f4d218 100644 --- a/core/mqtt_client.py +++ b/core/mqtt_client.py @@ -1,17 +1,154 @@ +# # In core/mqtt_client.py +# import socket +# from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot +# import paho.mqtt.client as mqtt +# import uuid + +# class MqttClient(QObject): +# # --- MODIFIED SIGNAL: Now sends a bool and a string --- +# connection_status_changed = pyqtSignal(bool, str) +# message_received = pyqtSignal(str, bytes) +# connection_error = pyqtSignal(str) +# stop_logging_signal = pyqtSignal() +# connected = pyqtSignal() +# disconnected = pyqtSignal() + +# def __init__(self, broker, port, user, password, client_id): +# super().__init__() +# self.broker = broker +# self.port = port + +# self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id) +# if user and password: +# self.client.username_pw_set(user, password) + +# self.client.on_connect = self.on_connect +# self.client.on_disconnect = self.on_disconnect +# self.client.on_message = self.on_message +# # self.client.on_subscribe = self.on_subscribe + +# def on_connect(self, client, userdata, flags, rc, properties): +# if rc == 0: +# print("Connection to MQTT Broker successful!") +# self.connection_status_changed.emit(True, "✅ Connected") +# self.connected.emit() +# # The app is connected, so we should NOT emit disconnected signals here. +# else: +# print(f"Failed to connect, return code {rc}\n") +# self.connection_status_changed.emit(False, f"❌ Connection failed (Code: {rc})") +# self.disconnected.emit() # Connection failed, so we are disconnected + +# def on_disconnect(self, client, userdata, flags, rc, properties): +# print("Disconnected from MQTT Broker.") +# # Correctly emit signals for a disconnection +# # Change the icon in the line below from 🔌 to 🔴 ❌ 🚫 💔 +# self.connection_status_changed.emit(False, "💔 Disconnected") +# self.disconnected.emit() +# self.stop_logging_signal.emit() # It's appropriate to stop logging here + +# def on_message(self, client, userdata, msg): +# # print(f"Received {len(msg.payload)} bytes of binary data from topic `{msg.topic}`") +# self.message_received.emit(msg.topic, msg.payload) + +# # --- MODIFIED connect_to_broker METHOD --- +# def connect_to_broker(self): +# print(f"Attempting to connect to {self.broker}:{self.port}...") +# try: +# self.client.connect(self.broker, self.port, 120) +# print(f"Attempting to connect to {self.broker}:{self.port}...") +# self.client.loop_start() +# except socket.gaierror: +# msg = "Host not found. Check internet." +# print(f"❌ Connection Error: {msg}") +# self.connection_status_changed.emit(False, f"❌ {msg}") +# except (socket.error, TimeoutError): +# msg = "Connection failed. Server offline?" +# print(f"❌ Connection Error: {msg}") +# self.connection_status_changed.emit(False, f"❌ {msg}") +# except Exception as e: +# msg = f"An unexpected error occurred: {e}" +# print(f"❌ {msg}") +# self.connection_status_changed.emit(False, f"❌ Error") + + +# def run(self): +# """ +# Connects to the broker and starts the network loop. +# Handles all common connection errors gracefully. +# """ +# print(f"Attempting to connect to {self.broker}:{self.port}...") +# try: +# # 1. Attempt to connect +# # print(f"Attempting to connect to {self.broker}:{self.port}...") +# self.client.connect(self.broker, self.port, 120) + +# # 2. Run the blocking network loop +# # This will run until self.client.disconnect() is called +# self.client.loop_forever() + +# except socket.gaierror: +# msg = "Host not found. Check the broker address or your internet connection." +# print(f"❌ {msg}") +# self.connection_error.emit(msg) # Report error to the main window + +# except (socket.error, ConnectionRefusedError): +# msg = "Connection refused. Is the server offline or the port incorrect?" +# print(f"❌ {msg}") +# self.connection_error.emit(msg) + +# except TimeoutError: +# msg = "Connection timed out. The server is not responding." +# print(f"❌ {msg}") +# self.connection_error.emit(msg) + +# except Exception as e: +# # Catch any other unexpected errors during connection or loop +# msg = f"An unexpected error occurred: {e}" +# print(f"❌ {msg}") +# self.connection_error.emit(msg) + +# # def on_subscribe(self, client, userdata, mid, reason_code_list, properties): +# # """Callback function for when the broker responds to a subscription request.""" +# # if reason_code_list[0].is_failure: +# # print(f"❌ Broker rejected subscription: {reason_code_list[0]}") +# # else: +# # print(f"✅ Broker accepted subscription with QoS: {reason_code_list[0].value}") + +# # --- (The rest of the file remains the same) --- +# @pyqtSlot() +# def disconnect_from_broker(self): +# """Stops the MQTT client's network loop.""" +# if self.client: +# self.client.loop_stop() +# self.client.disconnect() +# print("Stopping MQTT network loop.") + +# def subscribe_to_topic(self, topic): # Add qos parameter +# print(f"Subscribing to topic: {topic}") +# self.client.subscribe(topic) + +# def publish_message(self, topic, payload): +# self.client.publish(topic, payload) + +# def cleanup(self): +# print("Stopping MQTT network loop.") +# self.client.loop_stop() + + # In core/mqtt_client.py import socket from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot import paho.mqtt.client as mqtt -import uuid class MqttClient(QObject): - # --- MODIFIED SIGNAL: Now sends a bool and a string --- + # Sends connection state (bool) and a message (str) connection_status_changed = pyqtSignal(bool, str) - message_received = pyqtSignal(str, bytes) - connection_error = pyqtSignal(str) - stop_logging_signal = pyqtSignal() + # Generic signals for success or failure/disconnection connected = pyqtSignal() disconnected = pyqtSignal() + stop_logging_signal = pyqtSignal() + # Sends topic (str) and payload (bytes) when a message is received + message_received = pyqtSignal(str, bytes) def __init__(self, broker, port, user, password, client_id): super().__init__() @@ -25,112 +162,55 @@ class MqttClient(QObject): self.client.on_connect = self.on_connect self.client.on_disconnect = self.on_disconnect self.client.on_message = self.on_message - # self.client.on_subscribe = self.on_subscribe def on_connect(self, client, userdata, flags, rc, properties): if rc == 0: print("Connection to MQTT Broker successful!") self.connection_status_changed.emit(True, "✅ Connected") self.connected.emit() - # The app is connected, so we should NOT emit disconnected signals here. else: print(f"Failed to connect, return code {rc}\n") self.connection_status_changed.emit(False, f"❌ Connection failed (Code: {rc})") - self.disconnected.emit() # Connection failed, so we are disconnected + self.disconnected.emit() def on_disconnect(self, client, userdata, flags, rc, properties): print("Disconnected from MQTT Broker.") - # Correctly emit signals for a disconnection - # Change the icon in the line below from 🔌 to 🔴 ❌ 🚫 💔 self.connection_status_changed.emit(False, "💔 Disconnected") self.disconnected.emit() - self.stop_logging_signal.emit() # It's appropriate to stop logging here + self.stop_logging_signal.emit() def on_message(self, client, userdata, msg): - # print(f"Received {len(msg.payload)} bytes of binary data from topic `{msg.topic}`") self.message_received.emit(msg.topic, msg.payload) - # --- MODIFIED connect_to_broker METHOD --- + @pyqtSlot() def connect_to_broker(self): print(f"Attempting to connect to {self.broker}:{self.port}...") try: - self.client.connect(self.broker, self.port, 120) - print(f"Attempting to connect to {self.broker}:{self.port}...") + self.client.connect(self.broker, self.port, 60) self.client.loop_start() except socket.gaierror: - msg = "Host not found. Check internet." + msg = "Host not found. Check broker address or your internet connection." print(f"❌ Connection Error: {msg}") - self.connection_status_changed.emit(False, f"❌ {msg}") - except (socket.error, TimeoutError): - msg = "Connection failed. Server offline?" + self.connection_status_changed.emit(False, msg) + except (socket.error, ConnectionRefusedError, TimeoutError): + msg = "Connection failed. Is the server offline or the port incorrect?" print(f"❌ Connection Error: {msg}") - self.connection_status_changed.emit(False, f"❌ {msg}") + self.connection_status_changed.emit(False, msg) except Exception as e: msg = f"An unexpected error occurred: {e}" print(f"❌ {msg}") - self.connection_status_changed.emit(False, f"❌ Error") + self.connection_status_changed.emit(False, f"Error: {e}") - - def run(self): - """ - Connects to the broker and starts the network loop. - Handles all common connection errors gracefully. - """ - print(f"Attempting to connect to {self.broker}:{self.port}...") - try: - # 1. Attempt to connect - # print(f"Attempting to connect to {self.broker}:{self.port}...") - self.client.connect(self.broker, self.port, 120) - - # 2. Run the blocking network loop - # This will run until self.client.disconnect() is called - self.client.loop_forever() - - except socket.gaierror: - msg = "Host not found. Check the broker address or your internet connection." - print(f"❌ {msg}") - self.connection_error.emit(msg) # Report error to the main window - - except (socket.error, ConnectionRefusedError): - msg = "Connection refused. Is the server offline or the port incorrect?" - print(f"❌ {msg}") - self.connection_error.emit(msg) - - except TimeoutError: - msg = "Connection timed out. The server is not responding." - print(f"❌ {msg}") - self.connection_error.emit(msg) - - except Exception as e: - # Catch any other unexpected errors during connection or loop - msg = f"An unexpected error occurred: {e}" - print(f"❌ {msg}") - self.connection_error.emit(msg) - - # def on_subscribe(self, client, userdata, mid, reason_code_list, properties): - # """Callback function for when the broker responds to a subscription request.""" - # if reason_code_list[0].is_failure: - # print(f"❌ Broker rejected subscription: {reason_code_list[0]}") - # else: - # print(f"✅ Broker accepted subscription with QoS: {reason_code_list[0].value}") - - # --- (The rest of the file remains the same) --- @pyqtSlot() def disconnect_from_broker(self): - """Stops the MQTT client's network loop.""" if self.client: self.client.loop_stop() self.client.disconnect() print("Stopping MQTT network loop.") - def subscribe_to_topic(self, topic): # Add qos parameter + def subscribe_to_topic(self, topic): print(f"Subscribing to topic: {topic}") self.client.subscribe(topic) def publish_message(self, topic, payload): self.client.publish(topic, payload) - - def cleanup(self): - print("Stopping MQTT network loop.") - self.client.loop_stop() - diff --git a/ui/main_window.py b/ui/main_window.py index 923a6e3..c5afc3a 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -535,7 +535,7 @@ class MainWindow(QMainWindow): self.theme_button.setText("🌙") self.theme_button.setToolTip("Switch to Dark Theme") self.setStyleSheet(get_light_theme_styles(self.scale_factor)) - self.timestamp_label.setStyleSheet("color: #ecf0f1; background-color: transparent;") + self.status_bar_device_id_label.setStyleSheet("color: #ecf0f1; background-color: transparent;") self.apply_config_tab_styles() def _on_fade_finished(self, overlay): @@ -592,10 +592,18 @@ class MainWindow(QMainWindow): logo_label = QLabel("BSS Dashboard") logo_label.setFont(QFont("Arial", max(9, int(11 * self.scale_factor)), QFont.Weight.Bold)) logo_label.setStyleSheet("color: #ecf0f1; background-color: transparent;") - self.timestamp_label = QLabel("Last Update: N/A") - self.timestamp_label.setFont(QFont("Arial", max(8, int(9 * self.scale_factor)))) + + self.status_bar_device_id_label = QLabel("Device ID: --- |") # Create the new label + self.status_bar_device_id_label.setFont(QFont("Arial", max(10, int(9 * self.scale_factor)))) + self.status_bar_device_id_label.setStyleSheet("color: #000000; background-color: transparent; margin-left: 10px;") + + self.status_bar_timestamp_label = QLabel("Last Update: --- ") # Create the new timestamp label + self.status_bar_timestamp_label.setFont(QFont("Arial", max(10, int(9 * self.scale_factor)))) + self.status_bar_timestamp_label.setStyleSheet("color: #ffffffff; background-color: transparent; margin-left: 10px;") + left_layout.addWidget(logo_label) - left_layout.addWidget(self.timestamp_label) + left_layout.addWidget(self.status_bar_device_id_label) + left_layout.addWidget(self.status_bar_timestamp_label) left_layout.addStretch() logo_path = resource_path("logo/vec_logo_svg.svg") @@ -778,6 +786,17 @@ class MainWindow(QMainWindow): top_bar_layout = QHBoxLayout() + device_id_label = QLabel("DEVICE ID:") + device_id_label.setObjectName("TimestampTitleLabel") + top_bar_layout.addWidget(device_id_label) + + self.device_id_display_field = QLineEdit("---- ---- ---- ---- ----") + self.device_id_display_field.setReadOnly(True) + self.device_id_display_field.setObjectName("TimestampDataField") + top_bar_layout.addWidget(self.device_id_display_field) + + top_bar_layout.addSpacerItem(QSpacerItem(20, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum)) + ts_label = QLabel("LAST RECV TS:") ts_label.setObjectName("TimestampTitleLabel") top_bar_layout.addWidget(ts_label) @@ -787,6 +806,7 @@ class MainWindow(QMainWindow): self.last_recv_ts_field.setObjectName("TimestampDataField") top_bar_layout.addWidget(self.last_recv_ts_field) + top_bar_layout.addSpacerItem(QSpacerItem(20, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum)) # top_bar_layout.addWidget(QLabel("Backup Supply:")) @@ -809,6 +829,7 @@ class MainWindow(QMainWindow): refresh_btn = QPushButton("⟳ Refresh") refresh_btn.setObjectName("RefreshButton") refresh_btn.clicked.connect(self.reset_dashboard_ui) + refresh_btn.clicked.connect(self.clear_instance_log) top_bar_layout.addWidget(refresh_btn) reset_btn = QPushButton("Station Reset") @@ -976,14 +997,18 @@ class MainWindow(QMainWindow): cursor.removeSelectedText() def setup_logs_ui(self): - layout = QHBoxLayout(self.logs_tab) + # Use a main vertical layout to hold the logs and the new button + main_layout = QVBoxLayout(self.logs_tab) + + # Create a horizontal layout for the two log panels + logs_layout = QHBoxLayout() # --- Setup the Request Logs area --- request_group = QGroupBox("Request Logs (Dashboard -> MQTT Server)") request_layout = QVBoxLayout(request_group) self.request_log_area = QPlainTextEdit() self.request_log_area.setReadOnly(True) - self.request_log_area.setObjectName("LogPanel") + self.request_log_area.setObjectName("LogPanel") request_layout.addWidget(self.request_log_area) # --- Setup the Event Logs area --- @@ -994,8 +1019,21 @@ class MainWindow(QMainWindow): self.event_log_area.setObjectName("LogPanel") event_layout.addWidget(self.event_log_area) - layout.addWidget(request_group) - layout.addWidget(event_group) + logs_layout.addWidget(request_group) + logs_layout.addWidget(event_group) + + # --- ADD THIS BUTTON --- + button_layout = QHBoxLayout() + clear_logs_btn = QPushButton("Clear All Logs") + clear_logs_btn.setObjectName("ClearLogButton") + clear_logs_btn.clicked.connect(self.clear_all_logs) + button_layout.addStretch() # Pushes the button to the right + button_layout.addWidget(clear_logs_btn) + # --- END OF ADDED CODE --- + + # Add both layouts to the main vertical layout + main_layout.addLayout(logs_layout) + main_layout.addLayout(button_layout) def _create_main_status_field(self): field = QLineEdit() @@ -1057,8 +1095,6 @@ class MainWindow(QMainWindow): # --- THIS IS THE CRITICAL METHOD TO UPDATE --- def on_message_received(self, topic, payload): - now = datetime.datetime.now() - self.timestamp_label.setText(f"Last Update: {now.strftime('%Y-%m-%d %H:%M:%S')}") try: msg_type = topic.split('/')[-1] @@ -1070,6 +1106,8 @@ class MainWindow(QMainWindow): # print(data_dict) self._log_periodic_to_terminal(decoded_payload, data_dict) self.update_main_dashboard(data_dict) + now = datetime.datetime.fromtimestamp(data_dict.get('ts', datetime.datetime.now().timestamp())).strftime('%Y-%m-%d %H:%M:%S') + self.status_bar_timestamp_label.setText(f"| Last Update: {now}") if self.save_logs_checkbox.isChecked(): log_payload_str = json.dumps(data_dict) self.log_data_signal.emit([now, topic, data_dict, payload]) @@ -1244,30 +1282,68 @@ class MainWindow(QMainWindow): self.mqtt_client.publish_message(topic, serialized_payload) def handle_open_door(self, chamber_num): - print(f"Requesting to open door for chamber {chamber_num}...") - request = rpcRequest( - ts=int(time.time()), - jobId=f"job_{int(time.time())}", - jobType=jobType_e.JOBTYPE_GATE_OPEN_CLOSE - ) - request.slotInfo.slotId = chamber_num - request.slotInfo.state = 1 - self._send_rpc_request(request) + # --- ADDED: Confirmation Dialog --- + reply = QMessageBox.question(self, 'Confirm Open Door', + f"Are you sure you want to open the door for Chamber {chamber_num}?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No) + + # --- ADDED: Check the user's reply --- + if reply == QMessageBox.StandardButton.Yes: + # If user confirms, proceed with sending the command + print(f"Requesting to open door for chamber {chamber_num}...") + request = rpcRequest( + ts=int(time.time()), + jobId=f"job_{int(time.time())}", + jobType=jobType_e.JOBTYPE_GATE_OPEN_CLOSE + ) + request.slotInfo.slotId = chamber_num + request.slotInfo.state = 1 # State 1 for OPEN + self._send_rpc_request(request) + else: + # If user cancels, log the cancellation + self.log_to_instance_view(f"Open Door command for Chamber {chamber_num} cancelled.") + + # def handle_charger_control(self, chamber_num, state): + # action = "ON" if state else "OFF" + # print(f"Requesting to turn charger {action} for chamber {chamber_num}...") + # request = rpcRequest( + # ts=int(time.time()), + # jobId=f"job_{int(time.time())}", + # jobType=jobType_e.JOBTYPE_CHARGER_ENABLE_DISABLE + # ) + # request.slotInfo.slotId = chamber_num + # request.slotInfo.state = 1 if state else 0 + # self._send_rpc_request(request) def handle_charger_control(self, chamber_num, state): action = "ON" if state else "OFF" - print(f"Requesting to turn charger {action} for chamber {chamber_num}...") - request = rpcRequest( - ts=int(time.time()), - jobId=f"job_{int(time.time())}", - jobType=jobType_e.JOBTYPE_CHARGER_ENABLE_DISABLE - ) - request.slotInfo.slotId = chamber_num - request.slotInfo.state = 1 if state else 0 - self._send_rpc_request(request) + + # --- ADDED: Confirmation Dialog --- + reply = QMessageBox.question(self, f'Confirm Charger Control', + f"Are you sure you want to turn the charger {action} for Chamber {chamber_num}?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No) + + # --- ADDED: Check the user's reply --- + if reply == QMessageBox.StandardButton.Yes: + # If user confirms, proceed with sending the command + print(f"Requesting to turn charger {action} for chamber {chamber_num}...") + request = rpcRequest( + ts=int(time.time()), + jobId=f"job_{int(time.time())}", + jobType=jobType_e.JOBTYPE_CHARGER_ENABLE_DISABLE + ) + request.slotInfo.slotId = chamber_num + request.slotInfo.state = 1 if state else 0 + self._send_rpc_request(request) + else: + # If user cancels, log the cancellation + self.log_to_instance_view(f"Charger {action} command for Chamber {chamber_num} cancelled.") def reset_dashboard_ui(self): self.log_request("INFO", "Dashboard UI cleared by user.") + self.device_id_display_field.setText("---- ---- ---- ---- ----") self.last_recv_ts_field.setText("No Data") self.sdc_field.setText("") for chamber in self.chamber_widgets: @@ -1286,9 +1362,9 @@ class MainWindow(QMainWindow): print("Cleaning up previous MQTT thread...") if self.mqtt_client: self.mqtt_client.disconnect_from_broker() - self.mqtt_client.cleanup() + # self.mqtt_client.cleanup() self.mqtt_thread.quit() - self.mqtt_thread.wait() + self.mqtt_thread.wait(1000) if self.save_logs_checkbox.isChecked(): self.start_csv_logger() self.reset_dashboard_ui() @@ -1302,7 +1378,7 @@ class MainWindow(QMainWindow): try: port = int(self.port_input.text()) except ValueError: - self.timestamp_label.setText("Error: Port must be a number.") + self.status_bar_device_id_label.setText("Error: Port must be a number.") return # ========================================================== @@ -1378,6 +1454,18 @@ class MainWindow(QMainWindow): self.csv_logger = None self.logger_thread = None + def clear_instance_log(self): + """Clears the text in the instance log area.""" + if self.instance_log_area: + self.instance_log_area.clear() + + def clear_all_logs(self): + """Clears the text in both the request and event log areas.""" + if self.request_log_area: + self.request_log_area.clear() + if self.event_log_area: + self.event_log_area.clear() + def on_connection_status_changed(self, is_connected, message): """Handles connection status updates from the MQTT client.""" self.connect_button.setEnabled(not is_connected) @@ -1385,7 +1473,7 @@ class MainWindow(QMainWindow): self.set_config_inputs_enabled(not is_connected) # Set the text of the label to the message from the client - self.timestamp_label.setText(message) + self.status_bar_device_id_label.setText(message) self.log_to_instance_view(message.strip("✅❌🔌🔴 ")) @@ -1393,6 +1481,8 @@ class MainWindow(QMainWindow): client_id = self.client_id_input.text() version = self.version_input.text() device_id = self.device_id_input.text() + + self.status_bar_device_id_label.setText(f"Device ID: {device_id}") periodic_topic = f"VEC/{client_id}/{version}/{device_id}/PERIODIC" events_topic = f"VEC/{client_id}/{version}/{device_id}/EVENTS" @@ -1409,10 +1499,20 @@ class MainWindow(QMainWindow): self.tabs.setCurrentWidget(self.main_tab) + else: + + QMessageBox.critical(self, "Connection Failed", message) + + self.status_bar_device_id_label.setText("Device ID: --- |") # Clear the device ID label on disconnect + self.status_bar_timestamp_label.setText("Last Update: ---") # Clear the timestamp label on disconnect + + def update_main_dashboard(self, data): try: ts = datetime.datetime.fromtimestamp(data.get('ts', datetime.datetime.now().timestamp())).strftime('%Y-%m-%d %H:%M:%S') + device_id = data.get("deviceId", "---- ---- ---- ---- ----") + self.device_id_display_field.setText(device_id) self.last_recv_ts_field.setText(ts) slot_payloads = data.get("slotLevelPayload", []) for i, slot_data in enumerate(slot_payloads): diff --git a/ui/styles.py b/ui/styles.py index 03f0574..6c751aa 100644 --- a/ui/styles.py +++ b/ui/styles.py @@ -119,8 +119,8 @@ def get_light_theme_styles(scale=1.0): #ChamberChgOffButton:hover {{ background-color: #d42318; }} /* Status & alarms */ - QLabel[status="present"] {{ background-color: #2ecc71; color: white; border-radius: {int(4*scale)}px; padding: {int(3*scale)}px; }} - QLabel[status="absent"] {{ background-color: #e74c3c; color: white; border-radius: {int(4*scale)}px; padding: {int(3*scale)}px; }} + QLabel[status="present"] {{ background-color: #2ecc71; color: white; border-radius: {int(4*scale)}px; padding: {int(6*scale)}px; }} + QLabel[status="absent"] {{ background-color: #e74c3c; color: white; border-radius: {int(4*scale)}px; padding: {int(6*scale)}px; }} QLabel[alarm="active"] {{ background-color: #e74c3c; color: white; font-weight: bold; border-radius: {int(4*scale)}px; padding: {int(2*scale)}px; }} QLabel[alarm="inactive"] {{ background-color: transparent; color: black; }} QGroupBox#ChamberWidget {{ border: 2px solid #007bff; }} @@ -365,8 +365,8 @@ def get_dark_theme_styles(scale=1.0): #SendAudioButton {{ background-color: #3498db; color: white; border: none; font-size: {max(10, int(14 * scale))}px; }} #SendAudioButton:hover {{ background-color: #5dade2; }} - QLabel[status="present"] {{ background-color: #2ecc71; color: white; border-radius: {int(4*scale)}px; padding: {int(3*scale)}px; }} - QLabel[status="absent"] {{ background-color: #e74c3c; color: white; border-radius: {int(4*scale)}px; padding: {int(3*scale)}px; }} + QLabel[status="present"] {{ background-color: #2ecc71; color: white; border-radius: {int(4*scale)}px; padding: {int(6*scale)}px; }} + QLabel[status="absent"] {{ background-color: #e74c3c; color: white; border-radius: {int(4*scale)}px; padding: {int(6*scale)}px; }} QLabel[alarm="active"] {{ background-color: #e74c3c; color: white; font-weight: bold; border-radius: {int(4*scale)}px; padding: {int(2*scale)}px; }} QLabel[alarm="inactive"] {{ background-color: transparent; color: #f0f0f0; }} QGroupBox#ChamberWidget {{ border: 2px solid #3498db; }} diff --git a/ui/widgets.py b/ui/widgets.py index e3d90df..89bf678 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -102,6 +102,8 @@ class ChamberWidget(QGroupBox): self.door_status_field.setObjectName("DoorStatusField") self.door_status_field.setMaximumWidth(int(140 * scale)) button_layout.addWidget(self.door_status_field) + + self.door_status_field.setAlignment(Qt.AlignmentFlag.AlignCenter) # Part 2: Buttons self.open_door_btn = QPushButton("OPEN DOOR") @@ -135,92 +137,67 @@ class ChamberWidget(QGroupBox): return field def update_data(self, data): - # Retrieve battery and charger presence with fallback keys - battery_present_status = data.get("batteryPresent") - # <-- MODIFIED: Now checks for 'chargerPresent' key from your payload - charger_on_status = data.get("chargerPresent", data.get("chargerOn", data.get("chgStatus", 0))) + # --- Get the status of the battery and charger first --- + battery_is_present = data.get("batteryPresent") == 1 + charger_is_present = data.get("chargerPresent") == 1 - if battery_present_status == 1: + # --- Update Battery Status Label --- + if battery_is_present: self.battery_status_label.setText("PRESENT") self.battery_status_label.setProperty("status", "present") else: self.battery_status_label.setText("ABSENT") self.battery_status_label.setProperty("status", "absent") - if charger_on_status == 1: + # --- Update Charger Status Label --- + if charger_is_present: self.charger_status_label.setText("CHARGER ON") self.charger_status_label.setProperty("status", "present") else: self.charger_status_label.setText("CHARGER OFF") self.charger_status_label.setProperty("status", "absent") + # Re-apply stylesheets for the status labels for widget in [self.battery_status_label, self.charger_status_label]: widget.style().unpolish(widget) widget.style().polish(widget) - self.id_field.setText(data.get("batteryIdentification", " ")) - self.soc_field.setText(f'{data.get("soc", 0)}%') - self.voltage_field.setText(f'{data.get("voltage", 0) / 1000.0:.2f} V') - self.temp_field.setText(f'{data.get("batteryMaxTemp", 0) / 10.0:.1f} °C') - self.battery_fault_field.setText(str(data.get("batteryFaultCode", 0))) - - # <-- MODIFIED: Now checks for 'chargerMaxTemp' key from your payload - charger_temp = data.get("chargerMaxTemp", data.get("chargerTemp", data.get("chgTemp", 0))) - self.chg_temp_field.setText(f'{charger_temp / 10.0:.1f} °C') - self.charger_fault_field.setText(str(data.get("chargerFaultCode", 0))) - self.current_field.setText(f'{data.get("current", 0) / 1000.0:.2f} A') - - slot_temp = data.get("slotTemperature", data.get("slotTemp", 0)) - self.slot_temp_field.setText(f'{slot_temp / 10.0:.1f} °C') + # --- Conditionally update battery-specific fields --- + if battery_is_present: + self.soc_field.setText(f'{data.get("soc", 0)}%') + self.voltage_field.setText(f'{data.get("voltage", 0) / 1000.0:.2f} V') + self.temp_field.setText(f'{data.get("batteryMaxTemp", 0) / 10.0:.1f} °C') + self.battery_fault_field.setText(str(data.get("batteryFaultCode", 0))) + else: + # Clear the fields if the battery is absent + self.soc_field.setText(" ") + self.voltage_field.setText(" ") + self.temp_field.setText(" ") + self.battery_fault_field.setText(" ") + # --- Conditionally update charger-specific fields --- + if charger_is_present: + charger_temp = data.get("chargerMaxTemp", 0) + self.chg_temp_field.setText(f'{charger_temp / 10.0:.1f} °C') + self.charger_fault_field.setText(str(data.get("chargerFaultCode", 0))) + self.current_field.setText(f'{data.get("current", 0) / 1000.0:.2f} A') + else: + # Clear the fields if the charger is off/absent + self.chg_temp_field.setText(" ") + self.charger_fault_field.setText(" ") + self.current_field.setText(" ") + + # --- Update fields that are always visible --- + self.id_field.setText(data.get("batteryIdentification", " ")) + slot_temp = data.get("slotTemperature", 0) + self.slot_temp_field.setText(f'{slot_temp / 10.0:.1f} °C') + door_status = "OPEN" if data.get("doorStatus") == 1 else "CLOSED" self.door_status_field.setText(door_status) - if door_status == "OPEN": - self.door_status_field.setStyleSheet("background-color: #2E7D32; color: white; border-radius: 3px;") + self.door_status_field.setStyleSheet("background-color: #30ce72; color: white; border-radius: 3px;") else: - self.door_status_field.setStyleSheet("background-color: #C62828; color: white; border-radius: 3px;") - - - - # def update_data(self, data): - # if data.get("batteryPresent") == 1: - # self.battery_status_label.setText("PRESENT") - # self.battery_status_label.setProperty("status", "present") - # else: - # self.battery_status_label.setText("ABSENT") - # self.battery_status_label.setProperty("status", "absent") - - # if data.get("chargerOn") == 1: - # self.charger_status_label.setText("CHARGER ON") - # self.charger_status_label.setProperty("status", "present") - # else: - # self.charger_status_label.setText("CHARGER OFF") - # self.charger_status_label.setProperty("status", "absent") - - # for widget in [self.battery_status_label, self.charger_status_label]: - # widget.style().unpolish(widget) - # widget.style().polish(widget) - - # self.id_field.setText(data.get("batteryIdentification", " ")) - # self.soc_field.setText(f'{data.get("soc", 0)}%') - # self.voltage_field.setText(f'{data.get("voltage", 0) / 1000.0:.2f} V') - # self.temp_field.setText(f'{data.get("batteryMaxTemp", 0) / 10.0:.1f} °C') - # self.battery_fault_field.setText(str(data.get("batteryFaultCode", 0))) - - # self.chg_temp_field.setText(f'{data.get("chargerTemp", 0) / 10.0:.1f} °C') - # self.charger_fault_field.setText(str(data.get("chargerFaultCode", 0))) - # self.current_field.setText(f'{data.get("current", 0) / 1000.0:.2f} A') - # self.slot_temp_field.setText(f'{data.get("slotTemperature", 0) / 10.0:.1f} °C') - - # door_status = "OPEN" if data.get("doorStatus") == 1 else "CLOSED" - # self.door_status_field.setText(door_status) - - # # --- ADDED: Set color based on door status --- - # if door_status == "OPEN": - # self.door_status_field.setStyleSheet("background-color: #2E7D32; color: white; border-radius: 3px;") - # else: # CLOSED - # self.door_status_field.setStyleSheet("background-color: #C62828; color: white; border-radius: 3px;") + self.door_status_field.setStyleSheet("background-color: #fd5443; color: white; border-radius: 3px;") def reset_to_default(self): """Resets all fields in this chamber widget to their default state."""