Source code for herosdevices.core.bus.visa

"""Primitive functions and classes representing visa connections."""

from collections.abc import Iterator
from contextlib import contextmanager
from typing import TYPE_CHECKING, cast

if TYPE_CHECKING:
    from types import ModuleType

    from pyvisa.resources import MessageBasedResource

from heros.helper import log

try:
    import pyvisa
    from pyvisa.rname import ResourceName
except ModuleNotFoundError:
    pyvisa = cast("ModuleType", None)

try:
    import usb
except ModuleNotFoundError:
    usb = cast("ModuleType", None)

_USB488_REQUEST_GO_TO_LOCAL = 161  # USB488.1 GO_TO_LOCAL control request


[docs] class VisaConnection: """A class to manage VISA communication connections. This class provides functionality to handle visa connections including opening/closing connections, reading data, and writing data. Args: resource: The resource name of the visa instrument (e.g. ``TCPIP::my_device_hostname::INSTR``). keep_alive: Flag indicating whether to keep the connection open between operations. **kwargs: Keyword arguments passed to :code:`pyvisa.open_resource` """ resource: str def __init__( self, resource: str, keep_alive: bool = True, **kwargs, ) -> None: self.resource = resource if pyvisa is None: raise ModuleNotFoundError( "Could not import the 'pyvisa' python module, visa devices will not be available available" ) try: ResourceName.from_string(self.resource) except ValueError: log.exception(f"Invalid resource: {self.resource}") self._pyvisa_rm: pyvisa.ResourceManager | None = None self.connection: pyvisa.resources.MessageBasedResource | None = None self.keep_alive = keep_alive self._resource_kwargs = kwargs def _open(self) -> "MessageBasedResource": log.info(f"Connecting to {self.resource}") if self._pyvisa_rm is None: self._pyvisa_rm = pyvisa.ResourceManager("@py") assert self._pyvisa_rm is not None resource = self._pyvisa_rm.open_resource(self.resource, **self._resource_kwargs) assert isinstance(resource, pyvisa.resources.MessageBasedResource) return resource def _teardown(self) -> None: try: assert self.connection is not None self.connection.close() except pyvisa.errors.Error: log.exception("Error while closing.") finally: self.connection = None
[docs] @contextmanager def operation(self) -> Iterator[None]: """Context manager for handling visa connection operations. Ensures the visa connection is open before performing operations and closes it afterward if :code:`self.keep_alive` is False. Yields: Yields control back to the caller for performing operations within the context. """ if self.connection is None: self.connection = self._open() try: yield finally: if not self.keep_alive: self._teardown()
[docs] def close(self) -> None: """Close the visa connection if it is open. The connection is reopened automatically on the next read or write operation. """ if self.connection is not None: self._teardown()
[docs] def goto_local(self) -> None: """Set the device back to local mode by sending the USB488 GO_TO_LOCAL request. pyvisa-py asserts the remote enable line of USBTMC devices when opening a connection but never releases it, leaving the device in remote mode (e.g. with a locked front panel) even after the connection is closed. Sending this request sets the device back to local mode. This is a no-op if the connection is not a pyvisa-py USBTMC connection whose device supports the request. Errors are logged instead of raised, as this is a best effort cleanup operation. """ if usb is None: log.warning("Could not import the 'usb' python module, can not set the device to local mode") return try: with self.operation(): assert self.connection is not None session = getattr(self.connection.visalib, "sessions", {}).get(self.connection.session) interface = getattr(session, "interface", None) capabilities = getattr(interface, "_capabilities", None) if interface is None or capabilities is None or not capabilities.ren_control: return interface.usb_dev.ctrl_transfer( usb.util.build_request_type( usb.util.CTRL_IN, usb.util.CTRL_TYPE_CLASS, usb.util.CTRL_RECIPIENT_INTERFACE ), _USB488_REQUEST_GO_TO_LOCAL, 0x0000, interface.usb_intf.index, 0x0000, timeout=interface.timeout, ) except Exception: # noqa: BLE001 log.exception("Error while setting the device to local mode")
[docs] def read(self) -> str | None: """Do device read query. Returns: The decoded data as string, or None if an error occurs. """ with self.operation(): assert self.connection is not None try: return self.connection.read().rstrip("\n") except pyvisa.errors.VisaIOError: log.exception("Error during query!") self._teardown() return None
[docs] def write(self, message: str, read_echo: bool = False, *args, **kwargs) -> str | None: # noqa: ARG002 """Write to the visa connection. Args: message: The message to be written to the serial connection. read_echo: If True, reads back the echo after writing. Defaults to False. """ with self.operation(): assert self.connection is not None try: self.connection.write(message) if read_echo: return self.connection.read() except pyvisa.errors.VisaIOError: log.exception("Error during query!") self._teardown() return None