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,678 @@
1
+ #!/usr/bin/env python
2
+
3
+ """Data recorder for Soil Power Sensor
4
+
5
+ The recorder controls the SPS firmware and a Keithley 2400 Source
6
+ Measurement Unit (SMU). Ideally the script should work with any microcontrollre
7
+ flashed with the firmware and any source measurement unit that supports
8
+ Standard Commands for Programable Instruments (SCPI). The units listed are the
9
+ ones that the script was developed and tested on. It allows to step through
10
+ a range of output voltages on the Keithley and measure the voltage and current
11
+ from both the SMU and the Soil Power Sensor (SPS).
12
+ """
13
+
14
+ import time
15
+ import socket
16
+ import serial
17
+ from typing import Tuple
18
+ from tqdm import tqdm
19
+ from ..proto import decode_measurement
20
+
21
+
22
+ class SerialController:
23
+ """Generic serial controller that will open and close serial connections"""
24
+
25
+ # Serial port
26
+ ser = None
27
+
28
+ def __init__(self, port, baudrate=115200, xonxoff=False):
29
+ """Constructor
30
+
31
+ Initialises connection to serial port.
32
+
33
+ Parameters
34
+ ----------
35
+ port : str
36
+ Serial port of device
37
+ baudrate : int, optional
38
+ Baud rate for serial communication (default is 115200, STM32 functions at 115200)
39
+ xonxoff : bool, optional
40
+ Flow control (default is on)
41
+ """
42
+
43
+ self.ser = serial.Serial(port, baudrate=baudrate, xonxoff=xonxoff)
44
+
45
+ def __del__(self):
46
+ """Destructor
47
+
48
+ Closes connection to serial port.
49
+ """
50
+
51
+ self.ser.close()
52
+
53
+
54
+ class LANController:
55
+ """Generic LAN controller that will open and close LAN connections"""
56
+
57
+ # Socket
58
+ sock = None
59
+
60
+ def __init__(self, host, port):
61
+ """Constructor
62
+
63
+ Initialises connection to LAN device.
64
+
65
+ Parameters
66
+ ----------
67
+ host : str
68
+ IP address or hostname of the device
69
+ port : int
70
+ Port number of the device
71
+ """
72
+
73
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
74
+ self.sock.connect((host, port))
75
+
76
+ def __del__(self):
77
+ """Destructor
78
+
79
+ Closes connection to LAN device.
80
+ """
81
+
82
+ self.sock.close()
83
+
84
+
85
+ class SoilPowerSensorController(SerialController):
86
+ """Controller used to read values from the SPS"""
87
+
88
+ def __init__(self, port):
89
+ """Constructor
90
+
91
+ Opens serial connection and checks functionality
92
+
93
+ Parameters
94
+ ----------
95
+ port : str
96
+ Serial port of device
97
+ """
98
+ super().__init__(port)
99
+ self.check()
100
+
101
+ def get_power(self) -> Tuple[float, float]:
102
+ """Measure voltage from SPS
103
+
104
+ Returns
105
+ -------
106
+ tuple[float, float
107
+ voltage, current
108
+ """
109
+
110
+ self.ser.write(b"0") # send a command to the SPS to send a power measurment
111
+
112
+ # read a single byte for the length
113
+ resp_len_bytes = self.ser.read()
114
+
115
+ resp_len = int.from_bytes(resp_len_bytes)
116
+
117
+ reply = self.ser.read(resp_len) # read said measurment
118
+
119
+ meas_dict = decode_measurement(reply) # decode using protobuf
120
+
121
+ voltage_value = meas_dict["data"]["voltage"]
122
+ current_value = meas_dict["data"]["current"]
123
+
124
+ return float(voltage_value), float(current_value)
125
+
126
+ def check(self):
127
+ """Performs a check of the connection to the board
128
+
129
+ Raises
130
+ ------
131
+ RuntimeError
132
+ Checks that SPS replies "ok" when sent "check"
133
+ """
134
+
135
+ # needed sleep to get write to work
136
+ # possibly due to linux usb stack initialized or mcu waiting to startup
137
+ time.sleep(1)
138
+ self.ser.write(b"check\n")
139
+
140
+ reply = self.ser.readline()
141
+ # reply = reply.decode()
142
+ # reply = reply.strip("\r\n")
143
+
144
+ if reply != b"ok\n":
145
+ raise RuntimeError(f"SPS check failed. Reply received: {reply}")
146
+
147
+
148
+ class SMUSerialController(SerialController):
149
+ """Controller for the Keithley 2400 SMU used to supply known voltage to the
150
+ SPS
151
+
152
+ Uses SCPI (Standard Control of Programmable Instruments) to control the SMU
153
+ through its RS232 port. Written for the Keithley 2400 SMU, but should work
154
+ for any other SMU that uses SCPI.
155
+ """
156
+
157
+ class VoltageIterator:
158
+ """VoltageIterator Class
159
+
160
+ Implements a iterator for looping through voltage output values
161
+ """
162
+
163
+ def __init__(self, ser, start, stop, step):
164
+ """Constructor
165
+
166
+ Parameters
167
+ ----------
168
+ ser : serial.Serial
169
+ Initialised serial connection
170
+ start : float
171
+ Starting voltage
172
+ stop : float
173
+ End voltage
174
+ step : float
175
+ Voltage step
176
+ """
177
+
178
+ self.ser = ser
179
+ self.start = start
180
+ self.stop = stop
181
+ self.step = step
182
+
183
+ def __iter__(self):
184
+ """Iterator
185
+
186
+ Sets current value to start
187
+ """
188
+
189
+ self.v = None
190
+ self.ser.write(b":OUTP ON\n")
191
+ return self
192
+
193
+ def __next__(self):
194
+ """Next
195
+
196
+ Steps to next voltage level, stopping once stop is reached
197
+
198
+ Raises
199
+ ------
200
+ StopIteration
201
+ When the next step exceeds the stop level
202
+ """
203
+
204
+ if self.v is None:
205
+ return self.set_voltage(self.start)
206
+
207
+ v_next = self.v + self.step
208
+
209
+ if v_next <= self.stop:
210
+ return self.set_voltage(v_next)
211
+ else:
212
+ raise StopIteration
213
+
214
+ def set_voltage(self, v):
215
+ """Sets the voltage output"""
216
+
217
+ self.v = v
218
+ cmd = f":SOUR:VOLT:LEV {v}\n"
219
+ self.ser.write(bytes(cmd, "ascii"))
220
+ return self.v
221
+
222
+ def __init__(self, port, source_mode):
223
+ """Constructor
224
+
225
+ Opens serial port, restore to known defaults
226
+
227
+ Parameters
228
+ ----------
229
+ port : str
230
+ Serial port of device
231
+ """
232
+
233
+ super().__init__(port)
234
+ # Reset settings
235
+ self.ser.write(b"*RST\n")
236
+ # Voltage source
237
+ self.ser.write(b":SOUR:FUNC VOLT\n")
238
+ self.ser.write(b":SOUR:VOLT:MODE FIXED\n")
239
+ # 1mA compliance
240
+ self.ser.write(b":SENS:CURR:PROT 10e-3\n")
241
+ # Sensing functions
242
+ self.ser.write(b":SENS:CURR:RANGE:AUTO ON\n")
243
+ self.ser.write(b":SENS:FUNC:OFF:ALL\n")
244
+ self.ser.write(b':SENS:FUNC:ON "VOLT"\n')
245
+ self.ser.write(b':SENS:FUNC:ON "CURR"\n')
246
+
247
+ def __del__(self):
248
+ """Destructor
249
+
250
+ Turns off output
251
+ """
252
+
253
+ self.ser.write(b":OUTP OFF\n")
254
+
255
+ def vrange(self, start, stop, step) -> VoltageIterator:
256
+ """Gets iterator to range of voltages
257
+
258
+ Parameters
259
+ ----------
260
+ start : float
261
+ Starting voltage
262
+ stop : float
263
+ End voltage
264
+ step : float
265
+ Voltage step
266
+ """
267
+
268
+ return self.VoltageIterator(self.ser, start, stop, step)
269
+
270
+ def get_voltage(self) -> float:
271
+ """Measure voltage supplied to the SPS from SMU
272
+
273
+ Returns
274
+ -------
275
+ float
276
+ Measured voltage
277
+ """
278
+
279
+ self.ser.write(b":FORM:ELEM VOLT\n")
280
+ self.ser.write(b":READ?\n")
281
+ reply = self.ser.readline().decode()
282
+ reply = reply.strip("\r")
283
+ return float(reply)
284
+
285
+ def get_current(self) -> float:
286
+ """Measure current supplied to the SPS from SMU
287
+
288
+ Returns
289
+ -------
290
+ float
291
+ Measured current
292
+ """
293
+
294
+ self.ser.write(
295
+ b":FORM:ELEM CURR\n"
296
+ ) # replace with serial.write with socket.write commands, std library aviable, lots of example code
297
+ self.ser.write(b":READ?\n")
298
+ reply = self.ser.readline().decode()
299
+ reply = reply.strip("\r")
300
+ return float(reply)
301
+
302
+
303
+ class SMULANController(LANController):
304
+ """Controller for the Keithley 2400 SMU used to supply known voltage to the
305
+ SPS
306
+
307
+ Uses SCPI (Standard Control of Programmable Instruments) to control the SMU
308
+ through its RS232 port. Written for the Keithley 2400 SMU, but should work
309
+ for any other SMU that uses SCPI.
310
+ """
311
+
312
+ class VoltageIterator:
313
+ """VoltageIterator Class
314
+
315
+ Implements a iterator for looping through voltage output values
316
+ """
317
+
318
+ def __init__(self, sock, start, stop, step):
319
+ """Constructor
320
+
321
+ Parameters
322
+ ----------
323
+ sock : serial.Socket
324
+ Initialised socket connection
325
+ start : float
326
+ Starting voltage
327
+ stop : float
328
+ End voltage
329
+ step : float
330
+ Voltage step
331
+ """
332
+
333
+ self.sock = sock
334
+ self.start = start
335
+ self.stop = stop
336
+ self.step = step
337
+
338
+ def __iter__(self):
339
+ """Iterator
340
+
341
+ Sets current value to start
342
+ """
343
+
344
+ self.v = None
345
+ self.sock.sendall(b":OUTP ON\n")
346
+ return self
347
+
348
+ def __next__(self):
349
+ """Next
350
+
351
+ Steps to next voltage level, stopping once stop is reached
352
+
353
+ Raises
354
+ ------
355
+ StopIteration
356
+ When the next step exceeds the stop level
357
+ """
358
+
359
+ if self.v is None:
360
+ return self.set_voltage(self.start)
361
+
362
+ v_next = self.v + self.step
363
+
364
+ if v_next <= self.stop:
365
+ return self.set_voltage(v_next)
366
+ else:
367
+ raise StopIteration
368
+
369
+ def __len__(self):
370
+ """Len
371
+
372
+ The number of measurements points
373
+ """
374
+ return int((self.stop - self.start) / self.step) + 1
375
+
376
+ def set_voltage(self, v):
377
+ """Sets the voltage output"""
378
+
379
+ self.v = v
380
+ cmd = f":SOUR:VOLT:LEV {v}\n"
381
+ self.sock.sendall(bytes(cmd, "ascii"))
382
+ return self.v
383
+
384
+ class CurrentIterator:
385
+ """CurrentIterator Class
386
+
387
+ Implements a iterator for looping through current output values
388
+ """
389
+
390
+ def __init__(self, sock, start, stop, step):
391
+ """Constructor
392
+
393
+ Parameters
394
+ ----------
395
+ sock : serial.Socket
396
+ Initialised socket connection
397
+ start : float
398
+ Starting current
399
+ stop : float
400
+ End current
401
+ step : float
402
+ Current step
403
+ """
404
+ self.sock = sock
405
+ self.start = start
406
+ self.stop = stop
407
+ self.step = step
408
+
409
+ def __iter__(self):
410
+ """Iterator
411
+
412
+ Sets current value to start
413
+ """
414
+
415
+ self.i = None
416
+ self.sock.sendall(b":OUTP ON\n")
417
+ return self
418
+
419
+ def __next__(self):
420
+ """Next
421
+
422
+ Steps to next voltage level, stopping once stop is reached
423
+
424
+ Raises
425
+ ------
426
+ StopIteration
427
+ When the next step exceeds the stop level
428
+ """
429
+
430
+ if self.i is None:
431
+ return self.set_current(self.start)
432
+ i_next = self.i + self.step
433
+ if i_next <= self.stop:
434
+ return self.set_current(i_next)
435
+ else:
436
+ raise StopIteration
437
+
438
+ def __len__(self):
439
+ """Len
440
+
441
+ The number of measurements points
442
+ """
443
+ return int((self.stop - self.start) / self.step) + 1
444
+
445
+ def set_current(self, i):
446
+ """Sets the current output"""
447
+
448
+ self.i = i
449
+ cmd = f":SOUR:CURR:LEV {i}\n"
450
+ self.sock.sendall(bytes(cmd, "ascii"))
451
+ return self.i
452
+
453
+ def __init__(self, host, port):
454
+ """Constructor
455
+
456
+ Opens LAN connection and sets initial SMU configurations.
457
+
458
+ Parameters
459
+ ----------
460
+ host : str
461
+ IP address or hostname of the SMU
462
+ port : int
463
+ Port number used for the LAN connection
464
+ """
465
+
466
+ super().__init__(host, port)
467
+
468
+ def setup_voltage(self):
469
+ """Configures smu for sourcing voltage"""
470
+
471
+ self.sock.sendall(b"*RST\n")
472
+ # Voltage source
473
+ self.sock.sendall(b":SOUR:FUNC VOLT\n")
474
+ self.sock.sendall(b":SOUR:VOLT:MODE FIXED\n")
475
+ # 1mA compliance
476
+ self.sock.sendall(b":SENS:CURR:PROT 10e-3\n")
477
+ # Sensing functions
478
+ self.sock.sendall(b":SENS:CURR:RANGE:AUTO ON\n")
479
+ self.sock.sendall(b":SENS:FUNC:OFF:ALL\n")
480
+ self.sock.sendall(b':SENS:FUNC:ON "VOLT"\n')
481
+ self.sock.sendall(b':SENS:FUNC:ON "CURR"\n')
482
+
483
+ def setup_current(self):
484
+ """Configured smu for sourcing current"""
485
+
486
+ self.sock.sendall(b"*RST\n")
487
+ # Current source
488
+ self.sock.sendall(b":SOUR:FUNC CURR\n")
489
+ self.sock.sendall(b":SOUR:CURR:MODE FIXED\n")
490
+ # 1V compliance
491
+ self.sock.sendall(b":SENSE:CURR:PROT 1\n")
492
+ # Sensing functions
493
+ self.sock.sendall(b":SENS:VOLT:RANGE:AUTO ON\n")
494
+ self.sock.sendall(b":SENS:FUNC:OFF:ALL\n")
495
+ self.sock.sendall(b':SENS:FUNC:ON "CURR"\n')
496
+ self.sock.sendall(b':SENS:FUNC:ON "VOLT"\n')
497
+
498
+ def __del__(self):
499
+ """Destructor
500
+
501
+ Turns off output
502
+ """
503
+
504
+ self.sock.sendall(b":OUTP OFF\n")
505
+
506
+ def vrange(self, start, stop, step) -> VoltageIterator:
507
+ """Gets iterator to range of voltages
508
+
509
+ Parameters
510
+ ----------
511
+ start : float
512
+ Starting voltage
513
+ stop : float
514
+ End voltage
515
+ step : float
516
+ Voltage step
517
+ """
518
+
519
+ self.setup_voltage()
520
+ return self.VoltageIterator(self.sock, start, stop, step)
521
+
522
+ def irange(self, start, stop, step) -> CurrentIterator:
523
+ """Gets iterator to range of currents
524
+
525
+ Parameters
526
+ ----------
527
+ start : float
528
+ Starting current
529
+ stop : float
530
+ End current
531
+ step : float
532
+ Current step
533
+ """
534
+
535
+ self.setup_current()
536
+ return self.CurrentIterator(self.sock, start, stop, step)
537
+
538
+ def get_voltage(self) -> float:
539
+ """Measure voltage supplied to the SPS from SMU
540
+
541
+ Returns
542
+ -------
543
+ float
544
+ Measured voltage
545
+ """
546
+
547
+ self.sock.sendall(b":FORM:ELEM VOLT\n")
548
+ self.sock.sendall(b":READ?\n")
549
+ # receive response
550
+ reply = self.sock.recv(256)
551
+ # strip trailing \r\n characters
552
+ reply = reply.strip()
553
+ # convert to float and return
554
+ return float(reply)
555
+
556
+ def get_current(self) -> float:
557
+ """Measure current supplied to the SPS from SMU
558
+
559
+ Returns
560
+ -------
561
+ float
562
+ Measured current
563
+ """
564
+
565
+ self.sock.sendall(
566
+ b":FORM:ELEM CURR\n"
567
+ ) # replace with serial.write with socket.write commands, std library aviable, lots of example code
568
+ self.sock.sendall(b":READ?\n")
569
+ # receive response
570
+ reply = self.sock.recv(256)
571
+ # strip trailing \r\n characters
572
+ reply = reply.strip()
573
+ # convert to float and return
574
+ return float(reply)
575
+
576
+
577
+ class Recorder:
578
+ def __init__(self, serialport: str, host: str, port: int, delay: int = 1):
579
+ """Recorder constructor
580
+
581
+ Initializes the connection to smu and sps.
582
+
583
+ Args:
584
+ serialport: Serial port to sps
585
+ host: Hostname/ip of smu
586
+ port: TCP port to smu
587
+ delay: Delay between smu step and power measurement in seconds
588
+ """
589
+
590
+ self.sps = SoilPowerSensorController(serialport)
591
+ self.smu = SMULANController(host, port)
592
+ self.delay = delay
593
+
594
+ def record_voltage(
595
+ self, start: float, stop: float, step: float, samples: int
596
+ ) -> dict[str, list]:
597
+ """Records voltage measurements from the smu and sps
598
+
599
+ Input arguments given in volts units. Returned dictionary has keys for the
600
+ expected voltage (number from python), actual voltage (value measured by
601
+ the smu), and meas voltage (value measured by the sps).
602
+
603
+ Args:
604
+ start: Start voltage
605
+ stop: Stop voltage (inclusive)
606
+ step: Step between voltage
607
+ samples: Number of samples taken at each voltage
608
+
609
+ Returns:
610
+ Dictionary in the following pandas compatable format:
611
+ {
612
+ "expected": [],
613
+ "actual": [],
614
+ "meas": [],
615
+ }
616
+ """
617
+
618
+ data = {
619
+ "expected": [],
620
+ "actual": [],
621
+ "meas": [],
622
+ }
623
+
624
+ for value in tqdm(self.smu.vrange(start, stop, step)):
625
+ time.sleep(self.delay)
626
+ for _ in range(samples):
627
+ # expected
628
+ data["expected"].append(value)
629
+ # smu
630
+ data["actual"].append(self.smu.get_voltage())
631
+ # sps
632
+ sps_v, _ = self.sps.get_power()
633
+ data["meas"].append(sps_v)
634
+
635
+ return data
636
+
637
+ def record_current(
638
+ self, start: float, stop: float, step: float, samples: int
639
+ ) -> dict[str, list]:
640
+ """Records current measurements from the smu and sps
641
+
642
+ Input arguments given in amps units. Returned dictionary has keys for the
643
+ expected current (number from python), actual current (value measured by
644
+ the smu), and meas current (value measured by the sps).
645
+
646
+ Args:
647
+ start: Start current
648
+ stop: Stop current (inclusive)
649
+ step: Step between current
650
+ samples: Number of samples taken at each voltage
651
+
652
+ Returns:
653
+ Dictionary in the following pandas compatable format:
654
+ {
655
+ "expected": [],
656
+ "actual": [],
657
+ "meas": [],
658
+ }
659
+ """
660
+
661
+ data = {
662
+ "expected": [],
663
+ "actual": [],
664
+ "meas": [],
665
+ }
666
+
667
+ for value in tqdm(self.smu.irange(start, stop, step)):
668
+ time.sleep(self.delay)
669
+ for _ in range(samples):
670
+ # expected
671
+ data["expected"].append(value)
672
+ # smu
673
+ data["actual"].append(self.smu.get_current())
674
+ # sps
675
+ _, sps_i = self.sps.get_power()
676
+ data["meas"].append(sps_i)
677
+
678
+ return data
@@ -0,0 +1,9 @@
1
+ matplotlib
2
+ pandas
3
+ pyserial
4
+ tqdm
5
+ scikit-learn
6
+ rocketlogger
7
+ pyyaml
8
+ ents
9
+ protobuf==4.25.5