Source code for stoner_measurement.instruments.transport.gpib_transport

"""GPIB transport implementation via PyVISA.

Provides a :class:`BaseTransport` implementation that communicates with
instruments over a GPIB (IEEE-488) bus using the :mod:`pyvisa` library.
PyVISA is an optional run-time dependency; an :exc:`ImportError` is raised
at construction time if it is not installed.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from stoner_measurement.instruments.transport.base import BaseTransport

if TYPE_CHECKING:
    import pyvisa
    import pyvisa.resources


[docs] class GpibTransport(BaseTransport): """GPIB transport using PyVISA. Wraps a PyVISA ``GPIBInstrument`` resource to provide the standard :class:`BaseTransport` interface. The VISA resource string is constructed from *address* and *board* as ``"GPIB<board>::<address>::INSTR"``. Attributes: address (int): GPIB primary address of the instrument (0–30). board (int): GPIB board (interface) index. Defaults to ``0``. timeout (float): Read timeout in seconds. Defaults to ``2.0``. Examples: >>> from stoner_measurement.instruments.transport import GpibTransport >>> t = GpibTransport(address=22) >>> t.address 22 >>> t.board 0 >>> t.resource_string 'GPIB0::22::INSTR' """ def __init__(self, address: int, board: int = 0, timeout: float = 2.0) -> None: """Initialise the GPIB transport. Args: address (int): GPIB primary address (0–30). Keyword Parameters: board (int): GPIB board index. Defaults to ``0``. timeout (float): Read timeout in seconds. Defaults to ``2.0``. Raises: ImportError: If :mod:`pyvisa` is not installed. """ try: import pyvisa as _pyvisa # noqa: F401 except ImportError as exc: raise ImportError("pyvisa is required for GpibTransport. " "Install it with: pip install pyvisa") from exc super().__init__(timeout=timeout) self.address = address self.board = board self._resource: pyvisa.resources.GPIBInstrument | None = None self._rm: pyvisa.ResourceManager | None = None @property def resource_string(self) -> str: """VISA resource string for this GPIB address. Returns: (str): Resource string in the form ``"GPIB<board>::<address>::INSTR"``. Examples: >>> from stoner_measurement.instruments.transport import GpibTransport >>> GpibTransport(address=14, board=1).resource_string 'GPIB1::14::INSTR' """ return f"GPIB{self.board}::{self.address}::INSTR"
[docs] def open(self) -> None: """Open the GPIB resource via PyVISA. Raises: ConnectionError: If the resource cannot be opened. """ import pyvisa try: self._rm = pyvisa.ResourceManager() self._resource = self._rm.open_resource(self.resource_string) self._resource.timeout = int(self._timeout * 1000) # pyvisa uses milliseconds self._is_open = True except pyvisa.VisaIOError as exc: raise ConnectionError(f"Cannot open GPIB resource {self.resource_string!r}: {exc}") from exc
[docs] def close(self) -> None: """Close the GPIB resource.""" if self._resource is not None: self._resource.close() self._resource = None if self._rm is not None: self._rm.close() self._rm = None self._is_open = False
[docs] def write(self, data: bytes) -> None: """Write *data* to the GPIB instrument. Args: data (bytes): Raw bytes to transmit. The bytes are decoded as ASCII before being passed to PyVISA's ``write_raw``. Raises: ConnectionError: If the transport is not open. """ if self._resource is None: raise ConnectionError("GPIB transport is not open.") self._resource.write_raw(data)
[docs] def read(self, num_bytes: int = 4096) -> bytes: """Read up to *num_bytes* from the GPIB instrument. Args: num_bytes (int): Maximum number of bytes to read. Defaults to ``4096``. Returns: (bytes): Bytes received. Raises: ConnectionError: If the transport is not open. TimeoutError: If no data arrives within :attr:`timeout` seconds. """ import pyvisa if self._resource is None: raise ConnectionError("GPIB transport is not open.") try: return self._resource.read_raw(num_bytes) except pyvisa.errors.VisaIOError as exc: raise TimeoutError(f"Timeout reading from GPIB address {self.address}: {exc}") from exc
@property def transport_address(self) -> str: """Return the VISA resource string for this GPIB instrument. Returns: (str): VISA resource string, e.g. ``"GPIB0::22::INSTR"``. Examples: >>> from stoner_measurement.instruments.transport import GpibTransport >>> GpibTransport(address=22).transport_address 'GPIB0::22::INSTR' """ return self.resource_string def _apply_timeout(self, value: float) -> None: """Update the PyVISA timeout on a live connection. PyVISA expresses timeout in milliseconds. """ if self._resource is not None: self._resource.timeout = int(value * 1000)
[docs] def read_status_byte(self) -> int | None: """Return the IEEE 488.2 status byte via a GPIB serial poll. Performs an out-of-band serial poll (PyVISA ``read_stb()``) without transmitting any command to the instrument. This is the preferred mechanism for checking the GPIB status byte because it does not consume a response from the instrument's output buffer. Returns: (int | None): The 8-bit status byte, or ``None`` if the transport is not currently open. Examples: >>> from stoner_measurement.instruments.transport import GpibTransport >>> t = GpibTransport(address=22) >>> t.read_status_byte() is None # not open True """ if self._resource is None: return None return int(self._resource.read_stb())