Fix: Improve MQTT connection and error handling
parent
77ff85c541
commit
7284626248
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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; }}
|
||||
|
|
|
|||
105
ui/widgets.py
105
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."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue