Source code for ents.config.user_config

"""
@brief PyQt5 GUI Application for Configuring User Settings

This module provides a PyQt5-based graphical interface for configuring user settings.
The application allows users to input configuration details, including Logger ID, Cell ID,
Upload Method (WiFi or LoRa), Upload Interval, Enabled Sensors, and Calibration parameters
for voltage and current (V/I Slope and Offset).

Key features:

- **Save and Load**: Users can save configurations to a file or load previous configurations for easy reuse.
- **Real-time Configuration**: By pressing the "Send Configuration" button, the settings are serialized with Protobuf
  and transmitted over UART to the STM32 for direct application.

@file user_config.py
@author Ahmed Hassan Falah
@date 2024-10-10
"""

import json
import os
import re  # For validating URL input
import sys

import serial
import serial.tools.list_ports
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QInputDialog

from ..proto import decode_user_configuration, encode_user_configuration


[docs] class Ui_MainWindow:
[docs] def setupUi(self, MainWindow): """ @brief Sets up the user interface components. Initializes the main window and creates the layout for user configurations. """ MainWindow.setObjectName("MainWindow") MainWindow.resize(600, 500) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.layout = QtWidgets.QVBoxLayout(self.centralwidget) # Group boxes self.setupGroupBoxes() # Save and Load Buttons self.setupSaveAndLoadButtons() MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 600, 26)) self.menubar.setObjectName("menubar") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setObjectName("statusbar") MainWindow.setStatusBar(self.statusbar) _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "User Configuration")) QtCore.QMetaObject.connectSlotsByName(MainWindow) # Center the window initially screen = QtWidgets.QDesktopWidget().screenGeometry() screen_width = screen.width() screen_height = screen.height() window_width = MainWindow.width() window_height = MainWindow.height() x = (screen_width - window_width) // 2 y = (screen_height - window_height) // 3 MainWindow.setGeometry(x, y, window_width, window_height)
[docs] def setupGroupBoxes(self): """ @brief Sets up the group boxes for different configuration sections. """ font = QtGui.QFont() font.setPointSize(10) font.setBold(True) # Upload Settings group self.uploadSettingsGroupBox = QtWidgets.QGroupBox( "Upload Settings", self.centralwidget ) self.uploadSettingsLayout = QtWidgets.QGridLayout(self.uploadSettingsGroupBox) self.Logger_ID = self.createLabel("Logger ID", font) self.lineEdit_Logger_ID = self.createLineEdit( "Enter Logger ID (positive integer)" ) self.uploadSettingsLayout.addWidget(self.Logger_ID, 0, 0) self.uploadSettingsLayout.addWidget(self.lineEdit_Logger_ID, 0, 1) self.Cell_ID = self.createLabel("Cell ID", font) self.lineEdit_Cell_ID = self.createLineEdit("Enter Cell ID (positive integer)") self.uploadSettingsLayout.addWidget(self.Cell_ID, 1, 0) self.uploadSettingsLayout.addWidget(self.lineEdit_Cell_ID, 1, 1) self.Upload_Method = self.createLabel("Upload Method", font) self.comboBox_Upload_Method = QtWidgets.QComboBox(self.centralwidget) self.comboBox_Upload_Method.addItems(["WiFi", "LoRa"]) self.comboBox_Upload_Method.setCurrentIndex(1) # Set default to LoRa self.comboBox_Upload_Method.currentIndexChanged.connect(self.toggleUploadMethod) self.uploadSettingsLayout.addWidget(self.Upload_Method, 2, 0) self.uploadSettingsLayout.addWidget(self.comboBox_Upload_Method, 2, 1) self.Upload_Interval = self.createLabel("Upload Interval", font) self.layout_Upload_Interval = QtWidgets.QHBoxLayout() # Upload_Interval: Input fields for Days, Hours, Minutes, Seconds self.lineEdit_Days = self.createLineEdit("0") self.lineEdit_Days.setFixedWidth(40) self.layout_Upload_Interval.addWidget(self.lineEdit_Days) self.label_Days = QtWidgets.QLabel("days") self.layout_Upload_Interval.addWidget(self.label_Days) self.lineEdit_Hours = self.createLineEdit("0") self.lineEdit_Hours.setFixedWidth(40) self.layout_Upload_Interval.addWidget(self.lineEdit_Hours) self.label_Hours = QtWidgets.QLabel("hours") self.layout_Upload_Interval.addWidget(self.label_Hours) self.lineEdit_Minutes = self.createLineEdit("0") self.lineEdit_Minutes.setFixedWidth(40) self.layout_Upload_Interval.addWidget(self.lineEdit_Minutes) self.label_Minutes = QtWidgets.QLabel("minutes") self.layout_Upload_Interval.addWidget(self.label_Minutes) self.lineEdit_Seconds = self.createLineEdit("0") self.lineEdit_Seconds.setFixedWidth(40) self.layout_Upload_Interval.addWidget(self.lineEdit_Seconds) self.label_Seconds = QtWidgets.QLabel("seconds") self.layout_Upload_Interval.addWidget(self.label_Seconds) self.uploadSettingsLayout.addWidget(self.Upload_Interval, 3, 0) self.uploadSettingsLayout.addLayout(self.layout_Upload_Interval, 3, 1) self.layout.addWidget(self.uploadSettingsGroupBox) # Measurement Settings group self.measurementSettingsGroupBox = QtWidgets.QGroupBox( "Measurement Settings", self.centralwidget ) self.measurementSettingsLayout = QtWidgets.QGridLayout( self.measurementSettingsGroupBox ) self.Enabled_Sensors = self.createLabel("Enabled Sensors", font) self.checkBox_Voltage = QtWidgets.QCheckBox("Voltage") self.checkBox_Current = QtWidgets.QCheckBox("Current") self.checkBox_Teros12 = QtWidgets.QCheckBox("Teros12") self.checkBox_Teros21 = QtWidgets.QCheckBox("Teros21") self.checkBox_BME280 = QtWidgets.QCheckBox("BME280") self.measurementSettingsLayout.addWidget(self.Enabled_Sensors, 0, 0) self.measurementSettingsLayout.addWidget(self.checkBox_Voltage, 0, 1) self.measurementSettingsLayout.addWidget(self.checkBox_Current, 1, 1) self.measurementSettingsLayout.addWidget(self.checkBox_Teros12, 2, 1) self.measurementSettingsLayout.addWidget(self.checkBox_Teros21, 3, 1) self.measurementSettingsLayout.addWidget(self.checkBox_BME280, 4, 1) self.Calibration_V_Slope = self.createLabel("Calibration V Slope", font) self.lineEdit_V_Slope = self.createLineEdit( "Enter Voltage Slope (floating-point)" ) self.measurementSettingsLayout.addWidget(self.Calibration_V_Slope, 5, 0) self.measurementSettingsLayout.addWidget(self.lineEdit_V_Slope, 5, 1) self.Calibration_V_Offset = self.createLabel("Calibration V Offset", font) self.lineEdit_V_Offset = self.createLineEdit( "Enter Voltage Offset (floating-point)" ) self.measurementSettingsLayout.addWidget(self.Calibration_V_Offset, 6, 0) self.measurementSettingsLayout.addWidget(self.lineEdit_V_Offset, 6, 1) self.Calibration_I_Slope = self.createLabel("Calibration I Slope", font) self.lineEdit_I_Slope = self.createLineEdit( "Enter Current Slope (floating-point)" ) self.measurementSettingsLayout.addWidget(self.Calibration_I_Slope, 7, 0) self.measurementSettingsLayout.addWidget(self.lineEdit_I_Slope, 7, 1) self.Calibration_I_Offset = self.createLabel("Calibration I Offset", font) self.lineEdit_I_Offset = self.createLineEdit( "Enter Current Offset (floating-point)" ) self.measurementSettingsLayout.addWidget(self.Calibration_I_Offset, 8, 0) self.measurementSettingsLayout.addWidget(self.lineEdit_I_Offset, 8, 1) self.layout.addWidget(self.measurementSettingsGroupBox) # WiFi Settings group (initially hidden) self.wifiGroupBox = QtWidgets.QGroupBox( "WiFi Configuration", self.centralwidget ) self.wifiLayout = QtWidgets.QGridLayout(self.wifiGroupBox) self.WiFi_SSID = self.createLabel("WiFi SSID", font) self.lineEdit_WiFi_SSID = self.createLineEdit("Enter WiFi SSID") self.wifiLayout.addWidget(self.WiFi_SSID, 0, 0) self.wifiLayout.addWidget(self.lineEdit_WiFi_SSID, 0, 1) self.WiFi_Password = self.createLabel("WiFi Password", font) self.lineEdit_WiFi_Password = self.createLineEdit("Enter WiFi Password") self.lineEdit_WiFi_Password.setEchoMode(QtWidgets.QLineEdit.Password) self.wifiLayout.addWidget(self.WiFi_Password, 1, 0) self.wifiLayout.addWidget(self.lineEdit_WiFi_Password, 1, 1) self.API_Endpoint_URL = self.createLabel("API Endpoint URL", font) self.lineEdit_API_Endpoint_URL = self.createLineEdit( "Enter API Endpoint URL (start with http:// or https://)" ) self.wifiLayout.addWidget(self.API_Endpoint_URL, 2, 0) self.wifiLayout.addWidget(self.lineEdit_API_Endpoint_URL, 2, 1) self.API_Port = self.createLabel("API Port", font) self.lineEdit_API_Port = self.createLineEdit("Enter API Port (integer)") self.wifiLayout.addWidget(self.API_Port, 3, 0) self.wifiLayout.addWidget(self.lineEdit_API_Port, 3, 1) # Ensure consistent size for wifiGroupBox self.wifiGroupBox.setFixedHeight(self.wifiGroupBox.minimumSizeHint().height()) self.layout.addWidget(self.wifiGroupBox) # Show or hide WiFi settings based on upload method self.toggleUploadMethod()
[docs] def toggleUploadMethod(self): """ @brief Shows or hides WiFi settings based on the upload method selected. """ if self.comboBox_Upload_Method.currentText() == "WiFi": self.showWiFiSettings() else: self.hideWiFiSettings()
[docs] def showWiFiSettings(self): """ @brief Displays the WiFi configuration settings. """ self.lineEdit_WiFi_SSID.setEnabled(True) self.lineEdit_WiFi_Password.setEnabled(True) self.lineEdit_API_Endpoint_URL.setEnabled(True) self.lineEdit_API_Port.setEnabled(True) self.lineEdit_WiFi_SSID.show() self.lineEdit_WiFi_Password.show() self.lineEdit_API_Endpoint_URL.show() self.lineEdit_API_Port.show() # set default values for API URL & PORT self.lineEdit_API_Endpoint_URL.setText("https://dirtviz.jlab.ucsc.edu/api/") self.lineEdit_API_Port.setText("443")
[docs] def hideWiFiSettings(self): """ @brief Hides the WiFi configuration settings. """ self.lineEdit_WiFi_SSID.setEnabled(False) self.lineEdit_WiFi_Password.setEnabled(False) self.lineEdit_API_Endpoint_URL.setEnabled(False) self.lineEdit_API_Port.setEnabled(False) self.lineEdit_WiFi_SSID.hide() self.lineEdit_WiFi_Password.hide() self.lineEdit_API_Endpoint_URL.hide() self.lineEdit_API_Port.hide()
[docs] def createLabel(self, text, font): """ @brief Creates a label with the specified text and font. @param text Text for the label. @param font Font settings for the label. @return QLabel instance """ label = QtWidgets.QLabel(text) label.setFont(font) return label
[docs] def createLineEdit(self, placeholder): """ @brief Creates a QLineEdit with a placeholder. @param placeholder Placeholder text for the QLineEdit. @return QLineEdit instance """ lineEdit = QtWidgets.QLineEdit(self.centralwidget) lineEdit.setPlaceholderText(placeholder) return lineEdit
[docs] def setupSaveAndLoadButtons(self): """ @brief Creates and configures the save and load buttons. """ # Create a grid layout for precise placement button_layout = QtWidgets.QGridLayout() # Load button self.loadButton = QtWidgets.QPushButton("Load", self.centralwidget) self.loadButton.setFixedSize(100, 30) self.loadButton.clicked.connect(self.loadConfiguration) button_layout.addWidget(self.loadButton, 1, 0) # Row 0, Column 0 # Save button self.saveButton = QtWidgets.QPushButton("Save", self.centralwidget) self.saveButton.setFixedSize(100, 30) self.saveButton.clicked.connect(self.saveConfiguration) button_layout.addWidget(self.saveButton, 1, 1) # Row 0, Column 1 # Send Configuration button self.saveConfigurationButton = QtWidgets.QPushButton( "Send Configuration", self.centralwidget ) self.saveConfigurationButton.setFixedSize(300, 30) self.saveConfigurationButton.clicked.connect( lambda: self.saveConfiguration(flag="send") ) button_layout.addWidget(self.saveConfigurationButton, 1, 2) # Row 0, Column 2 # Load current Configuration button self.loadCurrentConfigButton = QtWidgets.QPushButton( "Load current Configuration", self.centralwidget ) self.loadCurrentConfigButton.setFixedSize(300, 30) self.loadCurrentConfigButton.clicked.connect( lambda: self.loadConfiguration(flag="loadCurrent") ) button_layout.addWidget(self.loadCurrentConfigButton, 0, 2) # Row 1, Column 2 # Add the grid layout to the main layout self.layout.addLayout(button_layout)
[docs] def saveConfiguration(self, flag: str): """ @brief Validates inputs, encodes the configuration, and sends it via UART. """ try: logger_id = self.validateUInt(self.lineEdit_Logger_ID.text(), "Logger ID") cell_id = self.validateUInt(self.lineEdit_Cell_ID.text(), "Cell ID") upload_method = self.comboBox_Upload_Method.currentText() # Calculate upload interval in seconds days = ( 0 if self.lineEdit_Days.text() == "" else self.validateUInt(self.lineEdit_Days.text(), "Days") ) hours = ( 0 if self.lineEdit_Hours.text() == "" else self.validateUInt(self.lineEdit_Hours.text(), "Hours") ) minutes = ( 0 if self.lineEdit_Minutes.text() == "" else self.validateUInt(self.lineEdit_Minutes.text(), "Minutes") ) seconds = ( 0 if self.lineEdit_Seconds.text() == "" else self.validateUInt(self.lineEdit_Seconds.text(), "Seconds") ) upload_interval = days * 86400 + hours * 3600 + minutes * 60 + seconds # Check if the user entered time in upload interval if upload_interval == 0: raise ValueError('You must Enter preferred time in "upload interval".') # Check if the user selected at least one sensor option if not ( self.checkBox_Voltage.isChecked() or self.checkBox_Current.isChecked() or self.checkBox_Teros12.isChecked() or self.checkBox_Teros21.isChecked() or self.checkBox_BME280.isChecked() ): raise ValueError("You must choose at least one sensor.") # Convert enabled sensors into a list and filter out empty strings enabled_sensors = [ sensor for sensor in [ "Voltage" if self.checkBox_Voltage.isChecked() else "", "Current" if self.checkBox_Current.isChecked() else "", "Teros12" if self.checkBox_Teros12.isChecked() else "", "Teros21" if self.checkBox_Teros21.isChecked() else "", "BME280" if self.checkBox_BME280.isChecked() else "", ] if sensor ] # Checked sensors to be saved in json enabled_sensors_json = { "Voltage": self.checkBox_Voltage.isChecked(), "Current": self.checkBox_Current.isChecked(), "Teros12": self.checkBox_Teros12.isChecked(), "Teros21": self.checkBox_Teros21.isChecked(), "BME280": self.checkBox_BME280.isChecked(), } calibration_v_slope = self.validateFloat( self.lineEdit_V_Slope.text(), "Calibration V Slope" ) calibration_v_offset = self.validateFloat( self.lineEdit_V_Offset.text(), "Calibration V Offset" ) calibration_i_slope = self.validateFloat( self.lineEdit_I_Slope.text(), "Calibration I Slope" ) calibration_i_offset = self.validateFloat( self.lineEdit_I_Offset.text(), "Calibration I Offset" ) # Add WiFi settings if WiFi is selected as the upload method if upload_method == "WiFi": wifi_ssid = self.lineEdit_WiFi_SSID.text() wifi_password = self.lineEdit_WiFi_Password.text() api_endpoint_url = self.validateURL( self.lineEdit_API_Endpoint_URL.text(), "API Endpoint URL" ) api_port = self.validateUInt(self.lineEdit_API_Port.text(), "API Port") else: wifi_ssid = "" wifi_password = "" api_endpoint_url = "" api_port = 0 # Validate user input on case of WiFi if upload_method == "WiFi" and not self.lineEdit_WiFi_SSID.text(): raise ValueError("WiFi SSID cannot be empty.") # Construct the configuration dictionary to be saved in json file configuration = { "Logger ID": logger_id, "Cell ID": cell_id, "Upload Method": upload_method, "Days": days, "Hours": hours, "Minutes": minutes, "Seconds": seconds, "Enabled Sensors": enabled_sensors_json, "Calibration V Slope": calibration_v_slope, "Calibration V Offset": calibration_v_offset, "Calibration I Slope": calibration_i_slope, "Calibration I Offset": calibration_i_offset, "WiFi SSID": wifi_ssid, "WiFi Password": wifi_password, "API Endpoint URL": api_endpoint_url, "API Port": api_port, } # Check whether the user wants to send or just to save if flag == "send": # Construct the configuration dictionary to be encoded encoded_data = encode_user_configuration( int(logger_id), int(cell_id), upload_method, int(upload_interval), enabled_sensors, float(calibration_v_slope), float(calibration_v_offset), float(calibration_i_slope), float(calibration_i_offset), wifi_ssid, wifi_password, api_endpoint_url, int(api_port), ) # Send the encoded configuration via UART success = self.sendToUART(encoded_data) if not success: return # Don't save if sending failed print("------------------------------------------") print(encoded_data) print("------------------------------------------") try: # Ensure the 'Load' directory exists load_dir = os.path.join(os.path.dirname(__file__), "Load") os.makedirs(load_dir, exist_ok=True) # Save configuration as JSON config_path = os.path.join(load_dir, f"cell_{cell_id}_.json") with open(config_path, "w") as json_file: json.dump(configuration, json_file, indent=4) QtWidgets.QMessageBox.information( self.centralwidget, "Success", "Configurations saved successfully." ) except Exception as e: # noqa: BLE001 QtWidgets.QMessageBox.critical( self.centralwidget, "Error", f"Failed to save configurations: {e}" ) # Print success message in case of saving the config or saving and sending the config if flag == "send": print( f"Configuration saved and sent to STM32 successfully! Backup JSON file: {'cell_' + str(cell_id) + '_'}" ) else: print( f"Configuration saved successfully! Backup JSON file: {'cell_' + str(cell_id) + '_'}" ) except ValueError as e: # Show error message if validation fails QtWidgets.QMessageBox.critical(self.centralwidget, "Error", str(e))
[docs] def loadConfiguration(self, flag: str): """ @brief Loads configuration from a selected JSON file and fills the input fields. """ # Load current configuration in STM32 and display it on the GUI if flag == "loadCurrent": success, encoded_data = self.receiveFromUART() if not success: return decoded_data = decode_user_configuration(encoded_data) print(decoded_data) # Update GUI elements with decoded data self.lineEdit_Logger_ID.setText(str(decoded_data["loggerId"])) self.lineEdit_Cell_ID.setText(str(decoded_data["cellId"])) self.comboBox_Upload_Method.setCurrentText(decoded_data["UploadMethod"]) # Calculate upload interval and update GUI fields upload_interval = decoded_data["UploadInterval"] days = upload_interval // 86400 remaining_seconds = upload_interval % 86400 hours = remaining_seconds // 3600 remaining_seconds %= 3600 minutes = remaining_seconds // 60 seconds = remaining_seconds % 60 self.lineEdit_Days.setText(str(days)) self.lineEdit_Hours.setText(str(hours)) self.lineEdit_Minutes.setText(str(minutes)) self.lineEdit_Seconds.setText(str(seconds)) # Update sensor checkboxes self.checkBox_Voltage.setChecked(False) self.checkBox_Current.setChecked(False) self.checkBox_Teros12.setChecked(False) self.checkBox_Teros21.setChecked(False) self.checkBox_BME280.setChecked(False) for sensor in decoded_data["enabledSensors"]: if sensor == "Voltage": self.checkBox_Voltage.setChecked(True) elif sensor == "Current": self.checkBox_Current.setChecked(True) elif sensor == "Teros12": self.checkBox_Teros12.setChecked(True) elif sensor == "Teros21": self.checkBox_Teros21.setChecked(True) elif sensor == "BME280": self.checkBox_BME280.setChecked(True) # Fill calibration fields self.lineEdit_V_Slope.setText(str(decoded_data["VoltageSlope"])) self.lineEdit_V_Offset.setText(str(decoded_data["VoltageOffset"])) self.lineEdit_I_Slope.setText(str(decoded_data["CurrentSlope"])) self.lineEdit_I_Offset.setText(str(decoded_data["CurrentOffset"])) # Fill WiFi settings if upload method is WiFi if decoded_data["UploadMethod"] == "WiFi": self.lineEdit_WiFi_SSID.setText(decoded_data["WiFiSSID"]) self.lineEdit_WiFi_Password.setText(decoded_data["WiFiPassword"]) self.lineEdit_API_Endpoint_URL.setText(decoded_data["APIEndpointURL"]) self.lineEdit_API_Port.setText(str(decoded_data["APIEndpointPort"])) else: # Clear WiFi fields if upload method is not WiFi self.lineEdit_WiFi_SSID.clear() self.lineEdit_WiFi_Password.clear() self.lineEdit_API_Endpoint_URL.clear() self.lineEdit_API_Port.clear() QtWidgets.QMessageBox.information( self.centralwidget, "Success", "Configuration loaded successfully From FRAM.", ) return # Load configuration from JSON file else: # Open a file dialog to select the JSON file options = QtWidgets.QFileDialog.Options() file_path, _ = QtWidgets.QFileDialog.getOpenFileName( self.centralwidget, "Select Configuration File", "", "JSON Files (*.json)", options=options, ) if file_path: try: with open(file_path, "r") as json_file: config = json.load(json_file) # Fill the GUI fields with the loaded configuration self.lineEdit_Logger_ID.setText(str(config.get("Logger ID", ""))) self.lineEdit_Cell_ID.setText(str(config.get("Cell ID", ""))) self.comboBox_Upload_Method.setCurrentText( config.get("Upload Method", "WiFi") ) # Fill the upload interval fields self.lineEdit_Days.setText(str(config.get("Days", 0))) self.lineEdit_Hours.setText(str(config.get("Hours", 0))) self.lineEdit_Minutes.setText(str(config.get("Minutes", 0))) self.lineEdit_Seconds.setText(str(config.get("Seconds", 0))) # Fill sensor checkboxes enabled_sensors = config.get("Enabled Sensors", {}) self.checkBox_Voltage.setChecked(enabled_sensors.get("Voltage", False)) self.checkBox_Current.setChecked(enabled_sensors.get("Current", False)) self.checkBox_Teros12.setChecked(enabled_sensors.get("Teros12", False)) self.checkBox_Teros21.setChecked(enabled_sensors.get("Teros21", False)) self.checkBox_BME280.setChecked(enabled_sensors.get("BME280", False)) # Fill calibration fields self.lineEdit_V_Slope.setText( str(config.get("Calibration V Slope", "")) ) self.lineEdit_V_Offset.setText( str(config.get("Calibration V Offset", "")) ) self.lineEdit_I_Slope.setText( str(config.get("Calibration I Slope", "")) ) self.lineEdit_I_Offset.setText( str(config.get("Calibration I Offset", "")) ) # Fill WiFi settings if upload method is WiFi if config.get("Upload Method") == "WiFi": self.lineEdit_WiFi_SSID.setText(config.get("WiFi SSID", "")) self.lineEdit_WiFi_Password.setText(config.get("WiFi Password", "")) self.lineEdit_API_Endpoint_URL.setText( config.get("API Endpoint URL", "") ) self.lineEdit_API_Port.setText(str(config.get("API Port", ""))) else: # Clear the WiFi fields if the method is not WiFi self.lineEdit_WiFi_SSID.clear() self.lineEdit_WiFi_Password.clear() self.lineEdit_API_Endpoint_URL.clear() self.lineEdit_API_Port.clear() QtWidgets.QMessageBox.information( self.centralwidget, "Success", "Configuration loaded successfully." ) except Exception as e: # noqa: BLE001 QtWidgets.QMessageBox.critical( self.centralwidget, "Error", f"Failed to load configuration: {e}" )
[docs] def sendToUART(self, encoded_data): """ @brief Sends the encoded configuration data via UART. @param data Encoded configuration data. """ ser = None try: # List available ports with descriptions ports = serial.tools.list_ports.comports() available_ports = [f"{port.device} - {port.description}" for port in ports] if not available_ports: QtWidgets.QMessageBox.critical( self.centralwidget, "Error", "No serial ports available." ) return False # Ask the user to select a port selected_port, ok = QInputDialog.getItem( self.centralwidget, "Select Port", "Available Serial Ports:", available_ports, 0, False, ) if not ok or not selected_port: QtWidgets.QMessageBox.critical( self.centralwidget, "Error", "No port selected." ) return False # Extract the port name port_name = selected_port.split(" ")[0] # Open the serial port ser = serial.Serial(port=port_name, baudrate=115200, timeout=20) # Step 0: Send 1 indicating sending new config to be stored ser.flush() ser.write(bytes([1])) print(f"Sending: {bytes([1])}") # Step 1: Send the length of the encoded data (2 bytes) data_length = len(encoded_data) ser.write( data_length.to_bytes(2, byteorder="big") ) # Send length as 2-byte big-endian integer # Step 2: Send the encoded data ser.write(encoded_data) print( "________________________________________________________________________" ) print(f"length: {data_length}") print(f"{encoded_data}") # Step 3: Wait for acknowledgment ("ACK") ack = ser.read(3) # Read 3 bytes (assuming "ACK" is 3 bytes) print(ack) print( "________________________________________________________________________" ) if ack == b"ACK": QtWidgets.QMessageBox.information( self.centralwidget, "Success", "Received ACK from STM32" ) else: QtWidgets.QMessageBox.critical( self.centralwidget, "UART Error", "No acknowledgment received" ) return False # Step 4: After ACK, read back the same data from STM32 received_data_length = int.from_bytes( ser.read(2), byteorder="big" ) # Read the length of received data print( "________________________________________________________________________" ) print(f"length: {received_data_length}") print( "________________________________________________________________________" ) print( "________________________________________________________________________" ) received_data = ser.read( received_data_length ) # Read the received data based on the length print(f"Received from STM32: {received_data}") print( "________________________________________________________________________" ) # Step 5: Display the received data to confirm it's the same if received_data == encoded_data: QtWidgets.QMessageBox.information( self.centralwidget, "Success", "Data received matches the sent data.", ) else: QtWidgets.QMessageBox.critical( self.centralwidget, "Error", "Received data does not match sent data.", ) return True except serial.SerialException as e: QtWidgets.QMessageBox.critical( self.centralwidget, "UART Error", f"Failed to send data: {e}" ) return False finally: if ser is not None: ser.close()
[docs] def receiveFromUART(self): """ @brief Receives the current encoded configuration data via UART. @param void. @return (success, data): A tuple containing success status and decoded data or an error message. """ ser = None try: # List available ports with descriptions ports = serial.tools.list_ports.comports() available_ports = [f"{port.device} - {port.description}" for port in ports] if not available_ports: QtWidgets.QMessageBox.critical( self.centralwidget, "Error", "No serial ports available." ) return False, "No serial ports available." # Ask the user to select a port selected_port, ok = QInputDialog.getItem( self.centralwidget, "Select Port", "Available Serial Ports:", available_ports, 0, False, ) if not ok or not selected_port: QtWidgets.QMessageBox.critical( self.centralwidget, "Error", "No port selected." ) return False, "No port selected." # Extract the port name port_name = selected_port.split(" ")[0] # Open the serial port ser = serial.Serial(port=port_name, baudrate=115200, timeout=2) # Step 0: Send 2 indicating loading the current configurations from the FRAM ser.write(bytes([2])) print(f"Sending: {bytes([2])}") # Step 1: Wait for acknowledgment ("ACK") ack = ser.read(3) # Read 3 bytes (assuming "ACK" is 3 bytes) print(ack) if ack == b"ACK": QtWidgets.QMessageBox.information( self.centralwidget, "Success", "Received ACK from STM32" ) else: QtWidgets.QMessageBox.critical( self.centralwidget, "UART Error", "No acknowledgment received" ) return False, "No acknowledgment received from STM32." # Step 2: After ACK, read data from STM32 received_data_length = int.from_bytes( ser.read(2), byteorder="big" ) # Read the length of received data print( "________________________________________________________________________" ) print(f"length: {received_data_length}") print( "________________________________________________________________________" ) print( "________________________________________________________________________" ) received_data = ser.read( received_data_length ) # Read the received data based on the length print(f"Received from STM32: {received_data}") print( "________________________________________________________________________" ) return True, received_data except serial.SerialException as e: QtWidgets.QMessageBox.critical( self.centralwidget, "UART Error", f"Failed to send data: {e}" ) return False, f"Failed to send or receive data: {e}" finally: if ser is not None: ser.close()
[docs] def validateURL(self, value, field_name): """ @brief Validates that the input is a valid URL. @param value The input value to validate. @param field_name The name of the field being validated. @return The validated URL string. @throws ValueError if the input is invalid. """ url_pattern = re.compile( r"^(https?|ftp):\/\/" # http:// or https:// or ftp:// r"([a-zA-Z0-9_-]+(?:(?:\.[a-zA-Z0-9_-]+)+))" # domain r"(:[0-9]{1,5})?" # port (?: optional) r"(\/.*)?$" # path (?: optional) ) if not url_pattern.match(value): raise ValueError(f"Invalid {field_name}. Must be a valid URL.") return value
[docs] def validateUInt(self, value, name): """ @brief Validates that the input is a positive integer. @param value The input value to validate. @param name The name of the parameter (for error messages). @return Validated unsigned integer value. """ if not value.isdigit() or int(value) < 0: raise ValueError(f"{name} must be a positive integer.") return int(value)
[docs] def validateInt(self, value, name): """ @brief Validates that the input is an integer. @param value The input value to validate. @param name The name of the parameter (for error messages). @return Validated integer value. """ try: return int(value) except ValueError: raise ValueError(f"{name} must be an integer.")
[docs] def validateFloat(self, value, name): """ @brief Validates that the input is a float. @param value The input value to validate. @param name The name of the parameter (for error messages). @return Validated float value. """ try: return float(value) except ValueError: raise ValueError(f"{name} must be a floating-point number.")
[docs] def main(): app = QtWidgets.QApplication(sys.argv) MainWindow = QtWidgets.QMainWindow() ui = Ui_MainWindow() ui.setupUi(MainWindow) MainWindow.show() sys.exit(app.exec_())
if __name__ == "__main__": main()