Source code for pyrolab.drivers

# Copyright © PyroLab Project Contributors
# Licensed under the terms of the GNU GPLv3+ License
# (see pyrolab/__init__.py for details)

"""
Drivers
=======

Submodule containing drivers for each supported instrument type.
"""

import logging
from typing import Any, Dict, List

from pyrolab.api import expose
from pyrolab.service import Service

log = logging.getLogger(__name__)


[docs] class Instrument(Service): """ Abstract base class provides a common interface for services and instruments. While not a true abstract base class (it *can* be instantiated), all the essential functions raise NotImplementedErrors when run. They are therefore required to be overridden by derived classes. Note that, in order to support autoconnect within the PyroLab framework, the ``__init__`` method is never used to set up or connect to the instrument. This is because when objects are hosted by a PyroLab server, they are instantiated and that object exists in perpetuity. Since we'd like to be able to connect and disconnect from devices while leaving the server running (an example use case would be to use the device locally, in a lab, manipulating physical controls, without killing the server connection), separate methods ``connect()`` and ``close()`` are required. In this way, instruments can be forcibly disconnected without leaving the hosting object in an unrecoverable state. Attributes ---------- _autoconnect_params : dict A private dictionary of parameters that will be used to connect to the instrument when hosted by a PyroLab server. This value should never be manipulated by a user. It is listed here to prevent accidental overwriting by an unwitting user wanting to use the same name. """ def __init__(self) -> None: if not hasattr(self, "_autoconnect_params"): self._autoconnect_params: Dict[str, Any] = {} def __del__(self) -> None: """ Destructor. Automatically calls ``close()``. Automatically releases any potentially claimed resources. # TODO: This function is unsafe! It is not guaranteed to be called! # Enforce calling close() explicitly. """ log.info("Destructing %s", self.__class__.__name__) self.close()
[docs] @staticmethod def detect_devices() -> List[Dict[str, Any]]: """ Returns a list of connection parameters for available devices. Static function that can be called without object instantiation. Returns all available devices that can be detected on the local computer. Each available devices is represented by a dictionary that could be passed directly to :py:func:`connect` using dictionary unpacking. Returns ------- List[Dict[str, Any]] Each list item represents a unique device. The dictionary is the keyword arguments passed to ``connect()``. If devices cannot be detected, this should return an empty list. Examples -------- >>> available = Instrument.detect_devices() >>> device = Instrument() >>> device.connect(**available[0]) """ raise NotImplementedError
[docs] def connect(self, **kwargs) -> bool: """ Connects to instruments or services that require initialization. All parameters must be keyword arguments; the presence of any positional arguments will break the functionality of ``autoconnect()``. The base class implements parameters as a keyword argument dictionary. Derived classes may declare explicit parameter lists, but all arguments are required to be keyword arguments, i.e. parameters with default values. Default values do not necessarily need to be sensible; for example, a default value of None for some required parameter. The code should then raise a ValueError for missing parameters. The dictionary construct must be used because the autoconnect functionality delivers values to this function as an unpacked dictionary. Parameters ---------- kwargs : dict A dictionary of parameters required for connection and setup. Returns ------- bool True if connection was successful, False otherwise. Raises ------ NotImplementedError If the derived class does not implement this method. """ raise NotImplementedError
[docs] @expose def autoconnect(self) -> bool: """ Autoconnect to an instrument using internally stored parameters. If the device is persisted in PyroLab's program data, the parameters for connect should be saved and associated with the object (in the attribute :py:attr:`_autoconnect_params`). ``autoconnect()`` simply calls :py:func:`connect` with the saved parameters. Returns ------- bool True if connection was successful, False otherwise. Raises ------ Exception If the autoconnect parameters are missing. """ if not self._autoconnect_params: raise Exception("No autoconnection parameters available") else: return self.connect(**self._autoconnect_params)
[docs] def close(self) -> None: """ Releases resources, hardware or otherwise. Deletion of object should also automatically call ``close()``, which should take no parameters. Raises ------ NotImplementedError If not overridden; should return None otherwise. """ raise NotImplementedError