diff --git a/core/csv_logger.py b/core/csv_logger.py index 70daa80..e324226 100644 --- a/core/csv_logger.py +++ b/core/csv_logger.py @@ -24,7 +24,7 @@ class CsvLogger(QObject): self.num_slots = 9 def start_logging(self): - self.timer.start(100) + self.timer.start(10) print(f"✅ CSV logging service started for session: {self.session_name}") def _get_writer(self, device_id, file_group): diff --git a/core/mqtt_client.py b/core/mqtt_client.py index d7783ec..4a11887 100644 --- a/core/mqtt_client.py +++ b/core/mqtt_client.py @@ -30,30 +30,32 @@ class MqttClient(QObject): def on_connect(self, client, userdata, flags, rc, properties): if rc == 0: print("Connection to MQTT Broker successful!") - # --- MODIFIED EMIT: Send a success message --- 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") - # --- MODIFIED EMIT: Send a failure message --- self.connection_status_changed.emit(False, f"❌ Connection failed (Code: {rc})") - self.stop_logging_signal.emit() # Emit the signal + 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) - def on_disconnect(self, client, userdata, flags, rc, properties): - print("Disconnected from MQTT Broker.") - # Change the icon in the line below from 🔌 to 🔴 ❌ 🚫 💔 - self.connection_status_changed.emit(False, "💔 Disconnected") - self.disconnected.emit() - # --- 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, 60) + 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." @@ -77,7 +79,8 @@ class MqttClient(QObject): print(f"Attempting to connect to {self.broker}:{self.port}...") try: # 1. Attempt to connect - self.client.connect(self.broker, self.port, 60) + # 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 @@ -129,4 +132,5 @@ class MqttClient(QObject): def cleanup(self): print("Stopping MQTT network loop.") - self.client.loop_stop() \ No newline at end of file + self.client.loop_stop() + diff --git a/logo/vec_logo.png b/logo/vec_logo.png deleted file mode 100644 index 55ec978..0000000 Binary files a/logo/vec_logo.png and /dev/null differ diff --git a/ui/main_window.py b/ui/main_window.py index 554270f..923a6e3 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -43,7 +43,7 @@ class MainWindow(QMainWindow): super().__init__() # self.setWindowIcon(QIcon("logo/v_logo.png")) self.scale_factor = scale_factor - self.setWindowTitle("Battery Swap Station Dashboard v4.1") + self.setWindowTitle("Battery Swap Station Dashboard v4.2") self.setWindowIcon(QIcon(resource_path("assets/icon.ico"))) self.settings = QSettings("VECMOCON", "BatterySwapDashboard") @@ -355,7 +355,7 @@ class MainWindow(QMainWindow): title_box.addWidget(subtitle) header.addLayout(title_box, 1) - badge = QLabel("Version 4.1") + badge = QLabel("Version 4.2") badge.setObjectName("badge") header.addWidget(badge, 0, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) @@ -721,10 +721,15 @@ class MainWindow(QMainWindow): default_log_filename = f"Station_Mqtt_Dashboard_log_{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv" # self.log_filename_input = QLineEdit(default_log_filename) log_dir_layout = QHBoxLayout() - script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + script_dir = self.get_base_path() default_log_dir = os.path.join(script_dir, "logs") self.log_dir_input = QLineEdit(default_log_dir) browse_btn = QPushButton("Browse") + +# Add these lines after them to disable them: + self.log_dir_input.setEnabled(False) + self.log_dir_input.setToolTip("This is now set automatically to a 'logs' folder next to the application.") + browse_btn.clicked.connect(self.select_log_directory) log_dir_layout.addWidget(self.log_dir_input) log_dir_layout.addWidget(browse_btn) @@ -785,7 +790,7 @@ class MainWindow(QMainWindow): top_bar_layout.addSpacerItem(QSpacerItem(20, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum)) # top_bar_layout.addWidget(QLabel("Backup Supply:")) - self.backup_supply_indicator = QLabel("N/A") + self.backup_supply_indicator = QLabel("Backup") self.backup_supply_indicator.setFixedSize(80, 25) self.backup_supply_indicator.setAlignment(Qt.AlignmentFlag.AlignCenter) self.backup_supply_indicator.setStyleSheet( @@ -1133,11 +1138,11 @@ class MainWindow(QMainWindow): click_count = self.swap_button_clicks[chamber_num] if click_count == 1: self.swap_sequence.append(chamber_num) - sender.setStyleSheet("background-color: #27ae60;") + sender.setStyleSheet("background-color: #05abd0;") elif click_count == 2: self.swap_sequence.append(chamber_num) sender.setText(f"{chamber_num}") - sender.setStyleSheet("background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #27ae60, stop:0.5 #27ae60, stop:0.51 #2ecc71, stop:1 #2ecc71);") + sender.setStyleSheet("background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #1f7a9a, stop:0.5 #1f7a9a, stop:0.51 #1f7a9a, stop:1 #1f7a9a);") elif click_count >= 3: self.swap_sequence = [num for num in self.swap_sequence if num != chamber_num] self.swap_button_clicks[chamber_num] = 0 @@ -1286,7 +1291,7 @@ class MainWindow(QMainWindow): self.mqtt_thread.wait() if self.save_logs_checkbox.isChecked(): self.start_csv_logger() - + self.reset_dashboard_ui() broker = self.broker_input.text() user = self.username_input.text() password = self.password_input.text() @@ -1331,11 +1336,23 @@ class MainWindow(QMainWindow): if self.mqtt_client: self.mqtt_client.disconnect_from_broker() def start_csv_logger(self): - base_log_dir = self.log_dir_input.text() + # REMOVED: Your old path finding method is not reliable for packaged apps. + # script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + # default_log_dir = os.path.join(script_dir, "logs") + + # <-- MODIFIED: Use the correct, reliable method to get the app's base directory + app_dir = self.get_base_path() + # print(f"Application base directory: {app_dir}") + + # Create a 'logs' sub-directory for better organization + base_log_dir = os.path.join(app_dir, 'logs') + + # Ensure the log directory exists + os.makedirs(base_log_dir, exist_ok=True) + session_folder_name = f"session_{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}" self.logger_thread = QThread() - # Pass BOTH the base directory and the session name to the logger self.csv_logger = CsvLogger(base_log_dir, session_folder_name) self.csv_logger.moveToThread(self.logger_thread) @@ -1343,6 +1360,16 @@ class MainWindow(QMainWindow): self.logger_thread.started.connect(self.csv_logger.start_logging) self.logger_thread.start() + def get_base_path(self): + """Gets the base path for the application, whether it's a script or a frozen exe.""" + if getattr(sys, 'frozen', False): + # If packaged, the base path is the directory of the executable + base_path = os.path.dirname(sys.executable) + else: + # If run as a normal script, the base path is the script's directory + base_path = os.path.dirname(os.path.abspath(__file__)) + return base_path + def stop_csv_logger(self): if self.logger_thread and self.logger_thread.isRunning(): self.csv_logger.stop_logging() @@ -1391,6 +1418,7 @@ class MainWindow(QMainWindow): 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 "") diff --git a/ui/styles.py b/ui/styles.py index 99fef20..03f0574 100644 --- a/ui/styles.py +++ b/ui/styles.py @@ -111,9 +111,12 @@ def get_light_theme_styles(scale=1.0): font-weight: bold; border-radius: {int(4*scale)}px; }} - #ChamberOpenDoorButton:hover {{ background-color: #607d8b; }} - #ChamberChgOnButton:hover {{ background-color: #52be80; }} - #ChamberChgOffButton:hover {{ background-color: #cd6155; }} + #ChamberOpenDoorButton {{ background-color: #607d8b; }} + #ChamberChgOnButton {{ background-color: #52be80; }} + #ChamberChgOffButton {{ background-color: #cd6155; }} + #ChamberOpenDoorButton:hover {{ background-color: #485c64; }} + #ChamberChgOnButton:hover {{ background-color: #04d45d; }} + #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; }} @@ -331,12 +334,12 @@ def get_dark_theme_styles(scale=1.0): font-weight: bold; border-radius: {int(4*scale)}px; }} - #ChamberOpenDoorButton {{ background-color: #3C3C3C; }} - #ChamberChgOnButton {{ background-color: #3C3C3C; }} - #ChamberChgOffButton {{ background-color: #3C3C3C; }} - #ChamberOpenDoorButton:hover {{ background-color: #607d8b; }} - #ChamberChgOnButton:hover {{ background-color: #52be80; }} - #ChamberChgOffButton:hover {{ background-color: #cd6155; }} + #ChamberOpenDoorButton {{ background-color: #607d8b; }} + #ChamberChgOnButton {{ background-color: #52be80; }} + #ChamberChgOffButton {{ background-color: #cd6155; }} + #ChamberOpenDoorButton:hover {{ background-color: #485c64; }} + #ChamberChgOnButton:hover {{ background-color: #04d45d; }} + #ChamberChgOffButton:hover {{ background-color: #d42318; }} #ConnectButton, #DisconnectButton {{ padding: {int(6 * scale)}px {int(16 * scale)}px; diff --git a/ui/widgets.py b/ui/widgets.py index 12dee41..e3d90df 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -1,13 +1,13 @@ # --- REPLACE the entire content of ui/widgets.py with this --- from PyQt6.QtWidgets import ( - QGroupBox, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QFrame, QPushButton, QFormLayout + QGroupBox, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QFrame, QPushButton, QFormLayout, QSizePolicy ) -from PyQt6.QtCore import Qt, pyqtSignal # <-- IMPORT pyqtSignal +from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFont class ChamberWidget(QGroupBox): - # --- ADD SIGNALS HERE --- + # Signals for button presses open_door_requested = pyqtSignal() chg_on_requested = pyqtSignal() chg_off_requested = pyqtSignal() @@ -20,21 +20,24 @@ class ChamberWidget(QGroupBox): main_layout = QVBoxLayout(self) main_layout.setSpacing(max(2, int(4 * scale))) + # --- Battery ID --- id_layout = QHBoxLayout() id_layout.addWidget(QLabel("BAT ID: ")) self.id_field = self._create_data_field(scale) - self.id_field.setObjectName("BatIdField") # Set object name for styling + self.id_field.setObjectName("BatIdField") id_layout.addWidget(self.id_field) main_layout.addLayout(id_layout) + # --- Main Data Columns --- columns_layout = QHBoxLayout() + + # == Left Column: Battery Info == battery_form_layout = QFormLayout() battery_form_layout.setVerticalSpacing(max(2, int(4 * scale))) self.battery_status_label = self._create_status_label("ABSENT", scale) self.battery_status_label.setProperty("status", "absent") - # --- SET OBJECT NAMES FOR EACH DATA FIELD --- self.soc_field = self._create_data_field(scale) self.soc_field.setObjectName("DataField") @@ -46,7 +49,6 @@ class ChamberWidget(QGroupBox): self.battery_fault_field = self._create_data_field(scale) self.battery_fault_field.setObjectName("DataField") - # --- END OF OBJECT NAMES --- battery_form_layout.addRow("Status:", self.battery_status_label) battery_form_layout.addRow("SOC:", self.soc_field) @@ -54,35 +56,35 @@ class ChamberWidget(QGroupBox): battery_form_layout.addRow("Temp:", self.temp_field) battery_form_layout.addRow("Fault:", self.battery_fault_field) + # == Separator == separator_line = QFrame() separator_line.setFrameShape(QFrame.Shape.VLine) separator_line.setFrameShadow(QFrame.Shadow.Sunken) + # == Right Column: Charger Info == charger_form_layout = QFormLayout() charger_form_layout.setVerticalSpacing(max(2, int(4 * scale))) self.charger_status_label = self._create_status_label("OFF", scale) self.charger_status_label.setProperty("status", "absent") - # --- SET OBJECT NAMES FOR EACH DATA FIELD --- - self.slot_temp_field = self._create_data_field(scale) - self.slot_temp_field.setObjectName("DataField") - self.chg_temp_field = self._create_data_field(scale) self.chg_temp_field.setObjectName("DataField") - self.door_status_field = self._create_data_field(scale) - self.door_status_field.setObjectName("DoorStatusField") - self.charger_fault_field = self._create_data_field(scale) self.charger_fault_field.setObjectName("DataField") - # --- END OF OBJECT NAMES --- + + self.current_field = self._create_data_field(scale) + self.current_field.setObjectName("DataField") + + self.slot_temp_field = self._create_data_field(scale) + self.slot_temp_field.setObjectName("DataField") charger_form_layout.addRow("Chg Status:", self.charger_status_label) charger_form_layout.addRow("Chg Temp:", self.chg_temp_field) - charger_form_layout.addRow("Slot Temp:", self.slot_temp_field) - charger_form_layout.addRow("Door Status:", self.door_status_field) charger_form_layout.addRow("Fault:", self.charger_fault_field) + charger_form_layout.addRow("Current:", self.current_field) + charger_form_layout.addRow("Slot Temp:", self.slot_temp_field) columns_layout.addLayout(battery_form_layout) columns_layout.addWidget(separator_line) @@ -91,7 +93,17 @@ class ChamberWidget(QGroupBox): main_layout.addStretch() + # --- Door status and buttons are on one line --- button_layout = QHBoxLayout() + + # Part 1: Door Status (fixed size on the left) + button_layout.addWidget(QLabel("Door Status:")) + self.door_status_field = self._create_data_field(scale) + self.door_status_field.setObjectName("DoorStatusField") + self.door_status_field.setMaximumWidth(int(140 * scale)) + button_layout.addWidget(self.door_status_field) + + # Part 2: Buttons self.open_door_btn = QPushButton("OPEN DOOR") self.chg_on_btn = QPushButton("CHG ON") self.chg_off_btn = QPushButton("CHG OFF") @@ -107,9 +119,9 @@ class ChamberWidget(QGroupBox): button_layout.addWidget(self.open_door_btn) button_layout.addWidget(self.chg_on_btn) button_layout.addWidget(self.chg_off_btn) + main_layout.addLayout(button_layout) - # ... (the rest of the class is unchanged) ... def _create_status_label(self, text, scale): label = QLabel(text) label.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -117,20 +129,25 @@ class ChamberWidget(QGroupBox): return label def _create_data_field(self, scale): - field = QLineEdit("N/A") + field = QLineEdit(" ") field.setReadOnly(True) field.setFont(QFont("Arial", max(7, int(8 * scale)))) return field def update_data(self, data): - if data.get("batteryPresent") == 1: + # 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))) + + if battery_present_status == 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("chargerPresent") == 1: + if charger_on_status == 1: self.charger_status_label.setText("CHARGER ON") self.charger_status_label.setProperty("status", "present") else: @@ -141,17 +158,70 @@ class ChamberWidget(QGroupBox): widget.style().unpolish(widget) widget.style().polish(widget) - self.id_field.setText(data.get("batteryIdentification", "N/A")) + 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.slot_temp_field.setText(f'{data.get("slotTemperature", 0) / 10.0:.1f} °C') - self.chg_temp_field.setText(f'{data.get("chargerTemp", 0) / 10.0:.1f} °C') + + # <-- 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') + 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;") + 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;") + def reset_to_default(self): """Resets all fields in this chamber widget to their default state.""" self.battery_status_label.setText("ABSENT") @@ -159,17 +229,20 @@ class ChamberWidget(QGroupBox): self.charger_status_label.setText("CHARGER OFF") self.charger_status_label.setProperty("status", "absent") - # Re-apply the stylesheet 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("N/A") - self.soc_field.setText("N/A") - self.voltage_field.setText("N/A") - self.temp_field.setText("N/A") - self.battery_fault_field.setText("N/A") - self.slot_temp_field.setText("N/A") - self.chg_temp_field.setText("N/A") - self.door_status_field.setText("N/A") - self.charger_fault_field.setText("N/A") \ No newline at end of file + self.id_field.setText(" ") + self.soc_field.setText(" ") + self.voltage_field.setText(" ") + self.temp_field.setText(" ") + self.battery_fault_field.setText(" ") + self.chg_temp_field.setText(" ") + self.charger_fault_field.setText(" ") + self.current_field.setText(" ") + self.slot_temp_field.setText(" ") + self.door_status_field.setText(" ") + + # --- ADDED: Reset the color --- + self.door_status_field.setStyleSheet("") \ No newline at end of file