mmcb-rs232-avt 1.0.14__py3-none-any.whl → 1.0.18__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.
mmcb_rs232/psuset.py ADDED
@@ -0,0 +1,938 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Set the voltage on a power supply connected by RS232.
4
+
5
+ All power supplies supported by detect.py are usable by this script.
6
+
7
+ the script will return an appropriate unix return value on completion, so
8
+ the return value can be used by the shell, e.g.
9
+
10
+ ./psuset.py -50 && ./capture2.py -t 60
11
+ """
12
+
13
+ import argparse
14
+ import logging
15
+ import math
16
+ import sys
17
+ import threading
18
+ import time
19
+ import types
20
+
21
+ import serial
22
+ import numpy as np
23
+
24
+ from mmcb import common
25
+ from mmcb import lexicon
26
+ from mmcb import sequence
27
+
28
+
29
+ ##############################################################################
30
+ # command line option handler
31
+ ##############################################################################
32
+
33
+ def check_voltage(val):
34
+ """
35
+ check voltage range
36
+
37
+ --------------------------------------------------------------------------
38
+ args
39
+ val : float
40
+ bias voltage
41
+ --------------------------------------------------------------------------
42
+ returns : float
43
+ --------------------------------------------------------------------------
44
+ """
45
+ val = float(val)
46
+ if not -1100 <= val <= 1100:
47
+ raise argparse.ArgumentTypeError(
48
+ f'{val}: '
49
+ 'voltage value should be between -1100 and 1100')
50
+ return val
51
+
52
+
53
+ def check_settlingtime(val):
54
+ """
55
+ check settling time
56
+
57
+ --------------------------------------------------------------------------
58
+ args
59
+ val : float
60
+ settling time in seconds
61
+ --------------------------------------------------------------------------
62
+ returns : float
63
+ --------------------------------------------------------------------------
64
+ """
65
+ val = float(val)
66
+ if not 0.0 <= val <= 60.0:
67
+ raise argparse.ArgumentTypeError(
68
+ f'{val}: '
69
+ 'settling time value should be between 0 and 60 seconds')
70
+ return val
71
+
72
+
73
+ def check_arguments(settings):
74
+ """
75
+ handle command line options
76
+
77
+ --------------------------------------------------------------------------
78
+ args
79
+ settings : dictionary
80
+ contains core information about the test environment
81
+ --------------------------------------------------------------------------
82
+ returns : none
83
+ --------------------------------------------------------------------------
84
+ """
85
+ parser = argparse.ArgumentParser(
86
+ description='Set voltage and/or current limit of a single power\
87
+ supply channel connected by RS232. Currently supported PSUs are: Hameg\
88
+ (Rohde & Schwarz) HMP4040; Keithley 2410, 2614b; ISEG SHQ 222M, 224M;\
89
+ and Agilent E3647A, E3634A (the latter may also be branded Keysight\
90
+ or Hewlett-Packard). When multiple power supplies are\
91
+ connected, use command line options --manufacturer, --model,\
92
+ --serial, --channel and --port to identify the individual power\
93
+ supply. For brevity, specify the minimum number of identifying\
94
+ parameters to uniquely identify the device; if there is only one\
95
+ single-channel power supply attached, just specify the voltage to be\
96
+ set. Note that ISEG power supplies can take a long time to settle to\
97
+ final values, and at low voltages they may never converge to the set\
98
+ voltage.')
99
+ parser.add_argument(
100
+ 'voltage', nargs='*', metavar='voltage',
101
+ help='value in volts, values range -1100 to +1100',
102
+ type=check_voltage, default=None)
103
+ parser.add_argument(
104
+ '--manufacturer', nargs=1, metavar='manufacturer',
105
+ choices=['agilent', 'hameg', 'iseg', 'keithley'],
106
+ help='PSU manufacturer.',
107
+ default=None)
108
+ parser.add_argument(
109
+ '--model', nargs=1, metavar='model',
110
+ choices=['2410', '2614b', 'e3634a', 'e3647a', 'hmp4040', 'shq'],
111
+ help='PSU model.',
112
+ default=None)
113
+ parser.add_argument(
114
+ '--serial', nargs=1, metavar='serial',
115
+ help='PSU serial number. A part of the serial may be supplied if it\
116
+ is unique amongst connected devices.',
117
+ default=None)
118
+ parser.add_argument(
119
+ '--channel', nargs=1, metavar='channel',
120
+ choices=['1', '2', '3', '4', 'a', 'A', 'b', 'B', 'c', 'C', 'd', 'D'],
121
+ help='PSU channel number. These can be specified numerically or\
122
+ alphabetically.',
123
+ default=None)
124
+ parser.add_argument(
125
+ '--port', nargs=1, metavar='port',
126
+ help='Serial port identifier. This is useful where multiple Agilent/\
127
+ Keysight/Hewlett-Packard power supplies are connected, since they do\
128
+ not provide a serial number over RS232. A part of the serial may be\
129
+ supplied if it is unique amongst connected devices.',
130
+ default=None)
131
+ parser.add_argument(
132
+ '--reset',
133
+ action='store_true',
134
+ help='Reset power supply before setting voltage (Keithley 2410).')
135
+ parser.add_argument(
136
+ '-i', '--immediate',
137
+ action='store_true',
138
+ help='On high-voltage power supplies, set the specified voltage\
139
+ immediately. By default the voltage will ramp from the current\
140
+ value to the specified value (Keithley and ISEG). For low-voltage\
141
+ power supplies the specified voltage is always set immediately\
142
+ unless --peltier is used with the HMP4040.')
143
+ parser.add_argument(
144
+ '-l', '--limit', nargs=1, metavar='current_limit',
145
+ help='Set the power supply channel current limit.\
146
+ Values can be specified with either scientific (10e-9) or\
147
+ engineering notation (10n). Supported for Keithley 2410, 2614b;\
148
+ Rohde & Schwarz HMP4040. For the latter, the minimum value is 1mA.',
149
+ type=common.check_current)
150
+ parser.add_argument(
151
+ '-p', '--peltier',
152
+ action='store_true',
153
+ help='Use gradual voltage changes for HMP4040.')
154
+ parser.add_argument(
155
+ '-v', '--voltspersecond', nargs=1, metavar='rateofchange',
156
+ help='When not using --immediate, this option sets the rate of change\
157
+ in volts per second. The default is 10V/s (Keithley and ISEG).')
158
+ parser.add_argument(
159
+ '-s', '--settlingtime', nargs=1, metavar='settlingtime',
160
+ help='Custom settling time in seconds between asserting voltage\
161
+ and reading back. Default is 0.5s.',
162
+ type=check_settlingtime, default=[0.5])
163
+ parser.add_argument(
164
+ '--forwardbias',
165
+ action='store_true',
166
+ help='For HV supplies biassing detectors it is expected negative\
167
+ voltages will be used, so by default user requests for positive values\
168
+ will be suppressed, unless this option is used. Implemented for the\
169
+ Keithley 2410/2614b only.')
170
+
171
+ group1 = parser.add_mutually_exclusive_group()
172
+ group1.add_argument(
173
+ '-f', '--front',
174
+ action='store_true',
175
+ help='Use front output (Keithley 2410).')
176
+ group1.add_argument(
177
+ '-r', '--rear',
178
+ action='store_true',
179
+ help='Use rear output (Keithley 2410).')
180
+
181
+ group2 = parser.add_mutually_exclusive_group()
182
+ group2.add_argument(
183
+ '--on',
184
+ action='store_true',
185
+ help='Turn PSU (channel) output on, by default the script leaves the\
186
+ power supply output unchanged (Keithley and Agilent).')
187
+ group2.add_argument(
188
+ '--off',
189
+ action='store_true',
190
+ help='Turn PSU output off, by default the script leaves the power\
191
+ supply output unchanged (Keithley and Agilent).')
192
+
193
+ group3 = parser.add_mutually_exclusive_group()
194
+ group3.add_argument(
195
+ '--verbose',
196
+ action='store_true',
197
+ help='Display all power supply channels detected, and search terms\
198
+ supplied')
199
+ group3.add_argument(
200
+ '-q', '--quiet',
201
+ action='store_true',
202
+ help='Minimal text output during normal operation')
203
+
204
+ args = parser.parse_args()
205
+
206
+ settings['verbose'] = args.verbose
207
+ settings['quiet'] = args.quiet
208
+
209
+ # default: leave power supply settings unchanged
210
+ if args.front:
211
+ settings['rear'] = False
212
+ elif args.rear:
213
+ settings['rear'] = True
214
+
215
+ # default: leave power supply settings unchanged
216
+ if args.on:
217
+ settings['on'] = True
218
+ elif args.off:
219
+ settings['on'] = False
220
+
221
+ if args.reset:
222
+ settings['reset'] = args.reset
223
+
224
+ if args.immediate:
225
+ settings['immediate'] = args.immediate
226
+
227
+ if args.forwardbias:
228
+ settings['forwardbias'] = args.forwardbias
229
+
230
+ if args.peltier:
231
+ settings['peltier'] = args.peltier
232
+
233
+ # voltage will always be present
234
+ if args.voltage is not None and len(args.voltage) == 1:
235
+ settings['voltage'] = args.voltage[0]
236
+
237
+ if args.serial:
238
+ settings['serial'] = args.serial[0]
239
+
240
+ if args.channel:
241
+ settings['channel'] = args.channel[0].lower()
242
+
243
+ if args.manufacturer:
244
+ settings['manufacturer'] = args.manufacturer[0]
245
+
246
+ if args.model:
247
+ settings['model'] = args.model[0]
248
+
249
+ if args.port:
250
+ settings['port'] = args.port[0]
251
+
252
+ if args.voltspersecond:
253
+ settings['voltspersecond'] = int(args.voltspersecond[0])
254
+
255
+ if args.limit:
256
+ settings['current_limit'] = args.limit[0]
257
+
258
+ settings['settlingtime'] = args.settlingtime[0]
259
+
260
+
261
+ ##############################################################################
262
+ # set psu values
263
+ ##############################################################################
264
+
265
+ def read_measured_voltage(settings, pipeline, ser, dev):
266
+ """
267
+ read the voltage as measured at the psu output terminals
268
+
269
+ --------------------------------------------------------------------------
270
+ args
271
+ settings : dictionary
272
+ contains core information about the test environment
273
+ pipeline : instance of class Production
274
+ contains all the queues through which the production pipeline
275
+ processes communicate
276
+ ser : serial.Serial
277
+ reference for serial port
278
+ dev : instance of class Channel
279
+ contains details of a device and its serial port
280
+ --------------------------------------------------------------------------
281
+ returns
282
+ success : bool
283
+ --------------------------------------------------------------------------
284
+ """
285
+ measured_voltage = None
286
+
287
+ if dev.manufacturer == 'iseg':
288
+ command_string = lexicon.power(dev.model, 'read voltage', channel=dev.channel)
289
+ local_buffer = common.atomic_send_command_read_response(pipeline, ser, dev,
290
+ command_string)
291
+
292
+ measured_voltage = common.iseg_value_to_float(local_buffer)
293
+
294
+ elif dev.manufacturer == 'keithley':
295
+ command_string = lexicon.power(dev.model, 'read voltage', channel=dev.channel)
296
+ local_buffer = common.atomic_send_command_read_response(pipeline, ser, dev,
297
+ command_string)
298
+
299
+ if local_buffer is not None:
300
+ separator = ',' if dev.model == '2410' else None
301
+ items = (float(x) for x in local_buffer.split(separator))
302
+
303
+ try:
304
+ measured_voltage = next(items)
305
+ except (StopIteration, ValueError):
306
+ message = f'{dev.ident} problem reading measured voltage'
307
+ common.log_with_colour(logging.WARNING, message)
308
+
309
+ elif dev.manufacturer in {'agilent', 'hameg'}:
310
+ if dev.model == 'e3634a':
311
+ command_string = lexicon.power(dev.model, 'set remote')
312
+ common.send_command(pipeline, ser, dev, command_string)
313
+
314
+ command_string = lexicon.power(dev.model, 'read voltage', channel=dev.channel)
315
+ local_buffer = common.atomic_send_command_read_response(pipeline, ser, dev,
316
+ command_string)
317
+
318
+ try:
319
+ volt = float(local_buffer)
320
+ except ValueError:
321
+ pass
322
+ else:
323
+ measured_voltage = common.decimal_quantize(volt, settings['decimal_places'])
324
+
325
+ return measured_voltage
326
+
327
+
328
+ def check_measured_voltage(settings, pipeline, ser, dev, set_voltage):
329
+ """
330
+ compare the voltage as measured at the psu output terminals to the
331
+ given voltage
332
+
333
+ --------------------------------------------------------------------------
334
+ args
335
+ settings : dictionary
336
+ contains core information about the test environment
337
+ pipeline : instance of class Production
338
+ contains all the queues through which the production pipeline
339
+ processes communicate
340
+ ser : serial.Serial
341
+ reference for serial port
342
+ dev : instance of class Channel
343
+ contains details of a device and its serial port
344
+ set_voltage : decimal.Decimal
345
+ --------------------------------------------------------------------------
346
+ returns
347
+ success : bool
348
+ --------------------------------------------------------------------------
349
+ """
350
+ success = False
351
+ measured_voltage = read_measured_voltage(settings, pipeline, ser, dev)
352
+
353
+ # comparison
354
+ if measured_voltage is not None:
355
+ set_voltage = common.decimal_quantize(set_voltage, settings['decimal_places'])
356
+ measured_voltage = common.decimal_quantize(measured_voltage, settings['decimal_places'])
357
+
358
+ volt = common.si_prefix(measured_voltage)
359
+ common.log_with_colour(logging.INFO, f'measured: {volt}V')
360
+
361
+ if set_voltage == measured_voltage:
362
+ success = True
363
+
364
+ return success
365
+
366
+
367
+ def current_limit(settings, pipeline, ser, dev):
368
+ """
369
+ Set and check current limit on Hameg HMP4040, Keithley 2614b and 2410.
370
+
371
+ HMP4040 notes:
372
+
373
+ The PSU seems to support setting of values down to 100uA for current limit
374
+ values < 1A, and down to 1mA for current limit >= 1A. However, support
375
+ seems patchy. 0.9992A, 0.0012 and 1.2m set correctly, but 0.0002A, 0.0002
376
+ and 0.2m do not. So limit this to 1mA (3 decimal places) to avoid
377
+ confusion.
378
+
379
+ --------------------------------------------------------------------------
380
+ args
381
+ settings : dictionary
382
+ contains core information about the test environment
383
+ pipeline : instance of class Production
384
+ contains all the queues through which the production pipeline
385
+ processes communicate
386
+ ser : serial.Serial
387
+ reference for serial port
388
+ dev : instance of class Channel
389
+ contains details of a device and its serial port
390
+ --------------------------------------------------------------------------
391
+ returns : none
392
+ --------------------------------------------------------------------------
393
+ """
394
+ if dev.model not in ('hmp4040', '2614b', '2410'):
395
+ return
396
+
397
+ # set current limit
398
+ if dev.model == 'hmp4040':
399
+ dec_places = 3
400
+ compliance = f'{settings["current_limit"]:.{dec_places}f}'
401
+ else:
402
+ compliance = f'{settings["current_limit"]:e}'
403
+
404
+ command_string = lexicon.power(dev.model, 'set current limit',
405
+ compliance, channel=dev.channel)
406
+ common.send_command(pipeline, ser, dev, command_string)
407
+
408
+ # read back current limit from PSU
409
+ command_string = lexicon.power(dev.model, 'get current limit', channel=dev.channel)
410
+ response = common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
411
+ one_percent = 0.005
412
+ onehundred_microamps = 0.0001
413
+ close_enough = math.isclose(float(response), settings['current_limit'],
414
+ rel_tol=one_percent, abs_tol=onehundred_microamps)
415
+
416
+ # report current limit
417
+ message = ('requested current limit '
418
+ f'{common.si_prefix(settings["current_limit"])}A, '
419
+ f'set to {common.si_prefix(response)}A')
420
+ if close_enough:
421
+ common.log_with_colour(logging.INFO, message)
422
+ else:
423
+ common.log_with_colour(logging.WARNING, message)
424
+
425
+
426
+ def hmp4040_constant_current(ser, pipeline, dev):
427
+ """
428
+ Check if HMP4040 channel is configured for constant current.
429
+
430
+ --------------------------------------------------------------------------
431
+ args
432
+ ser : serial.Serial
433
+ reference for serial port
434
+ pipeline : instance of class Production
435
+ contains all the queues through which the production pipeline
436
+ processes communicate
437
+ dev : instance of class Channel
438
+ contains details of a device and its serial port
439
+ --------------------------------------------------------------------------
440
+ returns : bool
441
+ --------------------------------------------------------------------------
442
+ """
443
+ assert dev.manufacturer == 'hameg', 'function only callable for Hameg PSU'
444
+
445
+ command_string = lexicon.power(dev.model, 'read channel mode', channel=dev.channel)
446
+ register = common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
447
+
448
+ try:
449
+ regval = int(register)
450
+ except ValueError:
451
+ retval = False
452
+ else:
453
+ constant_curr = (regval & (1 << 0)) != 0
454
+ retval = constant_curr
455
+
456
+ return retval
457
+
458
+
459
+ def configure_lvpsu(settings, pipeline, ser, dev):
460
+ """
461
+ Set the power supply voltage and read back the value to confirm it has
462
+ been correctly set.
463
+
464
+ Handles: Agilent E3634A, E3647A; Hameg HMP4040.
465
+
466
+ For Agilent, though the low range is set by default, if the voltage value
467
+ submitted exceeds the range threshold by over a volt or so, the PSU will
468
+ automatically select the high range.
469
+
470
+ --------------------------------------------------------------------------
471
+ args
472
+ settings : dictionary
473
+ contains core information about the test environment
474
+ pipeline : instance of class Production
475
+ contains all the queues through which the production pipeline
476
+ processes communicate
477
+ ser : serial.Serial
478
+ reference for serial port
479
+ dev : instance of class Channel
480
+ contains details of a device and its serial port
481
+ --------------------------------------------------------------------------
482
+ returns
483
+ success : bool
484
+ True if the device was found, False otherwise
485
+ --------------------------------------------------------------------------
486
+ """
487
+ success = False
488
+
489
+ if settings['reset']:
490
+ if dev.manufacturer == 'hameg':
491
+ # reset to default conditions, output off
492
+ # allow device time to complete reset before sending more commands
493
+ command_string = lexicon.power(dev.model, 'reset')
494
+ common.send_command(pipeline, ser, dev, command_string)
495
+ common.log_with_colour(logging.INFO, 'reset')
496
+ time.sleep(0.2)
497
+ else:
498
+ message = f'{dev.manufacturer} {dev.model}: reset not supported'
499
+ common.log_with_colour(logging.WARNING, message)
500
+
501
+ # set and verify current limit
502
+ if settings['current_limit'] is not None:
503
+ current_limit(settings, pipeline, ser, dev)
504
+
505
+ # change output on/off if requested, otherwise do not change it
506
+ if settings['on'] is not None:
507
+ comtxt = 'output on' if settings['on'] else 'output off'
508
+ command_string = lexicon.power(dev.model, comtxt, channel=dev.channel)
509
+ common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
510
+ common.log_with_colour(logging.INFO, comtxt)
511
+
512
+ # set voltage
513
+ if settings['voltage'] is None:
514
+ # success is not modified before this point
515
+ return True
516
+
517
+ # constrain the voltage to be set to the given number of decimal places
518
+ voltage = common.decimal_quantize(settings['voltage'], settings['decimal_places'])
519
+
520
+ if voltage < 0:
521
+ message = f'{dev.manufacturer} {dev.model}: cannot use a negative voltage'
522
+ common.log_with_colour(logging.ERROR, message)
523
+ else:
524
+ message = f'voltage set to {common.si_prefix(voltage)}V'
525
+ common.log_with_colour(logging.INFO, message)
526
+
527
+ if dev.manufacturer == 'hameg' and settings['peltier']:
528
+ measured_voltage = read_measured_voltage(settings, pipeline, ser, dev)
529
+ if measured_voltage is not None:
530
+ transition_voltage(settings, pipeline, measured_voltage, voltage, ser, dev)
531
+ else:
532
+ common.set_psu_voltage(settings, pipeline, voltage, ser, dev)
533
+
534
+ # Agilent E3647A seems to require a minimum of half a second to apply
535
+ # the setting before any further RS232 interaction is reliable
536
+ time.sleep(0.5)
537
+
538
+ # Arguably the voltage set for the channel should be read back too, so
539
+ # for the case where the channel output is off, some confirmation that
540
+ # the value has been set correctly can be given to the user.
541
+ #
542
+ # check measured voltage (can only be read back if output is on)
543
+ if not common.report_output_status(ser, pipeline, dev):
544
+ # allow a little time for the voltage to settle before reading
545
+ time.sleep(settings['settlingtime'])
546
+
547
+ if dev.manufacturer == 'hameg' and hmp4040_constant_current(ser, pipeline, dev):
548
+ # When configured as a constant current source, the measured
549
+ # voltage will not match the set voltage
550
+ success = True
551
+ else:
552
+ success = check_measured_voltage(settings, pipeline, ser, dev, voltage)
553
+ else:
554
+ message = 'with PSU output switched off, set voltage cannot be read back'
555
+ common.log_with_colour(logging.WARNING, message)
556
+ common.log_with_colour(logging.WARNING, 'assuming all is well')
557
+ success = True
558
+
559
+ return success
560
+
561
+
562
+ def configure_hvpsu(settings, pipeline, ser, dev):
563
+ """
564
+ Amend power supply settings. If a change of voltage has been requested,
565
+ read back the value to confirm it has been correctly set.
566
+
567
+ Process requests relevant to the selected power supply in this order:
568
+
569
+ * reset
570
+ * set range
571
+ * select front/rear output
572
+ * output on/off
573
+ * set voltage
574
+
575
+ Handles: Keithley 2410, 2614b; ISEG SHQ.
576
+
577
+ --------------------------------------------------------------------------
578
+ args
579
+ settings : dictionary
580
+ contains core information about the test environment
581
+ pipeline : instance of class Production
582
+ contains all the queues through which the production pipeline
583
+ processes communicate
584
+ ser : serial.Serial
585
+ reference for serial port
586
+ dev : instance of class Channel
587
+ contains details of a device and its serial port
588
+ --------------------------------------------------------------------------
589
+ returns
590
+ success : bool
591
+ True if the device was found, False otherwise
592
+ --------------------------------------------------------------------------
593
+ """
594
+ success = False
595
+
596
+ # ------------------------------------------------------------------------
597
+ # protection against accidental forward bias operation
598
+ # ------------------------------------------------------------------------
599
+
600
+ if settings['voltage'] > 0 and not settings['forwardbias']:
601
+ common.log_with_colour(
602
+ logging.ERROR,
603
+ 'HV PSU positive voltage specified without --forwardbias'
604
+ )
605
+ return success
606
+
607
+ if settings['voltage'] < 0 and settings['forwardbias']:
608
+ common.log_with_colour(
609
+ logging.ERROR,
610
+ 'HV PSU negative voltage specified with --forwardbias'
611
+ )
612
+ return success
613
+
614
+ # ------------------------------------------------------------------------
615
+
616
+ if dev.manufacturer == 'keithley':
617
+ command_string = lexicon.power(dev.model, 'clear event registers')
618
+ common.send_command(pipeline, ser, dev, command_string)
619
+
620
+ if settings['reset']:
621
+ if dev.manufacturer == 'keithley' and dev.model == '2410':
622
+ # reset to default conditions, output off
623
+ # allow device time to complete reset before sending more commands
624
+ common.log_with_colour(logging.INFO, 'reset')
625
+ command_string = lexicon.power(dev.model, 'reset')
626
+ common.send_command(pipeline, ser, dev, command_string)
627
+ time.sleep(0.2)
628
+ else:
629
+ message = f'{dev.manufacturer} {dev.model}: reset not supported'
630
+ common.log_with_colour(logging.WARNING, message)
631
+
632
+ # set and verify current limit
633
+ if settings['current_limit'] is not None:
634
+ current_limit(settings, pipeline, ser, dev)
635
+
636
+ # set voltage
637
+ if settings['voltage'] is not None:
638
+ # as a general rule this script should avoid making changes to the
639
+ # user's power supply settings, since the context it's being used in
640
+ # may be unclear. However, for this project the highest voltage range
641
+ # will always be used, and it is not unreasonable to set it here.
642
+ #
643
+ # It would be more correct for 'configure range' to be performed
644
+ # immediately after a reset (above, selectively). Performing it here
645
+ # (always) is more friendly to the user, e.g. if the user has just
646
+ # switched on the power supply (it may well be in a low voltage range)
647
+ # and runs this script with a high voltage, it will generate a series
648
+ # of
649
+ # "settings conflict" errors (on the 2410) as the script tries to set
650
+ # voltages higher than the current range allows.
651
+ common.log_with_colour(logging.INFO, 'configure range')
652
+ command_string = lexicon.power(dev.model, 'configure range', channel=dev.channel)
653
+ common.send_command(pipeline, ser, dev, command_string)
654
+
655
+ # Change front/rear routing if requested, otherwise do not change it.
656
+ #
657
+ # Since changing this setting will turn the output off, it must be performed
658
+ # before processing any user request to turn the output on or off.
659
+ if settings['rear'] is not None:
660
+ if dev.manufacturer == 'keithley' and dev.model == '2410':
661
+ destination = 'REAR' if settings['rear'] else 'FRON'
662
+ command_string = lexicon.power(dev.model, 'set route', destination)
663
+ common.send_command(pipeline, ser, dev, command_string)
664
+ else:
665
+ message = f'{dev.manufacturer} {dev.model}: front/rear not supported'
666
+ common.log_with_colour(logging.WARNING, message)
667
+
668
+ # change output on/off if requested, otherwise do not change it
669
+ if settings['on'] is not None:
670
+ if dev.manufacturer == 'keithley':
671
+ comtxt = 'output on' if settings['on'] else 'output off'
672
+ common.log_with_colour(logging.INFO, comtxt)
673
+ command_string = lexicon.power(dev.model, comtxt, channel=dev.channel)
674
+ common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
675
+ else:
676
+ message = f'{dev.manufacturer} {dev.model}: output on/off not supported'
677
+ common.log_with_colour(logging.WARNING, message)
678
+
679
+ ##############################################################################
680
+ # set voltage
681
+ if settings['voltage'] is None:
682
+ # success is not modified before this point
683
+ return True
684
+
685
+ # constrain the voltage to be set to the given number of decimal places
686
+ voltage = common.decimal_quantize(settings['voltage'], settings['decimal_places'])
687
+ message = f'setting voltage: {common.si_prefix(voltage)}V'
688
+ common.log_with_colour(logging.INFO, message)
689
+
690
+ # set voltage
691
+ if settings['immediate']:
692
+ common.set_psu_voltage(settings, pipeline, voltage, ser, dev)
693
+ else:
694
+ measured_voltage = read_measured_voltage(settings, pipeline, ser, dev)
695
+ if measured_voltage is not None:
696
+ transition_voltage(settings, pipeline, measured_voltage, voltage, ser, dev)
697
+
698
+ # check set voltage (can only be read back if output is on)
699
+ if not common.report_output_status(ser, pipeline, dev):
700
+ # allow a little time for the voltage to settle before reading
701
+ time.sleep(settings['settlingtime'])
702
+ success = check_measured_voltage(settings, pipeline, ser, dev, voltage)
703
+ else:
704
+ # keithley 2410, 2614b
705
+ message = 'with PSU output switched off, set voltage cannot be read back'
706
+ common.log_with_colour(logging.WARNING, message)
707
+ common.log_with_colour(logging.WARNING, 'assuming all is well')
708
+ success = True
709
+
710
+ return success
711
+
712
+
713
+ def transition_voltage(settings, pipeline, initial_voltage, target_voltage, ser, dev):
714
+ """
715
+ Gradual voltage transitions.
716
+
717
+ --------------------------------------------------------------------------
718
+ args
719
+ settings : dictionary
720
+ contains core information about the test environment
721
+ pipeline : instance of class Production
722
+ contains all the queues through which the production pipeline
723
+ processes communicate
724
+ initial_voltage : float
725
+ set voltage at present
726
+ target_voltage : float
727
+ voltage to transition to
728
+ ser : serial.Serial
729
+ reference for serial port
730
+ dev : instance of class Channel
731
+ contains details of a device and its serial port
732
+ --------------------------------------------------------------------------
733
+ returns : none
734
+ --------------------------------------------------------------------------
735
+ """
736
+ if initial_voltage != target_voltage:
737
+ message = f'{dev.ident}, transitioning from {initial_voltage}V to {target_voltage}V'
738
+ common.log_with_colour(logging.INFO, message)
739
+
740
+ if dev.manufacturer == 'iseg':
741
+ # use power supply's internal ramp feature to avoid having manage
742
+ # the ISEG SHQ's tardy response time
743
+
744
+ # read voltage rate of change
745
+ command_string = lexicon.power(dev.model,
746
+ 'read max rate of change',
747
+ channel=dev.channel)
748
+ max_vroc = common.atomic_send_command_read_response(pipeline, ser, dev,
749
+ command_string)
750
+
751
+ # set voltage rate of change for ramp
752
+ if settings['voltspersecond'] is None:
753
+ volts_per_second = 10
754
+ else:
755
+ volts_per_second = settings['voltspersecond']
756
+
757
+ command_string = lexicon.power(dev.model,
758
+ 'set voltage max rate of change',
759
+ volts_per_second,
760
+ channel=dev.channel)
761
+ common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
762
+
763
+ # auto ramp will progress towards the given voltage at the given rate
764
+ command_string = lexicon.power(dev.model,
765
+ 'set voltage',
766
+ abs(target_voltage),
767
+ channel=dev.channel)
768
+
769
+ common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
770
+
771
+ # wait to reach given voltage
772
+ common.wait_for_voltage_to_stabilise(ser, pipeline, dev, target_voltage)
773
+
774
+ # revert voltage rate of change to original value
775
+ command_string = lexicon.power(dev.model,
776
+ 'set voltage max rate of change',
777
+ max_vroc,
778
+ channel=dev.channel)
779
+ common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
780
+
781
+ elif dev.manufacturer == 'keithley':
782
+ timestamp = None
783
+ duration = 1 if settings['voltspersecond'] is None else 10 / settings['voltspersecond']
784
+ step = 10
785
+
786
+ for voltage in sequence.to_original(initial_voltage, target_voltage, step):
787
+ timestamp = common.rate_limit(timestamp, duration)
788
+ common.set_psu_voltage(settings, pipeline, voltage, ser, dev)
789
+
790
+ common.log_with_colour(logging.INFO, f'{dev.ident}, transition complete')
791
+
792
+ elif dev.model == 'hmp4040':
793
+ # quick addition - special case for Peltier usage
794
+ # aim for 2V in 30s, 1.5s per 0.1V step
795
+ initial_voltage = float(initial_voltage)
796
+ target_voltage = float(target_voltage)
797
+ timestamp = None
798
+ step = 0.1 if target_voltage > initial_voltage else -0.1
799
+ duration = 30 / (2 / abs(step))
800
+
801
+ for voltage in np.arange(initial_voltage + step, target_voltage + step, step):
802
+ timestamp = common.rate_limit(timestamp, duration)
803
+ common.set_psu_voltage(settings, pipeline, voltage, ser, dev)
804
+
805
+
806
+ ##############################################################################
807
+ # main
808
+ ##############################################################################
809
+
810
+ def main():
811
+ """ set the voltage on a power supply connected by RS232 """
812
+
813
+ status = types.SimpleNamespace(success=0, unreserved_error_code=3)
814
+
815
+ success = False
816
+ settings = {
817
+ 'alias': None,
818
+ 'channel': None,
819
+ 'current_limit': None,
820
+ 'debug': None,
821
+ 'decimal_places': 2,
822
+ 'forwardbias': False,
823
+ 'immediate': False,
824
+ 'manufacturer': None,
825
+ 'model': None,
826
+ 'on': None,
827
+ 'peltier': False,
828
+ 'port': None,
829
+ 'quiet': False,
830
+ 'rear': None,
831
+ 'reset': False,
832
+ 'serial': None,
833
+ 'settlingtime': 0.5,
834
+ 'verbose': None,
835
+ 'voltage': None,
836
+ 'voltspersecond': 10
837
+ }
838
+
839
+ check_arguments(settings)
840
+
841
+ ##########################################################################
842
+ # enable logging to screen
843
+ ##########################################################################
844
+
845
+ logging.basicConfig(
846
+ level=logging.DEBUG,
847
+ format='%(asctime)s : %(levelname)s : %(message)s')
848
+
849
+ # enable logging to screen
850
+ console = logging.StreamHandler()
851
+ console.setLevel(logging.INFO)
852
+
853
+ # set logging time to UTC to match session timestamp
854
+ logging.Formatter.converter = time.gmtime
855
+
856
+ ##########################################################################
857
+ # filter power supplies from cache leaving just a single channel
858
+ ##########################################################################
859
+
860
+ psus = common.cache_read(['hvpsu', 'lvpsu'])
861
+ channels = common.ports_to_channels(settings, psus)
862
+
863
+ if settings['verbose']:
864
+ print('detected power supply channels:')
865
+ for channel in channels:
866
+ print(channel)
867
+ print('search terms:')
868
+ maxlen = max(len(x) for x in settings)
869
+ for setting, value in settings.items():
870
+ print(f'{setting:>{maxlen}} {value}')
871
+
872
+ if not common.unique(settings, psus, channels):
873
+ print('could not identify a single power supply channel')
874
+ print('check use of --manufacturer, --model, --serial, --port, and --channel')
875
+ sys.exit()
876
+
877
+ ##########################################################################
878
+ # set up resources for threads
879
+ #
880
+ # this is overkill since only one power supply channel will be used
881
+ # but does allow the same well-tested interface employed by other scripts
882
+ # in the suite to be used
883
+ ##########################################################################
884
+
885
+ class Production:
886
+ """ Locks to support threaded operation. """
887
+ portaccess = {port: threading.Lock()
888
+ for port in {channel.port for channel in channels}}
889
+
890
+ pipeline = Production()
891
+
892
+ ##########################################################################
893
+ # Check status of outputs and interlock (inhibit) on all power supplies
894
+ ##########################################################################
895
+
896
+ common.initial_power_supply_check(settings, pipeline, psus, channels, psuset=True)
897
+
898
+ ##########################################################################
899
+ # display details of selected power supply
900
+ ##########################################################################
901
+
902
+ try:
903
+ channel = channels[0]
904
+ except IndexError:
905
+ pass
906
+ else:
907
+ text = f'selected power supply: {channel.manufacturer} {channel.model}'
908
+ if channel.serial_number:
909
+ text += f' s.no. {channel.serial_number}'
910
+ if channel.channel:
911
+ text += f' channel {channel.channel}'
912
+ if settings['port'] is not None:
913
+ text += f' port {channel.port}'
914
+ common.log_with_colour(logging.INFO, text, quiet=settings['quiet'])
915
+
916
+ ##########################################################################
917
+ # set values on given power supply channel
918
+ ##########################################################################
919
+
920
+ setpsu = {'lvpsu': configure_lvpsu, 'hvpsu': configure_hvpsu}
921
+
922
+ with serial.Serial(port=channel.port) as ser:
923
+ ser.apply_settings(channel.config)
924
+ ser.reset_input_buffer()
925
+ ser.reset_output_buffer()
926
+ success = setpsu[channel.category](settings, pipeline, ser, channel)
927
+ if success:
928
+ common.log_with_colour(
929
+ logging.INFO, 'operation successful',
930
+ quiet=settings['quiet']
931
+ )
932
+
933
+ return status.success if success else status.unreserved_error_code
934
+
935
+
936
+ ##############################################################################
937
+ if __name__ == '__main__':
938
+ sys.exit(main())