New Upstream Release - pyvisa-py
Ready changes
Summary
Merged new upstream version: 0.6.3 (was: 0.5.1).
Diff
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..0fd5a13
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,17 @@
+[flake8]
+exclude =
+ .git,
+ __pycache__,
+ docs/source/conf.py,
+ old,
+ build,
+ dist,
+ignore = E203, E266, E501, W503, E731
+# line length is intentionally set to 80 here because pyvisa uses Bugbear
+# See https://github.com/psf/black/blob/master/README.md#line-length for more details
+max-line-length = 80
+max-complexity = 18
+select = B,C,E,F,W,T4,B9
+per-file-ignores =
+ pyvisa_py/protocols/vxi11.py:E221
+ pyvisa_py/serial.py:C901
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..db00e4e
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,12 @@
+# These are supported funding model platforms
+
+github: [MatthieuDartiailh]
+patreon: # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: # Replace with a single Liberapay username
+issuehunt: # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..3bad94f
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+ # Maintain dependencies for GitHub Actions
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 797173e..695fbd6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,20 +1,19 @@
name: Continuous Integration
on:
schedule:
- - cron: '0 0 * * 2'
+ - cron: "0 0 * * 2"
push:
branches:
- - master
+ - main
- staging
- trying
pull_request:
branches:
- - master
+ - main
paths:
- .github/workflows/ci.yml
- - pyvisa_py/*
+ - "pyvisa_py/**"
- pyproject.toml
- - setup.cfg
- setup.py
jobs:
@@ -22,19 +21,19 @@ jobs:
name: Check code formatting
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: 3.8
- name: Install tools
run: |
python -m pip install --upgrade pip
- pip install flake8 black isort mypy
- pip install git+https://github.com/pyvisa/pyvisa.git#egg=pyvisa
+ pip install flake8 black isort mypy pytest
+ pip install git+https://github.com/pyvisa/pyvisa.git@main
- name: Isort
run: |
- isort pyvisa_y -c;
+ isort pyvisa_py -c;
- name: Black
run: |
black pyvisa_py --check;
@@ -50,11 +49,11 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
- python-version: [3.6, 3.7, 3.8]
+ python-version: ["3.8", "3.9", "3.10", "3.11"]
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
@@ -69,7 +68,7 @@ jobs:
pip install pytest-cov
pytest pyvisa_py/testsuite --cov pyvisa_py --cov-report xml
- name: Upload coverage to Codecov
- uses: codecov/codecov-action@v1
+ uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: unittests
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 8c53d3f..18061d4 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -1,19 +1,19 @@
name: Documentation building
on:
schedule:
- - cron: '0 0 * * 2'
+ - cron: "0 0 * * 2"
push:
branches:
- - master
+ - main
- staging
- trying
pull_request:
branches:
- - master
+ - main
paths:
- .github/workflows/docs.yml
- - pyvisa_py/*
- - docs/*
+ - "pyvisa_py/**"
+ - "docs/**"
- setup.py
jobs:
@@ -27,15 +27,13 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
+ pip install -r docs/requirements.txt
pip install git+https://github.com/pyvisa/pyvisa.git
- name: Install project
run: |
- python setup.py develop
+ pip install .
- name: Install graphviz
- uses: kamiazya/setup-graphviz@v1
- - name: Install doc building tools
- run: |
- pip install sphinx sphinx_rtd_theme
+ uses: ts-graphviz/setup-graphviz@v1
- name: Build documentation
run: |
mkdir docs_output;
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..f79ab12
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,111 @@
+name: Build and upload wheels
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: '0 0 * * 3'
+ push:
+ tags:
+ - '*'
+
+jobs:
+ build_sdist:
+ name: Build sdist
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Get history and tags for SCM versioning to work
+ run: |
+ git fetch --prune --unshallow
+ git fetch --depth=1 origin +refs/tags/*:refs/tags/*
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ - name: Build sdist
+ run: |
+ pip install --upgrade pip
+ pip install wheel build
+ python -m build . -s
+ - name: Test sdist
+ run: |
+ pip install pytest
+ pip install dist/*.tar.gz
+ python -X dev -m pytest --pyargs pyvisa_py
+ - name: Store artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ name: artifact
+ path: dist/*
+
+ build_wheel:
+ name: Build wheel
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v2
+ - name: Get history and tags for SCM versioning to work
+ run: |
+ git fetch --prune --unshallow
+ git fetch --depth=1 origin +refs/tags/*:refs/tags/*
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ - name: Build wheels
+ run: |
+ pip install --upgrade pip
+ pip install wheel build
+ python -m build . -w
+ - name: Test wheel
+ run: |
+ pip install pytest
+ pip install dist/*.whl
+ python -X dev -m pytest --pyargs pyvisa_py
+ - name: Store artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ name: artifact
+ path: dist/*.whl
+
+ release_upload:
+ name: Create Release and Upload Release Asset
+ runs-on: ubuntu-latest
+ if: github.event_name == 'push'
+ needs: [build_wheel, build_sdist]
+ steps:
+ - name: Create Release
+ id: create_release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ github.ref }}
+ release_name: Release ${{ github.ref }}
+ draft: false
+ prerelease: ${{ contains(github.ref, 'rc') || contains(github.ref, 'a') || contains(github.ref, 'b')}}
+ - uses: actions/download-artifact@v3
+ with:
+ name: artifact
+ path: dist
+ - name: Upload Release Asset
+ id: upload-release-asset
+ uses: shogo82148/actions-upload-release-asset@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ steps.create_release.outputs.upload_url }}
+ asset_path: dist/*
+
+ upload_pypi:
+ if: github.event_name == 'push'
+ needs: [build_wheel, build_sdist]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/download-artifact@v3
+ with:
+ name: artifact
+ path: dist
+
+ - uses: pypa/gh-action-pypi-publish@master
+ with:
+ user: __token__
+ password: ${{ secrets.pypi_password }}
+ # To test:
+ # repository_url: https://test.pypi.org/legacy/
diff --git a/.gitignore b/.gitignore
index 2cbb923..b701ef2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,8 @@ _test/
.mypy_cache/
.pytest_cache/
.cache/
-.vscode
\ No newline at end of file
+.vscode
+.dmypy.json
+
+# auto-generated version file
+version.py
\ No newline at end of file
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..17a318d
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,27 @@
+# .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the version of Python and other tools you might need
+build:
+ os: ubuntu-20.04
+ tools:
+ python: "3.9"
+
+# Build documentation in the docs/source directory with Sphinx
+sphinx:
+ configuration: docs/source/conf.py
+
+# Enable epub output
+formats:
+ - epub
+
+# Optionally declare the Python requirements required to build your docs
+python:
+ install:
+ - requirements: docs/requirements.txt
+ - method: pip
+ path: .
diff --git a/AUTHORS b/AUTHORS
index 5f17e57..37bbe6f 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,4 +1,6 @@
-pyvisa-py is written and maintained by Hernan E. Grecco <hernan.grecco@gmail.com>.
+pyvisa-py was started by Hernan E. Grecco <hernan.grecco@gmail.com>.
+
+It is now maintained by Matthieu C. Dartiailh <m.dartiailh@gmail.com>
Other contributors, listed alphabetically, are:
@@ -8,7 +10,7 @@ Other contributors, listed alphabetically, are:
* Colin Marquardt <github@marquardt-home.de>
* Lance McCulley <lancemcculley@gmail.com>
* Martin Ritter <ritter@mpp.mpg.de>
-* Matthieu Dartiailh <marul@laposte.net>
+* Matthieu C. Dartiailh <m.dartiailh@gmail.com>
* Sebastian Held <sebastian.held@imst.de>
* Thomas Kopp <20.kopp@gmail.com>
* Thorsten Liebig <liebig@imst.de>
diff --git a/CHANGES b/CHANGES
index 42294c7..7202cdf 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,6 +1,55 @@
PyVISA-py Changelog
===================
+0.6.3 (17-02-2023)
+------------------
+
+- fix bad behavior on PyVISA 1.12 and hence on Python 3.7 PR #357
+ 0.6.x is the last version that will support Python 3.7
+
+0.6.2 (08-02-2023)
+------------------
+
+- fix usb resource handling by avoiding multiple calls to set_configuration PR #352
+- formatting fixes on files using "black" PR #352
+
+0.6.1 (25-01-2023)
+------------------
+
+- fix listing resources when some optional dependencies are missing PR #349
+- properly list discovered TCPIP resources PR #349
+- fix pyvisa-info output for TCPIP and GPIB resources PR #349
+
+0.6.0 (22-12-2022)
+------------------
+
+- fix writing large messages over TCPIP using the VXI-11 protocol PR #343
+- add support for the hislip protocol over TCPIP PR #331
+- allow to list TCPIP resources PR #326
+ In order to discover resources over all subnets psutil needs to be installed
+- attempt to stabilize access to USBTMC resources PR #335
+ Reduce the number of device reset performed and only set all settings if it
+ is meaningful (more than one settings exist.)
+
+ A huge thanks to @bobmacnamara for his work adding hislip and vicp support !
+
+0.5.3 (12-05-2022)
+------------------
+- fix tcp/ip connections dropping from inside Docker containers after 5 minute idling #285
+- fix ControlFlow.none as an invalid attribute in serial.py PR #317
+- VXI11 bug fix: skip over stale rx packets instead of raising an exception. PR #322
+- VXI11 bug fix: to ensure all data gets sent, replace calls to sock.send()
+ with calls to sock.sendall(), and replace calls to sock.sendto() with
+ calls to a routine that loops until all data is sent. PR #322
+
+0.5.2 (04-02-2020)
+------------------
+
+- handle SUPPRESS_END_EN in usb.py to fix #293 PR #294
+- add python_requires to avoid people trying to get a
+ new pyvisa-py on Python 2 PR #295
+ This addresses pyvisa issue #578
+
0.5.1 (30-09-2020)
------------------
diff --git a/MANIFEST.in b/MANIFEST.in
index 8385f90..9d0e1a1 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,5 @@
include README AUTHORS CHANGES LICENSE
recursive-include pyvisa-py *
recursive-include docs *
-prune docs/_build
+prune docs/build
global-exclude *.pyc *~ .DS_Store *__pycache__* *.pyo
diff --git a/README.rst b/README.rst
index 616f573..aca7520 100644
--- a/README.rst
+++ b/README.rst
@@ -7,10 +7,10 @@ PyVISA-py
.. image:: https://github.com/pyvisa/pyvisa-py/workflows/Documentation%20building/badge.svg
:target: https://github.com/pyvisa/pyvisa-py/actions
:alt: Documentation building
-.. image:: https://dev.azure.com/pyvisa/pyvisa-py/_apis/build/status/pyvisa.pyvisa-py.keysight-assisted?branchName=master
+.. image:: https://dev.azure.com/pyvisa/pyvisa-py/_apis/build/status/pyvisa.pyvisa-py.keysight-assisted?branchName=main
:target: https://dev.azure.com/pyvisa/pyvisa-py/_build
:alt: Keysight assisted testing
-.. image:: https://codecov.io/gh/pyvisa/pyvisa-py/branch/master/graph/badge.svg
+.. image:: https://codecov.io/gh/pyvisa/pyvisa-py/branch/main/graph/badge.svg
:target: https://codecov.io/gh/pyvisa/pyvisa-py
:alt: Code Coverage
.. image:: https://readthedocs.org/projects/pyvisa-py/badge/?version=latest
@@ -64,11 +64,15 @@ Requirements
- Python (tested with 3.6+)
- PyVISA 1.11+
-Optionally
+Optionally:
+
- PySerial (to interface with Serial instruments)
- PyUSB (to interface with USB instruments)
- linux-gpib (to interface with gpib instruments, only on linux)
- gpib-ctypes (to interface with GPIB instruments on Windows and Linux)
+- psutil (to discover TCPIP devices across multiple interfaces)
+- zeroconf (for HiSLIP and VICP devices discovery)
+- pyvicp (to enable the Teledyne LeCroy proprietary VICP protocol)
Installation
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 059df03..b98d8ab 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -6,63 +6,61 @@
trigger:
branches:
include:
- - master
- - staging
- - trying
+ - main
+ - staging
+ - trying
pr:
-- master
+ - main
variables:
PYVISA_KEYSIGHT_VIRTUAL_INSTR: 1
-
pool:
- name: default
+ name: Keysight-based
demands: KEYSIGHT -equals TCPIP
steps:
-- script: |
- echo Activate conda
- call $(CONDA_PATH)\activate.bat
- echo Create environment
- conda create -n test_ python=3.7 numpy --yes
- displayName: 'Create environment'
+ - script: |
+ export PATH="$HOME/miniconda3/bin:$PATH"
+ echo Create environment
+ conda create -n test_ python=3.9 numpy --yes
+ displayName: "Create environment"
-- script: |
- echo Activate conda
- call $(CONDA_PATH)\activate.bat
- echo Activate environment
- call conda activate test_
- echo Install project
- pip install git+https://github.com/pyvisa/pyvisa.git#egg=pyvisa
- pip install -e .
- displayName: 'Install dependencies'
+ - script: |
+ export PATH="$HOME/miniconda3/bin:$PATH"
+ source $HOME/miniconda3/bin/activate
+ echo Activate environment
+ call conda activate test_
+ echo Install project
+ pip install git+https://github.com/pyvisa/pyvisa.git#egg=pyvisa
+ pip install -e .
+ displayName: "Install dependencies"
-- script: |
- echo Activate conda
- call $(CONDA_PATH)\activate.bat
- echo Activate environment
- call conda activate test_
- echo Install pytest and co
- pip install pytest pytest-azurepipelines pytest-cov
- echo Run pytest
- python -X dev -m pytest --pyargs pyvisa_py --cov pyvisa_py --cov-report xml -v
- displayName: 'Run tests'
+ - script: |
+ export PATH="$HOME/miniconda3/bin:$PATH"
+ source $HOME/miniconda3/bin/activate
+ echo Activate environment
+ call conda activate test_
+ echo Install pytest and co
+ pip install pytest pytest-azurepipelines pytest-cov
+ echo Run pytest
+ python -X dev -m pytest --pyargs pyvisa_py --cov pyvisa_py --cov-report xml -v
+ displayName: "Run tests"
-- script: |
- echo Activate conda
- call $(CONDA_PATH)\activate.bat
- echo Activate environment
- call conda activate test_
- echo Install codecov
- pip install codecov
- echo Run codecov
- codecov --file coverage.xml --token $(CODECOV_TOKEN) --env PYVISA_KEYSIGHT_VIRTUAL_INSTR --tries 5 --required -F unittest --name codecov-umbrella
- displayName: 'Upload test coverage results'
+ - script: |
+ export PATH="$HOME/miniconda3/bin:$PATH"
+ source $HOME/miniconda3/bin/activate
+ echo Activate environment
+ call conda activate test_
+ echo Install codecov
+ pip install codecov
+ echo Run codecov
+ codecov --file coverage.xml --token $(CODECOV_TOKEN) --env PYVISA_KEYSIGHT_VIRTUAL_INSTR --tries 5 --required -F unittest --name codecov-umbrella
+ displayName: "Upload test coverage results"
-- script: |
- call $(CONDA_PATH)\activate.bat
- conda remove -n test_ --all --yes
- displayName: 'Remove test environment'
- condition: always()
+ - script: |
+ export PATH="$HOME/miniconda3/bin:$PATH"
+ conda remove -n test_ --all --yes
+ displayName: "Remove test environment"
+ condition: always()
diff --git a/debian/changelog b/debian/changelog
index 665541c..7d656f8 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+pyvisa-py (0.6.3-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Sun, 26 Feb 2023 03:12:10 -0000
+
pyvisa-py (0.5.1-3) unstable; urgency=medium
[ Debian Janitor ]
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..ead9e81
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,2 @@
+sphinx>=4
+sphinx-rtd-theme>=1
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 533bea5..3177965 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -10,10 +10,14 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
+import datetime
import os
import sys
-import datetime
-from importlib.metadata import version as get_version
+
+if sys.version_info >= (3, 8):
+ from importlib.metadata import version as get_version
+else:
+ from importlib_metadata import version as get_version
# If extensions (or modules to document with autodoc) are in another directory,
diff --git a/docs/source/faq.rst b/docs/source/faq.rst
index 4733418..e4da8b0 100644
--- a/docs/source/faq.rst
+++ b/docs/source/faq.rst
@@ -16,25 +16,43 @@ need, let us know.
Why are you developing this?
----------------------------
-The IVI compliant VISA implementation available (`National Instruments's VISA`_ ,
-Keysight, Tektronik, etc) are proprietary libraries that only works on
+The IVI compliant VISA implementations available (`National Instruments NI-VISA`_ ,
+`Keysight IO Libraries`_, `Tektronix TekVISA`_, etc) are proprietary libraries that only work on
certain systems. We wanted to provide a compatible alternative.
Can PyVISA-py be used from a VM?
--------------------------------
-Because PyVISA-py access hardware resources (such as USB ports) running from a
-VM can cause issues, such as unexpected timeouts because the VM does not
-receive the response. You may be able to set the VM in such that it works but
-you should refer to your VM manual.
-(see https://github.com/pyvisa/pyvisa-py/issues/243 for the kind of issue it
-can cause)
+
+Because PyVISA-py access hardware resources such as USB ports, running from a
+VM can cause issues like unexpected timeouts because the VM does not
+receive the response. You should consult your VM manual to determine
+if you are able to setup the VM in such a way that it works. See
+https://github.com/pyvisa/pyvisa-py/issues/243 for the kind of issue
+it can cause.
+
+
+Can PyVISA-py be used from a Docker container?
+----------------------------------------------
+As the Windows variant of Docker can forward neither USB ports nor GPIB
+interfaces, the obvious choice would be to connect via TCP/IP. The problem of a
+Docker container is that idle connections are disconnected by the VPN garbage
+collection. For this reason it is reasonable to enable keepalive packets.
+The VISA attribute `VI_ATTR_TCPIP_KEEPALIVE` has been modified to work
+for all TCP/IP instruments. Enabling this option can be done with:
+
+ inst.set_visa_attribute(pyvisa.constants.ResourceAttribute.tcpip_keepalive, True)
+
+where `inst` is an active TCP/IP visa session.
+(see https://tech.xing.com/a-reason-for-unexplained-connection-timeouts-on-kubernetes-docker-abd041cf7e02
+if you want to read more about connection dropping in docker containers)
Why not using LibreVISA?
------------------------
-LibreVISA_ is still young and appears mostly unmaintained at this point.
+LibreVISA_ is still young and appears mostly unmaintained at this
+point (latest release is from 2013).
However, you can already use it with the IVI backend as it has the same API.
We think that PyVISA-py is easier to hack and we can quickly reach feature parity
with other IVI-VISA implementation for message-based instruments.
@@ -55,8 +73,10 @@ from higher level applications.
.. _PyUSB: https://github.com/pyusb/pyusb
.. _PyPI: https://pypi.python.org/pypi/PyVISA-py
.. _GitHub: https://github.com/pyvisa/pyvisa-py
-.. _`National Instruments's VISA`: http://ni.com/visa/
+.. _`National Instruments NI-VISA`: http://ni.com/visa/
.. _`LibreVISA`: http://www.librevisa.org/
.. _`issue tracker`: https://github.com/pyvisa/pyvisa-py/issues
.. _`linux-gpib`: http://linux-gpib.sourceforge.net/
.. _`gpib-ctypes`: https://pypi.org/project/gpib-ctypes/
+.. _`Tektronix TekVISA`: https://www.tek.com/en/support/software/driver/tekvisa-connectivity-software-v420
+.. _`Keysight IO Libraries`: https://www.keysight.com/us/en/lib/software-detail/computer-software/io-libraries-suite-downloads-2175637.html
diff --git a/docs/source/installation.rst b/docs/source/installation.rst
index 9310bc5..bb69d06 100644
--- a/docs/source/installation.rst
+++ b/docs/source/installation.rst
@@ -23,6 +23,18 @@ Pyvisa-py relies on :py:mod:`socket` module in the Python Standard Library to
interact with the instrument which you do not need to install any extra library
to access those resources.
+To discover VXI-11 devices on all network interfaces, please install
+`psutil`_. Otherwise, discovery will only occur on the default network
+interface.
+
+Discovery of both HiSLIP and VICP devices relies on `mDNS`_, which is a protocol for
+service discovery in a local area network. To enable resource
+discovery for HiSLIP and VICP, you should install `zeroconf`_.
+
+The TCP/IP VICP protocol (proprietary to Teledyne LeCroy) depends on
+the `pyvicp`_ package. You should install this package if you need to
+use VICP.
+
Serial resources: ASRL INSTR
----------------------------
@@ -74,7 +86,7 @@ How do I know if PyVISA-py is properly installed?
Using the pyvisa information tool. Run in your console::
- python -m visa info
+ pyvisa-info
You will get info about PyVISA, the installed backends and their options.
@@ -97,4 +109,8 @@ form GitHub_::
.. _`LibreVISA`: http://www.librevisa.org/
.. _`issue tracker`: https://github.com/pyvisa/pyvisa-py/issues
.. _`linux-gpib`: http://linux-gpib.sourceforge.net/
-.. _`gpib-ctypes`: https://pypi.org/project/gpib-ctypes/
\ No newline at end of file
+.. _`gpib-ctypes`: https://pypi.org/project/gpib-ctypes/
+.. _`psutil`: https://pypi.org/project/psutil/
+.. _`mDNS`: https://en.wikipedia.org/wiki/Multicast_DNS
+.. _`zeroconf`: https://pypi.org/project/zeroconf/
+.. _`pyvicp`: https://pypi.org/project/pyvicp/
diff --git a/pyproject.toml b/pyproject.toml
index 5f713a4..0cdd2e3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,111 @@
+[project]
+name = "PyVISA-py"
+description = "Pure Python implementation of a VISA library."
+readme = "README.rst"
+requires-python = ">=3.7"
+license = {file = "LICENSE"}
+authors = [
+ {name = "Hernan E. Grecco", email = "hernan.grecco@gmail.com"},
+]
+maintainers = [
+ {name = "Matthieu C. Dartiailh", email = "m.dartiailh@gmail.com"}
+]
+keywords = [
+ "VISA",
+ "GPIB",
+ "USB",
+ "serial",
+ "RS232",
+ "measurement",
+ "acquisition",
+]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Science/Research",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: Microsoft :: Windows",
+ "Operating System :: POSIX :: Linux",
+ "Operating System :: MacOS :: MacOS X",
+ "Programming Language :: Python",
+ "Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+]
+dependencies = [
+ "pyvisa>=1.12.0",
+ "typing_extensions",
+ "importlib-metadata; python_version<'3.8'",
+]
+dynamic=["version"]
+
+[project.optional-dependencies]
+gpib-ctypes = ["gpib-ctypes>=0.3.0"]
+serial = ["pyserial>=3.0"]
+usb = ["pyusb"]
+psutil = ["psutil"]
+hislip-discovery = ["zeroconf"]
+vicp = ["pyvicp", "zeroconf"]
+
+
+[project.urls]
+homepage = "https://github.com/pyvisa/pyvisa-py"
+documentation = "https://pyvisa-py.readthedocs.io/en/latest/"
+repository = "https://github.com/pyvisa/pyvisa-py"
+changelog = "https://github.com/pyvisa/pyvisa-py/blob/main/CHANGES"
+
[build-system]
-requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.4.3"]
+requires = ["setuptools>=61.2", "wheel", "setuptools_scm[toml]>=3.4.3"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
+write_to = "pyvisa_py/version.py"
+write_to_template = """
+# This file is auto-generated by setuptools-scm do NOT edit it.
+
+from collections import namedtuple
+
+#: A namedtuple of the version info for the current release.
+_version_info = namedtuple("_version_info", "major minor micro status")
+
+parts = "{version}".split(".", 3)
+version_info = _version_info(
+ int(parts[0]),
+ int(parts[1]),
+ int(parts[2]),
+ parts[3] if len(parts) == 4 else "",
+)
+
+# Remove everything but the 'version_info' from this module.
+del namedtuple, _version_info, parts
+
+__version__ = "{version}"
+"""
+
+[tool.black]
+line-length = 88 # Enforce the default value
+
+[tool.pytest.ini_options]
+minversion = "6.0"
+
+[tool.mypy]
+follow_imports = "normal"
+strict_optional = true
+
+[[tool.mypy.overrides]]
+module = [
+ "usb.*",
+ "serial.*",
+ "gpib.*",
+ "Gpib.*",
+ "gpib_ctypes.*",
+
+]
+ignore_missing_imports = true
+
+[tool.isort]
+profile = "black"
+combine_as_imports = true
diff --git a/pyvisa_py/__init__.py b/pyvisa_py/__init__.py
index 494053c..0520291 100644
--- a/pyvisa_py/__init__.py
+++ b/pyvisa_py/__init__.py
@@ -20,6 +20,8 @@ except PackageNotFoundError:
# package is not installed
pass
+# noqa: we need to import so that __init_subclass__() is executed once
+from . import attributes # noqa: F401
from .highlevel import PyVisaLibrary
WRAPPER_CLASS = PyVisaLibrary
diff --git a/pyvisa_py/attributes.py b/pyvisa_py/attributes.py
new file mode 100644
index 0000000..62ac4c0
--- /dev/null
+++ b/pyvisa_py/attributes.py
@@ -0,0 +1,30 @@
+# -*- coding: utf-8 -*-
+"""Additional Attributes for specific use with the pyvisa-py package.
+
+For additional information and VISA attributes see pyvisa.constants
+
+:copyright: 2014-2020 by PyVISA-py Authors, see AUTHORS for more details.
+:license: MIT, see LICENSE for more details.
+"""
+
+from pyvisa import constants
+from pyvisa.attributes import AttrVI_ATTR_TCPIP_KEEPALIVE as former_keepalive
+
+
+class AttrVI_ATTR_TCPIP_KEEPALIVE(former_keepalive):
+ """Requests that a TCP/IP provider enable the use of keep-alive packets.
+
+ Altering the standard PyVISA attribute to also work on INSTR sessions as
+ they are using sockets in pyvisa-py as well.
+
+ After the system detects that a connection was dropped, VISA returns a lost
+ connection error code on subsequent I/O calls on the session. The time required
+ for the system to detect that the connection was dropped is dependent on the
+ system and is not settable.
+
+ """
+
+ resources = [
+ (constants.InterfaceType.tcpip, "SOCKET"),
+ (constants.InterfaceType.tcpip, "INSTR"),
+ ]
diff --git a/pyvisa_py/gpib.py b/pyvisa_py/gpib.py
index a2fc6ff..dac0104 100644
--- a/pyvisa_py/gpib.py
+++ b/pyvisa_py/gpib.py
@@ -22,16 +22,27 @@ try:
from gpib_ctypes.Gpib import Gpib # typing: ignore
from gpib_ctypes.gpib.gpib import _lib as gpib_lib # typing: ignore
- # Add some extra binding not available by default
- extra_funcs = [
- ("ibcac", [ctypes.c_int, ctypes.c_int], ctypes.c_int),
- ("ibgts", [ctypes.c_int, ctypes.c_int], ctypes.c_int),
- ("ibpct", [ctypes.c_int], ctypes.c_int),
- ]
- for name, argtypes, restype in extra_funcs:
- libfunction = gpib_lib[name]
- libfunction.argtypes = argtypes
- libfunction.restype = restype
+ try:
+ # Add some extra binding not available by default
+ extra_funcs = [
+ ("ibcac", [ctypes.c_int, ctypes.c_int], ctypes.c_int),
+ ("ibgts", [ctypes.c_int, ctypes.c_int], ctypes.c_int),
+ ("ibpct", [ctypes.c_int], ctypes.c_int),
+ ]
+ for name, argtypes, restype in extra_funcs:
+ libfunction = gpib_lib[name]
+ libfunction.argtypes = argtypes
+ libfunction.restype = restype
+ except TypeError:
+ Session.register_unavailable(
+ constants.InterfaceType.gpib,
+ "INSTR",
+ "gpib_ctypes is installed but could not locate the gpib library.\n"
+ "Please manually load it using:\n"
+ " gpib_ctypes.gpib.gpib._load_lib(filename)\n"
+ "before importing pyvisa.",
+ )
+ raise
except ImportError:
GPIB_CTYPES = False
@@ -188,7 +199,7 @@ def convert_gpib_error(
# feels brittle. As a consequence we only try to be smart when using
# gpib-ctypes. However in both cases we log the exception at debug level.
else:
- logger.debug("Failed to %s.", exc_info=error)
+ logger.debug("Failed to %s.", operation, exc_info=error)
if not GPIB_CTYPES:
return StatusCode.error_system_error
if error.code == 1:
@@ -255,6 +266,8 @@ class _GPIBCommon(Session):
def after_parsing(self) -> None:
minor = int(self.parsed.board)
+ # Secondary address (SAD) values should be in the range 96 to 126,
+ # 0 means the SAD is disabled.
sad = 0
timeout = 13
send_eoi = 1
diff --git a/pyvisa_py/highlevel.py b/pyvisa_py/highlevel.py
index 22a5f31..052d632 100644
--- a/pyvisa_py/highlevel.py
+++ b/pyvisa_py/highlevel.py
@@ -13,7 +13,7 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple, Union, cast
from pyvisa import constants, highlevel, rname
from pyvisa.constants import StatusCode
from pyvisa.typing import VISAEventContext, VISARMSession, VISASession
-from pyvisa.util import LibraryPath
+from pyvisa.util import DebugInfo, LibraryPath
from . import sessions
from .common import logger
@@ -75,7 +75,7 @@ class PyVisaLibrary(highlevel.VisaLibraryBase):
return (LibraryPath("py"),)
@staticmethod
- def get_debug_info() -> Dict[str, Union[str, List[str], Dict[str, str]]]:
+ def get_debug_info() -> DebugInfo:
"""Return a list of lines with backend info."""
from . import __version__
@@ -116,7 +116,7 @@ class PyVisaLibrary(highlevel.VisaLibraryBase):
session: VISARMSession,
resource_name: str,
access_mode: constants.AccessModes = constants.AccessModes.no_lock,
- open_timeout: int = constants.VI_TMO_IMMEDIATE,
+ open_timeout: Optional[int] = constants.VI_TMO_IMMEDIATE,
) -> Tuple[VISASession, StatusCode]:
"""Opens a session to the specified resource.
@@ -145,7 +145,7 @@ class PyVisaLibrary(highlevel.VisaLibraryBase):
"""
try:
- open_timeout = int(open_timeout)
+ open_timeout = None if open_timeout is None else int(open_timeout)
except ValueError:
raise ValueError(
"open_timeout (%r) must be an integer (or compatible type)"
@@ -175,7 +175,7 @@ class PyVisaLibrary(highlevel.VisaLibraryBase):
Parameters
----------
- session : typin.VISASession
+ session : typing.VISASession
Unique logical identifier to a session.
Returns
@@ -752,7 +752,7 @@ class PyVisaLibrary(highlevel.VisaLibraryBase):
Return value of the library call.
"""
- pass
+ return StatusCode.error_nonimplemented_operation
def discard_events(
self,
@@ -779,4 +779,4 @@ class PyVisaLibrary(highlevel.VisaLibraryBase):
Return value of the library call.
"""
- pass
+ return StatusCode.error_nonimplemented_operation
diff --git a/pyvisa_py/protocols/hislip.py b/pyvisa_py/protocols/hislip.py
new file mode 100644
index 0000000..60c95e5
--- /dev/null
+++ b/pyvisa_py/protocols/hislip.py
@@ -0,0 +1,773 @@
+"""
+ Python implementation of HiSLIP protocol. Based on the HiSLIP spec:
+
+ http://www.ivifoundation.org/downloads/Class%20Specifications/IVI-6.1_HiSLIP-1.1-2011-02-24.pdf
+"""
+
+import socket
+import struct
+import time
+from typing import Dict, Optional, Tuple
+
+PORT = 4880
+
+MESSAGETYPE_STR: Dict[int, str] = {
+ 0: "Initialize",
+ 1: "InitializeResponse",
+ 2: "FatalError",
+ 3: "Error",
+ 4: "AsyncLock",
+ 5: "AsyncLockResponse",
+ 6: "Data",
+ 7: "DataEnd",
+ 8: "DeviceClearComplete",
+ 9: "DeviceClearAcknowledge",
+ 10: "AsyncRemoteLocalControl",
+ 11: "AsyncRemoteLocalResponse",
+ 12: "Trigger",
+ 13: "Interrupted",
+ 14: "AsyncInterrupted",
+ 15: "AsyncMaxMsgSize",
+ 16: "AsyncMaxMsgSizeResponse",
+ 17: "AsyncInitialize",
+ 18: "AsyncInitializeResponse",
+ 19: "AsyncDeviceClear",
+ 20: "AsyncServiceRequest",
+ 21: "AsyncStatusQuery",
+ 22: "AsyncStatusResponse",
+ 23: "AsyncDeviceClearAcknowledge",
+ 24: "AsyncLockInfo",
+ 25: "AsyncLockInfoResponse",
+ 26: "GetDescriptors",
+ 27: "GetDescriptorsResponse",
+ 28: "StartTLS",
+ 29: "AsyncStartTLS",
+ 30: "AsyncStartTLSResponse",
+ 31: "EndTLS",
+ 32: "AsyncEndTLS",
+ 33: "AsyncEndTLSResponse",
+ 34: "GetSaslMechanismList",
+ 35: "GetSaslMechanismListResponse",
+ 36: "AuthenticationStart",
+ 37: "AuthenticationExchange",
+ 38: "AuthenticationResult",
+ # reserved for future use 39-127 inclusive
+ # VendorSpecific 128-255 inclusive
+}
+MESSAGETYPE: Dict[str, int] = {value: key for (key, value) in MESSAGETYPE_STR.items()}
+
+FATALERRORMESSAGE: Dict[int, str] = {
+ 0: "Unidentified error",
+ 1: "Poorly formed message header",
+ 2: "Attempt to use connection without both channels established",
+ 3: "Invalid Initialization sequence",
+ 4: "Server refused connection due to maximum number of clients exceeded",
+ 5: "Secure connection failed",
+ # 6-127: reserved for HiSLIP extensions
+ # 128-255: device defined errors
+}
+FATALERRORCODE: Dict[str, int] = {
+ value: key for (key, value) in FATALERRORMESSAGE.items()
+}
+
+ERRORMESSAGE: Dict[int, str] = {
+ 0: "Unidentified error",
+ 1: "Unrecognized Message Type",
+ 2: "Unrecognized control code",
+ 3: "Unrecognized Vendor Defined Message",
+ 4: "Message too large",
+ 5: "Authentication failed",
+ # 6-127: Reserved
+ # 128-255: Device defined errors
+}
+ERRORCODE: Dict[str, int] = {value: key for (key, value) in ERRORMESSAGE.items()}
+
+LOCKCONTROLCODE: Dict[str, int] = {
+ "release": 0,
+ "request": 1,
+}
+
+LOCKRESPONSE: Dict[int, str] = {
+ 0: "failure",
+ 1: "success", # or "success exclusive"
+ 2: "success shared",
+ 3: "error",
+}
+
+REMOTELOCALCONTROLCODE: Dict[str, int] = {
+ "disableRemote": 0,
+ "enableRemote": 1,
+ "disableAndGTL": 2,
+ "enableAndGotoRemote": 3,
+ "enableAndLockoutLocal": 4,
+ "enableAndGTRLLO": 5,
+ "justGTL": 6,
+}
+
+HEADER_FORMAT = "!2sBBIQ"
+# ! = network order,
+# 2s = prologue ('HS'),
+# B = message type (unsigned byte),
+# B = control code (unsigned byte),
+# I = message parameter (unsigned int),
+# Q = payload length (unsigned long long)
+HEADER_SIZE = struct.calcsize(HEADER_FORMAT)
+
+DEFAULT_MAX_MSG_SIZE = 1 << 20 # from VISA spec
+
+
+#########################################################################################
+
+
+def receive_flush(sock: socket.socket, recv_len: int) -> None:
+ """
+ receive exactly 'recv_len' bytes from 'sock'.
+ no explicit timeout is specified, since it is assumed
+ that a call to select indicated that data is available.
+ received data is thrown away and nothing is returned
+ """
+ # limit the size of the recv_buffer to something moderate
+ # in order to limit the impact on virtual memory
+ recv_buffer = bytearray(min(1 << 20, recv_len))
+ bytes_recvd = 0
+
+ while bytes_recvd < recv_len:
+ request_size = min(len(recv_buffer), recv_len - bytes_recvd)
+ data_len = sock.recv_into(recv_buffer, request_size)
+ bytes_recvd += data_len
+
+
+def receive_exact(sock: socket.socket, recv_len: int) -> bytes:
+ """
+ receive exactly 'recv_len' bytes from 'sock'.
+ no explicit timeout is specified, since it is assumed
+ that a call to select indicated that data is available.
+ returns a bytearray containing the received data.
+ """
+ recv_buffer = bytearray(recv_len)
+ receive_exact_into(sock, recv_buffer)
+ return recv_buffer
+
+
+def receive_exact_into(sock: socket.socket, recv_buffer: bytes) -> None:
+ """
+ receive data from 'sock' to exactly fill 'recv_buffer'.
+ no explicit timeout is specified, since it is assumed
+ that a call to select indicated that data is available.
+ """
+ view = memoryview(recv_buffer)
+ recv_len = len(recv_buffer)
+ bytes_recvd = 0
+
+ while bytes_recvd < recv_len:
+ request_size = recv_len - bytes_recvd
+ data_len = sock.recv_into(view, request_size)
+ bytes_recvd += data_len
+ view = view[data_len:]
+
+ if bytes_recvd > recv_len:
+ raise MemoryError("socket.recv_into scribbled past end of recv_buffer")
+
+
+def send_msg(
+ sock: socket.socket,
+ msg_type: str,
+ control_code: int,
+ message_parameter: Optional[int],
+ payload: bytes = b"",
+) -> None:
+ """Send a message on sock w/ payload."""
+ msg = bytearray(
+ struct.pack(
+ HEADER_FORMAT,
+ b"HS",
+ MESSAGETYPE[msg_type],
+ control_code,
+ message_parameter or 0,
+ len(payload),
+ )
+ )
+ # txdecode(msg, payload) # uncomment for debugging
+ msg.extend(payload)
+ sock.sendall(msg)
+
+
+class RxHeader:
+ """Generic base class for receiving messages.
+
+ specific protocol responses subclass this class.
+ """
+
+ def __init__(
+ self,
+ sock: socket.socket,
+ expected_message_type: Optional[str] = None,
+ ) -> None:
+ """receive and decode the HiSLIP message header"""
+ self.header = receive_exact(sock, HEADER_SIZE)
+ # rxdecode(self.header) # uncomment for debugging
+ (
+ prologue,
+ msg_type,
+ self.control_code,
+ self.message_parameter,
+ self.payload_length,
+ ) = struct.unpack(HEADER_FORMAT, self.header)
+
+ if prologue != b"HS":
+ # XXX we should send a 'Fatal Error' to the server, close the
+ # sockets, then raise an exception
+ raise RuntimeError("protocol synchronization error")
+
+ if msg_type not in MESSAGETYPE_STR:
+ # XXX we should send 'Unrecognized message type' to the
+ # server and discard this packet plus any payload.
+ raise RuntimeError("unrecognized message type: %d" % msg_type)
+
+ self.msg_type = MESSAGETYPE_STR[msg_type]
+
+ if expected_message_type is not None and self.msg_type != expected_message_type:
+ # XXX we should send an 'Error: Unidentified Error' to the server
+ # and discard this packet plus any payload
+ payload = (
+ (": " + str(receive_exact(sock, self.payload_length)))
+ if self.payload_length > 0
+ else b""
+ )
+ raise RuntimeError(
+ "expected message type '%s', received '%s%s'"
+ % (expected_message_type, self.msg_type, payload)
+ )
+
+ if self.msg_type == "DataEnd" or self.msg_type == "Data":
+ assert self.control_code == 0
+ self.message_id = self.message_parameter
+
+
+class InitializeResponse(RxHeader):
+ def __init__(self, sock: socket.socket) -> None:
+ super().__init__(sock, "InitializeResponse")
+ assert self.payload_length == 0
+ self.overlap = bool(self.control_code)
+ self.version, self.session_id = struct.unpack("!4xHH8x", self.header)
+
+
+class AsyncInitializeResponse(RxHeader):
+ def __init__(self, sock: socket.socket) -> None:
+ super().__init__(sock, "AsyncInitializeResponse")
+ assert self.control_code == 0
+ assert self.payload_length == 0
+ self.vendor_id = struct.unpack("!4x4s8x", self.header)
+
+
+class AsyncMaxMsgSizeResponse(RxHeader):
+ def __init__(self, sock: socket.socket) -> None:
+ super().__init__(sock, "AsyncMaxMsgSizeResponse")
+ assert self.control_code == 0
+ assert self.message_parameter == 0
+ assert self.payload_length == 8
+ payload = receive_exact(sock, self.payload_length)
+ self.max_msg_size = struct.unpack("!Q", payload)[0]
+
+
+class AsyncDeviceClearAcknowledge(RxHeader):
+ def __init__(self, sock: socket.socket) -> None:
+ super().__init__(sock, "AsyncDeviceClearAcknowledge")
+ self.feature_bitmap = self.control_code
+ assert self.message_parameter == 0
+ assert self.payload_length == 0
+
+
+class AsyncInterrupted(RxHeader):
+ def __init__(self, sock: socket.socket) -> None:
+ super().__init__(sock, "AsyncInterrupted")
+ assert self.control_code == 0
+ self.message_id = self.message_parameter
+ assert self.payload_length == 0
+
+
+class AsyncLockInfoResponse(RxHeader):
+ def __init__(self, sock: socket.socket) -> None:
+ super().__init__(sock, "AsyncLockInfoResponse")
+ self.exclusive_lock = self.control_code # 0: no lock, 1: lock granted
+ self.clients_holding_locks = self.message_parameter
+ assert self.payload_length == 0
+
+
+class AsyncLockResponse(RxHeader):
+ def __init__(self, sock: socket.socket) -> None:
+ super().__init__(sock, "AsyncLockResponse")
+ self.lock_response = LOCKRESPONSE[self.control_code]
+ assert self.message_parameter == 0
+ assert self.payload_length == 0
+
+
+class AsyncRemoteLocalResponse(RxHeader):
+ def __init__(self, sock: socket.socket) -> None:
+ super().__init__(sock, "AsyncRemoteLocalResponse")
+ assert self.control_code == 0
+ assert self.message_parameter == 0
+ assert self.payload_length == 0
+
+
+class AsyncServiceRequest(RxHeader):
+ def __init__(self, sock: socket.socket) -> None:
+ super().__init__(sock, "AsyncServiceRequest")
+ self.server_status = self.control_code
+ assert self.message_parameter == 0
+ assert self.payload_length == 0
+
+
+class AsyncStatusResponse(RxHeader):
+ def __init__(self, sock: socket.socket) -> None:
+ super().__init__(sock, "AsyncStatusResponse")
+ self.server_status = self.control_code
+ assert self.message_parameter == 0
+ assert self.payload_length == 0
+
+
+class DeviceClearAcknowledge(RxHeader):
+ def __init__(self, sock: socket.socket) -> None:
+ super().__init__(sock, "DeviceClearAcknowledge")
+ self.feature_bitmap = self.control_code
+ assert self.message_parameter == 0
+ assert self.payload_length == 0
+
+
+class Interrupted(RxHeader):
+ def __init__(self, sock: socket.socket) -> None:
+ super().__init__(sock, "Interrupted")
+ assert self.control_code == 0
+ self.message_id = self.message_parameter
+ assert self.payload_length == 0
+
+
+class Error(RxHeader):
+ def __init__(self, sock: socket.socket) -> None:
+ super().__init__(sock, "Error")
+ self.error_code = ERRORMESSAGE[self.control_code]
+ assert self.message_parameter == 0
+ self.error_message = receive_exact(sock, self.payload_length)
+
+
+class FatalError(RxHeader):
+ def __init__(self, sock: socket.socket) -> None:
+ super().__init__(sock, "FatalError")
+ self.error_code = FATALERRORMESSAGE[self.control_code]
+ assert self.message_parameter == 0
+ self.error_message = receive_exact(sock, self.payload_length)
+
+
+class Instrument:
+ """
+ this is the principal export from this module. it opens up a HiSLIP
+ connection to the instrument at the specified IP address.
+ """
+
+ def __init__(
+ self, ip_addr: str, timeout: Optional[float] = None, port: int = PORT
+ ) -> None:
+ # init transaction:
+ # C->S: Initialize
+ # S->C: InitializeResponse
+ # C->S: AsyncInitialize
+ # S->C: AsyncInitializeResponse
+
+ timeout = timeout or 5.0
+
+ # open the synchronous socket and send an initialize packet
+ self._sync = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self._sync.connect((ip_addr, port))
+ self._sync.settimeout(timeout)
+ self._sync.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+ init = self.initialize()
+ if init.overlap != 0:
+ print("**** prefer overlap = %d" % init.overlap)
+
+ # open the asynchronous socket and send an initialize packet
+ self._async = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self._async.connect((ip_addr, port))
+ self._async.settimeout(timeout)
+ self._async.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+ self._async_init = self.async_initialize(session_id=init.session_id)
+
+ # initialize variables
+ self.max_msg_size = DEFAULT_MAX_MSG_SIZE
+ self.keepalive = False
+ self.timeout = timeout
+ self._rmt = 0
+ self._expected_message_id: Optional[int] = None
+ self._message_id = 0xFFFF_FF00
+ self._last_message_id: Optional[int] = None
+ self._msg_type: str = ""
+ self._payload_remaining: int = 0
+
+ # ================ #
+ # MEMBER FUNCTIONS #
+ # ================ #
+
+ def close(self) -> None:
+ self._sync.close()
+ self._async.close()
+
+ @property
+ def timeout(self) -> float:
+ """Timeout value in seconds for both the sync and async sockets"""
+ return self._timeout
+
+ @timeout.setter
+ def timeout(self, val: float) -> None:
+ """Timeout value in seconds for both the sync and async sockets"""
+ self._timeout = val
+ self._sync.settimeout(self._timeout)
+ self._async.settimeout(self._timeout)
+
+ @property
+ def max_msg_size(self) -> int:
+ """Maximum HiSLIP message size in bytes."""
+ return self._max_msg_size
+
+ @max_msg_size.setter
+ def max_msg_size(self, size: int) -> None:
+ self._max_msg_size = self.async_maximum_message_size(size)
+
+ @property
+ def keepalive(self) -> bool:
+ """Status of the TCP keepalive.
+
+ Keepalive is on/off for both the sync and async sockets
+
+ If a connection is dropped as a result of “keepalives”, the error code
+ VI_ERROR_CONN_LOST is returned to current and subsequent I/O
+ calls on the session.
+
+ """
+ return self._keepalive
+
+ @keepalive.setter
+ def keepalive(self, keepalive: bool) -> None:
+ self._keepalive = bool(keepalive)
+ self._sync.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, bool(keepalive))
+ self._async.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, bool(keepalive))
+
+ def send(self, data: bytes) -> int:
+ """Send the data on the synchronous channel.
+
+ More than one packet may be necessary in order
+ to not exceed max_payload_size.
+ """
+ # print(f"send({data=})") # uncomment for debugging
+ data_view = memoryview(data)
+ num_bytes_to_send = len(data)
+ max_payload_size = self._max_msg_size - HEADER_SIZE
+
+ # send the data in chunks of max_payload_size bytes at a time
+ while num_bytes_to_send > 0:
+ if num_bytes_to_send <= max_payload_size:
+ assert len(data_view) == num_bytes_to_send
+ self._send_data_end_packet(data_view)
+ bytes_sent = num_bytes_to_send
+ else:
+ self._send_data_packet(data_view[:max_payload_size])
+ bytes_sent = max_payload_size
+
+ data_view = data_view[bytes_sent:]
+ num_bytes_to_send -= bytes_sent
+
+ return len(data)
+
+ def receive(self, max_len: int = 4096) -> bytes:
+ """Receive data on the synchronous channel.
+
+ Terminate after max_len bytes or after receiving a DataEnd message
+ """
+
+ # print(f"receive({max_len=})") # uncomment for debugging
+ # if we aren't already receiving, initialize the _expected_message_id
+ # and the payload length
+ if self._expected_message_id is None:
+ self._expected_message_id = self._last_message_id
+ self._msg_type = ""
+ self._payload_remaining = 0
+
+ # receive data, terminating after len(recv_buffer) bytes or
+ # after receiving a DataEnd message.
+ #
+ # note the use of receive_exact_into (which calls socket.recv_into),
+ # avoiding unnecessary copies.
+ #
+ recv_buffer = bytearray(max_len)
+ view = memoryview(recv_buffer)
+ bytes_recvd = 0
+
+ while bytes_recvd < max_len:
+ if self._payload_remaining <= 0:
+ if self._msg_type == "DataEnd":
+ # truncate to the actual number of bytes received
+ recv_buffer = recv_buffer[:bytes_recvd]
+ break
+ self._msg_type, self._payload_remaining = self._next_data_header()
+
+ request_size = min(self._payload_remaining, max_len - bytes_recvd)
+ receive_exact_into(self._sync, view[:request_size])
+ self._payload_remaining -= request_size
+ bytes_recvd += request_size
+ view = view[request_size:]
+
+ if bytes_recvd > max_len:
+ raise MemoryError("scribbled past end of recv_buffer")
+
+ # if there is no data remaining, set the RMT flag and set the
+ # _expected_message_id to None
+ if self._payload_remaining == 0 and self._msg_type == "DataEnd":
+ #
+ # From IEEE Std 488.2: Response Message Terminator.
+ #
+ # RMT is the new-line accompanied by END sent from the server
+ # to the client at the end of a response. Note that with HiSLIP
+ # this is implied by the DataEND message.
+ #
+ self._rmt = 1
+ self._expected_message_id = None
+
+ return recv_buffer
+
+ def _next_data_header(self) -> Tuple[str, int]:
+ """
+ receive the next data header (either Data or DataEnd), check the
+ message_id, and return the msg_type and payload_length.
+ """
+ while True:
+ header = RxHeader(self._sync)
+
+ if header.msg_type in ("Data", "DataEnd"):
+ # When receiving Data messages if the MessageID is not 0xffff ffff,
+ # then verify that the MessageID indicated in the Data message is
+ # the MessageID that the client sent to the server with the most
+ # recent Data, DataEND or Trigger message.
+ #
+ # If the MessageIDs do not match, the client shall clear any Data
+ # responses already buffered and discard the offending Data message
+
+ if (
+ header.message_parameter == 0xFFFF_FFFF
+ or header.message_parameter == self._expected_message_id
+ ):
+ break
+
+ # we're out of sync. flush this message and continue.
+ receive_flush(self._sync, header.payload_length)
+
+ return header.msg_type, header.payload_length
+
+ def device_clear(self) -> None:
+ feature = self.async_device_clear()
+ # Abandon pending messages and wait for in-process synchronous messages
+ # to complete.
+ time.sleep(0.1)
+ # Indicate to server that synchronous channel is cleared out.
+ self.device_clear_complete(feature)
+ # reset messageID and resume normal opreation
+ self._message_id = 0xFFFF_FF00
+
+ def initialize(
+ self,
+ version: tuple = (1, 0),
+ vendor_id: bytes = b"xx",
+ sub_address: bytes = b"hislip0",
+ ) -> InitializeResponse:
+ """
+ perform an Initialize transaction.
+ returns the InitializeResponse header.
+ """
+ major, minor = version
+ header = struct.pack(
+ "!2sBBBB2sQ",
+ b"HS",
+ MESSAGETYPE["Initialize"],
+ 0,
+ major,
+ minor,
+ vendor_id,
+ len(sub_address),
+ )
+ # txdecode(header, sub_address) # uncomment for debugging
+ self._sync.sendall(header + sub_address)
+ return InitializeResponse(self._sync)
+
+ def async_initialize(self, session_id: int) -> AsyncInitializeResponse:
+ """
+ perform an AsyncInitialize transaction.
+ returns the AsyncInitializeResponse header.
+ """
+ send_msg(self._async, "AsyncInitialize", 0, session_id)
+ return AsyncInitializeResponse(self._async)
+
+ def async_maximum_message_size(self, size: int) -> int:
+ """
+ perform an AsyncMaxMsgSize transaction.
+ returns the max_msg_size from the AsyncMaxMsgSizeResponse packet.
+ """
+ # maximum_message_size transaction:
+ # C->S: AsyncMaxMsgSize
+ # S->C: AsyncMaxMsgSizeResponse
+ payload = struct.pack("!Q", size)
+ send_msg(self._async, "AsyncMaxMsgSize", 0, 0, payload)
+ response = AsyncMaxMsgSizeResponse(self._async)
+ return response.max_msg_size
+
+ def async_lock_info(self) -> int:
+ """
+ perform an AsyncLockInfo transaction.
+ returns the exclusive_lock from the AsyncLockInfoResponse packet.
+ """
+ # async_lock_info transaction:
+ # C->S: AsyncLockInfo
+ # S->C: AsyncLockInfoResponse
+ send_msg(self._async, "AsyncLockInfo", 0, 0)
+ response = AsyncLockInfoResponse(self._async)
+ return response.exclusive_lock
+
+ def async_lock_request(self, timeout: float, lock_string: str = "") -> str:
+ """
+ perform an AsyncLock request transaction.
+ returns the lock_response from the AsyncLockResponse packet.
+ """
+ # async_lock transaction:
+ # C->S: AsyncLock
+ # S->C: AsyncLockResponse
+ ctrl_code = LOCKCONTROLCODE["request"]
+ timeout_ms = int(1e3 * timeout)
+ send_msg(self._async, "AsyncLock", ctrl_code, timeout_ms, lock_string.encode())
+ response = AsyncLockResponse(self._async)
+ return response.lock_response
+
+ def async_lock_release(self) -> str:
+ """
+ perform an AsyncLock release transaction.
+ returns the lock_response from the AsyncLockResponse packet.
+ """
+ # async_lock transaction:
+ # C->S: AsyncLock
+ # S->C: AsyncLockResponse
+ ctrl_code = LOCKCONTROLCODE["release"]
+ send_msg(self._async, "AsyncLock", ctrl_code, self._last_message_id)
+ response = AsyncLockResponse(self._async)
+ return response.lock_response
+
+ def async_remote_local_control(self, remotelocalcontrol: str) -> None:
+ """
+ perform an AsyncRemoteLocalControl transaction.
+ """
+ # remote_local transaction:
+ # C->S: AsyncRemoteLocalControl
+ # S->C: AsyncRemoteLocalResponse
+ ctrl_code = REMOTELOCALCONTROLCODE[remotelocalcontrol]
+ send_msg(
+ self._async, "AsyncRemoteLocalControl", ctrl_code, self._last_message_id
+ )
+ AsyncRemoteLocalResponse(self._async)
+
+ def async_status_query(self) -> int:
+ """
+ perform an AsyncStatusQuery transaction.
+ returns the server_status from the AsyncStatusResponse packet.
+ """
+ # async_status_query transaction:
+ # C->S: AsyncStatusQuery
+ # S->C: AsyncStatusResponse
+ send_msg(self._async, "AsyncStatusQuery", self._rmt, self._message_id)
+ self._rmt = 0
+ response = AsyncStatusResponse(self._async)
+ return response.server_status
+
+ def async_device_clear(self) -> int:
+ """
+ perform an AsyncDeviceClear transaction.
+ returns the feature_bitmap from the AsyncDeviceClearAcknowledge packet.
+ """
+ send_msg(self._async, "AsyncDeviceClear", 0, 0)
+ response = AsyncDeviceClearAcknowledge(self._async)
+ return response.feature_bitmap
+
+ def device_clear_complete(self, feature_bitmap: int) -> int:
+ """
+ perform a DeviceClear transaction.
+ returns the feature_bitmap from the DeviceClearAcknowledge packet.
+ """
+ send_msg(self._sync, "DeviceClearComplete", feature_bitmap, 0)
+ response = DeviceClearAcknowledge(self._sync)
+ return response.feature_bitmap
+
+ def trigger(self) -> None:
+ """send a Trigger packet on the sync channel"""
+ send_msg(self._sync, "Trigger", self._rmt, self._message_id)
+ self._rmt = 0
+ self._last_message_id = self._message_id
+ self._message_id = (self._message_id + 2) & 0xFFFF_FFFF
+
+ def _send_data_packet(self, payload: bytes) -> None:
+ """send a Data packet on the sync channel"""
+ send_msg(self._sync, "Data", self._rmt, self._message_id, payload)
+ self._rmt = 0
+ self._last_message_id = self._message_id
+ self._message_id = (self._message_id + 2) & 0xFFFF_FFFF
+
+ def _send_data_end_packet(self, payload: bytes) -> None:
+ """send a DataEnd packet on the sync channel"""
+ send_msg(self._sync, "DataEnd", self._rmt, self._message_id, payload)
+ self._rmt = 0
+ self._last_message_id = self._message_id
+ self._message_id = (self._message_id + 2) & 0xFFFF_FFFF
+
+ def fatal_error(self, error: str, error_message: str = "") -> None:
+ err_msg = (error_message or error).encode()
+ send_msg(self._sync, "FatalError", FATALERRORCODE[error], 0, err_msg)
+
+ def error(self, error: str, error_message: str = "") -> None:
+ err_msg = (error_message or error).encode()
+ send_msg(self._sync, "Error", ERRORCODE[error], 0, err_msg)
+
+
+# the following two routines are only used for debugging.
+# they are commented out because their f-strings use a feature
+# that is a syntax error in Python versions < 3.7
+
+# def rxdecode(header):
+# (
+# prologue,
+# msg_type,
+# control_code,
+# message_parameter,
+# payload_length,
+# ) = struct.unpack(HEADER_FORMAT, header)
+#
+# msg_type = MESSAGETYPE_STR[msg_type]
+# print(
+# f"Rx: {prologue=}, "
+# f"{msg_type=}, "
+# f"{control_code=}, "
+# f"{message_parameter=}, "
+# f"{payload_length=}"
+# )
+
+
+# def txdecode(header, payload=b""):
+# (
+# prologue,
+# msg_type,
+# control_code,
+# message_parameter,
+# payload_length,
+# ) = struct.unpack(HEADER_FORMAT, header)
+#
+# msg_type = MESSAGETYPE_STR[msg_type]
+# print(
+# f"Tx: {prologue=}, "
+# f"{msg_type=}, "
+# f"{control_code=}, "
+# f"{message_parameter=}, "
+# f"{payload_length=}, "
+# f"{len(payload)=}, "
+# f"{bytes(payload[:20]).decode('iso-8859-1')!r}"
+# )
diff --git a/pyvisa_py/protocols/rpc.py b/pyvisa_py/protocols/rpc.py
index 859b7a9..935bbaf 100644
--- a/pyvisa_py/protocols/rpc.py
+++ b/pyvisa_py/protocols/rpc.py
@@ -37,7 +37,6 @@ class MessagegType(enum.IntEnum):
class AuthorizationFlavor(enum.IntEnum):
-
null = 0
unix = 1
short = 2
@@ -45,13 +44,11 @@ class AuthorizationFlavor(enum.IntEnum):
class ReplyStatus(enum.IntEnum):
-
accepted = 0
denied = 1
class AcceptStatus(enum.IntEnum):
-
#: RPC executed successfully
success = 0
@@ -69,7 +66,6 @@ class AcceptStatus(enum.IntEnum):
class RejectStatus(enum.IntEnum):
-
#: RPC version number != 2
rpc_mismatch = 0
@@ -279,12 +275,23 @@ class Client(object):
# Record-Marking standard support
+def _sendto(sock, data, address):
+ """
+ loops calling sock.sendto() until all data is sent.
+ """
+ ptr = 0
+ while data[ptr:]:
+ ptr += sock.sendto(data[ptr:], address)
+
+
+# XXX sendfrag() should have been deleted when it was inlined into
+# _sendrecord() during refactoring
def sendfrag(sock, last, frag):
x = len(frag)
if last:
x = x | 0x80000000
header = struct.pack(">I", x)
- sock.send(header + frag)
+ sock.sendall(header + frag)
def _sendrecord(sock, record, fragsize=None, timeout=None):
@@ -292,7 +299,7 @@ def _sendrecord(sock, record, fragsize=None, timeout=None):
if timeout is not None:
r, w, x = select.select([], [sock], [], timeout)
if sock not in w:
- msg = "socket.timeout: The instrument seems to have stopped " "responding."
+ msg = "socket.timeout: The instrument seems to have stopped responding."
raise socket.timeout(msg)
last = False
@@ -306,12 +313,11 @@ def _sendrecord(sock, record, fragsize=None, timeout=None):
if last:
fragsize = fragsize | 0x80000000
header = struct.pack(">I", fragsize)
- sock.send(header + record[:fragsize])
+ sock.sendall(header + record[:fragsize])
record = record[fragsize:]
def _recvrecord(sock, timeout, read_fun=None, min_packages=0):
-
record = bytearray()
buffer = bytearray()
if not read_fun:
@@ -340,7 +346,6 @@ def _recvrecord(sock, timeout, read_fun=None, min_packages=0):
# time, when loop shall finish
finish_time = time.time() + timeout if timeout is not None else 0
while True:
-
# if more data for the current fragment is needed, use select
# to wait for read ready, max `select_timeout` seconds
if len(buffer) < exp_length:
@@ -443,7 +448,8 @@ class RawTCPClient(Client):
def __init__(self, host, prog, vers, port, open_timeout=5000):
Client.__init__(self, host, prog, vers, port)
- self.connect((open_timeout / 1000.0) + 1.0)
+ open_timeout = open_timeout if open_timeout is not None else 5000
+ self.connect(1e-3 * open_timeout)
# self.timeout defaults higher than the default 2 second VISA timeout,
# ensuring that VISA timeouts take precedence.
self.timeout = 4.0
@@ -495,14 +501,24 @@ class RawTCPClient(Client):
# This is a workaround for misbehaving instruments.
except AttributeError:
min_packages = 0
- reply = _recvrecord(self.sock, self.timeout, min_packages=min_packages)
- u = self.unpacker
- u.reset(reply)
- xid, verf = u.unpack_replyheader()
- if xid != self.lastxid:
- # Can't really happen since this is TCP...
- msg = "wrong xid in reply {0} instead of {1}"
- raise RPCError(msg.format(xid, self.lastxid))
+
+ while True:
+ reply = _recvrecord(self.sock, self.timeout, min_packages=min_packages)
+ u = self.unpacker
+ u.reset(reply)
+ xid, verf = u.unpack_replyheader()
+ if xid == self.lastxid:
+ # xid matches, we're done
+ return
+ elif xid < self.lastxid:
+ # Stale data in buffer due to interruption
+ # Discard and fetch another record
+ continue
+ else:
+ # xid larger than expected - packet from the future?
+ raise RPCError(
+ "wrong xid in reply %r instead of %r" % (xid, self.lastxid)
+ )
class RawUDPClient(Client):
@@ -525,7 +541,7 @@ class RawUDPClient(Client):
def do_call(self):
call = self.packer.get_buf()
- self.sock.send(call)
+ self.sock.sendall(call)
BUFSIZE = 8192 # Max UDP buffer size
timeout = 1
@@ -540,7 +556,7 @@ class RawUDPClient(Client):
raise RPCError("timeout")
if timeout < 25:
timeout = timeout * 2
- self.sock.send(call)
+ self.sock.sendall(call)
continue
reply = self.sock.recv(BUFSIZE)
u = self.unpacker
@@ -576,7 +592,7 @@ class RawBroadcastUDPClient(RawUDPClient):
if pack_func:
pack_func(args)
call = self.packer.get_buf()
- self.sock.sendto(call, (self.host, self.port))
+ _sendto(self.sock, call, (self.host, self.port))
BUFSIZE = 8192 # Max UDP buffer size (for reply)
replies = []
@@ -608,6 +624,52 @@ class RawBroadcastUDPClient(RawUDPClient):
self.reply_handler(reply, fromaddr)
return replies
+ def send_call(self, proc, args, pack_func):
+ if pack_func is None and args is not None:
+ raise TypeError("non-null args with null pack_func")
+ self.start_call(proc)
+ if pack_func:
+ pack_func(args)
+ call = self.packer.get_buf()
+ try:
+ _sendto(self.sock, call, (self.host, self.port))
+ except OSError as exc:
+ raise RPCError("unable to send broadcast") from exc
+
+ def recv_call(self, unpack_func):
+ BUFSIZE = 8192 # Max UDP buffer size (for reply)
+ replies = []
+ if unpack_func is None:
+
+ def dummy():
+ pass
+
+ unpack_func = dummy
+ while 1:
+ r, w, x = [self.sock], [], []
+ if select:
+ if self.timeout is None:
+ r, w, x = select.select(r, w, x)
+ else:
+ r, w, x = select.select(r, w, x, self.timeout)
+ if self.sock not in r:
+ break
+ try:
+ reply, fromaddr = self.sock.recvfrom(BUFSIZE)
+ except OSError as exc:
+ raise RPCError("unable to recieve broadcast") from exc
+ u = self.unpacker
+ u.reset(reply)
+ xid, verf = u.unpack_replyheader()
+ if xid != self.lastxid:
+ continue
+ reply = unpack_func()
+ self.unpacker.done()
+ replies.append((reply, fromaddr))
+ if self.reply_handler:
+ self.reply_handler(reply, fromaddr)
+ return replies
+
# Port mapper interface
@@ -708,6 +770,18 @@ class PartialPortMapperClient(object):
self.unpacker.unpack_uint,
)
+ def send_port(self, mapping):
+ return self.send_call(
+ PortMapperVersion.get_port,
+ mapping,
+ self.packer.pack_mapping,
+ )
+
+ def recv_port(self, mapping):
+ return self.recv_call(
+ self.unpacker.unpack_uint,
+ )
+
def dump(self):
return self.make_call(
PortMapperVersion.dump, None, None, self.unpacker.unpack_pmaplist
@@ -983,4 +1057,4 @@ class UDPServer(Server):
call, host_port = self.sock.recvfrom(8192)
reply = self.handle(call)
if reply is not None:
- self.sock.sendto(reply, host_port)
+ _sendto(self.sock, reply, host_port)
diff --git a/pyvisa_py/protocols/usbraw.py b/pyvisa_py/protocols/usbraw.py
index 1f21bf7..8b342e8 100644
--- a/pyvisa_py/protocols/usbraw.py
+++ b/pyvisa_py/protocols/usbraw.py
@@ -28,8 +28,7 @@ def find_raw_devices(
class USBRawDevice(USBRaw):
-
- RECV_CHUNK = 1024 ** 2
+ RECV_CHUNK = 1024**2
find_devices = staticmethod(find_raw_devices)
diff --git a/pyvisa_py/protocols/usbtmc.py b/pyvisa_py/protocols/usbtmc.py
index 7c67b93..f12493f 100644
--- a/pyvisa_py/protocols/usbtmc.py
+++ b/pyvisa_py/protocols/usbtmc.py
@@ -182,7 +182,7 @@ class USBRaw(object):
serial_number=None,
device_filters=None,
timeout=None,
- **kwargs
+ **kwargs,
):
super(USBRaw, self).__init__()
@@ -213,14 +213,35 @@ class USBRaw(object):
pass
try:
- self.usb_dev.set_configuration()
- except usb.core.USBError as e:
- raise Exception("failed to set configuration\n %s" % e)
-
- try:
- self.usb_dev.set_interface_altsetting()
+ cfg = self.usb_dev.get_active_configuration()
except usb.core.USBError:
- pass
+ cfg = None
+
+ if cfg is None:
+ try:
+ self.usb_dev.set_configuration()
+ cfg = self.usb_dev.get_active_configuration()
+ except usb.core.USBError as e:
+ raise Exception("failed to set configuration\n %s" % e)
+
+ intf = cfg[(0, 0)]
+
+ # Check if the interface exposes multiple alternative setting and
+ # set one only if there is more than one.
+ if (
+ len(
+ tuple(
+ usb.util.find_descriptor(
+ cfg, find_all=True, bInterfaceNumber=intf.bInterfaceNumber
+ )
+ )
+ )
+ > 1
+ ):
+ try:
+ self.usb_dev.set_interface_altsetting()
+ except usb.core.USBError:
+ pass
self.usb_intf = self._find_interface(self.usb_dev, self.INTERFACE)
@@ -277,9 +298,8 @@ class USBRaw(object):
class USBTMC(USBRaw):
-
# Maximum number of bytes per transfer (for sending and receiving).
- RECV_CHUNK = 1024 ** 2
+ RECV_CHUNK = 1024**2
find_devices = staticmethod(find_tmc_devices)
@@ -289,9 +309,6 @@ class USBTMC(USBRaw):
self.usb_intf, usb.ENDPOINT_IN, usb.ENDPOINT_TYPE_INTERRUPT
)
- self.usb_dev.reset()
- self.usb_dev.set_configuration()
-
time.sleep(0.01)
self._capabilities = self._get_capabilities()
@@ -438,7 +455,6 @@ class USBTMC(USBRaw):
return size
def read(self, size):
-
recv_chunk = self.RECV_CHUNK
if size > 0 and size < recv_chunk:
recv_chunk = size
diff --git a/pyvisa_py/serial.py b/pyvisa_py/serial.py
index f057016..087ab45 100644
--- a/pyvisa_py/serial.py
+++ b/pyvisa_py/serial.py
@@ -146,7 +146,7 @@ class SerialSession(Session):
checker = lambda current: False
elif end_in == SerialTermination.last_bit:
- mask = 2 ** self.interface.bytesize
+ mask = 2**self.interface.bytesize
checker = lambda current: bool(current[-1] & mask)
elif end_in == SerialTermination.termination_char:
@@ -418,7 +418,7 @@ class SerialSession(Session):
if not isinstance(attribute_state, int):
return StatusCode.error_nonsupported_attribute_state
- if not 0 < attribute_state < 8:
+ if not 0 <= attribute_state < 8:
return StatusCode.error_nonsupported_attribute_state
try:
diff --git a/pyvisa_py/sessions.py b/pyvisa_py/sessions.py
index 82c7e18..36a7c1b 100644
--- a/pyvisa_py/sessions.py
+++ b/pyvisa_py/sessions.py
@@ -122,8 +122,8 @@ class Session(metaclass=abc.ABCMeta):
#: Session type as (Interface Type, Resource Class)
session_type: Tuple[constants.InterfaceType, str]
- #: Timeout in seconds to use when opening the resource.
- open_timeout: Optional[float]
+ #: Timeout in milliseconds to use when opening the resource.
+ open_timeout: Optional[int]
#: Value of the timeout in seconds used for general operation
timeout: Optional[float]
@@ -223,7 +223,8 @@ class Session(metaclass=abc.ABCMeta):
logger.warning(
"%s is already registered in the "
"ResourceManager. Overwriting with %s",
- ((interface_type, resource_class), python_class),
+ (interface_type, resource_class),
+ python_class,
)
python_class.session_type = (interface_type, resource_class)
@@ -258,7 +259,6 @@ class Session(metaclass=abc.ABCMeta):
"""
class _internal(Session):
-
#: Message detailing why no session is available.
session_issue: str = msg
@@ -278,7 +278,8 @@ class Session(metaclass=abc.ABCMeta):
logger.warning(
"%s is already registered in the ResourceManager. "
"Overwriting with unavailable %s",
- ((interface_type, resource_class), msg),
+ (interface_type, resource_class),
+ msg,
)
cls._session_classes[(interface_type, resource_class)] = _internal
@@ -288,7 +289,7 @@ class Session(metaclass=abc.ABCMeta):
resource_manager_session: VISARMSession,
resource_name: str,
parsed: Optional[rname.ResourceName] = None,
- open_timeout: Optional[float] = None,
+ open_timeout: Optional[int] = None,
) -> None:
if parsed is None:
parsed = rname.parse_resource_name(resource_name)
@@ -324,11 +325,11 @@ class Session(metaclass=abc.ABCMeta):
def after_parsing(self) -> None:
"""Override this method to provide custom initialization code, to be
- called after the resourcename is properly parsed
+ called after the resource name is properly parsed
ResourceSession can register resource specific attributes handling of
them into self.attrs.
- It is also possible to change handling of already registerd common
+ It is also possible to change handling of already registered common
attributes. List of attributes is available in pyvisa package:
* name is in constants module as: VI_ATTR_<NAME>
* validity of attribute for resource is defined module attributes,
@@ -678,7 +679,7 @@ class Session(metaclass=abc.ABCMeta):
session.
Does a few checks before and calls before dispatching to
- `_gst_attribute`.
+ `_set_attribute`.
Parameters
----------
diff --git a/pyvisa_py/tcpip.py b/pyvisa_py/tcpip.py
index 36e8734..ccbe42a 100644
--- a/pyvisa_py/tcpip.py
+++ b/pyvisa_py/tcpip.py
@@ -6,18 +6,39 @@
:license: MIT, see LICENSE for more details.
"""
+import ipaddress
import random
import select
import socket
import time
-from typing import Any, List, Optional, Tuple
+import warnings
+from typing import Any, Dict, List, Optional, Tuple, Type
from pyvisa import attributes, constants, errors, rname
from pyvisa.constants import ResourceAttribute, StatusCode
from . import common
-from .protocols import rpc, vxi11
-from .sessions import Session, UnknownAttribute
+from .protocols import hislip, rpc, vxi11
+from .sessions import Session, UnknownAttribute, VISARMSession
+
+# Let psutil be optional dependency
+try:
+ import psutil # type: ignore
+except ImportError:
+ psutil = None
+
+# Let zeroconf be optional dependency
+try:
+ import zeroconf # type: ignore
+except ImportError:
+ zeroconf = None # type: ignore
+
+# Let pyvicp be optional dependency
+try:
+ import pyvicp # type: ignore
+except ImportError:
+ pyvicp = None # type: ignore
+
# Conversion between VXI11 error codes and VISA status
# TODO this is so far a best guess, in particular 6 and 29 are likely wrong
@@ -41,8 +62,311 @@ VXI11_ERRORS_TO_VISA = {
@Session.register(constants.InterfaceType.tcpip, "INSTR")
class TCPIPInstrSession(Session):
+ """A class to dispatch to VXI11 or HiSLIP, based on the protocol."""
+
+ def __new__(
+ cls,
+ resource_manager_session: VISARMSession,
+ resource_name: str,
+ parsed=None,
+ open_timeout: Optional[int] = None,
+ ):
+ newcls: Type
+
+ if parsed is None:
+ parsed = rname.parse_resource_name(resource_name)
+
+ if parsed.lan_device_name.lower().startswith("hislip"):
+ newcls = TCPIPInstrHiSLIP
+
+ else:
+ newcls = TCPIPInstrVxi11
+
+ return newcls(resource_manager_session, resource_name, parsed, open_timeout)
+
+ @staticmethod
+ def list_resources(wait_time=1.0) -> List[str]:
+ return TCPIPInstrVxi11.list_resources() + TCPIPInstrHiSLIP.list_resources()
+
+ @classmethod
+ def get_low_level_info(cls) -> str:
+ vxi11 = "ok" if psutil is not None else "partial (psutil not installed)"
+ hislip = "ok" if zeroconf is not None else "disabled (zeroconf not installed)"
+ return (
+ "\n Resource discovery:"
+ f"\n - VXI-11: {vxi11}"
+ f"\n - hislip: {hislip}"
+ )
+
+
+class TCPIPInstrHiSLIP(Session):
+ """A TCPIP Session built on socket standard library using HiSLIP protocol."""
+
+ # we don't decorate this class with Session.register() because we don't
+ # want it to be registered in the _session_classes array, but we still
+ # need to define session_type to make the set_attribute machinery work.
+ session_type = (constants.InterfaceType.tcpip, "INSTR")
+
+ # Override parsed to take into account the fact that this class is only used
+ # for a specific kind of resource
+ parsed: rname.TCPIPInstr
+
+ @staticmethod
+ def list_resources(wait_time=1.0) -> List[str]:
+ resources = []
+ try:
+ for host in get_services("_hislip._tcp.local.", wait_time=wait_time):
+ resources.append(f"TCPIP::{host}::hislip0,4880::INSTR")
+ except NotImplementedError:
+ warnings.warn(
+ "TCPIP::hislip resource discovery requires the zeroconf package "
+ "to be installed... try 'pip install zeroconf'",
+ UserWarning,
+ )
+ return sorted(resources)
+
+ def after_parsing(self) -> None:
+ # TODO: board_number not handled
+
+ if "," in self.parsed.lan_device_name:
+ _, port_str = self.parsed.lan_device_name.split(",")
+ port = int(port_str)
+ else:
+ port = 4880
+ self.interface = hislip.Instrument(
+ self.parsed.host_address, port=port, timeout=self.timeout
+ )
+
+ # initialize the constant attributes
+ self.attrs[ResourceAttribute.dma_allow_enabled] = constants.VI_FALSE
+ self.attrs[ResourceAttribute.file_append_enabled] = constants.VI_FALSE
+ self.attrs[ResourceAttribute.interface_instrument_name] = "TCPIP0 (HiSLIP)"
+ self.attrs[ResourceAttribute.interface_number] = 0
+ self.attrs[ResourceAttribute.io_prot] = constants.VI_PROT_NORMAL
+ self.attrs[
+ ResourceAttribute.read_buffer_operation_mode
+ ] = constants.VI_FLUSH_DISABLE
+ self.attrs[ResourceAttribute.resource_lock_state] = constants.VI_NO_LOCK
+ self.attrs[ResourceAttribute.send_end_enabled] = constants.VI_TRUE
+ self.attrs[ResourceAttribute.suppress_end_enabled] = constants.VI_FALSE
+ self.attrs[ResourceAttribute.tcpip_address] = self.parsed.host_address
+ self.attrs[ResourceAttribute.tcpip_device_name] = self.parsed.lan_device_name
+ self.attrs[ResourceAttribute.tcpip_hislip_overlap_enable] = constants.VI_FALSE
+ self.attrs[ResourceAttribute.tcpip_hislip_version] = 0x0010_0000
+ self.attrs[ResourceAttribute.tcpip_hostname] = self.parsed.host_address
+ self.attrs[ResourceAttribute.tcpip_is_hislip] = constants.VI_TRUE
+ self.attrs[ResourceAttribute.tcpip_nodelay] = constants.VI_TRUE
+ self.attrs[ResourceAttribute.tcpip_port] = port
+ self.attrs[ResourceAttribute.termchar] = ord("\n")
+ self.attrs[ResourceAttribute.termchar_enabled] = constants.VI_FALSE
+ self.attrs[
+ ResourceAttribute.write_buffer_operation_mode
+ ] = constants.VI_FLUSH_WHEN_FULL
+
+ # configure the variable attributes
+ self.attrs[ResourceAttribute.tcpip_hislip_max_message_kb] = (
+ self.get_max_message_kb,
+ self.set_max_message_kb,
+ )
+ self.attrs[ResourceAttribute.tcpip_keepalive] = (
+ self.get_keepalive,
+ self.set_keepalive,
+ )
+
+ # TODO: additional attributes (someday)
+ # self.attrs[ResourceAttribute.manufacturer_id] = 16711
+ # self.attrs[ResourceAttribute.max_queue_length] = 50
+ # self.attrs[ResourceAttribute.read_buffer_size] = 4096
+ # self.attrs[ResourceAttribute.resource_impl_version] = 0x0050_0c01
+ # self.attrs[ResourceAttribute.resource_manufacturer_id] = 4015
+ # self.attrs[ResourceAttribute.resource_manufacturer_name] = 'Rohde & Schwarz GmbH'
+ # self.attrs[ResourceAttribute.resource_spec_version] = 0x0050_0800
+ # self.attrs[ResourceAttribute.user_data] = 0
+ # self.attrs[ResourceAttribute.write_buffer_size] = 4096
+
+ def get_max_message_kb(
+ self, attribute: ResourceAttribute
+ ) -> Tuple[int, StatusCode]:
+ """Get the maximum HiSLIP message size in kilobytes."""
+ max_msg_size_kb = int(round(self.interface.max_msg_size / 1024))
+ return max_msg_size_kb, StatusCode.success
+
+ def set_max_message_kb(
+ self, attribute: ResourceAttribute, size_kb: int
+ ) -> StatusCode:
+ """Set the maximum HiSLIP message size in kilobytes."""
+ if size_kb < 1:
+ raise ValueError("size must be >= 1 kilobyte")
+
+ if size_kb > 0xFFFF_FFFF:
+ raise ValueError("size exceeds the range in the VISA spec")
+
+ self.interface.max_msg_size = int(round(size_kb * 1024))
+ return StatusCode.success
+
+ def get_keepalive(self, attribute: ResourceAttribute) -> Tuple[bool, StatusCode]:
+ """Is TCP keepalive enabled for the resource."""
+ return self.interface.keepalive, StatusCode.success
+
+ def set_keepalive(
+ self, attribute: ResourceAttribute, keepalive: bool
+ ) -> StatusCode:
+ """Turns TCP keepalive on/off for this connection."""
+ self.interface.keepalive = keepalive
+ return StatusCode.success
+
+ def close(self) -> StatusCode:
+ self.interface.close()
+ self.interface = None
+ return StatusCode.success
+
+ def _set_timeout(self, attribute: ResourceAttribute, value: int) -> StatusCode:
+ status = super()._set_timeout(attribute, value)
+ if hasattr(self.interface, "timeout"):
+ self.interface.timeout = 1e-3 * value
+
+ return status
+
+ def read(self, count: int) -> Tuple[bytes, StatusCode]:
+ """Reads data from device or interface synchronously.
+
+ Corresponds to viRead function of the VISA library.
+
+ Parameters
+ -----------
+ count : int
+ Number of bytes to be read.
+
+ Returns
+ -------
+ bytes
+ Data read from the device
+ StatusCode
+ Return value of the library call.
+
+ """
+ try:
+ data = self.interface.receive(count)
+ status = (
+ StatusCode.success_termination_character_read
+ if self.interface._rmt
+ else StatusCode.success_max_count_read
+ if len(data) >= count
+ else StatusCode.success
+ )
+
+ except socket.timeout:
+ data, status = b"", StatusCode.error_timeout
+
+ return data, status
+
+ def write(self, data: bytes) -> Tuple[int, StatusCode]:
+ """Writes data to device or interface synchronously.
+
+ Corresponds to viWrite function of the VISA library.
+
+ Parameters
+ ----------
+ data : bytes
+ Data to be written.
+
+ Returns
+ -------
+ int
+ Number of bytes actually transferred
+ StatusCode
+ Return value of the library call.
+
+ """
+ self.interface.send(data)
+
+ return len(data), StatusCode.success
+
+ def clear(self) -> StatusCode:
+ """Clears a device.
+
+ Corresponds to viClear function of the VISA library.
+
+ """
+ self.interface.device_clear()
+
+ return StatusCode.success
+
+ def _get_attribute(self, attribute: ResourceAttribute) -> Tuple[Any, StatusCode]:
+ """Get the value for a given VISA attribute for this session.
+
+ Use to implement custom logic for attributes.
+
+ Parameters
+ ----------
+ attribute : ResourceAttribute
+ Attribute for which the state query is made
+
+ Returns
+ -------
+ Any
+ State of the queried attribute for a specified resource
+ StatusCode
+ Return value of the library call.
+
+ """
+ raise UnknownAttribute(attribute)
+
+ def _set_attribute(
+ self, attribute: ResourceAttribute, attribute_state: Any
+ ) -> StatusCode:
+ """Sets the state of an attribute.
+
+ Corresponds to viSetAttribute function of the VISA library.
+
+ Parameters
+ ----------
+ attribute : constants.ResourceAttribute
+ Attribute for which the state is to be modified. (Attributes.*)
+ attribute_state : Any
+ The state of the attribute to be set for the specified object.
+
+ Returns
+ -------
+ StatusCode
+ Return value of the library call.
+
+ """
+ raise UnknownAttribute(attribute)
+
+
+class Vxi11CoreClient(vxi11.CoreClient):
+ """
+ make a connection using vxi11 protocol, optionally allowing the port number
+ to be specified. although in general the port number must be obtained by
+ querying the portmapper, in practice any given instrument typically always
+ uses the same port number. this allows you to open that port on a firewall
+ or set up an ssh tunnel to that port.
+
+ """
+
+ def __init__(
+ self, host: str, port: Optional[int], open_timeout: Optional[int] = 5000
+ ) -> None:
+ self.packer = vxi11.Vxi11Packer()
+ self.unpacker = vxi11.Vxi11Unpacker(b"")
+ prog, vers = vxi11.DEVICE_CORE_PROG, vxi11.DEVICE_CORE_VERS
+
+ if port is None:
+ rpc.TCPClient.__init__(self, host, prog, vers, open_timeout)
+ else:
+ # bypass the portmapper lookup and use the specified port instead
+ rpc.RawTCPClient.__init__(self, host, prog, vers, port, open_timeout)
+
+
+class TCPIPInstrVxi11(Session):
"""A TCPIP Session built on socket standard library using VXI-11 protocol."""
+ # we don't decorate this class with Session.register() because we don't
+ # want it to be registered in the _session_classes array, but we still
+ # need to define session_type to make the set_attribute machinery work.
+ session_type = (constants.InterfaceType.tcpip, "INSTR")
+
#: Maximum size of a chunk of data in bytes.
max_recv_size: int
@@ -59,23 +383,80 @@ class TCPIPInstrSession(Session):
# for a specific kind of resource
parsed: rname.TCPIPInstr
+ # Setting if keepalive has been activated
+ keepalive: bool
+
@staticmethod
def list_resources() -> List[str]:
- # TODO: is there a way to get this?
- return []
+ broadcast_addr = []
+ if psutil is not None:
+ # Get broadcast address for each interface
+ for interface, snics in psutil.net_if_addrs().items():
+ for snic in snics:
+ if snic.family is socket.AF_INET:
+ addr = snic.address
+ mask = snic.netmask
+ network = ipaddress.IPv4Network(addr + "/" + mask, strict=False)
+ broadcast_addr.append(str(network.broadcast_address))
+ else:
+ # If psutil unavailable fallback to default interface
+ broadcast_addr.append("255.255.255.255")
+ warnings.warn(
+ "TCPIP:instr resource discovery is limited to the default interface."
+ "Install psutil: pip install psutil if you want to scan all interfaces.",
+ UserWarning,
+ )
+
+ pmap_list = [rpc.BroadcastUDPPortMapperClient(ip) for ip in broadcast_addr]
+ for pmap in list(pmap_list):
+ pmap.set_timeout(0)
+ try:
+ pmap.send_port(
+ (vxi11.DEVICE_CORE_PROG, vxi11.DEVICE_CORE_VERS, rpc.IPPROTO_TCP, 0)
+ )
+ except rpc.RPCError:
+ pmap_list.remove(pmap)
+
+ # Timeout for responses
+ time.sleep(1)
+
+ all_res = []
+ for pmap in pmap_list:
+ try:
+ resp = pmap.recv_port(
+ (vxi11.DEVICE_CORE_PROG, vxi11.DEVICE_CORE_VERS, rpc.IPPROTO_TCP, 0)
+ )
+ except rpc.RPCError:
+ pass
+ else:
+ res = [r[1][0] for r in resp if r[0] > 0]
+ res = sorted(
+ res, key=lambda ip: tuple(int(part) for part in ip.split("."))
+ )
+ # TODO: Detect GPIB over TCPIP
+ res = ["TCPIP::{}::INSTR".format(host) for host in res]
+ all_res.extend(res)
+
+ return all_res
def after_parsing(self) -> None:
# TODO: board_number not handled
- # vx11 expect all timeouts to be expressed in ms and should be integers
+
+ host_address = self.parsed.host_address
+ if "," in host_address:
+ host_address, port_str = host_address.split(",")
+ port = int(port_str)
+ else:
+ port = None
try:
- self.interface = vxi11.CoreClient(
- self.parsed.host_address, self.open_timeout
- )
+ self.interface = Vxi11CoreClient(host_address, port, self.open_timeout)
except rpc.RPCError:
raise errors.VisaIOError(constants.VI_ERROR_RSRC_NFOUND)
+ # vxi11 expect all timeouts to be expressed in ms and should be integers
self.lock_timeout = 10000
self.client_id = random.getrandbits(31)
+ self.keepalive = False
error, link, abort_port, max_recv_size = self.interface.create_link(
self.client_id, 0, self.lock_timeout, self.parsed.lan_device_name
@@ -85,8 +466,12 @@ class TCPIPInstrSession(Session):
raise Exception("error creating link: %d" % error)
self.link = link
- self.max_recv_size = min(max_recv_size, 2 ** 30) # 1GB
+ self.max_recv_size = min(max_recv_size, 2**30) # 1GB
+ self.attrs[ResourceAttribute.tcpip_is_hislip] = False
+ self.attrs[ResourceAttribute.tcpip_address] = self.parsed.host_address
+ self.attrs[ResourceAttribute.tcpip_hostname] = ""
+ self.attrs[ResourceAttribute.tcpip_device_name] = self.parsed.lan_device_name
for name in ("SEND_END_EN", "TERMCHAR", "TERMCHAR_EN"):
attribute = getattr(constants, "VI_ATTR_" + name)
self.attrs[attribute] = attributes.AttributesByID[attribute].default
@@ -184,16 +569,13 @@ class TCPIPInstrSession(Session):
Return value of the library call.
"""
- send_end, _ = self.get_attribute(ResourceAttribute.send_end_enabled)
- chunk_size = 1024
-
try:
flags = 0
num = len(data)
offset = 0
while num > 0:
- if num <= chunk_size:
+ if num <= self.max_recv_size:
flags |= vxi11.OP_FLAG_END
block = data[offset : offset + self.max_recv_size]
@@ -234,23 +616,9 @@ class TCPIPInstrSession(Session):
Return value of the library call.
"""
- if attribute == constants.VI_ATTR_TCPIP_ADDR:
- return self.parsed.host_address, StatusCode.success
-
- elif attribute == constants.VI_ATTR_TCPIP_DEVICE_NAME:
- raise NotImplementedError
-
- elif attribute == constants.VI_ATTR_TCPIP_HOSTNAME:
- raise NotImplementedError
-
- elif attribute == constants.VI_ATTR_TCPIP_KEEPALIVE:
- raise NotImplementedError
-
- elif attribute == constants.VI_ATTR_TCPIP_NODELAY:
- raise NotImplementedError
-
- elif attribute == constants.VI_ATTR_TCPIP_PORT:
- raise NotImplementedError
+ # This is an abuse of the VISA standard
+ if attribute == constants.VI_ATTR_TCPIP_KEEPALIVE:
+ return self.keepalive, StatusCode.success
elif attribute == constants.VI_ATTR_SUPPRESS_END_EN:
raise NotImplementedError
@@ -277,6 +645,35 @@ class TCPIPInstrSession(Session):
Return value of the library call.
"""
+
+ # In case of an environment with idle socket garbage collection (like docker)
+ # sockets need to be kept alive. Set pyvisa.constants.ResourceAttribute.tcpip_keepalive to enable
+ # keepalive packets even for VXI11 protocol. To read more on this issue
+ # https://tech.xing.com/a-reason-for-unexplained-connection-timeouts-on-kubernetes-docker-abd041cf7e02
+ if attribute == constants.VI_ATTR_TCPIP_KEEPALIVE:
+ if attribute_state is True:
+ self.interface.sock.setsockopt(
+ socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1
+ )
+ self.interface.sock.setsockopt(
+ socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60
+ )
+ self.interface.sock.setsockopt(
+ socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 60
+ )
+ self.interface.sock.setsockopt(
+ socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5
+ )
+ self.keepalive = True
+ elif attribute_state is False:
+ self.interface.sock.setsockopt(
+ socket.SOL_SOCKET, socket.SO_KEEPALIVE, 0
+ )
+ self.keepalive = False
+ else:
+ return StatusCode.error_nonsupported_format
+ return StatusCode.success
+
raise UnknownAttribute(attribute)
def assert_trigger(self, protocol: constants.TriggerProtocol):
@@ -391,7 +788,7 @@ class TCPIPInstrSession(Session):
"""Sets timeout calculated value from python way to VI_ way"""
if value == constants.VI_TMO_INFINITE:
self.timeout = None
- self._io_timeout = 2 ** 32 - 1
+ self._io_timeout = 2**32 - 1
elif value == constants.VI_TMO_IMMEDIATE:
self.timeout = 0
self._io_timeout = 0
@@ -401,6 +798,236 @@ class TCPIPInstrSession(Session):
return StatusCode.success
+# Requires Pyvisa >= 1.13
+if hasattr(rname, "VICPInstr"):
+
+ class TCPIPInstrVicp(Session):
+ """VICP Session that uses pyvicp to do the low level communication."""
+
+ # Override parsed to take into account the fact that this class is only used
+ # for a specific kind of resource
+ parsed: rname.VICPInstr
+
+ @staticmethod
+ def list_resources(wait_time=1.0) -> List[str]:
+ resources = []
+ try:
+ services = get_services("_lxi._tcp.local.", wait_time=wait_time)
+ except NotImplementedError:
+ warnings.warn(
+ "VICP resources discovery requires the zeroconf package to be "
+ "installed... try 'pip install zeroconf'",
+ UserWarning,
+ )
+ return []
+
+ for host, properties in services.items():
+ if properties["Manufacturer"].lower().startswith("lecroy"):
+ resources.append(f"VICP::{host}::INSTR")
+
+ return sorted(resources)
+
+ def after_parsing(self) -> None:
+ # TODO: board_number not handled
+ if pyvicp is None:
+ raise NotImplementedError(
+ "VICP requires the pyvicp package to be installed... "
+ "try 'pip install pyvicp'"
+ )
+
+ host_address = self.parsed.host_address
+ if "," in host_address:
+ host_address, port_str = host_address.split(",")
+ port = int(port_str)
+ else:
+ port = 1861
+
+ self.interface = pyvicp.Client(
+ self.parsed.host_address, port, timeout=self.timeout
+ )
+
+ # initialize the constant attributes
+ for name in ("SEND_END_EN", "TERMCHAR", "TERMCHAR_EN"):
+ attribute = getattr(constants, "VI_ATTR_" + name)
+ self.attrs[attribute] = attributes.AttributesByID[attribute].default
+
+ self.attrs[ResourceAttribute.dma_allow_enabled] = constants.VI_FALSE
+ self.attrs[ResourceAttribute.file_append_enabled] = constants.VI_FALSE
+ self.attrs[ResourceAttribute.interface_instrument_name] = "TCPIP0 (VICP)"
+ self.attrs[ResourceAttribute.interface_number] = 0
+ self.attrs[ResourceAttribute.io_prot] = constants.VI_PROT_NORMAL
+ self.attrs[
+ ResourceAttribute.read_buffer_operation_mode
+ ] = constants.VI_FLUSH_DISABLE
+ self.attrs[ResourceAttribute.resource_lock_state] = constants.VI_NO_LOCK
+ self.attrs[ResourceAttribute.suppress_end_enabled] = constants.VI_FALSE
+ self.attrs[ResourceAttribute.tcpip_address] = self.parsed.host_address
+ self.attrs[ResourceAttribute.tcpip_hostname] = self.parsed.host_address
+ self.attrs[ResourceAttribute.tcpip_is_hislip] = constants.VI_FALSE
+ self.attrs[ResourceAttribute.tcpip_nodelay] = constants.VI_TRUE
+ self.attrs[ResourceAttribute.tcpip_port] = port
+ self.attrs[
+ ResourceAttribute.write_buffer_operation_mode
+ ] = constants.VI_FLUSH_WHEN_FULL
+
+ # configure the variable attributes
+ self.attrs[ResourceAttribute.tcpip_keepalive] = (
+ self.get_keepalive,
+ self.set_keepalive,
+ )
+
+ # TODO: additional attributes (someday)
+ # self.attrs[ResourceAttribute.manufacturer_id] = 16711
+ # self.attrs[ResourceAttribute.max_queue_length] = 50
+ # self.attrs[ResourceAttribute.read_buffer_size] = 4096
+ # self.attrs[ResourceAttribute.resource_impl_version] = 0x0050_0c01
+ # self.attrs[ResourceAttribute.resource_manufacturer_id] = 4015
+ # self.attrs[ResourceAttribute.resource_manufacturer_name] = 'Rohde & Schwarz GmbH'
+ # self.attrs[ResourceAttribute.resource_spec_version] = 0x0050_0800
+ # self.attrs[ResourceAttribute.user_data] = 0
+ # self.attrs[ResourceAttribute.write_buffer_size] = 4096
+
+ def get_keepalive(
+ self, attribute: ResourceAttribute
+ ) -> Tuple[bool, StatusCode]:
+ """Is TCP keepalive enabled for the resource."""
+ return self.interface.keepalive, StatusCode.success
+
+ def set_keepalive(
+ self, attribute: ResourceAttribute, keepalive: bool
+ ) -> StatusCode:
+ """Turns TCP keepalive on/off for this connection."""
+ self.interface.keepalive = keepalive
+ return StatusCode.success
+
+ def close(self) -> StatusCode:
+ self.interface.close()
+ self.interface = None
+ return StatusCode.success
+
+ def read(self, count: int) -> Tuple[bytes, StatusCode]:
+ """Reads data from device or interface synchronously.
+
+ Corresponds to viRead function of the VISA library.
+
+ Parameters
+ -----------
+ count : int
+ Number of bytes to be read.
+
+ Returns
+ -------
+ bytes
+ Data read from the device
+ StatusCode
+ Return value of the library call.
+
+ """
+ try:
+ data = self.interface.receive(count)
+ except socket.timeout:
+ return b"", StatusCode.error_timeout
+
+ if len(data) >= count:
+ return data, StatusCode.success_max_count_read
+ else:
+ return data, StatusCode.success_termination_character_read
+
+ def write(self, data: bytes) -> Tuple[int, StatusCode]:
+ """Writes data to device or interface synchronously.
+
+ Corresponds to viWrite function of the VISA library.
+
+ Parameters
+ ----------
+ data : bytes
+ Data to be written.
+
+ Returns
+ -------
+ int
+ Number of bytes actually transferred
+ StatusCode
+ Return value of the library call.
+
+ """
+ self.interface.send(data)
+
+ return len(data), StatusCode.success
+
+ def clear(self) -> StatusCode:
+ """Clears a device.
+
+ Corresponds to viClear function of the VISA library.
+
+ """
+ self.interface.device_clear()
+
+ return StatusCode.success
+
+ def _set_timeout(self, attribute: ResourceAttribute, value: int) -> StatusCode:
+ status = super()._set_timeout(attribute, value)
+ if hasattr(self.interface, "timeout"):
+ self.interface.timeout = 1e-3 * value
+
+ return status
+
+ def _get_attribute(
+ self, attribute: ResourceAttribute
+ ) -> Tuple[Any, StatusCode]:
+ """Get the value for a given VISA attribute for this session.
+
+ Use to implement custom logic for attributes.
+
+ Parameters
+ ----------
+ attribute : ResourceAttribute
+ Attribute for which the state query is made
+
+ Returns
+ -------
+ Any
+ State of the queried attribute for a specified resource
+ StatusCode
+ Return value of the library call.
+
+ """
+ raise UnknownAttribute(attribute)
+
+ def _set_attribute(
+ self, attribute: ResourceAttribute, attribute_state: Any
+ ) -> StatusCode:
+ """Sets the state of an attribute.
+
+ Corresponds to viSetAttribute function of the VISA library.
+
+ Parameters
+ ----------
+ attribute : constants.ResourceAttribute
+ Attribute for which the state is to be modified. (Attributes.*)
+ attribute_state : Any
+ The state of the attribute to be set for the specified object.
+
+ Returns
+ -------
+ StatusCode
+ Return value of the library call.
+
+ """
+ raise UnknownAttribute(attribute)
+
+
+if hasattr(constants.InterfaceType, "vicp"):
+ if pyvicp is not None:
+ Session.register(constants.InterfaceType.vicp, "INSTR")(TCPIPInstrVicp)
+ else:
+ Session.register_unavailable(
+ constants.InterfaceType.vicp,
+ "INSTR",
+ "Please install PyVICP to use this resource type.",
+ )
+
+
@Session.register(constants.InterfaceType.tcpip, "SOCKET")
class TCPIPSocketSession(Session):
"""A TCPIP Session that uses the network standard library to do the low
@@ -545,7 +1172,6 @@ class TCPIPSocketSession(Session):
# data arrives
finish_time = None if self.timeout is None else (time.time() + self.timeout)
while True:
-
# check, if we have any data received (from pending buffer or
# further reading)
if term_char_en and term_byte in self._pending_buffer:
@@ -616,7 +1242,6 @@ class TCPIPSocketSession(Session):
offset = 0
while num > 0:
-
block = data[offset : min(offset + chunk_size, sz)]
try:
@@ -692,6 +1317,9 @@ class TCPIPSocketSession(Session):
self.interface.setsockopt(
socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1 if attribute_state else 0
)
+ self.interface.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 60)
+ self.interface.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 60)
+ self.interface.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)
return StatusCode.success
return StatusCode.error_nonsupported_attribute
@@ -736,3 +1364,41 @@ class TCPIPSocketSession(Session):
"""
raise UnknownAttribute(attribute)
+
+
+def get_services(service_type: str, wait_time: float = 0.1) -> Dict[str, dict]:
+ if zeroconf is None:
+ raise NotImplementedError(
+ "Service discovery requires the zeroconf package to be installed... "
+ "try 'pip install zeroconf'"
+ )
+
+ class MyListener(zeroconf.ServiceListener):
+ def __init__(self, *args, **kwargs):
+ self.services = {}
+ super().__init__(*args, **kwargs)
+
+ def remove_service(self, zc: zeroconf.Zeroconf, type_: str, name: str) -> None:
+ del self.services[name]
+
+ def add_service(self, zc: zeroconf.Zeroconf, type_: str, name: str) -> None:
+ info = zc.get_service_info(type_, name)
+ if info is None:
+ return
+ properties = {}
+ for key, val in info.properties.items():
+ if key == b"txtvers":
+ continue
+ properties[key.decode()] = val.decode()
+ ipaddr = ipaddress.ip_address(info.addresses[0])
+ self.services[str(ipaddr)] = properties
+
+ update_service = add_service
+
+ zero_conf = zeroconf.Zeroconf()
+ listener = MyListener()
+ browser = zeroconf.ServiceBrowser(zero_conf, service_type, listener, delay=0)
+ time.sleep(wait_time)
+ browser.cancel()
+ zero_conf.close()
+ return listener.services
diff --git a/pyvisa_py/testsuite/__init__.py b/pyvisa_py/testsuite/__init__.py
index fe5ff8c..1ed148e 100644
--- a/pyvisa_py/testsuite/__init__.py
+++ b/pyvisa_py/testsuite/__init__.py
@@ -20,6 +20,6 @@ def main():
def run() -> unittest.TestResult:
- """Run all tests. """
+ """Run all tests."""
test_runner = unittest.TextTestRunner()
return test_runner.run(testsuite())
diff --git a/pyvisa_py/testsuite/keysight_assisted_tests/test_resource_manager.py b/pyvisa_py/testsuite/keysight_assisted_tests/test_resource_manager.py
index 73858a1..50f5f57 100644
--- a/pyvisa_py/testsuite/keysight_assisted_tests/test_resource_manager.py
+++ b/pyvisa_py/testsuite/keysight_assisted_tests/test_resource_manager.py
@@ -3,22 +3,31 @@
"""
import pytest
-from pyvisa.testsuite.keysight_assisted_tests import copy_func, require_virtual_instr
-from pyvisa.testsuite.keysight_assisted_tests.test_resource_manager import (
- TestResourceManager as BaseTestResourceManager,
+from pyvisa.rname import ResourceName
+from pyvisa.testsuite.keysight_assisted_tests import (
+ RESOURCE_ADDRESSES,
+ copy_func,
+ require_virtual_instr,
)
from pyvisa.testsuite.keysight_assisted_tests.test_resource_manager import (
+ TestResourceManager as BaseTestResourceManager,
TestResourceParsing as BaseTestResourceParsing,
)
@require_virtual_instr
class TestPyResourceManager(BaseTestResourceManager):
- """"""
-
- test_list_resource = pytest.mark.xfail(
- copy_func(BaseTestResourceManager.test_list_resource)
- )
+ """ """
+
+ def test_list_resource(self):
+ """Test listing the available resources.
+ The bot supports only TCPIP and of those resources we expect to be able
+ to list only INSTR resources not SOCKET.
+ """
+ # Default settings
+ resources = self.rm.list_resources()
+ for v in (v for v in RESOURCE_ADDRESSES.values() if v.endswith("INSTR")):
+ assert str(ResourceName.from_string(v)) in resources
test_last_status = pytest.mark.xfail(
copy_func(BaseTestResourceManager.test_last_status)
@@ -31,6 +40,6 @@ class TestPyResourceManager(BaseTestResourceManager):
@require_virtual_instr
class TestPyResourceParsing(BaseTestResourceParsing):
- """"""
+ """ """
pass
diff --git a/pyvisa_py/testsuite/keysight_assisted_tests/test_tcpip_resources.py b/pyvisa_py/testsuite/keysight_assisted_tests/test_tcpip_resources.py
index 31721db..ff72acd 100644
--- a/pyvisa_py/testsuite/keysight_assisted_tests/test_tcpip_resources.py
+++ b/pyvisa_py/testsuite/keysight_assisted_tests/test_tcpip_resources.py
@@ -2,7 +2,10 @@
"""Test the TCPIP based resources.
"""
+import socket
+
import pytest
+from pyvisa.constants import ResourceAttribute
from pyvisa.testsuite.keysight_assisted_tests import copy_func, require_virtual_instr
from pyvisa.testsuite.keysight_assisted_tests.test_tcpip_resources import (
TestTCPIPInstr as TCPIPInstrBaseTest,
@@ -85,6 +88,26 @@ class TestTCPIPInstr(TCPIPInstrBaseTest):
copy_func(TCPIPInstrBaseTest.test_attribute_handling)
)
+ def test_keepalive_attribute_vxi11(self):
+ assert self.instr.visalib.sessions[self.instr.session].keepalive is False
+ self.instr.set_visa_attribute(ResourceAttribute.tcpip_keepalive, True)
+ assert self.instr.visalib.sessions[self.instr.session].keepalive is True
+ assert (
+ self.instr.visalib.sessions[self.instr.session].interface.sock.getsockopt(
+ socket.SOL_SOCKET, socket.SO_KEEPALIVE
+ )
+ == 1
+ )
+
+ self.instr.set_visa_attribute(ResourceAttribute.tcpip_keepalive, False)
+ assert self.instr.visalib.sessions[self.instr.session].keepalive is False
+ assert (
+ self.instr.visalib.sessions[self.instr.session].interface.sock.getsockopt(
+ socket.SOL_SOCKET, socket.SO_KEEPALIVE
+ )
+ == 0
+ )
+
@require_virtual_instr
class TestTCPIPSocket(TCPIPSocketBaseTest):
diff --git a/pyvisa_py/usb.py b/pyvisa_py/usb.py
index 04583c6..9350c02 100644
--- a/pyvisa_py/usb.py
+++ b/pyvisa_py/usb.py
@@ -84,7 +84,7 @@ class USBSession(Session):
self.parsed.serial_number,
)
- for name in ("SEND_END_EN", "TERMCHAR", "TERMCHAR_EN"):
+ for name in ("SEND_END_EN", "SUPPRESS_END_EN", "TERMCHAR", "TERMCHAR_EN"):
attribute = getattr(constants, "VI_ATTR_" + name)
self.attrs[attribute] = attributes.AttributesByID[attribute].default
@@ -94,7 +94,7 @@ class USBSession(Session):
def _get_timeout(self, attribute: ResourceAttribute) -> Tuple[int, StatusCode]:
if self.interface:
- if self.interface.timeout == 2 ** 32 - 1:
+ if self.interface.timeout == 2**32 - 1:
self.timeout = None
else:
self.timeout = self.interface.timeout / 1000
@@ -102,8 +102,8 @@ class USBSession(Session):
def _set_timeout(self, attribute: ResourceAttribute, value: int) -> StatusCode:
status = super(USBSession, self)._set_timeout(attribute, value)
- timeout = int(self.timeout * 1000) if self.timeout else 2 ** 32 - 1
- timeout = min(timeout, 2 ** 32 - 1)
+ timeout = int(self.timeout * 1000) if self.timeout else 2**32 - 1
+ timeout = min(timeout, 2**32 - 1)
if self.interface:
self.interface.timeout = timeout
return status
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index a83a150..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,103 +0,0 @@
-[metadata]
-name = PyVISA-py
-author = Hernan E. Grecco
-author_email = hernan.grecco@gmail.com
-maintainer = Hernan E. Grecco
-maintainer_email = hernan.grecco@gmail.com
-license = MIT License
-description = Python VISA bindings for GPIB, RS232, and USB instruments
-keywords =
- Remote
- VISA
- GPIB
- USB
- serial
- RS232
- measurement
- acquisition
-url = https://github.com/pyvisa/pyvisa-py
-long_description = file: README.rst, AUTHORS, CHANGES
-long_description_content_type = text/x-rst
-classifiers =
- Development Status :: 4 - Beta
- Intended Audience :: Developers
- Intended Audience :: Science/Research
- License :: OSI Approved :: MIT License
- Operating System :: Microsoft :: Windows
- Operating System :: POSIX :: Linux
- Operating System :: MacOS :: MacOS X
- Programming Language :: Python
- Topic :: Scientific/Engineering :: Interface Engine/Protocol Translator
- Topic :: Software Development :: Libraries :: Python Modules
- Programming Language :: Python :: 3.6
- Programming Language :: Python :: 3.7
- Programming Language :: Python :: 3.8
-platforms = Linux; Windows; Mac
-
-[options]
-packages =
- pyvisa_py
- pyvisa_py.protocols
- pyvisa_py.testsuite
-zip_safe = False
-install_requires =
- pyvisa>=1.11.0
- typing_extensions
- importlib-metadata; python_version<"3.8"
-setup_requires = setuptools>=42; wheel; setuptools_scm[toml]>=3.4.3
-use_2to3 = False
-
-[options.extras_require]
-gpib-ctypes = gpib-ctypes>=0.3.0
-serial = pyserial>=3.0
-usb = pyusb
-
-[flake8]
-exclude =
- .git,
- __pycache__,
- docs/source/conf.py,
- old,
- build,
- dist,
-ignore = E203, E266, E501, W503, E731
-# line length is intentionally set to 80 here because pyvisa uses Bugbear
-# See https://github.com/psf/black/blob/master/README.md#line-length for more details
-max-line-length = 80
-max-complexity = 18
-select = B,C,E,F,W,T4,B9
-per-file-ignores =
- pyvisa_py/protocols/vxi11.py:E221
- pyvisa_py/serial.py:C901
-
-[mypy]
-follow_imports = normal
-strict_optional = True
-
-[mypy-usb.*]
-ignore_missing_imports = True
-
-[mypy-serial.*]
-ignore_missing_imports = True
-
-[mypy-gpib.*]
-ignore_missing_imports = True
-
-[mypy-Gpib.*]
-ignore_missing_imports = True
-
-[mypy-gpib_ctypes.*]
-ignore_missing_imports = True
-
-# XXX Ideally remove once pytest ships typing information
-[mypy-pytest.*]
-ignore_missing_imports = True
-
-[isort]
-multi_line_output = 3
-include_trailing_comma = true
-force_grid_wrap = 0
-use_parentheses = true
-line_length = 88
-skip = pyvisa-py/__init__.py
-known_third_party = numpy,setuptools,typing_extensions,pyvisa,pytest
More details
Historical runs
- failed: error: Multiple top-level packages discovered in a flat-layout: ['debian', 'pyvisa_py'].
- push-failed: Failed to push result branch: Connection closed: Connection closed early The remote server unexpectedly closed the connection.
- push-failed: Failed to push result branch: Connection closed: Connection closed early The remote server unexpectedly closed the connection.
- failed: error: Multiple top-level packages discovered in a flat-layout: ['debian', 'pyvisa_py'].