diff --git a/CHANGES b/CHANGES index 5573055c..05bb4861 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,13 @@ PyVISA-py Changelog 0.9.0 (unreleased) ------------------ +- VXI-11 (TCPIP::INSTR): + Locking compliance improvement. Exclusive locking works now, + Removed `session.lock_timeout`, no longer needed for instruments + that have flawed VXI-11 implementations like the DM3068. + Added attribute VI_KTATTR_LOCKWAIT. + Corrected error codes. + Closes #583 PR #633 - VXI-11 and HiSLIP: add support for remote/local #627 PR #636 - A VXI-11 read stopped by both the END indicator and the termination character now reports ``VI_SUCCESS`` rather than diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 6675a8de..de1fb9bd 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -24,15 +24,15 @@ The blocked read will return with ``VI_ERROR_ABORT``. The HiSLIP protocol state is automatically reset (via a device clear) so the session is ready for further I/O immediately:: - import pyvisa - rm = pyvisa.ResourceManager('@py') - inst = rm.open_resource('TCPIP::192.168.1.100::hislip0::INSTR') - - # From another thread, to cancel a blocked read: - inst.visalib.terminate(inst.session, None, None) - - # The blocked read returns VI_ERROR_ABORT. - # The session is ready for further I/O — no manual viClear() needed. + >>> import pyvisa + >>> rm = pyvisa.ResourceManager('@py') + >>> inst = rm.open_resource('TCPIP::192.168.1.100::hislip0::INSTR') + >>> + >>> # From another thread, to cancel a blocked read: + >>> inst.visalib.terminate(inst.session, None, None) + >>> + >>> # The blocked read returns VI_ERROR_ABORT. + >>> # The session is ready for further I/O — no manual viClear() needed. ``viTerminate()`` is not yet supported for VXI-11, USBTMC, or serial sessions. @@ -43,7 +43,7 @@ further I/O immediately:: Libraries' ``viTerminate()`` returns ``VI_SUCCESS`` but does not actually cancel a blocked synchronous ``viRead()`` — the read continues until the normal timeout expires. The VISA specification defines ``viTerminate()`` - primarily for asynchronous operations (``viReadAsync``/``viWriteAsync``), + primarily for asynchronous operations (``viReadAsync`` / ``viWriteAsync``), and its behavior on synchronous calls is implementation-defined. Code that relies on ``viTerminate()`` cancelling a synchronous read may not be portable to other VISA backends. @@ -63,12 +63,13 @@ Are GBIP secondary addresses supported? GPIB secondary addresses are supported in NI-VISA fashion, meaning that the secondary address is not 96 to 126 as transmitted on the bus, but 0 to 30. -For expample, `GPIB0::9::1::INSTR` is the address of the first VXI module -controlled by a GPIB VXI command module set to primary address `9`, while -the command module itself is found at `GPIB0::9::0::INSTR`, which is distinct -from a pure primary address like `GPIB0::9::INSTR`. +For expample, ``GPIB0::9::1::INSTR`` is the address of the first VXI module +controlled by a GPIB VXI command module set to primary address ``9``, while +the command module itself is found at ``GPIB0::9::0::INSTR``, which is distinct +from a pure primary address like ``GPIB0::9::INSTR``. -``ResourceManager.list_resources()`` has become slower as a result, +``ResourceManager.list_resources()`` can discover both primary and secondary +addressable ``GPIB::...::INSTR`` resources. As a result, it can be slow, as it now needs to check 992 addresses per GPIB controller instead of just 31. For every primary address where no listener is detected, all @@ -78,9 +79,16 @@ VXI modules controlled by an HP E1406A. For primary addresses where a listener is detected, no secondary addresses are checked as most devices simply ignore secondary addressing. -If you have a device that reacts to the primary address and has different +If you have an instrument that reacts to the primary address and has different functionality on some secondary addresses, please leave a bug report. +If you use a VXI-11.2 (VXI-11 to GPIB) gateway, you can use constructions like +``TCPIP::host::gpib0,9,1::INSTR``, where ``gpib0`` is the 'GPIB SICL Interface Name' +configured on the gateway, ``9`` is the primary address of the instrument, +and ``1`` is the secondary address of that instrument. +``ResourceManager.list_resources()`` will however not try to discover any resources +behind a VXI-11.2 gateway, as the SICL Interface Name is not automatically known. + Can PyVISA-py be used from a VM? -------------------------------- @@ -99,12 +107,12 @@ 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 +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) + >>> inst.set_visa_attribute(pyvisa.constants.ResourceAttribute.tcpip_keepalive, True) -where `inst` is an active TCP/IP visa session. +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) @@ -112,9 +120,8 @@ 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 (latest release is from 2013). -However, you can already use it with the IVI backend as it has the same API. +LibreVISA_ is unmaintained at this point (latest release is from 2013). +However, you can 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. @@ -129,21 +136,6 @@ By using PyVISA as a frontend to many backends, we abstract these things from higher level applications. -Why is my Ethernet instrument not working? ------------------------------------------- - -Some instruments, such as the Rigol DM3068 Digital Multimeter, -expect a non-default parameter in order to communicate successfully over Ethernet. -In the case of the DM3068, the VXI-11 lock timeout must be set to zero: - - >>> import pyvisa - >>> rm = pyvisa.ResourceManager('@py') - >>> dm3068 = rm.open_resource('TCPIP::rigol-dm3068-hostname::INSTR') - >>> # default lock_timeout is still 10000ms at this point - >>> rm.visalib.sessions[dm3068.session].lock_timeout = 0 - >>> # can now communicate successfully with the DM3068 - - What does ``open_timeout`` control? ----------------------------------- @@ -156,9 +148,9 @@ establishing the connection:: >>> # allow 10 s to reach an instrument across a slow link >>> inst = rm.open_resource('TCPIP::192.168.1.100::INSTR', open_timeout=10000) -If you do not pass one, the connection attempt is given 2000 ms. An -``open_timeout`` of 0 selects that same default rather than meaning "give up -immediately", since ``ResourceManager.open_resource`` passes 0 whenever you +If you do not specify ``open_timeout``, the connection attempt is given 2000 ms. An +``open_timeout`` of ``0`` selects that same default rather than meaning "give up +immediately", since ``ResourceManager.open_resource`` passes ``0`` whenever you omit the argument. .. note:: @@ -177,9 +169,117 @@ omit the argument. implementation should behave as if the timeout parameter is the VISA default timeout value of 2000 milliseconds. - The trade-off is that ``VI_TMO_IMMEDIATE`` can no longer request "never - wait on a lock". Set ``lock_timeout`` on the session for that, as in the - DM3068 example above. + +Locking +------- + +**PyVISA-Py only supports exclusive locking, and only on VXI-11. Shared locks and nested locking are not supported.** + +Socket instruments (``TCPIP::SOCKET``) do not support locking. +HiSLIP instruments (``TCPIP::hislip``) could support locking, but PyVISA-Py does not yet implement this feature. + +With exclusive locking, only one session can be used at a time on an instrument. +If another session has a lock, another client will not be able to communicate with +the instrument. Either ``open_resource`` will fail, either ``read`` / ``write`` / ``query`` /... +operations will fail. +The related error codes in that case are: +``VI_ERROR_RSRC_LOCKED`` (as it should), or ``VI_ERROR_TMO`` or ``VI_ERROR_RSRC_BUSY`` or ``VI_ERROR_IO``. + +There are two ways of using exclusive locking on an instrument session via pyvisa: + +- lock on open +- lock after open + +"Lock on open" is done via the ``access_mode`` argument to ``ResourceManager.open_resource``. The +default is ``pyvisa.constants.AccessModes.no_lock``, which does not request a lock. + + >>> import pyvisa + >>> rm = pyvisa.ResourceManager('@py') + >>> # allow 10 s to reach an instrument across a slow link, + >>> # and 10 s to acquire a lock on the instrument + >>> inst = rm.open_resource('TCPIP::192.168.1.100::INSTR', open_timeout=10000, + >>> access_mode=pyvisa.constants.AccessModes.exclusive_lock) + +The connection will be established with the same timeout handling as mentioned above, +but will then try to open a link with a lock timeout also governed by ``open_timeout``. +That lock request will succeed if the instrument grants it within this period. +An ``open_timeout`` of ``0`` or ``VI_TMO_IMMEDIATE`` there means: "give up immediately", +while None means "10 seconds", and ``VI_TMO_INFINITE`` means "wait indefinitely". + +If you want better control over the different timeout settings, use "lock after open": + + >>> import pyvisa + >>> rm = pyvisa.ResourceManager('@py') + >>> # allow 3 s to reach the instrument + >>> inst = rm.open_resource('TCPIP::192.168.1.100::INSTR', open_timeout=3000) + >>> # and then try to acquire a lock on the instrument with a 10 s timeout + >>> inst.lock_excl(timeout=10000) + +If you have not locked the instrument, and want to control the behaviour of your program +in case another program or session has locked it, you must choose one of the following methods: + +- Request a lock via ``inst.lock_excl()``. This is the most portable. See above. +- Configure the lock timeout via the Keysight and PyVISA-Py specific attribute ``VI_KTATTR_LOCKWAIT`` (0x0FFF002B) + + When using ``VI_KTATTR_LOCKWAIT`` on an instrument that is locked by another session: + + - If ``0``, operations will fail immediately. + - If ``1``, operations will wait for ``inst.timeout`` for the lock to be removed before failing. + + The default value of ``VI_KTATTR_LOCKWAIT`` is ``FALSE/0`` (do not wait). + + In theory ``VI_KTATTR_LOCKWAIT = False`` should behave the same as + ``inst.timeout = 0`` + ``VI_KTATTR_LOCKWAIT = True`` when applied to an operation on an instrument + that already has a lock: they should reply immediately with ``VI_ERROR_RSRC_LOCKED``. + However, in practice this is not always the case, and some instruments take quite some liberties with it. + + ``VI_KTATTR_LOCKWAIT`` may not be visible in the ``pyvisa/constants.py`` file, + but it is set by PyVISA-Py. Just use as follows: + + >>> import pyvisa + >>> import pyvisa.constants + >>> rm = pyvisa.ResourceManager('@py') + >>> inst = rm.open_resource('TCPIP::192.168.1.100::INSTR') + >>> + >>> # Set lockwait to True (VI_TRUE = 1) + >>> inst.set_visa_attribute(pyvisa.constants.VI_KTATTR_LOCKWAIT, 1) # type: ignore[attr-defined] + >>> # Read back the attribute value + >>> lockwait_val = inst.get_visa_attribute(pyvisa.constants.VI_KTATTR_LOCKWAIT) # type: ignore[attr-defined] + >>> print("Lockwait state:", lockwait_val) + >>> + >>> # Do your operations + + +Note that ``open_resource()`` and ``lock_excl()`` use their own timeout values, and do not use ``VI_KTATTR_LOCKWAIT``. + +``session.lock_timeout``, from previous PyVISA-Py versions, has been removed, and replaced by the +use of ``VI_KTATTR_LOCKWAIT``, as it is easier, more predictable and more portable. + +Event handling is not affected by locking. + + +.. note:: + + **Portability:** Use of `lock_excl()` is the most portable, robust, and predictable way to handle locking. + + Know that some devices (even recent ones from the big brands), and all of the VISA + backends, use a certain amount of liberties with regards to the standards. + Do not expect respect of the following: + + - the prescribed return codes (example: you may see "I/O Timeout" instead of "Resource already locked"), + - the length of the timeouts (timeouts may be significantly longer or shorter than specified) + - the sequencing: some devices, once already locked, will allow `open_resource` + to succeed (as they should per VXI-11 spec RULE B.6.6), but others don't. + + Keysight VISA and PyVISA-py both support the lock timeout attribute ``VI_KTATTR_LOCKWAIT``. + + NI-VISA and R&S VISA have no known means of controlling the lock timeout, and mostly + use the I/O timeout and/or internal timing for lock timeout handling. + + If you are debugging locking issues, note that NI-VISA supports + the lock-on-open method, but underneath uses the lock-after-open method, and, + after having established a lock, handles the locking internally without addressing + the instrument. Remote/Local control -------------------- diff --git a/pyvisa_py/attributes.py b/pyvisa_py/attributes.py index 99ba0446..8e1f105b 100644 --- a/pyvisa_py/attributes.py +++ b/pyvisa_py/attributes.py @@ -8,7 +8,10 @@ """ from pyvisa import constants -from pyvisa.attributes import AttrVI_ATTR_TCPIP_KEEPALIVE as former_keepalive +from pyvisa.attributes import ( + AttrVI_ATTR_TCPIP_KEEPALIVE as former_keepalive, + BooleanAttribute, +) class AttrVI_ATTR_TCPIP_KEEPALIVE(former_keepalive): @@ -29,3 +32,24 @@ class AttrVI_ATTR_TCPIP_KEEPALIVE(former_keepalive): (constants.InterfaceType.tcpip, "INSTR"), (constants.InterfaceType.vicp, "INSTR"), ] + + +# force the definition of the attribute in pyvisa.constants to be able to use it in pyvisa-py +if not hasattr(constants, "VI_KTATTR_LOCKWAIT"): + constants.VI_KTATTR_LOCKWAIT = 0x0FFF002B # type: ignore[attr-defined] + constants.ResourceAttribute.lockwait = constants.VI_KTATTR_LOCKWAIT # type: ignore[attr-defined] + + class AttrVI_KTATTR_LOCKWAIT(BooleanAttribute): + resources = [ + (constants.InterfaceType.tcpip, "INSTR"), + ] + + py_name = "" + + visa_name = "VI_KTATTR_LOCKWAIT" + + visa_type = "ViBoolean" + + default = False + + read, write, local = True, True, True diff --git a/pyvisa_py/gpib.py b/pyvisa_py/gpib.py index 86b884a8..5d8b5d15 100644 --- a/pyvisa_py/gpib.py +++ b/pyvisa_py/gpib.py @@ -42,6 +42,7 @@ def __new__( # type: ignore[misc] resource_manager_session: VISARMSession, resource_name: str, parsed=None, + access_mode: constants.AccessModes = constants.AccessModes.no_lock, open_timeout: int | None = None, ) -> Session: newcls: Type @@ -54,7 +55,9 @@ def __new__( # type: ignore[misc] else: newcls = GPIBSession - return newcls(resource_manager_session, resource_name, parsed, open_timeout) + return newcls( + resource_manager_session, resource_name, parsed, access_mode, open_timeout + ) @staticmethod def list_resources() -> List[str]: diff --git a/pyvisa_py/highlevel.py b/pyvisa_py/highlevel.py index 02942e54..dabdbf02 100644 --- a/pyvisa_py/highlevel.py +++ b/pyvisa_py/highlevel.py @@ -194,7 +194,7 @@ def open( ) try: - sess = cls(session, resource_name, parsed, open_timeout) + sess = cls(session, resource_name, parsed, access_mode, open_timeout) except OpenError as e: return VISASession(0), self.handle_return_value(None, e.error_code) diff --git a/pyvisa_py/prologix.py b/pyvisa_py/prologix.py index a44fde8e..dc56d453 100644 --- a/pyvisa_py/prologix.py +++ b/pyvisa_py/prologix.py @@ -55,9 +55,12 @@ def __init__( resource_manager_session: VISARMSession, resource_name: str, parsed: rname.ResourceName | None = None, + access_mode: constants.AccessModes = constants.AccessModes.no_lock, open_timeout: int | None = None, ) -> None: - super().__init__(resource_manager_session, resource_name, parsed, open_timeout) + super().__init__( + resource_manager_session, resource_name, parsed, access_mode, open_timeout + ) # store this instance in the dictionary of Prologix interfaces self.boards[self.parsed.board] = self diff --git a/pyvisa_py/sessions.py b/pyvisa_py/sessions.py index 54e10530..3cfeafe4 100644 --- a/pyvisa_py/sessions.py +++ b/pyvisa_py/sessions.py @@ -301,12 +301,14 @@ def __init__( resource_manager_session: VISARMSession, resource_name: str, parsed: Optional[rname.ResourceName] = None, + access_mode: constants.AccessModes = constants.AccessModes.no_lock, open_timeout: Optional[int] = None, ) -> None: if parsed is None: parsed = rname.parse_resource_name(resource_name) self.parsed = parsed + self.access_mode = access_mode self.open_timeout = open_timeout #: Used as a place holder for the object doing the lowlevel communication. diff --git a/pyvisa_py/tcpip.py b/pyvisa_py/tcpip.py index ed53422a..0acfe0ca 100644 --- a/pyvisa_py/tcpip.py +++ b/pyvisa_py/tcpip.py @@ -50,7 +50,7 @@ VXI11_ERRORS_TO_VISA = { 0: StatusCode.success, # no_error 1: StatusCode.error_invalid_format, # syntax_error - 3: StatusCode.error_connection_lost, # device_no_accessible + 3: StatusCode.error_connection_lost, # device_not_accessible 4: StatusCode.error_invalid_access_key, # invalid_link_identifier 5: StatusCode.error_invalid_parameter, # parameter_error 6: StatusCode.error_handler_not_installed, # channel_not_established @@ -65,6 +65,15 @@ } +def vxi11_error_to_visa(error_code: int) -> StatusCode: + """Translate a VXI-11 return code into a VISA status code. + + Unknown VXI-11 error codes are translated to ``VI_ERROR_IO`` because + VISA has no standard status code for them. + """ + return VXI11_ERRORS_TO_VISA.get(int(error_code), StatusCode.error_io) + + @Session.register(constants.InterfaceType.tcpip, "INSTR") class TCPIPInstrSession(Session): """A class to dispatch to VXI11 or HiSLIP, based on the protocol.""" @@ -74,6 +83,7 @@ def __new__( resource_manager_session: VISARMSession, resource_name: str, parsed=None, + access_mode: constants.AccessModes = constants.AccessModes.no_lock, open_timeout: Optional[int] = None, ): newcls: Type @@ -87,7 +97,9 @@ def __new__( else: newcls = TCPIPInstrVxi11 - return newcls(resource_manager_session, resource_name, parsed, open_timeout) + return newcls( + resource_manager_session, resource_name, parsed, access_mode, open_timeout + ) @staticmethod def list_resources(wait_time=1.0) -> List[str]: @@ -477,7 +489,11 @@ class Vxi11CoreClient(vxi11.CoreClient): """ def __init__( - self, host: str, port: Optional[int], open_timeout: Optional[int] = None + self, + host: str, + port: Optional[int], + access_mode: constants.AccessModes = constants.AccessModes.no_lock, + open_timeout: Optional[int] = None, ) -> None: self._lock = threading.Lock() self.packer = vxi11.Vxi11Packer() @@ -508,9 +524,6 @@ class TCPIPInstrVxi11(Session): #: Maximum size of a chunk of data in bytes. max_recv_size: int - #: Time to wait in ms before erroring with a timeout when trying to acquire a lock - lock_timeout: int = 10000 - #: Unique ID of the client used to authenticate messages. client_id: int @@ -587,7 +600,9 @@ def after_parsing(self) -> None: else: port = None try: - self.interface = Vxi11CoreClient(host_address, port, self.open_timeout) + self.interface = Vxi11CoreClient( + host_address, port, self.access_mode, self.open_timeout + ) except rpc.RPCError: LOGGER.exception( f"Failed to open VX11 connection to {host_address} on port {port}" @@ -599,8 +614,38 @@ def after_parsing(self) -> None: self._srq_server: vxi11.SrqInterruptTCPServer | None = None self._srq_lifecycle_lock = threading.Lock() + # RULE B.6.6: + # The operation of create_link SHALL ignore locks if lockDevice is false. + # RULE B.6.7: + # If lockDevice is true and the lock is not freed after at least lock_timeout milliseconds, create_link + # SHALL terminate without creating a link and return with error set to 11, device locked by another link. + + # However, some devices, even from the big brands, once they are locked, will respect nothing of the above. + # If there is already a lock, expect some devices to act as if lockDevice is True, + # to use arbitrarily longer timeouts, and expect a reply of "timeout" instead of "device locked by another link". + # Comparable liberties are likely to be taken on device_lock(). + + if self.access_mode & constants.AccessModes.exclusive_lock: + lock_device = 1 + # The below is for lock_timeout, the instrument has been opened already + # lock_timeout can be 0 for immediate + lock_timeout = self.open_timeout + if lock_timeout is None: + lock_timeout = ( + 10000 # default lock timeout in ms. This shouldn't happen + ) + if lock_timeout == constants.VI_TMO_INFINITE: + lock_timeout = ( + 2**32 - 1 + ) # This is dangerous, but hey, the caller wanted it. + if lock_timeout == constants.VI_TMO_IMMEDIATE: + lock_timeout = 0 # This is NOP, but makes the code more readable + else: + lock_device = 0 + lock_timeout = 0 # time is not used now. + error, link, _abort_port, max_recv_size = self.interface.create_link( - self.client_id, 0, self.lock_timeout, self.parsed.lan_device_name + self.client_id, lock_device, lock_timeout, self.parsed.lan_device_name ) if error: @@ -617,6 +662,10 @@ def after_parsing(self) -> None: attribute = getattr(constants, "VI_ATTR_" + name) self.attrs[attribute] = attributes.AttributesByID[attribute].default + # add the Keysight and PyVISA-Py specific lock wait attribute, which is a boolean that + # controls whether to wait for the lock or not + self.attrs[ResourceAttribute.lockwait] = constants.VI_FALSE # type: ignore[attr-defined] + def close(self) -> StatusCode: self._stop_event_monitor() try: @@ -668,7 +717,7 @@ def _start_event_monitor(self) -> StatusCode: server.sock.close() except Exception: pass - return StatusCode.error_nonsupported_operation + return vxi11_error_to_visa(error) error = self.interface.device_enable_srq(self.link, True, b"srq") if error: @@ -681,7 +730,7 @@ def _start_event_monitor(self) -> StatusCode: server.sock.close() except Exception: pass - return StatusCode.error_io + return vxi11_error_to_visa(error) with self._event_state._lock: if ( @@ -732,6 +781,16 @@ def _stop_event_monitor(self) -> None: except Exception: pass + def _adapt_flags_and_lock_timeout(self, flags: int) -> Tuple[int, int]: + # Do as Keysight does it: + + lock_timeout = constants.VI_TMO_IMMEDIATE + if self.attrs[ResourceAttribute.lockwait] == constants.VI_TRUE: # type: ignore[attr-defined] + # Get the timeout as cleaned up by the upper layers + lock_timeout = self._io_timeout # is in ms + flags |= vxi11.OP_FLAG_WAIT_BLOCK + return flags, lock_timeout + def _read_status_from_reason( self, reason: int, suppress_end_en: bool, termchar_en: bool ) -> StatusCode: @@ -781,6 +840,8 @@ def read(self, count: int) -> Tuple[bytes, StatusCode]: else: term_char = flags = 0 + flags, lock_timeout = self._adapt_flags_and_lock_timeout(flags) + suppress_end_en, _ = self.get_attribute(ResourceAttribute.suppress_end_enabled) read_data = bytearray() @@ -820,15 +881,13 @@ def read(self, count: int) -> Tuple[bytes, StatusCode]: self.link, chunk_length, chunk_timeout, - self.lock_timeout, + lock_timeout, flags, term_char, ) - if error == vxi11.ErrorCodes.io_timeout: - return bytes(read_data), StatusCode.error_timeout - elif error: - return bytes(read_data), StatusCode.error_io + if error: + return bytes(read_data), vxi11_error_to_visa(error) read_data.extend(data) count -= len(data) @@ -869,6 +928,8 @@ def write(self, data: bytes) -> Tuple[int, StatusCode]: num = len(data) offset = 0 + flags, lock_timeout = self._adapt_flags_and_lock_timeout(flags) + while num > 0: if num <= self.max_recv_size: flags |= vxi11.OP_FLAG_END @@ -876,14 +937,11 @@ def write(self, data: bytes) -> Tuple[int, StatusCode]: block = data[offset : offset + self.max_recv_size] error, size = self.interface.device_write( - self.link, self._io_timeout, self.lock_timeout, flags, block + self.link, self._io_timeout, lock_timeout, flags, block ) - if error == vxi11.ErrorCodes.io_timeout: - return offset, StatusCode.error_timeout - - elif error or size < len(block): - return offset, StatusCode.error_io + if error or size < len(block): + return offset, vxi11_error_to_visa(error) offset += size num -= size @@ -912,22 +970,23 @@ def gpib_control_ren(self, mode: constants.RENLineOperation) -> StatusCode: Return value of the library call. """ + flags, lock_timeout = self._adapt_flags_and_lock_timeout(0) if mode in ( constants.RENLineOperation.asrt_address, constants.RENLineOperation.asrt_address_llo, ): error = self.interface.device_remote( - self.link, 0, self.lock_timeout, self._io_timeout + self.link, flags, lock_timeout, self._io_timeout ) - return VXI11_ERRORS_TO_VISA[error] + return vxi11_error_to_visa(error) elif mode in ( constants.RENLineOperation.address_gtl, constants.RENLineOperation.deassert_gtl, ): error = self.interface.device_local( - self.link, 0, self.lock_timeout, self._io_timeout + self.link, flags, lock_timeout, self._io_timeout ) - return VXI11_ERRORS_TO_VISA[error] + return vxi11_error_to_visa(error) else: return constants.StatusCode.error_nonsupported_operation @@ -953,6 +1012,9 @@ def _get_attribute(self, attribute: ResourceAttribute) -> Tuple[Any, StatusCode] if attribute == constants.VI_ATTR_TCPIP_KEEPALIVE: return self.keepalive, StatusCode.success + if attribute == constants.VI_KTATTR_LOCKWAIT: # type: ignore[attr-defined] + return self.attrs[ResourceAttribute.lockwait], StatusCode.success # type: ignore[attr-defined] + raise UnknownAttribute(attribute) def _set_attribute( @@ -991,6 +1053,9 @@ def _set_attribute( return StatusCode.error_nonsupported_format return StatusCode.success + if attribute == constants.VI_KTATTR_LOCKWAIT: # type: ignore[attr-defined] + return StatusCode.success + raise UnknownAttribute(attribute) def assert_trigger(self, protocol: constants.TriggerProtocol): @@ -1009,12 +1074,16 @@ def assert_trigger(self, protocol: constants.TriggerProtocol): Return value of the library call. """ - # XXX make this nicer (either validate protocol or pass it) + # protocol is ignored, VXI-11 doesn't support multiple types + + flags = 0 + flags, lock_timeout = self._adapt_flags_and_lock_timeout(flags) + error = self.interface.device_trigger( - self.link, 0, self.lock_timeout, self._io_timeout + self.link, flags, lock_timeout, self._io_timeout ) - return VXI11_ERRORS_TO_VISA[error] + return vxi11_error_to_visa(error) def clear(self) -> StatusCode: """Clears a device. @@ -1022,11 +1091,14 @@ def clear(self) -> StatusCode: Corresponds to viClear function of the VISA library. """ + flags = 0 + flags, lock_timeout = self._adapt_flags_and_lock_timeout(flags) + error = self.interface.device_clear( - self.link, 0, self.lock_timeout, self._io_timeout + self.link, flags, lock_timeout, self._io_timeout ) - return VXI11_ERRORS_TO_VISA[error] + return vxi11_error_to_visa(error) def read_stb(self) -> Tuple[int, StatusCode]: """Reads a status byte of the service request. @@ -1041,11 +1113,14 @@ def read_stb(self) -> Tuple[int, StatusCode]: Return value of the library call. """ + flags = 0 + flags, lock_timeout = self._adapt_flags_and_lock_timeout(flags) + error, stb = self.interface.device_read_stb( - self.link, 0, self.lock_timeout, self._io_timeout + self.link, flags, lock_timeout, self._io_timeout ) - return stb, VXI11_ERRORS_TO_VISA[error] + return stb, vxi11_error_to_visa(error) def lock( self, @@ -1079,12 +1154,26 @@ def lock( Return value of the library call. """ - # TODO: lock type not implemented + # TODO: shared lock is not implemented + if lock_type == constants.Lock.shared: + return "", StatusCode.error_nonsupported_operation + + # The only remaining lock type is exclusive lock + + # RULE B.6.74: + # If some other link has the lock, device_lock SHALL examine the waitlock + # flag in flags. If the flag is set, device_lock SHALL block until the + # lock is free. If the flag is not set, device_lock SHALL terminate with + # error set to 11, device locked by another link. + flags = 0 + # The waitlock flag is the only flag used here + if timeout != constants.VI_TMO_IMMEDIATE: + flags = vxi11.OP_FLAG_WAIT_BLOCK - error = self.interface.device_lock(self.link, flags, self.lock_timeout) + error = self.interface.device_lock(self.link, flags, timeout) - return "", VXI11_ERRORS_TO_VISA[error] + return "", vxi11_error_to_visa(error) def unlock(self) -> constants.StatusCode: """Relinquish a lock for the specified resource. @@ -1099,13 +1188,14 @@ def unlock(self) -> constants.StatusCode: """ error = self.interface.device_unlock(self.link) - return VXI11_ERRORS_TO_VISA[error] + return vxi11_error_to_visa(error) def _set_timeout(self, attribute: ResourceAttribute, value: int) -> StatusCode: """Sets timeout calculated value from python way to VI_ way""" # value is in milliseconds, # and can be VI_TMO_INFINITE (2**32 - 1) or VI_TMO_IMMEDIATE (0) self._io_timeout = value + # self.timeout comes from the superclass, is in seconds, and can be None (infinite) or 0 (immediate) if value == constants.VI_TMO_INFINITE: self.timeout = None elif value == constants.VI_TMO_IMMEDIATE: diff --git a/pyvisa_py/testsuite/test_locks.py b/pyvisa_py/testsuite/test_locks.py new file mode 100644 index 00000000..7ea07e05 --- /dev/null +++ b/pyvisa_py/testsuite/test_locks.py @@ -0,0 +1,139 @@ +"""Tests for opening VXI-11 resources.""" + +from unittest.mock import ANY, MagicMock, patch + +import pytest + +from pyvisa import constants, errors, rname +from pyvisa_py import highlevel +from pyvisa_py.tcpip import TCPIPInstrVxi11 + + +@pytest.mark.parametrize( + "open_timeout, expected_lock_timeout", + [ + (0, 0), + (2500, 2500), + (constants.VI_TMO_INFINITE, 2**32 - 1), + ], +) +def test_open_with_exclusive_lock_passes_lock_to_create_link( + open_timeout, expected_lock_timeout +): + resource_name = "TCPIP::localhost::INSTR" + parsed = rname.parse_resource_name(resource_name) + client = MagicMock() + client.create_link.return_value = (0, 1, 0, 1024) + + with patch("pyvisa_py.tcpip.Vxi11CoreClient", return_value=client): + TCPIPInstrVxi11( + 1, + resource_name, + parsed, + constants.AccessModes.exclusive_lock, + open_timeout, + ) + + client.create_link.assert_called_once_with( + ANY, 1, expected_lock_timeout, parsed.lan_device_name + ) + + +def test_open_without_exclusive_lock_passes_lock_to_create_link(): + resource_name = "TCPIP::localhost::INSTR" + parsed = rname.parse_resource_name(resource_name) + client = MagicMock() + client.create_link.return_value = (0, 1, 0, 1024) + + with patch("pyvisa_py.tcpip.Vxi11CoreClient", return_value=client): + TCPIPInstrVxi11( + 1, + resource_name, + parsed, + constants.AccessModes.no_lock, + 1234, + ) + + client.create_link.assert_called_once_with(ANY, 0, 0, parsed.lan_device_name) + + +@pytest.mark.parametrize( + "lockwait, expected_flags", + [ + (0, 0x8), + (1, 0x9), + ], +) +def test_write_sets_device_write_flags_and_lock_timeout(lockwait, expected_flags): + resource_name = "TCPIP::localhost::INSTR" + parsed = rname.parse_resource_name(resource_name) + client = MagicMock() + client.create_link.return_value = (0, 1, 0, 1024) + client.device_write.return_value = (0, 3) + + with patch("pyvisa_py.tcpip.Vxi11CoreClient", return_value=client): + session = TCPIPInstrVxi11( + 1, + resource_name, + parsed, + constants.AccessModes.no_lock, + 1234, + ) + + session.attrs[constants.ResourceAttribute.lockwait] = lockwait # type: ignore[attr-defined] + if lockwait: + expected_lock_timeout = session._io_timeout + else: + expected_lock_timeout = constants.VI_TMO_IMMEDIATE + status = session.write(b"abc") + + assert status == (3, constants.StatusCode.success) + args, _ = client.device_write.call_args + assert args[0] == session.link + assert args[1] == session._io_timeout + assert args[2] == expected_lock_timeout + assert args[3] == expected_flags + assert args[4] == b"abc" + + +@pytest.mark.parametrize( + "lock_type, timeout, expected_flags", + [ + (constants.Lock.exclusive, 0, 0x0), + (constants.Lock.exclusive, 1000, 0x1), + (constants.Lock.shared, 0, None), + (constants.Lock.shared, 1000, None), + ], +) +def test_highlevel_lock_sets_vxi11_device_lock_flags_and_timeout( + lock_type, timeout, expected_flags +): + resource_name = "TCPIP::localhost::INSTR" + parsed = rname.parse_resource_name(resource_name) + client = MagicMock() + client.create_link.return_value = (0, 1, 0, 1024) + client.device_lock.return_value = 0 + + with patch("pyvisa_py.tcpip.Vxi11CoreClient", return_value=client): + session = TCPIPInstrVxi11( + 1, + resource_name, + parsed, + constants.AccessModes.no_lock, + 1234, + ) + + library = highlevel.PyVisaLibrary() + library.sessions = {1: session} + + if lock_type == constants.Lock.shared: + with pytest.raises(errors.VisaIOError): + library.lock(1, lock_type, timeout) + client.device_lock.assert_not_called() + return + + key, status = library.lock(1, lock_type, timeout) + + assert key == "" + assert status == constants.StatusCode.success + client.device_lock.assert_called_once_with(session.link, expected_flags, timeout) diff --git a/pyvisa_py/testsuite/test_open_timeout.py b/pyvisa_py/testsuite/test_open_timeout.py index 494ecc1c..a7367da8 100644 --- a/pyvisa_py/testsuite/test_open_timeout.py +++ b/pyvisa_py/testsuite/test_open_timeout.py @@ -9,6 +9,8 @@ import pytest +from pyvisa import constants, errors +from pyvisa_py import highlevel from pyvisa_py.common import DEFAULT_OPEN_TIMEOUT, connect_timeout from pyvisa_py.protocols import hislip, rpc @@ -63,3 +65,57 @@ class _Connected(Exception): hislip.Instrument("localhost", open_timeout=open_timeout) # The deadline is set before connect() is attempted. assert seen == [expected] + + +# Per per VPP-4.3 RECOMMENDATION 4.3.2, the access_mode argument to open() +# may be interpreted as having no influence on the open timeout. +# The code now passes the access_mode to the VXI-11 client, but it is not used in the timeout calculation. +# It does have an influence on the lock timeout, but that is test in `test_locks.py` + + +@pytest.mark.parametrize("access_mode", list(constants.AccessModes)) +@pytest.mark.parametrize( + "resource_name, transport", + [ + ("TCPIP::localhost::hislip0,4880::INSTR", "hislip"), + ("TCPIP::localhost,1234::INSTR", "vxi11"), + ], +) +def test_highlevel_open_access_modes_preserve_open_timeout( + monkeypatch, access_mode, resource_name, transport +): + """Every access mode preserves the open timeout through the high-level path.""" + seen = [] + + class FakeSocket: + def settimeout(self, value): + seen.append(value) + + def connect(self, address): + raise OSError("connection intentionally not established") + + def setblocking(self, value): + pass + + def connect_ex(self, address): + return 0 + + def close(self): + pass + + def setsockopt(self, *args): + pass + + def fake_connect(sock, host, port, timeout=0): + seen.append(timeout) + return False + + monkeypatch.setattr(hislip.socket, "socket", lambda *args, **kwargs: FakeSocket()) + monkeypatch.setattr(rpc, "_connect", fake_connect) + + library = highlevel.PyVisaLibrary() + resource_manager, _ = library.open_default_resource_manager() + with pytest.raises(errors.VisaIOError): + library.open(resource_manager, resource_name, access_mode, 2500) + + assert seen == [2.5] diff --git a/pyvisa_py/testsuite/test_remote_local.py b/pyvisa_py/testsuite/test_remote_local.py index 4e81f867..5d7b9b60 100644 --- a/pyvisa_py/testsuite/test_remote_local.py +++ b/pyvisa_py/testsuite/test_remote_local.py @@ -3,11 +3,11 @@ from __future__ import annotations -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest -from pyvisa import constants +from pyvisa import constants, rname from pyvisa_py.tcpip import TCPIPInstrHiSLIP, TCPIPInstrVxi11 @@ -21,11 +21,22 @@ ], ) def test_vxi11_gpib_control_ren_calls_expected_device_method(mode, expected_method): - session = object.__new__(TCPIPInstrVxi11) - session.interface = MagicMock() - session.link = 7 - session.lock_timeout = 1234 - session._io_timeout = 5678 + resource_name = "TCPIP::localhost::INSTR" + parsed = rname.parse_resource_name(resource_name) + client = MagicMock() + client.create_link.return_value = (0, 1, 0, 1024) + client.device_write.return_value = (0, 3) + + with patch("pyvisa_py.tcpip.Vxi11CoreClient", return_value=client): + session = TCPIPInstrVxi11( + 1, + resource_name, + parsed, + constants.AccessModes.no_lock, + 1234, + ) + + session.attrs[constants.ResourceAttribute.lockwait] = 0 # type: ignore[attr-defined] session.interface.device_local.return_value = 0 session.interface.device_remote.return_value = 0 @@ -33,7 +44,7 @@ def test_vxi11_gpib_control_ren_calls_expected_device_method(mode, expected_meth assert session.gpib_control_ren(mode) == constants.StatusCode.success getattr(session.interface, expected_method).assert_called_once_with( - session.link, 0, session.lock_timeout, session._io_timeout + session.link, 0, 0, session._io_timeout ) if expected_method == "device_remote": @@ -44,11 +55,22 @@ def test_vxi11_gpib_control_ren_calls_expected_device_method(mode, expected_meth @pytest.mark.parametrize("invalid_mode", [-1, 999, "bogus", None, object()]) def test_vxi11_gpib_control_ren_rejects_unsupported_modes(invalid_mode): - session = object.__new__(TCPIPInstrVxi11) - session.interface = MagicMock() - session.link = 7 - session.lock_timeout = 1234 - session._io_timeout = 5678 + resource_name = "TCPIP::localhost::INSTR" + parsed = rname.parse_resource_name(resource_name) + client = MagicMock() + client.create_link.return_value = (0, 1, 0, 1024) + client.device_write.return_value = (0, 3) + + with patch("pyvisa_py.tcpip.Vxi11CoreClient", return_value=client): + session = TCPIPInstrVxi11( + 1, + resource_name, + parsed, + constants.AccessModes.no_lock, + 1234, + ) + + session.attrs[constants.ResourceAttribute.lockwait] = 0 # type: ignore[attr-defined] assert session.gpib_control_ren(invalid_mode) == ( constants.StatusCode.error_nonsupported_operation diff --git a/pyvisa_py/testsuite/test_tcpip_vxi11_read.py b/pyvisa_py/testsuite/test_tcpip_vxi11_read.py index 66197b10..6b1d8aea 100644 --- a/pyvisa_py/testsuite/test_tcpip_vxi11_read.py +++ b/pyvisa_py/testsuite/test_tcpip_vxi11_read.py @@ -19,7 +19,6 @@ def _make_session( sess = object.__new__(TCPIPInstrVxi11) sess.interface = MagicMock() sess.link = 1 - sess.lock_timeout = 10000 sess.max_recv_size = 1024 sess._io_timeout = 5000 sess.timeout = 5 @@ -27,6 +26,7 @@ def _make_session( ResourceAttribute.termchar_enabled: termchar_enabled, ResourceAttribute.termchar: ord("\n"), ResourceAttribute.suppress_end_enabled: suppress_end_enabled, + ResourceAttribute.lockwait: 0, # type: ignore[attr-defined] } return sess