Source code for qcodes.instrument.ip

"""Ethernet instrument driver class based on sockets."""

from __future__ import annotations

import logging
import socket
from typing import TYPE_CHECKING, Any

from .instrument import Instrument

if TYPE_CHECKING:
    from collections.abc import Sequence
    from types import TracebackType
    from typing import Unpack

    from qcodes.metadatable import SnapshotUpdate

    from .instrument_base import InstrumentBaseKWArgs

log = logging.getLogger(__name__)


[docs] class IPInstrument(Instrument): r""" Bare socket ethernet instrument implementation. Use of `VisaInstrument` is promoted instead of this. Args: name: What this instrument is called locally. address: The IP address or name. If not given on construction, must be provided before any communication. port: The IP port. If not given on construction, must be provided before any communication. timeout: Seconds to allow for responses. Default 5. terminator: Character(s) to terminate each send. Default '\n'. persistent: Whether to leave the socket open between calls. Default True. write_confirmation: Whether the instrument acknowledges writes with some response we should read. Default True. **kwargs: Forwarded to the base class. See help for ``qcodes.Instrument`` for additional information on writing instrument subclasses. """ def __init__( self, name: str, address: str | None = None, port: int | None = None, timeout: float = 5, terminator: str = "\n", persistent: bool = True, write_confirmation: bool = True, **kwargs: Unpack[InstrumentBaseKWArgs], ): super().__init__(name, **kwargs) self._address = address self._port = port self._timeout = timeout self._terminator = terminator self._confirmation = write_confirmation self._ensure_connection = EnsureConnection(self) self._buffer_size = 1400 self._socket: socket.socket | None = None self.set_persistent(persistent)
[docs] def set_address(self, address: str | None = None, port: int | None = None) -> None: """ Change the IP address and/or port of this instrument. Args: address: The IP address or name. port: The IP port. """ if address is not None: self._address = address elif not hasattr(self, "_address"): raise TypeError( "This instrument doesn't have an address yet, you must provide one." ) if port is not None: self._port = port elif not hasattr(self, "_port"): raise TypeError( "This instrument doesn't have a port yet, you must provide one." ) self._disconnect() self.set_persistent(self._persistent)
[docs] def set_persistent(self, persistent: bool) -> None: """ Change whether this instrument keeps its socket open between calls. Args: persistent: Set True to keep the socket open all the time. """ self._persistent = persistent if persistent: self._connect() else: self._disconnect()
[docs] def flush_connection(self) -> None: self._recv()
def _connect(self) -> None: if self._socket is not None: self._disconnect() try: log.info("Opening socket") self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) log.info(f"Connecting socket to {self._address}:{self._port}") self._socket.connect((self._address, self._port)) self.set_timeout(self._timeout) except ConnectionRefusedError: log.warning("Socket connection failed") if self._socket is not None: self._socket.close() self._socket = None raise def _disconnect(self) -> None: if self._socket is None: return log.info("Socket shutdown") self._socket.shutdown(socket.SHUT_RDWR) log.info("Socket closing") self._socket.close() log.info("Socket closed") self._socket = None
[docs] def set_timeout(self, timeout: float) -> None: """ Change the read timeout for the socket. Args: timeout: Seconds to allow for responses. """ self._timeout = timeout if self._socket is not None: self._socket.settimeout(float(self._timeout))
[docs] def set_terminator(self, terminator: str) -> None: r""" Change the write terminator to use. Args: terminator: Character(s) to terminate each send. Default '\n'. """ self._terminator = terminator
def _send(self, cmd: str) -> None: if self._socket is None: raise RuntimeError(f"IPInstrument {self.name} is not connected") data = cmd + self._terminator log.debug(f"Writing {data} to instrument {self.name}") self._socket.sendall(data.encode()) def _recv(self) -> str: if self._socket is None: raise RuntimeError(f"IPInstrument {self.name} is not connected") result = self._socket.recv(self._buffer_size) log.debug(f"Got {result!r} from instrument {self.name}") if result == b"": log.warning("Got empty response from Socket recv() Connection broken.") return result.decode()
[docs] def close(self) -> None: """Disconnect and irreversibly tear down the instrument.""" self._disconnect() super().close()
[docs] def write_raw(self, cmd: str) -> None: """ Low-level interface to send a command that gets no response. Args: cmd: The command to send to the instrument. """ with self._ensure_connection: self._send(cmd) if self._confirmation: self._recv()
[docs] def ask_raw(self, cmd: str) -> str: """ Low-level interface to send a command an read a response. Args: cmd: The command to send to the instrument. Returns: The instrument's string response. """ with self._ensure_connection: self._send(cmd) return self._recv()
[docs] def snapshot_base( self, update: bool | SnapshotUpdate | None = "Only_invalid", params_to_skip_update: Sequence[str] | None = None, ) -> dict[Any, Any]: """ State of the instrument as a JSON-compatible dict (everything that the custom JSON encoder class :class:`.NumpyJSONEncoder` supports). Args: update: If ``"All"``, update the state by querying the instrument. If ``"Only_invalid"`` (the default) only update values whose cache is invalid. If ``"Never"``, just use the latest values in memory and never update. params_to_skip_update: List of parameter names that will be skipped in update even if update is ``"All"``. This is useful if you have parameters that are slow to update but can be updated in a different way (as in the qdac). If you want to skip the update of certain parameters in all snapshots, use the `snapshot_get` attribute of those parameters: instead. Returns: dict: base snapshot """ snap = super().snapshot_base( update=update, params_to_skip_update=params_to_skip_update ) snap["port"] = self._port snap["confirmation"] = self._confirmation snap["address"] = self._address snap["terminator"] = self._terminator snap["timeout"] = self._timeout snap["persistent"] = self._persistent return snap
class EnsureConnection: """ Context manager to ensure an instrument is connected when needed. Uses ``instrument._persistent`` to determine whether or not to close the connection immediately on completion. Args: instrument: the instance to connect. """ def __init__(self, instrument: IPInstrument): self.instrument = instrument def __enter__(self) -> None: """Make sure we connect when entering the context.""" if not self.instrument._persistent or self.instrument._socket is None: self.instrument._connect() def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ) -> None: """Possibly disconnect on exiting the context.""" if not self.instrument._persistent: self.instrument._disconnect()