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
ents/cli.py
ADDED
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import numpy as np
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
from .calibrate.recorder import Recorder
|
|
10
|
+
from .calibrate.linear_regression import (
|
|
11
|
+
linear_regression,
|
|
12
|
+
print_eval,
|
|
13
|
+
print_coef,
|
|
14
|
+
print_norm,
|
|
15
|
+
)
|
|
16
|
+
from .calibrate.plots import (
|
|
17
|
+
plot_measurements,
|
|
18
|
+
plot_calib,
|
|
19
|
+
plot_residuals,
|
|
20
|
+
plot_residuals_hist,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from .proto.encode import (
|
|
24
|
+
encode_power_measurement,
|
|
25
|
+
encode_phytos31_measurement,
|
|
26
|
+
encode_teros12_measurement,
|
|
27
|
+
encode_response,
|
|
28
|
+
)
|
|
29
|
+
from .proto.decode import decode_measurement, decode_response
|
|
30
|
+
from .proto.esp32 import encode_esp32command, decode_esp32command
|
|
31
|
+
|
|
32
|
+
from .simulator.node import NodeSimulator
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def entry():
|
|
36
|
+
"""Command line interface entry point"""
|
|
37
|
+
|
|
38
|
+
parser = argparse.ArgumentParser()
|
|
39
|
+
|
|
40
|
+
subparsers = parser.add_subparsers(help="Ents Utilities")
|
|
41
|
+
|
|
42
|
+
create_encode_parser(subparsers)
|
|
43
|
+
create_decode_parser(subparsers)
|
|
44
|
+
create_calib_parser(subparsers)
|
|
45
|
+
create_sim_parser(subparsers)
|
|
46
|
+
|
|
47
|
+
args = parser.parse_args()
|
|
48
|
+
args.func(args)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def create_sim_parser(subparsers):
|
|
52
|
+
"""Creates the simulation subparser
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
subparsers: Reference to subparser group
|
|
56
|
+
Returns:
|
|
57
|
+
Reference to new subparser
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
sim_p = subparsers.add_parser("sim", help="Simluate sensor uploads")
|
|
61
|
+
sim_p.add_argument(
|
|
62
|
+
"--url",
|
|
63
|
+
required=True,
|
|
64
|
+
type=str,
|
|
65
|
+
help="URL of the dirtviz instance (default: http://localhost:8000)",
|
|
66
|
+
)
|
|
67
|
+
sim_p.add_argument(
|
|
68
|
+
"--mode",
|
|
69
|
+
required=True,
|
|
70
|
+
choices=["batch", "stream"],
|
|
71
|
+
type=str,
|
|
72
|
+
help="Upload mode",
|
|
73
|
+
)
|
|
74
|
+
sim_p.add_argument(
|
|
75
|
+
"--sensor",
|
|
76
|
+
required=True,
|
|
77
|
+
choices=["power", "teros12", "teros21", "bme280"],
|
|
78
|
+
type=str,
|
|
79
|
+
nargs="+",
|
|
80
|
+
)
|
|
81
|
+
sim_p.add_argument("--cell", required=True, type=int, help="Cell Id")
|
|
82
|
+
sim_p.add_argument("--logger", required=True, type=int, help="Logger Id")
|
|
83
|
+
sim_p.add_argument("--start", type=str, help="Start date")
|
|
84
|
+
sim_p.add_argument("--end", type=str, help="End date")
|
|
85
|
+
sim_p.add_argument(
|
|
86
|
+
"--freq", default=10.0, type=float, help="Frequency of uploads (default: 10s)"
|
|
87
|
+
)
|
|
88
|
+
sim_p.set_defaults(func=simulate)
|
|
89
|
+
|
|
90
|
+
return sim_p
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def simulate(args):
|
|
94
|
+
simulation = NodeSimulator(
|
|
95
|
+
cell=args.cell,
|
|
96
|
+
logger=args.logger,
|
|
97
|
+
sensors=args.sensor,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if args.mode == "batch":
|
|
101
|
+
if (args.start is None) or (args.end is None):
|
|
102
|
+
raise ValueError("Start and end date must be provided for batch mode.")
|
|
103
|
+
|
|
104
|
+
# format dates
|
|
105
|
+
curr_dt = datetime.fromisoformat(args.start)
|
|
106
|
+
end_dt = datetime.fromisoformat(args.end)
|
|
107
|
+
|
|
108
|
+
# create list of measurements
|
|
109
|
+
while curr_dt <= end_dt:
|
|
110
|
+
ts = int(curr_dt.timestamp())
|
|
111
|
+
simulation.measure(ts)
|
|
112
|
+
curr_dt += timedelta(seconds=args.freq)
|
|
113
|
+
|
|
114
|
+
# send measurements
|
|
115
|
+
while simulation.send_next(args.url):
|
|
116
|
+
print(simulation)
|
|
117
|
+
|
|
118
|
+
print("Done!")
|
|
119
|
+
|
|
120
|
+
elif args.mode == "stream":
|
|
121
|
+
print("Use CTRL+C to stop the simulation")
|
|
122
|
+
try:
|
|
123
|
+
while True:
|
|
124
|
+
dt = datetime.now()
|
|
125
|
+
ts = int(dt.timestamp())
|
|
126
|
+
simulation.measure(ts)
|
|
127
|
+
while simulation.send_next(args.url):
|
|
128
|
+
print(simulation)
|
|
129
|
+
time.sleep(args.freq)
|
|
130
|
+
except KeyboardInterrupt as _:
|
|
131
|
+
print("Stopping simulation")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def create_calib_parser(subparsers):
|
|
135
|
+
"""Creates the calibration subparser
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
subparsers: Reference to subparser group
|
|
139
|
+
Returns:
|
|
140
|
+
Reference to new subparser
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
# calibration parser
|
|
144
|
+
calib_p = subparsers.add_parser("calib", help="Calibrate power measurements")
|
|
145
|
+
calib_p.add_argument(
|
|
146
|
+
"--samples",
|
|
147
|
+
type=int,
|
|
148
|
+
default=10,
|
|
149
|
+
required=False,
|
|
150
|
+
help="Samples taken at each step (default: 10)",
|
|
151
|
+
)
|
|
152
|
+
calib_p.add_argument(
|
|
153
|
+
"--plot", action="store_true", help="Show calibration parameter plots"
|
|
154
|
+
)
|
|
155
|
+
calib_p.add_argument(
|
|
156
|
+
"--mode",
|
|
157
|
+
type=str,
|
|
158
|
+
default="both",
|
|
159
|
+
required=False,
|
|
160
|
+
help="Either both, voltage, or current (default: both)",
|
|
161
|
+
)
|
|
162
|
+
calib_p.add_argument(
|
|
163
|
+
"--output", type=str, required=False, help="Output directory for measurements"
|
|
164
|
+
)
|
|
165
|
+
calib_p.add_argument("port", type=str, help="Board serial port")
|
|
166
|
+
calib_p.add_argument("host", type=str, help="Address and port of smu (ip:port)")
|
|
167
|
+
calib_p.set_defaults(func=calibrate)
|
|
168
|
+
|
|
169
|
+
return calib_p
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def create_encode_parser(subparsers):
|
|
173
|
+
"""Create encode command subparser
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
subparsers: Reference to subparser group
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Reference to new subparser
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
encode_parser = subparsers.add_parser("encode", help="Encode data")
|
|
183
|
+
|
|
184
|
+
print_format = encode_parser.add_mutually_exclusive_group()
|
|
185
|
+
print_format.add_argument(
|
|
186
|
+
"--hex", action="store_true", help="Print as hex values (default)"
|
|
187
|
+
)
|
|
188
|
+
print_format.add_argument(
|
|
189
|
+
"--raw", action="store_true", help="Print raw bytes object"
|
|
190
|
+
)
|
|
191
|
+
print_format.add_argument(
|
|
192
|
+
"--c", action="store_true", help="Print bytes for copying to c"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
encode_subparsers = encode_parser.add_subparsers(title="Message type", dest="type")
|
|
196
|
+
|
|
197
|
+
def create_measurement_parser(encode_subparsers):
|
|
198
|
+
measurement_parser = encode_subparsers.add_parser(
|
|
199
|
+
"measurement", help='Proto "Measurement" message'
|
|
200
|
+
)
|
|
201
|
+
measurement_subparser = measurement_parser.add_subparsers(
|
|
202
|
+
title="Measurement type"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# metadata
|
|
206
|
+
measurement_parser.add_argument("ts", type=int, help="Unix epoch timestamp")
|
|
207
|
+
measurement_parser.add_argument("cell", type=int, help="Cell Id")
|
|
208
|
+
measurement_parser.add_argument("logger", type=int, help="Logger Id")
|
|
209
|
+
|
|
210
|
+
power_parser = measurement_subparser.add_parser(
|
|
211
|
+
"power", help="PowerMeasurement"
|
|
212
|
+
)
|
|
213
|
+
power_parser.add_argument("voltage", type=float, help="Voltage in (V)")
|
|
214
|
+
power_parser.add_argument("current", type=float, help="Current in (A)")
|
|
215
|
+
power_parser.set_defaults(func=handle_encode_measurement_power)
|
|
216
|
+
|
|
217
|
+
teros12_parser = measurement_subparser.add_parser(
|
|
218
|
+
"teros12", help="Teros12Measurement"
|
|
219
|
+
)
|
|
220
|
+
teros12_parser.add_argument("vwc_raw", type=float, help="Raw vwc")
|
|
221
|
+
teros12_parser.add_argument("vwc_adj", type=float, help="Calibrated vwc")
|
|
222
|
+
teros12_parser.add_argument("temp", type=float, help="Temperature in C")
|
|
223
|
+
teros12_parser.add_argument("ec", type=int, help="Electrical conductivity")
|
|
224
|
+
teros12_parser.set_defaults(func=handle_encode_measurement_teros12)
|
|
225
|
+
|
|
226
|
+
phytos31_parser = measurement_subparser.add_parser(
|
|
227
|
+
"phytos31", help="Phytos31Measurement"
|
|
228
|
+
)
|
|
229
|
+
phytos31_parser.add_argument("voltage", type=float, help="Raw voltage (V)")
|
|
230
|
+
phytos31_parser.add_argument("leaf_wetness", type=float, help="Leaf wetness")
|
|
231
|
+
phytos31_parser.set_defaults(func=handle_encode_measurement_phytos31)
|
|
232
|
+
|
|
233
|
+
return measurement_parser
|
|
234
|
+
|
|
235
|
+
def create_response_parser(encode_subparsers):
|
|
236
|
+
response_parser = encode_subparsers.add_parser(
|
|
237
|
+
"response", help='Proto "Response" message'
|
|
238
|
+
)
|
|
239
|
+
response_parser.add_argument("status", type=str, help="Status")
|
|
240
|
+
response_parser.set_defaults(func=handle_encode_response)
|
|
241
|
+
|
|
242
|
+
return response_parser
|
|
243
|
+
|
|
244
|
+
def create_esp32command_parser(encode_subparsers):
|
|
245
|
+
esp32command_parser = encode_subparsers.add_parser(
|
|
246
|
+
"esp32command", help='Proto "Esp32Command" message'
|
|
247
|
+
)
|
|
248
|
+
esp32command_subparser = esp32command_parser.add_subparsers(
|
|
249
|
+
title="type", help="PageCommand"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
test_parser = esp32command_subparser.add_parser("test", help="TestCommand")
|
|
253
|
+
test_parser.add_argument("state", type=str, help="State to put module into")
|
|
254
|
+
test_parser.add_argument("data", type=int, help="Data associated with command")
|
|
255
|
+
test_parser.set_defaults(func=handle_encode_esp32command_test)
|
|
256
|
+
|
|
257
|
+
esp32_parser = esp32command_subparser.add_parser("page", help="PageCommand")
|
|
258
|
+
esp32_parser.add_argument("type", type=str, help="Request type")
|
|
259
|
+
esp32_parser.add_argument("fd", type=int, help="File descriptor")
|
|
260
|
+
esp32_parser.add_argument("bs", type=int, help="Block size")
|
|
261
|
+
esp32_parser.add_argument("num", type=int, help="Number of bytes")
|
|
262
|
+
esp32_parser.set_defaults(func=handle_encode_esp32command_page)
|
|
263
|
+
|
|
264
|
+
wifi_parser = esp32command_subparser.add_parser("wifi", help="WiFiCommand")
|
|
265
|
+
wifi_parser.add_argument("type", type=str, help="WiFi command type")
|
|
266
|
+
wifi_parser.add_argument("--ssid", type=str, default="", help="WiFi SSID")
|
|
267
|
+
wifi_parser.add_argument("--passwd", type=str, default="", help="WiFi password")
|
|
268
|
+
wifi_parser.add_argument("--url", type=str, default="", help="Endpoint url")
|
|
269
|
+
wifi_parser.add_argument("--port", type=int, default=0, help="Endpoint port")
|
|
270
|
+
wifi_parser.add_argument("--rc", type=int, default=0, help="Return code")
|
|
271
|
+
wifi_parser.add_argument("--ts", type=int, help="Timestamp in unix epochs")
|
|
272
|
+
wifi_parser.add_argument(
|
|
273
|
+
"--resp", type=str, default=b"", help="Serialized response message"
|
|
274
|
+
)
|
|
275
|
+
wifi_parser.set_defaults(func=handle_encode_esp32command_wifi)
|
|
276
|
+
|
|
277
|
+
return esp32command_parser
|
|
278
|
+
|
|
279
|
+
# create subparsers
|
|
280
|
+
create_measurement_parser(encode_subparsers)
|
|
281
|
+
create_response_parser(encode_subparsers)
|
|
282
|
+
create_esp32command_parser(encode_subparsers)
|
|
283
|
+
|
|
284
|
+
return encode_parser
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def handle_encode_measurement_power(args):
|
|
288
|
+
data = encode_power_measurement(
|
|
289
|
+
ts=args.ts,
|
|
290
|
+
cell_id=args.cell,
|
|
291
|
+
logger_id=args.logger,
|
|
292
|
+
voltage=args.voltage,
|
|
293
|
+
current=args.current,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
print_data(args, data)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def handle_encode_measurement_teros12(args):
|
|
300
|
+
data = encode_teros12_measurement(
|
|
301
|
+
ts=args.ts,
|
|
302
|
+
cell_id=args.cell,
|
|
303
|
+
logger_id=args.logger,
|
|
304
|
+
vwc_raw=args.vwc_raw,
|
|
305
|
+
vwc_adj=args.vwc_adj,
|
|
306
|
+
temp=args.temp,
|
|
307
|
+
ec=args.ec,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
print_data(args, data)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def handle_encode_measurement_phytos31(args):
|
|
314
|
+
data = encode_phytos31_measurement(
|
|
315
|
+
ts=args.ts,
|
|
316
|
+
cell_id=args.cell,
|
|
317
|
+
logger_id=args.logger,
|
|
318
|
+
voltage=args.voltage,
|
|
319
|
+
leaf_wetness=args.leaf_wetness,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
print_data(args, data)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def handle_encode_response(args):
|
|
326
|
+
valid_status = ["SUCCESS", "ERROR"]
|
|
327
|
+
if args.status not in valid_status:
|
|
328
|
+
raise NotImplementedError(f'Response status "{args.status}" not implemented')
|
|
329
|
+
|
|
330
|
+
data = encode_response(args.status)
|
|
331
|
+
print_data(args, data)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def handle_encode_esp32command_test(args):
|
|
335
|
+
data = encode_esp32command("test", state=args.state, data=args.data)
|
|
336
|
+
print_data(args, data)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def handle_encode_esp32command_page(args):
|
|
340
|
+
data = encode_esp32command(
|
|
341
|
+
"page", req=args.type.lower(), fd=args.fd, bs=args.bs, n=args.num
|
|
342
|
+
)
|
|
343
|
+
print_data(args, data)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def handle_encode_esp32command_wifi(args):
|
|
347
|
+
data = encode_esp32command(
|
|
348
|
+
"wifi",
|
|
349
|
+
_type=args.type.lower(),
|
|
350
|
+
ssid=args.ssid,
|
|
351
|
+
passwd=args.passwd,
|
|
352
|
+
url=args.url,
|
|
353
|
+
port=args.port,
|
|
354
|
+
rc=args.rc,
|
|
355
|
+
ts=args.ts,
|
|
356
|
+
resp=args.resp,
|
|
357
|
+
)
|
|
358
|
+
print_data(args, data)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def print_data(args, data: bytes) -> str:
|
|
362
|
+
if args.c:
|
|
363
|
+
print_bytes_c(data)
|
|
364
|
+
elif args.raw:
|
|
365
|
+
print(data)
|
|
366
|
+
else:
|
|
367
|
+
print(data.hex())
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def print_bytes_c(data: bytes) -> str:
|
|
371
|
+
"""Formats serialized data into c bytes array"""
|
|
372
|
+
|
|
373
|
+
# format data string
|
|
374
|
+
data_str = "uint8_t data[] = {"
|
|
375
|
+
hex_str = [hex(d) for d in data]
|
|
376
|
+
data_str += ", ".join(hex_str)
|
|
377
|
+
data_str += "};"
|
|
378
|
+
|
|
379
|
+
# print data string
|
|
380
|
+
print(data_str)
|
|
381
|
+
|
|
382
|
+
# print length of data string
|
|
383
|
+
print(f"size_t data_len = {len(hex_str)};")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def create_decode_parser(subparsers):
|
|
387
|
+
"""Create decode command parser
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
subparsers: Reference to subparser group
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Reference to new subparser
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
decode_parser = subparsers.add_parser("decode", help="Decode data")
|
|
397
|
+
|
|
398
|
+
decode_subparsers = decode_parser.add_subparsers(title="Message type", dest="type")
|
|
399
|
+
|
|
400
|
+
# measurement
|
|
401
|
+
measurement_parser = decode_subparsers.add_parser(
|
|
402
|
+
"measurement", help='Proto "Measurement" message'
|
|
403
|
+
)
|
|
404
|
+
measurement_parser.set_defaults(func=handle_decode_measurement)
|
|
405
|
+
|
|
406
|
+
# response
|
|
407
|
+
response_parser = decode_subparsers.add_parser(
|
|
408
|
+
"response", help='Proto "Response" message'
|
|
409
|
+
)
|
|
410
|
+
response_parser.set_defaults(func=handle_decode_response)
|
|
411
|
+
|
|
412
|
+
# esp32command
|
|
413
|
+
esp32command_parser = decode_subparsers.add_parser(
|
|
414
|
+
"esp32command", help='Proto "Esp32Command" message'
|
|
415
|
+
)
|
|
416
|
+
esp32command_parser.set_defaults(func=handle_decode_esp32command)
|
|
417
|
+
|
|
418
|
+
decode_parser.add_argument(
|
|
419
|
+
"data", type=str, help="Protobuf serialized data in hex format"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
return decode_parser
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def handle_decode_measurement(args):
|
|
426
|
+
data = bytes.fromhex(args.data)
|
|
427
|
+
vals = decode_measurement(data)
|
|
428
|
+
print(vals)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def handle_decode_response(args):
|
|
432
|
+
data = bytes.fromhex(args.data)
|
|
433
|
+
vals = decode_response(data)
|
|
434
|
+
print(vals)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def handle_decode_esp32command(args):
|
|
438
|
+
data = bytes.fromhex(args.data)
|
|
439
|
+
vals = decode_esp32command(data)
|
|
440
|
+
print(vals)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def calibrate(args):
|
|
444
|
+
print(
|
|
445
|
+
"If you don't see any output for 5 seconds, restart the calibration after resetting the ents board"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
host, port = args.host.split(":")
|
|
449
|
+
rec = Recorder(args.port, host, int(port))
|
|
450
|
+
|
|
451
|
+
if args.mode == "both":
|
|
452
|
+
run_v = True
|
|
453
|
+
run_i = True
|
|
454
|
+
elif args.mode in ["v", "volts", "voltage"]:
|
|
455
|
+
run_v = True
|
|
456
|
+
run_i = False
|
|
457
|
+
elif args.mode in ["i", "amps", "current"]:
|
|
458
|
+
run_v = True
|
|
459
|
+
run_i = True
|
|
460
|
+
else:
|
|
461
|
+
raise NotImplementedError(f"Calbration mode: {args.mode} not implemented")
|
|
462
|
+
|
|
463
|
+
V_START = -2.0
|
|
464
|
+
V_STOP = 2.0
|
|
465
|
+
V_STEP = 0.5
|
|
466
|
+
|
|
467
|
+
I_START = -0.009
|
|
468
|
+
I_STOP = 0.009
|
|
469
|
+
I_STEP = 0.0045
|
|
470
|
+
|
|
471
|
+
def record_calibrate(start, stop, step, name: str):
|
|
472
|
+
"""Record and calibrate
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
start: Start value
|
|
476
|
+
stop: Stop value (inclusive)
|
|
477
|
+
step: Step between values
|
|
478
|
+
name: Name of channel
|
|
479
|
+
"""
|
|
480
|
+
|
|
481
|
+
# TODO Unjank reference to member variables by moving the selection to
|
|
482
|
+
# the class.
|
|
483
|
+
if name == "voltage":
|
|
484
|
+
iterator = Recorder.record_voltage
|
|
485
|
+
elif name == "current":
|
|
486
|
+
iterator = Recorder.record_current
|
|
487
|
+
|
|
488
|
+
# collect data
|
|
489
|
+
print("Collecting calibration data")
|
|
490
|
+
cal = iterator(rec, start, stop, step, args.samples)
|
|
491
|
+
if args.output:
|
|
492
|
+
save_csv(cal, args.output, f"{name}-cal.csv")
|
|
493
|
+
|
|
494
|
+
print("Collecting evaluation data")
|
|
495
|
+
_eval = iterator(rec, start, stop, step, args.samples)
|
|
496
|
+
if args.output:
|
|
497
|
+
save_csv(_eval, args.output, f"{name}-eval.csv")
|
|
498
|
+
|
|
499
|
+
model = linear_regression(
|
|
500
|
+
np.array(cal["meas"]).reshape(-1, 1), np.array(cal["actual"]).reshape(-1, 1)
|
|
501
|
+
)
|
|
502
|
+
pred = model.predict(np.array(_eval["meas"]).reshape(-1, 1))
|
|
503
|
+
residuals = np.array(_eval["actual"]) - pred.flatten()
|
|
504
|
+
|
|
505
|
+
print("")
|
|
506
|
+
print("\r\rnCoefficients")
|
|
507
|
+
print_coef(model)
|
|
508
|
+
print("\r\nEvaluation")
|
|
509
|
+
print_eval(pred, _eval["actual"])
|
|
510
|
+
print("\r\nNormal fit")
|
|
511
|
+
print_norm(residuals)
|
|
512
|
+
print("")
|
|
513
|
+
|
|
514
|
+
# plots
|
|
515
|
+
if args.plot:
|
|
516
|
+
plot_measurements(cal["actual"], cal["meas"], title=name)
|
|
517
|
+
plot_calib(_eval["meas"], pred, title=name)
|
|
518
|
+
plot_residuals(pred, residuals, title=name)
|
|
519
|
+
plot_residuals_hist(residuals, title=name)
|
|
520
|
+
|
|
521
|
+
if run_v:
|
|
522
|
+
print("Connect smu to voltage inputs device and press ENTER")
|
|
523
|
+
input()
|
|
524
|
+
record_calibrate(V_START, V_STOP, V_STEP, "voltage")
|
|
525
|
+
|
|
526
|
+
if run_i:
|
|
527
|
+
print(
|
|
528
|
+
"Connect smu to a resistor in series with the current channels and press ENTER"
|
|
529
|
+
)
|
|
530
|
+
input()
|
|
531
|
+
record_calibrate(I_START, I_STOP, I_STEP, "current")
|
|
532
|
+
|
|
533
|
+
print("Press enter to close plots")
|
|
534
|
+
input()
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def save_csv(data: dict[str, list], path: str, name: str):
|
|
538
|
+
"""Save measurement dictionary to csv
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
data: Measurement data
|
|
542
|
+
path: Folder path
|
|
543
|
+
name: Name of csv file
|
|
544
|
+
"""
|
|
545
|
+
path = os.path.join(path, name)
|
|
546
|
+
pd.DataFrame(data).to_csv(path, index=False)
|
ents/config/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# GUI Application
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This PyQt5-based graphical user interface (GUI) allows users to configure settings for a device. It supports input fields for 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). Settings can be saved to a JSON file, loaded from a saved file, or directly transmitted to the device via UART.
|
|
6
|
+
|
|
7
|
+
The application provides options to:
|
|
8
|
+
|
|
9
|
+
- **Save configurations to a JSON file**
|
|
10
|
+
- **Load previously saved configurations**
|
|
11
|
+
- **Directly transmit settings to the device via UART**, using Protobuf for serialization.
|
|
12
|
+
- **Load current configuration** directly from the device.
|
|
13
|
+
|
|
14
|
+
## Key Features
|
|
15
|
+
|
|
16
|
+
- **Save and Load Configurations**: Save configurations to a JSON file named with the cell ID, or load an existing configuration.
|
|
17
|
+
- **Real-time Configuration Transmission**: Settings are serialized using Protobuf when the "Send Configuration" button is clicked, then transmitted over UART to the STM32 device.
|
|
18
|
+
- **Load Current Configuration**: Users can load the current configuration from the device by clicking the "Load Current Configuration" button, which reads the configuration stored in FRAM and displays it in the fields.
|
|
19
|
+
- **Input Validation**: Ensures only valid data is entered, prompting error messages for any invalid fields to guide users.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
To install the required dependencies, use the following command:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install -r requirements.txt
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Run the application by executing the following command:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
python user_config.py
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Opening the GUI from the Executable (No Python Required)
|
|
36
|
+
|
|
37
|
+
If you don't have Python installed or prefer to use a pre-built executable, follow these steps to open the GUI from the desktop executable:
|
|
38
|
+
|
|
39
|
+
**1- Open the Executable file path:**
|
|
40
|
+
|
|
41
|
+
- The executable file can be found in the following path:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
\proto\python\src\ents\config\dist
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**2- Run the Executable:**
|
|
48
|
+
|
|
49
|
+
- Navigate to the directory where **userConfig.exe** is located.
|
|
50
|
+
|
|
51
|
+
**3- Start Using the GUI:**
|
|
52
|
+
|
|
53
|
+
- Once the executable is opened, the GUI will appear. You can now configure your device settings as usual without needing Python or any development tools.
|
|
54
|
+
|
|
55
|
+
- The GUI will work on Windows, MacOS, and Linux platforms.
|
|
56
|
+
|
|
57
|
+
## Example JSON Configuration File
|
|
58
|
+
|
|
59
|
+
The following is an example JSON file generated after saving or sending user configuration settings:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
{
|
|
63
|
+
"Logger ID": 5,
|
|
64
|
+
"Cell ID": 5,
|
|
65
|
+
"Upload Method": "LoRa",
|
|
66
|
+
"Days": 5,
|
|
67
|
+
"Hours": 5,
|
|
68
|
+
"Minutes": 5,
|
|
69
|
+
"Seconds": 5,
|
|
70
|
+
"Enabled Sensors": {
|
|
71
|
+
"Voltage": true,
|
|
72
|
+
"Current": true,
|
|
73
|
+
"Teros12": true,
|
|
74
|
+
"Teros21": true,
|
|
75
|
+
"BME280": true
|
|
76
|
+
},
|
|
77
|
+
"Calibration V Slope": 5.0,
|
|
78
|
+
"Calibration V Offset": 5.0,
|
|
79
|
+
"Calibration I Slope": 5.0,
|
|
80
|
+
"Calibration I Offset": 5.0,
|
|
81
|
+
"WiFi SSID": "",
|
|
82
|
+
"WiFi Password": "",
|
|
83
|
+
"API Endpoint URL": "",
|
|
84
|
+
"API Port": 0
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Screenshots
|
|
89
|
+
|
|
90
|
+
### Application Screenshots
|
|
91
|
+
|
|
92
|
+
The following images show the GUI interface in different configurations:
|
|
93
|
+
|
|
94
|
+
|**WiFi Upload Method**|**LoRa Upload Method**|
|
|
95
|
+
|---------------------|-----------|
|
|
96
|
+
|||
|
|
97
|
+
|
|
98
|
+
### GUI Flow Diagram
|
|
99
|
+
|
|
100
|
+
The flow diagram below illustrates the general process and structure of the application:
|
|
101
|
+
|
|
102
|
+

|
|
103
|
+
|
|
104
|
+
## User Manual
|
|
105
|
+
|
|
106
|
+
### How to Use the GUI
|
|
107
|
+
|
|
108
|
+
**1- Save Configuration:**
|
|
109
|
+
|
|
110
|
+
- Fill in the necessary fields such as Logger ID, Cell ID, upload method, etc.
|
|
111
|
+
Click on the **"Save"** button to save the configuration to a JSON file. The file will be named with the Cell ID as the filename.
|
|
112
|
+
|
|
113
|
+
**2- Load Configuration:**
|
|
114
|
+
|
|
115
|
+
- To load a previously saved configuration, click on the **"Load"** button. Browse to the desired JSON file and load it into the application.
|
|
116
|
+
|
|
117
|
+
**3- Send Configuration to Device:**
|
|
118
|
+
|
|
119
|
+
- After entering the configuration settings, click on the **"Send Configuration"** button to transmit the settings to the device via UART. The data will be serialized using Protobuf and sent to the device.
|
|
120
|
+
|
|
121
|
+
**4- Load Current Configuration from Device:**
|
|
122
|
+
|
|
123
|
+
- To read the current configuration from the device, click on the **"Load Current Configuration"** button. This retrieves the stored configuration from FRAM and populates the fields with the current values.
|
ents/config/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# This file makes this directory a Python package.
|