ents 2.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,935 @@
1
+ """
2
+ @brief PyQt5 GUI Application for Configuring User Settings
3
+
4
+ This module provides a PyQt5-based graphical interface for configuring user settings.
5
+ The application allows users to input configuration details, including Logger ID, Cell ID,
6
+ Upload Method (WiFi or LoRa), Upload Interval, Enabled Sensors, and Calibration parameters
7
+ for voltage and current (V/I Slope and Offset).
8
+
9
+ Key features:
10
+ - **Save and Load**: Users can save configurations to a file or load previous configurations for easy reuse.
11
+ - **Real-time Configuration**: By pressing the "Send Configuration" button, the settings are serialized with Protobuf
12
+ and transmitted over UART to the STM32 for direct application.
13
+
14
+ @file user_config.py
15
+ @author Ahmed Hassan Falah
16
+ @date 2024-10-10
17
+ """
18
+
19
+ from PyQt5 import QtCore, QtGui, QtWidgets
20
+ from PyQt5.QtWidgets import QInputDialog
21
+ import json
22
+ import os
23
+ import sys
24
+ import serial
25
+ import serial.tools.list_ports
26
+ import re # For validating URL input
27
+ from ents.proto import (
28
+ encode_user_configuration,
29
+ decode_user_configuration,
30
+ )
31
+
32
+
33
+ class Ui_MainWindow(object):
34
+
35
+ def setupUi(self, MainWindow):
36
+ """
37
+ @brief Sets up the user interface components.
38
+
39
+ Initializes the main window and creates the layout for user configurations.
40
+ """
41
+ MainWindow.setObjectName("MainWindow")
42
+ MainWindow.resize(600, 500)
43
+ self.centralwidget = QtWidgets.QWidget(MainWindow)
44
+ self.centralwidget.setObjectName("centralwidget")
45
+
46
+ self.layout = QtWidgets.QVBoxLayout(self.centralwidget)
47
+
48
+ # Group boxes
49
+ self.setupGroupBoxes()
50
+ # Save and Load Buttons
51
+ self.setupSaveAndLoadButtons()
52
+
53
+ MainWindow.setCentralWidget(self.centralwidget)
54
+ self.menubar = QtWidgets.QMenuBar(MainWindow)
55
+ self.menubar.setGeometry(QtCore.QRect(0, 0, 600, 26))
56
+ self.menubar.setObjectName("menubar")
57
+ MainWindow.setMenuBar(self.menubar)
58
+ self.statusbar = QtWidgets.QStatusBar(MainWindow)
59
+ self.statusbar.setObjectName("statusbar")
60
+ MainWindow.setStatusBar(self.statusbar)
61
+ _translate = QtCore.QCoreApplication.translate
62
+ MainWindow.setWindowTitle(_translate("MainWindow", "User Configuration"))
63
+ QtCore.QMetaObject.connectSlotsByName(MainWindow)
64
+
65
+ # Center the window initially
66
+ screen = QtWidgets.QDesktopWidget().screenGeometry()
67
+ screen_width = screen.width()
68
+ screen_height = screen.height()
69
+ window_width = MainWindow.width()
70
+ window_height = MainWindow.height()
71
+ x = (screen_width - window_width) // 2
72
+ y = (screen_height - window_height) // 3
73
+ MainWindow.setGeometry(x, y, window_width, window_height)
74
+
75
+ def setupGroupBoxes(self):
76
+ """
77
+ @brief Sets up the group boxes for different configuration sections.
78
+ """
79
+ font = QtGui.QFont()
80
+ font.setPointSize(10)
81
+ font.setBold(True)
82
+
83
+ # Upload Settings group
84
+ self.uploadSettingsGroupBox = QtWidgets.QGroupBox(
85
+ "Upload Settings", self.centralwidget
86
+ )
87
+ self.uploadSettingsLayout = QtWidgets.QGridLayout(self.uploadSettingsGroupBox)
88
+
89
+ self.Logger_ID = self.createLabel("Logger ID", font)
90
+ self.lineEdit_Logger_ID = self.createLineEdit(
91
+ "Enter Logger ID (positive integer)"
92
+ )
93
+ self.uploadSettingsLayout.addWidget(self.Logger_ID, 0, 0)
94
+ self.uploadSettingsLayout.addWidget(self.lineEdit_Logger_ID, 0, 1)
95
+
96
+ self.Cell_ID = self.createLabel("Cell ID", font)
97
+ self.lineEdit_Cell_ID = self.createLineEdit("Enter Cell ID (positive integer)")
98
+ self.uploadSettingsLayout.addWidget(self.Cell_ID, 1, 0)
99
+ self.uploadSettingsLayout.addWidget(self.lineEdit_Cell_ID, 1, 1)
100
+
101
+ self.Upload_Method = self.createLabel("Upload Method", font)
102
+ self.comboBox_Upload_Method = QtWidgets.QComboBox(self.centralwidget)
103
+ self.comboBox_Upload_Method.addItems(["WiFi", "LoRa"])
104
+ self.comboBox_Upload_Method.setCurrentIndex(1) # Set default to LoRa
105
+ self.comboBox_Upload_Method.currentIndexChanged.connect(self.toggleUploadMethod)
106
+ self.uploadSettingsLayout.addWidget(self.Upload_Method, 2, 0)
107
+ self.uploadSettingsLayout.addWidget(self.comboBox_Upload_Method, 2, 1)
108
+
109
+ self.Upload_Interval = self.createLabel("Upload Interval", font)
110
+ self.layout_Upload_Interval = QtWidgets.QHBoxLayout()
111
+
112
+ # Upload_Interval: Input fields for Days, Hours, Minutes, Seconds
113
+ self.lineEdit_Days = self.createLineEdit("0")
114
+ self.lineEdit_Days.setFixedWidth(40)
115
+ self.layout_Upload_Interval.addWidget(self.lineEdit_Days)
116
+ self.label_Days = QtWidgets.QLabel("days")
117
+ self.layout_Upload_Interval.addWidget(self.label_Days)
118
+
119
+ self.lineEdit_Hours = self.createLineEdit("0")
120
+ self.lineEdit_Hours.setFixedWidth(40)
121
+ self.layout_Upload_Interval.addWidget(self.lineEdit_Hours)
122
+ self.label_Hours = QtWidgets.QLabel("hours")
123
+ self.layout_Upload_Interval.addWidget(self.label_Hours)
124
+
125
+ self.lineEdit_Minutes = self.createLineEdit("0")
126
+ self.lineEdit_Minutes.setFixedWidth(40)
127
+ self.layout_Upload_Interval.addWidget(self.lineEdit_Minutes)
128
+ self.label_Minutes = QtWidgets.QLabel("minutes")
129
+ self.layout_Upload_Interval.addWidget(self.label_Minutes)
130
+
131
+ self.lineEdit_Seconds = self.createLineEdit("0")
132
+ self.lineEdit_Seconds.setFixedWidth(40)
133
+ self.layout_Upload_Interval.addWidget(self.lineEdit_Seconds)
134
+ self.label_Seconds = QtWidgets.QLabel("seconds")
135
+ self.layout_Upload_Interval.addWidget(self.label_Seconds)
136
+
137
+ self.uploadSettingsLayout.addWidget(self.Upload_Interval, 3, 0)
138
+ self.uploadSettingsLayout.addLayout(self.layout_Upload_Interval, 3, 1)
139
+
140
+ self.layout.addWidget(self.uploadSettingsGroupBox)
141
+
142
+ # Measurement Settings group
143
+ self.measurementSettingsGroupBox = QtWidgets.QGroupBox(
144
+ "Measurement Settings", self.centralwidget
145
+ )
146
+ self.measurementSettingsLayout = QtWidgets.QGridLayout(
147
+ self.measurementSettingsGroupBox
148
+ )
149
+
150
+ self.Enabled_Sensors = self.createLabel("Enabled Sensors", font)
151
+ self.checkBox_Voltage = QtWidgets.QCheckBox("Voltage")
152
+ self.checkBox_Current = QtWidgets.QCheckBox("Current")
153
+ self.checkBox_Teros12 = QtWidgets.QCheckBox("Teros12")
154
+ self.checkBox_Teros21 = QtWidgets.QCheckBox("Teros21")
155
+ self.checkBox_BME280 = QtWidgets.QCheckBox("BME280")
156
+
157
+ self.measurementSettingsLayout.addWidget(self.Enabled_Sensors, 0, 0)
158
+ self.measurementSettingsLayout.addWidget(self.checkBox_Voltage, 0, 1)
159
+ self.measurementSettingsLayout.addWidget(self.checkBox_Current, 1, 1)
160
+ self.measurementSettingsLayout.addWidget(self.checkBox_Teros12, 2, 1)
161
+ self.measurementSettingsLayout.addWidget(self.checkBox_Teros21, 3, 1)
162
+ self.measurementSettingsLayout.addWidget(self.checkBox_BME280, 4, 1)
163
+
164
+ self.Calibration_V_Slope = self.createLabel("Calibration V Slope", font)
165
+ self.lineEdit_V_Slope = self.createLineEdit(
166
+ "Enter Voltage Slope (floating-point)"
167
+ )
168
+ self.measurementSettingsLayout.addWidget(self.Calibration_V_Slope, 5, 0)
169
+ self.measurementSettingsLayout.addWidget(self.lineEdit_V_Slope, 5, 1)
170
+
171
+ self.Calibration_V_Offset = self.createLabel("Calibration V Offset", font)
172
+ self.lineEdit_V_Offset = self.createLineEdit(
173
+ "Enter Voltage Offset (floating-point)"
174
+ )
175
+ self.measurementSettingsLayout.addWidget(self.Calibration_V_Offset, 6, 0)
176
+ self.measurementSettingsLayout.addWidget(self.lineEdit_V_Offset, 6, 1)
177
+
178
+ self.Calibration_I_Slope = self.createLabel("Calibration I Slope", font)
179
+ self.lineEdit_I_Slope = self.createLineEdit(
180
+ "Enter Current Slope (floating-point)"
181
+ )
182
+ self.measurementSettingsLayout.addWidget(self.Calibration_I_Slope, 7, 0)
183
+ self.measurementSettingsLayout.addWidget(self.lineEdit_I_Slope, 7, 1)
184
+
185
+ self.Calibration_I_Offset = self.createLabel("Calibration I Offset", font)
186
+ self.lineEdit_I_Offset = self.createLineEdit(
187
+ "Enter Current Offset (floating-point)"
188
+ )
189
+ self.measurementSettingsLayout.addWidget(self.Calibration_I_Offset, 8, 0)
190
+ self.measurementSettingsLayout.addWidget(self.lineEdit_I_Offset, 8, 1)
191
+
192
+ self.layout.addWidget(self.measurementSettingsGroupBox)
193
+
194
+ # WiFi Settings group (initially hidden)
195
+ self.wifiGroupBox = QtWidgets.QGroupBox(
196
+ "WiFi Configuration", self.centralwidget
197
+ )
198
+ self.wifiLayout = QtWidgets.QGridLayout(self.wifiGroupBox)
199
+
200
+ self.WiFi_SSID = self.createLabel("WiFi SSID", font)
201
+ self.lineEdit_WiFi_SSID = self.createLineEdit("Enter WiFi SSID")
202
+ self.wifiLayout.addWidget(self.WiFi_SSID, 0, 0)
203
+ self.wifiLayout.addWidget(self.lineEdit_WiFi_SSID, 0, 1)
204
+
205
+ self.WiFi_Password = self.createLabel("WiFi Password", font)
206
+ self.lineEdit_WiFi_Password = self.createLineEdit("Enter WiFi Password")
207
+ self.lineEdit_WiFi_Password.setEchoMode(QtWidgets.QLineEdit.Password)
208
+ self.wifiLayout.addWidget(self.WiFi_Password, 1, 0)
209
+ self.wifiLayout.addWidget(self.lineEdit_WiFi_Password, 1, 1)
210
+
211
+ self.API_Endpoint_URL = self.createLabel("API Endpoint URL", font)
212
+ self.lineEdit_API_Endpoint_URL = self.createLineEdit(
213
+ "Enter API Endpoint URL (start with http:// or https://)"
214
+ )
215
+ self.wifiLayout.addWidget(self.API_Endpoint_URL, 2, 0)
216
+ self.wifiLayout.addWidget(self.lineEdit_API_Endpoint_URL, 2, 1)
217
+
218
+ self.API_Port = self.createLabel("API Port", font)
219
+ self.lineEdit_API_Port = self.createLineEdit("Enter API Port (integer)")
220
+ self.wifiLayout.addWidget(self.API_Port, 3, 0)
221
+ self.wifiLayout.addWidget(self.lineEdit_API_Port, 3, 1)
222
+
223
+ # Ensure consistent size for wifiGroupBox
224
+ self.wifiGroupBox.setFixedHeight(self.wifiGroupBox.minimumSizeHint().height())
225
+ self.layout.addWidget(self.wifiGroupBox)
226
+
227
+ # Show or hide WiFi settings based on upload method
228
+ self.toggleUploadMethod()
229
+
230
+ def toggleUploadMethod(self):
231
+ """
232
+ @brief Shows or hides WiFi settings based on the upload method selected.
233
+ """
234
+ if self.comboBox_Upload_Method.currentText() == "WiFi":
235
+ self.showWiFiSettings()
236
+ else:
237
+ self.hideWiFiSettings()
238
+
239
+ def showWiFiSettings(self):
240
+ """
241
+ @brief Displays the WiFi configuration settings.
242
+ """
243
+ self.lineEdit_WiFi_SSID.setEnabled(True)
244
+ self.lineEdit_WiFi_Password.setEnabled(True)
245
+ self.lineEdit_API_Endpoint_URL.setEnabled(True)
246
+ self.lineEdit_API_Port.setEnabled(True)
247
+ self.lineEdit_WiFi_SSID.show()
248
+ self.lineEdit_WiFi_Password.show()
249
+ self.lineEdit_API_Endpoint_URL.show()
250
+ self.lineEdit_API_Port.show()
251
+ # set default values for API URL & PORT
252
+ self.lineEdit_API_Endpoint_URL.setText("https://dirtviz.jlab.ucsc.edu/api/")
253
+ self.lineEdit_API_Port.setText("443")
254
+
255
+ def hideWiFiSettings(self):
256
+ """
257
+ @brief Hides the WiFi configuration settings.
258
+ """
259
+ self.lineEdit_WiFi_SSID.setEnabled(False)
260
+ self.lineEdit_WiFi_Password.setEnabled(False)
261
+ self.lineEdit_API_Endpoint_URL.setEnabled(False)
262
+ self.lineEdit_API_Port.setEnabled(False)
263
+ self.lineEdit_WiFi_SSID.hide()
264
+ self.lineEdit_WiFi_Password.hide()
265
+ self.lineEdit_API_Endpoint_URL.hide()
266
+ self.lineEdit_API_Port.hide()
267
+
268
+ def createLabel(self, text, font):
269
+ """
270
+ @brief Creates a label with the specified text and font.
271
+
272
+ @param text Text for the label.
273
+ @param font Font settings for the label.
274
+ @return QLabel instance
275
+ """
276
+ label = QtWidgets.QLabel(text)
277
+ label.setFont(font)
278
+ return label
279
+
280
+ def createLineEdit(self, placeholder):
281
+ """
282
+ @brief Creates a QLineEdit with a placeholder.
283
+
284
+ @param placeholder Placeholder text for the QLineEdit.
285
+ @return QLineEdit instance
286
+ """
287
+ lineEdit = QtWidgets.QLineEdit(self.centralwidget)
288
+ lineEdit.setPlaceholderText(placeholder)
289
+ return lineEdit
290
+
291
+ def setupSaveAndLoadButtons(self):
292
+ """
293
+ @brief Creates and configures the save and load buttons.
294
+ """
295
+ # Create a grid layout for precise placement
296
+ button_layout = QtWidgets.QGridLayout()
297
+
298
+ # Load button
299
+ self.loadButton = QtWidgets.QPushButton("Load", self.centralwidget)
300
+ self.loadButton.setFixedSize(100, 30)
301
+ self.loadButton.clicked.connect(self.loadConfiguration)
302
+ button_layout.addWidget(self.loadButton, 1, 0) # Row 0, Column 0
303
+
304
+ # Save button
305
+ self.saveButton = QtWidgets.QPushButton("Save", self.centralwidget)
306
+ self.saveButton.setFixedSize(100, 30)
307
+ self.saveButton.clicked.connect(self.saveConfiguration)
308
+ button_layout.addWidget(self.saveButton, 1, 1) # Row 0, Column 1
309
+
310
+ # Send Configuration button
311
+ self.saveConfigurationButton = QtWidgets.QPushButton(
312
+ "Send Configuration", self.centralwidget
313
+ )
314
+ self.saveConfigurationButton.setFixedSize(300, 30)
315
+ self.saveConfigurationButton.clicked.connect(
316
+ lambda: self.saveConfiguration(flag="send")
317
+ )
318
+ button_layout.addWidget(self.saveConfigurationButton, 1, 2) # Row 0, Column 2
319
+
320
+ # Load current Configuration button
321
+ self.loadCurrentConfigButton = QtWidgets.QPushButton(
322
+ "Load current Configuration", self.centralwidget
323
+ )
324
+ self.loadCurrentConfigButton.setFixedSize(300, 30)
325
+ self.loadCurrentConfigButton.clicked.connect(
326
+ lambda: self.loadConfiguration(flag="loadCurrent")
327
+ )
328
+ button_layout.addWidget(self.loadCurrentConfigButton, 0, 2) # Row 1, Column 2
329
+
330
+ # Add the grid layout to the main layout
331
+ self.layout.addLayout(button_layout)
332
+
333
+ def saveConfiguration(self, flag: str):
334
+ """
335
+ @brief Validates inputs, encodes the configuration, and sends it via UART.
336
+ """
337
+ try:
338
+ logger_id = self.validateUInt(self.lineEdit_Logger_ID.text(), "Logger ID")
339
+ cell_id = self.validateUInt(self.lineEdit_Cell_ID.text(), "Cell ID")
340
+ upload_method = self.comboBox_Upload_Method.currentText()
341
+
342
+ # Calculate upload interval in seconds
343
+ days = (
344
+ 0
345
+ if self.lineEdit_Days.text() == ""
346
+ else self.validateUInt(self.lineEdit_Days.text(), "Days")
347
+ )
348
+ hours = (
349
+ 0
350
+ if self.lineEdit_Hours.text() == ""
351
+ else self.validateUInt(self.lineEdit_Hours.text(), "Hours")
352
+ )
353
+ minutes = (
354
+ 0
355
+ if self.lineEdit_Minutes.text() == ""
356
+ else self.validateUInt(self.lineEdit_Minutes.text(), "Minutes")
357
+ )
358
+ seconds = (
359
+ 0
360
+ if self.lineEdit_Seconds.text() == ""
361
+ else self.validateUInt(self.lineEdit_Seconds.text(), "Seconds")
362
+ )
363
+ upload_interval = days * 86400 + hours * 3600 + minutes * 60 + seconds
364
+
365
+ # Check if the user entered time in upload interval
366
+ if upload_interval == 0:
367
+ raise ValueError('You must Enter preferred time in "upload interval".')
368
+
369
+ # Check if the user selected at least one sensor option
370
+ if not (
371
+ self.checkBox_Voltage.isChecked()
372
+ or self.checkBox_Current.isChecked()
373
+ or self.checkBox_Teros12.isChecked()
374
+ or self.checkBox_Teros21.isChecked()
375
+ or self.checkBox_BME280.isChecked()
376
+ ):
377
+ raise ValueError("You must choose at least one sensor.")
378
+
379
+ # Convert enabled sensors into a list and filter out empty strings
380
+ enabled_sensors = [
381
+ sensor
382
+ for sensor in [
383
+ "Voltage" if self.checkBox_Voltage.isChecked() else "",
384
+ "Current" if self.checkBox_Current.isChecked() else "",
385
+ "Teros12" if self.checkBox_Teros12.isChecked() else "",
386
+ "Teros21" if self.checkBox_Teros21.isChecked() else "",
387
+ "BME280" if self.checkBox_BME280.isChecked() else "",
388
+ ]
389
+ if sensor
390
+ ]
391
+
392
+ # Checked sensors to be saved in json
393
+ enabled_sensors_json = {
394
+ "Voltage": self.checkBox_Voltage.isChecked(),
395
+ "Current": self.checkBox_Current.isChecked(),
396
+ "Teros12": self.checkBox_Teros12.isChecked(),
397
+ "Teros21": self.checkBox_Teros21.isChecked(),
398
+ "BME280": self.checkBox_BME280.isChecked(),
399
+ }
400
+ calibration_v_slope = self.validateFloat(
401
+ self.lineEdit_V_Slope.text(), "Calibration V Slope"
402
+ )
403
+ calibration_v_offset = self.validateFloat(
404
+ self.lineEdit_V_Offset.text(), "Calibration V Offset"
405
+ )
406
+ calibration_i_slope = self.validateFloat(
407
+ self.lineEdit_I_Slope.text(), "Calibration I Slope"
408
+ )
409
+ calibration_i_offset = self.validateFloat(
410
+ self.lineEdit_I_Offset.text(), "Calibration I Offset"
411
+ )
412
+
413
+ # Add WiFi settings if WiFi is selected as the upload method
414
+ if upload_method == "WiFi":
415
+ wifi_ssid = self.lineEdit_WiFi_SSID.text()
416
+ wifi_password = self.lineEdit_WiFi_Password.text()
417
+ api_endpoint_url = self.validateURL(
418
+ self.lineEdit_API_Endpoint_URL.text(), "API Endpoint URL"
419
+ )
420
+ api_port = self.validateUInt(self.lineEdit_API_Port.text(), "API Port")
421
+ else:
422
+ wifi_ssid = ""
423
+ wifi_password = ""
424
+ api_endpoint_url = ""
425
+ api_port = 0
426
+
427
+ # Validate user input on case of WiFi
428
+ if upload_method == "WiFi":
429
+ if not self.lineEdit_WiFi_SSID.text():
430
+ raise ValueError("WiFi SSID cannot be empty.")
431
+
432
+ # Construct the configuration dictionary to be saved in json file
433
+ configuration = {
434
+ "Logger ID": logger_id,
435
+ "Cell ID": cell_id,
436
+ "Upload Method": upload_method,
437
+ "Days": days,
438
+ "Hours": hours,
439
+ "Minutes": minutes,
440
+ "Seconds": seconds,
441
+ "Enabled Sensors": enabled_sensors_json,
442
+ "Calibration V Slope": calibration_v_slope,
443
+ "Calibration V Offset": calibration_v_offset,
444
+ "Calibration I Slope": calibration_i_slope,
445
+ "Calibration I Offset": calibration_i_offset,
446
+ "WiFi SSID": wifi_ssid,
447
+ "WiFi Password": wifi_password,
448
+ "API Endpoint URL": api_endpoint_url,
449
+ "API Port": api_port,
450
+ }
451
+
452
+ # Check whether the user wants to send or just to save
453
+ if flag == "send":
454
+ # Construct the configuration dictionary to be encoded
455
+ encoded_data = encode_user_configuration(
456
+ int(logger_id),
457
+ int(cell_id),
458
+ upload_method,
459
+ int(upload_interval),
460
+ enabled_sensors,
461
+ float(calibration_v_slope),
462
+ float(calibration_v_offset),
463
+ float(calibration_i_slope),
464
+ float(calibration_i_offset),
465
+ wifi_ssid,
466
+ wifi_password,
467
+ api_endpoint_url,
468
+ int(api_port),
469
+ )
470
+ # Send the encoded configuration via UART
471
+ success = self.sendToUART(encoded_data)
472
+ if not success:
473
+ return # Don't save if sending failed
474
+
475
+ print("------------------------------------------")
476
+ print(encoded_data)
477
+ print("------------------------------------------")
478
+ try:
479
+ # Ensure the 'Load' directory exists
480
+ load_dir = os.path.join(os.path.dirname(__file__), "Load")
481
+ os.makedirs(load_dir, exist_ok=True)
482
+ # Save configuration as JSON
483
+ config_path = os.path.join(load_dir, f"cell_{cell_id}_.json")
484
+ with open(config_path, "w") as json_file:
485
+ json.dump(configuration, json_file, indent=4)
486
+
487
+ QtWidgets.QMessageBox.information(
488
+ self.centralwidget, "Success", "Configurations saved successfully."
489
+ )
490
+ except Exception as e:
491
+ QtWidgets.QMessageBox.critical(
492
+ self.centralwidget, "Error", f"Failed to save configurations: {e}"
493
+ )
494
+
495
+ # Print success message in case of saving the config or saving and sending the config
496
+ if flag == "send":
497
+ print(
498
+ f"Configuration saved and sent to STM32 successfully! Backup JSON file: {'cell_'+ str(cell_id) + '_' }"
499
+ )
500
+ else:
501
+ print(
502
+ f"Configuration saved successfully! Backup JSON file: {'cell_'+ str(cell_id) + '_' }"
503
+ )
504
+
505
+ except ValueError as e:
506
+ # Show error message if validation fails
507
+ QtWidgets.QMessageBox.critical(self.centralwidget, "Error", str(e))
508
+
509
+ def loadConfiguration(self, flag: str):
510
+ """
511
+ @brief Loads configuration from a selected JSON file and fills the input fields.
512
+ """
513
+ # Load current configuration in STM32 and display it on the GUI
514
+ if flag == "loadCurrent":
515
+ success, encoded_data = self.receiveFromUART()
516
+ if not success:
517
+ return
518
+ decoded_data = decode_user_configuration(encoded_data)
519
+ print(decoded_data)
520
+
521
+ # Update GUI elements with decoded data
522
+ self.lineEdit_Logger_ID.setText(str(decoded_data["loggerId"]))
523
+ self.lineEdit_Cell_ID.setText(str(decoded_data["cellId"]))
524
+ self.comboBox_Upload_Method.setCurrentText(decoded_data["UploadMethod"])
525
+
526
+ # Calculate upload interval and update GUI fields
527
+ upload_interval = decoded_data["UploadInterval"]
528
+ days = upload_interval // 86400
529
+ remaining_seconds = upload_interval % 86400
530
+ hours = remaining_seconds // 3600
531
+ remaining_seconds %= 3600
532
+ minutes = remaining_seconds // 60
533
+ seconds = remaining_seconds % 60
534
+ self.lineEdit_Days.setText(str(days))
535
+ self.lineEdit_Hours.setText(str(hours))
536
+ self.lineEdit_Minutes.setText(str(minutes))
537
+ self.lineEdit_Seconds.setText(str(seconds))
538
+
539
+ # Update sensor checkboxes
540
+ self.checkBox_Voltage.setChecked(False)
541
+ self.checkBox_Current.setChecked(False)
542
+ self.checkBox_Teros12.setChecked(False)
543
+ self.checkBox_Teros21.setChecked(False)
544
+ self.checkBox_BME280.setChecked(False)
545
+ for sensor in decoded_data["enabledSensors"]:
546
+ if sensor == "Voltage":
547
+ self.checkBox_Voltage.setChecked(True)
548
+ elif sensor == "Current":
549
+ self.checkBox_Current.setChecked(True)
550
+ elif sensor == "Teros12":
551
+ self.checkBox_Teros12.setChecked(True)
552
+ elif sensor == "Teros21":
553
+ self.checkBox_Teros21.setChecked(True)
554
+ elif sensor == "BME280":
555
+ self.checkBox_BME280.setChecked(True)
556
+ # Fill calibration fields
557
+ self.lineEdit_V_Slope.setText(str(decoded_data["VoltageSlope"]))
558
+ self.lineEdit_V_Offset.setText(str(decoded_data["VoltageOffset"]))
559
+ self.lineEdit_I_Slope.setText(str(decoded_data["CurrentSlope"]))
560
+ self.lineEdit_I_Offset.setText(str(decoded_data["CurrentOffset"]))
561
+
562
+ # Fill WiFi settings if upload method is WiFi
563
+ if decoded_data["UploadMethod"] == "WiFi":
564
+ self.lineEdit_WiFi_SSID.setText(decoded_data["WiFiSSID"])
565
+ self.lineEdit_WiFi_Password.setText(decoded_data["WiFiPassword"])
566
+ self.lineEdit_API_Endpoint_URL.setText(decoded_data["APIEndpointURL"])
567
+ self.lineEdit_API_Port.setText(str(decoded_data["APIEndpointPort"]))
568
+ else:
569
+ # Clear WiFi fields if upload method is not WiFi
570
+ self.lineEdit_WiFi_SSID.clear()
571
+ self.lineEdit_WiFi_Password.clear()
572
+ self.lineEdit_API_Endpoint_URL.clear()
573
+ self.lineEdit_API_Port.clear()
574
+
575
+ QtWidgets.QMessageBox.information(
576
+ self.centralwidget,
577
+ "Success",
578
+ "Configuration loaded successfully From FRAM.",
579
+ )
580
+ return
581
+ # Load configuration from JSON file
582
+ else:
583
+ # Open a file dialog to select the JSON file
584
+ options = QtWidgets.QFileDialog.Options()
585
+ file_path, _ = QtWidgets.QFileDialog.getOpenFileName(
586
+ self.centralwidget,
587
+ "Select Configuration File",
588
+ "",
589
+ "JSON Files (*.json)",
590
+ options=options,
591
+ )
592
+
593
+ if file_path:
594
+ try:
595
+ with open(file_path, "r") as json_file:
596
+ config = json.load(json_file)
597
+
598
+ # Fill the GUI fields with the loaded configuration
599
+ self.lineEdit_Logger_ID.setText(str(config.get("Logger ID", "")))
600
+ self.lineEdit_Cell_ID.setText(str(config.get("Cell ID", "")))
601
+ self.comboBox_Upload_Method.setCurrentText(
602
+ config.get("Upload Method", "WiFi")
603
+ )
604
+
605
+ # Fill the upload interval fields
606
+ self.lineEdit_Days.setText(str(config.get("Days", 0)))
607
+ self.lineEdit_Hours.setText(str(config.get("Hours", 0)))
608
+ self.lineEdit_Minutes.setText(str(config.get("Minutes", 0)))
609
+ self.lineEdit_Seconds.setText(str(config.get("Seconds", 0)))
610
+
611
+ # Fill sensor checkboxes
612
+ enabled_sensors = config.get("Enabled Sensors", {})
613
+ self.checkBox_Voltage.setChecked(enabled_sensors.get("Voltage", False))
614
+ self.checkBox_Current.setChecked(enabled_sensors.get("Current", False))
615
+ self.checkBox_Teros12.setChecked(enabled_sensors.get("Teros12", False))
616
+ self.checkBox_Teros21.setChecked(enabled_sensors.get("Teros21", False))
617
+ self.checkBox_BME280.setChecked(enabled_sensors.get("BME280", False))
618
+
619
+ # Fill calibration fields
620
+ self.lineEdit_V_Slope.setText(
621
+ str(config.get("Calibration V Slope", ""))
622
+ )
623
+ self.lineEdit_V_Offset.setText(
624
+ str(config.get("Calibration V Offset", ""))
625
+ )
626
+ self.lineEdit_I_Slope.setText(
627
+ str(config.get("Calibration I Slope", ""))
628
+ )
629
+ self.lineEdit_I_Offset.setText(
630
+ str(config.get("Calibration I Offset", ""))
631
+ )
632
+
633
+ # Fill WiFi settings if upload method is WiFi
634
+ if config.get("Upload Method") == "WiFi":
635
+ self.lineEdit_WiFi_SSID.setText(config.get("WiFi SSID", ""))
636
+ self.lineEdit_WiFi_Password.setText(config.get("WiFi Password", ""))
637
+ self.lineEdit_API_Endpoint_URL.setText(
638
+ config.get("API Endpoint URL", "")
639
+ )
640
+ self.lineEdit_API_Port.setText(str(config.get("API Port", "")))
641
+ else:
642
+ # Clear the WiFi fields if the method is not WiFi
643
+ self.lineEdit_WiFi_SSID.clear()
644
+ self.lineEdit_WiFi_Password.clear()
645
+ self.lineEdit_API_Endpoint_URL.clear()
646
+ self.lineEdit_API_Port.clear()
647
+
648
+ QtWidgets.QMessageBox.information(
649
+ self.centralwidget, "Success", "Configuration loaded successfully."
650
+ )
651
+
652
+ except Exception as e:
653
+ QtWidgets.QMessageBox.critical(
654
+ self.centralwidget, "Error", f"Failed to load configuration: {e}"
655
+ )
656
+
657
+ def sendToUART(self, encoded_data):
658
+ """
659
+ @brief Sends the encoded configuration data via UART.
660
+
661
+ @param data Encoded configuration data.
662
+ """
663
+ ser = None
664
+ try:
665
+ # List available ports with descriptions
666
+ ports = serial.tools.list_ports.comports()
667
+ available_ports = [f"{port.device} - {port.description}" for port in ports]
668
+
669
+ if not available_ports:
670
+ QtWidgets.QMessageBox.critical(
671
+ self.centralwidget, "Error", "No serial ports available."
672
+ )
673
+ return False
674
+
675
+ # Ask the user to select a port
676
+ selected_port, ok = QInputDialog.getItem(
677
+ self.centralwidget,
678
+ "Select Port",
679
+ "Available Serial Ports:",
680
+ available_ports,
681
+ 0,
682
+ False,
683
+ )
684
+
685
+ if not ok or not selected_port:
686
+ QtWidgets.QMessageBox.critical(
687
+ self.centralwidget, "Error", "No port selected."
688
+ )
689
+ return False
690
+
691
+ # Extract the port name
692
+ port_name = selected_port.split(" ")[0]
693
+
694
+ # Open the serial port
695
+ ser = serial.Serial(port=port_name, baudrate=115200, timeout=20)
696
+ # Step 0: Send 1 indicating sending new config to be stored
697
+ ser.flush()
698
+ ser.write(bytes([1]))
699
+ print(f"Sending: {bytes([1])}")
700
+ # Step 1: Send the length of the encoded data (2 bytes)
701
+ data_length = len(encoded_data)
702
+ ser.write(
703
+ data_length.to_bytes(2, byteorder="big")
704
+ ) # Send length as 2-byte big-endian integer
705
+ # Step 2: Send the encoded data
706
+ ser.write(encoded_data)
707
+ print(
708
+ "________________________________________________________________________"
709
+ )
710
+ print(f"length: {data_length}")
711
+ print(f"{encoded_data}")
712
+
713
+ # Step 3: Wait for acknowledgment ("ACK")
714
+ ack = ser.read(3) # Read 3 bytes (assuming "ACK" is 3 bytes)
715
+ print(ack)
716
+ print(
717
+ "________________________________________________________________________"
718
+ )
719
+ if ack == b"ACK":
720
+ QtWidgets.QMessageBox.information(
721
+ self.centralwidget, "Success", "Received ACK from STM32"
722
+ )
723
+ else:
724
+ QtWidgets.QMessageBox.critical(
725
+ self.centralwidget, "UART Error", "No acknowledgment received"
726
+ )
727
+ return False
728
+
729
+ # Step 4: After ACK, read back the same data from STM32
730
+ received_data_length = int.from_bytes(
731
+ ser.read(2), byteorder="big"
732
+ ) # Read the length of received data
733
+ print(
734
+ "________________________________________________________________________"
735
+ )
736
+ print(f"length: {received_data_length}")
737
+ print(
738
+ "________________________________________________________________________"
739
+ )
740
+ print(
741
+ "________________________________________________________________________"
742
+ )
743
+
744
+ received_data = ser.read(
745
+ received_data_length
746
+ ) # Read the received data based on the length
747
+ print(f"Received from STM32: {received_data}")
748
+ print(
749
+ "________________________________________________________________________"
750
+ )
751
+
752
+ # Step 5: Display the received data to confirm it's the same
753
+ if received_data == encoded_data:
754
+ QtWidgets.QMessageBox.information(
755
+ self.centralwidget,
756
+ "Success",
757
+ "Data received matches the sent data.",
758
+ )
759
+ else:
760
+ QtWidgets.QMessageBox.critical(
761
+ self.centralwidget,
762
+ "Error",
763
+ "Received data does not match sent data.",
764
+ )
765
+
766
+ return True
767
+
768
+ except serial.SerialException as e:
769
+ QtWidgets.QMessageBox.critical(
770
+ self.centralwidget, "UART Error", f"Failed to send data: {e}"
771
+ )
772
+ return False
773
+
774
+ finally:
775
+ if ser is not None:
776
+ ser.close()
777
+
778
+ def receiveFromUART(self):
779
+ """
780
+ @brief Receives the current encoded configuration data via UART.
781
+
782
+ @param void.
783
+ @return (success, data): A tuple containing success status and decoded data or an error message.
784
+ """
785
+ ser = None
786
+ try:
787
+ # List available ports with descriptions
788
+ ports = serial.tools.list_ports.comports()
789
+ available_ports = [f"{port.device} - {port.description}" for port in ports]
790
+
791
+ if not available_ports:
792
+ QtWidgets.QMessageBox.critical(
793
+ self.centralwidget, "Error", "No serial ports available."
794
+ )
795
+ return False, "No serial ports available."
796
+
797
+ # Ask the user to select a port
798
+ selected_port, ok = QInputDialog.getItem(
799
+ self.centralwidget,
800
+ "Select Port",
801
+ "Available Serial Ports:",
802
+ available_ports,
803
+ 0,
804
+ False,
805
+ )
806
+
807
+ if not ok or not selected_port:
808
+ QtWidgets.QMessageBox.critical(
809
+ self.centralwidget, "Error", "No port selected."
810
+ )
811
+ return False, "No port selected."
812
+
813
+ # Extract the port name
814
+ port_name = selected_port.split(" ")[0]
815
+
816
+ # Open the serial port
817
+ ser = serial.Serial(port=port_name, baudrate=115200, timeout=2)
818
+ # Step 0: Send 2 indicating loading the current configurations from the FRAM
819
+ ser.write(bytes([2]))
820
+ print(f"Sending: {bytes([2])}")
821
+ # Step 1: Wait for acknowledgment ("ACK")
822
+ ack = ser.read(3) # Read 3 bytes (assuming "ACK" is 3 bytes)
823
+ print(ack)
824
+ if ack == b"ACK":
825
+ QtWidgets.QMessageBox.information(
826
+ self.centralwidget, "Success", "Received ACK from STM32"
827
+ )
828
+ else:
829
+ QtWidgets.QMessageBox.critical(
830
+ self.centralwidget, "UART Error", "No acknowledgment received"
831
+ )
832
+ return False, "No acknowledgment received from STM32."
833
+
834
+ # Step 2: After ACK, read data from STM32
835
+ received_data_length = int.from_bytes(
836
+ ser.read(2), byteorder="big"
837
+ ) # Read the length of received data
838
+ print(
839
+ "________________________________________________________________________"
840
+ )
841
+ print(f"length: {received_data_length}")
842
+ print(
843
+ "________________________________________________________________________"
844
+ )
845
+ print(
846
+ "________________________________________________________________________"
847
+ )
848
+
849
+ received_data = ser.read(
850
+ received_data_length
851
+ ) # Read the received data based on the length
852
+ print(f"Received from STM32: {received_data}")
853
+ print(
854
+ "________________________________________________________________________"
855
+ )
856
+ return True, received_data
857
+
858
+ except serial.SerialException as e:
859
+ QtWidgets.QMessageBox.critical(
860
+ self.centralwidget, "UART Error", f"Failed to send data: {e}"
861
+ )
862
+ return False, f"Failed to send or receive data: {e}"
863
+
864
+ finally:
865
+ if ser is not None:
866
+ ser.close()
867
+
868
+ def validateURL(self, value, field_name):
869
+ """
870
+ @brief Validates that the input is a valid URL.
871
+ @param value The input value to validate.
872
+ @param field_name The name of the field being validated.
873
+ @return The validated URL string.
874
+ @throws ValueError if the input is invalid.
875
+ """
876
+ url_pattern = re.compile(
877
+ r"^(https?|ftp):\/\/" # http:// or https:// or ftp://
878
+ r"([a-zA-Z0-9_-]+(?:(?:\.[a-zA-Z0-9_-]+)+))" # domain
879
+ r"(:[0-9]{1,5})?" # port (?: optional)
880
+ r"(\/.*)?$" # path (?: optional)
881
+ )
882
+ if not url_pattern.match(value):
883
+ raise ValueError(f"Invalid {field_name}. Must be a valid URL.")
884
+ return value
885
+
886
+ def validateUInt(self, value, name):
887
+ """
888
+ @brief Validates that the input is a positive integer.
889
+
890
+ @param value The input value to validate.
891
+ @param name The name of the parameter (for error messages).
892
+ @return Validated unsigned integer value.
893
+ """
894
+ if not value.isdigit() or int(value) < 0:
895
+ raise ValueError(f"{name} must be a positive integer.")
896
+ return int(value)
897
+
898
+ def validateInt(self, value, name):
899
+ """
900
+ @brief Validates that the input is an integer.
901
+
902
+ @param value The input value to validate.
903
+ @param name The name of the parameter (for error messages).
904
+ @return Validated integer value.
905
+ """
906
+ try:
907
+ return int(value)
908
+ except ValueError:
909
+ raise ValueError(f"{name} must be an integer.")
910
+
911
+ def validateFloat(self, value, name):
912
+ """
913
+ @brief Validates that the input is a float.
914
+
915
+ @param value The input value to validate.
916
+ @param name The name of the parameter (for error messages).
917
+ @return Validated float value.
918
+ """
919
+ try:
920
+ return float(value)
921
+ except ValueError:
922
+ raise ValueError(f"{name} must be a floating-point number.")
923
+
924
+
925
+ def main():
926
+ app = QtWidgets.QApplication(sys.argv)
927
+ MainWindow = QtWidgets.QMainWindow()
928
+ ui = Ui_MainWindow()
929
+ ui.setupUi(MainWindow)
930
+ MainWindow.show()
931
+ sys.exit(app.exec_())
932
+
933
+
934
+ if __name__ == "__main__":
935
+ main()