diff --git a/core/csv_logger.py b/core/csv_logger.py index e324226..82b71da 100644 --- a/core/csv_logger.py +++ b/core/csv_logger.py @@ -149,6 +149,7 @@ class CsvLogger(QObject): print(f"❌ An unexpected error occurred in the logger thread: {e}") continue + @pyqtSlot() def stop_logging(self): self.timer.stop() self._process_queue() diff --git a/core/mqtt_client.py b/core/mqtt_client.py index 8f4d218..03f2123 100644 --- a/core/mqtt_client.py +++ b/core/mqtt_client.py @@ -17,7 +17,8 @@ # super().__init__() # self.broker = broker # self.port = port - +# self._is_connected = False + # self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id) # if user and password: # self.client.username_pw_set(user, password) @@ -29,22 +30,35 @@ # def on_connect(self, client, userdata, flags, rc, properties): # if rc == 0: +# self._is_connected = True # Set flag on success # 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 +# # This block now handles the failure message, but not the disconnect signal +# error_message = f"Connection failed (Code: {rc})" +# if rc == 5: +# error_message = "❌ Not Authorized: Check username and password." + +# print(f"Failed to connect: {error_message}") +# self.connection_status_changed.emit(False, error_message) +# # The on_disconnect callback will handle the disconnected signal # 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 +# # --- MODIFIED: This entire block is now protected by the flag --- +# if not self._is_connected and rc != 0: +# # This is a connection failure, on_connect already handled the message +# pass +# else: +# # This is a true disconnection from an active session +# print("Disconnected from MQTT Broker.") +# self.connection_status_changed.emit(False, "💔 Disconnected") + +# # This logic now runs only ONCE per disconnect/failure event +# if self._is_connected or rc != 0: +# self.disconnected.emit() + +# self._is_connected = False # def on_message(self, client, userdata, msg): # # print(f"Received {len(msg.payload)} bytes of binary data from topic `{msg.topic}`") @@ -134,7 +148,6 @@ # print("Stopping MQTT network loop.") # self.client.loop_stop() - # In core/mqtt_client.py import socket from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot @@ -146,7 +159,6 @@ class MqttClient(QObject): # 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) @@ -154,6 +166,7 @@ class MqttClient(QObject): super().__init__() self.broker = broker self.port = port + self._is_connected = False # Flag to track connection state self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id) if user and password: @@ -165,19 +178,29 @@ class MqttClient(QObject): def on_connect(self, client, userdata, flags, rc, properties): if rc == 0: + self._is_connected = True print("Connection to MQTT Broker successful!") self.connection_status_changed.emit(True, "✅ Connected") self.connected.emit() else: - print(f"Failed to connect, return code {rc}\n") - self.connection_status_changed.emit(False, f"❌ Connection failed (Code: {rc})") - self.disconnected.emit() + # Report the specific error from the broker + error_message = f"Connection failed (Code: {rc})" + if rc == 5: + error_message = "❌ Not Authorized: Check username and password." + + print(f"Failed to connect: {error_message}") + self.connection_status_changed.emit(False, error_message) + # The on_disconnect callback will handle the generic 'disconnected' signal def on_disconnect(self, client, userdata, flags, rc, properties): - print("Disconnected from MQTT Broker.") - self.connection_status_changed.emit(False, "💔 Disconnected") + # Only show the generic "Disconnected" message if we were actually connected before. + if self._is_connected: + print("Disconnected from MQTT Broker.") + self.connection_status_changed.emit(False, "💔 Disconnected") + + # Always emit the generic disconnected signal to trigger cleanup in the main window. self.disconnected.emit() - self.stop_logging_signal.emit() + self._is_connected = False def on_message(self, client, userdata, msg): self.message_received.emit(msg.topic, msg.payload) @@ -188,29 +211,21 @@ class MqttClient(QObject): try: self.client.connect(self.broker, self.port, 60) self.client.loop_start() - except socket.gaierror: - msg = "Host not found. Check broker address or your internet connection." - print(f"❌ Connection Error: {msg}") - 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, msg) except Exception as e: - msg = f"An unexpected error occurred: {e}" + # Catch any exception during the initial connect call + msg = f"Connection Error: {e}" print(f"❌ {msg}") - self.connection_status_changed.emit(False, f"Error: {e}") + self.connection_status_changed.emit(False, msg) + self.disconnected.emit() @pyqtSlot() def disconnect_from_broker(self): if self.client: self.client.loop_stop() self.client.disconnect() - print("Stopping MQTT network loop.") 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) + self.client.publish(topic, payload) \ No newline at end of file diff --git a/ui/main_window.py b/ui/main_window.py index c5afc3a..a44e16c 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -1358,15 +1358,17 @@ class MainWindow(QMainWindow): self.update_swap_buttons_state() def connect_to_mqtt(self): + if self.mqtt_thread and self.mqtt_thread.isRunning(): print("Cleaning up previous MQTT thread...") if self.mqtt_client: self.mqtt_client.disconnect_from_broker() - # self.mqtt_client.cleanup() self.mqtt_thread.quit() - self.mqtt_thread.wait(1000) + self.mqtt_thread.wait(1000) # Use a timeout to prevent freezing + if self.save_logs_checkbox.isChecked(): self.start_csv_logger() + self.reset_dashboard_ui() broker = self.broker_input.text() user = self.username_input.text() @@ -1401,9 +1403,13 @@ class MainWindow(QMainWindow): self.mqtt_thread = QThread() self.mqtt_client = MqttClient(broker, port, user, password, client_id) self.mqtt_client.moveToThread(self.mqtt_thread) - self.mqtt_client.stop_logging_signal.connect(self.csv_logger.stop_logging) + + # Connect signals self.mqtt_client.connection_status_changed.connect(self.on_connection_status_changed) self.mqtt_client.message_received.connect(self.on_message_received) + self.mqtt_client.connected.connect(self.start_csv_logger) + self.mqtt_client.disconnected.connect(self.stop_csv_logger) + self.mqtt_thread.started.connect(self.mqtt_client.connect_to_broker) self.mqtt_thread.start() @@ -1501,32 +1507,92 @@ class MainWindow(QMainWindow): else: - QMessageBox.critical(self, "Connection Failed", message) + QMessageBox.critical(self, "Connection Status", 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): + # 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): + # if i < len(self.chamber_widgets): + # self.chamber_widgets[i].update_data(slot_data) + # # print("Updating chamber", i+1, slot_data) + # if (i+1) in self.swap_buttons: + # is_present = slot_data.get("batteryPresent") == 1 + # self.swap_buttons[i+1].setStyleSheet("background-color: #2ecc71;" if is_present else "") + # sdc_value = data.get("stationDiagnosticCode", 0) + # self.sdc_field.setText(str(sdc_value)) + # self.update_diagnostic_alarms(sdc_value) + + # backup_status = data.get("backupSupplyStatus", 0) # Default to 0 if not present + # if backup_status == 1: + # self.backup_supply_indicator.setText("Backup ON") + # self.backup_supply_indicator.setStyleSheet( + # """ + # QLabel { + # background-color: transparent; + # border: 2px solid #28a745; /* Green */ + # color: #28a745; + # border-radius: 5px; + # font-weight: bold; + # } + # """ + # ) + # # self.top_bar_frame.setStyleSheet("#topBarFrame { border: 1px solid #28a745; }") + # else: + # self.backup_supply_indicator.setText("Backup OFF") + # self.backup_supply_indicator.setStyleSheet( + # """ + # QLabel { + # background-color: transparent; + # border: 2px solid #dc3545; /* Red */ + # color: #dc3545; + # border-radius: 5px; + # font-weight: bold; + # } + # """ + # ) + # # self.top_bar_frame.setStyleSheet("#topBarFrame { border: 1px solid #dc3545; }") + + # except Exception as e: + # print(f"Error updating dashboard: {e}") + + + 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) + self.device_id_display_field.setText(data.get("deviceId", "---- ---- ---- ---- ----")) + slot_payloads = data.get("slotLevelPayload", []) for i, slot_data in enumerate(slot_payloads): if i < len(self.chamber_widgets): self.chamber_widgets[i].update_data(slot_data) - # print("Updating chamber", i+1, slot_data) - if (i+1) in self.swap_buttons: - is_present = slot_data.get("batteryPresent") == 1 - self.swap_buttons[i+1].setStyleSheet("background-color: #2ecc71;" if is_present else "") + + # --- THIS IS THE CORRECTED LOGIC --- + slot_number = i + 1 + if slot_number in self.swap_buttons: + # First, check if the button is already part of the user's selection + if slot_number not in self.swap_sequence: + # If NOT selected, update its color based on battery presence + is_present = slot_data.get("batteryPresent") == 1 + button = self.swap_buttons[slot_number] + # Set to green if present, otherwise clear the style + button.setStyleSheet("background-color: #2ecc71;" if is_present else "") + sdc_value = data.get("stationDiagnosticCode", 0) self.sdc_field.setText(str(sdc_value)) self.update_diagnostic_alarms(sdc_value) - backup_status = data.get("backupSupplyStatus", 0) # Default to 0 if not present + backup_status = data.get("backupSupplyStatus", 0) if backup_status == 1: self.backup_supply_indicator.setText("Backup ON") self.backup_supply_indicator.setStyleSheet( @@ -1540,7 +1606,6 @@ class MainWindow(QMainWindow): } """ ) - # self.top_bar_frame.setStyleSheet("#topBarFrame { border: 1px solid #28a745; }") else: self.backup_supply_indicator.setText("Backup OFF") self.backup_supply_indicator.setStyleSheet( @@ -1554,7 +1619,6 @@ class MainWindow(QMainWindow): } """ ) - # self.top_bar_frame.setStyleSheet("#topBarFrame { border: 1px solid #dc3545; }") except Exception as e: print(f"Error updating dashboard: {e}") @@ -1564,7 +1628,7 @@ class MainWindow(QMainWindow): """Formats and prints the periodic data to the terminal as a clean table.""" try: current_time = datetime.datetime.fromtimestamp(decoded_payload.ts).strftime('%Y-%m-%d %H:%M:%S') - device_id = data_dict.get("deviceId", "N/A") + device_id = data_dict.get("deviceId", " ") backup_supply = "ON" if decoded_payload.backupSupplyStatus == 1 else "OFF" station_sdc = decoded_payload.stationDiagnosticCode @@ -1587,21 +1651,21 @@ class MainWindow(QMainWindow): is_door_open = chamber_data.get("doorStatus") == 1 # Get and format the Slot Temperature - slot_temp_raw = chamber_data.get('slotTemperature', 'N/A') - slot_temp_celsius = f"{slot_temp_raw / 10:.1f}" if isinstance(slot_temp_raw, int) else "N/A" + slot_temp_raw = chamber_data.get('slotTemperature', ' ') + slot_temp_celsius = f"{slot_temp_raw / 10:.1f}" if isinstance(slot_temp_raw, int) else " " # Get and format the Battery Temperature - battery_temp_raw = chamber_data.get('batteryMaxTemp', 'N/A') - battery_temp_celsius = f"{battery_temp_raw / 10:.1f}" if isinstance(battery_temp_raw, int) else "N/A" + battery_temp_raw = chamber_data.get('batteryMaxTemp', ' ') + battery_temp_celsius = f"{battery_temp_raw / 10:.1f}" if isinstance(battery_temp_raw, int) else " " print(row_format.format( i, - chamber_data.get('batteryIdentification', 'N/A'), + chamber_data.get('batteryIdentification', ' '), "✅" if is_present else "❌", "✅" if is_charging else "❌", - chamber_data.get('soc', 'N/A'), - chamber_data.get('batVoltage', 'N/A'), - chamber_data.get('current', 'N/A'), + chamber_data.get('soc', ' '), + chamber_data.get('batVoltage', ' '), + chamber_data.get('current', ' '), slot_temp_celsius, battery_temp_celsius, "Open" if is_door_open else "Closed" diff --git a/ui/styles.py b/ui/styles.py index 6c751aa..b75ff23 100644 --- a/ui/styles.py +++ b/ui/styles.py @@ -111,10 +111,10 @@ def get_light_theme_styles(scale=1.0): font-weight: bold; border-radius: {int(4*scale)}px; }} - #ChamberOpenDoorButton {{ background-color: #607d8b; }} - #ChamberChgOnButton {{ background-color: #52be80; }} - #ChamberChgOffButton {{ background-color: #cd6155; }} - #ChamberOpenDoorButton:hover {{ background-color: #485c64; }} + #ChamberOpenDoorButton {{ background-color: #d4d4d4; }} + #ChamberChgOnButton {{ background-color: #d4d4d4; }} + #ChamberChgOffButton {{ background-color: #d4d4d4; }} + #ChamberOpenDoorButton:hover {{ background-color: #1aa89c; }} #ChamberChgOnButton:hover {{ background-color: #04d45d; }} #ChamberChgOffButton:hover {{ background-color: #d42318; }} @@ -334,10 +334,10 @@ def get_dark_theme_styles(scale=1.0): font-weight: bold; border-radius: {int(4*scale)}px; }} - #ChamberOpenDoorButton {{ background-color: #607d8b; }} - #ChamberChgOnButton {{ background-color: #52be80; }} - #ChamberChgOffButton {{ background-color: #cd6155; }} - #ChamberOpenDoorButton:hover {{ background-color: #485c64; }} + #ChamberOpenDoorButton {{ background-color: #5c5c5c; }} + #ChamberChgOnButton {{ background-color: #5c5c5c; }} + #ChamberChgOffButton {{ background-color: #5c5c5c; }} + #ChamberOpenDoorButton:hover {{ background-color: #1aa89c; }} #ChamberChgOnButton:hover {{ background-color: #04d45d; }} #ChamberChgOffButton:hover {{ background-color: #d42318; }}