Fix: Improve MQTT connection and error handling

main
Kirubakaran 2025-08-27 03:53:05 +05:30
parent 77ff85c541
commit 7284626248
4 changed files with 330 additions and 173 deletions

View File

@ -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()

View File

@ -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):

View File

@ -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; }}

View File

@ -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."""