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.
- ents/__init__.py +23 -0
- ents/calibrate/PingSMU.py +51 -0
- ents/calibrate/PingSPS.py +66 -0
- ents/calibrate/README.md +3 -0
- ents/calibrate/__init__.py +0 -0
- ents/calibrate/linear_regression.py +78 -0
- ents/calibrate/plots.py +83 -0
- ents/calibrate/recorder.py +678 -0
- ents/calibrate/requirements.txt +9 -0
- ents/cli.py +546 -0
- ents/config/README.md +123 -0
- ents/config/__init__.py +1 -0
- ents/config/adv_trace.py +56 -0
- ents/config/user_config.py +935 -0
- ents/proto/__init__.py +33 -0
- ents/proto/decode.py +106 -0
- ents/proto/encode.py +298 -0
- ents/proto/esp32.py +179 -0
- ents/proto/soil_power_sensor_pb2.py +72 -0
- ents/simulator/__init__.py +0 -0
- ents/simulator/node.py +161 -0
- ents-2.3.2.dist-info/METADATA +206 -0
- ents-2.3.2.dist-info/RECORD +26 -0
- ents-2.3.2.dist-info/WHEEL +4 -0
- ents-2.3.2.dist-info/entry_points.txt +5 -0
- ents-2.3.2.dist-info/licenses/LICENSE +21 -0
|
@@ -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()
|