diff --git a/CHANGES b/CHANGES index 6506b682..5573055c 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,7 @@ PyVISA-py Changelog 0.9.0 (unreleased) ------------------ +- 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 ``VI_SUCCESS_TERM_CHAR``. VPP-4.3 RULE 6.1.1 gives END priority over the diff --git a/docs/source/faq.rst b/docs/source/faq.rst index 1b4db1c9..6675a8de 100644 --- a/docs/source/faq.rst +++ b/docs/source/faq.rst @@ -181,6 +181,32 @@ omit the argument. wait on a lock". Set ``lock_timeout`` on the session for that, as in the DM3068 example above. +Remote/Local control +-------------------- + +Setting an instrument to Remote or Local is possible via VXI-11, HiSLIP and GPIB. + +In PyVISA, this is done through ``inst.control_ren(pyvisa.constants.RENLineOperation.{op})`` +where valid ``{op}`` values are: + +================ =========== ========================================= +RENLineOperation VXI-11 HiSLIP +================ =========== ========================================= +address_gtl goto local goto local, no change to remote enable +asrt error enable remote +asrt_address goto remote enable remote, goto remote +asrt_address_llo goto remote enable remote, goto remote, local lockout +asrt_llo error enable remote, local lockout +deassert error disable remote +deassert_gtl goto local disable remote, goto local +================ =========== ========================================= + +This is fully conform to what VPP-4.3 Rule 6.5.6 and Observations 6.5.1 + 6.5.2 say, +and what NI-VISA does, so this should be fully portable. + +GPIB has functionality comparable to HiSLIP, but may behave differently than NI-VISA, +depending on the type of interface. + .. _PySerial: https://pythonhosted.org/pyserial/ .. _PyVISA: http://pyvisa.readthedocs.org/ diff --git a/pyvisa_py/tcpip.py b/pyvisa_py/tcpip.py index f6287171..ed53422a 100644 --- a/pyvisa_py/tcpip.py +++ b/pyvisa_py/tcpip.py @@ -16,7 +16,7 @@ import threading import time import warnings -from typing import Any, Dict, List, Optional, Tuple, Type, cast +from typing import Any, Dict, Final, List, Optional, Tuple, Type, cast from pyvisa import attributes, constants, errors, rname from pyvisa.constants import BufferOperation, ResourceAttribute, StatusCode @@ -112,6 +112,16 @@ class TCPIPInstrHiSLIP(Session): # need to define session_type to make the set_attribute machinery work. session_type = (constants.InterfaceType.tcpip, "INSTR") + REMOTELOCALOPCODE: Final[dict[constants.RENLineOperation, str]] = { + constants.RENLineOperation.address_gtl: "justGTL", + constants.RENLineOperation.asrt: "enableRemote", + constants.RENLineOperation.asrt_address: "enableAndGotoRemote", + constants.RENLineOperation.asrt_address_llo: "enableAndGTRLLO", + constants.RENLineOperation.asrt_llo: "enableAndLockoutLocal", + constants.RENLineOperation.deassert: "disableRemote", + constants.RENLineOperation.deassert_gtl: "disableAndGTL", + } + # Override parsed to take into account the fact that this class is only used # for a specific kind of resource parsed: rname.TCPIPInstr @@ -315,6 +325,36 @@ def write(self, data: bytes) -> Tuple[int, StatusCode]: return len(data), StatusCode.success + def gpib_control_ren(self, mode: constants.RENLineOperation) -> StatusCode: + """Controls the state of the GPIB Remote Enable (REN) interface line. + + Optionally the remote/local state of the device is also controlled. + + Corresponds to viGpibControlREN function of the VISA library. + + Parameters + ---------- + mode : constants.RENLineOperation + Specifies the state of the REN line and optionally the device + remote/local state. + + Returns + ------- + StatusCode + Return value of the library call. + + """ + try: + method = self.REMOTELOCALOPCODE[mode] + except KeyError: + # unknown value? + return StatusCode.error_nonsupported_operation + + interface = cast(hislip.Instrument, self.interface) + interface.async_remote_local_control(method) + + return StatusCode.success + def clear(self) -> StatusCode: """Clears a device. @@ -853,6 +893,44 @@ def write(self, data: bytes) -> Tuple[int, StatusCode]: except vxi11.Vxi11Error: return 0, StatusCode.error_timeout + def gpib_control_ren(self, mode: constants.RENLineOperation) -> StatusCode: + """Controls the state of the GPIB Remote Enable (REN) interface line. + + Optionally the remote/local state of the device is also controlled. + + Corresponds to viGpibControlREN function of the VISA library. + + Parameters + ---------- + mode : constants.RENLineOperation + Specifies the state of the REN line and optionally the device + remote/local state. + + Returns + ------- + StatusCode + Return value of the library call. + + """ + 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 + ) + return VXI11_ERRORS_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 + ) + return VXI11_ERRORS_TO_VISA[error] + else: + return constants.StatusCode.error_nonsupported_operation + def _get_attribute(self, attribute: ResourceAttribute) -> Tuple[Any, StatusCode]: """Get the value for a given VISA attribute for this session. diff --git a/pyvisa_py/testsuite/test_remote_local.py b/pyvisa_py/testsuite/test_remote_local.py new file mode 100644 index 00000000..4e81f867 --- /dev/null +++ b/pyvisa_py/testsuite/test_remote_local.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +"""Unit tests for VXI11 and hislip remote/local operations.""" + +from __future__ import annotations + +from unittest.mock import MagicMock + +import pytest + +from pyvisa import constants +from pyvisa_py.tcpip import TCPIPInstrHiSLIP, TCPIPInstrVxi11 + + +@pytest.mark.parametrize( + "mode, expected_method", + [ + (constants.RENLineOperation.address_gtl, "device_local"), + (constants.RENLineOperation.asrt_address, "device_remote"), + (constants.RENLineOperation.asrt_address_llo, "device_remote"), + (constants.RENLineOperation.deassert_gtl, "device_local"), + ], +) +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 + + session.interface.device_local.return_value = 0 + session.interface.device_remote.return_value = 0 + + 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 + ) + + if expected_method == "device_remote": + session.interface.device_local.assert_not_called() + else: + session.interface.device_remote.assert_not_called() + + +@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 + + assert session.gpib_control_ren(invalid_mode) == ( + constants.StatusCode.error_nonsupported_operation + ) + session.interface.device_local.assert_not_called() + session.interface.device_remote.assert_not_called() + + +@pytest.mark.parametrize( + "mode, expected_method", + [ + (constants.RENLineOperation.address_gtl, "justGTL"), + (constants.RENLineOperation.asrt, "enableRemote"), + (constants.RENLineOperation.asrt_address, "enableAndGotoRemote"), + (constants.RENLineOperation.asrt_address_llo, "enableAndGTRLLO"), + (constants.RENLineOperation.asrt_llo, "enableAndLockoutLocal"), + (constants.RENLineOperation.deassert, "disableRemote"), + (constants.RENLineOperation.deassert_gtl, "disableAndGTL"), + ], +) +def test_hislip_gpib_control_ren_calls_expected_interface_method(mode, expected_method): + session = object.__new__(TCPIPInstrHiSLIP) + session.interface = MagicMock() + + assert session.gpib_control_ren(mode) == constants.StatusCode.success + session.interface.async_remote_local_control.assert_called_once_with( + expected_method + ) + + +@pytest.mark.parametrize("invalid_mode", [-1, 999, "bogus", None, object()]) +def test_hislip_gpib_control_ren_rejects_unsupported_modes(invalid_mode): + session = object.__new__(TCPIPInstrHiSLIP) + session.interface = MagicMock() + + assert session.gpib_control_ren(invalid_mode) == ( + constants.StatusCode.error_nonsupported_operation + ) + session.interface.async_remote_local_control.assert_not_called()