├── MANIFEST.in ├── msox3000 ├── __init__.py ├── SCPI.py └── MSOX3000.py ├── LICENSE ├── setup.py ├── .gitignore ├── from_web.py ├── testbed.py ├── README.md └── oscope.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include oscope.py 3 | -------------------------------------------------------------------------------- /msox3000/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Standard SCPI commands 3 | from msox3000.SCPI import SCPI 4 | 5 | # Support of HP/Agilent/Keysight MSO-X/DSO-X 3000A oscilloscope 6 | from msox3000.MSOX3000 import MSOX3000 7 | 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | #from distutils.core import setup 4 | import setuptools 5 | 6 | with open("README.md", "r", encoding="utf-8") as fh: 7 | long_description = fh.read() 8 | 9 | setuptools.setup(name="msox3000", 10 | version='0.4.0', 11 | description='Control of HP/Agilent/Keysight MSO-X/DSO-X 3000A Oscilloscope through python via PyVisa', 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url='https://github.com/sgoadhouse/msox3000', 15 | author='Stephen Goadhouse', 16 | author_email="sgoadhouse@virginia.edu", 17 | maintainer='Stephen Goadhouse', 18 | maintainer_email="sgoadhouse@virginia.edu", 19 | license='MIT', 20 | keywords=['HP', 'Agilent', 'Keysight', 'MSO3000', 'MSOX3000', 'DSO3000', 'DSOX3000' 'PyVISA', 'VISA', 'SCPI', 'INSTRUMENT'], 21 | classifiers=[ 22 | 'Development Status :: 4 - Beta', 23 | 'Intended Audience :: Developers', 24 | 'Intended Audience :: Education', 25 | 'Intended Audience :: Science/Research', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Programming Language :: Python', 28 | 'Topic :: Scientific/Engineering', 29 | 'Topic :: Scientific/Engineering :: Physics', 30 | 'Topic :: Software Development', 31 | 'Topic :: Software Development :: Libraries', 32 | 'Topic :: Software Development :: Libraries :: Python Modules'], 33 | install_requires=[ 34 | 'pyvisa>=1.11.3', 35 | 'pyvisa-py>=0.5.1', 36 | 'argparse', 37 | 'QuantiPhy>=2.3.0' 38 | ], 39 | python_requires='>=3.6', 40 | packages=setuptools.find_packages(), 41 | include_package_data=True, 42 | zip_safe=False 43 | ) 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MAC stuff 2 | .DS_Store 3 | */.DS_Store 4 | 5 | # Emacs stuff 6 | *~ 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | *$py.class 12 | 13 | # C extensions 14 | *.so 15 | 16 | # Distribution / packaging 17 | .Python 18 | env/ 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # celery beat schedule file 84 | celerybeat-schedule 85 | 86 | # SageMath parsed files 87 | *.sage.py 88 | 89 | # dotenv 90 | .env 91 | 92 | # virtualenv 93 | .venv 94 | venv/ 95 | ENV/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | -------------------------------------------------------------------------------- /from_web.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 | # Since 2.50 firmware update of MSO-X 3000 Oscilloscope, started getting 27 | # Timeouts when accessing oscilloscope via SCPI. So this kludgy approach 28 | # uses the web interface of the oscilloscope to grab the screen image. 29 | # 30 | # It is utilitarian for the moment but it is a working start. 31 | #------------------------------------------------------------------------------- 32 | 33 | 34 | 35 | 36 | import re 37 | import pycurl 38 | from os import environ 39 | 40 | url = "http://192.168.153.80/" 41 | path = "getImage.asp?inv=false" # can invert image with inv=true 42 | 43 | #@@@#pattern = '(.*?)' % path 44 | pattern = '' 45 | 46 | outfilename = environ.get('HOME', '.')+"/Downloads/out.png" 47 | print(outfilename) 48 | 49 | from io import BytesIO 50 | 51 | buffer = BytesIO() 52 | c = pycurl.Curl() 53 | c.setopt(c.URL, url+path) 54 | c.setopt(c.WRITEDATA, buffer) 55 | c.perform() 56 | c.close() 57 | 58 | body = buffer.getvalue() 59 | # Body is a byte string. 60 | # We have to know the encoding in order to print it to a text file 61 | # such as standard output. 62 | print(body.decode('iso-8859-1')) 63 | 64 | if (True): 65 | for filename in re.findall(pattern, body.decode('iso-8859-1')): 66 | print(filename) 67 | fp = open(outfilename, "wb") 68 | curl = pycurl.Curl() 69 | curl.setopt(pycurl.URL, url+filename) 70 | curl.setopt(pycurl.WRITEDATA, fp) 71 | curl.perform() 72 | curl.close() 73 | fp.close() 74 | -------------------------------------------------------------------------------- /testbed.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2021, Stephen Goadhouse 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of the Neotion nor the names of its contributors may 14 | # be used to endorse or promote products derived from this software 15 | # without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | # ARE DISCLAIMED. IN NO EVENT SHALL NEOTION BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 23 | # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 26 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | #------------------------------------------------------------------------------- 29 | # Get a screen capture from Agilent/KeySight MSO3034A scope and save it to a file 30 | # 31 | # Using new MSOX3000 Class 32 | 33 | # pyvisa 1.6 (or higher) (http://pyvisa.sourceforge.net/) 34 | # pyvisa-py 0.2 (https://pyvisa-py.readthedocs.io/en/latest/) 35 | # 36 | # NOTE: pyvisa-py replaces the need to install NI VISA libraries 37 | # (which are crappily written and buggy!) Wohoo! 38 | # 39 | #------------------------------------------------------------------------------- 40 | 41 | # For future Python3 compatibility: 42 | from __future__ import absolute_import 43 | from __future__ import division 44 | from __future__ import print_function 45 | 46 | import os 47 | import random 48 | import sys 49 | from time import sleep 50 | 51 | # Set to the IP address of the oscilloscope 52 | agilent_msox_3034a = os.environ.get('MSOX3000_IP', 'TCPIP0::172.16.2.13::INSTR') 53 | 54 | import argparse 55 | parser = argparse.ArgumentParser(description='Get a screen capture from Agilent/KeySight MSO3034A scope and save it to a file') 56 | parser.add_argument('ofile', nargs=1, help='Output file name') 57 | args = parser.parse_args() 58 | 59 | fn_ext = ".png" 60 | pn = os.environ['HOME'] + "/Downloads" 61 | fn = pn + "/" + args.ofile[0] 62 | 63 | while os.path.isfile(fn + fn_ext): 64 | fn += "-" + random.choice("abcdefghjkmnpqrstuvwxyz") 65 | 66 | fn += fn_ext 67 | 68 | from msox3000 import MSOX3000 69 | 70 | ## Connect to the Power Supply with default wait time of 100ms 71 | scope = MSOX3000(agilent_msox_3034a) 72 | scope.open() 73 | 74 | print(scope.idn()) 75 | 76 | print("Output file: %s" % fn ) 77 | scope.hardcopy(fn) 78 | scope.waveform(fn+"_1.csv", '1') 79 | scope.waveform(fn+"_2.csv", '2') 80 | scope.waveform(fn+"_3.csv", '3') 81 | scope.waveform(fn+"_4.csv", '4') 82 | 83 | chan = '1' 84 | print("Ch.{}: {}V ACRMS".format(chan,scope.measureDVMacrms(chan))) 85 | print("Ch.{}: {}V DC".format(chan,scope.measureDVMdc(chan))) 86 | print("Ch.{}: {}V DCRMS".format(chan,scope.measureDVMdcrms(chan))) 87 | print("Ch.{}: {}Hz FREQ".format(chan,scope.measureDVMfreq(chan))) 88 | 89 | scope.setupSave(fn+".stp") 90 | 91 | scope.setupAutoscale('1') 92 | scope.setupAutoscale('2') 93 | scope.setupAutoscale('3') 94 | 95 | scope.setupLoad(fn+".stp") 96 | 97 | if True: 98 | wait = 0.5 # just so can see if happen 99 | for chan in range(1,5): 100 | scope.outputOn(chan,wait) 101 | 102 | for chanEn in range(1,5): 103 | if (scope.isOutputOn(chanEn)): 104 | print("Channel {} is ON.".format(chanEn)) 105 | else: 106 | print("Channel {} is off.".format(chanEn)) 107 | print() 108 | 109 | for chan in range(1,5): 110 | scope.outputOff(chan,wait) 111 | 112 | for chanEn in range(1,5): 113 | if (scope.isOutputOn(chanEn)): 114 | print("Channel {} is ON.".format(chanEn)) 115 | else: 116 | print("Channel {} is off.".format(chanEn)) 117 | print() 118 | 119 | scope.outputOnAll(wait) 120 | for chanEn in range(1,5): 121 | if (scope.isOutputOn(chanEn)): 122 | print("Channel {} is ON.".format(chanEn)) 123 | else: 124 | print("Channel {} is off.".format(chanEn)) 125 | print() 126 | 127 | scope.outputOffAll(wait) 128 | for chanEn in range(1,5): 129 | if (scope.isOutputOn(chanEn)): 130 | print("Channel {} is ON.".format(chanEn)) 131 | else: 132 | print("Channel {} is off.".format(chanEn)) 133 | print() 134 | 135 | 136 | chan = '3' 137 | if (not scope.isOutputOn(chan)): 138 | scope.outputOn(chan) 139 | print(scope.measureVoltAmplitude('1',install=False)) 140 | print(scope.measureVoltMax('1',install=False)) 141 | print(scope.measureVoltAmplitude('2')) 142 | print(scope.measureVoltMax(install=False)) 143 | 144 | scope.measureStatistics() 145 | 146 | if True: 147 | print(scope.measureBitRate('4')) 148 | print(scope.measureBurstWidth('4')) 149 | print(scope.measureCounterFrequency('4')) 150 | print(scope.measureFrequency('4')) 151 | print(scope.measurePeriod('4')) 152 | print(scope.measurePosDutyCycle('4')) 153 | print(scope.measureNegDutyCycle('4')) 154 | print(scope.measureFallTime('4')) 155 | print(scope.measureFallEdgeCount('4')) 156 | print(scope.measureFallPulseCount('4')) 157 | print(scope.measureNegPulseWidth('4')) 158 | print(scope.measurePosPulseWidth('4')) 159 | print(scope.measureRiseTime('4')) 160 | print(scope.measureRiseEdgeCount('4')) 161 | print(scope.measureRisePulseCount('4')) 162 | print(scope.measureOvershoot('4')) 163 | print(scope.measurePreshoot('4')) 164 | print() 165 | print(scope.measureVoltAmplitude('1')) 166 | print(scope.measureVoltAmplitude('4')) 167 | print(scope.measureVoltTop('1')) 168 | print(scope.measureVoltTop('4')) 169 | print(scope.measureVoltBase('1')) 170 | print(scope.measureVoltBase('4')) 171 | print(scope.measureVoltMax('1')) 172 | print(scope.measureVoltMax('4')) 173 | print(scope.measureVoltAverage('1')) 174 | print(scope.measureVoltAverage('4')) 175 | print(scope.measureVoltMin('1')) 176 | print(scope.measureVoltMin('4')) 177 | print(scope.measureVoltPP('1')) 178 | print(scope.measureVoltPP('4')) 179 | print(scope.measureVoltRMS('1')) 180 | print(scope.measureVoltRMS('4')) 181 | 182 | print('Done') 183 | 184 | scope.close() 185 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # msox3000 2 | Control of HP/Agilent/Keysight MSO-X/DSO-X 3000A Oscilloscope through python via PyVisa 3 | 4 | Using my previous work on dcps as a guide, this is intended to be a 5 | generic package to control various Oscilloscopes. However, it is 6 | expected that very few oscilloscopes share the same commands so start 7 | off as a python Class specifically for the MSO-X/DSO-X 3000A 8 | Oscilloscope. So will start targeted toward that family of 9 | oscilloscope with a common SCPI.py Class. If it proves useful for 10 | other oscilloscopes, then will create a new project but at least this 11 | one would have started with that in mind. 12 | 13 | It may also work on the MSO-X/DSO-X 2000A oscilloscope 14 | but I have not looked into the differences to know for sure. Try it 15 | out and let me know. 16 | 17 | Like dcps, this will use the brilliant PyVISA python package along 18 | with the PyVisa-PY access mode which eliminates the need for the (very 19 | buggy) VISA library to be installed on your computer. 20 | 21 | # Installation 22 | To install the msox3000 package, run the command: 23 | 24 | ``` 25 | python setup.py install 26 | ``` 27 | 28 | Alternatively, can add a path to this package to the environment 29 | variable PYTHONPATH or even add the path to it at the start of your 30 | python script. Use your favorite web search engine to find out more 31 | details. If you follow this route, you will need to also install all 32 | of the dependant packages which are shown below under Requirements. 33 | 34 | Even better, msox3000 is on PyPi. So 35 | you can simply use the following and the required dependancies should 36 | get installed for you: 37 | 38 | ``` 39 | pip install msox3000 40 | ``` 41 | 42 | ## Requirements 43 | * [python](http://www.python.org/) 44 | * pyvisa no longer supports python 2.7+ so neither does this package - use older version of MSOX3000 if need python 2.7+ 45 | * [pyvisa 1.11.3](https://pyvisa.readthedocs.io/en/stable/) 46 | * [pyvisa-py 0.5.1](https://pyvisa-py.readthedocs.io/en/latest/) 47 | * [argparse](https://docs.python.org/3/library/argparse.html) 48 | * [quantiphy 2.3.0](http://quantiphy.readthedocs.io/en/stable/) 49 | 50 | With the use of pyvisa-py, should not have to install the National 51 | Instruments VISA driver. 52 | 53 | ## Features 54 | 55 | This code is not an exhaustive coverage of all available commands and 56 | queries of the oscilloscopes. The features that do exist are mainly 57 | ones that improve productivity like grabbing a screen hardcopy 58 | directly to an image file on a computer with a descriptive name. This 59 | eliminates the need to save to a USB stick with no descriptive name, 60 | keep track of which hardcopy is which and then eventually take the USB 61 | drive to a computer to download and attempt to figure out which 62 | hardcopy is which. Likewise, I have never bothered to use signal 63 | labels because the oscilloscope interface for adding the labels was 64 | primitive and impractical. With this code, can now easily send labels 65 | from the computer which are easy to create and update. 66 | 67 | Currently, this is a list of the features that are supported so far: 68 | 69 | * The only supported channels are the analog channels, '1', '2', etc., as well as 'POD1' for digital 0-7 and 'POD2' for digital 8-15 70 | * Reading of all available single channel measurements 71 | * Reading of all available DVM measurements 72 | * Installing measurements to statistics display 73 | * Reading data from statistics display 74 | * Screen Hardcopy to PNG image file 75 | * Reading actual waveform data to a csv file including for 'POD1' and 'POD2' 76 | * Saving oscilloscope setup to a file 77 | * Loading oscilloscope setup from saved file 78 | * Issuing Autoscale for channel(s) for all analog as well as 'POD1' and 'POD2' 79 | * Screen Annotation 80 | * Channel Labels for only the analog channels 81 | 82 | It is expected that new interfaces will be added over time to control 83 | and automate the oscilloscope. The key features that would be good to 84 | add next are: support for Digital/Math/etc. channels, run/stop 85 | control, trigger setup, horizontal and vertical scale control, zoom 86 | control 87 | 88 | ## Channels 89 | Almost all functions require a target channel. Once a channel is passed into a function, the object will remember it and make it the default for all subsequence function calls that do not supply a channel. The channel value is a string or can also be a list of strings, in the case of setupAutoscale(). Currently, the valid channel values are: 90 | * '1' for analog channel 1 91 | * '2' for analog channel 2 92 | * '3' for analog channel 3 if it exists on the oscilloscope 93 | * '4' for analog channel 4 if it exists on the oscilloscope 94 | * 'POD1' for the grouping of digital channels 0-7 on a MSO model 95 | * 'POD2' for the grouping of digital channels 8-15 on a MSO model 96 | 97 | ## Usage and Examples 98 | The code is a basic class for controlling and accessing the 99 | supported oscilloscopes. 100 | 101 | The examples are written to access the oscilloscope over 102 | ethernet/TCPIP. So the examples need to know the IP address of your 103 | specific oscilloscope. Also, PyVISA can support other access 104 | mechanisms, like USB. So the examples must be edited to use the 105 | resource string or VISA descriptor of your particular 106 | device. Alternatively, you can set an environment variable, MSOX3000\_IP to 107 | the desired resource string before running the code. If not using 108 | ethernet to access your device, search online for the proper resource 109 | string needed to access your device. 110 | 111 | For more detailed examples, see: 112 | 113 | ``` 114 | oscope.py -h 115 | ``` 116 | 117 | A basic example that installs a few measurements to the statistics 118 | display, adds some annotations and signal labels and then saves a 119 | hardcopy to a file. 120 | 121 | ```python 122 | # Lookup environment variable MSOX3000_IP and use it as the resource 123 | # name or use the TCPIP0 string if the environment variable does 124 | # not exist 125 | from msox3000 import MSOX3000 126 | from os import environ 127 | resource = environ.get('MSOX3000_IP', 'TCPIP0::172.16.2.13::INSTR') 128 | 129 | # create your visa instrument 130 | instr = MSOX3000(resource) 131 | instr.open() 132 | 133 | # set to channel 1 134 | # 135 | # NOTE: can pass channel to each method or just set it 136 | # once and it becomes the default for all following calls. If pass the 137 | # channel to a Class method call, it will become the default for 138 | # following method calls. 139 | instr.channel = '1' 140 | 141 | # Enable output of channel, if it is not already enabled 142 | if not instr.isOutputOn(): 143 | instr.outputOn() 144 | 145 | # Install measurements to display in statistics display and also 146 | # return their current values here 147 | print('Ch. {} Settings: {:6.4e} V PW {:6.4e} s\n'. 148 | format(instr.channel, instr.measureVoltAverage(install=True), 149 | instr.measurePosPulseWidth(install=True))) 150 | 151 | # Add an annotation to the screen before hardcopy 152 | instr.annotateColor("CH{}".format(instr.channel)) 153 | instr.annotate('{}\\n{} {}'.format('Example of Annotation','for Channel',instr.channel)) 154 | 155 | # Change label of the channel to "MySig" 156 | instr.channelLabel('MySig') 157 | 158 | # Make sure the statistics display is showing for the hardcopy 159 | instr._instWrite("SYSTem:MENU MEASure") 160 | instr._instWrite("MEASure:STATistics:DISPlay ON") 161 | 162 | ## Save a hardcopy of the screen to file 'outfile.png' 163 | instr.hardcopy('outfile.png') 164 | 165 | # Change label back to the default and turn it off 166 | instr.channelLabel('{}'.format(instr.channel)) 167 | instr.channelLabelOff() 168 | 169 | # Turn off the annotation 170 | instr.annotateOff() 171 | 172 | # turn off the channel 173 | instr.outputOff() 174 | 175 | # return to LOCAL mode 176 | instr.setLocal() 177 | 178 | instr.close() 179 | ``` 180 | 181 | ## Taking it Further 182 | This implements a small subset of available commands. 183 | 184 | For information on what is possible for the HP/Agilent/Keysight MSO-X/DSO-X 185 | 3000A, see the 186 | [Keysight InfiniiVision 187 | 3000 X-Series Oscilloscopes Programming Guide](https://www.keysight.com/upload/cmc_upload/All/3000_series_prog_guide.pdf) 188 | 189 | For what is possible with general instruments that adhere to the 190 | IEEE 488 SCPI specification, like the MSO-X 3000A, see the 191 | [SCPI 1999 Specification](http://www.ivifoundation.org/docs/scpi-99.pdf) 192 | and the 193 | [SCPI Wikipedia](https://en.wikipedia.org/wiki/Standard_Commands_for_Programmable_Instruments) entry. 194 | 195 | ## Contact 196 | Please send bug reports or feedback to Stephen Goadhouse 197 | 198 | -------------------------------------------------------------------------------- /oscope.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2018,2019,2020,2021, Stephen Goadhouse 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without 7 | # modification, are permitted provided that the following conditions are met: 8 | # * Redistributions of source code must retain the above copyright 9 | # notice, this list of conditions and the following disclaimer. 10 | # * Redistributions in binary form must reproduce the above copyright 11 | # notice, this list of conditions and the following disclaimer in the 12 | # documentation and/or other materials provided with the distribution. 13 | # * Neither the name of the Neotion nor the names of its contributors may 14 | # be used to endorse or promote products derived from this software 15 | # without specific prior written permission. 16 | # 17 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | # ARE DISCLAIMED. IN NO EVENT SHALL NEOTION BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, 23 | # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 25 | # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 26 | # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | 28 | #------------------------------------------------------------------------------- 29 | # Handle several remote functions of Agilent/KeySight MSO3034A scope 30 | # 31 | # Using my new MSOX3000 Class 32 | 33 | # pyvisa 1.6 (or higher) (http://pyvisa.sourceforge.net/) 34 | # pyvisa-py 0.2 (https://pyvisa-py.readthedocs.io/en/latest/) 35 | # 36 | # NOTE: pyvisa-py replaces the need to install NI VISA libraries 37 | # (which are crappily written and buggy!) Wohoo! 38 | # 39 | #------------------------------------------------------------------------------- 40 | 41 | # For future Python3 compatibility: 42 | from __future__ import absolute_import 43 | from __future__ import division 44 | from __future__ import print_function 45 | 46 | import os 47 | import random 48 | import sys 49 | import argparse 50 | 51 | from datetime import datetime 52 | from msox3000 import MSOX3000 53 | 54 | def handleFilename(fname, ext, unique=True, timestamp=True): 55 | 56 | # If extension exists in fname, strip it and add it back later 57 | # after handle versioning 58 | ext = '.' + ext # don't pass in extension with leading '.' 59 | if (fname.endswith(ext)): 60 | fname = fname[:-len(ext)] 61 | 62 | # Make sure filename has no path components, nor ends in a '/' 63 | if (fname.endswith('/')): 64 | fname = fname[:-1] 65 | 66 | pn = fname.split('/') 67 | fname = pn[-1] 68 | 69 | # Assemble full pathname so files go to ~/Downloads if (len(pp) > 1): 70 | pn = os.environ['HOME'] + "/Downloads" 71 | fn = pn + "/" + fname 72 | 73 | if (timestamp): 74 | # add timestamp suffix 75 | fn = fn + '-' + datetime.now().strftime("%Y%0m%0d-%0H%0M%0S") 76 | 77 | suffix = '' 78 | if (unique): 79 | # If given filename exists, try to find a unique one 80 | num = 0 81 | while(os.path.isfile(fn + suffix + ext)): 82 | num += 1 83 | suffix = "-{}".format(num) 84 | 85 | fn += suffix + ext 86 | 87 | return fn 88 | 89 | 90 | def main(): 91 | 92 | # Set to the IP address of the oscilloscope 93 | agilent_msox_3034a = os.environ.get('MSOX3000_IP', 'TCPIP0::172.16.2.13::INSTR') 94 | 95 | ## Connect to the Oscilloscope 96 | scope = MSOX3000(agilent_msox_3034a) 97 | scope.open() 98 | 99 | print(scope.idn()) 100 | 101 | if (args.dvm): 102 | for lst in args.dvm: 103 | try: 104 | chan = lst[0] 105 | acrms = scope.measureDVMacrms(chan) 106 | dc = scope.measureDVMdc(chan) 107 | dcrms = scope.measureDVMdcrms(chan) 108 | freq = scope.measureDVMfreq(chan) 109 | 110 | if (acrms >= MSOX3000.OverRange): 111 | acrms = 'INVALID ' 112 | if (dc >= MSOX3000.OverRange): 113 | dc = 'INVALID ' 114 | if (dcrms >= MSOX3000.OverRange): 115 | dcrms = 'INVALID ' 116 | if (freq >= MSOX3000.OverRange): 117 | freq = 'INVALID ' 118 | 119 | print("Ch.{}: {: 7.5f}V ACRMS".format(chan,acrms)) 120 | print("Ch.{}: {: 7.5f}V DC".format(chan,dc)) 121 | print("Ch.{}: {: 7.5f}V DCRMS".format(chan,dcrms)) 122 | print("Ch.{}: {}Hz FREQ".format(chan,freq)) 123 | 124 | except ValueError as exp: 125 | print(exp) 126 | 127 | if (args.statistics): 128 | stats = scope.measureStatistics() 129 | 130 | print('\nNOTE: If returned value is >= {}, then it is to be considered INVALID\n'.format(MSOX3000.OverRange)) 131 | print('{: ^24} {: ^12} {: ^12} {: ^12} {: ^12} {: ^12} {: ^12}'.format('Measure', 'Current', 'Mean', 'Min', 'Max', 'Std Dev', 'Count')) 132 | for stat in stats: 133 | measure = stat['label'].split('(')[0] # pull out the measurement name from the label (which has a '(channel)' suffix) 134 | print('{: <24} {:>12.6} {:>12.6} {:>12.6} {:>12.6} {:>12.6} {:>12.1}'.format( 135 | stat['label'], 136 | scope.polish(stat['CURR'],measure), 137 | scope.polish(stat['MIN'],measure), 138 | scope.polish(stat['MAX'],measure), 139 | scope.polish(stat['MEAN'],measure), 140 | scope.polish(stat['STDD'],measure), 141 | scope.polish(stat['COUN']) # no units needed here 142 | )) 143 | print() 144 | 145 | if (args.measure): 146 | for lst in args.measure: 147 | try: 148 | chan = lst[0] 149 | 150 | print('\nNOTE: If returned value is >= {}, then it is to be considered INVALID'.format(MSOX3000.OverRange)) 151 | print('\nMeasurements for Ch. {}:'.format(chan)) 152 | measurements = ['Bit Rate', 153 | 'Burst Width', 154 | 'Counter Freq', 155 | 'Frequency', 156 | 'Period', 157 | 'Duty', 158 | 'Neg Duty', 159 | '+ Width', 160 | '- Width', 161 | 'Rise Time', 162 | 'Num Rising', 163 | 'Num Pos Pulses', 164 | 'Fall Time', 165 | 'Num Falling', 166 | 'Num Neg Pulses', 167 | 'Overshoot', 168 | 'Preshoot', 169 | '', 170 | 'Amplitude', 171 | 'Pk-Pk', 172 | 'Top', 173 | 'Base', 174 | 'Maximum', 175 | 'Minimum', 176 | 'Average - Full Screen', 177 | 'RMS - Full Screen', 178 | ] 179 | for meas in measurements: 180 | if (meas is ''): 181 | # use a blank string to put in an extra line 182 | print() 183 | else: 184 | # using MSOX3000.measureTbl[] dictionary, call the 185 | # appropriate method to read the 186 | # measurement. Also, using the same measurement 187 | # name, pass it to the polish() method to format 188 | # the data with units and SI suffix. 189 | print('{: <24} {:>12.6}'.format(meas,scope.polish(MSOX3000.measureTbl[meas][1](scope, chan), meas))) 190 | 191 | except ValueError as exp: 192 | print(exp) 193 | 194 | if (args.annotate): 195 | text = args.annotate 196 | 197 | # If only whitespace is passed in, then turn off the 198 | # annotation. Doing it this way allows leading and trailing 199 | # whitespace in actual annotation if there are non-whitespace 200 | # characters as well. 201 | if (not text.strip()): 202 | scope.annotateOff() 203 | else: 204 | # TRAN = transparent background - can also be OPAQue or INVerted 205 | scope.annotate(text, background='TRAN') 206 | 207 | if (args.annocolor): 208 | # If the annocolor option is given, simply change the color, 209 | # even if not even enabled yet 210 | scope.annotateColor(args.annocolor[0]) 211 | 212 | if (args.label): 213 | # step through all label options 214 | for nxt in args.label: 215 | try: 216 | scope.channelLabel(nxt[1], channel=nxt[0]) 217 | except ValueError as exp: 218 | print(exp) 219 | 220 | if (args.hardcopy): 221 | fn = handleFilename(args.hardcopy, 'png') 222 | 223 | scope.hardcopy(fn) 224 | print("Hardcopy Output file: {}".format(fn) ) 225 | 226 | if (args.waveform): 227 | for nxt in args.waveform: 228 | try: 229 | # check the channel 230 | channel = nxt[0] 231 | if (channel in MSOX3000.chanAllValidList): 232 | fn = handleFilename(nxt[1], 'csv') 233 | dataLen = scope.waveform(fn, channel) 234 | print("Waveform Output of Channel {} in {} points to file {}".format(channel,dataLen,fn)) 235 | else: 236 | print('INVALID Channel Value: {} SKIPPING!'.format(channel)) 237 | except ValueError as exp: 238 | print(exp) 239 | 240 | if (args.setup_save): 241 | fn = handleFilename(args.setup_save, 'stp') 242 | 243 | dataLen = scope.setupSave(fn) 244 | print("Oscilloscope Setup bytes saved: {} to '{}'".format(dataLen,fn) ) 245 | 246 | if (args.setup_load): 247 | fn = handleFilename(args.setup_load, 'stp', unique=False, timestamp=False) 248 | 249 | if(not os.path.isfile(fn)): 250 | print('INVALID filename "{}" - must be exact and exist!'.format(fn)) 251 | else: 252 | dataLen = scope.setupLoad(fn) 253 | print("Oscilloscope Setup bytes loaded: {} from '{}'".format(dataLen,fn) ) 254 | 255 | if (args.autoscale): 256 | try: 257 | scope.setupAutoscale([x[0] for x in args.autoscale]) 258 | except ValueError as exp: 259 | print(exp) 260 | 261 | # a simple test of enabling/disabling the channels 262 | if False: 263 | wait = 0.5 # just so can see if happen 264 | for chan in range(1,5): 265 | scope.outputOn(chan,wait) 266 | 267 | for chanEn in range(1,5): 268 | if (scope.isOutputOn(chanEn)): 269 | print("Channel {} is ON.".format(chanEn)) 270 | else: 271 | print("Channel {} is off.".format(chanEn)) 272 | print() 273 | 274 | for chan in range(1,5): 275 | scope.outputOff(chan,wait) 276 | 277 | for chanEn in range(1,5): 278 | if (scope.isOutputOn(chanEn)): 279 | print("Channel {} is ON.".format(chanEn)) 280 | else: 281 | print("Channel {} is off.".format(chanEn)) 282 | print() 283 | 284 | scope.outputOnAll(wait) 285 | for chanEn in range(1,5): 286 | if (scope.isOutputOn(chanEn)): 287 | print("Channel {} is ON.".format(chanEn)) 288 | else: 289 | print("Channel {} is off.".format(chanEn)) 290 | print() 291 | 292 | scope.outputOffAll(wait) 293 | for chanEn in range(1,5): 294 | if (scope.isOutputOn(chanEn)): 295 | print("Channel {} is ON.".format(chanEn)) 296 | else: 297 | print("Channel {} is off.".format(chanEn)) 298 | print() 299 | 300 | 301 | 302 | print('Done') 303 | scope.close() 304 | 305 | 306 | if __name__ == '__main__': 307 | parser = argparse.ArgumentParser(description='Access Agilent/KeySight MSO3034A scope') 308 | parser.add_argument('--hardcopy', '-y', metavar='outfile.png', help='grab hardcopy of scope screen and output to named file as a PNG image') 309 | parser.add_argument('--waveform', '-w', nargs=2, metavar=('channel', 'outfile.csv'), action='append', 310 | help='grab waveform data of channel ('+ str(MSOX3000.chanAllValidList).strip('[]') + ') and output to named file as a CSV file') 311 | parser.add_argument('--setup_save', '-s', metavar='outfile.stp', help='save the current setup of the oscilloscope into the named file') 312 | parser.add_argument('--setup_load', '-l', metavar='infile.stp', help='load the current setup of the oscilloscope from the named file') 313 | parser.add_argument('--statistics', '-t', action='store_true', help='dump to the output the current displayed measurements') 314 | parser.add_argument('--autoscale', '-u', nargs=1, action='append', choices=MSOX3000.chanAllValidList, 315 | help='cause selected channel to autoscale') 316 | parser.add_argument('--dvm', '-d', nargs=1, action='append', choices=MSOX3000.chanAnaValidList, 317 | help='measure and output the DVM readings of selected channel') 318 | parser.add_argument('--measure', '-m', nargs=1, action='append', choices=MSOX3000.chanAnaValidList, 319 | help='measure and output the selected channel') 320 | parser.add_argument('--annotate', '-a', nargs='?', metavar='text', const=' ', help='Add annotation text to screen. Clear text if label is blank') 321 | parser.add_argument('--annocolor', '-c', nargs=1, metavar='color', 322 | choices=['ch1', 'ch2', 'ch3', 'ch4', 'dig', 'math', 'ref', 'marker', 'white', 'red'], 323 | help='Set the annotation color to use. Valid values: %(choices)s') 324 | parser.add_argument('--label', '-b', nargs=2, action='append', metavar=('channel', 'label'), 325 | help='Change label of selected channel (' + str(MSOX3000.chanAnaValidList).strip('[]') + ')') 326 | 327 | # Print help if no options are given on the command line 328 | if (len(sys.argv) <= 1): 329 | parser.print_help(sys.stderr) 330 | sys.exit(1) 331 | 332 | args = parser.parse_args() 333 | 334 | main() 335 | -------------------------------------------------------------------------------- /msox3000/SCPI.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | 5 | # Copyright (c) 2018,2019,2020,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 of HP/Agilent/Keysight MSO-X/DSO-X 3000A Oscilloscope using 27 | # standard SCPI commands with PyVISA 28 | # 29 | # For more information on SCPI, see: 30 | # https://en.wikipedia.org/wiki/Standard_Commands_for_Programmable_Instruments 31 | # http://www.ivifoundation.org/docs/scpi-99.pdf 32 | #------------------------------------------------------------------------------- 33 | 34 | # For future Python3 compatibility: 35 | from __future__ import absolute_import 36 | from __future__ import division 37 | from __future__ import print_function 38 | 39 | from time import sleep 40 | from sys import version_info 41 | from sys import exit 42 | import pyvisa as visa 43 | 44 | class SCPI(object): 45 | """Basic class for controlling and accessing an Oscilloscope with Standard SCPI Commands""" 46 | 47 | OverRange = +9.9E+37 # Number which indicates Over Range 48 | UnderRange = -9.9E+37 # Number which indicates Under Range 49 | ErrorQueue = 30 # Size of error queue 50 | 51 | def __init__(self, resource, max_chan=1, wait=0, 52 | cmd_prefix = '', 53 | read_strip = '', 54 | read_termination = '', 55 | write_termination = '', 56 | timeout = 15000): 57 | """Init the class with the instruments resource string 58 | 59 | resource - resource string or VISA descriptor, like TCPIP0::172.16.2.13::INSTR 60 | max_chan - number of channels 61 | wait - float that gives the default number of seconds to wait after sending each command 62 | cmd_prefix - optional command prefix (ie. some instruments require a ':' prefix) 63 | read_strip - optional read_strip parameter used to strip any returned termination characters 64 | read_termination - optional read_termination parameter to pass to open_resource() 65 | write_termination - optional write_termination parameter to pass to open_resource() 66 | """ 67 | self._resource = resource 68 | self._max_chan = max_chan # number of channels 69 | self._wait = wait 70 | self._prefix = cmd_prefix 71 | self._curr_chan = 1 # set the current channel to the first one 72 | self._read_strip = read_strip 73 | self._read_termination = read_termination 74 | self._write_termination = write_termination 75 | self._timeout = timeout 76 | self._version = 0.0 # set software versino to lowest value until it gets set 77 | self._inst = None 78 | 79 | def open(self): 80 | """Open a connection to the VISA device with PYVISA-py python library""" 81 | self._rm = visa.ResourceManager('@py') 82 | self._inst = self._rm.open_resource(self._resource, 83 | read_termination=self._read_termination, 84 | write_termination=self._write_termination) 85 | self._inst.timeout = self._timeout 86 | 87 | # Keysight recommends using clear() 88 | # 89 | # NOTE: must use pyvisa-py >= 0.5.0 to get this implementation 90 | self._inst.clear() 91 | 92 | # Read software version number so can deviate operation based 93 | # on changes to commands over history (WHY did they make changes?) 94 | # MUST be done before below clear() which sends first command. 95 | self._getVersion() 96 | 97 | # Also, send a *CLS system command to clear the command 98 | # handler (error queues and such) 99 | self.clear() 100 | 101 | 102 | def close(self): 103 | """Close the VISA connection""" 104 | self._inst.close() 105 | 106 | @property 107 | def channel(self): 108 | return self._curr_chan 109 | 110 | @channel.setter 111 | def channel(self, value): 112 | self._curr_chan = value 113 | 114 | def _instQuery(self, queryStr, checkErrors=True): 115 | if (queryStr[0] != '*'): 116 | queryStr = self._prefix + queryStr 117 | #print("QUERY:",queryStr) 118 | try: 119 | result = self._inst.query(queryStr) 120 | except visa.VisaIOError as err: 121 | # Got VISA exception so read and report any errors 122 | self.checkInstErrors(queryStr) 123 | print("Exited because of VISA IO Error: {}".format(err)) 124 | exit(1) 125 | 126 | if checkErrors: 127 | self.checkInstErrors(queryStr) 128 | return result.rstrip(self._read_strip) 129 | 130 | def _instQueryNumber(self, queryStr, checkErrors=True): 131 | return float(self._instQuery(queryStr, checkErrors)) 132 | 133 | def _instWrite(self, writeStr, checkErrors=True): 134 | if (writeStr[0] != '*'): 135 | writeStr = self._prefix + writeStr 136 | #print("WRITE:",writeStr) 137 | try: 138 | result = self._inst.write(writeStr) 139 | except visa.VisaIOError as err: 140 | # Got VISA exception so read and report any errors 141 | self.checkInstErrors(writeStr) 142 | print("Exited because of VISA IO Error: {}".format(err)) 143 | exit(1) 144 | 145 | if checkErrors: 146 | self.checkInstErrors(writeStr) 147 | return result 148 | 149 | def _chStr(self, channel): 150 | """return the channel string given the channel number and using the format CHx""" 151 | 152 | return 'CH{}'.format(channel) 153 | 154 | def _chanStr(self, channel): 155 | """return the channel string given the channel number and using the format x""" 156 | 157 | return '{}'.format(channel) 158 | 159 | def _channelStr(self, channel): 160 | """return the channel string given the channel number and using the format CHANnelx if x is numeric""" 161 | 162 | try: 163 | return 'CHANnel{}'.format(int(channel)) 164 | except ValueError: 165 | return self._chanStr(channel) 166 | 167 | def _onORoff(self, str): 168 | """Check if string says it is ON or OFF and return True if ON 169 | and False if OFF 170 | """ 171 | 172 | # Only check first two characters so do not need to deal with 173 | # trailing whitespace and such 174 | if str[:2] == 'ON': 175 | return True 176 | else: 177 | return False 178 | 179 | def _1OR0(self, str): 180 | """Check if string says it is 1 or 0 and return True if 1 181 | and False if 0 182 | """ 183 | 184 | # Only check first character so do not need to deal with 185 | # trailing whitespace and such 186 | if str[:1] == '1': 187 | return True 188 | else: 189 | return False 190 | 191 | def _chanNumber(self, str): 192 | """Decode the response as a channel number and return it. Return 0 if string does not decode properly. 193 | """ 194 | 195 | # Only check first character so do not need to deal with 196 | # trailing whitespace and such 197 | if str[:4] == 'CHAN': 198 | return int(str[4]) 199 | else: 200 | return 0 201 | 202 | def _wait(self): 203 | """Wait until all preceeding commands complete""" 204 | #self._instWrite('*WAI') 205 | self._instWrite('*OPC') 206 | wait = True 207 | while(wait): 208 | ret = self._instQuery('*OPC?') 209 | if ret[0] == '1': 210 | wait = False 211 | 212 | # ========================================================= 213 | # Taken from the MSO-X 3000 Programming Guide and modified to work 214 | # within this class ... 215 | # 216 | # UPDATE: Apparently "SYSTem:ERRor?" has changed but the 217 | # documentation is unclear so will make it work as it works on 218 | # MXR058A with v11.10 219 | # ========================================================= 220 | # Check for instrument errors: 221 | # ========================================================= 222 | def checkInstErrors(self, commandStr): 223 | 224 | if (self._version > 3.10): 225 | cmd = "SYSTem:ERRor? STR" 226 | noerr = ("0,", 0, 2) 227 | else: 228 | cmd = "SYSTem:ERRor?" 229 | noerr = ("+0,", 0, 3) 230 | 231 | errors = False 232 | # No need to read more times that the size of the Error Queue 233 | for reads in range(0,self.ErrorQueue): 234 | # checkErrors=False prevents infinite recursion! 235 | error_string = self._instQuery(cmd, checkErrors=False) 236 | error_string = error_string.strip() # remove trailing and leading whitespace 237 | if error_string: # If there is an error string value. 238 | if error_string.find(*noerr) == -1: 239 | # Not "No error". 240 | print("ERROR({:02d}): {}, command: '{}'".format(reads, error_string, commandStr)) 241 | #print "Exited because of error." 242 | #sys.exit(1) 243 | errors = True # indicate there was an error 244 | else: # "No error" 245 | break 246 | 247 | else: # :SYSTem:ERRor? should always return string. 248 | print("ERROR: :SYSTem:ERRor? returned nothing, command: '{}'".format(commandStr)) 249 | #print "Exited because of error." 250 | #sys.exit(1) 251 | errors = True # if unexpected response, then set as Error 252 | break 253 | 254 | return errors # indicate if there was an error 255 | 256 | # ========================================================= 257 | # Based on do_query_ieee_block() from the MSO-X 3000 Programming 258 | # Guide and modified to work within this class ... 259 | # ========================================================= 260 | def _instQueryIEEEBlock(self, queryStr): 261 | if (queryStr[0] != '*'): 262 | queryStr = self._prefix + queryStr 263 | #print("QUERYIEEEBlock:",queryStr) 264 | try: 265 | result = self._inst.query_binary_values(queryStr, datatype='s', container=bytes) 266 | except visa.VisaIOError as err: 267 | # Got VISA exception so read and report any errors 268 | self.checkInstErrors(queryStr) 269 | print("Exited because of VISA IO Error: {}".format(err)) 270 | exit(1) 271 | 272 | self.checkInstErrors(queryStr) 273 | return result 274 | 275 | # ========================================================= 276 | # Based on code from the MSO-X 3000 Programming 277 | # Guide and modified to work within this class ... 278 | # ========================================================= 279 | def _instQueryNumbers(self, queryStr): 280 | if (queryStr[0] != '*'): 281 | queryStr = self._prefix + queryStr 282 | #print("QUERYNumbers:",queryStr) 283 | try: 284 | result = self._inst.query_ascii_values(queryStr, converter='f', separator=',') 285 | except visa.VisaIOError as err: 286 | # Got VISA exception so read and report any errors 287 | self.checkInstErrors(queryStr) 288 | print("Exited because of VISA IO Error: {}".format(err)) 289 | exit(1) 290 | 291 | self.checkInstErrors(queryStr) 292 | return result 293 | 294 | # ========================================================= 295 | # Based on do_command_ieee_block() from the MSO-X 3000 Programming 296 | # Guide and modified to work within this class ... 297 | # ========================================================= 298 | def _instWriteIEEEBlock(self, writeStr, values): 299 | if (writeStr[0] != '*'): 300 | writeStr = self._prefix + writeStr 301 | #print("WRITE:",writeStr) 302 | 303 | if (version_info < (3,)): 304 | ## If PYTHON 2, must use datatype of 'c' 305 | datatype = 'c' 306 | else: 307 | ## If PYTHON 2, must use datatype of 'B' to get the same result 308 | datatype = 'B' 309 | 310 | try: 311 | result = self._inst.write_binary_values(writeStr, values, datatype=datatype) 312 | except visa.VisaIOError as err: 313 | # Got VISA exception so read and report any errors 314 | self.checkInstErrors(writeStr) 315 | print("Exited because of VISA IO Error: {}".format(err)) 316 | exit(1) 317 | 318 | self.checkInstErrors(writeStr) 319 | return result 320 | 321 | def _instWriteIEEENumbers(self, writeStr, values): 322 | if (writeStr[0] != '*'): 323 | writeStr = self._prefix + writeStr 324 | #print("WRITE:",writeStr) 325 | 326 | try: 327 | result = self._inst.write_binary_values(writeStr, values, datatype='f') 328 | except visa.VisaIOError as err: 329 | # Got VISA exception so read and report any errors 330 | self.checkInstErrors(writeStr) 331 | print("Exited because of VISA IO Error: {}".format(err)) 332 | exit(1) 333 | 334 | self.checkInstErrors(writeStr) 335 | return result 336 | 337 | def _getVersion(self): 338 | """Query Software Version to handle command history deviations. This is called from open().""" 339 | ## Skip Error check since handling of errors is version specific 340 | idn = self._instQuery('*IDN?', checkErrors=False).split(',') 341 | ver = idn[3].split('.') 342 | # put major and minor version into floating point format so can numerically compare 343 | self._version = float(ver[0]+'.'+ver[1]) 344 | 345 | def idn(self): 346 | """Return response to *IDN? message""" 347 | return self._instQuery('*IDN?') 348 | 349 | def clear(self): 350 | """Sends a *CLS message to clear status and error queues""" 351 | return self._instWrite('*CLS') 352 | 353 | def reset(self): 354 | """Sends a *RST message to reset to defaults""" 355 | return self._instWrite('*RST') 356 | 357 | def setLocal(self): 358 | """Set the power supply to LOCAL mode where front panel keys work again 359 | """ 360 | 361 | # Not sure if this is SCPI, but it appears to be supported 362 | # across different instruments 363 | self._instWrite('SYSTem:LOCK OFF') 364 | 365 | def setRemote(self): 366 | """Set the power supply to REMOTE mode where it is controlled via VISA 367 | """ 368 | 369 | # Not sure if this is SCPI, but it appears to be supported 370 | # across different instruments 371 | self._instWrite('SYSTem:LOCK ON') 372 | 373 | def setRemoteLock(self): 374 | """Set the power supply to REMOTE Lock mode where it is 375 | controlled via VISA & front panel is locked out 376 | """ 377 | 378 | # Not sure if this is SCPI, but it appears to be supported 379 | # across different instruments 380 | self._instWrite('SYSTem:LOCK ON') 381 | 382 | def beeperOn(self): 383 | """Enable the system beeper for the instrument""" 384 | # no beeper to turn off, so make it do nothing 385 | pass 386 | 387 | def beeperOff(self): 388 | """Disable the system beeper for the instrument""" 389 | # no beeper to turn off, so make it do nothing 390 | pass 391 | 392 | def isOutputOn(self, channel=None): 393 | """Return true if the output of channel is ON, else false 394 | 395 | channel - number of the channel starting at 1 396 | """ 397 | 398 | # If a channel number is passed in, make it the 399 | # current channel 400 | if channel is not None: 401 | self.channel = channel 402 | 403 | str = 'STATus? {}'.format(self._channelStr(self.channel)) 404 | ret = self._instQuery(str) 405 | # @@@print("1:", ret) 406 | return self._1OR0(ret) 407 | 408 | def outputOn(self, channel=None, wait=None): 409 | """Turn on the output for channel 410 | 411 | wait - number of seconds to wait after sending command 412 | channel - number of the channel starting at 1 413 | """ 414 | 415 | # If a channel number is passed in, make it the 416 | # current channel 417 | if channel is not None: 418 | self.channel = channel 419 | 420 | # If a wait time is NOT passed in, set wait to the 421 | # default time 422 | if wait is None: 423 | wait = self._wait 424 | 425 | str = 'VIEW {}'.format(self._channelStr(self.channel)) 426 | self._instWrite(str) 427 | sleep(wait) 428 | 429 | def outputOff(self, channel=None, wait=None): 430 | """Turn off the output for channel 431 | 432 | channel - number of the channel starting at 1 433 | """ 434 | 435 | # If a channel number is passed in, make it the 436 | # current channel 437 | if channel is not None: 438 | self.channel = channel 439 | 440 | # If a wait time is NOT passed in, set wait to the 441 | # default time 442 | if wait is None: 443 | wait = self._wait 444 | 445 | str = 'BLANK {}'.format(self._channelStr(self.channel)) 446 | self._instWrite(str) 447 | sleep(wait) 448 | 449 | def outputOnAll(self, wait=None): 450 | """Turn on the output for ALL channels 451 | 452 | """ 453 | 454 | # If a wait time is NOT passed in, set wait to the 455 | # default time 456 | if wait is None: 457 | wait = self._wait 458 | 459 | for chan in range(1,self._max_chan+1): 460 | str = 'VIEW {}'.format(self._channelStr(chan)) 461 | self._instWrite(str) 462 | 463 | sleep(wait) 464 | 465 | def outputOffAll(self, wait=None): 466 | """Turn off the output for ALL channels 467 | 468 | """ 469 | 470 | # If a wait time is NOT passed in, set wait to the 471 | # default time 472 | if wait is None: 473 | wait = self._wait 474 | 475 | #for chan in range(1,self._max_chan+1): 476 | # str = 'BLANK {}'.format(self._channelStr(chan)) 477 | # self._instWrite(str) 478 | 479 | # Blank without a parameter turns off ALL sources 480 | str = 'BLANK' 481 | self._instWrite(str) 482 | 483 | sleep(wait) # give some time for PS to respond 484 | 485 | def measureVoltage(self, channel=None): 486 | """Read and return a voltage measurement from 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 | str = 'INSTrument:NSELect {}; MEASure:VOLTage:DC?'.format(self.channel) 497 | val = self._instQueryNumber(str) 498 | return val 499 | -------------------------------------------------------------------------------- /msox3000/MSOX3000.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | 5 | # Copyright (c) 2018,2019,2020,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 of HP/Agilent/Keysight MSO-X/DSO-X 3000A Oscilloscope 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 Exception: 37 | from SCPI import SCPI 38 | 39 | from time import sleep 40 | from datetime import datetime 41 | from quantiphy import Quantity 42 | from sys import version_info 43 | import pyvisa as visa 44 | 45 | class MSOX3000(SCPI): 46 | """Basic class for controlling and accessing a HP/Agilent/Keysight MSO-X/DSO-X 3000A Oscilloscope""" 47 | 48 | maxChannel = 4 49 | 50 | # Return list of ALL valid channel strings. 51 | # 52 | # NOTE: Currently, only valid values are a numerical string for 53 | # the analog channels, POD1 for digital channels 0-7 or POD2 for 54 | # digital channels 8-15 55 | chanAllValidList = [str(x) for x in range(1,maxChannel+1)]+['POD1','POD2'] 56 | 57 | # Return list of valid analog channel strings. 58 | chanAnaValidList = [str(x) for x in range(1,maxChannel+1)] 59 | 60 | def __init__(self, resource, wait=0): 61 | """Init the class with the instruments resource string 62 | 63 | resource - resource string or VISA descriptor, like TCPIP0::172.16.2.13::INSTR 64 | wait - float that gives the default number of seconds to wait after sending each command 65 | """ 66 | super(MSOX3000, self).__init__(resource, max_chan=MSOX3000.maxChannel, wait=wait, 67 | cmd_prefix=':', 68 | read_strip='\n', 69 | read_termination='', 70 | write_termination='\n' 71 | ) 72 | 73 | # ========================================================= 74 | # Based on the save oscilloscope setup example from the MSO-X 3000 Programming 75 | # Guide and modified to work within this class ... 76 | # ========================================================= 77 | def setupSave(self, filename): 78 | """ Fetch the oscilloscope setup and save to a file with given filename. """ 79 | 80 | oscopeSetup = self._instQueryIEEEBlock("SYSTem:SETup?") 81 | 82 | # Save setup to file. 83 | f = open(filename, "wb") 84 | f.write(oscopeSetup) 85 | f.close() 86 | 87 | #print('Oscilloscope Setup bytes saved: {} to "{}"'.format(len(oscopeSetup),filename)) 88 | 89 | # Return number of bytes saved to file 90 | return len(oscopeSetup) 91 | 92 | # ========================================================= 93 | # Based on the loading a previous setup example from the MSO-X 3000 Programming 94 | # Guide and modified to work within this class ... 95 | # ========================================================= 96 | def setupLoad(self, filename): 97 | """ Restore the oscilloscope setup from file with given filename. """ 98 | 99 | # Load setup from file. 100 | f = open(filename, "rb") 101 | oscopeSetup = f.read() 102 | f.close() 103 | 104 | #print('Oscilloscope Setup bytes loaded: {} from "{}"'.format(len(oscopeSetup),filename)) 105 | 106 | self._instWriteIEEEBlock("SYSTem:SETup ", oscopeSetup) 107 | 108 | # Return number of bytes saved to file 109 | return len(oscopeSetup) 110 | 111 | 112 | def setupAutoscale(self, channel=None): 113 | """ Autoscale desired channel, which is a string. channel can also be a list of multiple strings""" 114 | 115 | # If a channel value is passed in, make it the 116 | # current channel 117 | if channel is not None: 118 | self.channel = channel 119 | 120 | # Make channel a list even if it is a single value 121 | if type(self.channel) is not list: 122 | chanlist = [self.channel] 123 | else: 124 | chanlist = self.channel 125 | 126 | # chanlist cannot have more than 5 elements 127 | if (len(chanlist) > 5): 128 | raise ValueError('Too many channels for AUTOSCALE! Max is 5. Aborting') 129 | 130 | chanstr = '' 131 | for chan in chanlist: 132 | # Check channel value 133 | if (chan not in MSOX3000.chanAllValidList): 134 | raise ValueError('INVALID Channel Value for AUTOSCALE: {} SKIPPING!'.format(chan)) 135 | else: 136 | chanstr += ',' + self._channelStr(chan) 137 | 138 | # remove the leading ',' when creating the command string with '[1:]' 139 | self._instWrite("AUToscale " + chanstr[1:]) 140 | 141 | def annotate(self, text, color=None, background='TRAN'): 142 | """ Add an annotation with text, color and background to screen 143 | 144 | text - text of annotation. Can include \n for newlines (two characters) 145 | 146 | color - string, one of {CH1 | CH2 | CH3 | CH4 | DIG | MATH | REF | MARK | WHIT | RED} 147 | 148 | background - string, one of TRAN - transparent, OPAQue or INVerted 149 | """ 150 | 151 | if (color): 152 | self.annotateColor(color) 153 | 154 | # Add an annotation to the screen 155 | self._instWrite("DISPlay:ANN:BACKground {}".format(background)) # transparent background - can also be OPAQue or INVerted 156 | self._instWrite('DISPlay:ANN:TEXT "{}"'.format(text)) 157 | self._instWrite("DISPlay:ANN ON") 158 | 159 | def annotateColor(self, color): 160 | """ Change screen annotation color """ 161 | 162 | ## NOTE: Only certain values are allowed: 163 | # {CH1 | CH2 | CH3 | CH4 | DIG | MATH | REF | MARK | WHIT | RED} 164 | # 165 | # The scope will respond with an error if an invalid color string is passed along 166 | self._instWrite("DISPlay:ANN:COLor {}".format(color)) 167 | 168 | def annotateOff(self): 169 | """ Turn off screen annotation """ 170 | 171 | self._instWrite("DISPlay:ANN OFF") 172 | 173 | 174 | def channelLabel(self, label, channel=None): 175 | """ Add a label to selected channel (or default one if None) 176 | 177 | label - text of label 178 | """ 179 | 180 | # If a channel value is passed in, make it the 181 | # current channel 182 | if channel is not None and type(channel) is not list: 183 | self.channel = channel 184 | 185 | # Make sure channel is NOT a list 186 | if type(self.channel) is list or type(channel) is list: 187 | raise ValueError('Channel cannot be a list for CHANNEL LABEL!') 188 | 189 | # Check channel value 190 | if (self.channel not in MSOX3000.chanAnaValidList): 191 | raise ValueError('INVALID Channel Value for CHANNEL LABEL: {} SKIPPING!'.format(self.channel)) 192 | 193 | self._instWrite('CHAN{}:LABel "{}"'.format(self.channel, label)) 194 | self._instWrite('DISPlay:LABel ON') 195 | 196 | def channelLabelOff(self): 197 | """ Turn off channel labels """ 198 | 199 | self._instWrite('DISPlay:LABel OFF') 200 | 201 | 202 | def polish(self, value, measure=None): 203 | """ Using the QuantiPhy package, return a value that is in apparopriate Si units. 204 | 205 | If value is >= SCPI.OverRange, then return the invalid string instead of a Quantity(). 206 | 207 | If the measure string is None, then no units are used by the SI suffix is. 208 | 209 | """ 210 | 211 | if (value >= SCPI.OverRange): 212 | pol = '------' 213 | else: 214 | try: 215 | pol = Quantity(value, MSOX3000.measureTbl[measure][0]) 216 | except KeyError: 217 | # If measure is None or does not exist 218 | pol = Quantity(value) 219 | 220 | return pol 221 | 222 | 223 | def measureStatistics(self): 224 | """Returns an array of dictionaries from the current statistics window. 225 | 226 | The definition of the returned dictionary can be easily gleaned 227 | from the code below. 228 | """ 229 | 230 | # turn on the statistics display 231 | self._instWrite("SYSTem:MENU MEASure") 232 | self._instWrite("MEASure:STATistics:DISPlay ON") 233 | 234 | # tell Results? return all values (as opposed to just one of them) 235 | self._instWrite("MEASure:STATistics ON") 236 | 237 | # create a list of the return values, which are seperated by a comma 238 | statFlat = self._instQuery("MEASure:RESults?").split(',') 239 | 240 | # convert the flat list into a two-dimentional matrix with seven columns per row 241 | statMat = [statFlat[i:i+7] for i in range(0,len(statFlat),7)] 242 | 243 | # convert each row into a dictionary, while converting text strings into numbers 244 | stats = [] 245 | for stat in statMat: 246 | stats.append({'label':stat[0], 247 | 'CURR':float(stat[1]), # Current Value 248 | 'MIN':float(stat[2]), # Minimum Value 249 | 'MAX':float(stat[3]), # Maximum Value 250 | 'MEAN':float(stat[4]), # Average/Mean Value 251 | 'STDD':float(stat[5]), # Standard Deviation 252 | 'COUN':int(stat[6]) # Count of measurements 253 | }) 254 | 255 | # return the result in an array of dictionaries 256 | return stats 257 | 258 | def _measure(self, mode, para=None, channel=None, wait=0.25, install=False): 259 | """Read and return a measurement of type mode from channel 260 | 261 | para - parameters to be passed to command 262 | 263 | channel - channel to be measured starting at 1. Must be a string, ie. '1' 264 | 265 | wait - if not None, number of seconds to wait before querying measurement 266 | 267 | install - if True, adds measurement to the statistics display 268 | """ 269 | 270 | # If a channel value is passed in, make it the 271 | # current channel 272 | if channel is not None and type(channel) is not list: 273 | self.channel = channel 274 | 275 | # Make sure channel is NOT a list 276 | if type(self.channel) is list or type(channel) is list: 277 | raise ValueError('Channel cannot be a list for MEASURE!') 278 | 279 | # Check channel value 280 | if (self.channel not in MSOX3000.chanAnaValidList): 281 | raise ValueError('INVALID Channel Value for MEASURE: {} SKIPPING!'.format(self.channel)) 282 | 283 | # Next check if desired channel is the source, if not switch it 284 | # 285 | # NOTE: doing it this way so as to not possibly break the 286 | # moving average since do not know if buffers are cleared when 287 | # the SOURCE command is sent even if the channel does not 288 | # change. 289 | src = self._instQuery("MEASure:SOURce?") 290 | #print("Source: {}".format(src)) 291 | if (self._chanNumber(src) != self.channel): 292 | # Different channel so switch it 293 | #print("Switching to {}".format(self.channel)) 294 | self._instWrite("MEASure:SOURce {}".format(self._channelStr(self.channel))) 295 | 296 | if (para): 297 | # Need to add parameters to the write and query strings 298 | strWr = "MEASure:{} {}".format(mode, para) 299 | strQu = "MEASure:{}? {}".format(mode, para) 300 | else: 301 | strWr = "MEASure:{}".format(mode) 302 | strQu = "MEASure:{}?".format(mode) 303 | 304 | if (install): 305 | # If desire to install the measurement, make sure the 306 | # statistics display is on and then use the command form of 307 | # the measurement to install the measurement. 308 | self._instWrite("MEASure:STATistics:DISPlay ON") 309 | self._instWrite(strWr) 310 | 311 | # wait a little before read value, if wait is not None 312 | if (wait): 313 | sleep(wait) 314 | 315 | # query the measurement (do not have to install to query it) 316 | val = self._instQuery(strQu) 317 | 318 | return float(val) 319 | 320 | def measureBitRate(self, channel=None, wait=0.25, install=False): 321 | """Measure and return the bit rate measurement. 322 | 323 | This measurement is defined as: 'measures all positive and 324 | negative pulse widths on the waveform, takes the minimum value 325 | found of either width type and inverts that minimum width to 326 | give a value in Hertz' 327 | 328 | If the returned value is >= SCPI.OverRange, then no valid value 329 | could be measured. 330 | 331 | channel: channel, as string, to be measured - default channel 332 | for future readings 333 | 334 | wait - if not None, number of seconds to wait before querying measurement 335 | 336 | install - if True, adds measurement to the statistics display 337 | 338 | """ 339 | 340 | return self._measure("BRATe", channel=channel, wait=wait, install=install) 341 | 342 | def measureBurstWidth(self, channel=None, wait=0.25, install=False): 343 | """Measure and return the bit rate measurement. 344 | 345 | This measurement is defined as: 'the width of the burst on the 346 | screen.' 347 | 348 | If the returned value is >= SCPI.OverRange, then no valid value 349 | could be measured. 350 | 351 | channel: channel, as string, to be measured - default channel 352 | for future readings 353 | 354 | wait - if not None, number of seconds to wait before querying measurement 355 | 356 | install - if True, adds measurement to the statistics display 357 | """ 358 | 359 | return self._measure("BWIDth", channel=channel, wait=wait, install=install) 360 | 361 | def measureCounterFrequency(self, channel=None, wait=0.25, install=False): 362 | """Measure and return the counter frequency 363 | 364 | This measurement is defined as: 'the counter frequency.' 365 | 366 | If the returned value is >= SCPI.OverRange, then no valid value 367 | could be measured. 368 | 369 | channel: channel, as string, to be measured - default channel 370 | for future readings 371 | 372 | wait - if not None, number of seconds to wait before querying measurement 373 | 374 | install - issues if install, so this paramter is ignored 375 | """ 376 | 377 | # NOTE: The programmer's guide suggests sending a :MEASURE:CLEAR 378 | # first because if COUNTER is installed for ANY channel, this 379 | # measurement will fail. Note doing the CLEAR, but if COUNTER 380 | # gets installed, this will fail until it gets manually CLEARed. 381 | 382 | return self._measure("COUNter", channel=channel, wait=wait, install=False) 383 | 384 | def measurePosDutyCycle(self, channel=None, wait=0.25, install=False): 385 | """Measure and return the positive duty cycle 386 | 387 | This measurement is defined as: 'The value returned for the duty 388 | cycle is the ratio of the positive pulse width to the 389 | period. The positive pulse width and the period of the specified 390 | signal are measured, then the duty cycle is calculated with the 391 | following formula: 392 | 393 | duty cycle = (+pulse width/period)*100' 394 | 395 | If the returned value is >= SCPI.OverRange, then no valid value 396 | could be measured. 397 | 398 | channel: channel, as string, to be measured - default channel 399 | for future readings 400 | 401 | wait - if not None, number of seconds to wait before querying measurement 402 | 403 | install - if True, adds measurement to the statistics display 404 | """ 405 | 406 | return self._measure("DUTYcycle", channel=channel, wait=wait, install=install) 407 | 408 | def measureFallTime(self, channel=None, wait=0.25, install=False): 409 | """Measure and return the fall time 410 | 411 | This measurement is defined as: 'the fall time of the displayed 412 | falling (negative-going) edge closest to the trigger 413 | reference. The fall time is determined by measuring the time at 414 | the upper threshold of the falling edge, then measuring the time 415 | at the lower threshold of the falling edge, and calculating the 416 | fall time with the following formula: 417 | 418 | fall time = time at lower threshold - time at upper threshold' 419 | 420 | If the returned value is >= SCPI.OverRange, then no valid value 421 | could be measured. 422 | 423 | channel: channel, as string, to be measured - default channel 424 | for future readings 425 | 426 | wait - if not None, number of seconds to wait before querying measurement 427 | 428 | install - if True, adds measurement to the statistics display 429 | """ 430 | 431 | return self._measure("FALLtime", channel=channel, wait=wait, install=install) 432 | 433 | def measureRiseTime(self, channel=None, wait=0.25, install=False): 434 | """Measure and return the rise time 435 | 436 | This measurement is defined as: 'the rise time of the displayed 437 | rising (positive-going) edge closest to the trigger 438 | reference. For maximum measurement accuracy, set the sweep speed 439 | as fast as possible while leaving the leading edge of the 440 | waveform on the display. The rise time is determined by 441 | measuring the time at the lower threshold of the rising edge and 442 | the time at the upper threshold of the rising edge, then 443 | calculating the rise time with the following formula: 444 | 445 | rise time = time at upper threshold - time at lower threshold' 446 | 447 | If the returned value is >= SCPI.OverRange, then no valid value 448 | could be measured. 449 | 450 | channel: channel, as string, to be measured - default channel 451 | for future readings 452 | 453 | wait - if not None, number of seconds to wait before querying measurement 454 | 455 | install - if True, adds measurement to the statistics display 456 | """ 457 | 458 | return self._measure("RISetime", channel=channel, wait=wait, install=install) 459 | 460 | def measureFrequency(self, channel=None, wait=0.25, install=False): 461 | """Measure and return the frequency of cycle on screen 462 | 463 | This measurement is defined as: 'the frequency of the cycle on 464 | the screen closest to the trigger reference.' 465 | 466 | If the returned value is >= SCPI.OverRange, then no valid value 467 | could be measured. 468 | 469 | channel: channel, as string, to be measured - default channel 470 | for future readings 471 | 472 | wait - if not None, number of seconds to wait before querying measurement 473 | 474 | install - if True, adds measurement to the statistics display 475 | """ 476 | 477 | return self._measure("FREQ", channel=channel, wait=wait, install=install) 478 | 479 | def measureNegDutyCycle(self, channel=None, wait=0.25, install=False): 480 | """Measure and return the negative duty cycle 481 | 482 | This measurement is defined as: 'The value returned for the duty 483 | cycle is the ratio of the negative pulse width to the 484 | period. The negative pulse width and the period of the specified 485 | signal are measured, then the duty cycle is calculated with the 486 | following formula: 487 | 488 | -duty cycle = (-pulse width/period)*100' 489 | 490 | If the returned value is >= SCPI.OverRange, then no valid value 491 | could be measured. 492 | 493 | channel: channel, as string, to be measured - default channel 494 | for future readings 495 | 496 | wait - if not None, number of seconds to wait before querying measurement 497 | 498 | install - if True, adds measurement to the statistics display 499 | """ 500 | 501 | return self._measure("NDUTy", channel=channel, wait=wait, install=install) 502 | 503 | def measureFallEdgeCount(self, channel=None, wait=0.25, install=False): 504 | """Measure and return the on-screen falling edge count 505 | 506 | This measurement is defined as: 'the on-screen falling edge 507 | count' 508 | 509 | If the returned value is >= SCPI.OverRange, then no valid value 510 | could be measured. 511 | 512 | channel: channel, as string, to be measured - default channel 513 | for future readings 514 | 515 | wait - if not None, number of seconds to wait before querying measurement 516 | 517 | install - if True, adds measurement to the statistics display 518 | """ 519 | 520 | return self._measure("NEDGes", channel=channel, wait=wait, install=install) 521 | 522 | def measureFallPulseCount(self, channel=None, wait=0.25, install=False): 523 | """Measure and return the on-screen falling pulse count 524 | 525 | This measurement is defined as: 'the on-screen falling pulse 526 | count' 527 | 528 | If the returned value is >= SCPI.OverRange, then no valid value 529 | could be measured. 530 | 531 | channel: channel, as string, to be measured - default channel 532 | for future readings 533 | 534 | wait - if not None, number of seconds to wait before querying measurement 535 | 536 | install - if True, adds measurement to the statistics display 537 | """ 538 | 539 | return self._measure("NPULses", channel=channel, wait=wait, install=install) 540 | 541 | def measureNegPulseWidth(self, channel=None, wait=0.25, install=False): 542 | """Measure and return the on-screen falling/negative pulse width 543 | 544 | This measurement is defined as: 'the width of the negative pulse 545 | on the screen closest to the trigger reference using the 546 | midpoint between the upper and lower thresholds. 547 | 548 | FOR the negative pulse closest to the trigger point: 549 | 550 | width = (time at trailing rising edge - time at leading falling edge)' 551 | 552 | If the returned value is >= SCPI.OverRange, then no valid value 553 | could be measured. 554 | 555 | channel: channel, as string, to be measured - default channel 556 | for future readings 557 | 558 | wait - if not None, number of seconds to wait before querying measurement 559 | 560 | install - if True, adds measurement to the statistics display 561 | """ 562 | 563 | return self._measure("NWIDth", channel=channel, wait=wait, install=install) 564 | 565 | def measureOvershoot(self, channel=None, wait=0.25, install=False): 566 | """Measure and return the on-screen voltage overshoot in percent 567 | 568 | This measurement is defined as: 'the overshoot of the edge 569 | closest to the trigger reference, displayed on the screen. The 570 | method used to determine overshoot is to make three different 571 | vertical value measurements: Vtop, Vbase, and either Vmax or 572 | Vmin, depending on whether the edge is rising or falling. 573 | 574 | For a rising edge: 575 | 576 | overshoot = ((Vmax-Vtop) / (Vtop-Vbase)) x 100 577 | 578 | For a falling edge: 579 | 580 | overshoot = ((Vbase-Vmin) / (Vtop-Vbase)) x 100 581 | 582 | Vtop and Vbase are taken from the normal histogram of all 583 | waveform vertical values. The extremum of Vmax or Vmin is taken 584 | from the waveform interval right after the chosen edge, halfway 585 | to the next edge. This more restricted definition is used 586 | instead of the normal one, because it is conceivable that a 587 | signal may have more preshoot than overshoot, and the normal 588 | extremum would then be dominated by the preshoot of the 589 | following edge.' 590 | 591 | If the returned value is >= SCPI.OverRange, then no valid value 592 | could be measured. 593 | 594 | channel: channel, as string, to be measured - default channel 595 | for future readings 596 | 597 | wait - if not None, number of seconds to wait before querying measurement 598 | 599 | install - if True, adds measurement to the statistics display 600 | """ 601 | 602 | return self._measure("OVERshoot", channel=channel, wait=wait, install=install) 603 | 604 | def measurePreshoot(self, channel=None, wait=0.25, install=False): 605 | """Measure and return the on-screen voltage preshoot in percent 606 | 607 | This measurement is defined as: 'the preshoot of the edge 608 | closest to the trigger, displayed on the screen. The method used 609 | to determine preshoot is to make three different vertical value 610 | measurements: Vtop, Vbase, and either Vmin or Vmax, depending on 611 | whether the edge is rising or falling. 612 | 613 | For a rising edge: 614 | 615 | preshoot = ((Vmin-Vbase) / (Vtop-Vbase)) x 100 616 | 617 | For a falling edge: 618 | 619 | preshoot = ((Vmax-Vtop) / (Vtop-Vbase)) x 100 620 | 621 | Vtop and Vbase are taken from the normal histogram of all 622 | waveform vertical values. The extremum of Vmax or Vmin is taken 623 | from the waveform interval right before the chosen edge, halfway 624 | back to the previous edge. This more restricted definition is 625 | used instead of the normal one, because it is likely that a 626 | signal may have more overshoot than preshoot, and the normal 627 | extremum would then be dominated by the overshoot of the 628 | preceding edge.' 629 | 630 | If the returned value is >= SCPI.OverRange, then no valid value 631 | could be measured. 632 | 633 | channel: channel, as string, to be measured - default channel 634 | for future readings 635 | 636 | wait - if not None, number of seconds to wait before querying measurement 637 | 638 | install - if True, adds measurement to the statistics display 639 | """ 640 | 641 | return self._measure("PREShoot", channel=channel, wait=wait, install=install) 642 | 643 | def measureRiseEdgeCount(self, channel=None, wait=0.25, install=False): 644 | """Measure and return the on-screen rising edge count 645 | 646 | This measurement is defined as: 'the on-screen rising edge 647 | count' 648 | 649 | If the returned value is >= SCPI.OverRange, then no valid value 650 | could be measured. 651 | 652 | channel: channel, as string, to be measured - default channel 653 | for future readings 654 | 655 | wait - if not None, number of seconds to wait before querying measurement 656 | 657 | install - if True, adds measurement to the statistics display 658 | """ 659 | 660 | return self._measure("PEDGes", channel=channel, wait=wait, install=install) 661 | 662 | def measureRisePulseCount(self, channel=None, wait=0.25, install=False): 663 | """Measure and return the on-screen rising pulse count 664 | 665 | This measurement is defined as: 'the on-screen rising pulse 666 | count' 667 | 668 | If the returned value is >= SCPI.OverRange, then no valid value 669 | could be measured. 670 | 671 | channel: channel, as string, to be measured - default channel 672 | for future readings 673 | 674 | wait - if not None, number of seconds to wait before querying measurement 675 | 676 | install - if True, adds measurement to the statistics display 677 | """ 678 | 679 | return self._measure("PPULses", channel=channel, wait=wait, install=install) 680 | 681 | def measurePosPulseWidth(self, channel=None, wait=0.25, install=False): 682 | """Measure and return the on-screen falling/positive pulse width 683 | 684 | This measurement is defined as: 'the width of the displayed 685 | positive pulse closest to the trigger reference. Pulse width is 686 | measured at the midpoint of the upper and lower thresholds. 687 | 688 | IF the edge on the screen closest to the trigger is falling: 689 | 690 | THEN width = (time at trailing falling edge - time at leading rising edge) 691 | 692 | ELSE width = (time at leading falling edge - time at leading rising edge)' 693 | 694 | If the returned value is >= SCPI.OverRange, then no valid value 695 | could be measured. 696 | 697 | channel: channel, as string, to be measured - default channel 698 | for future readings 699 | 700 | wait - if not None, number of seconds to wait before querying measurement 701 | 702 | install - if True, adds measurement to the statistics display 703 | """ 704 | 705 | return self._measure("PWIDth", channel=channel, wait=wait, install=install) 706 | 707 | def measurePeriod(self, channel=None, wait=0.25, install=False): 708 | """Measure and return the on-screen period 709 | 710 | This measurement is defined as: 'the period of the cycle closest 711 | to the trigger reference on the screen. The period is measured 712 | at the midpoint of the upper and lower thresholds. 713 | 714 | IF the edge closest to the trigger reference on screen is rising: 715 | 716 | THEN period = (time at trailing rising edge - time at leading rising edge) 717 | 718 | ELSE period = (time at trailing falling edge - time at leading falling edge)' 719 | 720 | If the returned value is >= SCPI.OverRange, then no valid value 721 | could be measured. 722 | 723 | channel: channel, as string, to be measured - default channel 724 | for future readings 725 | 726 | wait - if not None, number of seconds to wait before querying measurement 727 | 728 | install - if True, adds measurement to the statistics display 729 | """ 730 | 731 | return self._measure("PERiod", channel=channel, wait=wait, install=install) 732 | 733 | def measureVoltAmplitude(self, channel=None, wait=0.25, install=False): 734 | """Measure and return the vertical amplitude of the signal 735 | 736 | This measurement is defined as: 'the vertical amplitude of the 737 | waveform. To determine the amplitude, the instrument measures 738 | Vtop and Vbase, then calculates the amplitude as follows: 739 | 740 | vertical amplitude = Vtop - Vbase' 741 | 742 | If the returned value is >= SCPI.OverRange, then no valid value 743 | could be measured. 744 | 745 | channel: channel, as string, to be measured - default channel 746 | for future readings 747 | 748 | wait - if not None, number of seconds to wait before querying measurement 749 | 750 | install - if True, adds measurement to the statistics display 751 | """ 752 | 753 | return self._measure("VAMPlitude", channel=channel, wait=wait, install=install) 754 | 755 | def measureVoltAverage(self, channel=None, wait=0.25, install=False): 756 | """Measure and return the Average Voltage measurement. 757 | 758 | This measurement is defined as: 'average value of an integral 759 | number of periods of the signal. If at least three edges are not 760 | present, the oscilloscope averages all data points.' 761 | 762 | If the returned value is >= SCPI.OverRange, then no valid value 763 | could be measured. 764 | 765 | channel: channel, as string, to be measured - default channel 766 | for future readings 767 | 768 | wait - if not None, number of seconds to wait before querying measurement 769 | 770 | install - if True, adds measurement to the statistics display 771 | """ 772 | 773 | return self._measure("VAVerage", para="DISPlay", channel=channel, wait=wait, install=install) 774 | 775 | def measureVoltRMS(self, channel=None, wait=0.25, install=False): 776 | """Measure and return the DC RMS Voltage measurement. 777 | 778 | This measurement is defined as: 'the dc RMS value of the 779 | selected waveform. The dc RMS value is measured on an integral 780 | number of periods of the displayed signal. If at least three 781 | edges are not present, the oscilloscope computes the RMS value 782 | on all displayed data points.' 783 | 784 | If the returned value is >= SCPI.OverRange, then no valid value 785 | could be measured. 786 | 787 | channel: channel, as string, to be measured - default channel 788 | for future readings 789 | 790 | wait - if not None, number of seconds to wait before querying measurement 791 | 792 | install - if True, adds measurement to the statistics display 793 | """ 794 | 795 | return self._measure("VRMS", para="DISPlay", channel=channel, wait=wait, install=install) 796 | 797 | def measureVoltBase(self, channel=None, wait=0.25, install=False): 798 | """Measure and return the Voltage base measurement. 799 | 800 | This measurement is defined as: 'the vertical value at the base 801 | of the waveform. The base value of a pulse is normally not the 802 | same as the minimum value.' 803 | 804 | If the returned value is >= SCPI.OverRange, then no valid value 805 | could be measured. 806 | 807 | channel: channel, as string, to be measured - default channel 808 | for future readings 809 | 810 | wait - if not None, number of seconds to wait before querying measurement 811 | 812 | install - if True, adds measurement to the statistics display 813 | """ 814 | 815 | return self._measure("VBASe", channel=channel, wait=wait, install=install) 816 | 817 | def measureVoltTop(self, channel=None, wait=0.25, install=False): 818 | """Measure and return the Voltage Top measurement. 819 | 820 | This measurement is defined as: 'the vertical value at the top 821 | of the waveform. The top value of the pulse is normally not the 822 | same as the maximum value.' 823 | 824 | If the returned value is >= SCPI.OverRange, then no valid value 825 | could be measured. 826 | 827 | channel: channel, as string, to be measured - default channel 828 | for future readings 829 | 830 | wait - if not None, number of seconds to wait before querying measurement 831 | 832 | install - if True, adds measurement to the statistics display 833 | """ 834 | 835 | return self._measure("VTOP", channel=channel, wait=wait, install=install) 836 | 837 | def measureVoltMax(self, channel=None, wait=0.25, install=False): 838 | """Measure and return the Maximum Voltage measurement. 839 | 840 | This measurement is defined as: 'the maximum vertical value 841 | present on the selected waveform.' 842 | 843 | If the returned value is >= SCPI.OverRange, then no valid value 844 | could be measured. 845 | 846 | channel: channel, as string, to be measured - default channel 847 | for future readings 848 | 849 | wait - if not None, number of seconds to wait before querying measurement 850 | 851 | install - if True, adds measurement to the statistics display 852 | """ 853 | 854 | return self._measure("VMAX", channel=channel, wait=wait, install=install) 855 | 856 | 857 | def measureVoltMin(self, channel=None, wait=0.25, install=False): 858 | """Measure and return the Minimum Voltage measurement. 859 | 860 | This measurement is defined as: 'the minimum vertical value 861 | present on the selected waveform.' 862 | 863 | If the returned value is >= SCPI.OverRange, then no valid value 864 | could be measured. 865 | 866 | channel: channel, as string, to be measured - default channel 867 | for future readings 868 | 869 | wait - if not None, number of seconds to wait before querying measurement 870 | 871 | install - if True, adds measurement to the statistics display 872 | """ 873 | 874 | return self._measure("VMIN", channel=channel, wait=wait, install=install) 875 | 876 | 877 | def measureVoltPP(self, channel=None, wait=0.25, install=False): 878 | """Measure and return the voltage peak-to-peak measurement. 879 | 880 | This measurement is defined as: 'the maximum and minimum 881 | vertical value for the selected source, then calculates the 882 | vertical peak-to-peak value and returns that value. The 883 | peak-to-peak value (Vpp) is calculated with the following 884 | formula: 885 | 886 | Vpp = Vmax - Vmin 887 | 888 | Vmax and Vmin are the vertical maximum and minimum values 889 | present on the selected source.' 890 | 891 | If the returned value is >= SCPI.OverRange, then no valid value 892 | could be measured. 893 | 894 | channel: channel, as string, to be measured - default channel 895 | for future readings 896 | 897 | wait - if not None, number of seconds to wait before querying measurement 898 | 899 | install - if True, adds measurement to the statistics display 900 | """ 901 | 902 | return self._measure("VPP", channel=channel, wait=wait, install=install) 903 | 904 | 905 | def _readDVM(self, mode, channel=None, timeout=None, wait=0.5): 906 | """Read the DVM data of desired channel and return the value. 907 | 908 | channel: channel, as a string, to set to DVM mode and return its 909 | reading - becomes the default channel for future readings 910 | 911 | timeout: if None, no timeout, otherwise, time-out in seconds 912 | waiting for a valid number 913 | 914 | wait: Number of seconds after select DVM mode before trying to 915 | read values. Set to None for no waiting (not recommended) 916 | """ 917 | 918 | # If a channel value is passed in, make it the 919 | # current channel 920 | if channel is not None and type(channel) is not list: 921 | self.channel = channel 922 | 923 | # Make sure channel is NOT a list 924 | if type(self.channel) is list or type(channel) is list: 925 | raise ValueError('Channel cannot be a list for DVM!') 926 | 927 | # Check channel value 928 | if (self.channel not in MSOX3000.chanAnaValidList): 929 | raise ValueError('INVALID Channel Value for DVM: {} SKIPPING!'.format(self.channel)) 930 | 931 | # First check if DVM is enabled 932 | en = self._instQuery("DVM:ENABle?") 933 | if (not self._1OR0(en)): 934 | # It is not enabled, so enable it 935 | self._instWrite("DVM:ENABLE ON") 936 | 937 | # Next check if desired DVM channel is the source, if not switch it 938 | # 939 | # NOTE: doing it this way so as to not possibly break the 940 | # moving average since do not know if buffers are cleared when 941 | # the SOURCE command is sent even if the channel does not 942 | # change. 943 | src = self._instQuery("DVM:SOURce?") 944 | #print("Source: {}".format(src)) 945 | if (self._chanNumber(src) != self.channel): 946 | # Different channel value so switch it 947 | #print("Switching to {}".format(self.channel)) 948 | self._instWrite("DVM:SOURce {}".format(self._channelStr(self.channel))) 949 | 950 | # Select the desired DVM mode 951 | self._instWrite("DVM:MODE {}".format(mode)) 952 | 953 | # wait a little before read value to make sure everything is switched 954 | if (wait): 955 | sleep(wait) 956 | 957 | # Read value until get one < +9.9E+37 (per programming guide suggestion) 958 | startTime = datetime.now() 959 | val = SCPI.OverRange 960 | while (val >= SCPI.OverRange): 961 | duration = datetime.now() - startTime 962 | if (timeout is not None and duration.total_seconds() >= timeout): 963 | # if timeout is a value and have been waiting that 964 | # many seconds for a valid DVM value, stop waiting and 965 | # return this SCPI.OverRange number. 966 | break 967 | 968 | val = self._instQueryNumber("DVM:CURRent?") 969 | 970 | # if mode is frequency, read and return the 5-digit frequency instead 971 | if (mode == "FREQ"): 972 | val = self._instQueryNumber("DVM:FREQ?") 973 | 974 | return val 975 | 976 | def measureDVMacrms(self, channel=None, timeout=None, wait=0.5): 977 | """Measure and return the AC RMS reading of channel using DVM 978 | mode. 979 | 980 | AC RMS is defined as 'the root-mean-square value of the acquired 981 | data, with the DC component removed.' 982 | 983 | channel: channel, as a string, to set to DVM mode and return its 984 | reading - becomes the default channel for future readings 985 | 986 | timeout: if None, no timeout, otherwise, time-out in seconds 987 | waiting for a valid number - if timeout, returns SCPI.OverRange 988 | """ 989 | 990 | return self._readDVM("ACRM", channel, timeout, wait) 991 | 992 | def measureDVMdc(self, channel=None, timeout=None, wait=0.5): 993 | """ Measure and return the DC reading of channel using DVM mode. 994 | 995 | DC is defined as 'the DC value of the acquired data.' 996 | 997 | channel: channel, as a string, to set to DVM mode and return its 998 | reading - becomes the default channel for future readings 999 | 1000 | timeout: if None, no timeout, otherwise, time-out in seconds 1001 | waiting for a valid number - if timeout, returns SCPI.OverRange 1002 | """ 1003 | 1004 | return self._readDVM("DC", channel, timeout, wait) 1005 | 1006 | def measureDVMdcrms(self, channel=None, timeout=None, wait=0.5): 1007 | """ Measure and return the DC RMS reading of channel using DVM mode. 1008 | 1009 | DC RMS is defined as 'the root-mean-square value of the acquired data.' 1010 | 1011 | channel: channel, as a string, to set to DVM mode and return its 1012 | reading - becomes the default channel for future readings 1013 | 1014 | timeout: if None, no timeout, otherwise, time-out in seconds 1015 | waiting for a valid number - if timeout, returns SCPI.OverRange 1016 | """ 1017 | 1018 | return self._readDVM("DCRM", channel, timeout, wait) 1019 | 1020 | def measureDVMfreq(self, channel=None, timeout=3, wait=0.5): 1021 | """ Measure and return the FREQ reading of channel using DVM mode. 1022 | 1023 | FREQ is defined as 'the frequency counter measurement.' 1024 | 1025 | channel: channel, as a string, to set to DVM mode and return its 1026 | reading - becomes the default channel for future readings 1027 | 1028 | timeout: if None, no timeout, otherwise, time-out in seconds 1029 | waiting for a valid number - if timeout, returns SCPI.OverRange 1030 | 1031 | NOTE: If the signal is not periodic, this call will block until 1032 | a frequency is measured, unless a timeout value is given. 1033 | """ 1034 | 1035 | return self._readDVM("FREQ", channel, timeout, wait) 1036 | 1037 | 1038 | # ========================================================= 1039 | # Based on the screen image download example from the MSO-X 3000 Programming 1040 | # Guide and modified to work within this class ... 1041 | # ========================================================= 1042 | def hardcopy(self, filename): 1043 | """ Download the screen image to the given filename. """ 1044 | 1045 | self._instWrite("HARDcopy:INKSaver OFF") 1046 | scrImage = self._instQueryIEEEBlock("DISPlay:DATA? PNG, COLor") 1047 | 1048 | # Save display data values to file. 1049 | f = open(filename, "wb") 1050 | f.write(scrImage) 1051 | f.close() 1052 | 1053 | # ========================================================= 1054 | # Based on the Waveform data download example from the MSO-X 3000 Programming 1055 | # Guide and modified to work within this class ... 1056 | # ========================================================= 1057 | def waveform(self, filename, channel=None, points=None): 1058 | """ Download the Waveform Data of a particular Channel and saved to the given filename as a CSV file. """ 1059 | 1060 | DEBUG = False 1061 | import csv 1062 | 1063 | # If a channel value is passed in, make it the 1064 | # current channel 1065 | if channel is not None and type(channel) is not list: 1066 | self.channel = channel 1067 | 1068 | # Make sure channel is NOT a list 1069 | if type(self.channel) is list or type(channel) is list: 1070 | raise ValueError('Channel cannot be a list for WAVEFORM!') 1071 | 1072 | # Check channel value 1073 | if (self.channel not in MSOX3000.chanAllValidList): 1074 | raise ValueError('INVALID Channel Value for WAVEFORM: {} SKIPPING!'.format(self.channel)) 1075 | 1076 | if self.channel.upper().startswith('POD'): 1077 | pod = int(self.channel[-1]) 1078 | else: 1079 | pod = None 1080 | 1081 | # Download waveform data. 1082 | # Set the waveform points mode. 1083 | self._instWrite("WAVeform:POINts:MODE MAX") 1084 | if DEBUG: 1085 | qresult = self._instQuery("WAVeform:POINts:MODE?") 1086 | print( "Waveform points mode: {}".format(qresult) ) 1087 | 1088 | # Set the number of waveform points to fetch, if it was passed in 1089 | if (points is not None): 1090 | self._instWrite("WAVeform:POINts {}".format(points)) 1091 | if DEBUG: 1092 | qresult = self._instQuery("WAVeform:POINts?") 1093 | print( "Waveform points available: {}".format(qresult) ) 1094 | 1095 | # Set the waveform source. 1096 | self._instWrite("WAVeform:SOURce {}".format(self._channelStr(self.channel))) 1097 | if DEBUG: 1098 | qresult = self._instQuery("WAVeform:SOURce?") 1099 | print( "Waveform source: {}".format(qresult) ) 1100 | 1101 | # Choose the format of the data returned: 1102 | self._instWrite("WAVeform:FORMat BYTE") 1103 | if DEBUG: 1104 | print( "Waveform format: {}".format(self._instQuery("WAVeform:FORMat?")) ) 1105 | 1106 | if DEBUG: 1107 | # Display the waveform settings from preamble: 1108 | wav_form_dict = { 1109 | 0 : "BYTE", 1110 | 1 : "WORD", 1111 | 4 : "ASCii", } 1112 | 1113 | acq_type_dict = { 1114 | 0 : "NORMal", 1115 | 1 : "PEAK", 1116 | 2 : "AVERage", 1117 | 3 : "HRESolution", 1118 | } 1119 | 1120 | ( 1121 | wav_form_f, 1122 | acq_type_f, 1123 | wfmpts_f, 1124 | avgcnt_f, 1125 | x_increment, 1126 | x_origin, 1127 | x_reference_f, 1128 | y_increment, 1129 | y_origin, 1130 | y_reference_f 1131 | ) = self._instQueryNumbers("WAVeform:PREamble?") 1132 | 1133 | ## convert the numbers that are meant to be integers 1134 | ( 1135 | wav_form, 1136 | acq_type, 1137 | wfmpts, 1138 | avgcnt, 1139 | x_reference, 1140 | y_reference 1141 | ) = list(map(int, ( 1142 | wav_form_f, 1143 | acq_type_f, 1144 | wfmpts_f, 1145 | avgcnt_f, 1146 | x_reference_f, 1147 | y_reference_f 1148 | ))) 1149 | 1150 | 1151 | print( "Waveform format: {}".format(wav_form_dict[(wav_form)]) ) 1152 | print( "Acquire type: {}".format(acq_type_dict[(acq_type)]) ) 1153 | print( "Waveform points desired: {:d}".format((wfmpts)) ) 1154 | print( "Waveform average count: {:d}".format((avgcnt)) ) 1155 | print( "Waveform X increment: {:1.12f}".format(x_increment) ) 1156 | print( "Waveform X origin: {:1.9f}".format(x_origin) ) 1157 | print( "Waveform X reference: {:d}".format((x_reference)) ) # Always 0. 1158 | print( "Waveform Y increment: {:f}".format(y_increment) ) 1159 | print( "Waveform Y origin: {:f}".format(y_origin) ) 1160 | print( "Waveform Y reference: {:d}".format((y_reference)) ) # Always 125. 1161 | 1162 | # Get numeric values for later calculations. 1163 | x_increment = self._instQueryNumber("WAVeform:XINCrement?") 1164 | x_origin = self._instQueryNumber("WAVeform:XORigin?") 1165 | y_increment = self._instQueryNumber("WAVeform:YINCrement?") 1166 | y_origin = self._instQueryNumber("WAVeform:YORigin?") 1167 | y_reference = self._instQueryNumber("WAVeform:YREFerence?") 1168 | 1169 | # Get the waveform data. 1170 | waveform_data = self._instQueryIEEEBlock("WAVeform:DATA?") 1171 | 1172 | if (version_info < (3,)): 1173 | ## If PYTHON 2, waveform_data will be a string and needs to be converted into a list of integers 1174 | data_bytes = [ord(x) for x in waveform_data] 1175 | else: 1176 | ## If PYTHON 3, waveform_data is already in the correct format 1177 | data_bytes = waveform_data 1178 | 1179 | nLength = len(data_bytes) 1180 | if (DEBUG): 1181 | print( "Number of data values: {:d}".format(nLength) ) 1182 | 1183 | # Open file for output. 1184 | myFile = open(filename, 'w') 1185 | with myFile: 1186 | writer = csv.writer(myFile, dialect='excel', quoting=csv.QUOTE_NONNUMERIC) 1187 | if pod: 1188 | writer.writerow(['Time (s)'] + ['D{}'.format((pod-1) * 8 + ch) for ch in range(8)]) 1189 | else: 1190 | writer.writerow(['Time (s)', 'Voltage (V)']) 1191 | 1192 | # Output waveform data in CSV format. 1193 | for i in range(0, nLength - 1): 1194 | time_val = x_origin + (i * x_increment) 1195 | if pod: 1196 | writer.writerow([time_val] + [(data_bytes[i] >> ch) & 1 for ch in range(8)]) 1197 | else: 1198 | voltage = (data_bytes[i] - y_reference) * y_increment + y_origin 1199 | writer.writerow([time_val, voltage]) 1200 | 1201 | if (DEBUG): 1202 | print( "Waveform format BYTE data written to {}.".format(filename) ) 1203 | 1204 | # return number of entries written 1205 | return nLength 1206 | 1207 | ## This is a dictionary of measurement labels with their units and 1208 | ## method to get the data from the scope. 1209 | measureTbl = { 1210 | 'Bit Rate': ['Hz', measureBitRate], 1211 | 'Burst Width': ['s', measureBurstWidth], 1212 | 'Counter Freq': ['Hz', measureCounterFrequency], 1213 | 'Frequency': ['Hz', measureFrequency], 1214 | 'Period': ['s', measurePeriod], 1215 | 'Duty': ['%', measurePosDutyCycle], 1216 | 'Neg Duty': ['%', measureNegDutyCycle], 1217 | 'Fall Time': ['s', measureFallTime], 1218 | 'Rise Time': ['s', measureRiseTime], 1219 | 'Num Falling': ['', measureFallEdgeCount], 1220 | 'Num Neg Pulses': ['', measureFallPulseCount], 1221 | 'Num Rising': ['', measureRiseEdgeCount], 1222 | 'Num Pos Pulses': ['', measureRisePulseCount], 1223 | '- Width': ['s', measureNegPulseWidth], 1224 | '+ Width': ['s', measurePosPulseWidth], 1225 | 'Overshoot': ['%', measureOvershoot], 1226 | 'Preshoot': ['%', measurePreshoot], 1227 | 'Amplitude': ['V', measureVoltAmplitude], 1228 | 'Top': ['V', measureVoltTop], 1229 | 'Base': ['V', measureVoltBase], 1230 | 'Maximum': ['V', measureVoltMax], 1231 | 'Minimum': ['V', measureVoltMin], 1232 | 'Pk-Pk': ['V', measureVoltPP], 1233 | 'Average - Full Screen': ['V', measureVoltAverage], 1234 | 'RMS - Full Screen': ['V', measureVoltRMS], 1235 | } 1236 | 1237 | if __name__ == '__main__': 1238 | import argparse 1239 | parser = argparse.ArgumentParser(description='Access and control a MSO-X/DSO-X 3000 Oscilloscope') 1240 | parser.add_argument('chan', nargs='?', type=int, help='Channel to access/control (starts at 1)', default=1) 1241 | args = parser.parse_args() 1242 | 1243 | from os import environ 1244 | resource = environ.get('MSOX3000_IP', 'TCPIP0::172.16.2.13::INSTR') 1245 | instr = MSOX3000(resource) 1246 | instr.open() 1247 | 1248 | # set the channel (can pass channel to each method or just set it 1249 | # once and it becomes the default for all following calls) 1250 | instr.channel = str(args.chan) 1251 | 1252 | if not instr.isOutputOn(): 1253 | instr.outputOn() 1254 | 1255 | # Install measurements to display in statistics display and also 1256 | # return their current values 1257 | print('Ch. {} Settings: {:6.4e} V PW {:6.4e} s\n'. 1258 | format(instr.channel, instr.measureVoltAverage(install=True), 1259 | instr.measurePosPulseWidth(install=True))) 1260 | 1261 | # Add an annotation to the screen before hardcopy 1262 | instr._instWrite("DISPlay:ANN ON") 1263 | instr._instWrite('DISPlay:ANN:TEXT "{}\\n{} {}"'.format('Example of Annotation','for Channel',instr.channel)) 1264 | instr._instWrite("DISPlay:ANN:BACKground TRAN") # transparent background - can also be OPAQue or INVerted 1265 | instr._instWrite("DISPlay:ANN:COLor CH{}".format(instr.channel)) 1266 | 1267 | # Change label of the channel to "MySig" 1268 | instr._instWrite('CHAN{}:LABel "MySig"'.format(instr.channel)) 1269 | instr._instWrite('DISPlay:LABel ON') 1270 | 1271 | # Make sure the statistics display is showing 1272 | instr._instWrite("SYSTem:MENU MEASure") 1273 | instr._instWrite("MEASure:STATistics:DISPlay ON") 1274 | 1275 | ## Save a hardcopy of the screen 1276 | instr.hardcopy('outfile.png') 1277 | 1278 | # Change label back to the default 1279 | instr._instWrite('CHAN{}:LABel "{}"'.format(instr.channel, instr.channel)) 1280 | instr._instWrite('DISPlay:LABel OFF') 1281 | 1282 | # Turn off the annotation 1283 | instr._instWrite("DISPlay:ANN OFF") 1284 | 1285 | ## Read ALL available measurements from channel, without installing 1286 | ## to statistics display, with units 1287 | print('\nMeasurements for Ch. {}:'.format(instr.channel)) 1288 | measurements = ['Bit Rate', 1289 | 'Burst Width', 1290 | 'Counter Freq', 1291 | 'Frequency', 1292 | 'Period', 1293 | 'Duty', 1294 | 'Neg Duty', 1295 | '+ Width', 1296 | '- Width', 1297 | 'Rise Time', 1298 | 'Num Rising', 1299 | 'Num Pos Pulses', 1300 | 'Fall Time', 1301 | 'Num Falling', 1302 | 'Num Neg Pulses', 1303 | 'Overshoot', 1304 | 'Preshoot', 1305 | '', 1306 | 'Amplitude', 1307 | 'Pk-Pk', 1308 | 'Top', 1309 | 'Base', 1310 | 'Maximum', 1311 | 'Minimum', 1312 | 'Average - Full Screen', 1313 | 'RMS - Full Screen', 1314 | ] 1315 | for meas in measurements: 1316 | if (meas == ''): 1317 | # use a blank string to put in an extra line 1318 | print() 1319 | else: 1320 | # using MSOX3000.measureTbl[] dictionary, call the 1321 | # appropriate method to read the measurement. Also, using 1322 | # the same measurement name, pass it to the polish() method 1323 | # to format the data with units and SI suffix. 1324 | print('{: <24} {:>12.6}'.format(meas,instr.polish(MSOX3000.measureTbl[meas][1](instr), meas))) 1325 | 1326 | ## turn off the channel 1327 | instr.outputOff() 1328 | 1329 | ## return to LOCAL mode 1330 | instr.setLocal() 1331 | 1332 | instr.close() 1333 | --------------------------------------------------------------------------------