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/psustat.py ADDED
@@ -0,0 +1,705 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Instantaneous snapshot of all power supply channels from devices connected
4
+ by FTDI USB to RS232 adaptors.
5
+
6
+ All power supplies supported by detect.py are usable by this script.
7
+ """
8
+
9
+ import argparse
10
+ import collections
11
+ import contextlib
12
+ import sys
13
+ import time
14
+ import threading
15
+
16
+ from mmcb import common
17
+ from mmcb import lexicon
18
+
19
+
20
+ ##############################################################################
21
+ # command line option handler
22
+ ##############################################################################
23
+
24
+ def check_arguments(settings):
25
+ """
26
+ handle command line options
27
+
28
+ --------------------------------------------------------------------------
29
+ args : none
30
+ --------------------------------------------------------------------------
31
+ returns : none
32
+ --------------------------------------------------------------------------
33
+ """
34
+ parser = argparse.ArgumentParser(
35
+ description='Provides an instantaneous snapshot of the\
36
+ configuration and status of power supply channels from all Keithley\
37
+ 2410, 2614b; ISEG SHQ 222M, 224M; Agilent e3647a, e3634a; and Hameg\
38
+ (Rohde & Schwarz) HMP4040 power supplies connected by FTDI USB to\
39
+ RS232 adaptors.')
40
+ parser.add_argument(
41
+ '--manufacturer', nargs=1, metavar='manufacturer',
42
+ choices=['agilent', 'hameg', 'iseg', 'keithley'],
43
+ help='PSU manufacturer.',
44
+ default=None)
45
+ parser.add_argument(
46
+ '--model', nargs=1, metavar='model',
47
+ choices=['2410', '2614b', 'e3634a', 'e3647a', 'hmp4040', 'shq'],
48
+ help='PSU model.',
49
+ default=None)
50
+ parser.add_argument(
51
+ '--serial', nargs=1, metavar='serial',
52
+ help='PSU serial number. A part of the serial may be supplied if it\
53
+ is unique amongst connected devices.',
54
+ default=None)
55
+ parser.add_argument(
56
+ '--channel', nargs=1, metavar='channel',
57
+ choices=['1', '2', '3', '4', 'a', 'A', 'b', 'B', 'c', 'C', 'd', 'D'],
58
+ help='PSU channel number. These can be specified numerically or\
59
+ alphabetically.',
60
+ default=None)
61
+ parser.add_argument(
62
+ '--port', nargs=1, metavar='port',
63
+ help='Serial port identifier. This is useful where multiple Agilent/\
64
+ Keysight/Hewlett-Packard power supplies are connected, since they do\
65
+ not provide a serial number over RS232. A part of the serial may be\
66
+ supplied if it is unique amongst connected devices.',
67
+ default=None)
68
+
69
+ group1 = parser.add_mutually_exclusive_group()
70
+ group1.add_argument(
71
+ '--sv', action='store_true',
72
+ help='NOT IMPLEMENTED YET. Read set voltage.\
73
+ Only valid if the user has specified a PSU channel.',
74
+ )
75
+ group1.add_argument(
76
+ '--mv', action='store_true',
77
+ help='Read measured voltage. Only valid if the user has specified a PSU channel.',
78
+ )
79
+ group1.add_argument(
80
+ '--si', action='store_true',
81
+ help='NOT IMPLEMENTED YET. Read set current.\
82
+ Only valid if the user has specified a PSU channel.',
83
+ )
84
+ group1.add_argument(
85
+ '--mi', action='store_true',
86
+ help='Read measured current. Only valid if the user has specified a PSU channel.',
87
+ )
88
+
89
+ args = parser.parse_args()
90
+
91
+ if args.serial:
92
+ settings['serial'] = args.serial[0]
93
+
94
+ if args.channel:
95
+ settings['channel'] = args.channel[0].lower()
96
+
97
+ if args.manufacturer:
98
+ settings['manufacturer'] = args.manufacturer[0]
99
+
100
+ if args.model:
101
+ settings['model'] = args.model[0]
102
+
103
+ if args.port:
104
+ settings['port'] = args.port[0]
105
+
106
+ select = [
107
+ settings['serial'],
108
+ settings['channel'],
109
+ settings['manufacturer'],
110
+ settings['model'],
111
+ settings['port'],
112
+ ]
113
+ settings['filter'] = any(value is not None for value in select)
114
+
115
+ requests = collections.Counter([args.sv, args.mv, args.si, args.mi])
116
+
117
+ if settings['filter'] and requests[True] == 0:
118
+ print('select parameter to report with --sv, --mv, --si, --mi')
119
+ sys.exit()
120
+
121
+ settings['sv'] = args.sv
122
+ settings['mv'] = args.mv
123
+ settings['si'] = args.si
124
+ settings['mi'] = args.mi
125
+
126
+
127
+ ##############################################################################
128
+ # utilities
129
+ ##############################################################################
130
+
131
+ def hmp4040_constant_voltage_current_status(ser, pipeline, dev, tcolours):
132
+ """
133
+ Query HMP4040 channel for constant voltage/current configuration.
134
+
135
+ --------------------------------------------------------------------------
136
+ args
137
+ ser : serial.Serial
138
+ reference for serial port
139
+ pipeline : instance of class Production
140
+ contains all the queues through which the production pipeline
141
+ processes communicate
142
+ dev : instance of class Channel
143
+ contains details of a device and its serial port
144
+ tcolours : class
145
+ contains ANSI colour escape sequences
146
+ --------------------------------------------------------------------------
147
+ returns : string
148
+ --------------------------------------------------------------------------
149
+ """
150
+ assert dev.manufacturer == 'hameg', 'function only callable for Hameg PSU'
151
+
152
+ command_string = lexicon.power(dev.model, 'read channel mode', channel=dev.channel)
153
+ register = common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
154
+
155
+ status_categories = {
156
+ # constant_voltage, constant_current
157
+ (False, False): 'neither constant voltage nor constant current',
158
+ (False, True): f'{tcolours.BOLD}{tcolours.FG_BRIGHT_RED}constant current{tcolours.ENDC}',
159
+ (True, False): f'{tcolours.BOLD}{tcolours.FG_BRIGHT_GREEN}constant voltage{tcolours.ENDC}',
160
+ (True, True): 'both constant voltage and constant current (!)',
161
+ None: 'could not be determined'}
162
+
163
+ configuration = None
164
+
165
+ try:
166
+ regval = int(register)
167
+ except ValueError:
168
+ pass
169
+ else:
170
+ constant_curr = (regval & (1 << 0)) != 0
171
+ constant_volt = (regval & (1 << 1)) != 0
172
+ configuration = (constant_volt, constant_curr)
173
+
174
+ return status_categories[configuration]
175
+
176
+
177
+ def simple_current_limit(ser, pipeline, dev, eng_format=True):
178
+ """
179
+ Query current limit information for HMP4040, 2410, 2614b.
180
+
181
+ --------------------------------------------------------------------------
182
+ args
183
+ ser : serial.Serial
184
+ reference for serial port
185
+ pipeline : instance of class Production
186
+ contains all the queues through which the production pipeline
187
+ processes communicate
188
+ dev : instance of class Channel
189
+ contains details of a device and its serial port
190
+ --------------------------------------------------------------------------
191
+ returns : string
192
+ --------------------------------------------------------------------------
193
+ """
194
+ message = None
195
+
196
+ if dev.model in ('hmp4040', '2410', '2614b'):
197
+ command_string = lexicon.power(dev.model, 'get current limit', channel=dev.channel)
198
+ response = common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
199
+ if eng_format:
200
+ with contextlib.suppress(ValueError):
201
+ message = f'{common.si_prefix(response)}A'
202
+ else:
203
+ with contextlib.suppress(ValueError):
204
+ message = float(response)
205
+
206
+ return message
207
+
208
+
209
+ def simple_output_status(ser, pipeline, dev, tcolours):
210
+ """
211
+ Return the on/off output status of the given power supply channel.
212
+
213
+ Values returned in variable output:
214
+
215
+ +------------------+-----------+---------------+---------------+
216
+ | dev.manufacturer | dev.model | output OFF | output ON |
217
+ +------------------+-----------+---------------+---------------+
218
+ | 'agilent' | 'e3634a' | '0' | '1' |
219
+ | 'agilent' | 'e3647a' | '0' | '1' |
220
+ | 'hameg' | 'hmp4040' | '0' | '1' |
221
+ | 'iseg' | 'shq' | '=OFF' | '=ON' |
222
+ | 'keithley' | '2410' | '0' | '1' |
223
+ | 'keithley' | '2614b' | '0.00000e+00' | '1.00000e+00' |
224
+ +------------------+-----------+---------------+---------------+
225
+
226
+ --------------------------------------------------------------------------
227
+ args
228
+ ser : serial.Serial
229
+ reference for serial port
230
+ pipeline : instance of class Production
231
+ contains all the queues through which the production pipeline
232
+ processes communicate
233
+ dev : instance of class Channel
234
+ contains details of a device and its serial port
235
+ tcolours : class
236
+ contains ANSI colour escape sequences
237
+ --------------------------------------------------------------------------
238
+ returns : string
239
+ --------------------------------------------------------------------------
240
+ """
241
+ command_string = lexicon.power(dev.model, 'check output', channel=dev.channel)
242
+ output = common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
243
+
244
+ status_categories = {
245
+ True: f'{tcolours.BOLD}on{tcolours.ENDC}',
246
+ False: 'off',
247
+ None: 'could not be determined'}
248
+
249
+ channel_on = None
250
+
251
+ if dev.manufacturer == 'iseg':
252
+ if any(x in output for x in ['=ON', '=OFF']):
253
+ channel_on = '=ON' in output
254
+
255
+ elif dev.manufacturer in {'agilent', 'hameg', 'keithley'}:
256
+ try:
257
+ outval = int(float(output))
258
+ except ValueError:
259
+ pass
260
+ else:
261
+ if outval in {0, 1}:
262
+ channel_on = outval == 1
263
+
264
+ return status_categories[channel_on]
265
+
266
+
267
+ def simple_interlock_status(ser, pipeline, dev, tcolours):
268
+ """
269
+ Check status of power supply interlock.
270
+
271
+ This is per-PSU on Keithley, per-channel on ISEG.
272
+
273
+ --------------------------------------------------------------------------
274
+ args
275
+ ser : serial.Serial
276
+ reference for serial port
277
+ pipeline : instance of class Production
278
+ contains all the queues through which the production pipeline
279
+ processes communicate
280
+ dev : instance of class Channel
281
+ contains details of a device and its serial port
282
+ tcolours : class
283
+ contains ANSI colour escape sequences
284
+ --------------------------------------------------------------------------
285
+ returns : string
286
+ --------------------------------------------------------------------------
287
+ """
288
+ assert dev.manufacturer not in {'agilent', 'hameg'},\
289
+ 'function not callable for Agilent or Hameg PSU'
290
+
291
+ status_categories = {
292
+ True: 'active',
293
+ False: f'{tcolours.BOLD}inactive{tcolours.ENDC}',
294
+ None: 'could not be determined'}
295
+
296
+ interlock_set = None
297
+
298
+ command_string = lexicon.power(dev.model, 'check interlock', channel=dev.channel)
299
+ register = common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
300
+
301
+ if dev.manufacturer == 'iseg':
302
+ # Interlocks are per-channel for ISEG SHQ.
303
+ interlock_set = '=INH' in register
304
+ else:
305
+ # Interlocks are per-PSU for Keithley.
306
+ try:
307
+ regval = int(float(register))
308
+ except ValueError:
309
+ pass
310
+ else:
311
+ if dev.model == '2614b':
312
+ # bit 11 (status.measurement.INTERLOCK) p.11-280 (648)
313
+ # Without hardware interlock: '0.00000e+00'
314
+ # with hardware interlock: '2.04800e+03'
315
+ interlock_set = regval & 2048 == 0
316
+ elif dev.model == '2410':
317
+ if regval in {0, 1}:
318
+ # p.18-9 (389)
319
+ # returns '0' (disabled - no restrictions) or '1' (enabled)
320
+ interlock_set = register == 0
321
+
322
+ return status_categories[interlock_set]
323
+
324
+
325
+ def simple_polarity_status(ser, pipeline, dev, tcolours):
326
+ """
327
+ Returned string from 'check module status' command appears to be base 10.
328
+
329
+ e.g.
330
+ inhibit is bit 5 (0=inactive, 1=active),
331
+ polarity is bit 2 (0=negative, 1=positive)
332
+
333
+ For channel 1, with inhibit active (terminator removed from front
334
+ panel inhibit socket) and the polarity switch set to POS on the
335
+ rear panel, the 'S1' command returns '036'
336
+
337
+ e.g.
338
+ polarity set to positive: '004', negative: '000'
339
+
340
+ --------------------------------------------------------------------------
341
+ args
342
+ ser : serial.Serial
343
+ reference for serial port
344
+ pipeline : instance of class Production
345
+ contains all the queues through which the production pipeline
346
+ processes communicate
347
+ dev : instance of class Channel
348
+ contains details of a device and its serial port
349
+ tcolours : class
350
+ contains ANSI colour escape sequences
351
+ --------------------------------------------------------------------------
352
+ returns : string
353
+ --------------------------------------------------------------------------
354
+ """
355
+ assert dev.model == 'shq', 'function only callable for ISEG SHQ PSU'
356
+
357
+ status_categories = {
358
+ True: 'positive',
359
+ False: 'negative',
360
+ None: f'{tcolours.BG_RED}{tcolours.FG_WHITE}could not be determined{tcolours.ENDC}'}
361
+
362
+ polarity_positive = None
363
+
364
+ command_string = lexicon.power(dev.model, 'check module status', channel=dev.channel)
365
+ register = common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
366
+
367
+ with contextlib.suppress(ValueError):
368
+ polarity_positive = int(register) & 4 != 0
369
+
370
+ return status_categories[polarity_positive]
371
+
372
+
373
+ def power_supply_channel_status(settings, pipeline, psus, channels, tcolours):
374
+ """
375
+ Establishes RS232 communications with power supplies (as required).
376
+ Checks status of channel outputs and interlocks (inhibits).
377
+
378
+ --------------------------------------------------------------------------
379
+ args
380
+ settings : dictionary
381
+ contains core information about the test environment
382
+ pipeline : instance of class Production
383
+ contains all the queues through which the production pipeline
384
+ processes communicate
385
+ psus : dict
386
+ {port: ({port_config}, device_type, device_serial_number), ...}
387
+ contents of the cache filtered by hvpsu category
388
+ channels : list
389
+ contains instances of class Channel, one for each
390
+ power supply channel
391
+ tcolours : class
392
+ contains ANSI colour escape sequences
393
+ --------------------------------------------------------------------------
394
+ returns
395
+ channels : list
396
+ no explicit return, mutable type amended in place
397
+ --------------------------------------------------------------------------
398
+ """
399
+ port_used = collections.defaultdict(int)
400
+
401
+ # this function call may change the value of variable channels
402
+ # and returns a dict containing a mapping of ports to the serial ports
403
+ # which have been left open
404
+ spd = common.check_ports_accessible(psus, channels, close_after_check=False)
405
+
406
+ for dev in channels:
407
+ read_values = []
408
+
409
+ ######################################################################
410
+ # identify channel
411
+ ######################################################################
412
+
413
+ if not settings['filter']:
414
+ print()
415
+ print(dev)
416
+
417
+ # specify already opened serial port
418
+ ser = spd[dev.port]
419
+
420
+ # try to ensure consistent state
421
+ # clear FTDI output buffer state before sending
422
+ ser.reset_output_buffer()
423
+
424
+ # clear PSU state
425
+ if dev.model == '2614b':
426
+ command_string = lexicon.power(dev.model, 'terminator only',
427
+ channel=dev.channel)
428
+ common.send_command(pipeline, ser, dev, command_string)
429
+
430
+ # clear FTDI input buffer state
431
+ ser.reset_input_buffer()
432
+
433
+ # arbitrary settle time before proceeding
434
+ time.sleep(0.0)
435
+
436
+ # ensure serial port communication with PSU
437
+ # this is for ISEG SHQ only, on a per-PSU basis
438
+ if not port_used[dev.port]:
439
+ common.synchronise_psu(ser, pipeline, dev)
440
+
441
+ ##################################################################
442
+ # determine interlock status
443
+ ##################################################################
444
+
445
+ if not settings['filter']:
446
+ # this is per-PSU on Keithley, per-channel on ISEG
447
+ # still report it per-channel either way.
448
+ if dev.manufacturer not in {'agilent', 'hameg'}:
449
+ print(f'interlock status: {simple_interlock_status(ser, pipeline, dev, tcolours)}')
450
+
451
+ ##################################################################
452
+ # determine output status
453
+ ##################################################################
454
+
455
+ out_stat = simple_output_status(ser, pipeline, dev, tcolours)
456
+ set_volt = read_psu_set_voltage(pipeline, ser, dev)
457
+
458
+ if settings['filter']:
459
+ if settings['sv']:
460
+ print(set_volt)
461
+ else:
462
+ print(f'output status: {out_stat}')
463
+
464
+ if set_volt is None:
465
+ sv_text = 'could not be determined'
466
+ else:
467
+ sv_text = f'{common.si_prefix(set_volt)}V'
468
+ read_values.append(f'set voltage: {sv_text}')
469
+
470
+ ##################################################################
471
+ # determine current limit
472
+ ##################################################################
473
+
474
+ current_limit = simple_current_limit(ser, pipeline, dev)
475
+ if current_limit is not None:
476
+ if settings['filter']:
477
+ if settings['si']:
478
+ print(
479
+ simple_current_limit(ser, pipeline, dev, eng_format=False)
480
+ )
481
+ else:
482
+ print(f'current limit: {current_limit}')
483
+
484
+ ##################################################################
485
+ # report measured values
486
+ ##################################################################
487
+
488
+ if 'on' in out_stat:
489
+ mvolt, mcurr = read_psu_vi(pipeline, ser, dev)
490
+ if mvolt is None:
491
+ mvi_text = 'could not be determined'
492
+ else:
493
+ if not settings['filter']:
494
+ mvi_text = f'{common.si_prefix(mvolt)}V, {common.si_prefix(mcurr)}A'
495
+ else:
496
+ if settings['mi']:
497
+ print(mcurr)
498
+ elif settings['mv']:
499
+ print(mvolt)
500
+
501
+ if not settings['filter']:
502
+ read_values.append(f'measured: {mvi_text}')
503
+
504
+ else:
505
+ if settings['filter']:
506
+ if settings['mi'] or settings['mv']:
507
+ print('None')
508
+ else:
509
+ print('None')
510
+
511
+ ##################################################################
512
+ # determine polarity status
513
+ ##################################################################
514
+
515
+ if not settings['filter']:
516
+ if dev.manufacturer == 'iseg':
517
+ print(f'polarity: {simple_polarity_status(ser, pipeline, dev, tcolours)}')
518
+
519
+ ##################################################################
520
+ # determine constant voltage/current operation
521
+ ##################################################################
522
+
523
+ if not settings['filter']:
524
+ if dev.manufacturer == 'hameg':
525
+ print(f'mode: {hmp4040_constant_voltage_current_status(ser, pipeline, dev, tcolours)}')
526
+
527
+ ser.reset_input_buffer()
528
+ ser.reset_output_buffer()
529
+
530
+ if not settings['filter']:
531
+ print(', '.join(read_values))
532
+
533
+ port_used[dev.port] += 1
534
+
535
+ # close all the serial ports left open by the call above to
536
+ # common.check_ports_accessible()
537
+ for serial_port in spd.values():
538
+ serial_port.close()
539
+
540
+
541
+ def read_psu_vi(pipeline, ser, dev):
542
+ """
543
+ read measured voltage and current from PSU
544
+
545
+ --------------------------------------------------------------------------
546
+ args
547
+ pipeline : instance of class Production
548
+ contains all the queues through which the production pipeline
549
+ processes communicate
550
+ ser : serial.Serial
551
+ reference for serial port
552
+ dev : instance of class Channel
553
+ contains details of a device and its serial port
554
+ --------------------------------------------------------------------------
555
+ returns
556
+ dev : instance of class Channel
557
+ contains details of a device and its serial port
558
+ readings : list
559
+ [(float, float)]
560
+ --------------------------------------------------------------------------
561
+ """
562
+ if dev.manufacturer in {'keithley', 'iseg'}:
563
+ volt, curr = common.read_psu_measured_vi(pipeline, ser, dev)
564
+ elif dev.manufacturer in {'agilent', 'hameg', 'hewlett-packard'}:
565
+ if dev.model == 'e3634a':
566
+ command_string = lexicon.power(dev.model, 'set remote')
567
+ common.send_command(pipeline, ser, dev, command_string)
568
+
569
+ command_string = lexicon.power(dev.model, 'read voltage', channel=dev.channel)
570
+ measured_voltage = common.atomic_send_command_read_response(pipeline, ser,
571
+ dev, command_string)
572
+
573
+ command_string = lexicon.power(dev.model, 'read current', channel=dev.channel)
574
+ measured_current = common.atomic_send_command_read_response(pipeline, ser,
575
+ dev, command_string)
576
+ try:
577
+ volt = float(measured_voltage)
578
+ curr = float(measured_current)
579
+ except ValueError:
580
+ volt = curr = None
581
+
582
+ # ignore readings if output is off
583
+ if dev.manufacturer == 'hameg':
584
+ command_string = lexicon.power(dev.model, 'check output', channel=dev.channel)
585
+ output_status = common.atomic_send_command_read_response(pipeline, ser,
586
+ dev, command_string)
587
+ if output_status == '0':
588
+ volt = curr = None
589
+
590
+ else:
591
+ volt = curr = None
592
+
593
+ return volt, curr
594
+
595
+
596
+ def read_psu_set_voltage(pipeline, ser, dev):
597
+ """
598
+ Read the voltage that the PSU has been asked to output (rather than the
599
+ actual instantaneous voltage measured at the output terminals).
600
+
601
+ --------------------------------------------------------------------------
602
+ args
603
+ pipeline : instance of class Production
604
+ contains all the queues through which the production pipeline
605
+ processes communicate
606
+ ser : serial.Serial
607
+ reference for serial port
608
+ dev : instance of class Channel
609
+ contains details of a device and its serial port
610
+ --------------------------------------------------------------------------
611
+ returns
612
+ set_volt : float or None
613
+ float if the value could be read or None if it could not
614
+ --------------------------------------------------------------------------
615
+ """
616
+ set_volt = None
617
+
618
+ command_string = lexicon.power(dev.model, 'read set voltage', channel=dev.channel)
619
+ local_buffer = common.atomic_send_command_read_response(pipeline, ser, dev, command_string)
620
+
621
+ if dev.manufacturer == 'iseg' and dev.model == 'shq':
622
+ set_volt = common.iseg_value_to_float(local_buffer)
623
+
624
+ elif dev.manufacturer == 'keithley' and dev.model in {'2410', '2614b'}:
625
+ if dev.model == '2410':
626
+ # e.g. '-5.000000E+00,-1.005998E-10,+9.910000E+37,+1.185742E+04,+2.150800E+04'
627
+ item = next(iter(local_buffer.split(',')))
628
+ else:
629
+ # e.g. '-5.00000e+00'
630
+ item = local_buffer
631
+
632
+ with contextlib.suppress(TypeError, ValueError):
633
+ set_volt = float(item)
634
+
635
+ elif dev.manufacturer == 'hameg':
636
+ with contextlib.suppress(ValueError):
637
+ set_volt = float(local_buffer)
638
+
639
+ return set_volt
640
+
641
+
642
+ ##############################################################################
643
+ # main
644
+ ##############################################################################
645
+
646
+ def main():
647
+ """
648
+ Instantaneous snapshot of all power supply channels from devices
649
+ connected by FTDI USB to RS232 adaptors.
650
+ """
651
+ settings = {
652
+ 'alias': None,
653
+ 'channel': None,
654
+ 'manufacturer': None,
655
+ 'model': None,
656
+ 'port': None,
657
+ 'serial': None,
658
+ 'time': None,
659
+ }
660
+
661
+ check_arguments(settings)
662
+
663
+ ##########################################################################
664
+ # read cache
665
+ ##########################################################################
666
+
667
+ psus = common.cache_read(['hvpsu', 'lvpsu'], quiet=settings['filter'])
668
+ channels = common.ports_to_channels(settings, psus)
669
+
670
+ if settings['filter'] and not common.unique(settings, psus, channels):
671
+ print('could not identify a single power supply channel')
672
+ print('check use of --manufacturer, --model, --serial, --port, and --channel')
673
+ sys.exit()
674
+
675
+ ##########################################################################
676
+ # set up resources for threads
677
+ ##########################################################################
678
+
679
+ class Production:
680
+ """
681
+ Queues and locks to support threaded operation.
682
+
683
+ RS232 will not tolerate concurrent access. portaccess is used to
684
+ prevent more than one thread trying to write to the same RS232 port at
685
+ the same time for multi-channel power supplies. For simplicity, locks
686
+ are created for all power supplies, even if they only have a single
687
+ channel.
688
+ """
689
+ portaccess = {port: threading.Lock()
690
+ for port in {channel.port for channel in channels}}
691
+
692
+ pipeline = Production()
693
+
694
+ ##########################################################################
695
+ # Check status of outputs and interlock (inhibit) on all power supplies
696
+ ##########################################################################
697
+
698
+ power_supply_channel_status(
699
+ settings, pipeline, psus, channels, common.ANSIColours
700
+ )
701
+
702
+
703
+ ##############################################################################
704
+ if __name__ == '__main__':
705
+ main()