├── .gitignore ├── AUTHORS ├── COPYING ├── MANIFEST.in ├── README ├── README.md ├── doc ├── Makefile ├── appendix.rst ├── conf.py ├── examples.rst ├── index.rst ├── intro.rst ├── make.bat └── vxi11.rst ├── setup.cfg ├── setup.py ├── tests ├── __init__.py └── test_vxi11.py └── vxi11 ├── __init__.py ├── cli.py ├── rpc.py ├── version.py └── vxi11.py /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.kate-swp 3 | *.pyc 4 | 5 | build 6 | dist 7 | *.egg-info 8 | 9 | MANIFEST 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Alex Forencich 2 | Michael Walle 3 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2017 Alex Forencich and Michael Walle 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS COPYING MANIFEST.in 2 | include doc/*.rst doc/Makefile doc/make.bat -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python VXI-11 Readme 2 | 3 | For more information and updates: 4 | http://alexforencich.com/wiki/en/python-vxi11/start 5 | 6 | GitHub repository: 7 | https://github.com/python-ivi/python-vxi11 8 | 9 | Google group: 10 | https://groups.google.com/d/forum/python-ivi 11 | 12 | ## Introduction 13 | 14 | Python VXI-11 provides a pure Python VXI-11 driver for controlling instruments 15 | over Ethernet. 16 | 17 | ## Requirements 18 | 19 | * Python 2 or Python 3 20 | 21 | ## Installation 22 | 23 | Extract and run 24 | 25 | # python setup.py install 26 | 27 | ## Usage examples 28 | 29 | Connecting to Agilent MSO7104A via LXI: 30 | 31 | import vxi11 32 | instr = vxi11.Instrument("192.168.1.104") 33 | print(instr.ask("*IDN?")) 34 | # returns 'AGILENT TECHNOLOGIES,MSO7104A,MY********,06.16.0001' 35 | 36 | Connecting to Agilent E3649A on GPIB address 5 via HP 2050A GPIB bridge: 37 | 38 | import vxi11 39 | instr = vxi11.Instrument("192.168.1.105", "gpib,5") 40 | print(instr.ask("*IDN?")) 41 | # returns 'Agilent Technologies,E3649A,0,1.4-5.0-1.0' 42 | 43 | Connecting to Agilent MSO-X 3014A via USBTMC via Agilent E5810 GPIB bridge: 44 | 45 | import vxi11 46 | instr = vxi11.Instrument("192.168.1.201", "usb0[2391::6056::MY********::0]") 47 | print(instr.ask("*IDN?")) 48 | # returns 'AGILENT TECHNOLOGIES,MSO-X 3014A,MY********,02.35.2013061800' 49 | 50 | It is also possible to connect with VISA resource strings like so: 51 | 52 | import vxi11 53 | instr = vxi11.Instrument("TCPIP::192.168.1.104::INSTR") 54 | print(instr.ask("*IDN?")) 55 | # returns 'AGILENT TECHNOLOGIES,MSO7104A,MY********,06.16.0001' 56 | 57 | and: 58 | 59 | import vxi11 60 | instr = vxi11.Instrument("TCPIP::192.168.1.105::gpib,5::INSTR") 61 | print(instr.ask("*IDN?")) 62 | # returns 'Agilent Technologies,E3649A,0,1.4-5.0-1.0' 63 | 64 | and: 65 | 66 | import vxi11 67 | instr = vxi11.Instrument("TCPIP::192.168.1.201::usb0[2391::6056::MY********::0]::INSTR") 68 | print(instr.ask("*IDN?")) 69 | # returns 'AGILENT TECHNOLOGIES,MSO-X 3014A,MY********,02.35.2013061800' 70 | 71 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonVXI11.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonVXI11.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PythonVXI11" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PythonVXI11" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /doc/appendix.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Appendix 3 | ========== 4 | 5 | License 6 | ======= 7 | 8 | Copyright (c) 2012-2017 Alex Forencich and Michael Walle 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | 17 | The above copyright notice and this permission notice shall be included in 18 | all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY 22 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | THE SOFTWARE. 27 | 28 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Python VXI11 documentation build configuration file, created by 5 | # sphinx-quickstart on Fri Jul 26 09:25:03 2013. 6 | # 7 | # This file is execfile()d with the current directory set to its containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys, os 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | #sys.path.insert(0, os.path.abspath('.')) 21 | sys.path.insert(0, os.path.abspath("..")) 22 | 23 | 24 | # -- General configuration ----------------------------------------------------- 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be extensions 30 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 31 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # The suffix of source filenames. 37 | source_suffix = '.rst' 38 | 39 | # The encoding of source files. 40 | #source_encoding = 'utf-8-sig' 41 | 42 | # The master toctree document. 43 | master_doc = 'index' 44 | 45 | # General information about the project. 46 | project = 'Python VXI11' 47 | copyright = '2013, Alex Forencich' 48 | 49 | # The version info for the project you're documenting, acts as replacement for 50 | # |version| and |release|, also used in various other places throughout the 51 | # built documents. 52 | # 53 | # The short X.Y version. 54 | version = '0.1' 55 | # The full version, including alpha/beta/rc tags. 56 | release = '0.1' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | #language = None 61 | 62 | # There are two options for replacing |today|: either, you set today to some 63 | # non-false value, then it is used: 64 | #today = '' 65 | # Else, today_fmt is used as the format for a strftime call. 66 | #today_fmt = '%B %d, %Y' 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | exclude_patterns = ['_build'] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all documents. 73 | #default_role = None 74 | 75 | # If true, '()' will be appended to :func: etc. cross-reference text. 76 | #add_function_parentheses = True 77 | 78 | # If true, the current module name will be prepended to all description 79 | # unit titles (such as .. function::). 80 | #add_module_names = True 81 | 82 | # If true, sectionauthor and moduleauthor directives will be shown in the 83 | # output. They are ignored by default. 84 | #show_authors = False 85 | 86 | # The name of the Pygments (syntax highlighting) style to use. 87 | pygments_style = 'sphinx' 88 | 89 | # A list of ignored prefixes for module index sorting. 90 | #modindex_common_prefix = [] 91 | 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | html_theme = 'default' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | #html_theme_options = {} 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | #html_theme_path = [] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | #html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | #html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | #html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | html_static_path = ['_static'] 127 | 128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 129 | # using the given strftime format. 130 | #html_last_updated_fmt = '%b %d, %Y' 131 | 132 | # If true, SmartyPants will be used to convert quotes and dashes to 133 | # typographically correct entities. 134 | #html_use_smartypants = True 135 | 136 | # Custom sidebar templates, maps document names to template names. 137 | #html_sidebars = {} 138 | 139 | # Additional templates that should be rendered to pages, maps page names to 140 | # template names. 141 | #html_additional_pages = {} 142 | 143 | # If false, no module index is generated. 144 | #html_domain_indices = True 145 | 146 | # If false, no index is generated. 147 | #html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | #html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | #html_show_sourcelink = True 154 | 155 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 156 | #html_show_sphinx = True 157 | 158 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 159 | #html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = None 168 | 169 | # Output file base name for HTML help builder. 170 | htmlhelp_basename = 'PythonVXI11doc' 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | latex_elements = { 176 | # The paper size ('letterpaper' or 'a4paper'). 177 | #'papersize': 'letterpaper', 178 | 179 | # The font size ('10pt', '11pt' or '12pt'). 180 | #'pointsize': '10pt', 181 | 182 | # Additional stuff for the LaTeX preamble. 183 | #'preamble': '', 184 | } 185 | 186 | # Grouping the document tree into LaTeX files. List of tuples 187 | # (source start file, target name, title, author, documentclass [howto/manual]). 188 | latex_documents = [ 189 | ('index', 'PythonVXI11.tex', 'Python VXI11 Documentation', 190 | 'Alex Forencich', 'manual'), 191 | ] 192 | 193 | # The name of an image file (relative to this directory) to place at the top of 194 | # the title page. 195 | #latex_logo = None 196 | 197 | # For "manual" documents, if this is true, then toplevel headings are parts, 198 | # not chapters. 199 | #latex_use_parts = False 200 | 201 | # If true, show page references after internal links. 202 | #latex_show_pagerefs = False 203 | 204 | # If true, show URL addresses after external links. 205 | #latex_show_urls = False 206 | 207 | # Documents to append as an appendix to all manuals. 208 | #latex_appendices = [] 209 | 210 | # If false, no module index is generated. 211 | #latex_domain_indices = True 212 | 213 | 214 | # -- Options for manual page output -------------------------------------------- 215 | 216 | # One entry per manual page. List of tuples 217 | # (source start file, name, description, authors, manual section). 218 | man_pages = [ 219 | ('index', 'pythonvxi11', 'Python VXI11 Documentation', 220 | ['Alex Forencich'], 1) 221 | ] 222 | 223 | # If true, show URL addresses after external links. 224 | #man_show_urls = False 225 | 226 | 227 | # -- Options for Texinfo output ------------------------------------------------ 228 | 229 | # Grouping the document tree into Texinfo files. List of tuples 230 | # (source start file, target name, title, author, 231 | # dir menu entry, description, category) 232 | texinfo_documents = [ 233 | ('index', 'PythonVXI11', 'Python VXI11 Documentation', 234 | 'Alex Forencich', 'PythonVXI11', 'One line description of project.', 235 | 'Miscellaneous'), 236 | ] 237 | 238 | # Documents to append as an appendix to all manuals. 239 | #texinfo_appendices = [] 240 | 241 | # If false, no module index is generated. 242 | #texinfo_domain_indices = True 243 | 244 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 245 | #texinfo_show_urls = 'footnote' 246 | -------------------------------------------------------------------------------- /doc/examples.rst: -------------------------------------------------------------------------------- 1 | ===================== 2 | Python VXI11 Examples 3 | ===================== 4 | 5 | Opening a connection 6 | ==================== 7 | 8 | Connect to an Agilent MSO7104A oscilloscope on IP address 192.168.1.104:: 9 | 10 | >>> import vxi11 11 | >>> instr = vxi11.Instrument("192.168.1.104") 12 | >>> print(instr.ask("*IDN?")) 13 | 'AGILENT TECHNOLOGIES,MSO7104A,MY********,06.16.0001' 14 | 15 | Connect to an Agilent E3649A via an HP 2050A GPIB bridge:: 16 | 17 | >>> import vxi11 18 | >>> instr = vxi11.Instrument("192.168.1.105", "gpib,5") 19 | >>> print(instr.ask("*IDN?")) 20 | 'Agilent Technologies,E3649A,0,1.4-5.0-1.0' 21 | 22 | Configuring connections 23 | ======================= 24 | 25 | Open a connection and set the timeout:: 26 | 27 | >>> import vxi11 28 | >>> instr = vxi11.Instrument("192.168.1.104") 29 | >>> instr.timeout = 60*1000 30 | >>> print(instr.ask("*TST?")) 31 | '0' 32 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. Python VXI11 documentation master file 2 | 3 | Welcome to Python VXI11's documentation 4 | ======================================= 5 | 6 | This Python package supports the VXI-11 Ethernet instrument control protocol 7 | for controlling VXI11 and LXI compatible instruments. 8 | 9 | See also: 10 | 11 | - `Python VXI11 home page`_ 12 | - `GitHub repository`_ 13 | 14 | .. _`Python VXI11 home page`: http://alexforencich.com/wiki/en/python-vxi11/start 15 | .. _`GitHub repository`: https://github.com/python-ivi/python-vxi11 16 | 17 | Contents: 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | intro 23 | examples 24 | vxi11 25 | appendix 26 | 27 | Indices and tables 28 | ================== 29 | 30 | * :ref:`genindex` 31 | * :ref:`modindex` 32 | * :ref:`search` 33 | 34 | -------------------------------------------------------------------------------- /doc/intro.rst: -------------------------------------------------------------------------------- 1 | ============================ 2 | Introduction to Python VXI11 3 | ============================ 4 | 5 | Overview 6 | ======== 7 | This Python package supports the VXI-11 Ethernet instrument control protocol 8 | for controlling VXI11 and LXI compatible instruments. The implementation is 9 | pure Python and highly portable. 10 | 11 | It is released under the MIT license, see LICENSE_ for more 12 | details. 13 | 14 | Copyright (c) 2012-2017 Alex Forencich and Michael Walle 15 | 16 | See also: 17 | 18 | - `Python VXI11 home page`_ 19 | - `GitHub repository`_ 20 | 21 | .. _LICENSE: appendix.html#license 22 | .. _`Python VXI11 home page`: http://alexforencich.com/wiki/en/python-vxi11/start 23 | .. _`GitHub repository`: https://github.com/alexforencich/python-vxi11 24 | 25 | 26 | Features 27 | ======== 28 | - Supports Python 2 and Python 3 29 | - Pure Python 30 | - Highly portable 31 | - Communicates with instruments over the VXI11 TCP/IP instrument control protocol 32 | 33 | Requirements 34 | ============ 35 | - Python 2 or Python 3 36 | 37 | 38 | Installation 39 | ============ 40 | 41 | To install the module for all users on the system, administrator rights (root) 42 | are required. 43 | 44 | From source 45 | ~~~~~~~~~~~ 46 | Download the archive, extract, and run:: 47 | 48 | python setup.py install 49 | 50 | Packages 51 | ~~~~~~~~ 52 | There are also packaged versions for some Linux distributions: 53 | 54 | Arch Linux 55 | Python VXI11 is available under the name "python-vxi11-git" in the AUR. 56 | 57 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\PythonVXI11.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\PythonVXI11.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /doc/vxi11.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | :mod:`vxi11` --- VXI11 driver 3 | ============================= 4 | 5 | .. module:: vxi11 6 | :synopsis: VXI11 driver 7 | 8 | This module provides a VXI11 TCP/IP instrument driver. 9 | 10 | Functions and Exceptions 11 | ------------------------ 12 | 13 | .. exception:: Vxi11Error 14 | 15 | Exception raised on various occasions; argument is a string describing what 16 | is wrong. 17 | 18 | :class:`Instrument` class 19 | ------------------------- 20 | 21 | .. autoclass:: Instrument 22 | :members: 23 | 24 | 25 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | source-dir = doc/ 3 | build-dir = doc/_build 4 | all_files = 1 5 | 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import with_statement 3 | 4 | # http://docs.python.org/distutils/ 5 | # http://packages.python.org/distribute/ 6 | try: 7 | from setuptools import setup 8 | except: 9 | from distutils.core import setup 10 | 11 | import os.path 12 | 13 | version_py = os.path.join(os.path.dirname(__file__), 'vxi11', 'version.py') 14 | with open(version_py, 'r') as f: 15 | d = dict() 16 | exec(f.read(), d) 17 | version = d['__version__'] 18 | 19 | setup( 20 | name = 'python-vxi11', 21 | description = 'Python VXI-11 driver for controlling instruments over Ethernet', 22 | version = version, 23 | long_description = '''This Python package supports the VXI-11 Ethernet 24 | instrument control protocol for controlling VXI11 and LXI compatible instruments.''', 25 | author = 'Alex Forencich', 26 | author_email = 'alex@alexforencich.com', 27 | url = 'http://alexforencich.com/wiki/en/python-vxi11/start', 28 | download_url = 'http://github.com/python-ivi/python-vxi11/tarball/master', 29 | keywords = 'VXI LXI measurement instrument', 30 | license = 'MIT License', 31 | classifiers=[ 32 | 'Development Status :: 4 - Beta', 33 | 'Environment :: Console', 34 | 'License :: OSI Approved :: MIT License', 35 | 'Natural Language :: English', 36 | 'Operating System :: OS Independent', 37 | 'Intended Audience :: Science/Research', 38 | 'Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator', 39 | 'Topic :: Software Development :: Libraries :: Python Modules', 40 | 'Topic :: System :: Hardware :: Hardware Drivers', 41 | 'Topic :: System :: Networking', 42 | 'Programming Language :: Python :: 2', 43 | 'Programming Language :: Python :: 3' 44 | ], 45 | packages = ['vxi11'], 46 | entry_points = { 47 | 'console_scripts': [ 48 | 'vxi11-cli = vxi11.cli:main', 49 | ], 50 | }, 51 | ) 52 | 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-ivi/python-vxi11/cc4671da699f1f379137dc40ffc4a302d72e6f55/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_vxi11.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import nose 4 | from nose.tools import eq_ 5 | from vxi11.vxi11 import parse_visa_resource_string 6 | 7 | def test_parse_visa_resource_string(): 8 | f = parse_visa_resource_string 9 | 10 | res = f('TCPIP::10.0.0.1::INSTR') 11 | eq_(res['type'], 'TCPIP') 12 | eq_(res['prefix'], 'TCPIP') 13 | eq_(res['arg1'], '10.0.0.1') 14 | eq_(res['suffix'], 'INSTR') 15 | 16 | res = f('TCPIP0::10.0.0.1::INSTR') 17 | eq_(res['type'], 'TCPIP') 18 | eq_(res['prefix'], 'TCPIP0') 19 | eq_(res['arg1'], '10.0.0.1') 20 | eq_(res['suffix'], 'INSTR') 21 | 22 | res = f('TCPIP::10.0.0.1::gpib,5::INSTR') 23 | eq_(res['type'], 'TCPIP') 24 | eq_(res['prefix'], 'TCPIP') 25 | eq_(res['arg1'], '10.0.0.1') 26 | eq_(res['suffix'], 'INSTR') 27 | 28 | res = f('TCPIP0::10.0.0.1::gpib,5::INSTR') 29 | eq_(res['type'], 'TCPIP') 30 | eq_(res['prefix'], 'TCPIP0') 31 | eq_(res['arg1'], '10.0.0.1') 32 | eq_(res['arg2'], 'gpib,5') 33 | eq_(res['suffix'], 'INSTR') 34 | 35 | res = f('TCPIP0::10.0.0.1::usb0::INSTR') 36 | eq_(res['type'], 'TCPIP') 37 | eq_(res['prefix'], 'TCPIP0') 38 | eq_(res['arg1'], '10.0.0.1') 39 | eq_(res['arg2'], 'usb0') 40 | eq_(res['suffix'], 'INSTR') 41 | 42 | res = f('TCPIP0::10.0.0.1::usb0[1234::5678::MYSERIAL::0]::INSTR') 43 | eq_(res['type'], 'TCPIP') 44 | eq_(res['prefix'], 'TCPIP0') 45 | eq_(res['arg1'], '10.0.0.1') 46 | eq_(res['arg2'], 'usb0[1234::5678::MYSERIAL::0]') 47 | eq_(res['suffix'], 'INSTR') 48 | 49 | -------------------------------------------------------------------------------- /vxi11/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Python VXI-11 driver 4 | 5 | Copyright (c) 2012-2017 Alex Forencich and Michael Walle 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 15 | all 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 23 | THE SOFTWARE. 24 | 25 | """ 26 | 27 | __all__ = ["vxi11"] 28 | 29 | from .version import __version__ 30 | from .vxi11 import Instrument, InterfaceDevice, list_devices, list_resources 31 | -------------------------------------------------------------------------------- /vxi11/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Simple VXI-11 commandline interface 4 | # 5 | # Copyright (c) 2014-2017 Michael Walle 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 15 | # all 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 23 | # THE SOFTWARE. 24 | # 25 | # Description: 26 | # Commands are sent to the VXI-11 device after every newline. If the command 27 | # ends in '?' the response is received. 28 | # 29 | 30 | import time 31 | import sys 32 | import logging 33 | import readline 34 | from optparse import OptionParser 35 | 36 | from . import __version__ 37 | from .vxi11 import Instrument, Vxi11Exception 38 | 39 | try: input = raw_input 40 | except NameError: pass 41 | 42 | LOCAL_COMMANDS = { 43 | '%SLEEP': (1, 1, lambda a: time.sleep(float(a[0])/1000)), 44 | } 45 | 46 | def process_local_command(cmd): 47 | args = cmd.split() 48 | if args[0] in LOCAL_COMMANDS: 49 | cmd_info = LOCAL_COMMANDS[args[0]] 50 | if cmd_info[0] <= len(args[1:]) <= cmd_info[1]: 51 | cmd_info[2](args[1:]) 52 | else: 53 | print('Invalid number of arguments for command %s' % args[0]) 54 | else: 55 | print('Unknown command "%s"' % cmd) 56 | 57 | def main(): 58 | usage = 'usage: %prog [options] []' 59 | parser = OptionParser(usage=usage) 60 | parser.add_option('-d', action='store_true', dest='debug', 61 | help='enable debug messages') 62 | parser.add_option('-v', action='store_true', dest='verbose', 63 | help='be more verbose') 64 | parser.add_option('-V', action='store_true', dest='version', 65 | help='show version') 66 | parser.add_option('--always-check-esr', action='store_true', 67 | dest='check_esr', 68 | help='Check the error status register after every command') 69 | 70 | (options, args) = parser.parse_args() 71 | 72 | if options.version: 73 | print('vxi11-cli v%s' % (__version__,)) 74 | sys.exit(0) 75 | 76 | logging.basicConfig() 77 | if options.verbose: 78 | logging.getLogger('vxi11').setLevel(logging.INFO) 79 | if options.debug: 80 | logging.getLogger('vxi11').setLevel(logging.DEBUG) 81 | 82 | if len(args) < 1: 83 | print(parser.format_help()) 84 | sys.exit(1) 85 | 86 | host = args[0] 87 | name = None 88 | if len(args) > 1: 89 | name = args[1] 90 | 91 | v = Instrument(host, name) 92 | v.open() 93 | 94 | print("Enter command to send. Quit with 'q'. Read with '?'.") 95 | try: 96 | while True: 97 | cmd = input('=> ') 98 | if cmd == 'q': 99 | break 100 | if cmd.startswith('%'): 101 | process_local_command(cmd) 102 | continue 103 | if len(cmd) > 0: 104 | is_query = cmd.split(' ')[0][-1] == '?' 105 | try: 106 | if is_query: 107 | if len(cmd) > 1: 108 | v.write(cmd) 109 | print(v.read()) 110 | else: 111 | v.write(cmd) 112 | if options.check_esr: 113 | esr = int(v.ask('*ESR?').strip()) 114 | if esr != 0: 115 | print('Warning: ESR was %d' % esr) 116 | except Vxi11Exception: 117 | e = sys.exc_info()[1] 118 | print('ERROR: %s' % e) 119 | except EOFError: 120 | print('exiting...') 121 | 122 | v.close() 123 | 124 | if __name__ == '__main__': 125 | main() 126 | -------------------------------------------------------------------------------- /vxi11/rpc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sun RPC version 2 -- RFC1057 3 | 4 | This file is drawn from Python's RPC demo, updated for python 3. There 5 | doesn't seem to be an original author or license associated the original 6 | file. 7 | 8 | XXX There should be separate exceptions for the various reasons why 9 | XXX an RPC can fail, rather than using RuntimeError for everything 10 | 11 | XXX Need to use class based exceptions rather than string exceptions 12 | 13 | XXX The UDP version of the protocol resends requests when it does 14 | XXX not receive a timely reply -- use only for idempotent calls! 15 | 16 | XXX There is no provision for call timeout on TCP connections 17 | 18 | Original source: http://svn.python.org/projects/python/trunk/Demo/rpc/rpc.py 19 | 20 | """ 21 | 22 | import xdrlib 23 | import socket 24 | import os 25 | import struct 26 | 27 | RPCVERSION = 2 28 | 29 | CALL = 0 30 | REPLY = 1 31 | 32 | AUTH_NULL = 0 33 | AUTH_UNIX = 1 34 | AUTH_SHORT = 2 35 | AUTH_DES = 3 36 | 37 | MSG_ACCEPTED = 0 38 | MSG_DENIED = 1 39 | 40 | SUCCESS = 0 # RPC executed successfully 41 | PROG_UNAVAIL = 1 # remote hasn't exported program 42 | PROG_MISMATCH = 2 # remote can't support version # 43 | PROC_UNAVAIL = 3 # program can't support procedure 44 | GARBAGE_ARGS = 4 # procedure can't decode params 45 | 46 | RPC_MISMATCH = 0 # RPC version number != 2 47 | AUTH_ERROR = 1 # remote can't authenticate caller 48 | 49 | AUTH_BADCRED = 1 # bad credentials (seal broken) 50 | AUTH_REJECTEDCRED = 2 # client must begin new session 51 | AUTH_BADVERF = 3 # bad verifier (seal broken) 52 | AUTH_REJECTEDVERF = 4 # verifier expired or replayed 53 | AUTH_TOOWEAK = 5 # rejected for security reasons 54 | 55 | # Exceptions 56 | class RPCError(Exception): pass 57 | class RPCBadFormat(RPCError): pass 58 | class RPCBadVersion(RPCError): pass 59 | class RPCGarbageArgs(RPCError): pass 60 | class RPCUnpackError(RPCError): pass 61 | 62 | def make_auth_null(): 63 | return b'' 64 | 65 | class Packer(xdrlib.Packer): 66 | 67 | def pack_auth(self, auth): 68 | flavor, stuff = auth 69 | self.pack_enum(flavor) 70 | self.pack_opaque(stuff) 71 | 72 | def pack_auth_unix(self, stamp, machinename, uid, gid, gids): 73 | self.pack_uint(stamp) 74 | self.pack_string(machinename) 75 | self.pack_uint(uid) 76 | self.pack_uint(gid) 77 | self.pack_uint(len(gids)) 78 | for i in gids: 79 | self.pack_uint(i) 80 | 81 | def pack_callheader(self, xid, prog, vers, proc, cred, verf): 82 | self.pack_uint(xid) 83 | self.pack_enum(CALL) 84 | self.pack_uint(RPCVERSION) 85 | self.pack_uint(prog) 86 | self.pack_uint(vers) 87 | self.pack_uint(proc) 88 | self.pack_auth(cred) 89 | self.pack_auth(verf) 90 | # Caller must add procedure-specific part of call 91 | 92 | def pack_replyheader(self, xid, verf): 93 | self.pack_uint(xid) 94 | self.pack_enum(REPLY) 95 | self.pack_uint(MSG_ACCEPTED) 96 | self.pack_auth(verf) 97 | self.pack_enum(SUCCESS) 98 | # Caller must add procedure-specific part of reply 99 | 100 | class Unpacker(xdrlib.Unpacker): 101 | 102 | def unpack_auth(self): 103 | flavor = self.unpack_enum() 104 | stuff = self.unpack_opaque() 105 | return (flavor, stuff) 106 | 107 | def unpack_callheader(self): 108 | xid = self.unpack_uint() 109 | temp = self.unpack_enum() 110 | if temp != CALL: 111 | raise RPCBadFormat('no CALL but %r' % (temp,)) 112 | temp = self.unpack_uint() 113 | if temp != RPCVERSION: 114 | raise RPCBadVersion('bad RPC version %r' % (temp,)) 115 | prog = self.unpack_uint() 116 | vers = self.unpack_uint() 117 | proc = self.unpack_uint() 118 | cred = self.unpack_auth() 119 | verf = self.unpack_auth() 120 | return xid, prog, vers, proc, cred, verf 121 | # Caller must add procedure-specific part of call 122 | 123 | def unpack_replyheader(self): 124 | xid = self.unpack_uint() 125 | mtype = self.unpack_enum() 126 | if mtype != REPLY: 127 | raise RPCUnpackError('no REPLY but %r' % (mtype,)) 128 | stat = self.unpack_enum() 129 | if stat == MSG_DENIED: 130 | stat = self.unpack_enum() 131 | if stat == RPC_MISMATCH: 132 | low = self.unpack_uint() 133 | high = self.unpack_uint() 134 | raise RPCUnpackError('MSG_DENIED: RPC_MISMATCH: %r' % ((low, high),)) 135 | if stat == AUTH_ERROR: 136 | stat = self.unpack_uint() 137 | raise RPCUnpackError('MSG_DENIED: AUTH_ERROR: %r' % (stat,)) 138 | raise RPCUnpackError('MSG_DENIED: %r' % (stat,)) 139 | if stat != MSG_ACCEPTED: 140 | raise RPCUnpackError('Neither MSG_DENIED nor MSG_ACCEPTED: %r' % (stat,)) 141 | verf = self.unpack_auth() 142 | stat = self.unpack_enum() 143 | if stat == PROG_UNAVAIL: 144 | raise RPCUnpackError('call failed: PROG_UNAVAIL') 145 | if stat == PROG_MISMATCH: 146 | low = self.unpack_uint() 147 | high = self.unpack_uint() 148 | raise RPCUnpackError('call failed: PROG_MISMATCH: %r' % ((low, high),)) 149 | if stat == PROC_UNAVAIL: 150 | raise RPCUnpackError('call failed: PROC_UNAVAIL') 151 | if stat == GARBAGE_ARGS: 152 | raise RPCGarbageArgs 153 | if stat != SUCCESS: 154 | raise RPCUnpackError('call failed: %r' % (stat,)) 155 | return xid, verf 156 | # Caller must get procedure-specific part of reply 157 | 158 | # Common base class for clients 159 | 160 | class Client: 161 | 162 | def __init__(self, host, prog, vers, port): 163 | self.host = host 164 | self.prog = prog 165 | self.vers = vers 166 | self.port = port 167 | self.lastxid = 0 # XXX should be more random? 168 | self.cred = None 169 | self.verf = None 170 | 171 | def make_call(self, proc, args, pack_func, unpack_func): 172 | # Don't normally override this (but see Broadcast) 173 | if pack_func is None and args is not None: 174 | raise TypeError('non-null args with null pack_func') 175 | self.start_call(proc) 176 | if pack_func: 177 | pack_func(args) 178 | self.do_call() 179 | if unpack_func: 180 | result = unpack_func() 181 | else: 182 | result = None 183 | self.unpacker.done() 184 | return result 185 | 186 | def start_call(self, proc): 187 | # Don't override this 188 | self.lastxid = xid = self.lastxid + 1 189 | cred = self.mkcred() 190 | verf = self.mkverf() 191 | p = self.packer 192 | p.reset() 193 | p.pack_callheader(xid, self.prog, self.vers, proc, cred, verf) 194 | 195 | def do_call(self): 196 | # This MUST be overridden 197 | raise RPCError('do_call not defined') 198 | 199 | def mkcred(self): 200 | # Override this to use more powerful credentials 201 | if self.cred is None: 202 | self.cred = (AUTH_NULL, make_auth_null()) 203 | return self.cred 204 | 205 | def mkverf(self): 206 | # Override this to use a more powerful verifier 207 | if self.verf is None: 208 | self.verf = (AUTH_NULL, make_auth_null()) 209 | return self.verf 210 | 211 | def call_0(self): 212 | # Procedure 0 is always like this 213 | return self.make_call(0, None, None, None) 214 | 215 | 216 | # Record-Marking standard support 217 | 218 | def sendfrag(sock, last, frag): 219 | x = len(frag) 220 | if last: x = x | 0x80000000 221 | header = struct.pack(">I", x) 222 | sock.sendall(header + frag) 223 | 224 | def sendrecord(sock, record): 225 | if len(record) > 0: 226 | sendfrag(sock, 1, record) 227 | 228 | def recvfrag(sock): 229 | header = sock.recv(4) 230 | if len(header) < 4: 231 | raise EOFError 232 | x = struct.unpack(">I", header[0:4])[0] 233 | last = ((x & 0x80000000) != 0) 234 | n = int(x & 0x7fffffff) 235 | frag = bytearray() 236 | while len(frag) < n: 237 | buf = sock.recv(n - len(frag)) 238 | if not buf: raise EOFError 239 | frag.extend(buf) 240 | return last, frag 241 | 242 | def recvrecord(sock): 243 | record = bytearray() 244 | last = 0 245 | while not last: 246 | last, frag = recvfrag(sock) 247 | record.extend(frag) 248 | return bytes(record) 249 | 250 | 251 | # Client using TCP to a specific port 252 | 253 | class RawTCPClient(Client): 254 | def __init__(self, host, prog, vers, port): 255 | Client.__init__(self, host, prog, vers, port) 256 | self.connect() 257 | 258 | def connect(self): 259 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 260 | self.sock.connect((self.host, self.port)) 261 | 262 | def close(self): 263 | self.sock.close() 264 | 265 | def do_call(self): 266 | call = self.packer.get_buf() 267 | sendrecord(self.sock, call) 268 | while True: 269 | reply = recvrecord(self.sock) 270 | u = self.unpacker 271 | u.reset(reply) 272 | xid, verf = u.unpack_replyheader() 273 | if xid == self.lastxid: 274 | # xid matches, we're done 275 | return 276 | elif xid < self.lastxid: 277 | # Stale data in buffer due to interruption 278 | # Discard and fetch another record 279 | continue 280 | else: 281 | # xid larger than expected - packet from the future? 282 | raise RPCError('wrong xid in reply %r instead of %r' % (xid, self.lastxid)) 283 | 284 | 285 | # Client using UDP to a specific port 286 | 287 | class RawUDPClient(Client): 288 | def __init__(self, host, prog, vers, port): 289 | Client.__init__(self, host, prog, vers, port) 290 | self.connect() 291 | 292 | def connect(self): 293 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 294 | self.sock.connect((self.host, self.port)) 295 | 296 | def close(self): 297 | self.sock.close() 298 | 299 | def do_call(self): 300 | call = self.packer.get_buf() 301 | self.sock.send(call) 302 | try: 303 | from select import select 304 | except ImportError: 305 | print('WARNING: select not found, RPC may hang') 306 | select = None 307 | BUFSIZE = 8192 # Max UDP buffer size 308 | timeout = 1 309 | count = 5 310 | while 1: 311 | r, w, x = [self.sock], [], [] 312 | if select: 313 | r, w, x = select(r, w, x, timeout) 314 | if self.sock not in r: 315 | count = count - 1 316 | if count < 0: raise RPCError('timeout') 317 | if timeout < 25: timeout = timeout *2 318 | ## print 'RESEND', timeout, count 319 | self.sock.send(call) 320 | continue 321 | reply = self.sock.recv(BUFSIZE) 322 | u = self.unpacker 323 | u.reset(reply) 324 | xid, verf = u.unpack_replyheader() 325 | if xid != self.lastxid: 326 | ## print 'BAD xid' 327 | continue 328 | break 329 | 330 | 331 | # Client using UDP broadcast to a specific port 332 | 333 | class RawBroadcastUDPClient(RawUDPClient): 334 | 335 | def __init__(self, bcastaddr, prog, vers, port): 336 | RawUDPClient.__init__(self, bcastaddr, prog, vers, port) 337 | self.reply_handler = None 338 | self.timeout = 30 339 | 340 | def connect(self): 341 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 342 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 343 | 344 | def set_reply_handler(self, reply_handler): 345 | self.reply_handler = reply_handler 346 | 347 | def set_timeout(self, timeout): 348 | self.timeout = timeout # Use None for infinite timeout 349 | 350 | def make_call(self, proc, args, pack_func, unpack_func): 351 | if pack_func is None and args is not None: 352 | raise TypeError('non-null args with null pack_func') 353 | self.start_call(proc) 354 | if pack_func: 355 | pack_func(args) 356 | call = self.packer.get_buf() 357 | self.sock.sendto(call, (self.host, self.port)) 358 | try: 359 | from select import select 360 | except ImportError: 361 | print('WARNING: select not found, broadcast will hang') 362 | select = None 363 | BUFSIZE = 8192 # Max UDP buffer size (for reply) 364 | replies = [] 365 | if unpack_func is None: 366 | def dummy(): pass 367 | unpack_func = dummy 368 | while 1: 369 | r, w, x = [self.sock], [], [] 370 | if select: 371 | if self.timeout is None: 372 | r, w, x = select(r, w, x) 373 | else: 374 | r, w, x = select(r, w, x, self.timeout) 375 | if self.sock not in r: 376 | break 377 | reply, fromaddr = self.sock.recvfrom(BUFSIZE) 378 | u = self.unpacker 379 | u.reset(reply) 380 | xid, verf = u.unpack_replyheader() 381 | if xid != self.lastxid: 382 | ## print 'BAD xid' 383 | continue 384 | reply = unpack_func() 385 | self.unpacker.done() 386 | replies.append((reply, fromaddr)) 387 | if self.reply_handler: 388 | self.reply_handler(reply, fromaddr) 389 | return replies 390 | 391 | 392 | # Port mapper interface 393 | 394 | # Program number, version and (fixed!) port number 395 | PMAP_PROG = 100000 396 | PMAP_VERS = 2 397 | PMAP_PORT = 111 398 | 399 | # Procedure numbers 400 | PMAPPROC_NULL = 0 # (void) -> void 401 | PMAPPROC_SET = 1 # (mapping) -> bool 402 | PMAPPROC_UNSET = 2 # (mapping) -> bool 403 | PMAPPROC_GETPORT = 3 # (mapping) -> unsigned int 404 | PMAPPROC_DUMP = 4 # (void) -> pmaplist 405 | PMAPPROC_CALLIT = 5 # (call_args) -> call_result 406 | 407 | # A mapping is (prog, vers, prot, port) and prot is one of: 408 | 409 | IPPROTO_TCP = 6 410 | IPPROTO_UDP = 17 411 | 412 | # A pmaplist is a variable-length list of mappings, as follows: 413 | # either (1, mapping, pmaplist) or (0). 414 | 415 | # A call_args is (prog, vers, proc, args) where args is opaque; 416 | # a call_result is (port, res) where res is opaque. 417 | 418 | 419 | class PortMapperPacker(Packer): 420 | 421 | def pack_mapping(self, mapping): 422 | prog, vers, prot, port = mapping 423 | self.pack_uint(prog) 424 | self.pack_uint(vers) 425 | self.pack_uint(prot) 426 | self.pack_uint(port) 427 | 428 | def pack_pmaplist(self, list): 429 | self.pack_list(list, self.pack_mapping) 430 | 431 | def pack_call_args(self, ca): 432 | prog, vers, proc, args = ca 433 | self.pack_uint(prog) 434 | self.pack_uint(vers) 435 | self.pack_uint(proc) 436 | self.pack_opaque(args) 437 | 438 | 439 | class PortMapperUnpacker(Unpacker): 440 | 441 | def unpack_mapping(self): 442 | prog = self.unpack_uint() 443 | vers = self.unpack_uint() 444 | prot = self.unpack_uint() 445 | port = self.unpack_uint() 446 | return prog, vers, prot, port 447 | 448 | def unpack_pmaplist(self): 449 | return self.unpack_list(self.unpack_mapping) 450 | 451 | def unpack_call_result(self): 452 | port = self.unpack_uint() 453 | res = self.unpack_opaque() 454 | return port, res 455 | 456 | 457 | class PartialPortMapperClient: 458 | 459 | def __init__(self): 460 | self.packer = PortMapperPacker() 461 | self.unpacker = PortMapperUnpacker('') 462 | 463 | def set(self, mapping): 464 | return self.make_call(PMAPPROC_SET, mapping, \ 465 | self.packer.pack_mapping, \ 466 | self.unpacker.unpack_uint) 467 | 468 | def unset(self, mapping): 469 | return self.make_call(PMAPPROC_UNSET, mapping, \ 470 | self.packer.pack_mapping, \ 471 | self.unpacker.unpack_uint) 472 | 473 | def get_port(self, mapping): 474 | return self.make_call(PMAPPROC_GETPORT, mapping, \ 475 | self.packer.pack_mapping, \ 476 | self.unpacker.unpack_uint) 477 | 478 | def dump(self): 479 | return self.make_call(PMAPPROC_DUMP, None, \ 480 | None, \ 481 | self.unpacker.unpack_pmaplist) 482 | 483 | def callit(self, ca): 484 | return self.make_call(PMAPPROC_CALLIT, ca, \ 485 | self.packer.pack_call_args, \ 486 | self.unpacker.unpack_call_result) 487 | 488 | 489 | class TCPPortMapperClient(PartialPortMapperClient, RawTCPClient): 490 | 491 | def __init__(self, host): 492 | RawTCPClient.__init__(self, host, PMAP_PROG, PMAP_VERS, PMAP_PORT) 493 | PartialPortMapperClient.__init__(self) 494 | 495 | 496 | class UDPPortMapperClient(PartialPortMapperClient, RawUDPClient): 497 | 498 | def __init__(self, host): 499 | RawUDPClient.__init__(self, host, PMAP_PROG, PMAP_VERS, PMAP_PORT) 500 | PartialPortMapperClient.__init__(self) 501 | 502 | 503 | class BroadcastUDPPortMapperClient(PartialPortMapperClient, RawBroadcastUDPClient): 504 | 505 | def __init__(self, bcastaddr): 506 | RawBroadcastUDPClient.__init__(self, bcastaddr, PMAP_PROG, PMAP_VERS, PMAP_PORT) 507 | PartialPortMapperClient.__init__(self) 508 | 509 | 510 | # Generic clients that find their server through the Port mapper 511 | 512 | class TCPClient(RawTCPClient): 513 | 514 | def __init__(self, host, prog, vers, port=0): 515 | if port == 0: 516 | pmap = TCPPortMapperClient(host) 517 | port = pmap.get_port((prog, vers, IPPROTO_TCP, 0)) 518 | pmap.close() 519 | if port == 0: 520 | raise RPCError('program not registered') 521 | RawTCPClient.__init__(self, host, prog, vers, port) 522 | 523 | 524 | class UDPClient(RawUDPClient): 525 | 526 | def __init__(self, host, prog, vers, port=0): 527 | if port == 0: 528 | pmap = UDPPortMapperClient(host) 529 | port = pmap.get_port((prog, vers, IPPROTO_UDP, 0)) 530 | pmap.close() 531 | if port == 0: 532 | raise RPCError('program not registered') 533 | RawUDPClient.__init__(self, host, prog, vers, port) 534 | 535 | 536 | class BroadcastUDPClient(Client): 537 | 538 | def __init__(self, bcastaddr, prog, vers): 539 | self.pmap = BroadcastUDPPortMapperClient(bcastaddr) 540 | self.pmap.set_reply_handler(self.my_reply_handler) 541 | self.prog = prog 542 | self.vers = vers 543 | self.user_reply_handler = None 544 | self.addpackers() 545 | 546 | def close(self): 547 | self.pmap.close() 548 | 549 | def set_reply_handler(self, reply_handler): 550 | self.user_reply_handler = reply_handler 551 | 552 | def set_timeout(self, timeout): 553 | self.pmap.set_timeout(timeout) 554 | 555 | def my_reply_handler(self, reply, fromaddr): 556 | port, res = reply 557 | self.unpacker.reset(res) 558 | result = self.unpack_func() 559 | self.unpacker.done() 560 | self.replies.append((result, fromaddr)) 561 | if self.user_reply_handler is not None: 562 | self.user_reply_handler(result, fromaddr) 563 | 564 | def make_call(self, proc, args, pack_func, unpack_func): 565 | self.packer.reset() 566 | if pack_func: 567 | pack_func(args) 568 | if unpack_func is None: 569 | def dummy(): pass 570 | self.unpack_func = dummy 571 | else: 572 | self.unpack_func = unpack_func 573 | self.replies = [] 574 | packed_args = self.packer.get_buf() 575 | dummy_replies = self.pmap.Callit( \ 576 | (self.prog, self.vers, proc, packed_args)) 577 | return self.replies 578 | 579 | 580 | # Server classes 581 | 582 | # These are not symmetric to the Client classes 583 | # XXX No attempt is made to provide authorization hooks yet 584 | 585 | class Server: 586 | 587 | def __init__(self, host, prog, vers, port): 588 | self.host = host # Should normally be '' for default interface 589 | self.prog = prog 590 | self.vers = vers 591 | self.port = port # Should normally be 0 for random port 592 | self.registered = False 593 | self.addpackers() 594 | 595 | def __del__(self): 596 | # make sure to unregister on delete 597 | if self.registered: 598 | self.unregister() 599 | 600 | def register(self): 601 | mapping = self.prog, self.vers, self.prot, self.port 602 | p = TCPPortMapperClient(self.host) 603 | if not p.set(mapping): 604 | raise RPCError('register failed') 605 | self.registered = True 606 | 607 | def unregister(self): 608 | mapping = self.prog, self.vers, self.prot, self.port 609 | p = TCPPortMapperClient(self.host) 610 | if not p.unset(mapping): 611 | raise RPCError('unregister failed') 612 | self.registered = False 613 | 614 | def handle(self, call): 615 | # Don't use unpack_header but parse the header piecewise 616 | # XXX I have no idea if I am using the right error responses! 617 | self.unpacker.reset(call) 618 | self.packer.reset() 619 | xid = self.unpacker.unpack_uint() 620 | self.packer.pack_uint(xid) 621 | temp = self.unpacker.unpack_enum() 622 | if temp != CALL: 623 | return None # Not worthy of a reply 624 | self.packer.pack_uint(REPLY) 625 | temp = self.unpacker.unpack_uint() 626 | if temp != RPCVERSION: 627 | self.packer.pack_uint(MSG_DENIED) 628 | self.packer.pack_uint(RPC_MISMATCH) 629 | self.packer.pack_uint(RPCVERSION) 630 | self.packer.pack_uint(RPCVERSION) 631 | return self.packer.get_buf() 632 | self.packer.pack_uint(MSG_ACCEPTED) 633 | self.packer.pack_auth((AUTH_NULL, make_auth_null())) 634 | prog = self.unpacker.unpack_uint() 635 | if prog != self.prog: 636 | self.packer.pack_uint(PROG_UNAVAIL) 637 | return self.packer.get_buf() 638 | vers = self.unpacker.unpack_uint() 639 | if vers != self.vers: 640 | self.packer.pack_uint(PROG_MISMATCH) 641 | self.packer.pack_uint(self.vers) 642 | self.packer.pack_uint(self.vers) 643 | return self.packer.get_buf() 644 | proc = self.unpacker.unpack_uint() 645 | methname = 'handle_' + repr(proc) 646 | try: 647 | meth = getattr(self, methname) 648 | except AttributeError: 649 | self.packer.pack_uint(PROC_UNAVAIL) 650 | return self.packer.get_buf() 651 | cred = self.unpacker.unpack_auth() 652 | verf = self.unpacker.unpack_auth() 653 | try: 654 | meth() # Unpack args, call turn_around(), pack reply 655 | except (EOFError, RPCGarbageArgs): 656 | # Too few or too many arguments 657 | self.packer.reset() 658 | self.packer.pack_uint(xid) 659 | self.packer.pack_uint(REPLY) 660 | self.packer.pack_uint(MSG_ACCEPTED) 661 | self.packer.pack_auth((AUTH_NULL, make_auth_null())) 662 | self.packer.pack_uint(GARBAGE_ARGS) 663 | return self.packer.get_buf() 664 | 665 | def turn_around(self): 666 | try: 667 | self.unpacker.done() 668 | except RuntimeError: 669 | raise RPCGarbageArgs 670 | self.packer.pack_uint(SUCCESS) 671 | 672 | def handle_0(self): # Handle NULL message 673 | self.turn_around() 674 | 675 | def addpackers(self): 676 | # Override this to use derived classes from Packer/Unpacker 677 | self.packer = Packer() 678 | self.unpacker = Unpacker('') 679 | 680 | 681 | class TCPServer(Server): 682 | 683 | def __init__(self, host, prog, vers, port): 684 | Server.__init__(self, host, prog, vers, port) 685 | self.connect() 686 | 687 | def connect(self): 688 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 689 | self.prot = IPPROTO_TCP 690 | self.sock.bind((self.host, self.port)) 691 | self.host, self.port = self.sock.getsockname() 692 | 693 | def loop(self): 694 | self.sock.listen(0) 695 | while 1: 696 | self.session(self.sock.accept()) 697 | 698 | def session(self, connection): 699 | sock, (host, port) = connection 700 | while 1: 701 | try: 702 | call = recvrecord(sock) 703 | except EOFError: 704 | break 705 | except socket.error: 706 | print('socket error:', sys.exc_info()[0]) 707 | break 708 | reply = self.handle(call) 709 | if reply is not None: 710 | sendrecord(sock, reply) 711 | 712 | def forkingloop(self): 713 | # Like loop but uses forksession() 714 | self.sock.listen(0) 715 | while 1: 716 | self.forksession(self.sock.accept()) 717 | 718 | def forksession(self, connection): 719 | # Like session but forks off a subprocess 720 | import os 721 | # Wait for deceased children 722 | try: 723 | while 1: 724 | pid, sts = os.waitpid(0, os.WNOHANG) 725 | except os.error: 726 | pass 727 | pid = None 728 | try: 729 | pid = os.fork() 730 | if pid: # Parent 731 | connection[0].close() 732 | return 733 | # Child 734 | self.session(connection) 735 | finally: 736 | # Make sure we don't fall through in the parent 737 | if pid == 0: 738 | os._exit(0) 739 | 740 | 741 | class UDPServer(Server): 742 | 743 | def __init__(self, host, prog, vers, port): 744 | Server.__init__(self, host, prog, vers, port) 745 | self.connect() 746 | 747 | def connect(self): 748 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 749 | self.prot = IPPROTO_UDP 750 | self.sock.bind((self.host, self.port)) 751 | self.host, self.port = self.sock.getsockname() 752 | 753 | def loop(self): 754 | while 1: 755 | self.session() 756 | 757 | def session(self): 758 | call, host_port = self.sock.recvfrom(8192) 759 | reply = self.handle(call) 760 | if reply is not None: 761 | self.sock.sendto(reply, host_port) 762 | 763 | 764 | # Simple test program -- dump portmapper status 765 | 766 | def test(host = ''): 767 | pmap = UDPPortMapperClient(host) 768 | list = pmap.dump() 769 | list.sort() 770 | for prog, vers, prot, port in list: 771 | st = "%d %d " % (prog, vers) 772 | if prot == IPPROTO_TCP: st += "tcp " 773 | elif prot == IPPROTO_UDP: st += "udp " 774 | else: st += "%d " % prot 775 | st += "%d" % port 776 | print(st) 777 | 778 | 779 | -------------------------------------------------------------------------------- /vxi11/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.9' 2 | -------------------------------------------------------------------------------- /vxi11/vxi11.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Python VXI-11 driver 4 | 5 | Copyright (c) 2012-2017 Alex Forencich and Michael Walle 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 15 | all 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 23 | THE SOFTWARE. 24 | 25 | """ 26 | 27 | from . import rpc 28 | import random 29 | import re 30 | import struct 31 | import time 32 | 33 | # VXI-11 RPC constants 34 | 35 | # Device async 36 | DEVICE_ASYNC_PROG = 0x0607b0 37 | DEVICE_ASYNC_VERS = 1 38 | DEVICE_ABORT = 1 39 | 40 | # Device core 41 | DEVICE_CORE_PROG = 0x0607af 42 | DEVICE_CORE_VERS = 1 43 | CREATE_LINK = 10 44 | DEVICE_WRITE = 11 45 | DEVICE_READ = 12 46 | DEVICE_READSTB = 13 47 | DEVICE_TRIGGER = 14 48 | DEVICE_CLEAR = 15 49 | DEVICE_REMOTE = 16 50 | DEVICE_LOCAL = 17 51 | DEVICE_LOCK = 18 52 | DEVICE_UNLOCK = 19 53 | DEVICE_ENABLE_SRQ = 20 54 | DEVICE_DOCMD = 22 55 | DESTROY_LINK = 23 56 | CREATE_INTR_CHAN = 25 57 | DESTROY_INTR_CHAN = 26 58 | 59 | # Device intr 60 | DEVICE_INTR_PROG = 0x0607b1 61 | DEVICE_INTR_VERS = 1 62 | DEVICE_INTR_SRQ = 30 63 | 64 | # Error states 65 | ERR_NO_ERROR = 0 66 | ERR_SYNTAX_ERROR = 1 67 | ERR_DEVICE_NOT_ACCESSIBLE = 3 68 | ERR_INVALID_LINK_IDENTIFIER = 4 69 | ERR_PARAMETER_ERROR = 5 70 | ERR_CHANNEL_NOT_ESTABLISHED = 6 71 | ERR_OPERATION_NOT_SUPPORTED = 8 72 | ERR_OUT_OF_RESOURCES = 9 73 | ERR_DEVICE_LOCKED_BY_ANOTHER_LINK = 11 74 | ERR_NO_LOCK_HELD_BY_THIS_LINK = 12 75 | ERR_IO_TIMEOUT = 15 76 | ERR_IO_ERROR = 17 77 | ERR_INVALID_ADDRESS = 21 78 | ERR_ABORT = 23 79 | ERR_CHANNEL_ALREADY_ESTABLISHED = 29 80 | 81 | # Flags 82 | OP_FLAG_WAIT_BLOCK = 1 83 | OP_FLAG_END = 8 84 | OP_FLAG_TERMCHAR_SET = 128 85 | 86 | RX_REQCNT = 1 87 | RX_CHR = 2 88 | RX_END = 4 89 | 90 | # IEEE 488.1 interface device commands 91 | CMD_SEND_COMMAND = 0x020000 92 | CMD_BUS_STATUS = 0x020001 93 | CMD_ATN_CTRL = 0x020002 94 | CMD_REN_CTRL = 0x020003 95 | CMD_PASS_CTRL = 0x020004 96 | CMD_BUS_ADDRESS = 0x02000A 97 | CMD_IFC_CTRL = 0x020010 98 | 99 | CMD_BUS_STATUS_REMOTE = 1 100 | CMD_BUS_STATUS_SRQ = 2 101 | CMD_BUS_STATUS_NDAC = 3 102 | CMD_BUS_STATUS_SYSTEM_CONTROLLER = 4 103 | CMD_BUS_STATUS_CONTROLLER_IN_CHARGE = 5 104 | CMD_BUS_STATUS_TALKER = 6 105 | CMD_BUS_STATUS_LISTENER = 7 106 | CMD_BUS_STATUS_BUS_ADDRESS = 8 107 | 108 | GPIB_CMD_GTL = 0x01 # go to local 109 | GPIB_CMD_SDC = 0x04 # selected device clear 110 | GPIB_CMD_PPC = 0x05 # parallel poll config 111 | GPIB_CMD_GET = 0x08 # group execute trigger 112 | GPIB_CMD_TCT = 0x09 # take control 113 | GPIB_CMD_LLO = 0x11 # local lockout 114 | GPIB_CMD_DCL = 0x14 # device clear 115 | GPIB_CMD_PPU = 0x15 # parallel poll unconfigure 116 | GPIB_CMD_SPE = 0x18 # serial poll enable 117 | GPIB_CMD_SPD = 0x19 # serial poll disable 118 | GPIB_CMD_LAD = 0x20 # listen address (base) 119 | GPIB_CMD_UNL = 0x3F # unlisten 120 | GPIB_CMD_TAD = 0x40 # talk address (base) 121 | GPIB_CMD_UNT = 0x5F # untalk 122 | GPIB_CMD_SAD = 0x60 # my secondary address (base) 123 | GPIB_CMD_PPE = 0x60 # parallel poll enable (base) 124 | GPIB_CMD_PPD = 0x70 # parallel poll disable 125 | 126 | def parse_visa_resource_string(resource_string): 127 | # valid resource strings: 128 | # TCPIP::10.0.0.1::INSTR 129 | # TCPIP0::10.0.0.1::INSTR 130 | # TCPIP::10.0.0.1::gpib,5::INSTR 131 | # TCPIP0::10.0.0.1::gpib,5::INSTR 132 | # TCPIP0::10.0.0.1::usb0::INSTR 133 | # TCPIP0::10.0.0.1::usb0[1234::5678::MYSERIAL::0]::INSTR 134 | m = re.match('^(?P(?PTCPIP)\d*)(::(?P[^\s:]+))' 135 | '(::(?P[^\s:]+(\[.+\])?))?(::(?PINSTR))$', 136 | resource_string, re.I) 137 | 138 | if m is not None: 139 | return dict( 140 | type = m.group('type').upper(), 141 | prefix = m.group('prefix'), 142 | arg1 = m.group('arg1'), 143 | arg2 = m.group('arg2'), 144 | suffix = m.group('suffix'), 145 | ) 146 | 147 | # Exceptions 148 | class Vxi11Exception(Exception): 149 | em = {0: "No error", 150 | 1: "Syntax error", 151 | 3: "Device not accessible", 152 | 4: "Invalid link identifier", 153 | 5: "Parameter error", 154 | 6: "Channel not established", 155 | 8: "Operation not supported", 156 | 9: "Out of resources", 157 | 11: "Device locked by another link", 158 | 12: "No lock held by this link", 159 | 15: "IO timeout", 160 | 17: "IO error", 161 | 21: "Invalid address", 162 | 23: "Abort", 163 | 29: "Channel already established"} 164 | 165 | def __init__(self, err = None, note = None): 166 | self.err = err 167 | self.note = note 168 | self.msg = '' 169 | 170 | if err is None: 171 | self.msg = note 172 | else: 173 | if type(err) is int: 174 | if err in self.em: 175 | self.msg = "%d: %s" % (err, self.em[err]) 176 | else: 177 | self.msg = "%d: Unknown error" % err 178 | else: 179 | self.msg = err 180 | if note is not None: 181 | self.msg = "%s [%s]" % (self.msg, note) 182 | 183 | def __str__(self): 184 | return self.msg 185 | 186 | class Packer(rpc.Packer): 187 | def pack_device_link(self, link): 188 | self.pack_int(link) 189 | 190 | def pack_create_link_parms(self, params): 191 | id, lock_device, lock_timeout, device = params 192 | self.pack_int(id) 193 | self.pack_bool(lock_device) 194 | self.pack_uint(lock_timeout) 195 | self.pack_string(device) 196 | 197 | def pack_device_write_parms(self, params): 198 | link, timeout, lock_timeout, flags, data = params 199 | self.pack_int(link) 200 | self.pack_uint(timeout) 201 | self.pack_uint(lock_timeout) 202 | self.pack_int(flags) 203 | self.pack_opaque(data) 204 | 205 | def pack_device_read_parms(self, params): 206 | link, request_size, timeout, lock_timeout, flags, term_char = params 207 | self.pack_int(link) 208 | self.pack_uint(request_size) 209 | self.pack_uint(timeout) 210 | self.pack_uint(lock_timeout) 211 | self.pack_int(flags) 212 | self.pack_int(term_char) 213 | 214 | def pack_device_generic_parms(self, params): 215 | link, flags, lock_timeout, timeout = params 216 | self.pack_int(link) 217 | self.pack_int(flags) 218 | self.pack_uint(lock_timeout) 219 | self.pack_uint(timeout) 220 | 221 | def pack_device_remote_func_parms(self, params): 222 | host_addr, host_port, prog_num, prog_vers, prog_family = params 223 | self.pack_uint(host_addr) 224 | self.pack_uint(host_port) 225 | self.pack_uint(prog_num) 226 | self.pack_uint(prog_vers) 227 | self.pack_int(prog_family) 228 | 229 | def pack_device_enable_srq_parms(self, params): 230 | link, enable, handle = params 231 | self.pack_int(link) 232 | self.pack_bool(enable) 233 | if len(handle) > 40: 234 | raise Vxi11Exception("array length too long") 235 | self.pack_opaque(handle) 236 | 237 | def pack_device_lock_parms(self, params): 238 | link, flags, lock_timeout = params 239 | self.pack_int(link) 240 | self.pack_int(flags) 241 | self.pack_uint(lock_timeout) 242 | 243 | def pack_device_docmd_parms(self, params): 244 | link, flags, timeout, lock_timeout, cmd, network_order, datasize, data_in = params 245 | self.pack_int(link) 246 | self.pack_int(flags) 247 | self.pack_uint(timeout) 248 | self.pack_uint(lock_timeout) 249 | self.pack_int(cmd) 250 | self.pack_bool(network_order) 251 | self.pack_int(datasize) 252 | self.pack_opaque(data_in) 253 | 254 | def pack_device_error(self, error): 255 | self.pack_int(error) 256 | 257 | def pack_device_srq_parms(self, params): 258 | handle = params 259 | self.pack_opaque(handle) 260 | 261 | def pack_create_link_resp(self, params): 262 | error, link, abort_port, max_recv_size = params 263 | self.pack_int(error) 264 | self.pack_int(link) 265 | self.pack_uint(abort_port) 266 | self.pack_uint(max_recv_size) 267 | 268 | def pack_device_write_resp(self, params): 269 | error, size = params 270 | self.pack_int(error) 271 | self.pack_uint(size) 272 | 273 | def pack_device_read_resp(self, params): 274 | error, reason, data = params 275 | self.pack_int(error) 276 | self.pack_int(reason) 277 | self.pack_opaque(data) 278 | 279 | def pack_device_read_stb_resp(self, params): 280 | error, stb = params 281 | self.pack_int(error) 282 | self.pack_uint(stb) 283 | 284 | def pack_device_docmd_resp(self, params): 285 | error, data_out = params 286 | self.pack_int(error) 287 | self.pack_opaque(data_out) 288 | 289 | class Unpacker(rpc.Unpacker): 290 | def unpack_device_link(self): 291 | return self.unpack_int() 292 | 293 | def unpack_create_link_parms(self): 294 | id = self.unpack_int() 295 | lock_device = self.unpack_bool() 296 | lock_timeout = self.unpack_uint() 297 | device = self.unpack_string() 298 | return id, lock_device, lock_timeout, device 299 | 300 | def unpack_device_write_parms(self): 301 | link = self.unpack_int() 302 | timeout = self.unpack_uint() 303 | lock_timeout = self.unpack_uint() 304 | flags = self.unpack_int() 305 | data = self.unpack_opaque() 306 | return link, timeout, lock_timeout, flags, data 307 | 308 | def unpack_device_read_parms(self): 309 | link = self.unpack_int() 310 | request_size = self.unpack_uint() 311 | timeout = self.unpack_uint() 312 | lock_timeout = self.unpack_uint() 313 | flags = self.unpack_int() 314 | term_char = self.unpack_int() 315 | return link, request_size, timeout, lock_timeout, flags, term_char 316 | 317 | def unpack_device_generic_parms(self): 318 | link = self.unpack_int() 319 | flags = self.unpack_int() 320 | lock_timeout = self.unpack_uint() 321 | timeout = self.unpack_uint() 322 | return link, flags, lock_timeout, timeout 323 | 324 | def unpack_device_remote_func_parms(self): 325 | host_addr = self.unpack_uint() 326 | host_port = self.unpack_uint() 327 | prog_num = self.unpack_uint() 328 | prog_vers = self.unpack_uint() 329 | prog_family = self.unpack_int() 330 | return host_addr, host_port, prog_num, prog_vers, prog_family 331 | 332 | def unpack_device_enable_srq_parms(self): 333 | link = self.unpack_int() 334 | enable = self.unpack_bool() 335 | handle = self.unpack_opaque() 336 | return link, enable, handle 337 | 338 | def unpack_device_lock_parms(self): 339 | link = self.unpack_int() 340 | flags = self.unpack_int() 341 | lock_timeout = self.unpack_uint() 342 | return link, flags, lock_timeout 343 | 344 | def unpack_device_docmd_parms(self): 345 | link = self.unpack_int() 346 | flags = self.unpack_int() 347 | timeout = self.unpack_uint() 348 | lock_timeout = self.unpack_uint() 349 | cmd = self.unpack_int() 350 | network_order = self.unpack_bool() 351 | datasize = self.unpack_int() 352 | data_in = self.unpack_opaque() 353 | return link, flags, timeout, lock_timeout, cmd, network_order, datasize, data_in 354 | 355 | def unpack_device_error(self): 356 | return self.unpack_int() 357 | 358 | def unpack_device_srq_params(self): 359 | handle = self.unpack_opaque() 360 | return handle 361 | 362 | def unpack_create_link_resp(self): 363 | error = self.unpack_int() 364 | link = self.unpack_int() 365 | abort_port = self.unpack_uint() 366 | max_recv_size = self.unpack_uint() 367 | return error, link, abort_port, max_recv_size 368 | 369 | def unpack_device_write_resp(self): 370 | error = self.unpack_int() 371 | size = self.unpack_uint() 372 | return error, size 373 | 374 | def unpack_device_read_resp(self): 375 | error = self.unpack_int() 376 | reason = self.unpack_int() 377 | data = self.unpack_opaque() 378 | return error, reason, data 379 | 380 | def unpack_device_read_stb_resp(self): 381 | error = self.unpack_int() 382 | stb = self.unpack_uint() 383 | return error, stb 384 | 385 | def unpack_device_docmd_resp(self): 386 | error = self.unpack_int() 387 | data_out = self.unpack_opaque() 388 | return error, data_out 389 | 390 | def done(self): 391 | # ignore any trailing bytes 392 | pass 393 | 394 | 395 | class CoreClient(rpc.TCPClient): 396 | def __init__(self, host, port=0): 397 | self.packer = Packer() 398 | self.unpacker = Unpacker('') 399 | rpc.TCPClient.__init__(self, host, DEVICE_CORE_PROG, DEVICE_CORE_VERS, port) 400 | 401 | def create_link(self, id, lock_device, lock_timeout, name): 402 | params = (id, lock_device, lock_timeout, name) 403 | return self.make_call(CREATE_LINK, params, 404 | self.packer.pack_create_link_parms, 405 | self.unpacker.unpack_create_link_resp) 406 | 407 | def device_write(self, link, timeout, lock_timeout, flags, data): 408 | params = (link, timeout, lock_timeout, flags, data) 409 | return self.make_call(DEVICE_WRITE, params, 410 | self.packer.pack_device_write_parms, 411 | self.unpacker.unpack_device_write_resp) 412 | 413 | def device_read(self, link, request_size, timeout, lock_timeout, flags, term_char): 414 | params = (link, request_size, timeout, lock_timeout, flags, term_char) 415 | return self.make_call(DEVICE_READ, params, 416 | self.packer.pack_device_read_parms, 417 | self.unpacker.unpack_device_read_resp) 418 | 419 | def device_read_stb(self, link, flags, lock_timeout, timeout): 420 | params = (link, flags, lock_timeout, timeout) 421 | return self.make_call(DEVICE_READSTB, params, 422 | self.packer.pack_device_generic_parms, 423 | self.unpacker.unpack_device_read_stb_resp) 424 | 425 | def device_trigger(self, link, flags, lock_timeout, timeout): 426 | params = (link, flags, lock_timeout, timeout) 427 | return self.make_call(DEVICE_TRIGGER, params, 428 | self.packer.pack_device_generic_parms, 429 | self.unpacker.unpack_device_error) 430 | 431 | def device_clear(self, link, flags, lock_timeout, timeout): 432 | params = (link, flags, lock_timeout, timeout) 433 | return self.make_call(DEVICE_CLEAR, params, 434 | self.packer.pack_device_generic_parms, 435 | self.unpacker.unpack_device_error) 436 | 437 | def device_remote(self, link, flags, lock_timeout, timeout): 438 | params = (link, flags, lock_timeout, timeout) 439 | return self.make_call(DEVICE_REMOTE, params, 440 | self.packer.pack_device_generic_parms, 441 | self.unpacker.unpack_device_error) 442 | 443 | def device_local(self, link, flags, lock_timeout, timeout): 444 | params = (link, flags, lock_timeout, timeout) 445 | return self.make_call(DEVICE_LOCAL, params, 446 | self.packer.pack_device_generic_parms, 447 | self.unpacker.unpack_device_error) 448 | 449 | def device_lock(self, link, flags, lock_timeout): 450 | params = (link, flags, lock_timeout) 451 | return self.make_call(DEVICE_LOCK, params, 452 | self.packer.pack_device_lock_parms, 453 | self.unpacker.unpack_device_error) 454 | 455 | def device_unlock(self, link): 456 | return self.make_call(DEVICE_UNLOCK, link, 457 | self.packer.pack_device_link, 458 | self.unpacker.unpack_device_error) 459 | 460 | def device_enable_srq(self, link, enable, handle): 461 | params = (link, enable, handle) 462 | return self.make_call(DEVICE_ENABLE_SRQ, params, 463 | self.packer.pack_device_enable_srq_parms, 464 | self.unpacker.unpack_device_error) 465 | 466 | def device_docmd(self, link, flags, timeout, lock_timeout, cmd, network_order, datasize, data_in): 467 | params = (link, flags, timeout, lock_timeout, cmd, network_order, datasize, data_in) 468 | return self.make_call(DEVICE_DOCMD, params, 469 | self.packer.pack_device_docmd_parms, 470 | self.unpacker.unpack_device_docmd_resp) 471 | 472 | def destroy_link(self, link): 473 | return self.make_call(DESTROY_LINK, link, 474 | self.packer.pack_device_link, 475 | self.unpacker.unpack_device_error) 476 | 477 | def create_intr_chan(self, host_addr, host_port, prog_num, prog_vers, prog_family): 478 | params = (host_addr, host_port, prog_num, prog_vers, prog_family) 479 | return self.make_call(CREATE_INTR_CHAN, params, 480 | self.packer.pack_device_remote_func_parms, 481 | self.unpacker.unpack_device_error) 482 | 483 | def destroy_intr_chan(self): 484 | return self.make_call(DESTROY_INTR_CHAN, None, 485 | None, 486 | self.unpacker.unpack_device_error) 487 | 488 | 489 | class AbortClient(rpc.TCPClient): 490 | def __init__(self, host, port=0): 491 | self.packer = Packer() 492 | self.unpacker = Unpacker('') 493 | rpc.TCPClient.__init__(self, host, DEVICE_ASYNC_PROG, DEVICE_ASYNC_VERS, port) 494 | 495 | def device_abort(self, link): 496 | return self.make_call(DEVICE_ABORT, link, 497 | self.packer.pack_device_link, 498 | self.unpacker.unpack_device_error) 499 | 500 | 501 | def list_devices(ip=None, timeout=1): 502 | "Detect VXI-11 devices on network" 503 | 504 | if ip is None: 505 | ip = ['255.255.255.255'] 506 | 507 | if type(ip) is str: 508 | ip = [ip] 509 | 510 | hosts = [] 511 | 512 | for addr in ip: 513 | pmap = rpc.BroadcastUDPPortMapperClient(addr) 514 | pmap.set_timeout(timeout) 515 | resp = pmap.get_port((DEVICE_CORE_PROG, DEVICE_CORE_VERS, rpc.IPPROTO_TCP, 0)) 516 | 517 | l = [r[1][0] for r in resp if r[0] > 0] 518 | 519 | hosts.extend(l) 520 | 521 | return sorted(hosts, key=lambda ip: tuple(int(part) for part in ip.split('.'))) 522 | 523 | 524 | def list_resources(ip=None, timeout=1): 525 | "List resource strings for all detected VXI-11 devices" 526 | 527 | res = [] 528 | 529 | for host in list_devices(ip, timeout): 530 | try: 531 | # try connecting as an instrument 532 | instr = Instrument(host) 533 | instr.open() 534 | res.append("TCPIP::%s::INSTR" % host) 535 | except: 536 | try: 537 | # try connecting as a GPIB interface 538 | intf_dev = InterfaceDevice(host) 539 | # enumerate connected devices 540 | devs = intf_dev.find_listeners() 541 | res.extend(['TCPIP::%s::gpib0,%d::INSTR' % (host, d) for d in devs]) 542 | except: 543 | # if that fails, just list the host 544 | res.append("TCPIP::%s::INSTR" % host) 545 | 546 | return res 547 | 548 | 549 | class Device(object): 550 | "VXI-11 device interface client" 551 | def __init__(self, host, name = None, client_id = None, term_char = None): 552 | "Create new VXI-11 device object" 553 | 554 | if host.upper().startswith('TCPIP') and '::' in host: 555 | res = parse_visa_resource_string(host) 556 | 557 | if res is None: 558 | raise Vxi11Exception('Invalid resource string', 'init') 559 | 560 | host = res['arg1'] 561 | name = res['arg2'] 562 | 563 | if name is None: 564 | name = "inst0" 565 | 566 | if client_id is None: 567 | client_id = random.getrandbits(31) 568 | 569 | self.client = None 570 | self.abort_client = None 571 | 572 | self.host = host 573 | self.name = name 574 | self.client_id = client_id 575 | self.term_char = term_char 576 | self.lock_timeout = 10 577 | self.timeout = 10 578 | self.abort_port = 0 579 | self.link = None 580 | self.max_recv_size = 0 581 | self.max_read_len = 128*1024*1024 582 | self.locked = False 583 | 584 | def __del__(self): 585 | if self.link is not None: 586 | self.close() 587 | 588 | @property 589 | def timeout(self): 590 | return self._timeout 591 | 592 | @timeout.setter 593 | def timeout(self, val): 594 | self._timeout = val 595 | self._timeout_ms = int(val * 1000) 596 | if self.client is not None: 597 | self.client.sock.settimeout(self.timeout+1) 598 | if self.abort_client is not None: 599 | self.abort_client.sock.settimeout(self.timeout+1) 600 | 601 | @property 602 | def lock_timeout(self): 603 | return self._lock_timeout 604 | 605 | @lock_timeout.setter 606 | def lock_timeout(self, val): 607 | self._lock_timeout = val 608 | self._lock_timeout_ms = int(val * 1000) 609 | 610 | def open(self): 611 | "Open connection to VXI-11 device" 612 | if self.link is not None: 613 | return 614 | 615 | if self.client is None: 616 | self.client = CoreClient(self.host) 617 | 618 | self.client.sock.settimeout(self.timeout+1) 619 | error, link, abort_port, max_recv_size = self.client.create_link( 620 | self.client_id, 621 | 0, 622 | self._lock_timeout_ms, 623 | self.name.encode("utf-8") 624 | ) 625 | 626 | if error: 627 | raise Vxi11Exception(error, 'open') 628 | 629 | self.abort_port = abort_port 630 | 631 | self.link = link 632 | self.max_recv_size = min(max_recv_size, 1024*1024) 633 | 634 | def close(self): 635 | "Close connection" 636 | if self.link is None: 637 | return 638 | 639 | self.client.destroy_link(self.link) 640 | self.client.close() 641 | self.link = None 642 | self.client = None 643 | 644 | def abort(self): 645 | "Asynchronous abort" 646 | if self.link is None: 647 | self.open() 648 | 649 | if self.abort_client is None: 650 | self.abort_client = AbortClient(self.host, self.abort_port) 651 | self.abort_client.sock.settimeout(self.timeout) 652 | 653 | error = self.abort_client.device_abort(self.link) 654 | 655 | if error: 656 | raise Vxi11Exception(error, 'abort') 657 | 658 | def write_raw(self, data): 659 | "Write binary data to instrument" 660 | if self.link is None: 661 | self.open() 662 | 663 | if self.term_char is not None: 664 | flags = OP_FLAG_TERMCHAR_SET 665 | term_char = str(self.term_char).encode('utf-8')[0] 666 | data += term_char 667 | 668 | flags = 0 669 | 670 | num = len(data) 671 | 672 | offset = 0 673 | 674 | while num > 0: 675 | if num <= self.max_recv_size: 676 | flags |= OP_FLAG_END 677 | 678 | block = data[offset:offset+self.max_recv_size] 679 | 680 | error, size = self.client.device_write( 681 | self.link, 682 | self._timeout_ms, 683 | self._lock_timeout_ms, 684 | flags, 685 | block 686 | ) 687 | 688 | if error: 689 | raise Vxi11Exception(error, 'write') 690 | elif size < len(block): 691 | raise Vxi11Exception("did not write complete block", 'write') 692 | 693 | offset += size 694 | num -= size 695 | 696 | def read_raw(self, num=-1): 697 | "Read binary data from instrument" 698 | if self.link is None: 699 | self.open() 700 | 701 | read_len = self.max_read_len 702 | if num > 0: 703 | read_len = min(num, self.max_read_len) 704 | 705 | flags = 0 706 | reason = 0 707 | 708 | term_char = 0 709 | 710 | if self.term_char is not None: 711 | flags = OP_FLAG_TERMCHAR_SET 712 | term_char = str(self.term_char).encode('utf-8')[0] 713 | 714 | read_data = bytearray() 715 | 716 | while reason & (RX_END | RX_CHR) == 0: 717 | error, reason, data = self.client.device_read( 718 | self.link, 719 | read_len, 720 | self._timeout_ms, 721 | self._lock_timeout_ms, 722 | flags, 723 | term_char 724 | ) 725 | 726 | if error: 727 | raise Vxi11Exception(error, 'read') 728 | 729 | read_data.extend(data) 730 | 731 | if num > 0: 732 | num = num - len(data) 733 | if num <= 0: 734 | break 735 | if num < read_len: 736 | read_len = num 737 | 738 | return bytes(read_data) 739 | 740 | def ask_raw(self, data, num=-1): 741 | "Write then read binary data" 742 | self.write_raw(data) 743 | return self.read_raw(num) 744 | 745 | def write(self, message, encoding = 'utf-8'): 746 | "Write string to instrument" 747 | if type(message) is tuple or type(message) is list: 748 | # recursive call for a list of commands 749 | for message_i in message: 750 | self.write(message_i, encoding) 751 | return 752 | 753 | self.write_raw(str(message).encode(encoding)) 754 | 755 | def read(self, num=-1, encoding = 'utf-8'): 756 | "Read string from instrument" 757 | return self.read_raw(num).decode(encoding).rstrip('\r\n') 758 | 759 | def ask(self, message, num=-1, encoding = 'utf-8'): 760 | "Write then read string" 761 | if type(message) is tuple or type(message) is list: 762 | # recursive call for a list of commands 763 | val = list() 764 | for message_i in message: 765 | val.append(self.ask(message_i, num, encoding)) 766 | return val 767 | 768 | self.write(message, encoding) 769 | return self.read(num, encoding) 770 | 771 | def trigger(self): 772 | "Send trigger command" 773 | if self.link is None: 774 | self.open() 775 | 776 | flags = 0 777 | 778 | error = self.client.device_trigger( 779 | self.link, 780 | flags, 781 | self._lock_timeout_ms, 782 | self._timeout_ms 783 | ) 784 | 785 | if error: 786 | raise Vxi11Exception(error, 'trigger') 787 | 788 | def clear(self): 789 | "Send clear command" 790 | if self.link is None: 791 | self.open() 792 | 793 | flags = 0 794 | 795 | error = self.client.device_clear( 796 | self.link, 797 | flags, 798 | self._lock_timeout_ms, 799 | self._timeout_ms 800 | ) 801 | 802 | if error: 803 | raise Vxi11Exception(error, 'clear') 804 | 805 | def lock(self): 806 | "Send lock command" 807 | if self.link is None: 808 | self.open() 809 | 810 | flags = 0 811 | 812 | error = self.client.device_lock( 813 | self.link, 814 | flags, 815 | self._lock_timeout_ms 816 | ) 817 | 818 | if error: 819 | raise Vxi11Exception(error, 'lock') 820 | 821 | self.locked = True 822 | 823 | def unlock(self): 824 | "Send unlock command" 825 | if self.link is None: 826 | self.open() 827 | 828 | flags = 0 829 | 830 | error = self.client.device_unlock(self.link) 831 | 832 | if error: 833 | raise Vxi11Exception(error, 'unlock') 834 | 835 | self.locked = False 836 | 837 | 838 | class InterfaceDevice(Device): 839 | "VXI-11 IEEE 488.1 interface device interface client" 840 | def __init__(self, host, name = None, client_id = None, term_char = None): 841 | "Create new VXI-11 488.1 interface device object" 842 | 843 | if host.upper().startswith('TCPIP') and '::' in host: 844 | res = parse_visa_resource_string(host) 845 | 846 | if res is None: 847 | raise Vxi11Exception('Invalid resource string', 'init') 848 | 849 | host = res['arg1'] 850 | name = res['arg2'] 851 | 852 | if name is None: 853 | name = "gpib0" 854 | 855 | super(InterfaceDevice, self).__init__(host, name, client_id, term_char) 856 | 857 | self._bus_address = 0 858 | 859 | def open(self): 860 | "Open connection to VXI-11 device" 861 | if self.link is not None: 862 | return 863 | 864 | if ',' in self.name: 865 | raise Vxi11Exception("Cannot specify address for InterfaceDevice") 866 | 867 | super(InterfaceDevice, self).open() 868 | 869 | self._bus_address = self.get_bus_address() 870 | 871 | def send_command(self, data): 872 | "Send command" 873 | if self.link is None: 874 | self.open() 875 | 876 | flags = 0 877 | 878 | error, data_out = self.client.device_docmd( 879 | self.link, 880 | flags, 881 | self._timeout_ms, 882 | self._lock_timeout_ms, 883 | CMD_SEND_COMMAND, 884 | True, 885 | 1, 886 | data 887 | ) 888 | 889 | if error: 890 | raise Vxi11Exception(error, 'send_command') 891 | 892 | return data_out 893 | 894 | def create_setup(self, address_list): 895 | data = bytearray([self._bus_address | GPIB_CMD_TAD, GPIB_CMD_UNL]) 896 | 897 | if type(address_list) is int: 898 | address_list = [address_list] 899 | 900 | for addr in address_list: 901 | if type(addr) is tuple: 902 | if addr[0] < 0 or addr[0] > 30: 903 | raise Vxi11Exception("Invalid address", 'create_setup') 904 | data.append(addr[0] | GPIB_CMD_LAD) 905 | if len(addr) > 1: 906 | if addr[1] < 0 or addr[1] > 30: 907 | raise Vxi11Exception("Invalid address", 'create_setup') 908 | data.append(addr[1] | GPIB_CMD_SAD) 909 | else: 910 | if addr < 0 or addr > 30: 911 | raise Vxi11Exception("Invalid address", 'create_setup') 912 | data.append(addr | GPIB_CMD_LAD) 913 | 914 | return bytes(data) 915 | 916 | def send_setup(self, address_list): 917 | "Send setup" 918 | return self.send_command(self.create_setup(address_list)) 919 | 920 | def _bus_status(self, val): 921 | "Bus status" 922 | if self.link is None: 923 | self.open() 924 | 925 | flags = 0 926 | 927 | error, data_out = self.client.device_docmd( 928 | self.link, 929 | flags, 930 | self._timeout_ms, 931 | self._lock_timeout_ms, 932 | CMD_BUS_STATUS, 933 | True, 934 | 2, 935 | struct.pack('!H', val) 936 | ) 937 | 938 | if error: 939 | raise Vxi11Exception(error, 'bus_status') 940 | 941 | return struct.unpack('!H', data_out)[0] 942 | 943 | def test_ren(self): 944 | "Read REN line" 945 | return self._bus_status(CMD_BUS_STATUS_REMOTE) 946 | 947 | def test_srq(self): 948 | "Read SRQ line" 949 | return self._bus_status(CMD_BUS_STATUS_SRQ) 950 | 951 | def test_ndac(self): 952 | "Read NDAC line" 953 | return self._bus_status(CMD_BUS_STATUS_NDAC) 954 | 955 | def is_system_controller(self): 956 | "Check if interface device is a system controller" 957 | return self._bus_status(CMD_BUS_STATUS_SYSTEM_CONTROLLER) 958 | 959 | def is_controller_in_charge(self): 960 | "Check if interface device is the controller-in-charge" 961 | return self._bus_status(CMD_BUS_STATUS_CONTROLLER_IN_CHARGE) 962 | 963 | def is_talker(self): 964 | "Check if interface device is addressed as a talker" 965 | return self._bus_status(CMD_BUS_STATUS_TALKER) 966 | 967 | def is_listener(self): 968 | "Check if interface device is addressed as a listener" 969 | return self._bus_status(CMD_BUS_STATUS_LISTENER) 970 | 971 | def get_bus_address(self): 972 | "Get interface device bus address" 973 | return self._bus_status(CMD_BUS_STATUS_BUS_ADDRESS) 974 | 975 | def set_atn(self, val): 976 | "Set ATN line" 977 | if self.link is None: 978 | self.open() 979 | 980 | flags = 0 981 | 982 | error, data_out = self.client.device_docmd( 983 | self.link, 984 | flags, 985 | self._timeout_ms, 986 | self._lock_timeout_ms, 987 | CMD_ATN_CTRL, 988 | True, 989 | 2, 990 | struct.pack('!H', val) 991 | ) 992 | 993 | if error: 994 | raise Vxi11Exception(error, 'set_atn') 995 | 996 | return struct.unpack('!H', data_out)[0] 997 | 998 | def set_ren(self, val): 999 | "Set REN line" 1000 | if self.link is None: 1001 | self.open() 1002 | 1003 | flags = 0 1004 | 1005 | error, data_out = self.client.device_docmd( 1006 | self.link, 1007 | flags, 1008 | self._timeout_ms, 1009 | self._lock_timeout_ms, 1010 | CMD_REN_CTRL, 1011 | True, 1012 | 2, 1013 | struct.pack('!H', val) 1014 | ) 1015 | 1016 | if error: 1017 | raise Vxi11Exception(error, 'set_ren') 1018 | 1019 | return struct.unpack('!H', data_out)[0] 1020 | 1021 | def pass_control(self, addr): 1022 | "Pass control to another controller" 1023 | 1024 | if addr < 0 or addr > 30: 1025 | raise Vxi11Exception("Invalid address", 'pass_control') 1026 | 1027 | if self.link is None: 1028 | self.open() 1029 | 1030 | flags = 0 1031 | 1032 | error, data_out = self.client.device_docmd( 1033 | self.link, 1034 | flags, 1035 | self._timeout_ms, 1036 | self._lock_timeout_ms, 1037 | CMD_PASS_CTRL, 1038 | True, 1039 | 4, 1040 | struct.pack('!L', addr) 1041 | ) 1042 | 1043 | if error: 1044 | raise Vxi11Exception(error, 'pass_control') 1045 | 1046 | return struct.unpack('!L', data_out)[0] 1047 | 1048 | def set_bus_address(self, addr): 1049 | "Set interface device bus address" 1050 | 1051 | if addr < 0 or addr > 30: 1052 | raise Vxi11Exception("Invalid address", 'set_bus_address') 1053 | 1054 | if self.link is None: 1055 | self.open() 1056 | 1057 | flags = 0 1058 | 1059 | error, data_out = self.client.device_docmd( 1060 | self.link, 1061 | flags, 1062 | self._timeout_ms, 1063 | self._lock_timeout_ms, 1064 | CMD_BUS_ADDRESS, 1065 | True, 1066 | 4, 1067 | struct.pack('!L', addr) 1068 | ) 1069 | 1070 | if error: 1071 | raise Vxi11Exception(error, 'set_bus_address') 1072 | 1073 | self._bus_address = addr 1074 | 1075 | return struct.unpack('!L', data_out)[0] 1076 | 1077 | def send_ifc(self): 1078 | "Send IFC" 1079 | if self.link is None: 1080 | self.open() 1081 | 1082 | flags = 0 1083 | 1084 | error, data_out = self.client.device_docmd( 1085 | self.link, 1086 | flags, 1087 | self._timeout_ms, 1088 | self._lock_timeout_ms, 1089 | CMD_IFC_CTRL, 1090 | True, 1091 | 1, 1092 | b'' 1093 | ) 1094 | 1095 | if error: 1096 | raise Vxi11Exception(error, 'send_ifc') 1097 | 1098 | def find_listeners(self, address_list=None): 1099 | "Find devices" 1100 | if self.link is None: 1101 | self.open() 1102 | 1103 | if address_list is None: 1104 | address_list = list(range(31)) 1105 | address_list.remove(self._bus_address) 1106 | 1107 | found = [] 1108 | 1109 | try: 1110 | self.lock() 1111 | for addr in address_list: 1112 | # check for listener at primary address 1113 | cmd = bytearray([GPIB_CMD_UNL, GPIB_CMD_UNT]) 1114 | cmd.append(self._bus_address | GPIB_CMD_TAD) # spec says this is unnecessary, but doesn't appear to work without this 1115 | if type(addr) is tuple: 1116 | addr = addr[0] 1117 | if addr < 0 or addr > 30: 1118 | raise Vxi11Exception("Invalid address", 'find_listeners') 1119 | cmd.append(addr | GPIB_CMD_LAD) 1120 | self.send_command(cmd) 1121 | self.set_atn(False) 1122 | time.sleep(0.0015) # probably not necessary due to network delays 1123 | if self.test_ndac(): 1124 | found.append(addr) 1125 | else: 1126 | # check for listener at any sub-address 1127 | cmd = bytearray([GPIB_CMD_UNL, GPIB_CMD_UNT]) 1128 | cmd.append(self._bus_address | GPIB_CMD_TAD) # spec says this is unnecessary, but doesn't appear to work without this 1129 | cmd.append(addr | GPIB_CMD_LAD) 1130 | for sa in range(31): 1131 | cmd.append(sa | GPIB_CMD_SAD) 1132 | self.send_command(cmd) 1133 | self.set_atn(False) 1134 | time.sleep(0.0015) # probably not necessary due to network delays 1135 | if self.test_ndac(): 1136 | # find specific sub-address 1137 | for sa in range(31): 1138 | cmd = bytearray([GPIB_CMD_UNL, GPIB_CMD_UNT]) 1139 | cmd.append(self._bus_address | GPIB_CMD_TAD) # spec says this is unnecessary, but doesn't appear to work without this 1140 | cmd.append(addr | GPIB_CMD_LAD) 1141 | cmd.append(sa | GPIB_CMD_SAD) 1142 | self.send_command(cmd) 1143 | self.set_atn(False) 1144 | time.sleep(0.0015) # probably not necessary due to network delays 1145 | if self.test_ndac(): 1146 | found.append((addr, sa)) 1147 | self.unlock() 1148 | except: 1149 | self.unlock() 1150 | raise 1151 | 1152 | return found 1153 | 1154 | 1155 | class Instrument(Device): 1156 | "VXI-11 instrument interface client" 1157 | 1158 | def read_stb(self): 1159 | "Read status byte" 1160 | if self.link is None: 1161 | self.open() 1162 | 1163 | flags = 0 1164 | 1165 | error, stb = self.client.device_read_stb( 1166 | self.link, 1167 | flags, 1168 | self._lock_timeout_ms, 1169 | self._timeout_ms 1170 | ) 1171 | 1172 | if error: 1173 | raise Vxi11Exception(error, 'read_stb') 1174 | 1175 | return stb 1176 | 1177 | def remote(self): 1178 | "Send remote command" 1179 | if self.link is None: 1180 | self.open() 1181 | 1182 | flags = 0 1183 | 1184 | error = self.client.device_remote( 1185 | self.link, 1186 | flags, 1187 | self._lock_timeout_ms, 1188 | self._timeout_ms 1189 | ) 1190 | 1191 | if error: 1192 | raise Vxi11Exception(error, 'remote') 1193 | 1194 | def local(self): 1195 | "Send local command" 1196 | if self.link is None: 1197 | self.open() 1198 | 1199 | flags = 0 1200 | 1201 | error = self.client.device_local( 1202 | self.link, 1203 | flags, 1204 | self._lock_timeout_ms, 1205 | self._timeout_ms 1206 | ) 1207 | 1208 | if error: 1209 | raise Vxi11Exception(error, 'local') 1210 | 1211 | --------------------------------------------------------------------------------