├── MANIFEST.in ├── dcps ├── Warnings.py ├── power_tests_fix_data.py ├── __init__.py ├── RigolDP800.py ├── BK9115.py ├── KoradKAseries.py ├── PowerTestBoard.py ├── RigolDL3000.py ├── KeysightE364xA.py ├── AimTTiPLP.py ├── Keithley622x.py ├── IT6500C.py ├── Keithley2182.py └── Keithley2400.py ├── debug ├── testPYVISA.py ├── testUSB.py ├── testUSB2.py └── powerCycle.py ├── LICENSE ├── .gitignore ├── setup.py └── README.md /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /dcps/Warnings.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | class NotImplemented(UserWarning): 4 | pass -------------------------------------------------------------------------------- /debug/testPYVISA.py: -------------------------------------------------------------------------------- 1 | import pyvisa 2 | rm = pyvisa.ResourceManager('@py') 3 | print('PyVISA Resources Found:') 4 | print(" " + "\n ".join(rm.list_resources())) 5 | resource = 'USB0::0x1AB1::0x0E11::DP8B153600499::INSTR' 6 | print('opening resource: ' + resource) 7 | inst = rm.open_resource(resource) 8 | print(inst.query("*IDN?")) 9 | -------------------------------------------------------------------------------- /debug/testUSB.py: -------------------------------------------------------------------------------- 1 | 2 | import usb.core 3 | import usb.util 4 | import sys 5 | 6 | # got these using the command lsusb -vv 7 | VENDOR_ID = 0x1AB1 8 | PRODUCT_ID = 0x0E11 9 | DATA_SIZE = 1 10 | 11 | device = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID) 12 | 13 | #@@@#print(device.is_kernel_driver_active(0)) 14 | 15 | # was it found? 16 | if device is None: 17 | raise ValueError('USB Device not found') 18 | 19 | try: 20 | # set the active configuration. With no arguments, the first 21 | # configuration will be the active one 22 | device.set_configuration() 23 | except usb.core.USBError as e: 24 | raise Exception("failed to set configuration\n %s" % e) 25 | 26 | cfg = device.get_active_configuration() 27 | 28 | for cfg in device: 29 | sys.stdout.write(str(cfg.bConfigurationValue) + '\n') 30 | 31 | #@@@#device.read(0x81, 255, 1000000) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018, Stephen Goadhouse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dcps/power_tests_fix_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import argparse 5 | import numpy as np 6 | import pandas as pd 7 | 8 | parser = argparse.ArgumentParser(description='Fix data file by adding missing meta data') 9 | 10 | parser.add_argument('filename', help='filename of NPZ datafile') 11 | 12 | args = parser.parse_args() 13 | 14 | 15 | header=None 16 | meta=None 17 | with np.load(args.filename,allow_pickle=False) as data: 18 | rows = data['rows'] 19 | if 'header' in data.files: 20 | header = data['header'] 21 | if 'meta' in data.files: 22 | meta = data['meta'] 23 | 24 | print(rows[0:10]) 25 | 26 | if (True): 27 | # Add missing boardName & circuit name 28 | boardName = '1' 29 | circuit = '1V8-A' 30 | newrows = [ [boardName,circuit,int(r[0])]+list(r[1:]) for r in rows ] 31 | newheader = ["Board","Circuit"]+list(header) 32 | newmeta = [meta[0], meta[1], boardName, meta[2]] 33 | 34 | print('') 35 | print(newrows[0:10]) 36 | 37 | df = pd.DataFrame(newrows,columns=newheader) 38 | print('') 39 | print(df[0:10].info()) 40 | print(df[0:10]) 41 | print(type(df['Board'][0])) 42 | 43 | if (True): 44 | ## Save as a pandas pickle file 45 | df.to_pickle(args.filename+'.pkl') 46 | 47 | if (False): 48 | ## Save back as a NPZ file 49 | arrays = {'rows': newrows} 50 | if (newheader is not None): 51 | arrays['header']=newheader 52 | if (newmeta is not None): 53 | arrays['meta']=newmeta 54 | np.savez(args.filename+'.npz', **arrays) 55 | 56 | 57 | -------------------------------------------------------------------------------- /dcps/__init__.py: -------------------------------------------------------------------------------- 1 | # Custom warnings 2 | from Warnings import NotImplemented 3 | 4 | # Standard SCPI commands 5 | from dcps.SCPI import SCPI 6 | 7 | # Support of Rigol DP832A and other DP800 power supplies 8 | from dcps.RigolDP800 import RigolDP800 9 | 10 | # Support of Aim TTi PL-P Series power supplies 11 | from dcps.AimTTiPLP import AimTTiPLP 12 | 13 | # Support of BK Precision 9115 and related DC power supplies 14 | from dcps.BK9115 import BK9115 15 | 16 | # Support of HP/Agilent/Keysight E364xA series DC Power Supplies 17 | # connected to GPIB port through a KISS-488 Ethernet or Prologix Ethernet to GPIB interface 18 | from dcps.KeysightE364xA import KeysightE364xA 19 | 20 | # Support of Keithley/Tektronix 622X Precision Current Source 21 | # connected to GPIB port through a KISS-488 or Prologix Ethernet to GPIB interface 22 | from dcps.Keithley622x import Keithley622x 23 | 24 | # Support of Keithley/Tektronix 2182 Nanovoltmeter 25 | # connected to GPIB port through a KISS-488 or Prologix Ethernet to GPIB interface 26 | from dcps.Keithley2182 import Keithley2182 27 | 28 | # Support of Keithley/Tektronix 2400 Series SourceMeter 29 | # connected to GPIB port through a KISS-488 or Prologix Ethernet to GPIB interface 30 | from dcps.Keithley2400 import Keithley2400 31 | 32 | # Support of Rigol DL3031A and other DL3000 family electronic loads 33 | from dcps.RigolDL3000 import RigolDL3000 34 | 35 | # Support of Keithley DMM6500 Digital Multimeter 36 | from dcps.Keithley6500 import Keithley6500 37 | 38 | # Support of ITECH IT6500C series psus 39 | from dcps.IT6500C import IT6500C 40 | 41 | # Support of the KORAD KA-series psus 42 | from dcps.KoradKAseries import KAseries -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Emacs stuff 2 | *~ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | #from distutils.core import setup 4 | from setuptools import setup 5 | 6 | def readme(): 7 | with open('README.md') as f: 8 | return f.read() 9 | 10 | try: 11 | import pypandoc 12 | long_description = pypandoc.convert('README.md', 'rst') 13 | except(IOError, ImportError): 14 | long_description = open('README.md').read() 15 | 16 | 17 | setup(name="dcps", 18 | version='0.8.0', 19 | description='Control of DC Power Supplies/DC Electronic Loads/DMMs/SourceMeters through python', 20 | long_description_content_type='text/markdown', 21 | long_description=long_description, 22 | url='https://github.com/sgoadhouse/dcps', 23 | author='Stephen Goadhouse', 24 | author_email="sgoadhouse@virginia.edu", 25 | maintainer='Stephen Goadhouse', 26 | maintainer_email="sgoadhouse@virginia.edu", 27 | license='MIT', 28 | keywords=['Rigol', 'DP800', 'DP832A', 'DL3000', 'DL3031A', 'AimTTI', 'BK', '9115', 'Keysight', 'Agilent', 'Keithley', 'ITECH', 29 | 'E3642A', 'E364xA', '6220', '6221', '2182', '2182A', '2400', 'DMM6500', '6500C/D', 30 | 'PyVISA', 'VISA', 'SCPI', 'INSTRUMENT'], 31 | classifiers=[ 32 | 'Development Status :: 4 - Beta', 33 | 'Intended Audience :: Developers', 34 | 'Intended Audience :: Education', 35 | 'Intended Audience :: Science/Research', 36 | 'License :: OSI Approved :: MIT License', 37 | 'Programming Language :: Python', 38 | 'Topic :: Scientific/Engineering', 39 | 'Topic :: Scientific/Engineering :: Physics', 40 | 'Topic :: Software Development', 41 | 'Topic :: Software Development :: Libraries', 42 | 'Topic :: Software Development :: Libraries :: Python Modules'], 43 | install_requires=[ 44 | 'pyvisa>=1.9.0,!=1.11.0', 45 | 'pyvisa-py>=0.4.1' 46 | ], 47 | packages=["dcps"], 48 | include_package_data=True, 49 | zip_safe=False 50 | ) 51 | -------------------------------------------------------------------------------- /debug/testUSB2.py: -------------------------------------------------------------------------------- 1 | 2 | import usb.core 3 | import usb.util 4 | from usb.backend import libusb1 5 | import sys 6 | from fnmatch import fnmatch 7 | 8 | VENDOR_ID = 0x1AB1 9 | PRODUCT_ID = 0x0E11 10 | #@@@#SERIAL_NUM = 'DP8B153600499' 11 | SERIAL_NUM = None 12 | DATA_SIZE = 1 13 | 14 | def find_devices( 15 | vendor=None, product=None, serial_number=None, custom_match=None, **kwargs 16 | ): 17 | """Find connected USB devices matching certain keywords. 18 | 19 | Wildcards can be used for vendor, product and serial_number. 20 | 21 | :param vendor: name or id of the vendor (manufacturer) 22 | :param product: name or id of the product 23 | :param serial_number: serial number. 24 | :param custom_match: callable returning True or False that takes a device as only input. 25 | :param kwargs: other properties to match. See usb.core.find 26 | :return: 27 | """ 28 | kwargs = kwargs or {} 29 | attrs = {} 30 | if isinstance(vendor, str): 31 | attrs["manufacturer"] = vendor 32 | elif vendor is not None: 33 | kwargs["idVendor"] = vendor 34 | 35 | if isinstance(product, str): 36 | attrs["product"] = product 37 | elif product is not None: 38 | kwargs["idProduct"] = product 39 | 40 | if serial_number: 41 | attrs["serial_number"] = str(serial_number) 42 | 43 | if attrs: 44 | 45 | def cm(dev): 46 | if custom_match is not None and not custom_match(dev): 47 | return False 48 | for attr, pattern in attrs.items(): 49 | try: 50 | value = getattr(dev, attr) 51 | except (NotImplementedError, ValueError): 52 | return False 53 | if not fnmatch(value.lower(), pattern.lower()): 54 | return False 55 | return True 56 | 57 | else: 58 | cm = custom_match 59 | 60 | ## ADDED THIS to make sure using libusb in this test example 61 | be = libusb1.get_backend() 62 | 63 | return usb.core.find(backend=be, find_all=True, custom_match=cm, **kwargs) 64 | 65 | #@@@#device = usb.core.find(idVendor=VENDOR_ID, idProduct=PRODUCT_ID) 66 | 67 | devices = list( 68 | find_devices(VENDOR_ID, PRODUCT_ID, SERIAL_NUM, None) 69 | #@@@#return usb.core.find(find_all=True, custom_match=cm, **kwargs) 70 | ) 71 | 72 | if not devices: 73 | raise ValueError("No device found.") 74 | elif len(devices) > 1: 75 | desc = "\n".join(str(dev) for dev in devices) 76 | raise ValueError( 77 | "{} devices found:\n{}\nPlease narrow the search" 78 | " criteria".format(len(devices), desc) 79 | ) 80 | 81 | usb_dev = devices[0] 82 | 83 | try: 84 | if usb_dev.is_kernel_driver_active(0): 85 | usb_dev.detach_kernel_driver(0) 86 | except (usb.core.USBError, NotImplementedError): 87 | pass 88 | 89 | try: 90 | usb_dev.set_configuration() 91 | except usb.core.USBError as e: 92 | raise Exception("failed to set configuration\n %s" % e) 93 | 94 | try: 95 | usb_dev.set_interface_altsetting() 96 | except usb.core.USBError: 97 | pass 98 | 99 | cfg = usb_dev.get_active_configuration() 100 | 101 | for cfg in usb_dev: 102 | sys.stdout.write(str(cfg.bConfigurationValue) + '\n') 103 | 104 | -------------------------------------------------------------------------------- /dcps/RigolDP800.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | 5 | # Copyright (c) 2018, 2021, Stephen Goadhouse 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | #------------------------------------------------------------------------------- 26 | # Control a Rigol DP8xx family of DC Power Supplies with PyVISA 27 | #------------------------------------------------------------------------------- 28 | 29 | # For future Python3 compatibility: 30 | from __future__ import absolute_import 31 | from __future__ import division 32 | from __future__ import print_function 33 | 34 | try: 35 | from . import SCPI 36 | except: 37 | from SCPI import SCPI 38 | 39 | from time import sleep 40 | import pyvisa as visa 41 | 42 | class RigolDP800(SCPI): 43 | """Basic class for controlling and accessing a Rigol DP8xx Power Supply""" 44 | 45 | def __init__(self, resource, wait=1.0, verbosity=0, **kwargs): 46 | """Init the class with the instruments resource string 47 | 48 | resource - resource string or VISA descriptor, like TCPIP0::172.16.2.13::INSTR 49 | wait - float that gives the default number of seconds to wait after sending each command 50 | verbosity - verbosity output - set to 0 for no debug output 51 | kwargs - other named options to pass when PyVISA open() like open_timeout=2.0 52 | """ 53 | super(RigolDP800, self).__init__(resource, max_chan=3, wait=wait, cmd_prefix=':', verbosity = verbosity, **kwargs) 54 | 55 | 56 | 57 | if __name__ == '__main__': 58 | import argparse 59 | parser = argparse.ArgumentParser(description='Access and control a Rigol DP800 power supply') 60 | parser.add_argument('chan', nargs='?', type=int, help='Channel to access/control (starts at 1)', default=1) 61 | args = parser.parse_args() 62 | 63 | from time import sleep 64 | from os import environ 65 | resource = environ.get('DP800_IP', 'TCPIP0::172.16.2.13::INSTR') 66 | rigol = RigolDP800(resource) 67 | rigol.open() 68 | 69 | ## set Remote Lock On 70 | #rigol.setRemoteLock() 71 | 72 | rigol.beeperOff() 73 | 74 | if not rigol.isOutputOn(args.chan): 75 | rigol.outputOn() 76 | 77 | print('Ch. {} Settings: {:6.4f} V {:6.4f} A'. 78 | format(args.chan, rigol.queryVoltage(), 79 | rigol.queryCurrent())) 80 | 81 | voltageSave = rigol.queryVoltage() 82 | 83 | #print(rigol.idn()) 84 | print('{:6.4f} V'.format(rigol.measureVoltage())) 85 | print('{:6.4f} A'.format(rigol.measureCurrent())) 86 | 87 | rigol.setVoltage(2.7) 88 | 89 | print('{:6.4f} V'.format(rigol.measureVoltage())) 90 | print('{:6.4f} A'.format(rigol.measureCurrent())) 91 | 92 | rigol.setVoltage(2.3) 93 | 94 | print('{:6.4f} V'.format(rigol.measureVoltage())) 95 | print('{:6.4f} A'.format(rigol.measureCurrent())) 96 | 97 | rigol.setVoltage(voltageSave) 98 | 99 | print('{:6.4f} V'.format(rigol.measureVoltage())) 100 | print('{:6.4f} A'.format(rigol.measureCurrent())) 101 | 102 | ## turn off the channel 103 | rigol.outputOff() 104 | 105 | rigol.beeperOn() 106 | 107 | ## return to LOCAL mode 108 | rigol.setLocal() 109 | 110 | rigol.close() 111 | -------------------------------------------------------------------------------- /dcps/BK9115.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | 5 | # Copyright (c) 2020, Stephen Goadhouse 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | #------------------------------------------------------------------------------- 26 | # Control a BK Precision 9115 and related DC Power Supplies with PyVISA 27 | #------------------------------------------------------------------------------- 28 | 29 | # For future Python3 compatibility: 30 | from __future__ import absolute_import 31 | from __future__ import division 32 | from __future__ import print_function 33 | 34 | try: 35 | from . import SCPI 36 | except: 37 | from SCPI import SCPI 38 | 39 | from time import sleep 40 | import pyvisa as visa 41 | 42 | class BK9115(SCPI): 43 | """Basic class for controlling and accessing a BK Precision 9115 DC Power Supply""" 44 | 45 | def __init__(self, resource, wait=1.0, verbosity=0, **kwargs): 46 | """Init the class with the instruments resource string 47 | 48 | resource - resource string or VISA descriptor, like USB0::INSTR 49 | wait - float that gives the default number of seconds to wait after sending each command 50 | verbosity - verbosity output - set to 0 for no debug output 51 | kwargs - other named options to pass when PyVISA open() like open_timeout=2.0 52 | """ 53 | super(BK9115, self).__init__(resource, max_chan=1, wait=wait, cmd_prefix='', verbosity=verbosity, read_termination = None, write_termination = '\r\n', **kwargs) 54 | 55 | 56 | 57 | if __name__ == '__main__': 58 | import argparse 59 | parser = argparse.ArgumentParser(description='Access and control a BK Precision 9115 DC power supply') 60 | parser.add_argument('chan', nargs='?', type=int, help='Channel to access/control (starts at 1)', default=1) 61 | args = parser.parse_args() 62 | 63 | from time import sleep 64 | from os import environ 65 | resource = environ.get('BK9115_USB', 'USB0::INSTR') 66 | bkps = BK9115(resource) 67 | bkps.open() 68 | 69 | print(bkps.idn()) 70 | 71 | # IMPORTANT: 9115 requires Remote to be set or else commands are ignored 72 | bkps.setRemote() 73 | 74 | ## set Remote Lock On 75 | #bkps.setRemoteLock() 76 | 77 | bkps.beeperOff() 78 | 79 | # normally would get channel from args.chan 80 | chan = args.chan 81 | # BK Precision 9115 has a single channel, so force chan to be 1 82 | chan = 1 83 | 84 | if not bkps.isOutputOn(chan): 85 | bkps.outputOn() 86 | 87 | print('Ch. {} Settings: {:6.4f} V {:6.4f} A'. 88 | format(chan, bkps.queryVoltage(), 89 | bkps.queryCurrent())) 90 | 91 | voltageSave = bkps.queryVoltage() 92 | ovpSave = bkps.queryVoltageProtection() 93 | 94 | print('{:6.4f} V'.format(bkps.measureVoltage())) 95 | print('{:6.4f} A'.format(bkps.measureCurrent())) 96 | 97 | bkps.setCurrent(0.1) 98 | bkps.setVoltage(2.7) 99 | 100 | print('{:6.4f} V'.format(bkps.measureVoltage())) 101 | print('{:6.4f} A'.format(bkps.measureCurrent())) 102 | 103 | bkps.setVoltage(2.3) 104 | 105 | print('{:6.4f} V'.format(bkps.measureVoltage())) 106 | print('{:6.4f} A'.format(bkps.measureCurrent())) 107 | 108 | print('OVP:') 109 | print('A: {:6.4f} V'.format(bkps.queryVoltageProtection())) 110 | bkps.setVoltageProtection(11.3, delay=0.010) 111 | print('B: {:6.4f} V'.format(bkps.queryVoltageProtection())) 112 | bkps.voltageProtectionOn() 113 | 114 | bkps.setVoltage(voltageSave) 115 | bkps.setVoltageProtection(ovpSave) 116 | 117 | print('{:6.4f} V'.format(bkps.measureVoltage())) 118 | print('{:6.4f} A'.format(bkps.measureCurrent())) 119 | 120 | ## turn off the channel 121 | bkps.outputOff() 122 | 123 | # The beeper sucks, do not turn it back on! 124 | #@@@#bkps.beeperOn() 125 | 126 | ## return to LOCAL mode 127 | bkps.setLocal() 128 | 129 | bkps.close() 130 | -------------------------------------------------------------------------------- /debug/powerCycle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Use to make power cycling easier 4 | 5 | # For future Python3 compatibility: 6 | from __future__ import absolute_import 7 | from __future__ import division 8 | from __future__ import print_function 9 | 10 | import sys 11 | 12 | from time import sleep 13 | import os 14 | import binascii 15 | import argparse 16 | import random 17 | import sys 18 | #import tty, termios 19 | 20 | # Print out command line for recording 21 | print('Executed with:',' '.join(sys.argv)) 22 | 23 | # Parse command line arguments 24 | parser = argparse.ArgumentParser() 25 | parser.add_argument('--off', '-0', action='store_true', help='instead of power cycle, just turn off power') 26 | parser.add_argument('--on', '-1', action='store_true', help='instead of power cycle, just turn on power') 27 | parser.add_argument('--random_time', '-r', action='store_true', help='have power supply remain off for a random number of seconds (< 10s)') 28 | locgroup = parser.add_mutually_exclusive_group(required=True) 29 | locgroup.add_argument('--psa', '-a', action='store_true', help='use Power Supply A') 30 | locgroup.add_argument('--psb', '-b', action='store_true', help='use Power Supply B') 31 | locgroup.add_argument('--psc', '-c', action='store_true', help='use Power Supply C') 32 | 33 | args = parser.parse_args() 34 | 35 | 36 | if (args.psa): 37 | # For control of the Rigol Power Supply, channel 1 38 | from dcps import RigolDP800 39 | 40 | # open connection to power supply 41 | #@@@#pwr = RigolDP800('USB0::0x1AB1::0x0E11::DP8B153600499::INSTR', open_timeout=5.0, verbosity=1) 42 | pwr = RigolDP800('USB0::6833::3601::DP8B153600499::0::INSTR', open_timeout=5.0, verbosity=1) 43 | pwrChan = 1 44 | elif (args.psb): 45 | # For control of the Rigol Power Supply, channel 2 46 | from dcps import RigolDP800 47 | 48 | # open connection to power supply 49 | pwr = RigolDP800('USB0::0x1AB1::0x0E11::DP8B153600499::INSTR') 50 | pwrChan = 2 51 | elif (args.psc): 52 | # For control of the Rigol Power Supply, channel 2 53 | from dcps import RigolDP800 54 | 55 | # open connection to power supply 56 | pwr = RigolDP800('USB0::0x1AB1::0x0E11::DP8B153600499::INSTR') 57 | pwrChan = 3 58 | else: 59 | # Error - must pick a power supply 60 | print('ERROR, must choose a power supply!') 61 | sys.exit() 62 | 63 | pwr.open() 64 | 65 | print(pwr.idn()) 66 | 67 | #if not pwr.isOutputOn(pwrChan): 68 | # # Enable channel 69 | # pwr.outputOn(pwrChan) 70 | 71 | print('\n\nBefore any change ...') 72 | print('Power Supply, channel {}, is {}: {:6.4f} V {:6.4f} A\n'. 73 | format(pwrChan, 74 | pwr.isOutputOn(pwrChan) and "ON" or "OFF", 75 | pwr.measureVoltage(pwrChan), 76 | pwr.measureCurrent(pwrChan))) 77 | 78 | def dotSleep(seconds): 79 | """ Sleep for desired seconds and output a '.' for each full second that has expired """ 80 | 81 | #fd = sys.stdin.fileno() 82 | #old_settings = termios.tcgetattr(fd) 83 | 84 | ctrlc = False 85 | try: 86 | while(seconds > 0): 87 | if (seconds > 1): 88 | per = 1 89 | else: 90 | per = seconds 91 | 92 | sleep(per) 93 | 94 | if (per == 1): 95 | print('.', end='') 96 | sys.stdout.flush() 97 | 98 | seconds -= per 99 | 100 | #try: 101 | # tty.setcbreak(sys.stdin.fileno()) 102 | # ch = sys.stdin.read(1) 103 | #finally: 104 | # termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) 105 | # 106 | #if (ch): 107 | # print('ch=',ch,end='') 108 | # sys.stdout.flush() 109 | 110 | except KeyboardInterrupt: 111 | #print('Got Ctrl-C') 112 | ctrlc = True 113 | 114 | return ctrlc 115 | 116 | 117 | def powerCycle(): 118 | if (args.random_time): 119 | time_off = random.uniform(1.5, 10.0) 120 | else: 121 | time_off = 1.5 122 | 123 | print(' Power Cycling with off time={:.2f}s '.format(time_off), end='') 124 | sys.stdout.flush() 125 | 126 | # Disable channel 127 | pwr.outputOff(pwrChan) 128 | 129 | # give some time for power supply to enable its output 130 | if (dotSleep(time_off)): 131 | # If asked to Quit, then Quit 132 | print(' Quitting GBT Test as requested.\n') 133 | return 1 134 | 135 | # Enable channel 136 | pwr.outputOn(pwrChan) 137 | 138 | print('P', end='') 139 | sys.stdout.flush() 140 | # give a second for power supply to enable its output 141 | if (dotSleep(1.0)): 142 | # If asked to Quit, then Quit 143 | print(' Quitting GBT Test as requested.\n') 144 | return 1 145 | 146 | # continue as normal 147 | return 0 148 | 149 | if (args.on): 150 | print("Turning on channel {}".format(pwrChan)) 151 | pwr.outputOn(pwrChan) 152 | elif (args.off): 153 | print("Turning off channel {}".format(pwrChan)) 154 | pwr.outputOff(pwrChan) 155 | else: 156 | powerCycle() 157 | print() # add line feed 158 | 159 | # return to LOCAL mode 160 | pwr.setLocal() 161 | 162 | pwr.close() 163 | 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dcps 2 | 3 | Control of DC Power Supplies through python 4 | 5 | This is intended to be a generic package to control various DC power 6 | supplies using various access methods with a common API. It utilizes 7 | pyVISA and the SCPI command set. For now, this supports only the 8 | following DC power supplies: 9 | 10 | * Rigol DP800 series *(tested with DP832A)* 11 | * Aim TTi PL-P series *(tested with PL303QMD-P)* 12 | * Aim TTi CPX series *(tested by a contributor with CPX400DP)* 13 | * BK Precision 9115 series *(tested with 9115)* 14 | * Agilent/Keysight E364xA series *(tested with E3642A)* 15 | 16 | These DC power supplies are each part of a series of products. All 17 | products within the series that use a common programming language 18 | should be supported but only the indicated models were used for 19 | development and testing. 20 | 21 | As new power supplies are added, they should each have their own sub-package. 22 | 23 | Other contributors have added support for the following DC power supplies: 24 | * ITECH 6500C/D series 2 quadrant DC Power Supply/load 25 | * KORAD KA series *(tested with Velleman PS3005D)* 26 | 27 | In addition to the above traditional power supplies, a few other 28 | instruments have been added that have a similar control paradigm such 29 | as current sources, volt meters and, perhaps in the future, source 30 | meters that can both source and measure voltages and currents. These 31 | all can either source a voltage or current and/or measure a voltage or 32 | current. They stub off unused functions so that common scripts can 33 | still be created with a common interface and they retain the ability 34 | to target any of these instruments. These alternative instruments that 35 | are supported are: 36 | 37 | * Keithley/Tektronix 622x series Precision Current Source *(tested with 6220)* 38 | * Keithley/Tektronix 2182/2182A Nanovoltmeter *(tested with 2182A)* 39 | * Keithley/Tektronix 2400 series SourceMeter *(tested with 2400)* 40 | 41 | 42 | # Installation 43 | You need to install the pyvisa and pyvisa-py packages. 44 | 45 | To install the dcps package, run the command: 46 | 47 | ``` 48 | python setup.py install 49 | ``` 50 | 51 | Alternatively, can add a path to this package to the environment 52 | variable PYTHONPATH or even add the path to it at the start of your 53 | python script. Use your favorite web search engine to find out more 54 | details. 55 | 56 | Even better, dcps is now on PyPi, so you can simply use the following 57 | and the required depedancies should get installed for you: 58 | 59 | ``` 60 | pip install dcps 61 | ``` 62 | 63 | ## Requirements 64 | * [python](http://www.python.org/) [Works with 2.7+ and 3+] 65 | * Python 2 is now officially "end of life" so upgrade your code to Python 3 66 | * [pyvisa 1.9](https://pyvisa.readthedocs.io/en/1.9.0/) 67 | * *avoid 1.11.0 because it fails to work on Fedora/CentOS/etc.* 68 | * [pyvisa-py 0.4.1](https://github.com/pyvisa/pyvisa-py/tree/48fbf9af00f970452c4af4b32a1a84fb89ee74dc/) 69 | 70 | With the use of pyvisa-py, should not have to install the National 71 | Instruments NIVA VISA driver. 72 | 73 | If using the USB communications method, must also install: 74 | * [PyUSB 1.0.2](https://github.com/pyusb/pyusb) 75 | * [libusb](http://www.libusb.info/) 76 | 77 | ## Ethernet to GPIB Interface 78 | 79 | Several of these devices, such as the Agilent and Keithley models, 80 | have no Ethernet or USB interface. To make them easier to access in a 81 | lab environment, An Ethernet to GPIB or USB to GPIB interface can be 82 | used. The only such interfaces that have been tested so far are: 83 | 84 | * [Prologix Ethernet to GPIB adapter](http://prologix.biz/gpib-ethernet-controller.html)
85 | 86 | * [KISS-488 Ethernet to GPIB adapter](https://www.ebay.com/itm/114514724752)
87 | 88 | 89 | For the Agilent/Keysight E364xA, both the Prologix and KISS-488 have 90 | been tested and work. For the Keithley 622x, 2182 and 2400, only the 91 | Prologix interface works. If a `TCPIP0` resource string is used for 92 | these models, the code automatically determines which device is 93 | used. See the code comments for these models to learn more. 94 | 95 | # WARNING! 96 | Be *really* careful since you are controlling a power supply that may 97 | be connected to something that does not like to go to 33V when you 98 | meant to output 3.3V but a bug in your script commanded 33V. That 99 | device connected to the power supply may express its displeasure of 100 | getting 33V by exploding all over the place. So be sure to do ALL 101 | testing without a device connected, as much as possible, and make use 102 | of the protections built into the power supply. For example, you can 103 | often set voltage and current limits that the power supply will obey 104 | and ignore requests by these commands to go outside these allowable 105 | ranges. There are even SCPI commands to set these limits, although it 106 | may be safer that they be set manually. 107 | 108 | # Usage 109 | The code is a very basic class for controlling and accessing the 110 | supported power supplies. Before running any example, be extra sure 111 | that the power supply is disconnected from any device in case voltsges 112 | unexpectedly go to unexpected values. 113 | 114 | If running the examples embedded in the individual package source 115 | files, be sure to edit the resource string or VISA descriptor of your 116 | particular device. For many of the packages, an environment variable 117 | can be set and used as the VISA resource string. 118 | 119 | * for RigolDP800.py, it is `DP800_IP` 120 | * for AimTTiPLP.py, it is `TTIPLP_IP` 121 | * for BK 9115, it is `BK9115_USB` 122 | * for Keysight E364xA, it is `E364XA_VISA` 123 | * for Keithley 622x, it is `K622X_VISA` 124 | * for Keithley 2182, it is `K2182_VISA` 125 | * for Keithley 24xx, it is `K2400_VISA` 126 | 127 | ```python 128 | # Lookup environment variable DP800_IP and use it as the resource 129 | # name or use the TCPIP0 string if the environment variable does 130 | # not exist 131 | from dcps import RigolDP800 132 | from os import environ 133 | resource = environ.get('DP800_IP', 'TCPIP0::172.16.2.13::INSTR') 134 | 135 | # create your visa instrument 136 | rigol = RigolDP800(resource) 137 | rigol.open() 138 | 139 | # set to channel 1 140 | rigol.channel = 1 141 | 142 | # Query the voltage/current limits of the power supply 143 | print('Ch. {} Settings: {:6.4f} V {:6.4f} A'. 144 | format(rigol.channel, rigol.queryVoltage(), 145 | rigol.queryCurrent())) 146 | 147 | # Enable output of channel 148 | rigol.outputOn() 149 | 150 | # Measure actual voltage and current 151 | print('{:6.4f} V'.format(rigol.measureVoltage())) 152 | print('{:6.4f} A'.format(rigol.measureCurrent())) 153 | 154 | # change voltage output to 2.7V 155 | rigol.setVoltage(2.7) 156 | 157 | # turn off the channel 158 | rigol.outputOff() 159 | 160 | # return to LOCAL mode 161 | rigol.setLocal() 162 | 163 | rigol.close() 164 | ``` 165 | 166 | ## Taking it Further 167 | This implements a small subset of available commands. 168 | 169 | For information on what is possible with specific commands for the 170 | various supported power supplies and related equipment, see: 171 | 172 | * Rigol DP8xx: [Rigol DP800 Programming Guide](http://beyondmeasure.rigoltech.com/acton/attachment/1579/f-03a1/1/-/-/-/-/DP800%20Programming%20Guide.pdf) 173 | * Aim TTi PL-P: [New PL & PL-P Series Instruction Manual](http://resources.aimtti.com/manuals/New_PL+PL-P_Series_Instruction_Manual-Iss18.pdf) 174 | * Aim TTi CPX: [CPX400DP PowerFlex Dual DC Power Supply Instruction Manual](https://resources.aimtti.com/manuals/CPX400DP_Instruction_Manual-Iss1.pdf) 175 | * BK Precision 9115: [9115 Multi-Range DC Power Supply PROGRAMMING MANUAL](https://bkpmedia.s3.amazonaws.com/downloads/programming_manuals/en-us/9115_series_programming_manual.pdf) 176 | * ITECH IT6500C Series: [IT6500C Series User Manual](https://cdn.itechate.com/uploadfiles/用户手册/user%20manual/it6500/IT6500C%20User%20Manual-EN.pdf) 177 | and [IT6500C/D Series Programming Guide](https://www.calpower.it/gallery/cpit6500cd-programming-guide-en2020.pdf) 178 | * Agilent/Keysight E364xA: [Keysight E364xA Single Output DC Power Supplies](https://www.keysight.com/us/en/assets/9018-01165/user-manuals/9018-01165.pdf?success=true) 179 | * Keithley/Tektronix 622x: [Model 6220 DC Current Source Model 6221 AC and DC Current Source User's Manual](https://www.tek.com/product-series/ultra-sensitive-current-sources-series-6200-manual/model-6220-dc-current-source-model) 180 | * Keithley/Tektronix 2182/2182A Nanovoltmeter: [Models 2182 and 2182A Nanovoltmeter User's Manual](https://www.tek.com/keithley-low-level-sensitive-and-specialty-instruments/keithley-nanovoltmeter-model-2182a-manual/models-2182-and-2182a-nanovoltmeter-users-manual) 181 | * Keithley/Tektronix 2400 series SourceMeter: [Series 2400 SourceMeter User's Manual](https://download.tek.com/manual/2400S-900-01_K-Sep2011_User.pdf) 182 | 183 | For what is possible with general power supplies that adhere to the 184 | IEEE 488 SCPI specification, like the Rigol DP8xx, see the 185 | [SCPI 1999 Specification](http://www.ivifoundation.org/docs/scpi-99.pdf) 186 | and the 187 | [SCPI Wikipedia](https://en.wikipedia.org/wiki/Standard_Commands_for_Programmable_Instruments) entry. 188 | 189 | # Contact 190 | Please send bug reports or feedback to [Stephen Goadhouse](https://github.com/sgoadhouse) 191 | 192 | -------------------------------------------------------------------------------- /dcps/KoradKAseries.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | 5 | # Copyright (c) 2025, Mikkel Jeppesen 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | #------------------------------------------------------------------------------- 26 | # Control a KORAD KA-series, or compatible (many brands), powersupply with PyVISA 27 | #------------------------------------------------------------------------------- 28 | 29 | try: 30 | from . import SCPI 31 | from . import NotImplemented 32 | except (ImportError, ValueError): 33 | from SCPI import SCPI 34 | from Warnings import NotImplemented 35 | 36 | from time import sleep 37 | import pyvisa as visa 38 | import re 39 | import sys 40 | from dataclasses import dataclass 41 | from enum import IntEnum 42 | 43 | 44 | class tracking_mode(IntEnum): 45 | independent = 0b00 46 | series = 0b01 47 | parallel = 0b11 48 | undefined = 0b10 49 | 50 | class cc_cv_mode(IntEnum): 51 | CC = 0 52 | CV = 1 53 | 54 | @dataclass 55 | class Status: 56 | ch1_mode: cc_cv_mode = cc_cv_mode.CV 57 | ch2_mode: cc_cv_mode = cc_cv_mode.CV 58 | tracking: tracking_mode = tracking_mode.independent 59 | beeper: bool = True 60 | lock: bool = False 61 | output: bool = False 62 | 63 | class KAseries(SCPI): 64 | """Basic class for controlling and accessing a KORAD KA-Series power supplies 65 | This series of power supplies don't adhere to any LXI specifications or SCPI syntax. 66 | It does however implement *IDN? 67 | The underlying accessor functions of SCPI.py are used but the top level are all re-written 68 | below to handle the very different command syntax. This shows how 69 | one might add packages to support other such power supplies that 70 | only minimally adhere to the command standards. 71 | """ 72 | 73 | def __init__(self, resource, wait=1.0, verbosity=0, max_chan=1, **kwargs): 74 | """Init the class with the instruments resource string 75 | 76 | resource - resource string or VISA descriptor, like TCPIP0::192.168.1.100::9221::SOCKET 77 | wait - float that gives the default number of seconds to wait after sending each command 78 | verbosity - verbosity output - set to 0 for no debug output 79 | max_chan - Most KA-series PSUs are single channel. PSUs like the KA3305 however are 3ch. Set this to have more channels 80 | kwargs - other named options to pass when PyVISA open() like open_timeout=2.0 81 | 82 | Compatible Velleman rebrand manual https://web.archive.org/web/20250110222632/https://cdn.velleman.eu/downloads/2/ps3005da501.pdf 83 | Programming manual https://ia600605.us.archive.org/0/items/series-protocol-v-2.0-of-remote-control/Series%20Protocol%20V2.0%20of%20Remote%20Control.pdf 84 | """ 85 | 86 | 87 | super(KAseries, self).__init__(resource, max_chan=max_chan, wait=wait, 88 | cmd_prefix='', 89 | verbosity=verbosity, 90 | read_termination='\0', 91 | write_termination='', 92 | **kwargs) 93 | 94 | def setLocal(self): 95 | """This supply doesn't support this. It'll go to local mode after 8s of no commands 96 | """ 97 | NotImplemented('Function not implemented on device') 98 | pass 99 | 100 | def setRemote(self): 101 | """Set the power supply to REMOTE mode where it is controlled via VISA 102 | """ 103 | # Not supported explicitly by this power supply but the power 104 | # supply does switch to REMOTE automatically. So send anything to it 105 | self._instWrite('\n') 106 | 107 | def setRemoteLock(self): 108 | """Set the power supply to REMOTE Lock mode where it is 109 | controlled via VISA & front panel is locked out 110 | """ 111 | self.setRemote() 112 | 113 | def beeperOn(self): 114 | """Enable the system beeper for the instrument""" 115 | self._instWrite('BEEP1') 116 | 117 | def beeperOff(self): 118 | """Disable the system beeper for the instrument""" 119 | self._instWrite('BEEP0') 120 | 121 | def get_status(self) -> Status: 122 | """Parses the 8-bit status message returned from the PSU into a status object 123 | """ 124 | 125 | self._instWrite('STATUS?') 126 | resp = self._inst.read_bytes(count=1, chunk_size=1) 127 | resp = int.from_bytes(resp, sys.byteorder) 128 | #resp = self._inst.read_binary_values(is_big_endian = (sys.byteorder == 'big'), data_points=1, chunk_size=1) 129 | 130 | status = Status() 131 | status.ch1_mode = cc_cv_mode(resp & 0b00000001 >> 0) 132 | status.ch2_mode = cc_cv_mode(resp & 0b00000010 >> 1) 133 | status.tracking = tracking_mode((resp & 0b00001100) >>2) 134 | status.beeper = bool(resp & 0b00010000) 135 | status.lock = bool(resp & 0b00100000) 136 | status.output = bool(resp & 0b01000000) 137 | 138 | return status 139 | 140 | def isOutputOn(self, channel=None) -> bool: 141 | """Return true if the output of channel is ON, else false 142 | 143 | channel - ignored. All channels are on/off together 144 | """ 145 | 146 | # If a channel number is passed in, make it the 147 | # current channel 148 | if channel is not None: 149 | self.channel = channel 150 | 151 | status = self.get_status() 152 | return status.output 153 | 154 | def outputOn(self, channel=None, wait=None): 155 | """Turn on the output for channel 156 | 157 | wait - number of seconds to wait after sending command 158 | channel - ignored. All channels are on/off together 159 | """ 160 | 161 | # If a channel number is passed in, make it the 162 | # current channel 163 | if channel is not None: 164 | self.channel = channel 165 | 166 | # If a wait time is NOT passed in, set wait to the 167 | # default time 168 | if wait is None: 169 | wait = self._wait 170 | 171 | self._instWrite('OUT1') 172 | sleep(wait) # give some time for PS to respond 173 | 174 | def outputOff(self, channel=None, wait=None): 175 | """Turn off the output for channel 176 | 177 | channel - ignored. All channels are on/off together 178 | """ 179 | 180 | # If a channel number is passed in, make it the 181 | # current channel 182 | if channel is not None: 183 | self.channel = channel 184 | 185 | # If a wait time is NOT passed in, set wait to the 186 | # default time 187 | if wait is None: 188 | wait = self._wait 189 | 190 | self._instWrite('OUT0') 191 | sleep(wait) # give some time for PS to respond 192 | 193 | def outputOnAll(self, wait=None): 194 | """Turn on the output for ALL channels 195 | 196 | """ 197 | 198 | self.outputOn(wait=wait) 199 | 200 | def outputOffAll(self, wait=None): 201 | """Turn off the output for ALL channels 202 | 203 | """ 204 | 205 | self.outputOff(wait=wait) 206 | 207 | def setVoltage(self, voltage, channel=None, wait=None): 208 | """Set the voltage value for the channel 209 | 210 | voltage - desired voltage value as a floating point number 211 | wait - number of seconds to wait after sending command 212 | channel - number of the channel starting at 1 213 | """ 214 | 215 | # If a channel number is passed in, make it the 216 | # current channel 217 | if channel is not None: 218 | self.channel = channel 219 | 220 | # If a wait time is NOT passed in, set wait to the 221 | # default time 222 | if wait is None: 223 | wait = self._wait 224 | 225 | 226 | str = f'VSET{self.channel}:{voltage:05.2f}' 227 | self._instWrite(str) 228 | sleep(wait) # give some time for PS to respond 229 | 230 | def setCurrent(self, current, channel=None, wait=None): 231 | """Set the current value for the channel 232 | 233 | current - desired current value as a floating point number 234 | channel - number of the channel starting at 1 235 | wait - number of seconds to wait after sending command 236 | """ 237 | 238 | # If a channel number is passed in, make it the 239 | # current channel 240 | if channel is not None: 241 | self.channel = channel 242 | 243 | # If a wait time is NOT passed in, set wait to the 244 | # default time 245 | if wait is None: 246 | wait = self._wait 247 | 248 | str = f'ISET{self.channel}:{current:05.3f}' 249 | self._instWrite(str) 250 | sleep(wait) # give some time for PS to respond 251 | 252 | 253 | def queryVoltage(self, channel=None): 254 | """Return what voltage set value is (not the measured voltage, 255 | but the set voltage) 256 | 257 | channel - number of the channel starting at 1 258 | """ 259 | 260 | # If a channel number is passed in, make it the 261 | # current channel 262 | if channel is not None: 263 | self.channel = channel 264 | 265 | str = f'VSET{self.channel}?' 266 | self._instWrite(str) 267 | resp = self._inst.read_bytes(count=5, chunk_size=1) 268 | 269 | return float(resp) 270 | 271 | def queryCurrent(self, channel=None): 272 | """Return what current set value is (not the measured current, 273 | but the set current) 274 | 275 | channel - number of the channel starting at 1 276 | """ 277 | 278 | # If a channel number is passed in, make it the 279 | # current channel 280 | if channel is not None: 281 | self.channel = channel 282 | 283 | str = f'ISET{self.channel}?' 284 | self._instWrite(str) 285 | resp = self._inst.read_bytes(count=6, chunk_size=1) 286 | 287 | # There's a bug where the PSU returns 6 characters. The 6th character is garbage, so we drop it 288 | return float(resp[:5]) 289 | 290 | def measureVoltage(self, channel=None): 291 | """Read and return a voltage measurement from channel 292 | 293 | channel - number of the channel starting at 1 294 | """ 295 | 296 | NotImplemented("PSU doesn't support measuring voltage. Returing set value") 297 | return self.queryVoltage(channel) 298 | 299 | def measureCurrent(self, channel=None): 300 | """Read and return a current measurement from channel 301 | 302 | channel - number of the channel starting at 1 303 | """ 304 | 305 | NotImplemented("PSU doesn't support measuring current. Returing set value") 306 | return self.queryCurrent(channel) 307 | 308 | if __name__ == "__main__": 309 | psu = KAseries("ASRL8::INSTR") 310 | psu.open() 311 | print(psu.idn()) 312 | 313 | psu.setCurrent(1) 314 | psu.setVoltage(15) 315 | psu.outputOn() 316 | print(psu.get_status()) 317 | print(psu.queryVoltage()) 318 | print(psu.queryCurrent()) 319 | psu.outputOff() 320 | psu.setVoltage(10) 321 | psu.setCurrent(2) 322 | 323 | -------------------------------------------------------------------------------- /dcps/PowerTestBoard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2017, Emmanuel Blot 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # * Redistributions of source code must retain the above copyright 10 | # notice, this list of conditions and the following disclaimer. 11 | # * Redistributions in binary form must reproduce the above copyright 12 | # notice, this list of conditions and the following disclaimer in the 13 | # documentation and/or other materials provided with the distribution. 14 | # * Neither the name of the Neotion nor the names of its contributors may 15 | # be used to endorse or promote products derived from this software 16 | # without specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21 | # ARE DISCLAIMED. IN NO EVENT SHALL NEOTION BE LIABLE FOR ANY DIRECT, INDIRECT, 22 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 24 | # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 26 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 27 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | from binascii import hexlify 30 | import argparse 31 | #@@@#from doctest import testmod 32 | from os import environ 33 | from pyftdi import FtdiLogger 34 | from pyftdi.i2c import I2cController, I2cIOError, I2cNackError 35 | from pyftdi.misc import hexdump 36 | #@@@#from i2cflash.serialeeprom import SerialEepromManager 37 | from sys import modules, stdout 38 | #@@@#from math import floor 39 | import logging 40 | #@@@#import unittest 41 | from time import sleep 42 | 43 | #@@@#ftdi_url = 'ftdi://ftdi:4232h/1' 44 | ftdi_url_const = 'ftdi://ftdi:4232:FTK1RRYC/1' 45 | 46 | class I2cPca9534(object): 47 | """Simple Class to access a PCA9534 GPIO device on I2C bus 48 | """ 49 | 50 | def __init__(self, address=0x20): 51 | self._i2c = I2cController() 52 | self._addr = address 53 | self.regs = {'INPUT':0, 'OUTPUT':1, 'POLARITY':2, 'CONFIG':3} 54 | self._freq = 400000 55 | 56 | def open(self, url): 57 | """Open an I2c connection, specified by url, to a slave""" 58 | self._i2c.configure(url, frequency=self._freq) 59 | self._port = self._i2c.get_port(self._addr) # open a port to the I2C PCA9533 device 60 | 61 | def flush(self): 62 | """Flush the I2C connection""" 63 | self._i2c.flush() 64 | 65 | def close(self): 66 | """Close the I2C connection""" 67 | #@@@#self._i2c.flush() 68 | self._i2c.terminate() 69 | #pyftdi.ftdi.ftdi.close() 70 | 71 | def readReg(self, reg): 72 | regVal = [reg] 73 | regVal = [regVal[0]] # make sure only a single value for reg and not a list 74 | return self._port.exchange(regVal, 1)[0] 75 | 76 | def writeReg(self, reg, val): 77 | # Create data array to send. Start with the register number for 78 | # LS0 and add the desired value 79 | #data = bytes([reg]) 80 | #data = bytes(data[0]) + bytes([val]) # Make sure that reg parameter is a single value 81 | self._port.write_to(reg, [val]) 82 | 83 | def writeVal(self, val): 84 | """ Only send a value to write without preceeding with a register number """ 85 | 86 | # Create data array to send. Start with the register number for 87 | # LS0 and add the desired value 88 | #data = bytes([reg]) 89 | #data = bytes(data[0]) + bytes([val]) # Make sure that reg parameter is a single value 90 | self._port.write([val]) 91 | 92 | def writeBit(self, reg, bit, val): 93 | msk = 0x01 94 | vvv = val and 0x01 95 | ## Bit shift over to the corresponding bit 96 | for x in range(0,bit): 97 | msk <<= 1 98 | vvv <<= 1 99 | tmp = self.readReg(reg) # read from the register 100 | tmp &= ~msk # clear out the selected bit 101 | tmp |= vvv # OR in the new bit value 102 | self.writeReg(reg, tmp) 103 | 104 | def setBit(self, reg, bit): 105 | self.writeBit(reg, bit, 1) 106 | 107 | def clrBit(self, reg, bit): 108 | self.writeBit(reg, bit, 0) 109 | 110 | def readBit(self, reg, bit): 111 | msk = 0x01 112 | ## Bit shift over to the corresponding bit 113 | tmp = self.readReg(reg) # read from the register 114 | for x in range(0,bit): 115 | tmp >>= 1 116 | tmp &= msk # mask out the bit 117 | return tmp 118 | 119 | def read_all(self): 120 | """Read all registers and print their values out""" 121 | 122 | for regname in self.regs: 123 | print("{:10} (0x{:02x}): 0x{:02x}".format(regname, self.regs[regname], self.readReg(self.regs[regname]))) 124 | 125 | print('') 126 | 127 | 128 | class PowerTestBoard(object): 129 | """Simple Class to enable/disable DC circuits on the Power Test Board 130 | """ 131 | 132 | def __init__(self, ftdi_url): 133 | self._ftdi_url = ftdi_url 134 | self._circuits = { '0V85':0, 135 | '1V2-A':1, 136 | '1V2-B':2, 137 | '0V9':3, 138 | '1V8-A':4, 139 | '1V8-B':5, 140 | '1V8-C':6, 141 | '1V8-D':7, 142 | '3V3-A':8, 143 | '3V3-B':8, # 3V75-B circuit with switch set for 3.3V - so really using 3V3-A 144 | '3V75-B':9,# 3V75-B circuit with switch set for 3.75V 145 | '3V3-C':10, 146 | '3V3-D':11, 147 | } 148 | 149 | FtdiLogger.log.addHandler(logging.StreamHandler(stdout)) 150 | level = environ.get('FTDI_LOGLEVEL', 'info').upper() 151 | try: 152 | loglevel = getattr(logging, level) 153 | except AttributeError: 154 | raise ValueError('Invalid log level: %s', level) 155 | FtdiLogger.set_level(loglevel) 156 | 157 | 158 | @property 159 | def circuits(self): 160 | return self._circuits 161 | 162 | def test_i2c_gpio(self): 163 | i2c = I2cPca9534() 164 | i2c.open(self._ftdi_url) 165 | 166 | try: 167 | for x in range(0, 8): 168 | i2c.read_all() 169 | i2c.setBit(i2c.regs['POLARITY'], x) 170 | print('') 171 | 172 | i2c.read_all() 173 | print('') 174 | 175 | for x in range(0, 8): 176 | i2c.read_all() 177 | i2c.clrBit(i2c.regs['POLARITY'], x) 178 | print('') 179 | 180 | i2c.read_all() 181 | print('') 182 | 183 | 184 | except I2cIOError: 185 | print("I2C I/O Error!\n") 186 | #print("\nI2C Flush!") 187 | i2c.flush() 188 | 189 | except I2cNackError: 190 | print("I2C NACK Error!\n") 191 | #print("\nI2C Flush!") 192 | i2c.flush() 193 | 194 | except KeyboardInterrupt: 195 | #print("\nI2C Flush!") 196 | i2c.flush() 197 | 198 | #print("\nI2C Close!") 199 | #sleep(0.1) 200 | i2c.close() 201 | 202 | def powerEnable(self, circuit, on): 203 | # circuit is a value 0 - 11 204 | # on is 0 to turn off and non-o to turn on 205 | addrs = [0x20, 0x21, 0x22] # I2C addresses of PCA9534 206 | myaddr = addrs[circuit // 4] 207 | bit = circuit % 4 208 | 209 | i2c = I2cPca9534(myaddr) 210 | i2c.open(self._ftdi_url) 211 | 212 | try: 213 | # If circuit is 4-7 (i.e. addr 0x21) be sure to setup POLARITY 214 | # different than default 215 | if myaddr == 0x21: 216 | i2c.writeReg(i2c.regs['POLARITY'],0x30) 217 | 218 | # Write the OUTPUT bit 219 | i2c.writeBit(i2c.regs['OUTPUT'], bit, on) 220 | # Make sure bit is configured as an OUTPUT 221 | i2c.clrBit(i2c.regs['CONFIG'], bit) 222 | 223 | #@@@#i2c.read_all() 224 | #@@@#print('') 225 | 226 | 227 | except I2cIOError: 228 | print("I2C I/O Error!\n") 229 | #print("\nI2C Flush!") 230 | i2c.flush() 231 | 232 | except I2cNackError: 233 | print("I2C NACK Error!\n") 234 | #print("\nI2C Flush!") 235 | i2c.flush() 236 | 237 | except KeyboardInterrupt: 238 | #print("\nI2C Flush!") 239 | i2c.flush() 240 | 241 | #print("\nI2C Close!") 242 | #sleep(0.1) 243 | i2c.close() 244 | 245 | 246 | def powerStatus(self, circuit): 247 | # check circuit status 248 | #@@@ Make this a function! @@@ 249 | addrs = [0x20, 0x21, 0x22] # I2C addresses of PCA9534 250 | myaddr = addrs[circuit // 4] 251 | enBit = circuit % 4 252 | pgBit = enBit + 4 253 | 254 | # Set return values to -1 to indicate error if it does not work 255 | en = -1 256 | pg = -1 257 | 258 | i2c = I2cPca9534(myaddr) 259 | i2c.open(self._ftdi_url) 260 | 261 | try: 262 | # If circuit is 4-7 (i.e. addr 0x21) be sure to setup POLARITY 263 | # different than default 264 | if myaddr == 0x21: 265 | i2c.writeReg(i2c.regs['POLARITY'],0x30) 266 | 267 | # Read the enable INPUT bit 268 | en = i2c.readBit(i2c.regs['INPUT'], enBit) 269 | # Read the power good INPUT bit 270 | pg = i2c.readBit(i2c.regs['INPUT'], pgBit) 271 | 272 | except I2cIOError: 273 | print("I2C I/O Error!\n") 274 | #print("\nI2C Flush!") 275 | i2c.flush() 276 | 277 | except I2cNackError: 278 | print("I2C NACK Error!\n") 279 | #print("\nI2C Flush!") 280 | i2c.flush() 281 | 282 | except KeyboardInterrupt: 283 | #print("\nI2C Flush!") 284 | i2c.flush() 285 | 286 | #print("\nI2C Close!") 287 | #sleep(0.1) 288 | i2c.close() 289 | 290 | return (en, pg) 291 | 292 | 293 | def validate_circuits(self, value): 294 | value = value.upper() 295 | if value in self._circuits: 296 | return value 297 | else: 298 | raise argparse.ArgumentTypeError(f"'{value}' is not in recognized circuit list: \n{list(self._circuits.keys())}") 299 | 300 | if __name__ == '__main__': 301 | #@@@#testmod(modules[__name__]) 302 | 303 | ptb = PowerTestBoard(environ.get('FTDI_DEVICE', ftdi_url_const)) 304 | 305 | parser = argparse.ArgumentParser(description=f'Enable/Disable/Status DC Circuits on the Power Test Board. List of circuits: {list(ptb.circuits.keys())}') 306 | 307 | # Choose EITHER ON or OFF and the circuits on the command line 308 | # will be enabled or disable. If neither of these is selected, 309 | # then get status of all circuits. 310 | mutex_grp = parser.add_mutually_exclusive_group(required=False) 311 | mutex_grp.add_argument('-1', '--on', action='store_true', help='enable/turn ON the circuits') 312 | mutex_grp.add_argument('-0', '--off', action='store_true', help='disable/turn OFF the circuits') 313 | 314 | 315 | parser.add_argument('list_of_circuits', metavar='circuits', type=ptb.validate_circuits, nargs='*', help='a list of circuits - or all if omitted') 316 | 317 | args = parser.parse_args() 318 | 319 | try: 320 | circuit_list = args.list_of_circuits 321 | 322 | # If no list given, then use all circuits 323 | if len(circuit_list) <= 0: 324 | circuit_list = ptb.circuits.keys() 325 | 326 | for circ in circuit_list: 327 | if args.on: 328 | ptb.powerEnable(ptb.circuits[circ],1) 329 | print(f"{circ:8s}\tTurned ON") 330 | elif args.off: 331 | ptb.powerEnable(ptb.circuits[circ],0) 332 | print(f"{circ:8s}\tTurned OFF") 333 | else: 334 | (en, pg) = ptb.powerStatus(ptb.circuits[circ]) 335 | if (en == -1): 336 | print(f"{circ:8s}\tEN: ?\tPG: ?") 337 | else: 338 | print(f"{circ:8s}\tEN: {en}\tPG: {pg}") 339 | 340 | #@@@#test_i2c_gpio() 341 | 342 | #powerEnable(0,0) 343 | #powerEnable(1,0) 344 | #powerEnable(2,0) 345 | #powerEnable(3,0) 346 | 347 | #powerEnable(4,0) 348 | #powerEnable(5,0) 349 | #powerEnable(6,0) 350 | #powerEnable(7,0) 351 | 352 | #powerEnable(8,0) 353 | #powerEnable(9,0) 354 | #powerEnable(10,0) 355 | #powerEnable(11,0) 356 | 357 | except KeyboardInterrupt: 358 | exit(2) 359 | 360 | -------------------------------------------------------------------------------- /dcps/RigolDL3000.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | 5 | # Copyright (c) 2018, 2021, 2023, Stephen Goadhouse 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | #------------------------------------------------------------------------------- 26 | # Control a Rigol DL3000 family of DC Electronic Load with PyVISA 27 | #------------------------------------------------------------------------------- 28 | 29 | # For future Python3 compatibility: 30 | from __future__ import absolute_import 31 | from __future__ import division 32 | from __future__ import print_function 33 | 34 | try: 35 | from . import SCPI 36 | except: 37 | from SCPI import SCPI 38 | 39 | from time import sleep 40 | import pyvisa as visa 41 | 42 | class RigolDL3000(SCPI): 43 | """Basic class for controlling and accessing a Rigol DL3000 Family Electronic Load""" 44 | 45 | ## Dictionary to translate SCPI commands for this device 46 | _xlateCmdTbl = { 47 | 'setVoltage': 'SOURce:VOLTage:LEVel:IMMediate {}', 48 | 'setVoltageRangeAuto': None, 49 | 'setVoltageRange': 'SOURce:VOLTage:RANGe {1:}', 50 | 'setCurrent': 'SOURce:CURRent:LEVel:IMMediate {}', 51 | 'setCurrentRangeAuto': None, 52 | 'setCurrentRange': 'SOURce:CURRent:RANGe {1:}', 53 | 'queryVoltage': 'SOURce:VOLTage:LEVel:IMMediate?', 54 | 'queryVoltageRangeAuto': None, 55 | 'queryVoltageRange': 'SOURce:VOLTage:RANGe?', 56 | 'queryCurrent': 'SOURce:CURRent:LEVel:IMMediate?', 57 | 'queryCurrentRangeAuto': None, 58 | 'queryCurrentRange': 'SOURce:CURRent:RANGe?', 59 | } 60 | 61 | def __init__(self, resource, wait=1.0, verbosity=0, **kwargs): 62 | """Init the class with the instruments resource string 63 | 64 | resource - resource string or VISA descriptor, like TCPIP0::172.16.2.13::INSTR 65 | wait - float that gives the default number of seconds to wait after sending each command 66 | verbosity - verbosity output - set to 0 for no debug output 67 | kwargs - other named options to pass when PyVISA open() like open_timeout=2.0 68 | """ 69 | super(RigolDL3000, self).__init__(resource, max_chan=1, wait=wait, cmd_prefix=':', read_termination='\n', verbosity = verbosity, **kwargs) 70 | 71 | 72 | def beeperOn(self): 73 | """Enable the system beeper for the instrument""" 74 | # NOTE: Unsupported command by this device. However, 75 | # instead of raising an exception and breaking any scripts, 76 | # simply return quietly. 77 | pass 78 | 79 | def beeperOff(self): 80 | """Disable the system beeper for the instrument""" 81 | # NOTE: Unsupported command by this device. However, 82 | # instead of raising an exception and breaking any scripts, 83 | # simply return quietly. 84 | pass 85 | 86 | def setLocal(self): 87 | """Set the instrument to LOCAL mode where front panel keys 88 | work again. 89 | 90 | WARNING! MUST BE LAST Command sent or else Instrument goes back to Remote mode 91 | 92 | """ 93 | 94 | # enable Virtual Front Panel 95 | self._instWrite('DEBug:KEY 1') 96 | sleep(0.5) 97 | k = 8 # Press Local key 98 | self._instWrite('SYSTem:KEY {}'.format(k)) 99 | 100 | #for k in [9,40,40,40,40,0,8]: 101 | # print('Pressing key {}'.format(k)) 102 | # self._instWrite('SYSTem:KEY {}'.format(k)) 103 | # sleep(1.0) 104 | 105 | def setRemote(self): 106 | """Set the instrument to REMOTE mode where it is controlled via VISA 107 | """ 108 | 109 | # NOTE: Unsupported command by this device. However, with any 110 | # command sent to the DL3000, it automatically goes into 111 | # REMOTE mode. Instead of raising an exception and breaking 112 | # any scripts, simply return quietly. 113 | pass 114 | 115 | def setRemoteLock(self): 116 | """Set the instrument to REMOTE Lock mode where it is 117 | controlled via VISA & front panel is locked out 118 | """ 119 | # NOTE: Unsupported command by this device. However, with any 120 | # command sent to the DL3000, it automatically goes into 121 | # REMOTE mode. 122 | # 123 | # Disable the remote virtual panel, just in case 124 | self._instWrite('DEBug:KEY 0') 125 | 126 | 127 | 128 | ################################################################### 129 | # Commands Specific to DL3000 130 | ################################################################### 131 | 132 | def setImonExt(self,on): 133 | """Enable the IMON External output. After a *RST this is 134 | disabled. Could not find a command that sets this so having to 135 | use the KEY command and hope it is right since there is no 136 | feedback. 137 | 138 | """ 139 | 140 | #@@@#print("ImonExt") 141 | 142 | # enable Virtual Front Panel 143 | self._instWrite('DEBug:KEY 1') 144 | sleep(0.3) 145 | self._instWrite('SYSTem:KEY {}'.format(9)) # Utiliity 146 | 147 | for i in range(7): 148 | # Send 7 Down Arrows 149 | self._instWrite('SYSTem:KEY {}'.format(40)) # Down Arrow 150 | 151 | if (on): 152 | # If turning ON, must assume this is being called AFTER a *RST or when it is known to be OFF 153 | # Use Left Arrow to Enable it 154 | self._instWrite('SYSTem:KEY {}'.format(37)) # Left Arrow 155 | else: 156 | # If turning OFF, assume that it must be ON (no feedback so must do it this way) 157 | # Use Right Arrow to Enable it 158 | self._instWrite('SYSTem:KEY {}'.format(38)) # Right Arrow 159 | 160 | # Give time for someone to see this, if they are interested 161 | sleep(1.0) 162 | 163 | # Leave utility menu by "pressing" the key again 164 | self._instWrite('SYSTem:KEY {}'.format(9)) # Utiliity 165 | 166 | # disable Virtual Front Panel 167 | self._instWrite('DEBug:KEY 0') 168 | 169 | def setDigitalOutput(self,left,count): 170 | """Enable the Digital output. After a *RST this is 171 | disabled. Could not find a command that sets this so having to 172 | use the KEY command and hope it is right since there is no 173 | feedback. 174 | 175 | left - True/False: if True, use Left Arrow, if False use Right Arrow 176 | count - number of Left or Right arrows 177 | """ 178 | 179 | #@@@#print("Digital Output") 180 | 181 | # enable Virtual Front Panel 182 | self._instWrite('DEBug:KEY 1') 183 | sleep(0.3) 184 | self._instWrite('SYSTem:KEY {}'.format(9)) # Utiliity 185 | 186 | for i in range(4): 187 | # Send 4 Down Arrows 188 | self._instWrite('SYSTem:KEY {}'.format(40)) # Down Arrow 189 | 190 | for i in range(count): 191 | if (left): 192 | # using Left Arrow - the caller has to keep track of the position since this function cannot query it 193 | self._instWrite('SYSTem:KEY {}'.format(37)) # Left Arrow 194 | else: 195 | # using Right Arrow - the caller has to keep track of the position since this function cannot query it 196 | self._instWrite('SYSTem:KEY {}'.format(38)) # Right Arrow 197 | 198 | # Give time for someone to see this, if they are interested 199 | sleep(1.0) 200 | 201 | # Leave utility menu by "pressing" the key again 202 | self._instWrite('SYSTem:KEY {}'.format(9)) # Utiliity 203 | 204 | # disable Virtual Front Panel 205 | self._instWrite('DEBug:KEY 0') 206 | 207 | 208 | def setCurrentVON(self,voltage,wait=None): 209 | """Set the voltage level for when in Constant Current mode that the load starts to sink. 210 | 211 | voltage - desired voltage value as a floating point number 212 | wait - number of seconds to wait after sending command 213 | """ 214 | 215 | # If a wait time is NOT passed in, set wait to the 216 | # default time 217 | if wait is None: 218 | wait = self._wait 219 | 220 | self._instWrite('SOURce:CURRent:VON {}'.format(voltage)) 221 | 222 | sleep(wait) # give some time for device to respond 223 | 224 | def queryCurrentVON(self): 225 | """Query the voltage level when Constant Current mode starts to sink current 226 | 227 | returns the set voltage value as a floating point value 228 | """ 229 | 230 | return self.fetchGenericValue('SOURce:CURRent:VON?', channel) 231 | 232 | def setFunctionMode(self, mode, channel=None, wait=None): 233 | """Set the source function mode/input regulation mode for the channel 234 | 235 | mode - a string which names the desired function mode. valid ones: 236 | FIXed|LIST|WAVe|BATTery|OCP|OPP 237 | wait - number of seconds to wait after sending command 238 | channel - number of the channel starting at 1 239 | """ 240 | 241 | # Check mode to be valid 242 | if (mode[0:3] not in ["FIX", "WAV", "OCP", "OPP"] and 243 | mode[0:4] not in ["LIST", "BATT"]): 244 | raise ValueError('setFunctionMode(): "{}" is an unknown function mode.'.format(mode)) 245 | 246 | # If a channel number is passed in, make it the 247 | # current channel 248 | if channel is not None: 249 | self.channel = channel 250 | 251 | # If a wait time is NOT passed in, set wait to the 252 | # default time 253 | if wait is None: 254 | wait = self._wait 255 | 256 | str = 'SOURce:FUNCtion:MODE {}'.format(mode) 257 | self._instWrite(str) 258 | sleep(wait) # give some time for PS to respond 259 | 260 | def queryFunctionMode(self, channel=None, query_delay=None): 261 | """Return what the FUNCTION MODE/input regulation mode is set to 262 | 263 | channel - number of the channel starting at 1 264 | query_delay - number of seconds to wait between write and 265 | reading for read data (None uses default seconds) 266 | """ 267 | 268 | return self.fetchGenericString('SOURce:FUNCtion:MODE?', channel, query_delay) 269 | 270 | def setSenseState(self, on, channel=None, wait=None): 271 | """Enable or Disable the Sense Inputs 272 | 273 | on - set to True to Enable use of the Sense inputs or False to Disable them 274 | channel - number of the channel starting at 1 275 | wait - number of seconds to wait after sending command 276 | """ 277 | 278 | # If a channel number is passed in, make it the 279 | # current channel 280 | if channel is not None: 281 | self.channel = channel 282 | 283 | # If a wait time is NOT passed in, set wait to the 284 | # default time 285 | if wait is None: 286 | wait = self._wait 287 | 288 | str = 'SOURce:SENSE {}'.format(self._bool2onORoff(on)) 289 | 290 | self._instWrite(str) 291 | 292 | sleep(wait) # give some time for device to respond 293 | 294 | def querySenseState(self, channel=None, query_delay=None): 295 | """Return the state of the Sense Input 296 | 297 | channel - number of the channel starting at 1 298 | query_delay - number of seconds to wait between write and 299 | reading for read data (None uses default seconds) 300 | """ 301 | 302 | return self.fetchGenericBoolean('SOURce:SENSe?', channel, query_delay) 303 | 304 | if __name__ == '__main__': 305 | import argparse 306 | parser = argparse.ArgumentParser(description='Access and control a Rigol DL3000 electronic load') 307 | parser.add_argument('chan', nargs='?', type=int, help='Channel to access/control (starts at 1)', default=1) 308 | args = parser.parse_args() 309 | 310 | from time import sleep 311 | from os import environ 312 | resource = environ.get('DL3000_VISA', 'TCPIP0::172.16.2.13::INSTR') 313 | rigol = RigolDL3000(resource) 314 | rigol.open() 315 | 316 | # Reset 317 | rigol.rst(wait=1.0) 318 | rigol.cls(wait=1.0) 319 | 320 | print(rigol.idn()) 321 | 322 | ## set Remote Lock On 323 | rigol.setRemoteLock() 324 | 325 | rigol.beeperOff() 326 | 327 | if (True): 328 | print('Current function: {} & mode: {}'.format(rigol.queryFunction(),rigol.queryFunctionMode())) 329 | sleep(1.0) 330 | 331 | rigol.setFunctionMode("FIX", wait=2.0) 332 | 333 | for func in ["CURRent","RESistance","VOLTage","POWer"]: 334 | print('Set to function: {} ...'.format(func)) 335 | rigol.setFunction(func,wait=2.0) 336 | print('Current function: {} & mode: {}'.format(rigol.queryFunction(),rigol.queryFunctionMode())) 337 | 338 | for mode in ["FIXed","LIST","WAVe","BATTERY","OCP","OPP"]: 339 | print('Set to mode: {} ...'.format(mode)) 340 | rigol.setFunctionMode(mode,wait=2.0) 341 | print('Current function: {} & mode: {}'.format(rigol.queryFunction(),rigol.queryFunctionMode())) 342 | 343 | #@@@#rigol.setFunctionMode("FIX",wait=0.5) 344 | rigol.setFunction("CURR", wait=0.5) 345 | print('Current function: {} & mode: {}'.format(rigol.queryFunction(),rigol.queryFunctionMode())) 346 | sleep(1.0) 347 | 348 | print('\nCurrent Sense State: {}'.format('ON' if rigol.querySenseState() else 'OFF')) 349 | print('Enable State Inputs ...') 350 | rigol.setSenseState(True) 351 | print('Current Sense State: {}'.format('ON' if rigol.querySenseState() else 'OFF')) 352 | print('Disable State Inputs ...') 353 | rigol.setSenseState(False) 354 | print('Current Sense State: {}'.format('ON' if rigol.querySenseState() else 'OFF')) 355 | 356 | rigol.setSenseState(True) 357 | print('Current Sense State: {}'.format('ON' if rigol.querySenseState() else 'OFF')) 358 | 359 | if not rigol.isInputOn(args.chan): 360 | rigol.inputOn() 361 | 362 | print('Ch. {} Settings: {:6.4f} V {:6.4f} A'. 363 | format(args.chan, rigol.queryVoltage(), 364 | rigol.queryCurrent())) 365 | 366 | #print(rigol.idn()) 367 | print('{:6.4f} V'.format(rigol.measureVoltage())) 368 | print('{:6.4f} A'.format(rigol.measureCurrent())) 369 | 370 | rigol.setCurrent(0.2) 371 | 372 | print('{:6.4f} V'.format(rigol.measureVoltage())) 373 | print('{:6.4f} A'.format(rigol.measureCurrent())) 374 | 375 | rigol.setCurrent(0.4) 376 | 377 | print('{:6.4f} V'.format(rigol.measureVoltage())) 378 | print('{:6.4f} A'.format(rigol.measureCurrent())) 379 | 380 | ## turn off the channel 381 | rigol.inputOff() 382 | 383 | rigol.beeperOn() 384 | 385 | rigol.printAllErrors() 386 | rigol.cls() 387 | 388 | ## return to LOCAL mode 389 | rigol.setLocal() 390 | 391 | rigol.close() 392 | -------------------------------------------------------------------------------- /dcps/KeysightE364xA.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | 5 | # Copyright (c) 2021, Stephen Goadhouse 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | #----------------------------------------------------------------------------- 26 | # Control a HP/Agilent/Keysight E364xA series DC Power Supplies with PyVISA 27 | #----------------------------------------------------------------------------- 28 | 29 | # For future Python3 compatibility: 30 | from __future__ import absolute_import 31 | from __future__ import division 32 | from __future__ import print_function 33 | 34 | try: 35 | from . import SCPI 36 | except: 37 | from SCPI import SCPI 38 | 39 | import re 40 | from time import sleep 41 | import pyvisa as visa 42 | 43 | class KeysightE364xA(SCPI): 44 | """Basic class for controlling and accessing a HP/Agilent/Keysight E364xA DC Power Supply. 45 | 46 | If the VISA resource string is of the form TCPIP[n]::*::23::SOCKET, 47 | it is assumed that the power supply is being accessed using a 48 | KISS-488 Ethernet to GPIB adapter 49 | (https://www.ebay.com/itm/114514724752) that is properly 50 | configured to access the power supply at its GPIB address 51 | (default is 5). 52 | 53 | If the VISA resource string is of the form TCPIP[n]::*::1234::SOCKET, 54 | it is assumed that the power supply is being accessed using a 55 | Prologix Ethernet to GPIB adapter 56 | (http://prologix.biz/gpib-ethernet-controller.html). The 57 | Prologix has commands to set GPIB address and such. 58 | 59 | It should be possible to use this directly over GPIB or with a 60 | USB to GPIB interface by modifying the resource string but some 61 | minor code edits may be needed. For now, this code has only 62 | been tested with a KISS-488 or Prologix Ethernet to GPIB interface. 63 | 64 | """ 65 | 66 | ## Dictionary to translate SCPI commands for this device 67 | _xlateCmdTbl = { 68 | 'isOutput': 'OUTPut?', 69 | 'outputOn': 'OUTPut ON', 70 | 'outputOff': 'OUTPut OFF', 71 | 'setVoltage': 'VOLTage {}', 72 | 'setCurrent': 'CURRent {}', 73 | 'queryVoltage': 'VOLTage?', 74 | 'queryCurrent': 'CURRent?', 75 | 'measureVoltage': 'MEASure:VOLTage?', 76 | 'measureCurrent': 'MEASure:CURRent?', 77 | 'setVoltageProtection': 'VOLTage:PROTection:LEVel {}', 78 | 'queryVoltageProtection': 'VOLTage:PROTection:LEVel?', 79 | 'voltageProtectionOn': 'VOLTage:PROTection:STATe ON', 80 | 'voltageProtectionOff': 'VOLTage:PROTection:STATe OFF', 81 | } 82 | 83 | def __init__(self, resource, gaddr=5, wait=0.25, verbosity=0, query_delay=0.75, **kwargs): 84 | """Init the class with the instruments resource string 85 | 86 | resource - resource string or VISA descriptor, like TCPIP0::172.16.2.13::23::SOCKET 87 | gaddr - GPIB bus address of instrument - this is only useful if using Prologix interface 88 | wait - float that gives the default number of seconds to wait after sending each command 89 | verbosity - verbosity output - set to 0 for no debug output 90 | kwargs - other named options to pass when PyVISA open() like open_timeout=2.0 91 | """ 92 | 93 | # Set defaults 94 | self._enetgpib = False # True if an Ethernet to GPIB interface is being used 95 | self._kiss488 = False # True if the Ethernet to GPIB interface is a KISS-488 96 | self._prologix = False # True if the Ethernet to GPIB interface is a Prologix 97 | 98 | ## regexp for resource string that indicates it is being used with KISS-488 or Prologix 99 | reskiss488 = re.compile("^TCPIP[0-9]*::.*::23::SOCKET$") 100 | resprologix = re.compile("^TCPIP[0-9]*::.*::1234::SOCKET$") 101 | if (reskiss488.match(resource)): 102 | self._enetgpib = True 103 | self._kiss488 = True 104 | if (query_delay < 1.5): 105 | ## Found that for KISS-488 Interface, query_delay must be at least 1.5 106 | query_delay = 1.5 107 | elif (resprologix.match(resource)): 108 | self._enetgpib = True 109 | self._prologix = True 110 | 111 | # save some parameters in case need it 112 | self._gaddr = gaddr 113 | self._query_delay = query_delay 114 | 115 | super(KeysightE364xA, self).__init__(resource, max_chan=1, wait=wait, cmd_prefix='', 116 | verbosity = verbosity, 117 | read_termination = '\n', write_termination = '\n', 118 | timeout=2, # found that needed longer timeout 119 | query_delay=query_delay, # for open_resource() 120 | **kwargs) 121 | 122 | def open(self): 123 | """ Overloaded open() so can handle GPIB interfaces after opening the connection """ 124 | 125 | super(KeysightE364xA, self).open() 126 | 127 | if (self._kiss488): 128 | # Give the instrument time to output whatever initial output it may send 129 | sleep(1.5) 130 | 131 | ## Can clear strings instead of reading and printing them out 132 | #@@@#self._inst.clear() 133 | 134 | # Read out any strings that are sent after connecting (happens 135 | # for KISS-488 and may happen with other interfaces) 136 | try: 137 | while True: 138 | bytes = self._inst.read_raw() 139 | if (self._kiss488): 140 | # If the expected header from KISS-488, print it out, otherwise ignore. 141 | if ('KISS-488'.encode() in bytes): 142 | print(bytes.decode('utf-8').strip()) 143 | except visa.errors.VisaIOError as err: 144 | if (err.error_code != visa.constants.StatusCode.error_timeout): 145 | # Ignore timeouts here since just reading strings until they stop. 146 | # Output any other errors 147 | print("ERROR: {}, {}".format(err, type(err))) 148 | 149 | elif (self._prologix): 150 | # Configure mode, addr, auto and print out ver 151 | self._instWrite('++mode 1') # make sure in CONTROLLER mode 152 | self._instWrite('++auto 0') # will explicitly tell when to read instrument 153 | self._instWrite('++addr {}'.format(self._gaddr)) # set GPIB address 154 | self._instWrite('++eos 2') # append '\n' / LF to instrument commands 155 | self._instWrite('++eoi 1') # enable EOI assertion with commands 156 | self._instWrite('++read_tmo_ms 600') # Set the Read Timeout to 600 ms 157 | #@@@#self._instWrite('++eot_char 10') # @@@ 158 | self._instWrite('++eot_enable 0') # Do NOT append character when EOI detected 159 | 160 | # Read and print out Version string. Using write/read to 161 | # void having '++read' appended if use Query. It is not 162 | # needed for ++ commands and causes a warning if used. 163 | self._instWrite('++ver') 164 | sleep(self._query_delay) 165 | print(self._inst.read()) 166 | 167 | #@@@#self.printAllErrors() 168 | #@@@#self.cls() 169 | 170 | 171 | def _instQuery(self, queryStr): 172 | """ Overload _instQuery from SCPI.py so can append the \r if KISS-488 or add ++read if Prologix""" 173 | # Need to also strip out any leading or trailing white space from the response 174 | 175 | # KISS-488 requires queries to end in '\r' so it knows a response is expected 176 | if (self._kiss488): 177 | queryStr += '\r' 178 | elif (self._prologix): 179 | # Can use \n or 10 as terminator on reads but not faster than using eoi 180 | #queryStr += self._write_termination + '++read 10' 181 | queryStr += self._write_termination + '++read eoi' 182 | 183 | if self._verbosity >= 4: 184 | print("OUT/" + ":".join("{:02x}".format(ord(c)) for c in queryStr)) 185 | resp = super(KeysightE364xA, self)._instQuery(queryStr).strip() 186 | if self._verbosity >= 4: 187 | print("IN /" + ":".join("{:02x}".format(ord(c)) for c in resp)) 188 | print(resp) 189 | 190 | return resp 191 | 192 | def beeperOn(self): 193 | """Enable the system beeper for the instrument""" 194 | # NOTE: Unsupported command by this power supply. However, 195 | # instead of raising an exception and breaking any scripts, 196 | # simply return quietly. 197 | pass 198 | 199 | def beeperOff(self): 200 | """Disable the system beeper for the instrument""" 201 | # NOTE: Unsupported command by this power supply. However, 202 | # instead of raising an exception and breaking any scripts, 203 | # simply return quietly. 204 | pass 205 | 206 | def setLocal(self): 207 | """If KISS-488, disable the system local command for the instrument 208 | If Prologix, issue GPIB command to unlock the front panel 209 | """ 210 | 211 | if (self._kiss488): 212 | # NOTE: Unsupported command if using KISS-488 with this power 213 | # supply. However, instead of raising an exception and 214 | # breaking any scripts, simply return quietly. 215 | pass 216 | elif (self._prologix): 217 | self._instWrite('++loc') # issue GPIB command to enable front panel 218 | 219 | 220 | def setRemote(self): 221 | """If KISS-488, disable the system remote command for the instrument 222 | If Prologix, issue GPIB command to lock the front panel 223 | """ 224 | 225 | if (self._kiss488): 226 | # NOTE: Unsupported command if using KISS-488 with this power supply. However, 227 | # instead of raising an exception and breaking any scripts, 228 | # simply return quietly. 229 | pass 230 | elif (self._prologix): 231 | self._instWrite('++llo') # issue GPIB command to disable front panel 232 | 233 | def setRemoteLock(self): 234 | """Disable the system remote lock command for the instrument""" 235 | # NOTE: Unsupported command by this power supply. However, 236 | # instead of raising an exception and breaking any scripts, 237 | # simply return quietly. 238 | pass 239 | 240 | 241 | 242 | ################################################################### 243 | # Commands Specific to E364x 244 | ################################################################### 245 | 246 | def displayMessageOn(self, top=True): 247 | """Enable Display Message - gets enabled with setDisplayMessage() so ignore this 248 | 249 | top - Only a single display, so ignore this parameter 250 | """ 251 | pass 252 | 253 | def displayMessageOff(self, top=True): 254 | """Disable Display Message 255 | 256 | top - Only a single display, so ignore this parameter 257 | """ 258 | 259 | self._instWrite('DISP:WIND1:TEXT:CLE') 260 | 261 | 262 | def setDisplayMessage(self, message, top=True): 263 | """Set and display the message for Display. Use displayMessageOff() to 264 | enable or disable message, respectively. 265 | 266 | message - message to set 267 | top - Only a single display, so ignore this parameter 268 | 269 | """ 270 | 271 | # Maximum of 11 characters for top message 272 | if (len(message) > 11): 273 | message = message[:11] 274 | self._instWrite('DISP:WIND1:TEXT "{}"'.format(message)) 275 | 276 | 277 | if __name__ == '__main__': 278 | import argparse 279 | parser = argparse.ArgumentParser(description='Access and control a HP/Agilent/Keysight E364xA DC Power Supply') 280 | parser.add_argument('chan', nargs='?', type=int, help='Channel to access/control (max channel: 1)', default=1) 281 | args = parser.parse_args() 282 | 283 | from time import sleep 284 | from os import environ 285 | import sys 286 | 287 | resource = environ.get('E364XA_VISA', 'TCPIP0::192.168.1.20::23::SOCKET') 288 | dcpwr = KeysightE364xA(resource, gaddr=5, verbosity=1, query_delay=0.75) 289 | dcpwr.open() 290 | 291 | # Reset to power on default - need to wait a little longer before proceeding 292 | dcpwr.rst(wait=1.0) 293 | 294 | print(dcpwr.idn()) 295 | print() 296 | 297 | dcpwr.beeperOff() 298 | 299 | if 0: 300 | # For test and debug 301 | dcpwr._instWrite('VOLTage?\n++read eoi') 302 | sleep(0.25) 303 | print('VOLTage? response:', dcpwr._inst.read_raw()) 304 | sleep(1.0) 305 | dcpwr._instWrite('VOLTage?\n++read 10') 306 | sleep(0.25) 307 | print('VOLTage? response:', dcpwr._inst.read_raw()) 308 | sys.exit() 309 | 310 | # Set display messages - only 'top' message should work 311 | dcpwr.setDisplayMessage('Bottom Message', top=False) 312 | dcpwr.setDisplayMessage('All ur base ...', top=True) 313 | 314 | # Enable top one first 315 | dcpwr.displayMessageOn() 316 | sleep(1.0) 317 | dcpwr.displayMessageOn(top=False) 318 | sleep(2.0) 319 | 320 | # Disable bottom one first 321 | dcpwr.displayMessageOff(top=False) 322 | sleep(1.0) 323 | dcpwr.displayMessageOff(top=True) 324 | 325 | if not dcpwr.isOutputOn(args.chan): 326 | dcpwr.outputOn() 327 | 328 | print('Ch. {} Settings: {:6.4f} V {:6.4f} A'. 329 | format(args.chan, dcpwr.queryVoltage(), 330 | dcpwr.queryCurrent())) 331 | 332 | voltageSave = dcpwr.queryVoltage() 333 | 334 | print('{:6.4f}V / {:6.4f}A (limit: {:6.4f}A)\n'.format(dcpwr.measureVoltage(), dcpwr.measureCurrent(), dcpwr.queryCurrent())) 335 | 336 | print("Changing Output Voltage to 2.7V") 337 | dcpwr.setVoltage(2.7) 338 | print('{:6.4f}V / {:6.4f}A (limit: {:6.4f}A)\n'.format(dcpwr.measureVoltage(), dcpwr.measureCurrent(), dcpwr.queryCurrent())) 339 | 340 | print("Changing Output Voltage to 2.3V and current to 1.3A") 341 | dcpwr.setVoltage(2.3) 342 | dcpwr.setCurrent(1.3) 343 | print('{:6.4f}V / {:6.4f}A (limit: {:6.4f}A)\n'.format(dcpwr.measureVoltage(), dcpwr.measureCurrent(), dcpwr.queryCurrent())) 344 | 345 | print("Set Over-Voltage Protection to 3.6V") 346 | dcpwr.setVoltageProtection(3.6) 347 | print('OVP: {:6.4f}V\n'.format(dcpwr.queryVoltageProtection())) 348 | 349 | dcpwr.voltageProtectionOff() 350 | 351 | print("Changing Output Voltage to 3.7V with OVP off") 352 | dcpwr.setVoltage(3.7) 353 | print('{:6.4f}V / {:6.4f}A (limit: {:6.4f}A)\n'.format(dcpwr.measureVoltage(), dcpwr.measureCurrent(), dcpwr.queryCurrent())) 354 | 355 | if (dcpwr.isVoltageProtectionTripped()): 356 | print("OVP is TRIPPED but should NOT be - FAILURE\n") 357 | else: 358 | print("OVP is not TRIPPED as expected\n") 359 | 360 | print("Enable OVP") 361 | dcpwr.voltageProtectionOn() 362 | 363 | if (dcpwr.isVoltageProtectionTripped()): 364 | print("OVP is TRIPPED as expected.\n") 365 | else: 366 | print("OVP is not TRIPPED but is SHOULD be - FAILURE\n") 367 | 368 | print("Changing Output Voltage to 3.55V and clearing OVP Trip") 369 | dcpwr.setVoltage(3.55) 370 | dcpwr.voltageProtectionClear() 371 | print('{:6.4f}V / {:6.4f}A (limit: {:6.4f}A)\n'.format(dcpwr.measureVoltage(), dcpwr.measureCurrent(), dcpwr.queryCurrent())) 372 | 373 | if (dcpwr.isVoltageProtectionTripped()): 374 | print("OVP is still TRIPPED - FAILURE\n") 375 | else: 376 | print("OVP is not TRIPPED as is expected\n") 377 | 378 | 379 | print("Restoring original Output Voltage setting") 380 | dcpwr.setVoltage(voltageSave) 381 | print('{:6.4f}V / {:6.4f}A (limit: {:6.4f}A)\n'.format(dcpwr.measureVoltage(), dcpwr.measureCurrent(), dcpwr.queryCurrent())) 382 | 383 | ## turn off the channel 384 | dcpwr.outputOff() 385 | 386 | dcpwr.beeperOn() 387 | 388 | ## return to LOCAL mode 389 | dcpwr.setLocal() 390 | 391 | dcpwr.close() 392 | -------------------------------------------------------------------------------- /dcps/AimTTiPLP.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | 5 | # Copyright (c) 2018, Stephen Goadhouse 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | #------------------------------------------------------------------------------- 26 | # Control a Aim TTi PL-P Series DC Power Supplies with PyVISA 27 | #------------------------------------------------------------------------------- 28 | 29 | # For future Python3 compatibility: 30 | from __future__ import absolute_import 31 | from __future__ import division 32 | from __future__ import print_function 33 | 34 | try: 35 | from . import SCPI 36 | except ValueError: 37 | from SCPI import SCPI 38 | 39 | import warnings 40 | from time import sleep 41 | import pyvisa as visa 42 | import re 43 | 44 | class AimTTiPLP(SCPI): 45 | """Basic class for controlling and accessing an Aim TTi PL-P Series 46 | Power Supply. This series of power supplies only minimally adheres 47 | to any LXI specifications and so it uses its own commands although 48 | it adheres to the basic syntax of SCPI. The underlying accessor 49 | functions of SCPI.py are used but the top level are all re-written 50 | below to handle the very different command syntax. This shows how 51 | one might add packages to support other such power supplies that 52 | only minimally adhere to the command standards. 53 | """ 54 | 55 | def __init__(self, resource, wait=1.0, verbosity=0, warn=True, rewrite=True, **kwargs): 56 | """Init the class with the instruments resource string 57 | 58 | resource - Resource string or VISA descriptor, like TCPIP0::192.168.1.100::9221::SOCKET 59 | wait - float that gives the default number of seconds to wait after sending each command 60 | verbosity - verbosity output - set to 0 for no debug output 61 | warn - Warn about resource string using VXI-11 and/or automatic rewrites. Default True 62 | Will throw a UserWarning when detecting VXI-11 resource string or auto-rewriting the resource string 63 | rewrite - Automatically rewrite the VXI-11 resource string to a raw socket. Default True 64 | kwargs - other named options to pass when PyVISA open() like open_timeout=2.0 65 | 66 | NOTE: This instrument only implements enough VXI-11 to support the discovery protocol 67 | It ignores any writes to it, and returns an *IDN? style response to any read from the device. 68 | All communication with this device has to be done through a raw socket. 69 | This can be acomplished by changing the resource string 70 | from TCPIP::::inst0::INSTR 71 | to TCPIP::::9221::SOCKET 72 | As auto discovery will give an incompatible resource string, with unclear failure modes, this automatically does this 73 | replacement if the resource string starts with TCPIP and doesn't end with SOCKET 74 | https://web.archive.org/web/20240527022453/https://resources.aimtti.com/manuals/CPX400DP_Instruction_Manual-Iss1.pdf 75 | """ 76 | 77 | if resource.startswith('TCPIP') and not resource.endswith('SOCKET'): 78 | if rewrite: 79 | ip = resource.split('::')[1] 80 | old_resource = resource 81 | resource = f'TCPIP::{ip}::9221::SOCKET' 82 | warn_msg = f'Auto re-wrote resource string from {old_resource} to {resource}. See manual on VXI-11 implementation' 83 | else: 84 | warn_msg = 'These Aim TTI PSUs only implement auto-discovery in VXI-11. Please refer to the manual of the PSU' 85 | 86 | if warn: 87 | warnings.warn(warn_msg, stacklevel=2) 88 | 89 | 90 | super(AimTTiPLP, self).__init__(resource, max_chan=3, wait=wait, 91 | cmd_prefix='', 92 | verbosity=verbosity, 93 | read_termination='\n', 94 | write_termination='\r\n', 95 | **kwargs) 96 | 97 | def setLocal(self): 98 | """Set the power supply to LOCAL mode where front panel keys work again 99 | """ 100 | self._instWrite('LOCAL') 101 | 102 | def setRemote(self): 103 | """Set the power supply to REMOTE mode where it is controlled via VISA 104 | """ 105 | # Not supported explicitly by this power supply but the power 106 | # supply does switch to REMOTE automatically. So send any 107 | # command to do it. 108 | self._instWrite('*WAI') 109 | 110 | def setRemoteLock(self): 111 | """Set the power supply to REMOTE Lock mode where it is 112 | controlled via VISA & front panel is locked out 113 | """ 114 | self._instWrite('IFLOCK') 115 | 116 | def beeperOn(self): 117 | """Enable the system beeper for the instrument""" 118 | # NOTE: Unsupported command by this power supply. However, 119 | # instead of raising an exception and breaking any scripts, 120 | # simply return quietly. 121 | pass 122 | 123 | def beeperOff(self): 124 | """Disable the system beeper for the instrument""" 125 | # NOTE: Unsupported command by this power supply. However, 126 | # instead of raising an exception and breaking any scripts, 127 | # simply return quietly. 128 | pass 129 | 130 | def isOutputOn(self, channel=None): 131 | """Return true if the output of channel is ON, else false 132 | 133 | channel - number of the channel starting at 1 134 | """ 135 | 136 | # If a channel number is passed in, make it the 137 | # current channel 138 | if channel is not None: 139 | self.channel = channel 140 | 141 | str = 'OP{}?'.format(self.channel) 142 | ret = self._instQuery(str) 143 | 144 | # Only check first character so that there can be training whitespace that gets ignored 145 | if (ret[0] == '1'): 146 | return True 147 | else: 148 | return False 149 | 150 | def outputOn(self, channel=None, wait=None): 151 | """Turn on the output for channel 152 | 153 | wait - number of seconds to wait after sending command 154 | channel - number of the channel starting at 1 155 | """ 156 | 157 | # If a channel number is passed in, make it the 158 | # current channel 159 | if channel is not None: 160 | self.channel = channel 161 | 162 | # If a wait time is NOT passed in, set wait to the 163 | # default time 164 | if wait is None: 165 | wait = self._wait 166 | 167 | str = 'OP{} 1'.format(self.channel) 168 | self._instWrite(str) 169 | sleep(wait) # give some time for PS to respond 170 | 171 | def outputOff(self, channel=None, wait=None): 172 | """Turn off the output for channel 173 | 174 | channel - number of the channel starting at 1 175 | """ 176 | 177 | # If a channel number is passed in, make it the 178 | # current channel 179 | if channel is not None: 180 | self.channel = channel 181 | 182 | # If a wait time is NOT passed in, set wait to the 183 | # default time 184 | if wait is None: 185 | wait = self._wait 186 | 187 | str = 'OP{} 0'.format(self.channel) 188 | self._instWrite(str) 189 | sleep(wait) # give some time for PS to respond 190 | 191 | def outputOnAll(self, wait=None): 192 | """Turn on the output for ALL channels 193 | 194 | """ 195 | 196 | # If a wait time is NOT passed in, set wait to the 197 | # default time 198 | if wait is None: 199 | wait = self._wait 200 | 201 | str = 'OPALL 1'.format(self.channel) 202 | self._instWrite(str) 203 | sleep(wait) # give some time for PS to respond 204 | 205 | def outputOffAll(self, wait=None): 206 | """Turn off the output for ALL channels 207 | 208 | """ 209 | 210 | # If a wait time is NOT passed in, set wait to the 211 | # default time 212 | if wait is None: 213 | wait = self._wait 214 | 215 | str = 'OPALL 0'.format(self.channel) 216 | self._instWrite(str) 217 | sleep(wait) # give some time for PS to respond 218 | 219 | def setVoltage(self, voltage, channel=None, wait=None): 220 | """Set the voltage value for the channel 221 | 222 | voltage - desired voltage value as a floating point number 223 | wait - number of seconds to wait after sending command 224 | channel - number of the channel starting at 1 225 | """ 226 | 227 | # If a channel number is passed in, make it the 228 | # current channel 229 | if channel is not None: 230 | self.channel = channel 231 | 232 | # If a wait time is NOT passed in, set wait to the 233 | # default time 234 | if wait is None: 235 | wait = self._wait 236 | 237 | str = 'V{} {}'.format(self.channel, voltage) 238 | self._instWrite(str) 239 | sleep(wait) # give some time for PS to respond 240 | 241 | def setCurrent(self, current, channel=None, wait=None): 242 | """Set the current value for the channel 243 | 244 | current - desired current value as a floating point number 245 | channel - number of the channel starting at 1 246 | wait - number of seconds to wait after sending command 247 | """ 248 | 249 | # If a channel number is passed in, make it the 250 | # current channel 251 | if channel is not None: 252 | self.channel = channel 253 | 254 | # If a wait time is NOT passed in, set wait to the 255 | # default time 256 | if wait is None: 257 | wait = self._wait 258 | 259 | str = 'I{} {}'.format(self.channel, current) 260 | self._instWrite(str) 261 | sleep(wait) # give some time for PS to respond 262 | 263 | 264 | def queryVoltage(self, channel=None): 265 | """Return what voltage set value is (not the measured voltage, 266 | but the set voltage) 267 | 268 | channel - number of the channel starting at 1 269 | """ 270 | 271 | # If a channel number is passed in, make it the 272 | # current channel 273 | if channel is not None: 274 | self.channel = channel 275 | 276 | str = 'V{}?'.format(self.channel) 277 | ret = self._instQuery(str) 278 | 279 | # Pull out words from response 280 | match = re.match(r'^([^\s0-9]+)([0-9]+)\s+([0-9.+-]+)',ret) 281 | if (match == None): 282 | raise RuntimeError('Unexpected response: "{}"'.format(ret)) 283 | else: 284 | # break out the words from the response 285 | words = match.groups() 286 | if (len(words) != 3): 287 | raise RuntimeError('Unexpected number of words in response: "{}"'.format(ret)) 288 | elif(words[0] != 'V' or int(words[1]) != self.channel): 289 | raise ValueError('Unexpected response format: "{}"'.format(ret)) 290 | else: 291 | # response checks out so return the fixed point response as a float() 292 | return float(words[2]) 293 | 294 | def queryCurrent(self, channel=None): 295 | """Return what current set value is (not the measured current, 296 | but the set current) 297 | 298 | channel - number of the channel starting at 1 299 | """ 300 | 301 | # If a channel number is passed in, make it the 302 | # current channel 303 | if channel is not None: 304 | self.channel = channel 305 | 306 | str = 'I{}?'.format(self.channel) 307 | ret = self._instQuery(str) 308 | 309 | # Pull out words from response 310 | match = re.match(r'^([^\s0-9]+)([0-9]+)\s+([0-9.+-]+)',ret) 311 | if (match == None): 312 | raise RuntimeError('Unexpected response: "{}"'.format(ret)) 313 | else: 314 | # break out the words from the response 315 | words = match.groups() 316 | if (len(words) != 3): 317 | raise RuntimeError('Unexpected number of words in response: "{}"'.format(ret)) 318 | elif(words[0] != 'I' or int(words[1]) != self.channel): 319 | raise ValueError('Unexpected response format: "{}"'.format(ret)) 320 | else: 321 | # response checks out so return the fixed point response as a float() 322 | return float(words[2]) 323 | 324 | def measureVoltage(self, channel=None): 325 | """Read and return a voltage measurement from channel 326 | 327 | channel - number of the channel starting at 1 328 | """ 329 | 330 | # If a channel number is passed in, make it the 331 | # current channel 332 | if channel is not None: 333 | self.channel = channel 334 | 335 | str = 'V{}O?'.format(self.channel) 336 | ret = self._instQuery(str) 337 | 338 | # Pull out words from response 339 | match = re.match(r'^([0-9.+-]+)([^\s]+)',ret) 340 | if (match == None): 341 | raise RuntimeError('Unexpected response: "{}"'.format(ret)) 342 | else: 343 | # break out the words from the response 344 | words = match.groups() 345 | if (len(words) != 2): 346 | raise RuntimeError('Unexpected number of words in response: "{}"'.format(ret)) 347 | elif(words[1] != 'V'): 348 | raise ValueError('Unexpected response format: "{}"'.format(ret)) 349 | else: 350 | # response checks out so return the fixed point response as a float() 351 | return float(words[0]) 352 | 353 | def measureCurrent(self, channel=None): 354 | """Read and return a current measurement from channel 355 | 356 | channel - number of the channel starting at 1 357 | """ 358 | 359 | # If a channel number is passed in, make it the 360 | # current channel 361 | if channel is not None: 362 | self.channel = channel 363 | 364 | str = 'I{}O?'.format(self.channel) 365 | ret = self._instQuery(str) 366 | 367 | # Pull out words from response 368 | match = re.match(r'^([0-9.+-]+)([^\s]+)',ret) 369 | if (match == None): 370 | raise RuntimeError('Unexpected response: "{}"'.format(ret)) 371 | else: 372 | # break out the words from the response 373 | words = match.groups() 374 | if (len(words) != 2): 375 | raise RuntimeError('Unexpected number of words in response: "{}"'.format(ret)) 376 | elif(words[1] != 'A'): 377 | raise ValueError('Unexpected response format: "{}"'.format(ret)) 378 | else: 379 | # response checks out so return the fixed point response as a float() 380 | return float(words[0]) 381 | 382 | 383 | if __name__ == '__main__': 384 | import argparse 385 | parser = argparse.ArgumentParser(description='Access and control a Aim TTi PL-P Series power supply') 386 | parser.add_argument('chan', nargs='?', type=int, help='Channel to access/control (starts at 1)', default=1) 387 | args = parser.parse_args() 388 | 389 | from time import sleep 390 | from os import environ 391 | resource = environ.get('TTIPLP_IP', 'TCPIP0::192.168.1.100::9221::SOCKET') 392 | ttiplp = AimTTiPLP(resource) 393 | ttiplp.open() 394 | 395 | ## set Remote Lock On 396 | #ttiplp.setRemoteLock() 397 | 398 | ttiplp.beeperOff() 399 | 400 | if not ttiplp.isOutputOn(args.chan): 401 | ttiplp.outputOn() 402 | 403 | print('Ch. {} Settings: {:6.4f} V {:6.4f} A'. 404 | format(args.chan, ttiplp.queryVoltage(), 405 | ttiplp.queryCurrent())) 406 | 407 | voltageSave = ttiplp.queryVoltage() 408 | 409 | #print(ttiplp.idn()) 410 | print('{:6.4f} V'.format(ttiplp.measureVoltage())) 411 | print('{:6.4f} A'.format(ttiplp.measureCurrent())) 412 | 413 | ttiplp.setVoltage(2.7) 414 | 415 | print('{:6.4f} V'.format(ttiplp.measureVoltage())) 416 | print('{:6.4f} A'.format(ttiplp.measureCurrent())) 417 | 418 | ttiplp.setVoltage(2.3) 419 | 420 | print('{:6.4f} V'.format(ttiplp.measureVoltage())) 421 | print('{:6.4f} A'.format(ttiplp.measureCurrent())) 422 | 423 | ttiplp.setVoltage(voltageSave) 424 | 425 | print('{:6.4f} V'.format(ttiplp.measureVoltage())) 426 | print('{:6.4f} A'.format(ttiplp.measureCurrent())) 427 | 428 | ## turn off the channel 429 | ttiplp.outputOff() 430 | 431 | ttiplp.beeperOn() 432 | 433 | ## return to LOCAL mode 434 | ttiplp.setLocal() 435 | 436 | ttiplp.close() 437 | -------------------------------------------------------------------------------- /dcps/Keithley622x.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | 5 | # Copyright (c) 2021, Stephen Goadhouse 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | #----------------------------------------------------------------------------- 26 | # Control a Keithley/Tektronix 622x series Precision Current Source 27 | #----------------------------------------------------------------------------- 28 | 29 | # For future Python3 compatibility: 30 | from __future__ import absolute_import 31 | from __future__ import division 32 | from __future__ import print_function 33 | 34 | try: 35 | from . import SCPI 36 | except: 37 | from SCPI import SCPI 38 | 39 | import re 40 | from time import sleep 41 | import pyvisa as visa 42 | 43 | class Keithley622x(SCPI): 44 | """Basic class for controlling and accessing a Keithley/Tektronix 622x 45 | series Precision Current Source. Although this is not a 46 | traditional DC power supply, it uses many of the same interface 47 | commands so including here seems logical. 48 | 49 | If the VISA resource string is of the form TCPIP[n]::*::23::SOCKET, 50 | it is assumed that the power supply is being accessed using a 51 | KISS-488 Ethernet to GPIB adapter 52 | (https://www.ebay.com/itm/114514724752) that is properly 53 | configured to access the power supply at its GPIB address 54 | (default is 12). 55 | 56 | If the VISA resource string is of the form TCPIP[n]::*::1234::SOCKET, 57 | it is assumed that the power supply is being accessed using a 58 | Prologix Ethernet to GPIB adapter 59 | (http://prologix.biz/gpib-ethernet-controller.html). The 60 | Prologix has commands to set GPIB address and such. 61 | 62 | It should be possible to use this directly over GPIB or with a 63 | USB to GPIB interface by modifying the resource string but some 64 | minor code edits may be needed. For now, this code has only 65 | been tested with a KISS-488 or Prologix Ethernet to GPIB interface. 66 | 67 | Currently, could not get the KISS-488 interface to fully 68 | support the Keithley 622x although it works with other 69 | devices. So recommend to only attempt to use the Prologix with 70 | the 622x. 71 | 72 | """ 73 | 74 | ## Dictionary to translate SCPI commands for this device 75 | _xlateCmdTbl = { 76 | # 'isOutput': 'OUTP?', 77 | # 'outputOn': 'OUTPut ON', 78 | # 'outputOff': 'OUTPut OFF', 79 | 'setCurrent': 'SOURce:CURRent:RANGe {0:.2e}\nSOURce:CURRent {0:.2e}', 80 | 'setVoltageProtection': 'SOURce:CURRent:COMPliance {}', # not exactly the same but analogous 81 | } 82 | 83 | def __init__(self, resource, gaddr=12, wait=0.25, verbosity=0, query_delay=0.75, **kwargs): 84 | """Init the class with the instruments resource string 85 | 86 | resource - resource string or VISA descriptor, like TCPIP0::172.16.2.13::23::SOCKET 87 | gaddr - GPIB bus address of instrument - this is only useful if using Prologix interface 88 | wait - float that gives the default number of seconds to wait after sending each command 89 | verbosity - verbosity output - set to 0 for no debug output 90 | kwargs - other named options to pass when PyVISA open() like open_timeout=2.0 91 | """ 92 | 93 | # Set defaults 94 | self._enetgpib = False # True if an Ethernet to GPIB interface is being used 95 | self._kiss488 = False # True if the Ethernet to GPIB interface is a KISS-488 96 | self._prologix = False # True if the Ethernet to GPIB interface is a Prologix 97 | 98 | ## regexp for resource string that indicates it is being used with KISS-488 or Prologix 99 | reskiss488 = re.compile("^TCPIP[0-9]*::.*::23::SOCKET$") 100 | resprologix = re.compile("^TCPIP[0-9]*::.*::1234::SOCKET$") 101 | if (reskiss488.match(resource)): 102 | self._enetgpib = True 103 | self._kiss488 = True 104 | elif (resprologix.match(resource)): 105 | self._enetgpib = True 106 | self._prologix = True 107 | 108 | # save some parameters in case need it 109 | self._gaddr = gaddr 110 | self._query_delay = query_delay 111 | 112 | super(Keithley622x, self).__init__(resource, max_chan=1, wait=wait, cmd_prefix='', 113 | verbosity = verbosity, 114 | read_termination = '\n', write_termination = '\n', 115 | timeout=2, # found that needed longer timeout 116 | query_delay=query_delay, # for open_resource() 117 | **kwargs) 118 | 119 | def open(self): 120 | """ Overloaded open() so can handle GPIB interfaces after opening the connection """ 121 | 122 | super(Keithley622x, self).open() 123 | 124 | if (self._kiss488): 125 | # Give the instrument time to output whatever initial output it may send 126 | sleep(1.5) 127 | 128 | ## Can clear strings instead of reading and printing them out 129 | #@@@#self._inst.clear() 130 | 131 | # Read out any strings that are sent after connecting (happens 132 | # for KISS-488 and may happen with other interfaces) 133 | try: 134 | while True: 135 | bytes = self._inst.read_raw() 136 | if (self._kiss488): 137 | # If the expected header from KISS-488, print it out, otherwise ignore. 138 | if ('KISS-488'.encode() in bytes): 139 | print(bytes.decode('utf-8').strip()) 140 | except visa.errors.VisaIOError as err: 141 | if (err.error_code != visa.constants.StatusCode.error_timeout): 142 | # Ignore timeouts here since just reading strings until they stop. 143 | # Output any other errors 144 | print("ERROR: {}, {}".format(err, type(err))) 145 | 146 | elif (self._prologix): 147 | # Configure mode, addr, auto and print out ver 148 | self._instWrite('++mode 1') # make sure in CONTROLLER mode 149 | self._instWrite('++auto 0') # will explicitly tell when to read instrument 150 | self._instWrite('++addr {}'.format(self._gaddr)) # set GPIB address 151 | self._instWrite('++eos 2') # append '\n' / LF to instrument commands 152 | self._instWrite('++eoi 1') # enable EOI assertion with commands 153 | self._instWrite('++read_tmo_ms 600') # Set the Read Timeout to 600 ms 154 | #@@@#self._instWrite('++eot_char 10') # @@@ 155 | self._instWrite('++eot_enable 0') # Do NOT append character when EOI detected 156 | 157 | # Read and print out Version string. Using write/read to 158 | # void having '++read' appended if use Query. It is not 159 | # needed for ++ commands and causes a warning if used. 160 | self._instWrite('++ver') 161 | sleep(self._query_delay) 162 | print(self._inst.read()) 163 | 164 | #@@@#self.printAllErrors() 165 | #@@@#self.cls() 166 | 167 | 168 | def _instQuery(self, queryStr): 169 | """ Overload _instQuery from SCPI.py so can append the \r if KISS-488 or add ++read if Prologix""" 170 | # Need to also strip out any leading or trailing white space from the response 171 | 172 | # KISS-488 requires queries to end in '\r' so it knows a response is expected 173 | if (self._kiss488): 174 | queryStr += '\r' 175 | elif (self._prologix): 176 | queryStr += self._write_termination + '++read eoi' 177 | 178 | if self._verbosity >= 4: 179 | print("OUT/" + ":".join("{:02x}".format(ord(c)) for c in queryStr)) 180 | resp = super(Keithley622x, self)._instQuery(queryStr).strip() 181 | if self._verbosity >= 4: 182 | print("IN /" + ":".join("{:02x}".format(ord(c)) for c in resp)) 183 | print(resp) 184 | 185 | return resp 186 | 187 | def setLocal(self): 188 | """If KISS-488, disable the system local command for the instrument 189 | If Prologix, issue GPIB command to unlock the front panel 190 | """ 191 | 192 | if (self._kiss488): 193 | # NOTE: Unsupported command if using KISS-488 with this power 194 | # supply. However, instead of raising an exception and 195 | # breaking any scripts, simply return quietly. 196 | pass 197 | elif (self._prologix): 198 | self._instWrite('++loc') # issue GPIB command to enable front panel 199 | 200 | 201 | def setRemote(self): 202 | """If KISS-488, disable the system remote command for the instrument 203 | If Prologix, issue GPIB command to lock the front panel 204 | """ 205 | 206 | if (self._kiss488): 207 | # NOTE: Unsupported command if using KISS-488 with this power supply. However, 208 | # instead of raising an exception and breaking any scripts, 209 | # simply return quietly. 210 | pass 211 | elif (self._prologix): 212 | self._instWrite('++llo') # issue GPIB command to disable front panel 213 | 214 | def setRemoteLock(self): 215 | """Disable the system remote lock command for the instrument""" 216 | # NOTE: Unsupported command by this power supply. However, 217 | # instead of raising an exception and breaking any scripts, 218 | # simply return quietly. 219 | pass 220 | 221 | def setVoltage(self, voltage, channel=None, wait=None): 222 | """The 622x has no way to set the output voltage. Ignoring command. 223 | 224 | voltage - desired voltage value as a floating point number 225 | wait - number of seconds to wait after sending command 226 | channel - number of the channel starting at 1 227 | """ 228 | print('NOTE: The Keithley 622x cannot set a voltage. Perhaps you meant setVoltageProtection()?.\nIgnoring this command') 229 | 230 | def queryVoltage(self, channel=None): 231 | """The 622x has no way to query the output voltage setting. Return invalid value. 232 | 233 | channel - number of the channel starting at 1 234 | """ 235 | return (SCPI.NaN) 236 | 237 | def queryCurrent(self, channel=None): 238 | """The 622x has no way to query the output current setting. Return invalid value. 239 | 240 | channel - number of the channel starting at 1 241 | """ 242 | return (SCPI.NaN) 243 | 244 | def measureVoltage(self, channel=None): 245 | """The 622x performs no measurements so override this command 246 | 247 | channel - number of the channel starting at 1 248 | """ 249 | print('NOTE: The Keithley 622x performs no measurements of its own. Ignoring this command') 250 | return (SCPI.NaN) 251 | 252 | def measureCurrent(self, channel=None): 253 | """The 622x performs no measurements so override this command 254 | 255 | channel - number of the channel starting at 1 256 | """ 257 | print('NOTE: The Keithley 622x performs no measurements of its own. Ignoring this command') 258 | return (SCPI.NaN) 259 | 260 | def queryVoltageProtection(self, channel=None): 261 | """The 622x has no way to query the output voltage protection/compliance setting. Return invalid value. 262 | 263 | channel - number of the channel starting at 1 264 | """ 265 | return (SCPI.NaN) 266 | 267 | def voltageProtectionOn(self, channel=None, wait=None): 268 | """The 622x always has voltage protection/compliance. Ignore command. 269 | 270 | wait - number of seconds to wait after sending command 271 | channel - number of the channel starting at 1 272 | """ 273 | pass 274 | 275 | def voltageProtectionOff(self, channel=None, wait=None): 276 | """The 622x always has voltage protection/compliance. Ignore command. 277 | 278 | channel - number of the channel starting at 1 279 | """ 280 | pass 281 | 282 | def voltageProtectionClear(self, channel=None, wait=None): 283 | """The 622x always has voltage protection/compliance. Ignore command. 284 | 285 | channel - number of the channel starting at 1 286 | """ 287 | pass 288 | 289 | def isVoltageProtectionTripped(self, channel=None): 290 | """The 622x cannot tell if the compliance limit has been reached or 291 | not. So always return True so if someone uses this, 292 | hopefully the True will force them to figure out what is 293 | going on. 294 | 295 | channel - number of the channel starting at 1 296 | 297 | """ 298 | return True 299 | 300 | ################################################################### 301 | # Commands Specific to 622x 302 | ################################################################### 303 | 304 | def displayMessageOn(self, top=True): 305 | """Enable Display Message 306 | 307 | top - True if enabling the Top message, else enable Bottom message 308 | """ 309 | 310 | if (top): 311 | self._instWrite('DISP:TEXT:STAT ON') 312 | else: 313 | self._instWrite('DISP:WIND2:TEXT:STAT ON') 314 | 315 | def displayMessageOff(self, top=True): 316 | """Disable Display Message 317 | 318 | top - True if disabling the Top message, else disable Bottom message 319 | """ 320 | 321 | if (top): 322 | self._instWrite('DISP:TEXT:STAT OFF') 323 | else: 324 | self._instWrite('DISP:WIND2:TEXT:STAT OFF') 325 | 326 | 327 | def setDisplayMessage(self, message, top=True): 328 | """Set the Message for Display. Use displayMessageOn() or 329 | displayMessageOff() to enable or disable message, respectively. 330 | 331 | message - message to set 332 | top - True if setting the Top message, else set Bottom message 333 | 334 | """ 335 | 336 | if (top): 337 | # Maximum of 20 characters for top message 338 | if (len(message) > 20): 339 | message = message[:20] 340 | self._instWrite('DISP:TEXT "{}"'.format(message)) 341 | else: 342 | # Maximum of 32 characters for bottom message 343 | if (len(message) > 32): 344 | message = message[:32] 345 | self._instWrite('DISP:WIND2:TEXT "{}"'.format(message)) 346 | 347 | def isInterlockTripped(self): 348 | """Return true if the Interlock is Tripped, else false 349 | """ 350 | 351 | ret = self._instQuery('OUTP:INT:TRIP?') 352 | # For whatever reason, command returns '0' if interlock is 353 | # tripped, so logical invert it 354 | return (not self._onORoff_1OR0_yesORno(ret)) 355 | 356 | 357 | if __name__ == '__main__': 358 | import argparse 359 | parser = argparse.ArgumentParser(description='Access and control a Keithley/Tektronix 622x series Precision Current Source') 360 | parser.add_argument('chan', nargs='?', type=int, help='Channel to access/control (max channel: 1)', default=1) 361 | args = parser.parse_args() 362 | 363 | from time import sleep 364 | from os import environ 365 | import sys 366 | 367 | resource = environ.get('K622X_VISA', 'TCPIP0::192.168.1.20::23::SOCKET') 368 | currsrc = Keithley622x(resource, gaddr=12, verbosity=1, query_delay=0.75) 369 | currsrc.open() 370 | 371 | # Reset to power on default 372 | currsrc.rst() 373 | 374 | print(currsrc.idn()) 375 | print() 376 | 377 | currsrc.beeperOff() 378 | 379 | if 0: 380 | # For test and debug 381 | currsrc._instWrite('OUTPut?\n++read eoi') 382 | sleep(2.0) 383 | print('OUTP? response:', currsrc._inst.read_raw()) 384 | print('OUTP? response:', currsrc._inst.read_raw()) 385 | print('OUTP? response:', currsrc._inst.read_raw()) 386 | print('OUTP? response:', currsrc._inst.read_raw()) 387 | 388 | # Set display messages 389 | currsrc.setDisplayMessage('Bottom Message', top=False) 390 | currsrc.setDisplayMessage('Top Message', top=True) 391 | 392 | # Enable top one first 393 | currsrc.displayMessageOn() 394 | sleep(1.0) 395 | currsrc.displayMessageOn(top=False) 396 | sleep(2.0) 397 | 398 | # Disable bottom one first 399 | currsrc.displayMessageOff(top=False) 400 | sleep(1.0) 401 | currsrc.displayMessageOff(top=True) 402 | 403 | if currsrc.isInterlockTripped(): 404 | print('Interlock is tripped. Stopping') 405 | sys.exit() 406 | else: 407 | print('Interlock is NOT tripped. Continuing') 408 | 409 | 410 | ## NOTE: Most of the following functions are attempting to treat 411 | ## the 622x like a power supply. The 622x will either ignore most 412 | ## of these or return SCPI.NaN. These functions are here mainly to 413 | ## make sure that these unused functions are handled cleanly. 414 | 415 | print('Ch. {} Settings: {:6.4f} V {:6.4f} A'. 416 | format(args.chan, currsrc.queryVoltage(), 417 | currsrc.queryCurrent())) 418 | 419 | voltageSave = currsrc.queryVoltage() 420 | 421 | print('{:6.4f}V / {:6.4f}A (limit: {:6.4f}A)\n'.format(currsrc.measureVoltage(), currsrc.measureCurrent(), currsrc.queryCurrent())) 422 | 423 | print("Changing Output Voltage to 2.3V") 424 | currsrc.setVoltage(2.3) 425 | print('{:6.4f}V / {:6.4f}A (limit: {:6.4f}A)\n'.format(currsrc.measureVoltage(), currsrc.measureCurrent(), currsrc.queryCurrent())) 426 | 427 | print("Set Over-Voltage Protection to 3.6V") 428 | currsrc.setVoltageProtection(3.6) 429 | print('OVP: {:6.4f}V\n'.format(currsrc.queryVoltageProtection())) 430 | 431 | currsrc.voltageProtectionOff() 432 | 433 | print("Changing Output Voltage to 3.7V with OVP off") 434 | currsrc.setVoltage(3.7) 435 | print('{:6.4f}V / {:6.4f}A (limit: {:6.4f}A)\n'.format(currsrc.measureVoltage(), currsrc.measureCurrent(), currsrc.queryCurrent())) 436 | 437 | if (currsrc.isVoltageProtectionTripped()): 438 | print("OVP is TRIPPED as expected\n") 439 | else: 440 | print("OVP is not TRIPPED - FAILURE!\n") 441 | 442 | print("Enable OVP") 443 | currsrc.voltageProtectionOn() 444 | 445 | if (currsrc.isVoltageProtectionTripped()): 446 | print("OVP is TRIPPED as expected.\n") 447 | else: 448 | print("OVP is not TRIPPED - FAILURE!\n") 449 | 450 | print("Changing Output Voltage to 3.55V and clearing OVP Trip") 451 | currsrc.setVoltage(3.55) 452 | currsrc.voltageProtectionClear() 453 | print('{:6.4f}V / {:6.4f}A (limit: {:6.4f}A)\n'.format(currsrc.measureVoltage(), currsrc.measureCurrent(), currsrc.queryCurrent())) 454 | 455 | if (currsrc.isVoltageProtectionTripped()): 456 | print("OVP is TRIPPED as expected.\n") 457 | else: 458 | print("OVP is not TRIPPED - FAILURE!\n") 459 | 460 | ## Now, lets get to what 622x can actually do 461 | test_list = [(105e-3,0.1), (-105e-3,0.2), (1e-10,0.3), (-2.3e-12,1.4), (-567e-15,1.5), (50e-15,1.6), ] 462 | 463 | for vals in test_list: 464 | currsrc.setCurrent(vals[0]) 465 | currsrc.setVoltageProtection(vals[1]) 466 | 467 | if not currsrc.isOutputOn(args.chan): 468 | currsrc.outputOn() 469 | 470 | print ("Expect to see CURRENT set to {:3.4G} and Compliance set to {:3.4G}".format(*vals)) 471 | 472 | sleep (2.0) 473 | currsrc.outputOff() 474 | 475 | currsrc.beeperOn() 476 | 477 | ## return to LOCAL mode 478 | currsrc.setLocal() 479 | 480 | currsrc.close() 481 | -------------------------------------------------------------------------------- /dcps/IT6500C.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | 5 | # Copyright (c) 2023, Mikkel Jeppesen 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | #------------------------------------------------------------------------------- 26 | # Control an ITECH IT6500C/D series 2-quadrant power supply with PyVISA 27 | #------------------------------------------------------------------------------- 28 | 29 | # For future Python3 compatibility: 30 | from __future__ import absolute_import 31 | from __future__ import division 32 | from __future__ import print_function 33 | 34 | try: 35 | from . import SCPI 36 | except (ValueError, ImportError): 37 | from SCPI import SCPI 38 | 39 | import time 40 | import pyvisa as visa 41 | import re 42 | from warnings import warn 43 | 44 | class IT6500C(SCPI): 45 | """Basic class for controlling and accessing an ITECH 6500C/D series 2 quadrant DC Power Supply/load""" 46 | 47 | _xlateCmdTbl = { 48 | # Overrides 49 | 'isInput': 'LOAD:STATe?', 50 | 'inputOn': 'LOAD ON', 51 | 'inputOff': 'LOAD OFF', 52 | 'isVoltageProtectionTripped': 'PROTection:TRIGgered?', 53 | 'voltageProtectionClear': 'PROTection:CLEar', 54 | # new 55 | 'setCurrentRise': 'CURRent:Rise {}', 56 | 'setCurrentFall': 'CURRent:Fall {}', 57 | 'queryCurrentRise': 'CURRent:RISE?', 58 | 'queryCurrentFall': 'CURRent:Fall?', 59 | 'setVoltageRise': 'VOLTage:Rise {}', 60 | 'setVoltageFall': 'VOLTage:Fall {}', 61 | 'queryVoltageRise': 'VOLTage:RISE?', 62 | 'queryVoltageFall': 'VOLTage:Fall?', 63 | 'setPowerRise': 'POWer:Rise {}', 64 | 'setPowerFall': 'POWer:Fall {}', 65 | 'queryPowerRise': 'POWer:RISE?', 66 | 'queryPowerFall': 'POWer:Fall?', 67 | 'setDCRCapacity': 'DCR:BATTery:CAPACity {}', 68 | 'queryDCRCapacity': 'DCR:BATTery:CAPACity?', 69 | 'DCROn': 'DCR ON', 70 | 'DCROff': 'DCR OFF', 71 | 'isDCR': 'DCR?', 72 | 'DCRData': 'DCR:DATA?', 73 | } 74 | 75 | def __init__(self, resource, wait=1.0, verbosity=0, **kwargs): 76 | """Init the class with the instruments resource string 77 | 78 | resource - resource string or VISA descriptor, like USB0::INSTR 79 | wait - float that gives the default number of seconds to wait after sending each command 80 | verbosity - verbosity output - set to 0 for no debug output 81 | kwargs - other named options to pass when PyVISA open() like open_timeout=2.0 82 | """ 83 | super(IT6500C, self).__init__( 84 | resource, 85 | max_chan=1, 86 | wait=wait, 87 | cmd_prefix='', 88 | verbosity=verbosity, 89 | read_termination = '\n', 90 | write_termination = '\n', 91 | **kwargs) 92 | 93 | #------------------------------------------------ 94 | # Additional functions 95 | #------------------------------------------------ 96 | def setInternalResistance(self, resistance, wait=None): 97 | """Set the internal resistance of the PSU. Useful for battery simulation. 98 | 99 | resistance - floating point value for the internal resistance. 100 | Supports suffixes like E+2, M, K, m or u. 101 | wait - float of seconds to wait after sending command 102 | """ 103 | 104 | if wait is None: 105 | wait = self._wait 106 | 107 | str = "RES {}".format(resistance) 108 | self._instWrite(str) 109 | time.sleep(wait) 110 | 111 | def queryInternalResistance(self): 112 | """Returns what resistance the the PSU is configured to. 113 | """ 114 | 115 | return self.fetchGenericValue("RES?") 116 | 117 | def setCVPriority(self, priority, wait=None): 118 | """Set the priority of CV-loop. 119 | 120 | priority - LOW or HIGH 121 | wait - float of seconds to wait after sending command 122 | """ 123 | 124 | try: 125 | priority = priority.upper() 126 | assert priority in ['LOW', 'HIGH'] 127 | except (AttributeError, AssertionError): 128 | warn("Invalid priority: {priority}") 129 | 130 | if wait is None: 131 | wait = self._wait 132 | 133 | str = "CV:PRIority {}".format(priority) 134 | self._instWrite(str) 135 | time.sleep(wait) 136 | 137 | def setCCPriority(self, priority, wait=None): 138 | """Set the priority of CC-loop. High priority CC-loop is useful for example for LEDs or lasers 139 | 140 | priority - LOW or HIGH 141 | wait - float of seconds to wait after sending command 142 | """ 143 | 144 | try: 145 | priority = priority.upper() 146 | assert priority in ['LOW', 'HIGH'] 147 | except (AttributeError, AssertionError): 148 | warn("Invalid priority: {priority}") 149 | 150 | if wait is None: 151 | wait = self._wait 152 | 153 | str = "CC:PRIority {}".format(priority) 154 | self._instWrite(str) 155 | 156 | def queryCVPriority(self): 157 | """Returns what the current priority of the constant voltage loop is 158 | """ 159 | 160 | return self.fetchGenericString("CV:PRIority?") 161 | 162 | def queryCCPriority(self): 163 | """Returns what the current priority of the constant current loop is 164 | """ 165 | 166 | return self.fetchGenericString("CC:PRIority?") 167 | 168 | def setCurrentRise(self, seconds, wait=None): 169 | """Set the rise time of the output current 170 | 171 | seconds - The rise time in seconds 172 | wait - float of seconds to wait after sending command 173 | """ 174 | 175 | if wait is None: 176 | wait = self._wait 177 | 178 | str = self._Cmd('setCurrentRise').format(seconds) 179 | self._instWrite(str) 180 | time.sleep(wait) 181 | 182 | def setCurrentRise(self, seconds, wait=None): 183 | """Set the fall time of the output current 184 | 185 | seconds - The fall time in seconds 186 | wait - float of seconds to wait after sending command 187 | """ 188 | 189 | if wait is None: 190 | wait = self._wait 191 | 192 | str = self._Cmd('setCurrentFall').format(seconds) 193 | self._instWrite(str) 194 | time.sleep(wait) 195 | 196 | def queryCurrentRise(self): 197 | """Return what the current rise time is configured to 198 | """ 199 | 200 | return self.fetchGenericValue(self._Cmd('queryCurrentRise')) 201 | 202 | def queryCurrentFall(self): 203 | """Return what the current fall time is configured to 204 | """ 205 | 206 | return self.fetchGenericValue(self._Cmd('queryCurrentFall')) 207 | 208 | def setVoltageRise(self, seconds, wait=None): 209 | """Set the rise time of the output voltage 210 | 211 | seconds - The rise time in seconds 212 | wait - float of seconds to wait after sending command 213 | """ 214 | 215 | if wait is None: 216 | wait = self._wait 217 | 218 | str = self._Cmd('setVoltageRise').format(seconds) 219 | self._instWrite(str) 220 | time.sleep(wait) 221 | 222 | def setVoltageRise(self, seconds, wait=None): 223 | """Set the fall time of the output voltage 224 | 225 | seconds - The fall time in seconds 226 | wait - float of seconds to wait after sending command 227 | """ 228 | 229 | if wait is None: 230 | wait = self._wait 231 | 232 | str = self._Cmd('setVoltageFall').format(seconds) 233 | self._instWrite(str) 234 | time.sleep(wait) 235 | 236 | def queryVoltageRise(self): 237 | """Return what the voltage rise time is configured to 238 | """ 239 | 240 | return self.fetchGenericValue(self._Cmd('queryVoltageRise')) 241 | 242 | def queryVoltageFall(self): 243 | """Return what the voltage fall time is configured to 244 | """ 245 | 246 | return self.fetchGenericValue(self._Cmd('queryVoltageFall')) 247 | 248 | def setPowerRise(self, seconds, wait=None): 249 | """Set the rise time of the output power 250 | 251 | seconds - The rise time in seconds 252 | wait - float of seconds to wait after sending command 253 | """ 254 | 255 | if wait is None: 256 | wait = self._wait 257 | 258 | str = self._Cmd('setPowerRise').format(seconds) 259 | self._instWrite(str) 260 | time.sleep(wait) 261 | 262 | def setPowerRise(self, seconds, wait=None): 263 | """Set the fall time of the output power 264 | 265 | seconds - The fall time in seconds 266 | wait - float of seconds to wait after sending command 267 | """ 268 | 269 | if wait is None: 270 | wait = self._wait 271 | 272 | str = self._Cmd('setPowerFall').format(seconds) 273 | self._instWrite(str) 274 | time.sleep(wait) 275 | 276 | def queryPowerRise(self): 277 | """Return what the power rise time is configured to 278 | """ 279 | 280 | return self.fetchGenericValue(self._Cmd('queryPowerRise')) 281 | 282 | def queryPowerFall(self): 283 | """Return what the power fall time is configured to 284 | """ 285 | 286 | return self.fetchGenericValue(self._Cmd('queryPowerFall')) 287 | 288 | def setDCRCapacity(self, amp_hours, wait=None): 289 | """Set the capacity of the battery under test 290 | 291 | amp_hours - The capacity of the battery in amp amp_hours 292 | wait - float of seconds to wait after sending command 293 | """ 294 | 295 | if wait is None: 296 | wait = self._wait 297 | 298 | str = self._Cmd('setDCRCapacity').format(amp_hours) 299 | self._instWrite(str) 300 | time.sleep(wait) 301 | 302 | def queryDCRCapacity(self): 303 | """Returns the configured battery capacity of the DCR DUT 304 | """ 305 | 306 | return self.fetchGenericValue(self._Cmd('queryDCRCapacity')) 307 | 308 | def DCROn(self, wait=None): 309 | """Enables the DCR measurement 310 | """ 311 | 312 | if wait is None: 313 | wait = self._wait 314 | 315 | str = self._Cmd('DCROn') 316 | self._instWrite(str) 317 | time.sleep(wait) 318 | 319 | def DCROff(self, wait=None): 320 | """Disables the DCR measurement 321 | """ 322 | 323 | if wait is None: 324 | wait = self._wait 325 | 326 | str = self._Cmd('DCROff') 327 | self._instWrite(str) 328 | time.sleep(wait) 329 | 330 | def isDCROn(self): 331 | """Returns True if the DCR measurement is enabled 332 | """ 333 | 334 | str = self._Cmd('isDCR') 335 | return self.fetchGenericBoolean(str) 336 | 337 | def measureDCR(self): 338 | """Returns the DC resistance of the battery DUT being charged 339 | """ 340 | 341 | str = self._Cmd('DCRData') 342 | return self.fetchGenericValue(str) 343 | 344 | #------------------------------------------------ 345 | # Unsupported function overloads. Will warn user 346 | #------------------------------------------------ 347 | def queryVoltageRange(self, channel=None): 348 | """UNSUPPORTED: Query the voltage range for channel 349 | 350 | channel - number of the channel starting at 1 351 | """ 352 | 353 | warn("Not supported on this device") 354 | 355 | def queryCurrentRange(self, channel=None): 356 | """UNSUPPORTED: Query the voltage range for channel 357 | 358 | channel - number of the channel starting at 1 359 | """ 360 | 361 | warn("Not supported on this device") 362 | 363 | def setFunction(self, function, channel=None, wait=None): 364 | """UNSUPPORTED: Set the source function for the channel 365 | 366 | function - a string which names the function. common ones: 367 | VOLTage, CURRent, RESistance, POWer 368 | wait - number of seconds to wait after sending command 369 | channel - number of the channel starting at 1 370 | """ 371 | 372 | warn("Not supported on this device") 373 | 374 | def queryFunction(self, channel=None, query_delay=None): 375 | """UNSUPPORTED Return what FUNCTION is the current one for sourcing 376 | 377 | channel - number of the channel starting at 1 378 | query_delay - number of seconds to wait between write and 379 | reading for read data (None uses default seconds) 380 | """ 381 | 382 | warn("Not supported on this device") 383 | 384 | def setCurrentRange(self, upper, channel=None, wait=None): 385 | """UNSUPPORTED Set the current range for channel 386 | 387 | upper - floating point value for upper current range, set to None for AUTO 388 | channel - number of the channel starting at 1 389 | wait - number of seconds to wait after sending command 390 | """ 391 | 392 | warn("Not supported on this device") 393 | 394 | def setVoltageRange(self, value, channel=None, wait=None): 395 | """Unsupported Set the voltage range for channel 396 | 397 | value - floating point value for voltage range, set to None for AUTO 398 | channel - number of the channel starting at 1 399 | wait - number of seconds to wait after sending command 400 | """ 401 | 402 | warn("Not supported on this device") 403 | 404 | def measureVoltageMax(self, channel=None): 405 | """UNSUPPORTED: Read and return the maximum voltage measurement from channel 406 | 407 | channel - number of the channel starting at 1 408 | """ 409 | 410 | warn("Not supported on this device") 411 | 412 | def measureVoltageMin(self, channel=None): 413 | """UNSUPPORTED: Read and return the minimum voltage measurement from channel 414 | 415 | channel - number of the channel starting at 1 416 | """ 417 | 418 | warn("Not supported on this device") 419 | 420 | def measureCurrentMax(self, channel=None): 421 | """UNSUPPORTED: Read and return the maximum current measurement from channel 422 | 423 | channel - number of the channel starting at 1 424 | """ 425 | 426 | warn("Not supported on this device") 427 | 428 | def measureCurrentMin(self, channel=None): 429 | """UNSUPPORTED: Read and return the minimum current measurement from channel 430 | 431 | channel - number of the channel starting at 1 432 | """ 433 | 434 | warn("Not supported on this device") 435 | 436 | def setMeasureVoltageRange(self, upper, channel=None, wait=None): 437 | """UNSUPPORTED: Set the measurement voltage range for channel 438 | 439 | upper - floating point value for upper voltage range, set to None for AUTO 440 | channel - number of the channel starting at 1 441 | wait - number of seconds to wait after sending command 442 | """ 443 | 444 | warn("Not supported on this device") 445 | 446 | def queryMeasureVoltageRange(self, channel=None): 447 | """UNSUPPORTED: Query the measurement voltage range for channel 448 | 449 | channel - number of the channel starting at 1 450 | """ 451 | 452 | warn("Not supported on this device") 453 | 454 | def measureResistance(self, channel=None): 455 | """UNSUPPORTED: Read and return a resistance measurement from channel 456 | 457 | channel - number of the channel starting at 1 458 | """ 459 | 460 | warn("Not supported on this device") 461 | 462 | def setVoltageCompliance(self, ovp, channel=None, wait=None): 463 | """UNSUPPORTED: Set the over-voltage compliance value for the channel. This is the measurement value at which the output is disabled. 464 | 465 | ovp - desired over-voltage compliance value as a floating point number 466 | channel - number of the channel starting at 1 467 | wait - number of seconds to wait after sending command 468 | """ 469 | 470 | warn("Not supported on this device") 471 | 472 | def queryVoltageCompliance(self, channel=None): 473 | """UNSUPPORTED: Return what the over-voltage compliance set value is 474 | 475 | channel - number of the channel starting at 1 476 | """ 477 | 478 | warn("Not supported on this device") 479 | 480 | def isVoltageComplianceTripped(self, channel=None): 481 | """UNSUPPORTED: Return true if the Voltage Compliance of channel is Tripped, else false 482 | 483 | channel - number of the channel starting at 1 484 | """ 485 | 486 | warn("Not supported on this device") 487 | 488 | def voltageComplianceClear(self, channel=None, wait=None): 489 | """UNSUPPORTED: Clear Voltage Compliance Trip on the output for channel 490 | 491 | channel - number of the channel starting at 1 492 | wait - number of seconds to wait after sending command 493 | """ 494 | 495 | warn("Not supported on this device") 496 | 497 | def isCurrentProtectionTripped(self, channel=None): 498 | """UNSUPPORTED: Return true if the OverCurrent Protection of channel is Tripped, else false 499 | 500 | channel - number of the channel starting at 1 501 | """ 502 | 503 | warn("Not supported on this device") 504 | 505 | def currentProtectionClear(self, channel=None, wait=None): 506 | """UNSUPPORTED: Clear Over-Current Protection Trip on the output for channel 507 | 508 | channel - number of the channel starting at 1 509 | wait - number of seconds to wait after sending command 510 | """ 511 | 512 | warn("Not supported on this device") 513 | 514 | def setCurrentCompliance(self, ocp, channel=None, wait=None): 515 | """UNSUPPORTED: Set the over-current compliance value for the channel. This is the measurement value at which the output is disabled. 516 | 517 | ocp - desired over-current compliance value as a floating point number 518 | channel - number of the channel starting at 1 519 | wait - number of seconds to wait after sending command 520 | """ 521 | 522 | warn("Not supported on this device") 523 | 524 | def queryCurrentCompliance(self, channel=None): 525 | """UNSUPPORTED: Return what the over-current compliance set value is 526 | 527 | channel - number of the channel starting at 1 528 | """ 529 | 530 | warn("Not supported on this device") 531 | 532 | def isCurrentComplianceTripped(self, channel=None): 533 | """UNSUPPORTED: Return true if the Current Compliance of channel is Tripped, else false 534 | 535 | channel - number of the channel starting at 1 536 | """ 537 | 538 | warn("Not supported on this device") 539 | 540 | def currentComplianceClear(self, channel=None, wait=None): 541 | """UNSUPPORTED: Clear Current Compliance Trip on the output for channel 542 | 543 | channel - number of the channel starting at 1 544 | wait - number of seconds to wait after sending command 545 | """ 546 | 547 | warn("Not supported on this device") 548 | 549 | 550 | 551 | 552 | if __name__ == '__main__': 553 | import pyvisa 554 | 555 | rm = pyvisa.ResourceManager('@py') 556 | devices = rm.list_resources() 557 | 558 | print(devices) 559 | 560 | resource = None 561 | 562 | if len(devices) == 1: 563 | resource = devices[0] 564 | else: 565 | # Print 5 devices at a time, 566 | # have the user pick one of them by entering their ID or 'n' for next 5 567 | offset = 0 568 | while resource is None: 569 | dev_slice = devices[offset:offset+5] 570 | for idx, dev in enumerate(dev_slice): 571 | print("{}: {}".format(idx+offset, dev)) 572 | 573 | print('n: for next 5 resources') 574 | while True: 575 | choice = input("Pick a resource (enter the number)") 576 | if choice == 'n': 577 | offset += 5 578 | if offset > len(devices): 579 | offset = 0 580 | print('') 581 | break 582 | 583 | try: 584 | choice = int(choice) 585 | except ValueError: 586 | pass 587 | else: 588 | if choice >= offset and choice < offset + 5-1: 589 | resource = devices[choice] 590 | break 591 | 592 | # If we get here the user didn't provide a valid input 593 | print("Invalid input") 594 | 595 | psu = IT6500C(resource) 596 | psu.open() 597 | 598 | ## set Remote mode 599 | psu.setRemote() 600 | 601 | # psu.beeperOff() 602 | 603 | if not psu.isOutputOn(): 604 | psu.outputOn() 605 | 606 | 607 | vol = psu.queryVoltage() 608 | cur = psu.queryCurrent() 609 | print(f'Settings: {vol:6.4f} V {cur:6.4f} A') 610 | 611 | voltageSave = psu.queryVoltage() 612 | 613 | #print(psu.idn()) 614 | print('{:6.4f} V'.format(psu.measureVoltage())) 615 | print('{:6.4f} A'.format(psu.measureCurrent())) 616 | print('{:6.4f} W'.format(psu.measurePower())) 617 | 618 | psu.setVoltage(2.7) 619 | 620 | print('{:6.4f} V'.format(psu.measureVoltage())) 621 | print('{:6.4f} A'.format(psu.measureCurrent())) 622 | print('{:6.4f} W'.format(psu.measurePower())) 623 | time.sleep(2) 624 | psu.setVoltage(2.3) 625 | 626 | print('{:6.4f} V'.format(psu.measureVoltage())) 627 | print('{:6.4f} A'.format(psu.measureCurrent())) 628 | print('{:6.4f} W'.format(psu.measurePower())) 629 | 630 | time.sleep(2) 631 | psu.setVoltage(voltageSave) 632 | 633 | print('{:6.4f} V'.format(psu.measureVoltage()), end='\t') 634 | print('{:6.4f} A'.format(psu.measureCurrent()), end='\t') 635 | print('{:6.4f} W'.format(psu.measurePower())) 636 | time.sleep(1) 637 | 638 | ## turn off the channel 639 | psu.outputOff() 640 | 641 | psu.beeperOn() 642 | 643 | ## return to LOCAL mode 644 | psu.setLocal() 645 | 646 | psu.close() 647 | -------------------------------------------------------------------------------- /dcps/Keithley2182.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | 5 | # Copyright (c) 2021, Stephen Goadhouse 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | #----------------------------------------------------------------------------- 26 | # Control a Keithley/Tektronix 2182/2182A Nanovoltmeter 27 | #----------------------------------------------------------------------------- 28 | 29 | # For future Python3 compatibility: 30 | from __future__ import absolute_import 31 | from __future__ import division 32 | from __future__ import print_function 33 | 34 | try: 35 | from . import SCPI 36 | except: 37 | from SCPI import SCPI 38 | 39 | import re 40 | from time import sleep 41 | import pyvisa as visa 42 | 43 | class Keithley2182(SCPI): 44 | """Basic class for controlling and accessing a Keithley/Tektronix 45 | 2182/2182A Nanovoltmeter. Although this is not a traditional DC 46 | power supply, it uses many of the same interface commands so 47 | including here seems logical. 48 | 49 | If the VISA resource string is of the form TCPIP[n]::*::23::SOCKET, 50 | it is assumed that the power supply is being accessed using a 51 | KISS-488 Ethernet to GPIB adapter 52 | (https://www.ebay.com/itm/114514724752) that is properly 53 | configured to access the power supply at its GPIB address 54 | (default is 7). 55 | 56 | If the VISA resource string is of the form TCPIP[n]::*::1234::SOCKET, 57 | it is assumed that the power supply is being accessed using a 58 | Prologix Ethernet to GPIB adapter 59 | (http://prologix.biz/gpib-ethernet-controller.html). The 60 | Prologix has meta-commands to set GPIB address and such. 61 | 62 | It should be possible to use this directly over GPIB or with a 63 | USB to GPIB interface by modifying the resource string but some 64 | minor code edits may be needed. For now, this code has only 65 | been tested with a KISS-488 or Prologix Ethernet to GPIB interface. 66 | 67 | Currently, could not get the KISS-488 interface to fully 68 | support the Keithley 2182 although it works with other devices. 69 | The Prologix interface worked great with the 2182. 70 | 71 | """ 72 | 73 | ## Dictionary to translate SCPI commands for this device 74 | _xlateCmdTbl = { 75 | 'chanSelect': 'SENS:CHAN {}', 76 | } 77 | 78 | def __init__(self, resource, gaddr=7, wait=0.25, verbosity=0, query_delay=0.8, **kwargs): 79 | """Init the class with the instruments resource string 80 | 81 | resource - resource string or VISA descriptor, like TCPIP0::172.16.2.13::23::SOCKET 82 | gaddr - GPIB bus address of instrument - this is only useful if using Prologix interface 83 | wait - float that gives the default number of seconds to wait after sending each command 84 | verbosity - verbosity output - set to 0 for no debug output 85 | kwargs - other named options to pass when PyVISA open() like open_timeout=2.0 86 | """ 87 | 88 | # Set defaults 89 | self._enetgpib = False # True if an Ethernet to GPIB interface is being used 90 | self._kiss488 = False # True if the Ethernet to GPIB interface is a KISS-488 91 | self._prologix = False # True if the Ethernet to GPIB interface is a Prologix 92 | 93 | ## regexp for resource string that indicates it is being used with KISS-488 or Prologix 94 | reskiss488 = re.compile("^TCPIP[0-9]*::.*::23::SOCKET$") 95 | resprologix = re.compile("^TCPIP[0-9]*::.*::1234::SOCKET$") 96 | if (reskiss488.match(resource)): 97 | self._enetgpib = True 98 | self._kiss488 = True 99 | elif (resprologix.match(resource)): 100 | self._enetgpib = True 101 | self._prologix = True 102 | 103 | # save some parameters in case need it 104 | self._gaddr = gaddr 105 | self._query_delay = query_delay 106 | 107 | super(Keithley2182, self).__init__(resource, max_chan=2, wait=wait, cmd_prefix=':', 108 | verbosity = verbosity, 109 | read_termination = '\n', write_termination = '\n', 110 | timeout=2, # found that needed longer timeout 111 | query_delay=query_delay, # for open_resource() 112 | **kwargs) 113 | 114 | # NaN for this instrument 115 | self.NaN = +9.9E37 116 | 117 | 118 | def open(self): 119 | """ Overloaded open() so can handle GPIB interfaces after opening the connection """ 120 | 121 | super(Keithley2182, self).open() 122 | 123 | if (self._kiss488): 124 | # Give the instrument time to output whatever initial output it may send 125 | sleep(1.5) 126 | 127 | ## Can clear strings instead of reading and printing them out 128 | #@@@#self._inst.clear() 129 | 130 | # Read out any strings that are sent after connecting (happens 131 | # for KISS-488 and may happen with other interfaces) 132 | try: 133 | while True: 134 | bytes = self._inst.read_raw() 135 | if (self._kiss488): 136 | # If the expected header from KISS-488, print it out, otherwise ignore. 137 | if ('KISS-488'.encode() in bytes): 138 | print(bytes.decode('utf-8').strip()) 139 | except visa.errors.VisaIOError as err: 140 | if (err.error_code != visa.constants.StatusCode.error_timeout): 141 | # Ignore timeouts here since just reading strings until they stop. 142 | # Output any other errors 143 | print("ERROR: {}, {}".format(err, type(err))) 144 | 145 | elif (self._prologix): 146 | # Configure mode, addr, auto and print out ver 147 | self._instWrite('++mode 1') # make sure in CONTROLLER mode 148 | self._instWrite('++auto 0') # will explicitly tell when to read instrument 149 | self._instWrite('++addr {}'.format(self._gaddr)) # set GPIB address 150 | self._instWrite('++eos 2') # append '\n' / LF to instrument commands 151 | self._instWrite('++eoi 1') # enable EOI assertion with commands 152 | self._instWrite('++read_tmo_ms 1200') # Set the Read Timeout to 1200 ms 153 | #@@@#self._instWrite('++eot_char 10') # @@@ 154 | self._instWrite('++eot_enable 0') # Do NOT append character when EOI detected 155 | 156 | # Read and print out Version string. Using write/read to 157 | # avoid having '++read' appended if use Query. It is not 158 | # needed for ++ commands and causes a warning if used. 159 | self._instWrite('++ver') 160 | sleep(self._query_delay) 161 | print(self._inst.read()) 162 | 163 | #@@@#self.printAllErrors() 164 | #@@@#self.cls() 165 | 166 | 167 | def _instQuery(self, queryStr): 168 | """ Overload _instQuery from SCPI.py so can append the \r if KISS-488 or add ++read if Prologix""" 169 | # Need to also strip out any leading or trailing white space from the response 170 | 171 | # KISS-488 requires queries to end in '\r' so it knows a response is expected 172 | if (self._kiss488): 173 | queryStr += '\r' 174 | elif (self._prologix): 175 | queryStr += self._write_termination + '++read eoi' 176 | 177 | if self._verbosity >= 4: 178 | print("OUT/" + ":".join("{:02x}".format(ord(c)) for c in queryStr)) 179 | resp = super(Keithley2182, self)._instQuery(queryStr).strip() 180 | if self._verbosity >= 4: 181 | print("IN /" + ":".join("{:02x}".format(ord(c)) for c in resp)) 182 | print(resp) 183 | 184 | return resp 185 | 186 | def setLocal(self): 187 | """If KISS-488, disable the system local command for the instrument 188 | If Prologix, issue GPIB command to unlock the front panel 189 | """ 190 | 191 | if (self._kiss488): 192 | # NOTE: Unsupported command if using KISS-488 with this power 193 | # supply. However, instead of raising an exception and 194 | # breaking any scripts, simply return quietly. 195 | pass 196 | elif (self._prologix): 197 | self._instWrite('++loc') # issue GPIB command to enable front panel 198 | 199 | 200 | def setRemote(self): 201 | """If KISS-488, disable the system remote command for the instrument 202 | If Prologix, issue GPIB command to lock the front panel 203 | """ 204 | 205 | if (self._kiss488): 206 | # NOTE: Unsupported command if using KISS-488 with this power supply. However, 207 | # instead of raising an exception and breaking any scripts, 208 | # simply return quietly. 209 | pass 210 | elif (self._prologix): 211 | self._instWrite('++llo') # issue GPIB command to disable front panel 212 | 213 | def setRemoteLock(self): 214 | """Disable the system remote lock command for the instrument""" 215 | # NOTE: Unsupported command by this power supply. However, 216 | # instead of raising an exception and breaking any scripts, 217 | # simply return quietly. 218 | pass 219 | 220 | def setVoltage(self, voltage, channel=None, wait=None): 221 | """The 2182 has no way to set the output voltage. Ignoring command. 222 | 223 | voltage - desired voltage value as a floating point number 224 | wait - number of seconds to wait after sending command 225 | channel - number of the channel starting at 1 226 | """ 227 | print('NOTE: The Keithley 2182 cannot set a voltage. It can only measure voltages.\nIgnoring this command') 228 | 229 | def queryVoltage(self, channel=None): 230 | """The 2182 has no way to query the output voltage setting. Return invalid value. 231 | 232 | channel - number of the channel starting at 1 233 | """ 234 | # If a channel number is passed in, make it the 235 | # current channel 236 | if channel is not None: 237 | self.channel = channel 238 | 239 | if (self._max_chan > 1 and channel is not None): 240 | # If multi-channel device and channel parameter is passed, select it 241 | self._instWrite(self._Cmd('chanSelect').format(self.channel)) 242 | 243 | return (self.NaN) 244 | 245 | def queryCurrent(self, channel=None): 246 | """The 2182 has no way to query the output current setting. Return invalid value. 247 | 248 | channel - number of the channel starting at 1 249 | """ 250 | # If a channel number is passed in, make it the 251 | # current channel 252 | if channel is not None: 253 | self.channel = channel 254 | 255 | if (self._max_chan > 1 and channel is not None): 256 | # If multi-channel device and channel parameter is passed, select it 257 | self._instWrite(self._Cmd('chanSelect').format(self.channel)) 258 | 259 | return (self.NaN) 260 | 261 | def measureVoltage(self, channel=None): 262 | """Read and return a voltage measurement from channel 263 | 264 | channel - number of the channel starting at 1 265 | """ 266 | 267 | # If a channel number is passed in, make it the 268 | # current channel 269 | if channel is not None: 270 | self.channel = channel 271 | 272 | # select voltage function 273 | # 274 | # NOTE: not sure if it slows things down to do this every time 275 | # so may want to make this smarter if need to speed up measurements. 276 | self._instWrite("SENS:FUNC 'VOLT'") 277 | 278 | # Always select channel, even if channel parameter is not passed in. 279 | self._instWrite(self._Cmd('chanSelect').format(self.channel)) 280 | 281 | val = self._instQuery('READ?') 282 | return float(val) 283 | 284 | def measureCurrent(self, channel=None): 285 | """The 2182 performs no current measurements so override this command 286 | 287 | channel - number of the channel starting at 1 288 | """ 289 | print('NOTE: The Keithley 2182 performs no current measurements. Ignoring this command') 290 | return (self.NaN) 291 | 292 | def setVoltageProtection(self, ovp, delay=None, channel=None, wait=None): 293 | """The 2182 has no output voltage protection/compliance to be set. Ignore except for any channel setting. 294 | 295 | ovp - desired over-voltage value as a floating point number 296 | delay - desired voltage protection delay time in seconds (not always supported) 297 | wait - number of seconds to wait after sending command 298 | channel - number of the channel starting at 1 299 | """ 300 | 301 | # If a channel number is passed in, make it the 302 | # current channel 303 | if channel is not None: 304 | self.channel = channel 305 | 306 | if (self._max_chan > 1 and channel is not None): 307 | # If multi-channel device and channel parameter is passed, select it 308 | self._instWrite(self._Cmd('chanSelect').format(self.channel)) 309 | 310 | def queryVoltageProtection(self, channel=None): 311 | """The 2182 has no output voltage protection/compliance setting to query. Return invalid value. 312 | 313 | channel - number of the channel starting at 1 314 | """ 315 | # If a channel number is passed in, make it the 316 | # current channel 317 | if channel is not None: 318 | self.channel = channel 319 | 320 | if (self._max_chan > 1 and channel is not None): 321 | # If multi-channel device and channel parameter is passed, select it 322 | self._instWrite(self._Cmd('chanSelect').format(self.channel)) 323 | 324 | return (self.NaN) 325 | 326 | def voltageProtectionOn(self, channel=None, wait=None): 327 | """The 2182 has no voltage protection/compliance. Ignore command. 328 | 329 | wait - number of seconds to wait after sending command 330 | channel - number of the channel starting at 1 331 | """ 332 | # If a channel number is passed in, make it the 333 | # current channel 334 | if channel is not None: 335 | self.channel = channel 336 | 337 | if (self._max_chan > 1 and channel is not None): 338 | # If multi-channel device and channel parameter is passed, select it 339 | self._instWrite(self._Cmd('chanSelect').format(self.channel)) 340 | 341 | pass 342 | 343 | def voltageProtectionOff(self, channel=None, wait=None): 344 | """The 2182 has no voltage protection/compliance. Ignore command. 345 | 346 | channel - number of the channel starting at 1 347 | """ 348 | # If a channel number is passed in, make it the 349 | # current channel 350 | if channel is not None: 351 | self.channel = channel 352 | 353 | if (self._max_chan > 1 and channel is not None): 354 | # If multi-channel device and channel parameter is passed, select it 355 | self._instWrite(self._Cmd('chanSelect').format(self.channel)) 356 | 357 | pass 358 | 359 | def voltageProtectionClear(self, channel=None, wait=None): 360 | """The 2182 has no voltage protection/compliance. Ignore command. 361 | 362 | channel - number of the channel starting at 1 363 | """ 364 | # If a channel number is passed in, make it the 365 | # current channel 366 | if channel is not None: 367 | self.channel = channel 368 | 369 | if (self._max_chan > 1 and channel is not None): 370 | # If multi-channel device and channel parameter is passed, select it 371 | self._instWrite(self._Cmd('chanSelect').format(self.channel)) 372 | 373 | pass 374 | 375 | def isVoltageProtectionTripped(self, channel=None): 376 | """The 2182 cannot tell if the compliance limit has been reached or 377 | not. So always return True so if someone uses this, 378 | hopefully the True will force them to figure out what is 379 | going on. 380 | 381 | channel - number of the channel starting at 1 382 | 383 | """ 384 | # If a channel number is passed in, make it the 385 | # current channel 386 | if channel is not None: 387 | self.channel = channel 388 | 389 | if (self._max_chan > 1 and channel is not None): 390 | # If multi-channel device and channel parameter is passed, select it 391 | self._instWrite(self._Cmd('chanSelect').format(self.channel)) 392 | 393 | return True 394 | 395 | ################################################################### 396 | # Commands Specific to 2182 397 | ################################################################### 398 | 399 | def setLineSync(self,on,wait=None): 400 | """Enable/Disable Line Cycle Synchronization to reduce/increase measurement noise at expense of acquisition time. 401 | 402 | on - if True, Enable LSYNC, else Disable LSYNC 403 | wait - number of seconds to wait after sending command (need some time) 404 | """ 405 | 406 | # If a wait time is NOT passed in, set wait to the 407 | # default time 408 | if wait is None: 409 | wait = self._wait 410 | 411 | self._instWrite('SYSTem:LSYNc {}'.format(self._bool2onORoff(on))) 412 | 413 | sleep(wait) # give some time for device to respond 414 | 415 | def queryLineSync(self): 416 | """Query state of Line Cycle Synchronization 417 | 418 | returns True if LSYNC is Enabled, else returns False 419 | """ 420 | 421 | ret = self._instQuery('SYSTem:LSYNc?') 422 | return self._onORoff_1OR0_yesORno(ret) 423 | 424 | def displayMessageOn(self, top=True): 425 | """Enable Display Message 426 | 427 | top - Ignored (used by other models) 428 | """ 429 | 430 | self._instWrite('DISP:WIND1:TEXT:STAT ON') 431 | 432 | def displayMessageOff(self, top=True): 433 | """Disable Display Message 434 | 435 | top - Ignored (used by other models) 436 | """ 437 | 438 | self._instWrite('DISP:WIND1:TEXT:STAT OFF') 439 | 440 | 441 | def setDisplayMessage(self, message, top=True): 442 | """Set the Message for Display. Use displayMessageOn() or 443 | displayMessageOff() to enable or disable message, respectively. 444 | 445 | message - message to set 446 | top - Ignored (used by other models) 447 | 448 | """ 449 | 450 | # Maximum of 12 characters for top message 451 | if (len(message) > 12): 452 | message = message[:12] 453 | self._instWrite('DISP:WIND1:TEXT:DATA "{}"'.format(message)) 454 | 455 | def queryIntTemperature(self): 456 | """Return the internal temperature of meter 457 | """ 458 | 459 | ret = self._instQuery('SENS:TEMP:RTEM?') 460 | return float(ret) 461 | 462 | def setVoltageRange(self, upper, channel=None): 463 | """Set the voltage range for channel 464 | 465 | upper - floating point value for upper voltage range, set to None for AUTO 466 | channel - number of the channel starting at 1 467 | """ 468 | 469 | # If a channel number is passed in, make it the 470 | # current channel 471 | if channel is not None: 472 | self.channel = channel 473 | 474 | if (upper is None): 475 | # Set for AUTO range 476 | str = 'SENS:VOLT:CHAN{:1d}:RANG:AUTO ON'.format(self.channel) 477 | self._instWrite(str) 478 | else: 479 | # Disable AUTO range and set the upper value to upper argument 480 | str = 'SENS:VOLT:CHAN{:1d}:RANG:AUTO OFF'.format(self.channel) 481 | self._instWrite(str) 482 | str = 'SENS:VOLT:CHAN{:1d}:RANG {:.3e}'.format(self.channel,float(upper)) 483 | self._instWrite(str) 484 | 485 | def queryVoltageRange(self, channel=None): 486 | """Query the voltage range for channel 487 | 488 | channel - number of the channel starting at 1 489 | """ 490 | 491 | # If a channel number is passed in, make it the 492 | # current channel 493 | if channel is not None: 494 | self.channel = channel 495 | 496 | # First, query if AUTO is set and then query UPPER range setting 497 | qry = 'SENS:VOLT:CHAN{:1d}:RANG:AUTO?'.format(self.channel) 498 | auto = self._instQuery(qry) 499 | 500 | qry = 'SENS:VOLT:CHAN{:1d}:RANG?'.format(self.channel) 501 | upper = self._instQuery(qry) 502 | 503 | # If AUTO is enabled, return string 'AUTO', else return the upper range string 504 | if (self._onORoff_1OR0_yesORno(auto)): 505 | return 'AUTO' 506 | else: 507 | return upper 508 | 509 | if __name__ == '__main__': 510 | import argparse 511 | parser = argparse.ArgumentParser(description='Access and control a Keithley/Tektronix 2182 Nanovoltmeter') 512 | parser.add_argument('chan', nargs='?', type=int, help='Channel to access/control (max channel: 2)', default=1) 513 | args = parser.parse_args() 514 | 515 | from time import sleep 516 | from os import environ 517 | import sys 518 | 519 | resource = environ.get('K2182_VISA', 'TCPIP0::192.168.1.20::23::SOCKET') 520 | nanovm = Keithley2182(resource, gaddr=7, verbosity=1, query_delay=0.8) 521 | nanovm.open() 522 | 523 | # Reset to power on default 524 | nanovm.rst() 525 | 526 | print(nanovm.idn()) 527 | print() 528 | 529 | # See if any errors in queue and print them if there are 530 | print('\nQuerying and printing out any SCPI errors in error queue of instrument:') 531 | nanovm.printAllErrors() 532 | print() 533 | 534 | nanovm.beeperOff() 535 | 536 | origLineSync = nanovm.queryLineSync() 537 | if (not origLineSync): 538 | # Enable Line Sync if not enabled 539 | nanovm.setLineSync(True) 540 | 541 | # Set Voltage Range to AUTO 542 | nanovm.setVoltageRange(None) 543 | 544 | print('Internal Temperature: {:6.4f} C'. 545 | format(nanovm.queryIntTemperature())) 546 | 547 | print('Voltage: {:6.4e} V\n'.format(nanovm.measureVoltage())) 548 | sleep(2.0) 549 | 550 | # Set display messages 551 | nanovm.setDisplayMessage('Hey Man!') 552 | 553 | # Enable it 554 | nanovm.displayMessageOn() 555 | sleep(2.0) 556 | 557 | # Disable it 558 | nanovm.displayMessageOff() 559 | sleep(1.0) 560 | 561 | ## NOTE: Most of the following functions are attempting to treat 562 | ## the 2182 like a power supply. The 2182 will either ignore most 563 | ## of these or return self.NaN. These functions are here mainly to 564 | ## make sure that these unused functions are handled cleanly. 565 | 566 | print('Ch. {} Settings: {:6.4f} V {:6.4f} A {:4.2f} C '. 567 | format(args.chan, nanovm.queryVoltage(), 568 | nanovm.queryCurrent(), 569 | nanovm.queryIntTemperature())) 570 | 571 | voltageSave = nanovm.queryVoltage() 572 | 573 | print('{:6.4f}V / {:6.4f}A (limit: {:6.4f}A)\n'.format(nanovm.measureVoltage(), nanovm.measureCurrent(), nanovm.queryCurrent())) 574 | 575 | print("Changing Output Voltage to 2.3V") 576 | nanovm.setVoltage(2.3) 577 | print('{:6.4f}V / {:6.4f}A (limit: {:6.4f}A)\n'.format(nanovm.measureVoltage(), nanovm.measureCurrent(), nanovm.queryCurrent())) 578 | 579 | print("Set Over-Voltage Protection to 3.6V") 580 | nanovm.setVoltageProtection(3.6) 581 | print('OVP: {:6.4f}V\n'.format(nanovm.queryVoltageProtection())) 582 | 583 | nanovm.voltageProtectionOff() 584 | 585 | print("Changing Output Voltage to 3.7V with OVP off") 586 | nanovm.setVoltage(3.7) 587 | print('{:6.4f}V / {:6.4f}A (limit: {:6.4f}A)\n'.format(nanovm.measureVoltage(), nanovm.measureCurrent(), nanovm.queryCurrent())) 588 | 589 | ## Now, lets get to what 2182 can actually do 590 | 591 | print("Step through different channel and voltage range settings.") 592 | print("Ch. 1 has five ranges: 10mV, 100mV, 1V, 10V, 100V") 593 | print("Ch. 2 has three ranges: 100mV, 1V, 10V\n") 594 | 595 | test_list = [(1,None,'AUTO'), 596 | (1,100, '100.000000'), 597 | (1,110, '100.000000'), 598 | (2,None, 'AUTO'), 599 | (2,1.0, '1.000000'), 600 | (2,8, '10.000000'), 601 | (2,11, '10.000000'), 602 | (2,0.9, '1.000000'), 603 | (2,10.1,'10.000000'), 604 | (2,1e-6, '0.100000'), 605 | (1,1e-3, '0.010000'), 606 | (1,0.001,'0.010000'), 607 | (2,1e-8, '0.100000'), 608 | (1,1e-8, '0.010000'), 609 | (1,0.007,'0.010000'), 610 | (1, 10, '10.000000'), 611 | (1,0.8, '1.000000'), 612 | (1,2e-2, '0.100000'), 613 | ] 614 | 615 | for vals in test_list: 616 | nanovm.setVoltageRange(vals[1],channel=vals[0]) 617 | 618 | volt = nanovm.measureVoltage() 619 | if volt == nanovm.NaN: 620 | voltstr = ' Ovrflow' 621 | else: 622 | voltstr = '{:6.4e}'.format(volt) 623 | 624 | rangestr = nanovm.queryVoltageRange() 625 | print('Test: Ch. {}/VRange {:5s} |{:10s}| Results: {} V {:4.2f} C '. 626 | format(nanovm.channel, str(vals[1]), rangestr, voltstr, 627 | nanovm.queryIntTemperature())) 628 | if (vals[2] != rangestr): 629 | print('Unexpected Voltage Range Query: Exp. {} Act. {}'.format(vals[2],rangestr)) 630 | 631 | #@@@#nanovm.printAllErrors() 632 | print() 633 | 634 | 635 | # Turn off Line Sync if it was off originally 636 | if (not origLineSync): 637 | # Restore Line Sync 638 | nanovm.setLineSync(False) 639 | 640 | nanovm.beeperOn() 641 | 642 | # See if any errors in queue and print them if there are 643 | print('\nQuerying and printing out any SCPI errors in error queue of instrument:') 644 | nanovm.printAllErrors() 645 | print() 646 | 647 | ## return to LOCAL mode 648 | nanovm.setLocal() 649 | 650 | nanovm.close() 651 | -------------------------------------------------------------------------------- /dcps/Keithley2400.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | 5 | # Copyright (c) 2021, Stephen Goadhouse 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | #----------------------------------------------------------------------------- 26 | # Control a Keithley/Tektronix 2400, 2401, 2420, 2440, 2410 SourceMeter 27 | #----------------------------------------------------------------------------- 28 | 29 | # For future Python3 compatibility: 30 | from __future__ import absolute_import 31 | from __future__ import division 32 | from __future__ import print_function 33 | 34 | try: 35 | from . import SCPI 36 | except: 37 | from SCPI import SCPI 38 | 39 | import re 40 | from time import sleep 41 | import pyvisa as visa 42 | 43 | class Keithley2400(SCPI): 44 | """Basic class for controlling and accessing a Keithley/Tektronix 2400 45 | SourceMeter. This also supports 2400 variations list 2401, 46 | 2420, 2440 and 2410. Although this is not a traditional DC 47 | power supply, it uses many of the same interface commands so 48 | including here seems logical. 49 | 50 | If the VISA resource string is of the form TCPIP[n]::*::23::SOCKET, 51 | it is assumed that the power supply is being accessed using a 52 | KISS-488 Ethernet to GPIB adapter 53 | (https://www.ebay.com/itm/114514724752) that is properly 54 | configured to access the power supply at its GPIB address 55 | (default is 7). 56 | 57 | If the VISA resource string is of the form TCPIP[n]::*::1234::SOCKET, 58 | it is assumed that the power supply is being accessed using a 59 | Prologix Ethernet to GPIB adapter 60 | (http://prologix.biz/gpib-ethernet-controller.html). The 61 | Prologix has meta-commands to set GPIB address and such. 62 | 63 | It should be possible to use this directly over GPIB or with a 64 | USB to GPIB interface by modifying the resource string but some 65 | minor code edits may be needed. For now, this code has only 66 | been tested with a KISS-488 or Prologix Ethernet to GPIB interface. 67 | 68 | Currently, could not get the KISS-488 interface to fully 69 | support the Keithley 2400 although it works with other devices. 70 | The Prologix interface worked great with the 2400. 71 | 72 | """ 73 | 74 | ## Dictionary to translate SCPI commands for this device 75 | _xlateCmdTbl = { 76 | #@@@'chanSelect': 'SENS:CHAN {}', 77 | } 78 | 79 | def __init__(self, resource, gaddr=24, wait=0.25, verbosity=0, query_delay=0.8, **kwargs): 80 | """Init the class with the instruments resource string 81 | 82 | resource - resource string or VISA descriptor, like TCPIP0::172.16.2.13::23::SOCKET 83 | gaddr - GPIB bus address of instrument - this is only useful if using Prologix interface 84 | wait - float that gives the default number of seconds to wait after sending each command 85 | verbosity - verbosity output - set to 0 for no debug output 86 | kwargs - other named options to pass when PyVISA open() like open_timeout=2.0 87 | """ 88 | 89 | # Set defaults 90 | self._enetgpib = False # True if an Ethernet to GPIB interface is being used 91 | self._kiss488 = False # True if the Ethernet to GPIB interface is a KISS-488 92 | self._prologix = False # True if the Ethernet to GPIB interface is a Prologix 93 | 94 | ## regexp for resource string that indicates it is being used with KISS-488 or Prologix 95 | reskiss488 = re.compile("^TCPIP[0-9]*::.*::23::SOCKET$") 96 | resprologix = re.compile("^TCPIP[0-9]*::.*::1234::SOCKET$") 97 | if (reskiss488.match(resource)): 98 | self._enetgpib = True 99 | self._kiss488 = True 100 | elif (resprologix.match(resource)): 101 | self._enetgpib = True 102 | self._prologix = True 103 | 104 | # save some parameters in case need it 105 | self._gaddr = gaddr 106 | self._query_delay = query_delay 107 | 108 | super(Keithley2400, self).__init__(resource, max_chan=1, wait=wait, cmd_prefix=':', 109 | verbosity = verbosity, 110 | read_termination = '\n', write_termination = '\n', 111 | timeout=2, # found that needed longer timeout 112 | query_delay=query_delay, # for open_resource() 113 | **kwargs) 114 | 115 | # NaN for this instrument 116 | self.NaN = +9.91E37 117 | 118 | 119 | def open(self): 120 | """ Overloaded open() so can handle GPIB interfaces after opening the connection """ 121 | 122 | super(Keithley2400, self).open() 123 | 124 | if (self._kiss488): 125 | # Give the instrument time to output whatever initial output it may send 126 | sleep(1.5) 127 | 128 | ## Can clear strings instead of reading and printing them out 129 | #@@@#self._inst.clear() 130 | 131 | # Read out any strings that are sent after connecting (happens 132 | # for KISS-488 and may happen with other interfaces) 133 | try: 134 | while True: 135 | bytes = self._inst.read_raw() 136 | if (self._kiss488): 137 | # If the expected header from KISS-488, print it out, otherwise ignore. 138 | if ('KISS-488'.encode() in bytes): 139 | print(bytes.decode('utf-8').strip()) 140 | except visa.errors.VisaIOError as err: 141 | if (err.error_code != visa.constants.StatusCode.error_timeout): 142 | # Ignore timeouts here since just reading strings until they stop. 143 | # Output any other errors 144 | print("ERROR: {}, {}".format(err, type(err))) 145 | 146 | elif (self._prologix): 147 | # Configure mode, addr, auto and print out ver 148 | self._instWrite('++mode 1') # make sure in CONTROLLER mode 149 | self._instWrite('++auto 0') # will explicitly tell when to read instrument 150 | self._instWrite('++addr {}'.format(self._gaddr)) # set GPIB address 151 | self._instWrite('++eos 2') # append '\n' / LF to instrument commands 152 | self._instWrite('++eoi 1') # enable EOI assertion with commands 153 | self._instWrite('++read_tmo_ms 800') # Set the Read Timeout to 800 ms 154 | #@@@#self._instWrite('++eot_char 10') # @@@ 155 | self._instWrite('++eot_enable 0') # Do NOT append character when EOI detected 156 | 157 | # Read and print out Version string. Using write/read to 158 | # avoid having '++read' appended if use Query. It is not 159 | # needed for ++ commands and causes a warning if used. 160 | self._instWrite('++ver') 161 | sleep(self._query_delay) 162 | print(self._inst.read()) 163 | 164 | #@@@#self.printAllErrors() 165 | #@@@#self.cls() 166 | 167 | 168 | def _instQuery(self, queryStr): 169 | """ Overload _instQuery from SCPI.py so can append the \r if KISS-488 or add ++read if Prologix""" 170 | # Need to also strip out any leading or trailing white space from the response 171 | 172 | # KISS-488 requires queries to end in '\r' so it knows a response is expected 173 | if (self._kiss488): 174 | queryStr += '\r' 175 | elif (self._prologix): 176 | queryStr += self._write_termination + '++read eoi' 177 | 178 | if self._verbosity >= 4: 179 | print("OUT/" + ":".join("{:02x}".format(ord(c)) for c in queryStr)) 180 | resp = super(Keithley2400, self)._instQuery(queryStr).strip() 181 | if self._verbosity >= 4: 182 | print("IN /" + ":".join("{:02x}".format(ord(c)) for c in resp)) 183 | print(resp) 184 | 185 | return resp 186 | 187 | def setLocal(self): 188 | """If KISS-488, disable the system local command for the instrument 189 | If Prologix, issue GPIB command to unlock the front panel 190 | """ 191 | 192 | if (self._kiss488): 193 | # NOTE: Unsupported command if using KISS-488 with this power 194 | # supply. However, instead of raising an exception and 195 | # breaking any scripts, simply return quietly. 196 | pass 197 | elif (self._prologix): 198 | self._instWrite('++loc') # issue GPIB command to enable front panel 199 | 200 | 201 | def setRemote(self): 202 | """If KISS-488, disable the system remote command for the instrument 203 | If Prologix, issue GPIB command to lock the front panel 204 | """ 205 | 206 | if (self._kiss488): 207 | # NOTE: Unsupported command if using KISS-488 with this power supply. However, 208 | # instead of raising an exception and breaking any scripts, 209 | # simply return quietly. 210 | pass 211 | elif (self._prologix): 212 | self._instWrite('++llo') # issue GPIB command to disable front panel 213 | 214 | def setRemoteLock(self): 215 | """Disable the system remote lock command for the instrument""" 216 | # NOTE: Unsupported command by this power supply. However, 217 | # instead of raising an exception and breaking any scripts, 218 | # simply return quietly. 219 | pass 220 | 221 | def measureVoltage(self, channel=None): 222 | """Read and return a voltage measurement from channel 223 | 224 | channel - number of the channel starting at 1 225 | """ 226 | 227 | # If this function is used, assume non-concurrent measurements 228 | self.setMeasureFunction(concurrent=False,voltage=True,channel=channel) 229 | 230 | # vals is a list of the return string [0] is voltage, [1] is current, [2] is resistance, [3] is timestamp, [4] is status 231 | vals = self._instQuery('READ?').split(',') 232 | return float(vals[0]) 233 | 234 | def measureCurrent(self, channel=None): 235 | """Read and return a current measurement from channel 236 | 237 | channel - number of the channel starting at 1 238 | """ 239 | 240 | # If this function is used, assume non-concurrent measurements 241 | self.setMeasureFunction(concurrent=False,current=True,channel=channel) 242 | 243 | # vals is a list of the return string [0] is voltage, [1] is current, [2] is resistance, [3] is timestamp, [4] is status 244 | vals = self._instQuery('READ?').split(',') 245 | return float(vals[1]) 246 | 247 | def voltageProtectionOn(self, channel=None, wait=None): 248 | """The 2400 has no way to enable/disable voltage protection. Ignore command. 249 | 250 | channel - number of the channel starting at 1 251 | wait - number of seconds to wait after sending command 252 | """ 253 | # If a channel number is passed in, make it the 254 | # current channel 255 | if channel is not None: 256 | self.channel = channel 257 | 258 | if (self._max_chan > 1 and channel is not None): 259 | # If multi-channel device and channel parameter is passed, select it 260 | self._instWrite(self._Cmd('chanSelect').format(self.channel)) 261 | 262 | pass 263 | 264 | def voltageProtectionOff(self, channel=None, wait=None): 265 | """The 2400 has no way to enable/disable voltage protection. Ignore command. 266 | 267 | channel - number of the channel starting at 1 268 | wait - number of seconds to wait after sending command 269 | """ 270 | # If a channel number is passed in, make it the 271 | # current channel 272 | if channel is not None: 273 | self.channel = channel 274 | 275 | if (self._max_chan > 1 and channel is not None): 276 | # If multi-channel device and channel parameter is passed, select it 277 | self._instWrite(self._Cmd('chanSelect').format(self.channel)) 278 | 279 | pass 280 | 281 | def voltageProtectionClear(self, channel=None, wait=None): 282 | """The 2400 automatically clears voltage protection trips. Ignore command. 283 | 284 | channel - number of the channel starting at 1 285 | """ 286 | # If a channel number is passed in, make it the 287 | # current channel 288 | if channel is not None: 289 | self.channel = channel 290 | 291 | if (self._max_chan > 1 and channel is not None): 292 | # If multi-channel device and channel parameter is passed, select it 293 | self._instWrite(self._Cmd('chanSelect').format(self.channel)) 294 | 295 | pass 296 | 297 | ################################################################### 298 | # Commands Specific to 2400 299 | ################################################################### 300 | 301 | def displayMessageOn(self, top=True): 302 | """Enable Display Message 303 | 304 | top - True if enabling the Top message, else enable Bottom message 305 | """ 306 | 307 | if (top): 308 | window = 'WIND1:' 309 | else: 310 | window = 'WIND2:' 311 | 312 | self._instWrite('DISP:{}TEXT:STAT ON'.format(window)) 313 | 314 | def displayMessageOff(self, top=True): 315 | """Disable Display Message 316 | 317 | top - True if disabling the Top message, else disable Bottom message 318 | """ 319 | 320 | if (top): 321 | window = 'WIND1:' 322 | else: 323 | window = 'WIND2:' 324 | 325 | self._instWrite('DISP:{}TEXT:STAT OFF'.format(window)) 326 | 327 | 328 | def setDisplayMessage(self, message, top=True): 329 | """Set the Message for Display. Use displayMessageOn() or 330 | displayMessageOff() to enable or disable message, respectively. 331 | 332 | message - message to set 333 | top - True if setting the Top message, else set Bottom message 334 | 335 | """ 336 | 337 | if (top): 338 | # Maximum of 20 characters for top message 339 | if (len(message) > 20): 340 | message = message[:20] 341 | window = 'WIND1:' 342 | else: 343 | # Maximum of 32 characters for bottom message 344 | if (len(message) > 32): 345 | message = message[:32] 346 | window = 'WIND2:' 347 | 348 | self._instWrite('DISP:{}TEXT "{}"'.format(window,message)) 349 | 350 | def setSourceFunction(self, voltage=False, current=False, channel=None, wait=None): 351 | """Set the Source Function for channel - either Voltage or Current 352 | 353 | voltage - set to True to measure voltage, else False 354 | current - set to True to measure current, else False 355 | channel - number of the channel starting at 1 356 | wait - number of seconds to wait after sending command 357 | 358 | NOTE: Error returned if more than one mode (voltage or current) is True. 359 | """ 360 | 361 | # Check that one and only one mode is True 362 | if (not (voltage and not current) and 363 | not (not voltage and current )): 364 | 365 | raise ValueError('setSourceFunction(): one and only one mode can be True.') 366 | 367 | # If a channel number is passed in, make it the 368 | # current channel 369 | if channel is not None: 370 | self.channel = channel 371 | 372 | # If a wait time is NOT passed in, set wait to the 373 | # default time 374 | if wait is None: 375 | wait = self._wait 376 | 377 | str = 'SOUR{}:FUNC:MODE'.format(self.channel) 378 | 379 | if (voltage): 380 | self._instWrite(str+' VOLT') 381 | 382 | if (current): 383 | self._instWrite(str+' CURR') 384 | 385 | 386 | def setMeasureFunction(self, concurrent=False, voltage=False, current=False, resistance=False, channel=None, wait=None): 387 | """Set the Measure Function for channel 388 | 389 | concurrent - set to True for multiple, concurrent measurements; otherwise False 390 | voltage - set to True to measure voltage, else False 391 | current - set to True to measure current, else False 392 | resistance - set to True to measure resistance, else False 393 | channel - number of the channel starting at 1 394 | wait - number of seconds to wait after sending command 395 | 396 | NOTE: Error returned if concurrent is False and more than one mode (voltage, current or resistance) is True. 397 | """ 398 | 399 | # Check that at least 1 mode is True 400 | if (not voltage and not current and not resistance): 401 | raise ValueError('setMeasureFunction(): At least one mode (voltage, current or resistance) must be True.') 402 | 403 | # Check that if current is False, only one mode is True 404 | if (not concurrent and 405 | not (voltage and not current and not resistance) and 406 | not (not voltage and current and not resistance) and 407 | not (not voltage and not current and resistance)): 408 | 409 | raise ValueError('setMeasureFunction(): If concurrent is False, only one mode can be True.') 410 | 411 | # If a channel number is passed in, make it the 412 | # current channel 413 | if channel is not None: 414 | self.channel = channel 415 | 416 | # If a wait time is NOT passed in, set wait to the 417 | # default time 418 | if wait is None: 419 | wait = self._wait 420 | 421 | str = 'SENS{}:FUNC'.format(self.channel) 422 | 423 | if (concurrent): 424 | self._instWrite(str+':CONC ON') 425 | else: 426 | self._instWrite(str+':CONC OFF') 427 | 428 | # The :OFF commands should only execute if concurrent is True 429 | if (voltage): 430 | self._instWrite(str+':ON "VOLT"') 431 | elif (concurrent): 432 | self._instWrite(str+':OFF "VOLT"') 433 | 434 | if (current): 435 | self._instWrite(str+':ON "CURR"') 436 | elif (concurrent): 437 | self._instWrite(str+':OFF "CURR"') 438 | 439 | if (resistance): 440 | self._instWrite(str+':ON "RES"') 441 | elif (concurrent): 442 | self._instWrite(str+':OFF "RES"') 443 | 444 | def measureResistance(self, channel=None): 445 | """Read and return a resistance measurement from channel 446 | 447 | channel - number of the channel starting at 1 448 | """ 449 | 450 | # If this function is used, assume non-concurrent measurements 451 | self.setMeasureFunction(concurrent=False,resistance=True,channel=channel) 452 | 453 | # vals is a list of the return string [0] is voltage, [1] is current, [2] is resistance, [3] is timestamp, [4] is status 454 | vals = self._instQuery('READ?').split(',') 455 | return float(vals[2]) 456 | 457 | def measureVCR(self, channel=None): 458 | """Read and return a voltage, current and resistance measurement from channel 459 | 460 | channel - number of the channel starting at 1 461 | 462 | NOTE: This does not force CONCURRENT measurements but for 463 | best results, before calling this, call 464 | setMeasureFunction(True,True,True,True). 465 | 466 | """ 467 | 468 | # NOTE: DO NOT change MeasureFunction. Allow it to be whatever has been set so far (for speed of execution) 469 | 470 | # valstrs is a list of the return string [0] is voltage, [1] is current, [2] is resistance, [3] is timestamp, [4] is status 471 | valstrs = self._instQuery('READ?').split(',') 472 | # convert to floating point 473 | vals = [float(f) for f in valstrs] 474 | # status is really a binary value, so convert to int 475 | vals[4] = int(vals[4]) 476 | # vals is a list of the return floats [0] is voltage, [1] is current, [2] is resistance, [3] is timestamp, [4] is status 477 | # status is a binary integer - bit definitions from documentation: 478 | # Bit 0 (OFLO) — Set to 1 if measurement was made while in over-range. 479 | # Bit 1 (Filter) — Set to 1 if measurement was made with the filter enabled. 480 | # Bit 2 (Front/Rear) — Set to 1 if FRONT terminals are selected. 481 | # Bit 3 (Compliance) — Set to 1 if in real compliance. 482 | # Bit 4 (OVP) — Set to 1 if the over voltage protection limit was reached. 483 | # Bit 5 (Math) — Set to 1 if math expression (calc1) is enabled. 484 | # Bit 6 (Null) — Set to 1 if Null is enabled. 485 | # Bit 7 (Limits) — Set to 1 if a limit test (calc2) is enabled. 486 | # Bits 8 and 9 (Limit Results) — Provides limit test results (see grading and sorting modes below). 487 | # Bit 10 (Auto-ohms) — Set to 1 if auto-ohms enabled. 488 | # Bit 11 (V-Meas) — Set to 1 if V-Measure is enabled. 489 | # Bit 12 (I-Meas) — Set to 1 if I-Measure is enabled. 490 | # Bit 13 (Ω-Meas) — Set to 1 if Ω-Measure is enabled. 491 | # Bit 14 (V-Sour) — Set to 1 if V-Source used. 492 | # Bit 15 (I-Sour) — Set to 1 if I-Source used. 493 | # Bit 16 (Range Compliance) — Set to 1 if in range compliance. 494 | return vals 495 | 496 | 497 | if __name__ == '__main__': 498 | import argparse 499 | parser = argparse.ArgumentParser(description='Access and control a Keithley/Tektronix 2400 SourceMeter') 500 | parser.add_argument('chan', nargs='?', type=int, help='Channel to access/control (max channel: 1)', default=1) 501 | args = parser.parse_args() 502 | 503 | from time import sleep 504 | from os import environ 505 | import sys 506 | 507 | resource = environ.get('K2400_VISA', 'TCPIP0::192.168.1.20::23::SOCKET') 508 | srcmtr = Keithley2400(resource, gaddr=24, verbosity=1, query_delay=0.8) 509 | srcmtr.open() 510 | 511 | # Reset to power on default 512 | srcmtr.rst() 513 | 514 | print(srcmtr.idn()) 515 | print() 516 | 517 | # See if any errors in queue and print them if there are 518 | print('\nQuerying and printing out any SCPI errors in error queue of instrument:') 519 | srcmtr.printAllErrors() 520 | print() 521 | 522 | srcmtr.beeperOff() 523 | 524 | # Set Voltage Range to AUTO 525 | srcmtr.setVoltageRange(None) 526 | 527 | # Set display messages 528 | srcmtr.setDisplayMessage('Bottom Message', top=False) 529 | srcmtr.setDisplayMessage('Hey Man!', top=True) 530 | 531 | # Enable top one first 532 | srcmtr.displayMessageOn() 533 | sleep(1.0) 534 | srcmtr.displayMessageOn(top=False) 535 | sleep(2.0) 536 | 537 | # Disable bottom one first 538 | srcmtr.displayMessageOff(top=False) 539 | sleep(1.0) 540 | srcmtr.displayMessageOff(top=True) 541 | 542 | # Test unique setMeasureFunction() 543 | # 544 | # Should get ValueError exception since no mode is true 545 | test_params = ([False], [True], 546 | [False, True, True, False], 547 | [False, True, True, True], 548 | [False, False, True, True], 549 | [False, True, False, True]) 550 | for tp in test_params: 551 | try: 552 | srcmtr.setMeasureFunction(*tp) 553 | except ValueError as err: 554 | print('Got ValueError as expected: {}'.format(err)) 555 | else: 556 | print('ERROR! Should have gotten a ValueError but did not. STOP TEST!') 557 | srcmtr.outputOff() 558 | srcmtr.beeperOn() 559 | srcmtr.setLocal() 560 | srcmtr.close() 561 | sys.exit(2) 562 | 563 | # Disable concurrent measurements 564 | srcmtr.setMeasureFunction(concurrent=False, resistance=True) 565 | 566 | if not srcmtr.isOutputOn(args.chan): 567 | srcmtr.outputOn() 568 | 569 | print('Ch. {} Settings: {:6.4f} V {:6.4f} A '. 570 | format(args.chan, srcmtr.queryVoltage(), srcmtr.queryCurrent())) 571 | 572 | voltageSave = srcmtr.queryVoltage() 573 | 574 | print('Voltage: {:6.4e} V\n'.format(srcmtr.measureVoltage())) 575 | sleep(2.0) 576 | 577 | print('{:6.4e}V / {:6.4e}A (limit: {:6.4e}A)\n'.format(srcmtr.measureVoltage(), srcmtr.measureCurrent(), srcmtr.queryCurrent())) 578 | 579 | if (srcmtr.isVoltageComplianceTripped()): 580 | print('Good! Voltage Compliance Tripped as expected.') 581 | else: 582 | print('ERROR! Voltage Compliance should be Tripped!') 583 | srcmtr.outputOff() 584 | srcmtr.beeperOn() 585 | srcmtr.setLocal() 586 | srcmtr.close() 587 | sys.exit(2) 588 | 589 | print("Changing Output Voltage to 2.3V") 590 | srcmtr.setVoltage(2.3) 591 | print('{:6.4e}V / {:6.4e}A (limit: {:6.4e}A)\n'.format(srcmtr.measureVoltage(), srcmtr.measureCurrent(), srcmtr.queryCurrent())) 592 | 593 | print("Set Over-Voltage Protection to 10V") 594 | srcmtr.setVoltageProtection(10) 595 | print('OVP: {:6.4g}V\n'.format(srcmtr.queryVoltageProtection())) 596 | 597 | print("Set Voltage Compliance to 3.6V") 598 | srcmtr.setVoltageCompliance(3.6) 599 | print('Cmpl: {:6.4g}V\n'.format(srcmtr.queryVoltageCompliance())) 600 | 601 | srcmtr.outputOff() 602 | print("Source Voltage") 603 | srcmtr.setSourceFunction(voltage=True) 604 | srcmtr.outputOn() 605 | 606 | print('{:6.4e}V / {:6.4e}A (limit: {:6.4e}V)\n'.format(srcmtr.measureVoltage(), srcmtr.measureCurrent(), srcmtr.queryVoltage())) 607 | 608 | srcmtr.voltageProtectionOff() 609 | 610 | print("Changing Output Voltage to 23.7V - protection cannot be off") 611 | srcmtr.setVoltage(23.7) 612 | 613 | if (srcmtr.isVoltageProtectionTripped()): 614 | print('Good! Voltage Protection Tripped as expected.') 615 | else: 616 | print('ERROR! Voltage Protection should be Tripped!') 617 | srcmtr.outputOff() 618 | srcmtr.beeperOn() 619 | srcmtr.setLocal() 620 | srcmtr.close() 621 | sys.exit(2) 622 | 623 | print('{:6.4f}V / {:6.4f}A (limit: {:6.4f}V)\n'.format(srcmtr.measureVoltage(), srcmtr.measureCurrent(), srcmtr.queryVoltage())) 624 | 625 | ######################################## 626 | 627 | srcmtr.outputOff() 628 | print("Source Current") 629 | srcmtr.setSourceFunction(current=True) 630 | # Set Auto current range 631 | srcmtr.setCurrentRange(None) 632 | # Set Concurrent Measurements 633 | srcmtr.setMeasureFunction(concurrent=True,voltage=True,current=True,resistance=False) 634 | srcmtr.setCurrent(1.34e-3) 635 | srcmtr.outputOn() 636 | 637 | print('{:6.4f}V / {:6.4g}A / {:6.4f}ohms\n'.format(*srcmtr.measureVCR()[0:3])) 638 | 639 | ######################################## 640 | 641 | srcmtr.outputOff() 642 | 643 | srcmtr.beeperOn() 644 | 645 | # See if any errors in queue and print them if there are 646 | print('\nQuerying and printing out any SCPI errors in error queue of instrument:') 647 | srcmtr.printAllErrors() 648 | print() 649 | 650 | ## return to LOCAL mode 651 | srcmtr.setLocal() 652 | 653 | srcmtr.close() 654 | --------------------------------------------------------------------------------