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

Full run details

Historical runs