diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5ffad16..a2f2e993 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,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@v4 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} flags: unittests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f06c9cca..eca451a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,14 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.8.3 + rev: v0.8.4 hooks: # Run the linter. - id: ruff # Run the formatter. - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy - rev: 'v1.13.0' # Use the sha / tag you want to point at + rev: 'v1.14.0' # Use the sha / tag you want to point at hooks: - id: mypy additional_dependencies: [numpy, typing_extensions] \ No newline at end of file diff --git a/CHANGES b/CHANGES index d79cd2fb..99717d70 100644 --- a/CHANGES +++ b/CHANGES @@ -12,7 +12,9 @@ PyVISA-py Changelog Read now reads from usb until a "short packet" or if all data (`transfer_size`) PR #465 has been read (see specification), and only expects a header on the first packet received. - fix usbtmc implementation to properly discard the alignment bytes - ensuring only the actual data (`transfer_size`) is retained in the message PR #465 + ensuring only the actual data (`transfer_size`) is retained in the message PR #465 +- Implemented partial USBTMC message functionality that allows reading the amount of bytes + specified by host PR #470 - add support for VI_ATTR_SUPPRESS_END_EN for USB resources PR #449 0.7.2 (07/03/2024) diff --git a/pyvisa_py/attributes.py b/pyvisa_py/attributes.py index 753d20db..99ba0446 100644 --- a/pyvisa_py/attributes.py +++ b/pyvisa_py/attributes.py @@ -27,4 +27,5 @@ class AttrVI_ATTR_TCPIP_KEEPALIVE(former_keepalive): resources = [ (constants.InterfaceType.tcpip, "SOCKET"), (constants.InterfaceType.tcpip, "INSTR"), + (constants.InterfaceType.vicp, "INSTR"), ] diff --git a/pyvisa_py/protocols/usbtmc.py b/pyvisa_py/protocols/usbtmc.py index 8bbac953..da0cc888 100644 --- a/pyvisa_py/protocols/usbtmc.py +++ b/pyvisa_py/protocols/usbtmc.py @@ -12,6 +12,7 @@ """ import enum +import math import struct import time import warnings @@ -455,13 +456,7 @@ def write(self, data): return size def read(self, size): - header_size = 12 - max_padding = 511 - recv_chunk = self.usb_recv_ep.wMaxPacketSize - header_size - - if size > 0 and size < recv_chunk: - recv_chunk = size - + usbtmc_header_size = 12 eom = False raw_read = super(USBTMC, self).read @@ -473,33 +468,61 @@ def read(self, size): received_transfer = bytearray() self._btag = (self._btag % 255) + 1 - req = BulkInMessage.build_array(self._btag, recv_chunk, None) - + req = BulkInMessage.build_array(self._btag, size, None) raw_write(req) try: - resp = raw_read(recv_chunk + header_size + max_padding) + # make sure the data request is in multitudes of wMaxPacketSize. + # + 1 * wMaxPacketSize for message sizes that equals wMaxPacketSize == size + usbtmc_header_size. + # This to be able to retrieve a short package to end communication + # (see USB 2.0 Section 5.8.3 and USBTMC Section 3.3) + chunk_size = ( + math.floor( + (size + usbtmc_header_size) / self.usb_recv_ep.wMaxPacketSize + ) + + 1 + ) * self.usb_recv_ep.wMaxPacketSize + resp = raw_read(chunk_size) + response = BulkInMessage.from_bytes(resp) received_transfer.extend(response.data) - while ( - len(resp) == self.usb_recv_ep.wMaxPacketSize - or len(received_transfer) < response.transfer_size - ): - # USBTMC Section 3.3 specifies that the first usb packet - # must contain the header. the remaining packets do not need - # the header the message is finished when a "short packet" - # is sent (one whose length is less than wMaxPacketSize) - # wMaxPacketSize may be incorrectly reported by certain drivers. - # Therefore, continue reading until the transfer_size is reached. - resp = raw_read(recv_chunk + header_size + max_padding) - received_transfer.extend(resp) # Detect EOM only when device sends all expected bytes. if len(received_transfer) >= response.transfer_size: eom = response.transfer_attributes & 1 - # Truncate data to the specified length (discard padding) - # USBTMC header (12 bytes) has already truncated - received_message.extend(received_transfer[: response.transfer_size]) + if not eom and len(received_transfer) >= size: + # Read asking for 'size' bytes from the device. + # This may be less then the device wants to send back in a message + # Therefore the request does not mean that we must receive a EOM. + # Multiple `transfers` will be required to retrieve the remaining bytes. + eom = True + else: + while ( + (len(resp) % self.usb_recv_ep.wMaxPacketSize) == 0 + or len(received_transfer) < response.transfer_size + ) and not eom: + # USBTMC Section 3.3 specifies that the first usb packet + # must contain the header. the remaining packets do not need + # the header the message is finished when a "short packet" + # is sent (one whose length is less than wMaxPacketSize) + # wMaxPacketSize may be incorrectly reported by certain drivers. + # Therefore, continue reading until the transfer_size is reached. + chunk_size = ( + math.floor( + (size - len(received_transfer)) + / self.usb_recv_ep.wMaxPacketSize + ) + + 1 + ) * self.usb_recv_ep.wMaxPacketSize + resp = raw_read(chunk_size) + received_transfer.extend(resp) + if len(received_transfer) >= response.transfer_size: + eom = response.transfer_attributes & 1 + if not eom and len(received_transfer) >= size: + eom = True + # Truncate data to the specified length (discard padding) + # USBTMC header (12 bytes) has already truncated + received_message.extend(received_transfer[: response.transfer_size]) except (usb.core.USBError, ValueError): # Abort failed Bulk-IN operation. self._abort_bulk_in(self._btag)