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/detect.py ADDED
@@ -0,0 +1,1053 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Detect devices present on the serial ports, that are attached via
4
+ FTDI USB to RS232 adaptors. This will write a cache file that can
5
+ be used by the other utility scripts.
6
+
7
+ The script will recognise the following devices, and more than one device of
8
+ each type can be detected:
9
+
10
+ controller : Newport ESP300, MM4006 and SMC100
11
+ lvpsu : Agilent E3647A, E3634A, Hameg (Rohde & Schwarz) HMP4040
12
+ hvpsu : Keithley 2410, 2614b; ISEG SHQ 222M, 224M
13
+
14
+ The terminator should set to be the same on all devices to make sure each
15
+ device can cleanly reject commands intended for other devices. <CRLF> is
16
+ expected.
17
+ """
18
+
19
+ import argparse
20
+ import collections
21
+ import concurrent.futures as cf
22
+ import functools
23
+ import itertools
24
+ import json
25
+ import sys
26
+ import time
27
+
28
+ import serial
29
+ import serial.tools.list_ports as stlp
30
+
31
+ from mmcb import common
32
+ from mmcb import lexicon
33
+
34
+
35
+ ##############################################################################
36
+ # constants
37
+ ##############################################################################
38
+
39
+ DEBUG = False
40
+
41
+ ##############################################################################
42
+ # command line option handler
43
+ ##############################################################################
44
+
45
+
46
+ def check_arguments():
47
+ """
48
+ handle command line options
49
+
50
+ --------------------------------------------------------------------------
51
+ args : none
52
+ --------------------------------------------------------------------------
53
+ returns
54
+ args : <class 'argparse.Namespace'>
55
+ e.g. Namespace(assume=False, nocache=False)
56
+ --------------------------------------------------------------------------
57
+ """
58
+ parser = argparse.ArgumentParser(
59
+ description='A diagnostic tool to detect devices attached to a\
60
+ computer\'s RS232 serial ports via FTDI USB to RS232 adaptors.\
61
+ Devices that are recognised are:\
62
+ Newport ESP300, MM4006 and SMC100 stage controllers;\
63
+ Hameg (Rohde & Schwarz) HMP4040, Keithley 2410/2614B,\
64
+ ISEG SHQ 222M/224M and Agilent E3647A/E3634A PSUs.\
65
+ Writes a cache file containing detected devices that can be used\
66
+ by other scripts in this suite.'
67
+ )
68
+ parser.add_argument(
69
+ '--nocache',
70
+ action='store_true',
71
+ help='do not create cache file from list of detected devices',
72
+ )
73
+ parser.add_argument(
74
+ '--delay_dmm6500',
75
+ action='store_true',
76
+ help='Keithley 2614b may not be detected if tested after DMM 6500'
77
+ )
78
+
79
+ return parser.parse_args()
80
+
81
+
82
+ ##############################################################################
83
+ # detect devices on serial ports
84
+ ##############################################################################
85
+
86
+
87
+ def find_controller(port, config):
88
+ """
89
+ Check if an ESP300 or mm4006 is present on the given serial port.
90
+
91
+ Note that the device's error-FIFO is manually cleared to ensure further
92
+ communications can be processed successfully.
93
+
94
+ from reference/ESP300.pdf page 3-7 (page 47 of the PDF):
95
+
96
+ "A controller command (or a sequence of commands) has to be terminated with
97
+ a carriage return character. However, responses from the controller are
98
+ always terminated by a carriage return/line feed combination. This setting
99
+ may not be changed."
100
+
101
+ Newport MM4006 responds to "VE" with:
102
+ "ve mm4006 controller version 7.01 date 04-02-2003"
103
+ mm4006 ignores rs232 commands when user is in the config menu.
104
+
105
+ Note that this function is called quite late in the detection process. By
106
+ the time it is called, this script will already have sent any esp300 or
107
+ mm4006 present on the specified serial port multiple commands it would
108
+ find incomprehensible, even if they were sent at a matching baud rate.
109
+ Hence, the need to clear the ESP300's error FIFO before detection can
110
+ commence.
111
+
112
+ --------------------------------------------------------------------------
113
+ args
114
+ port : string
115
+ port name
116
+ config : dict
117
+ serial port configuration
118
+ --------------------------------------------------------------------------
119
+ returns
120
+ retval : tuple
121
+ (category : string,
122
+ settings : dict,
123
+ (port : string,
124
+ manufacturer : string,
125
+ model : string,
126
+ serial_number : string,
127
+ detected : bool,
128
+ channels : list,
129
+ release_delay : float))
130
+ --------------------------------------------------------------------------
131
+ """
132
+ retval = (None, None, (port, None, None, None, None, None, None))
133
+ ser = serial.Serial()
134
+ ser.apply_settings(config)
135
+ ser.port = port
136
+
137
+ # initialise and open serial port
138
+ try:
139
+ ser.open()
140
+ except (OSError, serial.SerialException):
141
+ sys.exit(f'could not open port {port}, exiting.')
142
+ else:
143
+ ser.reset_output_buffer()
144
+ ser.reset_input_buffer()
145
+
146
+ # manually empty ESP30X's 10 item error FIFO
147
+ # the mm4006 - like the smc100 - only holds the last error
148
+ #
149
+ # FIXME
150
+ #
151
+ # The commands differ: TE? (esp300), TE (mm4006).
152
+ # This does not seem to cause problems with the mm4006, however check
153
+ # how the mm4006 handles this. The esp300 needs to have the FIFO
154
+ # cleared; perhaps the mm4006 is more tolerant in its error handling
155
+ # or it allows (undocumented) use of a trailing '?' for queries.
156
+ #
157
+ # Also investigate possibility of early termination to (slightly)
158
+ # reduce detection time:
159
+ #
160
+ # ESP300.pdf, p. 3-139
161
+ # TE? returns '0' if no error
162
+ #
163
+ # MM4K6_701_E3.pdf, p. 3.143
164
+ # TE returns 'TE@' if no error
165
+ esp300_read_error = lexicon.motion('esp300', 'read error')
166
+ esp300_fifo_size = 10
167
+ for _ in itertools.repeat(None, esp300_fifo_size):
168
+ ser.write(esp300_read_error)
169
+ try:
170
+ ser.readline()
171
+ except serial.SerialException:
172
+ print(f'cannot access serial port {port}')
173
+ ser.close()
174
+ return retval
175
+
176
+ # command string is identical for esp300 and mm4006
177
+ ser.write(lexicon.motion('esp300', 'read controller version'))
178
+
179
+ # get response
180
+ try:
181
+ controller = ser.readline().decode('utf-8').strip().lower()
182
+ except UnicodeDecodeError:
183
+ ser.close()
184
+ else:
185
+ channels = ['']
186
+ release_delay = None
187
+
188
+ # we might have an ESP300 or ESP301 controller/driver
189
+ if 'esp30' in controller:
190
+ manufacturer = 'newport'
191
+ model = controller.partition(' ')[0]
192
+ serial_number = ''
193
+ detected = True
194
+ settings = ser.get_settings()
195
+ retval = (
196
+ 'controller',
197
+ settings,
198
+ (
199
+ port,
200
+ manufacturer,
201
+ model,
202
+ serial_number,
203
+ detected,
204
+ channels,
205
+ release_delay,
206
+ ),
207
+ )
208
+ elif 'mm4006' in controller:
209
+ manufacturer = 'newport'
210
+ model = controller.split()[1]
211
+ serial_number = ''
212
+ detected = True
213
+ settings = ser.get_settings()
214
+ retval = (
215
+ 'controller',
216
+ settings,
217
+ (
218
+ port,
219
+ manufacturer,
220
+ model,
221
+ serial_number,
222
+ detected,
223
+ channels,
224
+ release_delay,
225
+ ),
226
+ )
227
+
228
+ # no device detected
229
+ ser.close()
230
+
231
+ return retval
232
+
233
+
234
+ def find_controller_smc(port, config):
235
+ """
236
+ Check if an SMC100 controller is present on the given serial port.
237
+
238
+ The SMC100 controller has non-configurable RS232 settings so handle it
239
+ separately in this function rather than making find_controller()
240
+ overly long.
241
+
242
+ Note that the device's error-FIFO is manually cleared to ensure further
243
+ communications can be processed successfully.
244
+
245
+ Newport SMC100 responds to 1VE with:
246
+ "1VE SMC_CC - Controller-driver version 3. 1. 2"
247
+
248
+ ASSUME that we only have one SMC controller, and it is at address 1.
249
+
250
+ --------------------------------------------------------------------------
251
+ args
252
+ port : string
253
+ port name
254
+ config : dict
255
+ serial port configuration
256
+ --------------------------------------------------------------------------
257
+ returns
258
+ retval : tuple
259
+ (category : string,
260
+ settings : dict,
261
+ (port : string,
262
+ manufacturer : string,
263
+ model : string,
264
+ serial_number : string,
265
+ detected : bool,
266
+ channels : list,
267
+ release_delay : float))
268
+ --------------------------------------------------------------------------
269
+ """
270
+ retval = (None, None, (port, None, None, None, None, None, None))
271
+ ser = serial.Serial()
272
+ ser.apply_settings(config)
273
+ ser.port = port
274
+
275
+ # initialise and open serial port
276
+ try:
277
+ ser.open()
278
+ except (OSError, serial.SerialException):
279
+ sys.exit(f'could not open port {port}, exiting.')
280
+ else:
281
+ ser.reset_output_buffer()
282
+ ser.reset_input_buffer()
283
+
284
+ # the SMC100 retains a record of the last error only
285
+ ser.write(lexicon.motion('smc', 'read error', identifier='1'))
286
+ try:
287
+ ser.readline()
288
+ except serial.SerialException:
289
+ print(f'cannot access serial port {port}')
290
+ ser.close()
291
+ return retval
292
+
293
+ ser.write(lexicon.motion('smc', 'read controller version', identifier='1'))
294
+
295
+ # get response
296
+ try:
297
+ controller = ser.readline().decode('utf-8').strip().lower()
298
+ except UnicodeDecodeError:
299
+ ser.close()
300
+ else:
301
+ if 'smc' in controller:
302
+ release_delay = None
303
+ channels = ['']
304
+ manufacturer = 'newport'
305
+ model = 'smc'
306
+ serial_number = ''
307
+ detected = True
308
+ settings = ser.get_settings()
309
+ retval = (
310
+ 'controller',
311
+ settings,
312
+ (
313
+ port,
314
+ manufacturer,
315
+ model,
316
+ serial_number,
317
+ detected,
318
+ channels,
319
+ release_delay,
320
+ ),
321
+ )
322
+
323
+ # no device detected
324
+ ser.close()
325
+
326
+ return retval
327
+
328
+
329
+ def find_lvpsu(port, config):
330
+ """
331
+ check if any of these devices are present on the given serial port:
332
+
333
+ Keysight/Agilent E3647A (dual output)
334
+ Hewlett-Packard/Agilent E3634A (single output)
335
+
336
+ notes for this type of PSU:
337
+
338
+ a null-modem/crossover adaptor needs to be added to the serial cable
339
+
340
+ test commands using miniterm rather than putty, e.g.
341
+
342
+ [avt@pc048057 dev]$ miniterm.py --port=/dev/ttyUSB1
343
+ --- Miniterm on /dev/ttyUSB1: 9600,8,N,1 ---
344
+ --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
345
+ Agilent Technologies,E3647A,0,1.7-5.0-1.0
346
+
347
+ When using Keysight Connection Expert 2018 on Windows, the Flow control
348
+ setting in Windows Device Manger seems to have no effect. Hardware, None,
349
+ or Xon / Xoff all seem to work.
350
+
351
+ in essence, ignore what the manual says about remote mode and dsrstr:
352
+
353
+ (1) do not put the PSU into remote mode. Keysight Connection Expert
354
+ 2018 doesn't do this on Windows, and it seems to cause problems here
355
+ (2) do not use dsrdtr
356
+
357
+ response for Keysight E3634A:
358
+
359
+ [avt@pc048057 utilities]$ miniterm.py --port=/dev/ttyUSB0
360
+ --- Miniterm on /dev/ttyUSB0: 9600,8,N,1 ---
361
+ --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
362
+ HEWLETT-PACKARD,E3634A,0,2.4-6.1-2.1
363
+
364
+ SYST:VERS? yields the SCPI version e.g. 1997.0
365
+ YYYY.V where the Y represents the year of the version,
366
+ and the V represents the version number for that year.
367
+
368
+ --------------------------------------------------------------------------
369
+ args
370
+ port : string
371
+ port name
372
+ config : dict
373
+ serial port configuration
374
+ --------------------------------------------------------------------------
375
+ returns
376
+ retval : tuple
377
+ (category : string,
378
+ settings : dict,
379
+ (port : string,
380
+ manufacturer : string,
381
+ model : string,
382
+ serial_number : string,
383
+ detected : bool,
384
+ channels : list,
385
+ release_delay : float))
386
+ --------------------------------------------------------------------------
387
+ """
388
+ retval = (None, None, (port, None, None, None, None, None, None))
389
+ ser = serial.Serial()
390
+ ser.apply_settings(config)
391
+ ser.port = port
392
+
393
+ # initialise and open serial port
394
+ try:
395
+ ser.open()
396
+ except (OSError, serial.SerialException):
397
+ sys.exit(f'could not open port {port}, exiting.')
398
+ else:
399
+ ser.reset_input_buffer()
400
+ ser.reset_output_buffer()
401
+
402
+ # ask device to identify itself
403
+ ser.write(lexicon.power('e3647a', 'identify'))
404
+
405
+ # attempt to read back response
406
+ try:
407
+ response = ser.readline()
408
+ except (serial.SerialException, AttributeError):
409
+ print(f'cannot access serial port {port}')
410
+ else:
411
+ response = response.lower()
412
+ if b'agilent' in response or b'hewlett-packard' in response:
413
+ parts = response.decode('utf-8').split(',')
414
+ manufacturer = 'agilent'
415
+ model = parts[1].strip()
416
+ serial_number = ''
417
+ detected = True
418
+ settings = ser.get_settings()
419
+
420
+ if model in {'e3647a', 'e3634a'}:
421
+ if model == 'e3647a':
422
+ release_delay = 0.01
423
+ channels = ['1', '2']
424
+ else:
425
+ release_delay = None
426
+ channels = ['']
427
+
428
+ retval = (
429
+ 'lvpsu',
430
+ settings,
431
+ (
432
+ port,
433
+ manufacturer,
434
+ model,
435
+ serial_number,
436
+ detected,
437
+ channels,
438
+ release_delay,
439
+ ),
440
+ )
441
+ else:
442
+ print(f'lvpsu: unrecognised model {model}')
443
+
444
+ ser.close()
445
+
446
+ return retval
447
+
448
+
449
+ def find_lvpsu_hmp4040(port, config):
450
+ """
451
+ check if any of these devices are present on the given serial port:
452
+
453
+ Rohde & Schwarz HMP4040
454
+
455
+ response to *IDN?:
456
+
457
+ macbook:utilities avt$ python3 -m serial.tools.miniterm
458
+
459
+ --- Available ports:
460
+ --- 1: /dev/cu.Bluetooth-Incoming-Port 'n/a'
461
+ --- 2: /dev/cu.SSDC 'n/a'
462
+ --- 3: /dev/cu.usbserial-AH06DY15 'FT232R USB UART'
463
+ --- Enter port index or full name: 3
464
+ --- Miniterm on /dev/cu.usbserial-AH06DY15 9600,8,N,1 ---
465
+ --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
466
+ HAMEG,HMP4040,103781,HW50020001/SW2.51
467
+
468
+ Some models also identify as ROHDE&SCHWARZ instead of HAMEG.
469
+
470
+ (pve) pi@raspberrypi $ python3 -m serial.tools.miniterm
471
+
472
+ --- Available ports:
473
+ --- 1: /dev/ttyAMA0 'ttyAMA0'
474
+ --- 2: /dev/ttyUSB0 'FT232R USB UART - FT232R USB UART'
475
+ --- Enter port index or full name: 2
476
+ --- Miniterm on /dev/ttyUSB0 9600,8,N,1 ---
477
+ --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
478
+ ROHDE&SCHWARZ,HMP4040,120224,HW50020003/SW2.70
479
+
480
+ --------------------------------------------------------------------------
481
+ args
482
+ port : string
483
+ port name
484
+ config : dict
485
+ serial port configuration
486
+ --------------------------------------------------------------------------
487
+ returns
488
+ retval : tuple
489
+ (category : string,
490
+ settings : dict,
491
+ (port : string,
492
+ manufacturer : string,
493
+ model : string,
494
+ serial_number : string,
495
+ detected : bool,
496
+ channels : list,
497
+ release_delay : float))
498
+ --------------------------------------------------------------------------
499
+ """
500
+ retval = (None, None, (port, None, None, None, None, None, None))
501
+ ser = serial.Serial()
502
+ ser.apply_settings(config)
503
+ ser.port = port
504
+
505
+ # initialise and open serial port
506
+ try:
507
+ ser.open()
508
+ except (OSError, serial.SerialException):
509
+ sys.exit(f'could not open port {port}, exiting.')
510
+ else:
511
+ ser.reset_input_buffer()
512
+ ser.reset_output_buffer()
513
+
514
+ # ask device to identify itself
515
+ ser.write(lexicon.power('hmp4040', 'identify'))
516
+
517
+ # attempt to read back response
518
+ try:
519
+ response = ser.readline()
520
+ except (serial.SerialException, AttributeError):
521
+ print(f'cannot access serial port {port}')
522
+ else:
523
+ response = response.lower()
524
+ if b'hameg' in response or b'rohde' in response:
525
+ release_delay = 0.026
526
+ parts = response.decode('utf-8').split(',')
527
+ manufacturer = 'hameg'
528
+ model = parts[1].strip()
529
+ serial_number = parts[2].strip()
530
+ detected = True
531
+ settings = ser.get_settings()
532
+ channels = ['1', '2', '3', '4']
533
+ retval = (
534
+ 'lvpsu',
535
+ settings,
536
+ (
537
+ port,
538
+ manufacturer,
539
+ model,
540
+ serial_number,
541
+ detected,
542
+ channels,
543
+ release_delay,
544
+ ),
545
+ )
546
+
547
+ ser.close()
548
+
549
+ return retval
550
+
551
+
552
+ def find_hvpsu(port, config):
553
+ """
554
+ check if one of the following power supplies is present on the given
555
+ serial port
556
+
557
+ Keithley 2410
558
+ ISEG SHQ 222M
559
+ ISEG SHQ 224M
560
+
561
+ this function also successfully identifies:
562
+ Keithley 6517B electrometer / high resistance meter
563
+
564
+ Keithley 2410 response to *IDN?
565
+ KEITHLEY INSTRUMENTS INC.,MODEL 2410,4343654,C34 Sep 21 2016 15:30:00/A02 /K/M
566
+ --------------------------------------------------------------------------
567
+ args
568
+ port : string
569
+ port name
570
+ config : dict
571
+ serial port configuration
572
+ --------------------------------------------------------------------------
573
+ returns
574
+ retval : tuple
575
+ (category : string,
576
+ settings : dict,
577
+ (port : string,
578
+ manufacturer : string,
579
+ model : string,
580
+ serial_number : string,
581
+ detected : bool,
582
+ channels : list,
583
+ release_delay : float))
584
+ --------------------------------------------------------------------------
585
+ """
586
+ retval = (None, None, (port, None, None, None, None, None, None))
587
+ ser = serial.Serial()
588
+ ser.apply_settings(config)
589
+ ser.port = port
590
+
591
+ # initialise and open serial port
592
+ try:
593
+ ser.open()
594
+ except (OSError, serial.SerialException):
595
+ sys.exit(f'could not open port {port}, exiting.')
596
+ else:
597
+ ser.reset_input_buffer()
598
+ ser.reset_output_buffer()
599
+ ser.write(lexicon.power('2410', 'identify'))
600
+
601
+ # attempt to read back response
602
+ try:
603
+ response = ser.readline().lower()
604
+ except serial.SerialException:
605
+ print(f'cannot access serial port {port}')
606
+ else:
607
+ if b'keithley' in response:
608
+ release_delay = None
609
+ parts = response.decode('utf-8').split(',')
610
+ manufacturer = parts[0].partition(' ')[0]
611
+ model = parts[1].rpartition(' ')[-1]
612
+ serial_number = parts[2]
613
+ detected = True
614
+ channels = ['']
615
+ settings = ser.get_settings()
616
+ retval = (
617
+ 'hvpsu',
618
+ settings,
619
+ (
620
+ port,
621
+ manufacturer,
622
+ model,
623
+ serial_number,
624
+ detected,
625
+ channels,
626
+ release_delay,
627
+ ),
628
+ )
629
+ else:
630
+ # check for ISEG SHQ dual-channel power supplies
631
+
632
+ ser.reset_input_buffer()
633
+ ser.reset_output_buffer()
634
+ time.sleep(0.5)
635
+
636
+ # send CRLF first for sync
637
+ ser.write(lexicon.power('shq', 'synchronise'))
638
+ ser.readline() # consume local echo
639
+ ser.readline() # consume local echo
640
+ ser.readline() # consume response to command in buffer
641
+
642
+ # '#' command: read device identifier
643
+ # device identifier does not contain a model number:
644
+ # serial number;software release;Vout max;Iout max
645
+ # e.g. ISEG SHQ 224M returns '484216;3.09;4000V;3mA'
646
+ # e.g. ISEG SHQ 222M returns '484230;3.10;2000V;6mA'
647
+ ser.write(lexicon.power('shq', 'identify'))
648
+ ser.readline() # consume local echo
649
+ response = ser.readline()
650
+ parts = [x.strip() for x in response.decode('utf-8').split(';')]
651
+ if len(parts) == 4:
652
+ release_delay = 0.01
653
+ manufacturer = 'iseg'
654
+ model = 'shq'
655
+ serial_number = parts[0]
656
+ detected = True
657
+ channels = ['1', '2']
658
+ settings = ser.get_settings()
659
+ retval = (
660
+ 'hvpsu',
661
+ settings,
662
+ (
663
+ port,
664
+ manufacturer,
665
+ model,
666
+ serial_number,
667
+ detected,
668
+ channels,
669
+ release_delay,
670
+ ),
671
+ )
672
+
673
+ ser.close()
674
+
675
+ return retval
676
+
677
+
678
+ def find_hvpsu_2614b(port, config):
679
+ """
680
+ check if a Keithley 2614b is present on the given serial port
681
+
682
+ note that the language used for this generation of Keithley is quite
683
+ different from earlier models
684
+
685
+ e.g.
686
+
687
+ d=localnode.description
688
+ print(d)
689
+
690
+ returns:
691
+
692
+ Keithley Instruments SMU 2614B - 4428182
693
+
694
+ --------------------------------------------------------------------------
695
+ args
696
+ port : string
697
+ port name
698
+ config : dict
699
+ serial port configuration
700
+ --------------------------------------------------------------------------
701
+ returns
702
+ retval : tuple
703
+ (category : string,
704
+ settings : dict,
705
+ (port : string,
706
+ manufacturer : string,
707
+ model : string,
708
+ serial_number : string,
709
+ detected : bool,
710
+ channels : list,
711
+ release_delay : float))
712
+ --------------------------------------------------------------------------
713
+ """
714
+ retval = (None, None, (port, None, None, None, None, None, None))
715
+ ser = serial.Serial()
716
+ ser.apply_settings(config)
717
+ ser.port = port
718
+
719
+ # initialise and open serial port
720
+ try:
721
+ ser.open()
722
+ except (OSError, serial.SerialException):
723
+ sys.exit(f'could not open port {port}, exiting.')
724
+ else:
725
+ ser.reset_input_buffer()
726
+ ser.reset_output_buffer()
727
+ ser.write(lexicon.power('2614b', 'identify'))
728
+
729
+ # attempt to read back response
730
+ try:
731
+ response = ser.readline().lower()
732
+ except serial.SerialException:
733
+ print(f'cannot access serial port {port}')
734
+ else:
735
+ if b'keithley' in response:
736
+ try:
737
+ release_delay = 0.01
738
+ parts = response.decode('utf-8').split()
739
+ manufacturer = parts[0]
740
+ model = parts[3]
741
+ serial_number = parts[5]
742
+ detected = True
743
+ channels = ['a', 'b']
744
+ settings = ser.get_settings()
745
+ retval = (
746
+ 'hvpsu',
747
+ settings,
748
+ (
749
+ port,
750
+ manufacturer,
751
+ model,
752
+ serial_number,
753
+ detected,
754
+ channels,
755
+ release_delay,
756
+ ),
757
+ )
758
+ except IndexError:
759
+ pass
760
+
761
+ ser.close()
762
+
763
+ return retval
764
+
765
+
766
+ def find_dmm_6500(port, config):
767
+ """
768
+ check if a Keithley DMM6500 is present on the given serial port
769
+
770
+ *IDN? returns:
771
+ KEITHLEY INSTRUMENTS,MODEL DMM6500,04592428,1.7.12b
772
+
773
+ --------------------------------------------------------------------------
774
+ args
775
+ port : string
776
+ port name
777
+ config : dict
778
+ serial port configuration
779
+ --------------------------------------------------------------------------
780
+ returns
781
+ retval : tuple
782
+ (category : string,
783
+ settings : dict,
784
+ (port : string,
785
+ manufacturer : string,
786
+ model : string,
787
+ serial_number : string,
788
+ detected : bool,
789
+ channels : list,
790
+ release_delay : float))
791
+ --------------------------------------------------------------------------
792
+ """
793
+ retval = (None, None, (port, None, None, None, None, None, None))
794
+ ser = serial.Serial()
795
+ ser.apply_settings(config)
796
+ ser.port = port
797
+
798
+ # initialise and open serial port
799
+ try:
800
+ ser.open()
801
+ except (OSError, serial.SerialException):
802
+ sys.exit(f'could not open port {port}, exiting.')
803
+ else:
804
+ ser.reset_input_buffer()
805
+ ser.reset_output_buffer()
806
+ ser.write(lexicon.instrument('dmm6500', 'identify'))
807
+
808
+ # attempt to read back response
809
+ try:
810
+ response = ser.readline().lower()
811
+ except serial.SerialException:
812
+ print(f'cannot access serial port {port}')
813
+ else:
814
+ if b'keithley' in response:
815
+ try:
816
+ release_delay = 0.01
817
+ # ['KEITHLEY INSTRUMENTS', 'MODEL DMM6500', '04592428', '1.7.12b']
818
+ parts = response.decode('utf-8').lower().split(',')
819
+ manufacturer = parts[0].split()[0]
820
+ model = parts[1].split()[-1]
821
+ serial_number = parts[2]
822
+ detected = True
823
+ channels = ['']
824
+ settings = ser.get_settings()
825
+ retval = (
826
+ 'instrument',
827
+ settings,
828
+ (
829
+ port,
830
+ manufacturer,
831
+ model,
832
+ serial_number,
833
+ detected,
834
+ channels,
835
+ release_delay,
836
+ ),
837
+ )
838
+ except IndexError:
839
+ pass
840
+
841
+ ser.close()
842
+
843
+ return retval
844
+
845
+
846
+ def detect_device_on_port(port, tests, configs):
847
+ """
848
+ try all tests on the given port in a bid to identify which category of
849
+ device is present
850
+
851
+ --------------------------------------------------------------------------
852
+ args
853
+ port : string
854
+ port name
855
+ tests : dict
856
+ contains the function to call for each category of device
857
+ configs : dict of dicts
858
+ {device_type: serial_port_configuration, ...}
859
+ --------------------------------------------------------------------------
860
+ returns
861
+ retval : tuple
862
+ (category : string,
863
+ settings : dict,
864
+ (port : string,
865
+ manufacturer : string,
866
+ model : string,
867
+ serial_number : string,
868
+ detected : bool,
869
+ channels : list,
870
+ release_delay : float))
871
+ --------------------------------------------------------------------------
872
+ """
873
+ retval = (None, None, (port, None, None, None, None, None, None))
874
+
875
+ for test in tests:
876
+ if DEBUG:
877
+ print(f'checking {port} for {test}')
878
+
879
+ retval = tests[test](port, configs[test])
880
+ if retval[0] is not None:
881
+ break
882
+
883
+ # macOS seems to process serial port events more swiftly than CentOS
884
+ # and Windows 7: need to give the devices enough time to respond
885
+ # between requests
886
+ time.sleep(0.3)
887
+
888
+ return retval
889
+
890
+
891
+ ##############################################################################
892
+ # format detail for detected device
893
+ ##############################################################################
894
+
895
+
896
+ def display_found(device_type, value, padding):
897
+ """
898
+ print details for a device identified on a serial port
899
+
900
+ --------------------------------------------------------------------------
901
+ args
902
+ device_type : string
903
+ e.g. 'hvpsu'
904
+ value : tuple (string, string, string, string, bool)
905
+ e.g. (port, manufacturer, model, serial_number, detected_flag)
906
+ padding : int
907
+ the length of the longest device type
908
+ --------------------------------------------------------------------------
909
+ returns : none
910
+ --------------------------------------------------------------------------
911
+ """
912
+ port, manufacturer, model, serial_number, *_ = value
913
+
914
+ if device_type is None:
915
+ print(f'{"None".rjust(padding)} on {port}: (no device identified)')
916
+ else:
917
+ details = [d for d in (manufacturer, model) if d]
918
+ if serial_number:
919
+ details.append(f's.no. {serial_number}')
920
+
921
+ suffix = ' '.join(details)
922
+ print(f'{device_type.rjust(padding)} on {port}: {suffix}')
923
+
924
+
925
+ ##############################################################################
926
+ # main
927
+ ##############################################################################
928
+
929
+
930
+ def main():
931
+ """
932
+ detect devices present on the serial ports, that are attached via
933
+ FTDI USB to RS232 adaptors
934
+ """
935
+ args = check_arguments()
936
+
937
+ ports = [
938
+ com.device
939
+ for com in stlp.comports()
940
+ if common.rs232_port_is_valid(com)
941
+ ]
942
+ if not ports:
943
+ all_ports = ', '.join(com.device for com in stlp.comports())
944
+ sys.exit(f'No usable serial ports found: {all_ports}')
945
+
946
+ if args.delay_dmm6500:
947
+ tests = collections.OrderedDict(
948
+ {
949
+ 'lvpsu': find_lvpsu,
950
+ 'lvpsu_hmp4040': find_lvpsu_hmp4040,
951
+ 'hvpsu': find_hvpsu,
952
+ 'hvpsu_2614b': find_hvpsu_2614b,
953
+ 'dmm_6500': find_dmm_6500,
954
+ 'controller': find_controller,
955
+ 'controller_smc': find_controller_smc,
956
+ }
957
+ )
958
+ else:
959
+ tests = collections.OrderedDict(
960
+ {
961
+ 'dmm_6500': find_dmm_6500,
962
+ 'lvpsu': find_lvpsu,
963
+ 'lvpsu_hmp4040': find_lvpsu_hmp4040,
964
+ 'hvpsu': find_hvpsu,
965
+ 'hvpsu_2614b': find_hvpsu_2614b,
966
+ 'controller': find_controller,
967
+ 'controller_smc': find_controller_smc,
968
+ }
969
+ )
970
+
971
+ # define serial port connection settings for each device
972
+ # see https://pyserial.readthedocs.io/en/latest/pyserial_api.html
973
+ common_configuration = {
974
+ 'bytesize': serial.EIGHTBITS,
975
+ 'parity': serial.PARITY_NONE,
976
+ 'stopbits': serial.STOPBITS_ONE,
977
+ 'dsrdtr': False,
978
+ 'timeout': 1,
979
+ 'write_timeout': 1,
980
+ 'inter_byte_timeout': None,
981
+ }
982
+ configs = {
983
+ 'hvpsu': {
984
+ 'baudrate': 9600,
985
+ 'xonxoff': False,
986
+ 'rtscts': False,
987
+ },
988
+ 'hvpsu_2614b': {
989
+ 'baudrate': 57600,
990
+ 'xonxoff': False,
991
+ 'rtscts': False,
992
+ },
993
+ 'dmm_6500': {
994
+ 'baudrate': 38400,
995
+ 'xonxoff': False,
996
+ 'rtscts': False,
997
+ },
998
+ 'controller': {
999
+ 'baudrate': 19200,
1000
+ 'xonxoff': False,
1001
+ 'rtscts': False,
1002
+ },
1003
+ 'controller_smc': {
1004
+ 'baudrate': 57600,
1005
+ 'xonxoff': True,
1006
+ 'rtscts': False,
1007
+ },
1008
+ 'lvpsu': {
1009
+ 'baudrate': 9600,
1010
+ 'xonxoff': False,
1011
+ 'rtscts': False,
1012
+ },
1013
+ 'lvpsu_hmp4040': {
1014
+ 'baudrate': 9600,
1015
+ 'xonxoff': False,
1016
+ 'rtscts': True,
1017
+ },
1018
+ }
1019
+ # create unions of common and device-specific configurations
1020
+ configs = {
1021
+ device: {**device_specific_configuration, **common_configuration}
1022
+ for device, device_specific_configuration in configs.items()
1023
+ }
1024
+
1025
+ padding = max(map(len, tests))
1026
+
1027
+ # store creation platform in cache
1028
+ found = {'platform': sys.platform.lower()}
1029
+
1030
+ # detect devices on ports
1031
+ _ddop_pf = functools.partial(detect_device_on_port, tests=tests, configs=configs)
1032
+ with cf.ThreadPoolExecutor() as executor:
1033
+ detected = (executor.submit(_ddop_pf, port) for port in ports)
1034
+ for future in cf.as_completed(detected):
1035
+ key, settings, value = future.result()
1036
+ if key not in found:
1037
+ found[key] = (settings, [value])
1038
+ else:
1039
+ found[key][1].append(value)
1040
+
1041
+ display_found(key, value, padding)
1042
+
1043
+ # JSON output
1044
+ if args.nocache:
1045
+ print(f'\nJSON:\n\n{json.dumps(found)}')
1046
+ else:
1047
+ common.configcache_write(found, common.DEVICE_CACHE)
1048
+ print(f'\nWrote detected configuration to: {common.DEVICE_CACHE}')
1049
+
1050
+
1051
+ ##############################################################################
1052
+ if __name__ == '__main__':
1053
+ main()