diff --git a/.claude/skills/pyhardwarelibrary/SKILL.md b/.claude/skills/pyhardwarelibrary/SKILL.md index 1ba0ff2..61552c1 100644 --- a/.claude/skills/pyhardwarelibrary/SKILL.md +++ b/.claude/skills/pyhardwarelibrary/SKILL.md @@ -131,11 +131,11 @@ the capabilities it declares — check the table. | Capability | Methods | |---|---| -| `OnOffControl` | `turnOn()`, `turnOff()`, `isLaserOn()`, `canTurnOn()` | -| `ShutterControl` | `openShutter()`, `closeShutter()`, `isShutterOpen()` | -| `PowerControl` | `setPower(watts)`, `power()` | -| `InterlockControl` | `interlock()` | -| `WavelengthControl` | `setWavelength(nm)`, `wavelength()`, `wavelengthRange()` | +| `OnOffCapability` | `turnOn()`, `turnOff()`, `isLaserOn()`, `canTurnOn()` | +| `ShutterCapability` | `openShutter()`, `closeShutter()`, `isShutterOpen()` | +| `PowerCapability` | `setPower(watts)`, `power()` | +| `InterlockCapability` | `interlock()` | +| `WavelengthCapability` | `setWavelength(nm)`, `wavelength()`, `wavelengthRange()` | ```python from hardwarelibrary.sources.millennia import MillenniaEv25Device @@ -153,7 +153,7 @@ laser.shutdownDevice() - Cobolt: `CoboltDevice(portPath="COM3")` — OnOff + Power (+ autostart constraints; it may refuse `turnOn()` when autostart is on, raising `CoboltCantTurnOnWithAutostartOn`). -- Matisse: `MatisseDevice(...)` over TCP — `WavelengthControl` (`setWavelength`/`wavelength`) +- Matisse: `MatisseDevice(...)` over TCP — `WavelengthCapability` (`setWavelength`/`wavelength`) plus BiFi/etalon/piezo/scan methods. ### Power meters — Gentec-EO Integra @@ -181,7 +181,7 @@ v = daq.getAnalogVoltage(channel=0) # read daq.setAnalogVoltage(2.5, channel=1) # write (U3 DACs are slow PWM) bit = daq.getDigitalValue(channel=4) daq.setDigitalValue(1, channel=5) -# Hardware-timed acquisition: daq.acquireWaveform(...) (AnalogInputStreamDevice) +# Hardware-timed acquisition: daq.acquireWaveform(...) (AnalogInputStreamCapability) daq.shutdownDevice() ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index af8892e..328f8af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,8 @@ API changes can land even when the minor version is unchanged. lab Genesis CX-Vis (head `G532`). A HOPS supply is not a serial device: its FTDI FT2232 (`0x0403:0x6010`) is driven as bit-banged I2C, with power DAC, ADC, shutter/enable GPIO, and the head identity/calibration EEPROM all on one I2C - bus (see `manuals/Coherent-HOPS-*`). `VerdiGDevice` combines `OnOffControl`, - `ShutterControl`, `PowerControl`, and `InterlockControl`, and drives the bus + bus (see `manuals/Coherent-HOPS-*`). `VerdiGDevice` combines `OnOffCapability`, + `ShutterCapability`, `PowerCapability`, and `InterlockCapability`, and drives the bus through an interchangeable `HOPSInterface`: - `HOPSNativeInterface` (`sources/hopsnative.py`): **pure-Python** pyftdi I2C, no DLL (macOS/Linux). Hardware-confirmed end to end on the lab unit @@ -28,6 +28,39 @@ API changes can land even when the minor version is unchanged. decode in `manuals/Coherent-HOPS-2-USB-and-DLL-Protocol.md` and `manuals/Coherent-HOPS-3-I2C-Wire-Protocol.md`. +### Changed +- **Breaking:** capability mixins across all families now use a uniform + `*Capability` suffix, reserving `*Device` for instantiable hardware drivers. + Public methods and behavior are unchanged; only the mixin class names change. + Drivers subclassing these must update their base-class lists and imports. + - DAQ: `AnalogInputDevice` -> `AnalogInputCapability`, `AnalogOutputDevice` -> + `AnalogOutputCapability`, `AnalogIODevice` -> `AnalogIOCapability`, + `AnalogInputStreamDevice` -> `AnalogInputStreamCapability`, + `DigitalInputDevice` -> `DigitalInputCapability`, `DigitalOutputDevice` -> + `DigitalOutputCapability`, `DigitalIODevice` -> `DigitalIOCapability`, + `PhaseLockedDetectionDevice` -> `PhaseLockedDetectionCapability`, + `TriggerableDevice` -> `TriggerCapability`. + - Laser sources: `OnOffControl` -> `OnOffCapability`, `ShutterControl` -> + `ShutterCapability`, `PowerControl` -> `PowerCapability`, `InterlockControl` + -> `InterlockCapability`, `AutostartControl` -> `AutostartCapability`, + `WavelengthControl` -> `WavelengthCapability`, `DispersionControl` -> + `DispersionCapability`. + - Power meters: `WavelengthCalibratable` -> `WavelengthCalibrationCapability`, + `AutoScalable` -> `AutoScaleCapability`, `ScaleAdjustable` -> + `ScaleCapability`. +- **Breaking:** all capability mixins are consolidated into a single module, + `hardwarelibrary/capabilities.py`, and share one `Capability` base class (the + per-family `sources/capabilities.py`, `powermeters/capabilities.py`, and + `daq/daqdevice.py` are removed; the DAQ enums `InputSource`, `TriggerSource`, + `SampleClock` move there too, and the acquisition notification enum is now + nested as `AnalogInputStreamCapability.Notification`). Imports must point at + `hardwarelibrary.capabilities` (the family package `__init__`s still re-export + their own mixins, so `from hardwarelibrary.daq import AnalogIOCapability` and + the like keep working). `capabilities()` / `hasCapability()` are hoisted onto + `PhysicalDevice`, so every device -- including DAQ drivers -- now supports + capability introspection; the duplicated methods on `LaserSourceDevice` and + `PowerMeterDevice` are gone (`LaserSourceDevice` is now a pure marker). + ## [1.4.0] - 2026-07-08 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index eea8fed..c5ee73d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,16 +59,17 @@ Each family has an **abstract base class**. A driver subclasses it and implement **Thorlabs linear motion** is a backend dispatcher: `ThorlabsDevice` (in `motion/thorlabs.py`) routes to `ThorlabsKinesisDevice`, which drives the stage through `pylablib`'s Kinesis support. The old FTDI path (`ThorlabsFTDIDevice`) was dropped. Install with the `thorlabs` extra (`pip install -e .[thorlabs]`). -Both the **DAQ and laser-source families use interface-segregated capability mixins** instead of one fat base class, because a device may implement any subset of the capabilities. +All families **use interface-segregated capability mixins** instead of one fat base class, because a device may implement any subset of the capabilities. Every mixin lives in the single module `hardwarelibrary/capabilities.py` and subclasses the one `Capability` base there; mixins carry the `*Capability` suffix, and only instantiable hardware drivers are named `*Device`. `PhysicalDevice` provides `capabilities()` / `hasCapability(cls)`, which introspect any device by walking its MRO for `Capability` subclasses — so every family gets capability introspection for free. -- DAQ (`daq/daqdevice.py`): `AnalogInputDevice` (`getAnalogVoltage`), `AnalogOutputDevice` (`setAnalogVoltage`), `DigitalInputDevice` (`getDigitalValue`), `DigitalOutputDevice` (`setDigitalValue`), plus `AnalogIODevice` / `DigitalIODevice` that combine each pair, and `AnalogInputStreamDevice` for hardware-timed acquisition. The `configure*` and `direction*` methods are optional no-op hooks. Example: `class LabjackDevice(PhysicalDevice, AnalogIODevice, DigitalIODevice, AnalogInputStreamDevice)`. -- Laser sources (`sources/capabilities.py`): `OnOffControl` (`turnOn`/`turnOff`/`isLaserOn`), `ShutterControl` (`openShutter`/`closeShutter`/`isShutterOpen`), `PowerControl` (`setPower`/`power`), `InterlockControl` (`interlock`), `AutostartControl`, `WavelengthControl` (`setWavelength`/`wavelength`), `DispersionControl`. Each exposes a public method that calls a `do*` abstract hook the driver implements. `LaserSourceDevice` is a thin marker base; the behavior comes from the mixins. Examples: `class CoboltDevice(LaserSourceDevice, OnOffControl, PowerControl, ...)`, `class MillenniaEv25Device(LaserSourceDevice, OnOffControl, ShutterControl, PowerControl)`, `class MatisseDevice(PhysicalDevice, WavelengthControl)`. +- DAQ: `AnalogInputCapability` (`getAnalogVoltage`), `AnalogOutputCapability` (`setAnalogVoltage`), `DigitalInputCapability` (`getDigitalValue`), `DigitalOutputCapability` (`setDigitalValue`), plus `AnalogIOCapability` / `DigitalIOCapability` that combine each pair, and `AnalogInputStreamCapability` for hardware-timed acquisition. The `configure*` and `direction*` methods are optional no-op hooks. Example: `class LabjackDevice(PhysicalDevice, AnalogIOCapability, DigitalIOCapability, AnalogInputStreamCapability)`. +- Laser sources: `OnOffCapability` (`turnOn`/`turnOff`/`isLaserOn`), `ShutterCapability` (`openShutter`/`closeShutter`/`isShutterOpen`), `PowerCapability` (`setPower`/`power`), `InterlockCapability` (`interlock`), `AutostartCapability`, `WavelengthCapability` (`setWavelength`/`wavelength`), `DispersionCapability`. Each exposes a public method that calls a `do*` abstract hook the driver implements. `LaserSourceDevice` is a thin marker base; the behavior comes from the mixins. Examples: `class CoboltDevice(LaserSourceDevice, OnOffCapability, PowerCapability, ...)`, `class MillenniaEv25Device(LaserSourceDevice, OnOffCapability, ShutterCapability, PowerCapability)`, `class MatisseDevice(PhysicalDevice, WavelengthCapability)`. +- Power meters: `WavelengthCalibrationCapability`, `AutoScaleCapability`, `ScaleCapability` (on top of the base `doGetAbsolutePower` every meter implements). ## Adding a new device Reference implementation: `hardwarelibrary/daq/labjackdevice.py`. The pattern: -1. Subclass the family base (it already extends `PhysicalDevice`). For DAQ, combine `PhysicalDevice` with the capability mixins you need, e.g. `class FooDAQ(PhysicalDevice, AnalogIODevice)`. +1. Subclass the family base (it already extends `PhysicalDevice`). For DAQ, combine `PhysicalDevice` with the capability mixins you need, e.g. `class FooDAQ(PhysicalDevice, AnalogIOCapability)`. 2. Set class attributes `classIdVendor` and `classIdProduct` (USB VID/PID, or the equivalent for serial-only devices). 3. Implement `doInitializeDevice` and `doShutdownDevice`. Keep them minimal — open the port, close the port. 4. Implement the family's abstract hooks (see the table). Omitting one raises `TypeError` at instantiation, so the class will not even construct until the contract is complete. diff --git a/docs/source/api/core.rst b/docs/source/api/core.rst index 45c3497..a7601c6 100644 --- a/docs/source/api/core.rst +++ b/docs/source/api/core.rst @@ -9,6 +9,15 @@ PhysicalDevice .. automodule:: hardwarelibrary.physicaldevice +Capabilities +------------ + +Interface-segregated capability mixins shared by every device family. Each +subclasses the single ``Capability`` base; ``PhysicalDevice`` introspects them +through ``capabilities()`` / ``hasCapability()``. + +.. automodule:: hardwarelibrary.capabilities + DeviceManager ------------- diff --git a/docs/source/api/daq.rst b/docs/source/api/daq.rst index a7d887d..3cf3533 100644 --- a/docs/source/api/daq.rst +++ b/docs/source/api/daq.rst @@ -1,12 +1,9 @@ Data acquisition (DAQ) ====================== -Capability mixins and DAQ drivers. - -DAQDevice ---------- - -.. automodule:: hardwarelibrary.daq.daqdevice +DAQ drivers. The DAQ capability mixins (``AnalogIOCapability``, +``DigitalIOCapability``, ``AnalogInputStreamCapability``, ...) live in +:mod:`hardwarelibrary.capabilities`; see the Core page. LabJack ------- diff --git a/docs/source/api/sources.rst b/docs/source/api/sources.rst index bb3ba43..f923ad7 100644 --- a/docs/source/api/sources.rst +++ b/docs/source/api/sources.rst @@ -8,10 +8,8 @@ LaserSourceDevice .. automodule:: hardwarelibrary.sources.lasersourcedevice -Capabilities ------------- - -.. automodule:: hardwarelibrary.sources.capabilities +The laser-source capability mixins (``OnOffCapability``, ``ShutterCapability``, +...) live in :mod:`hardwarelibrary.capabilities`; see the Core page. Cobolt ------ diff --git a/hardwarelibrary/daq/daqdevice.py b/hardwarelibrary/capabilities.py similarity index 52% rename from hardwarelibrary/daq/daqdevice.py rename to hardwarelibrary/capabilities.py index 7f1a7d2..f197da5 100644 --- a/hardwarelibrary/daq/daqdevice.py +++ b/hardwarelibrary/capabilities.py @@ -1,13 +1,294 @@ +"""Capability mixins shared by every device family. + +A capability is a feature an instrument may have (turn on/off, open a shutter, +read a voltage, ...). A driver declares the capabilities it supports by mixing +them alongside a PhysicalDevice subclass; the mixin's public methods delegate to +the do* hooks (or, for some families, are themselves the abstract hook) the +driver implements. Mixins carry the *Capability suffix; only instantiable +hardware drivers are named *Device. + +PhysicalDevice.capabilities() / hasCapability() introspect these by walking the +MRO for Capability subclasses, so every capability across every family must +subclass the single Capability base defined here. +""" + from abc import ABC, abstractmethod from enum import Enum -class DAQNotification(Enum): - willAcquire = "willAcquire" - didAcquire = "didAcquire" +class Capability(ABC): + # Capability mixins are combined with a PhysicalDevice subclass, which sits + # ahead of them in the MRO. PhysicalDevice is a cooperative base (it calls + # super().__init__() after consuming the device-identity arguments), so a + # mixin that holds per-instance state may define __init__ as long as it + # takes no required arguments and forwards with super().__init__(). + pass + + +# --------------------------------------------------------------------------- +# Laser source capabilities +# --------------------------------------------------------------------------- + +class OnOffCapability(Capability): + def isLaserOn(self) -> bool: + return self.doGetOnOffState() + + def turnOn(self): + self.doTurnOn() + + def turnOff(self): + self.doTurnOff() + + # Advisory availability flag for callers/UIs; a driver overrides it when an + # external condition (e.g. Cobolt autostart) forbids manual turn-on. The + # driver's doTurnOn still enforces and raises if called while not allowed. + def canTurnOn(self) -> bool: + return True + + @abstractmethod + def doTurnOn(self): + ... + + @abstractmethod + def doTurnOff(self): + ... + + @abstractmethod + def doGetOnOffState(self) -> bool: + ... + + +class ShutterCapability(Capability): + # Distinct from OnOffCapability: the shutter is a mechanical block in front of + # the output, so it can be opened or closed while the laser stays on. + def isShutterOpen(self) -> bool: + return self.doGetShutterState() + + def openShutter(self): + self.doOpenShutter() + + def closeShutter(self): + self.doCloseShutter() + + @abstractmethod + def doOpenShutter(self): + ... + + @abstractmethod + def doCloseShutter(self): + ... + + @abstractmethod + def doGetShutterState(self) -> bool: + ... + + +class PowerCapability(Capability): + unit = "W" + isReadable = True + isWritable = True + + def setPower(self, power: float): + return self.doSetPower(power) + + def power(self) -> float: + return self.doGetPower() + + @abstractmethod + def doSetPower(self, power: float): + ... + + @abstractmethod + def doGetPower(self) -> float: + ... + + +class InterlockCapability(Capability): + isReadable = True + isWritable = False + + def interlock(self) -> bool: + return self.doGetInterlockState() + + @abstractmethod + def doGetInterlockState(self) -> bool: + ... + + +class AutostartCapability(Capability): + def autostartIsOn(self) -> bool: + return self.doGetAutostart() + + def turnAutostartOn(self): + self.doTurnAutostartOn() + + def turnAutostartOff(self): + self.doTurnAutostartOff() + + @abstractmethod + def doGetAutostart(self) -> bool: + ... + + @abstractmethod + def doTurnAutostartOn(self): + ... + + @abstractmethod + def doTurnAutostartOff(self): + ... + + +class WavelengthCapability(Capability): + unit = "nm" + isReadable = True + isWritable = True + + def setWavelength(self, wavelength: float): + return self.doSetWavelength(wavelength) + + def wavelength(self) -> float: + return self.doGetWavelength() + + def wavelengthRange(self) -> tuple: + return self.doGetWavelengthRange() + + @abstractmethod + def doSetWavelength(self, wavelength: float): + ... + + @abstractmethod + def doGetWavelength(self) -> float: + ... + + @abstractmethod + def doGetWavelengthRange(self) -> tuple: + ... -class AnalogInputDevice(ABC): +class DispersionCapability(Capability): + unit = "fs^2" # group delay dispersion (GDD) + isReadable = True + isWritable = True + + def setDispersion(self, dispersion: float): + return self.doSetDispersion(dispersion) + + def dispersion(self) -> float: + return self.doGetDispersion() + + def dispersionRange(self) -> tuple: + return self.doGetDispersionRange() + + @abstractmethod + def doSetDispersion(self, dispersion: float): + ... + + @abstractmethod + def doGetDispersion(self) -> float: + ... + + @abstractmethod + def doGetDispersionRange(self) -> tuple: + ... + + +# --------------------------------------------------------------------------- +# Power meter capabilities +# --------------------------------------------------------------------------- + +class WavelengthCalibrationCapability(Capability): + unit = "nm" + isReadable = True + isWritable = True + + def __init__(self): + super().__init__() + self.calibrationWavelength = None + + def getCalibrationWavelength(self): + self.doGetCalibrationWavelength() + return self.calibrationWavelength + + def setCalibrationWavelength(self, wavelength): + self.doSetCalibrationWavelength(wavelength) + self.doGetCalibrationWavelength() + + @abstractmethod + def doGetCalibrationWavelength(self): + ... + + @abstractmethod + def doSetCalibrationWavelength(self, wavelength): + ... + + +class AutoScaleCapability(Capability): + # The meter picks its measurement range automatically when auto-scaling is + # on; turning it off pins the range to whatever scale is active. A meter may + # also expose ScaleCapability to choose that range by hand. + def autoScaleIsOn(self) -> bool: + return self.doGetAutoScale() + + def turnAutoScaleOn(self): + self.doTurnAutoScaleOn() + + def turnAutoScaleOff(self): + self.doTurnAutoScaleOff() + + @abstractmethod + def doGetAutoScale(self) -> bool: + ... + + @abstractmethod + def doTurnAutoScaleOn(self): + ... + + @abstractmethod + def doTurnAutoScaleOff(self): + ... + + +class ScaleCapability(Capability): + # The full-scale measurement range (e.g. 200e-3 W). Independent of + # AutoScaleCapability: setting a scale by hand generally requires auto-scaling + # off. + unit = "W" + isReadable = True + isWritable = True + + def __init__(self): + super().__init__() + self.scale = None + + def getScale(self): + self.doGetScale() + return self.scale + + def setScale(self, scale): + self.doSetScale(scale) + self.doGetScale() + + def availableScales(self) -> list: + return self.doGetAvailableScales() + + @abstractmethod + def doGetScale(self): + ... + + @abstractmethod + def doSetScale(self, scale): + ... + + @abstractmethod + def doGetAvailableScales(self) -> list: + ... + + +# --------------------------------------------------------------------------- +# DAQ capabilities +# --------------------------------------------------------------------------- + +class AnalogInputCapability(Capability): """Analog input capability (ADC). Combine with PhysicalDevice in a driver.""" @abstractmethod @@ -15,7 +296,7 @@ def getAnalogVoltage(self, channel): ... -class AnalogOutputDevice(ABC): +class AnalogOutputCapability(Capability): """Analog output capability (DAC). Combine with PhysicalDevice in a driver.""" @abstractmethod @@ -23,7 +304,7 @@ def setAnalogVoltage(self, value, channel): ... -class AnalogIODevice(AnalogInputDevice, AnalogOutputDevice): +class AnalogIOCapability(AnalogInputCapability, AnalogOutputCapability): """Both analog input and output. The configure and direction hooks are optional and default to no-ops. @@ -39,7 +320,7 @@ def setAnalogDirection(self, channel): pass -class AnalogInputStreamDevice(AnalogInputDevice): +class AnalogInputStreamCapability(AnalogInputCapability): """Hardware-timed analog input (waveform acquisition). Combine with PhysicalDevice in a driver. The driver implements the four @@ -65,6 +346,10 @@ class AnalogInputStreamDevice(AnalogInputDevice): device.stopStream() """ + class Notification(Enum): + willAcquire = "willAcquire" + didAcquire = "didAcquire" + @abstractmethod def configureStream(self, channels, sampleRate): """Set up a hardware-timed acquisition of channels at sampleRate (Hz).""" @@ -119,7 +404,7 @@ class InputSource(Enum): Current100M = "Current100M" -class PhaseLockedDetectionDevice(ABC): +class PhaseLockedDetectionCapability(Capability): """Phase-locked (lock-in) detection capability. Combine with PhysicalDevice. Reads the demodulated outputs (X, Y, R, theta) and reference frequency, and @@ -232,7 +517,7 @@ class SampleClock(Enum): External = "External" -class TriggerableDevice(ABC): +class TriggerCapability(Capability): """Capability for a device whose acquisition can be armed to a trigger. setTriggerSource selects an internal (immediate) start versus waiting for an @@ -261,7 +546,7 @@ def supportedTriggerSources(self): return None -class DigitalInputDevice(ABC): +class DigitalInputCapability(Capability): """Digital input capability. Combine with PhysicalDevice in a driver.""" @abstractmethod @@ -269,7 +554,7 @@ def getDigitalValue(self, channel): ... -class DigitalOutputDevice(ABC): +class DigitalOutputCapability(Capability): """Digital output capability. Combine with PhysicalDevice in a driver.""" @abstractmethod @@ -277,7 +562,7 @@ def setDigitalValue(self, value, channel): ... -class DigitalIODevice(DigitalInputDevice, DigitalOutputDevice): +class DigitalIOCapability(DigitalInputCapability, DigitalOutputCapability): """Both digital input and output. The configure and direction hooks are optional and default to no-ops. diff --git a/hardwarelibrary/daq/__init__.py b/hardwarelibrary/daq/__init__.py index a2cefe9..8fbaa2a 100644 --- a/hardwarelibrary/daq/__init__.py +++ b/hardwarelibrary/daq/__init__.py @@ -1,10 +1,10 @@ #__all__ = ["sutterdevice"] -from .daqdevice import ( - AnalogInputDevice, AnalogOutputDevice, AnalogIODevice, AnalogInputStreamDevice, - DigitalInputDevice, DigitalOutputDevice, DigitalIODevice, - PhaseLockedDetectionDevice, InputSource, - TriggerableDevice, TriggerSource, SampleClock, +from hardwarelibrary.capabilities import ( + AnalogInputCapability, AnalogOutputCapability, AnalogIOCapability, AnalogInputStreamCapability, + DigitalInputCapability, DigitalOutputCapability, DigitalIOCapability, + PhaseLockedDetectionCapability, InputSource, + TriggerCapability, TriggerSource, SampleClock, ) from .labjackdevice import LabjackDevice, DebugLabjackDevice from .sr830device import ( diff --git a/hardwarelibrary/daq/labjackdevice.py b/hardwarelibrary/daq/labjackdevice.py index bd59549..c13d015 100644 --- a/hardwarelibrary/daq/labjackdevice.py +++ b/hardwarelibrary/daq/labjackdevice.py @@ -1,9 +1,9 @@ from hardwarelibrary.physicaldevice import PhysicalDevice, DeviceState, PhysicalDeviceNotification -from hardwarelibrary.daq import AnalogIODevice, DigitalIODevice, AnalogInputStreamDevice +from hardwarelibrary.capabilities import AnalogIOCapability, DigitalIOCapability, AnalogInputStreamCapability import inspect -class LabjackDevice(PhysicalDevice, AnalogIODevice, DigitalIODevice, AnalogInputStreamDevice): +class LabjackDevice(PhysicalDevice, AnalogIOCapability, DigitalIOCapability, AnalogInputStreamCapability): """LabJack U3 (LV and HV). Use self.dev for features beyond the wrapped methods. The DAC outputs are slow: they are PWM-based (a ~732 Hz PWM signal smoothed by a @@ -103,7 +103,7 @@ def toggleLED(self): def configureStream(self, channels, sampleRate=None, scanRate=None): # scanRate is a deprecated synonym for sampleRate, kept temporarily for - # callers written against the old AnalogInputStreamDevice contract. + # callers written against the pre-1.4.0 configureStream signature. if sampleRate is None: sampleRate = scanRate self._streamChannels = list(channels) diff --git a/hardwarelibrary/daq/sr830device.py b/hardwarelibrary/daq/sr830device.py index 93b5f93..b0bc9cb 100644 --- a/hardwarelibrary/daq/sr830device.py +++ b/hardwarelibrary/daq/sr830device.py @@ -9,9 +9,9 @@ from hardwarelibrary.communication.communicationport import ( CommunicationPort, CommunicationReadTimeout, ) -from hardwarelibrary.daq.daqdevice import ( - AnalogInputStreamDevice, AnalogOutputDevice, PhaseLockedDetectionDevice, - TriggerableDevice, InputSource, TriggerSource, SampleClock, +from hardwarelibrary.capabilities import ( + AnalogInputStreamCapability, AnalogOutputCapability, PhaseLockedDetectionCapability, + TriggerCapability, InputSource, TriggerSource, SampleClock, ) FLOAT_PATTERN = r"([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)" @@ -93,21 +93,21 @@ class StreamChannel(Enum): } -class SR830Device(PhysicalDevice, AnalogInputStreamDevice, AnalogOutputDevice, - PhaseLockedDetectionDevice, TriggerableDevice): +class SR830Device(PhysicalDevice, AnalogInputStreamCapability, AnalogOutputCapability, + PhaseLockedDetectionCapability, TriggerCapability): """Stanford Research Systems SR830 DSP lock-in amplifier over a Prologix GPIB-USB controller. The SR830 is a GPIB instrument; the Prologix adaptor bridges it to a host serial port (a generic FTDI cable, VID 0x0403 / PID 0x6001), so this driver talks to a PrologixGPIBPort with the ordinary readString/writeString - primitives. It exposes several capabilities: AnalogInputStreamDevice for the + primitives. It exposes several capabilities: AnalogInputStreamCapability for the four rear-panel Aux A/D inputs (OAUX?, 1-4) plus hardware-timed buffered acquisition of the demodulated outputs (the internal data buffer), - AnalogOutputDevice for the four rear-panel Aux D/A outputs (AUXV, 1-4), - PhaseLockedDetectionDevice for the demodulated outputs (X, Y, R, theta via + AnalogOutputCapability for the four rear-panel Aux D/A outputs (AUXV, 1-4), + PhaseLockedDetectionCapability for the demodulated outputs (X, Y, R, theta via OUTP?/SNAP?), the signal input source (ISRC), and the sensitivity and - time-constant settings, and TriggerableDevice for the rear-panel TRIG IN. The + time-constant settings, and TriggerCapability for the rear-panel TRIG IN. The Aux inputs and outputs are general-purpose and independent of the lock-in signal path. """ @@ -296,7 +296,7 @@ def getDemodulatedValues(self): return {"X": x, "Y": y, "R": r, "theta": theta, "referenceFrequency": self.getReferenceFrequency()} - # AnalogInputStreamDevice: hardware-timed acquisition into the SR830 data + # AnalogInputStreamCapability: hardware-timed acquisition into the SR830 data # buffer. channels are StreamChannel members (at most one CH1 and one CH2 # quantity). acquireWaveform (from the base) loops readStream for you. @@ -370,7 +370,7 @@ def _sampleRateIndexFor(self, rate): return min(range(len(self.sampleRates)), key=lambda index: abs(self.sampleRates[index] - rate)) - # TriggerableDevice: the rear-panel TRIG IN. setTriggerSource(External) arms + # TriggerCapability: the rear-panel TRIG IN. setTriggerSource(External) arms # the scan to start on a trigger edge (TSTR); trigger() issues a software edge. def setTriggerSource(self, source: TriggerSource): diff --git a/hardwarelibrary/manuals/Coherent-HOPS-1-Overview-and-Runbook.md b/hardwarelibrary/manuals/Coherent-HOPS-1-Overview-and-Runbook.md index 523d19f..9ed952b 100644 --- a/hardwarelibrary/manuals/Coherent-HOPS-1-Overview-and-Runbook.md +++ b/hardwarelibrary/manuals/Coherent-HOPS-1-Overview-and-Runbook.md @@ -245,8 +245,8 @@ cleared interlock + `REM=1` + `KSWCMD=1` + `PCMD>0`; the mechanical shutter ## The driver `hardwarelibrary/sources/verdig.py` — `VerdiGDevice` (+ `DebugVerdiGDevice`), one -laser-source driver combining `OnOffControl` / `ShutterControl` / `PowerControl` -/ `InterlockControl`. It drives the bus through an interchangeable +laser-source driver combining `OnOffCapability` / `ShutterCapability` / `PowerCapability` +/ `InterlockCapability`. It drives the bus through an interchangeable `HOPSInterface`, chosen with `VerdiGDevice(interface="auto")` (native first, then DLL), or forced with `"native"` / `"dll"` / an instance: diff --git a/hardwarelibrary/manuals/Spectra-Physics-Millennia-eV-Serial-Commands.md b/hardwarelibrary/manuals/Spectra-Physics-Millennia-eV-Serial-Commands.md index 6545ad5..8ac9f82 100644 --- a/hardwarelibrary/manuals/Spectra-Physics-Millennia-eV-Serial-Commands.md +++ b/hardwarelibrary/manuals/Spectra-Physics-Millennia-eV-Serial-Commands.md @@ -76,14 +76,14 @@ All commands and responses are ASCII. | Capability | Action / query | Command | Reply | |---|---|---|---| -| OnOffControl | turn diodes on | `ON` | none | -| OnOffControl | turn diodes off | `OFF` | none | -| OnOffControl | diode emission state | `?D` | `1` (on) / `0` (off) | -| ShutterControl | open shutter | `SHT:1` | none | -| ShutterControl | close shutter | `SHT:0` | none | -| ShutterControl | shutter state | `?SHT` | `1` (open) / `0` (closed) | -| PowerControl | set output power (W) | `P:` | none | -| PowerControl | read output power (W) | `?P` | e.g. `4.90` (or `4.90 W` on some firmware) | +| OnOffCapability | turn diodes on | `ON` | none | +| OnOffCapability | turn diodes off | `OFF` | none | +| OnOffCapability | diode emission state | `?D` | `1` (on) / `0` (off) | +| ShutterCapability | open shutter | `SHT:1` | none | +| ShutterCapability | close shutter | `SHT:0` | none | +| ShutterCapability | shutter state | `?SHT` | `1` (open) / `0` (closed) | +| PowerCapability | set output power (W) | `P:` | none | +| PowerCapability | read output power (W) | `?P` | e.g. `4.90` (or `4.90 W` on some firmware) | | (init only) | identification | `*IDN?` | comma-separated: manufacturer, model, serial, firmware | `ON`/`OFF` gate the pump diodes; the shutter is a separate electromechanical diff --git a/hardwarelibrary/physicaldevice.py b/hardwarelibrary/physicaldevice.py index 86c1899..02650d5 100644 --- a/hardwarelibrary/physicaldevice.py +++ b/hardwarelibrary/physicaldevice.py @@ -3,6 +3,7 @@ from threading import Thread, RLock from hardwarelibrary import utils +from hardwarelibrary.capabilities import Capability from hardwarelibrary.notificationcenter import NotificationCenter import typing import time @@ -72,12 +73,24 @@ def __init__(self, serialNumber:str, idProduct:int, idVendor:int): self.refreshInterval = 1.0 # Cooperative base: forward to the rest of the MRO so capability mixins - # mixed in alongside a device (e.g. WavelengthCalibratable) get their + # mixed in alongside a device (e.g. WavelengthCalibrationCapability) get their # own __init__ run. The device-identity arguments are consumed here, so # nothing is forwarded; a mixin __init__ must therefore take no required # arguments and call super().__init__() itself. super().__init__() + def capabilities(self) -> list: + # The capability mixins, not the Capability marker nor the device class + # itself (a driver is a Capability subclass too, but it is a + # PhysicalDevice). + return [klass for klass in type(self).__mro__ + if issubclass(klass, Capability) + and klass is not Capability + and not issubclass(klass, PhysicalDevice)] + + def hasCapability(self, capabilityClass) -> bool: + return isinstance(self, capabilityClass) + @classmethod def vidpids(cls): if cls.usesGenericSerialConverter: diff --git a/hardwarelibrary/powermeters/__init__.py b/hardwarelibrary/powermeters/__init__.py index 265a596..df5c976 100644 --- a/hardwarelibrary/powermeters/__init__.py +++ b/hardwarelibrary/powermeters/__init__.py @@ -1,5 +1,5 @@ -from .capabilities import Capability, WavelengthCalibratable, AutoScalable, ScaleAdjustable +from hardwarelibrary.capabilities import Capability, WavelengthCalibrationCapability, AutoScaleCapability, ScaleCapability from .powermeterdevice import PowerMeterDevice from .integradevice import IntegraDevice from .fieldmasterdevice import FieldMasterDevice, DebugFieldMasterDevice diff --git a/hardwarelibrary/powermeters/capabilities.py b/hardwarelibrary/powermeters/capabilities.py deleted file mode 100644 index 3a9d339..0000000 --- a/hardwarelibrary/powermeters/capabilities.py +++ /dev/null @@ -1,97 +0,0 @@ -from abc import ABC, abstractmethod - - -class Capability(ABC): - # Capability mixins are combined with a PhysicalDevice subclass, which sits - # ahead of them in the MRO. PhysicalDevice is a cooperative base (it calls - # super().__init__() after consuming the device-identity arguments), so a - # mixin that holds per-instance state may define __init__ as long as it - # takes no required arguments and forwards with super().__init__(). - pass - - -class WavelengthCalibratable(Capability): - unit = "nm" - isReadable = True - isWritable = True - - def __init__(self): - super().__init__() - self.calibrationWavelength = None - - def getCalibrationWavelength(self): - self.doGetCalibrationWavelength() - return self.calibrationWavelength - - def setCalibrationWavelength(self, wavelength): - self.doSetCalibrationWavelength(wavelength) - self.doGetCalibrationWavelength() - - @abstractmethod - def doGetCalibrationWavelength(self): - ... - - @abstractmethod - def doSetCalibrationWavelength(self, wavelength): - ... - - -class AutoScalable(Capability): - # The meter picks its measurement range automatically when auto-scaling is - # on; turning it off pins the range to whatever scale is active. A meter may - # also expose ScaleAdjustable to choose that range by hand. - def autoScaleIsOn(self) -> bool: - return self.doGetAutoScale() - - def turnAutoScaleOn(self): - self.doTurnAutoScaleOn() - - def turnAutoScaleOff(self): - self.doTurnAutoScaleOff() - - @abstractmethod - def doGetAutoScale(self) -> bool: - ... - - @abstractmethod - def doTurnAutoScaleOn(self): - ... - - @abstractmethod - def doTurnAutoScaleOff(self): - ... - - -class ScaleAdjustable(Capability): - # The full-scale measurement range (e.g. 200e-3 W). Independent of - # AutoScalable: setting a scale by hand generally requires auto-scaling off. - unit = "W" - isReadable = True - isWritable = True - - def __init__(self): - super().__init__() - self.scale = None - - def getScale(self): - self.doGetScale() - return self.scale - - def setScale(self, scale): - self.doSetScale(scale) - self.doGetScale() - - def availableScales(self) -> list: - return self.doGetAvailableScales() - - @abstractmethod - def doGetScale(self): - ... - - @abstractmethod - def doSetScale(self, scale): - ... - - @abstractmethod - def doGetAvailableScales(self) -> list: - ... diff --git a/hardwarelibrary/powermeters/fieldmasterdevice.py b/hardwarelibrary/powermeters/fieldmasterdevice.py index ced8204..20b0348 100644 --- a/hardwarelibrary/powermeters/fieldmasterdevice.py +++ b/hardwarelibrary/powermeters/fieldmasterdevice.py @@ -7,12 +7,12 @@ ) from hardwarelibrary.physicaldevice import PhysicalDevice from hardwarelibrary.powermeters.powermeterdevice import PowerMeterDevice -from hardwarelibrary.powermeters.capabilities import WavelengthCalibratable +from hardwarelibrary.capabilities import WavelengthCalibrationCapability FLOAT_PATTERN = r"([-+]?\d*\.?\d+(?:[eE][-+]?\d+)?)" -class FieldMasterDevice(PowerMeterDevice, WavelengthCalibratable): +class FieldMasterDevice(PowerMeterDevice, WavelengthCalibrationCapability): """Coherent FieldMaster GS laser power/energy meter over RS-232. The meter has no USB identity of its own: it connects through a generic diff --git a/hardwarelibrary/powermeters/integradevice.py b/hardwarelibrary/powermeters/integradevice.py index 34c2ed8..ed871cd 100644 --- a/hardwarelibrary/powermeters/integradevice.py +++ b/hardwarelibrary/powermeters/integradevice.py @@ -2,10 +2,10 @@ from enum import Enum from hardwarelibrary.communication import USBPort, TextCommand, MultilineTextCommand from hardwarelibrary.powermeters.powermeterdevice import PowerMeterDevice -from hardwarelibrary.powermeters.capabilities import WavelengthCalibratable +from hardwarelibrary.capabilities import WavelengthCalibrationCapability from hardwarelibrary.notificationcenter import NotificationCenter, Notification -class IntegraDevice(PowerMeterDevice, WavelengthCalibratable): +class IntegraDevice(PowerMeterDevice, WavelengthCalibrationCapability): classIdProduct = 0x0300 classIdVendor = 0x1ad5 commands = { diff --git a/hardwarelibrary/powermeters/powermeterdevice.py b/hardwarelibrary/powermeters/powermeterdevice.py index f24a56b..5e5ad27 100644 --- a/hardwarelibrary/powermeters/powermeterdevice.py +++ b/hardwarelibrary/powermeters/powermeterdevice.py @@ -5,7 +5,6 @@ from hardwarelibrary.communication import USBPort, TextCommand from hardwarelibrary.physicaldevice import * from hardwarelibrary.notificationcenter import NotificationCenter, Notification -from hardwarelibrary.powermeters.capabilities import Capability class PowerMeterNotification(Enum): didMeasure = "didMeasure" @@ -16,16 +15,7 @@ def __init__(self, serialNumber:str, idProduct:int, idVendor:int): super().__init__(serialNumber, idProduct, idVendor) self.absolutePower = 0 - def capabilities(self) -> list: - # The capability mixins, not the marker nor the device class itself - # (a driver is a Capability subclass too, but it is a PhysicalDevice). - return [klass for klass in type(self).__mro__ - if issubclass(klass, Capability) - and klass is not Capability - and not issubclass(klass, PhysicalDevice)] - - def hasCapability(self, capabilityClass) -> bool: - return isinstance(self, capabilityClass) + # capabilities() / hasCapability() are inherited from PhysicalDevice. # Measuring absolute power is the one capability every power meter has, so # its hook stays on the base; optional capabilities live in mixins. diff --git a/hardwarelibrary/sources/__init__.py b/hardwarelibrary/sources/__init__.py index e0adb0c..016a0c5 100644 --- a/hardwarelibrary/sources/__init__.py +++ b/hardwarelibrary/sources/__init__.py @@ -1,7 +1,7 @@ -from .capabilities import ( +from hardwarelibrary.capabilities import ( Capability, - OnOffControl, ShutterControl, PowerControl, InterlockControl, - AutostartControl, WavelengthControl, DispersionControl, + OnOffCapability, ShutterCapability, PowerCapability, InterlockCapability, + AutostartCapability, WavelengthCapability, DispersionCapability, ) from .lasersourcedevice import LaserSourceDevice from .cobolt import CoboltDevice, CoboltCantTurnOnWithAutostartOn diff --git a/hardwarelibrary/sources/capabilities.py b/hardwarelibrary/sources/capabilities.py deleted file mode 100644 index 1d4ac2e..0000000 --- a/hardwarelibrary/sources/capabilities.py +++ /dev/null @@ -1,168 +0,0 @@ -from abc import ABC, abstractmethod - - -class Capability(ABC): - pass - - -class OnOffControl(Capability): - def isLaserOn(self) -> bool: - return self.doGetOnOffState() - - def turnOn(self): - self.doTurnOn() - - def turnOff(self): - self.doTurnOff() - - # Advisory availability flag for callers/UIs; a driver overrides it when an - # external condition (e.g. Cobolt autostart) forbids manual turn-on. The - # driver's doTurnOn still enforces and raises if called while not allowed. - def canTurnOn(self) -> bool: - return True - - @abstractmethod - def doTurnOn(self): - ... - - @abstractmethod - def doTurnOff(self): - ... - - @abstractmethod - def doGetOnOffState(self) -> bool: - ... - - -class ShutterControl(Capability): - # Distinct from OnOffControl: the shutter is a mechanical block in front of - # the output, so it can be opened or closed while the laser stays on. - def isShutterOpen(self) -> bool: - return self.doGetShutterState() - - def openShutter(self): - self.doOpenShutter() - - def closeShutter(self): - self.doCloseShutter() - - @abstractmethod - def doOpenShutter(self): - ... - - @abstractmethod - def doCloseShutter(self): - ... - - @abstractmethod - def doGetShutterState(self) -> bool: - ... - - -class PowerControl(Capability): - unit = "W" - isReadable = True - isWritable = True - - def setPower(self, power: float): - return self.doSetPower(power) - - def power(self) -> float: - return self.doGetPower() - - @abstractmethod - def doSetPower(self, power: float): - ... - - @abstractmethod - def doGetPower(self) -> float: - ... - - -class InterlockControl(Capability): - isReadable = True - isWritable = False - - def interlock(self) -> bool: - return self.doGetInterlockState() - - @abstractmethod - def doGetInterlockState(self) -> bool: - ... - - -class AutostartControl(Capability): - def autostartIsOn(self) -> bool: - return self.doGetAutostart() - - def turnAutostartOn(self): - self.doTurnAutostartOn() - - def turnAutostartOff(self): - self.doTurnAutostartOff() - - @abstractmethod - def doGetAutostart(self) -> bool: - ... - - @abstractmethod - def doTurnAutostartOn(self): - ... - - @abstractmethod - def doTurnAutostartOff(self): - ... - - -class WavelengthControl(Capability): - unit = "nm" - isReadable = True - isWritable = True - - def setWavelength(self, wavelength: float): - return self.doSetWavelength(wavelength) - - def wavelength(self) -> float: - return self.doGetWavelength() - - def wavelengthRange(self) -> tuple: - return self.doGetWavelengthRange() - - @abstractmethod - def doSetWavelength(self, wavelength: float): - ... - - @abstractmethod - def doGetWavelength(self) -> float: - ... - - @abstractmethod - def doGetWavelengthRange(self) -> tuple: - ... - - -class DispersionControl(Capability): - unit = "fs^2" # group delay dispersion (GDD) - isReadable = True - isWritable = True - - def setDispersion(self, dispersion: float): - return self.doSetDispersion(dispersion) - - def dispersion(self) -> float: - return self.doGetDispersion() - - def dispersionRange(self) -> tuple: - return self.doGetDispersionRange() - - @abstractmethod - def doSetDispersion(self, dispersion: float): - ... - - @abstractmethod - def doGetDispersion(self) -> float: - ... - - @abstractmethod - def doGetDispersionRange(self) -> tuple: - ... diff --git a/hardwarelibrary/sources/cobolt.py b/hardwarelibrary/sources/cobolt.py index da9f486..8e6612e 100644 --- a/hardwarelibrary/sources/cobolt.py +++ b/hardwarelibrary/sources/cobolt.py @@ -3,7 +3,7 @@ from hardwarelibrary.communication.commands import TextCommand from hardwarelibrary.communication.debugport import TableDrivenDebugPort from .lasersourcedevice import LaserSourceDevice -from .capabilities import OnOffControl, PowerControl, InterlockControl, AutostartControl +from hardwarelibrary.capabilities import OnOffCapability, PowerCapability, InterlockCapability, AutostartCapability import re import time @@ -14,8 +14,8 @@ class CoboltCantTurnOnWithAutostartOn(Exception): pass -class CoboltDevice(LaserSourceDevice, OnOffControl, PowerControl, - InterlockControl, AutostartControl): +class CoboltDevice(LaserSourceDevice, OnOffCapability, PowerCapability, + InterlockCapability, AutostartCapability): commands = { "GET_POWER": TextCommand(name="GET_POWER", requestEncoder="pa?\r", diff --git a/hardwarelibrary/sources/lasersourcedevice.py b/hardwarelibrary/sources/lasersourcedevice.py index 7d524cc..6ebc73f 100644 --- a/hardwarelibrary/sources/lasersourcedevice.py +++ b/hardwarelibrary/sources/lasersourcedevice.py @@ -1,15 +1,8 @@ from hardwarelibrary.physicaldevice import PhysicalDevice -from hardwarelibrary.sources.capabilities import Capability class LaserSourceDevice(PhysicalDevice): - def capabilities(self) -> list: - # The capability mixins, not the marker nor the device class itself - # (a driver is a Capability subclass too, but it is a PhysicalDevice). - return [klass for klass in type(self).__mro__ - if issubclass(klass, Capability) - and klass is not Capability - and not issubclass(klass, PhysicalDevice)] - - def hasCapability(self, capabilityClass) -> bool: - return isinstance(self, capabilityClass) + # A thin marker base for laser sources. The behavior comes from the + # *Capability mixins a driver combines with it; capability introspection + # (capabilities() / hasCapability()) is inherited from PhysicalDevice. + pass diff --git a/hardwarelibrary/sources/matisse.py b/hardwarelibrary/sources/matisse.py index 646aa37..3fbfc66 100644 --- a/hardwarelibrary/sources/matisse.py +++ b/hardwarelibrary/sources/matisse.py @@ -4,14 +4,14 @@ from ..communication.labviewtcpport import LabviewTCPPort from ..communication.communicationport import CommunicationReadError from ..communication.debugport import DebugPort -from .capabilities import WavelengthControl +from hardwarelibrary.capabilities import WavelengthCapability class MatisseCommanderError(Exception): pass -class MatisseDevice(PhysicalDevice, WavelengthControl): +class MatisseDevice(PhysicalDevice, WavelengthCapability): """Sirah Matisse tunable laser, controlled over the Matisse Commander server. Wavelength tuning is a hierarchy of intracavity elements, coarse to fine: @@ -19,7 +19,7 @@ class MatisseDevice(PhysicalDevice, WavelengthControl): narrows it, and the piezo etalon, slow piezo and fast piezo provide successively finer, faster control; a scan engine sweeps the fine elements. Each element is exposed below with its own get/set/motion/lock methods. The - high-level WavelengthControl interface (setWavelength/wavelength) drives the + high-level WavelengthCapability interface (setWavelength/wavelength) drives the BiFi, which is the coarse wavelength selector; use the etalon and piezo methods for fine tuning. @@ -403,7 +403,7 @@ def scanFallingSpeed(self) -> float: def setScanFallingSpeed(self, speed): self.sendSetting("SCAN:FSPD {0:.6f}".format(speed)) - # WavelengthControl hooks (the BiFi is the coarse wavelength selector) + # WavelengthCapability hooks (the BiFi is the coarse wavelength selector) def doGetWavelength(self) -> float: return self.bifiWavelength() diff --git a/hardwarelibrary/sources/millennia.py b/hardwarelibrary/sources/millennia.py index 55cd351..da208de 100644 --- a/hardwarelibrary/sources/millennia.py +++ b/hardwarelibrary/sources/millennia.py @@ -3,10 +3,10 @@ from ..physicaldevice import PhysicalDevice from ..communication.serialport import SerialPort from .lasersourcedevice import LaserSourceDevice -from .capabilities import OnOffControl, ShutterControl, PowerControl +from hardwarelibrary.capabilities import OnOffCapability, ShutterCapability, PowerCapability -class MillenniaEv25Device(LaserSourceDevice, OnOffControl, ShutterControl, PowerControl): +class MillenniaEv25Device(LaserSourceDevice, OnOffCapability, ShutterCapability, PowerCapability): """Spectra-Physics Millennia eV CW DPSS pump laser (532 nm). Transport is the eV's back-panel USB port, which exposes a virtual COM @@ -160,7 +160,7 @@ def writeActionAndConfirm(self, command, query, expectedReply, description): "Millennia did not confirm {0} (last {1}={2!r})".format( description, query, reply)) - # OnOffControl hooks (the pump diodes) + # OnOffCapability hooks (the pump diodes) def doTurnOn(self): self.writeActionAndConfirm("ON", "?D", "1", "diodes on") @@ -171,7 +171,7 @@ def doTurnOff(self): def doGetOnOffState(self) -> bool: return self.queryString("?D") == "1" - # ShutterControl hooks (the output-blocking shutter) + # ShutterCapability hooks (the output-blocking shutter) def doOpenShutter(self): self.writeCommand("SHT:1") @@ -182,7 +182,7 @@ def doCloseShutter(self): def doGetShutterState(self) -> bool: return self.queryString("?SHT") == "1" - # PowerControl hooks (output power in watts) + # PowerCapability hooks (output power in watts) def doSetPower(self, power: float): # The eV silently ignores out-of-range P: values, which would mask the diff --git a/hardwarelibrary/sources/verdig.py b/hardwarelibrary/sources/verdig.py index d17807e..8cba818 100644 --- a/hardwarelibrary/sources/verdig.py +++ b/hardwarelibrary/sources/verdig.py @@ -16,7 +16,7 @@ "native"/"dll" to force one, or an interface instance directly. The debug device uses an in-memory DebugHOPSInterface and needs neither hardware nor pyftdi/DLL. -The lab unit reports as a Genesis CX-Vis (head G532). InterlockControl is part of +The lab unit reports as a Genesis CX-Vis (head G532). InterlockCapability is part of the contract, but the native interface cannot decode faults yet, so on the native transport interlock()/faults() raise HOPSInterface.NotSupported (to be fixed once the ?FF decode is reverse-engineered). @@ -26,7 +26,7 @@ from ..physicaldevice import PhysicalDevice from .lasersourcedevice import LaserSourceDevice -from .capabilities import OnOffControl, ShutterControl, PowerControl, InterlockControl +from hardwarelibrary.capabilities import OnOffCapability, ShutterCapability, PowerCapability, InterlockCapability class HOPSInterface(ABC): @@ -95,8 +95,8 @@ def diagnostics(self) -> dict: return {} -class VerdiGDevice(LaserSourceDevice, OnOffControl, ShutterControl, PowerControl, - InterlockControl): +class VerdiGDevice(LaserSourceDevice, OnOffCapability, ShutterCapability, PowerCapability, + InterlockCapability): """Coherent Verdi-G laser on a HOPS supply (the lab head reports as a Genesis CX-Vis, G532), driven through an interchangeable HOPSInterface -- native pyftdi I2C or CohrHOPS.dll. See the module docstring.""" @@ -169,7 +169,7 @@ def doShutdownDevice(self): self.interface.close() self.interface = None - # OnOffControl (emission enable) + # OnOffCapability (emission enable) def doTurnOn(self): self.interface.setEmission(True) @@ -179,7 +179,7 @@ def doTurnOff(self): def doGetOnOffState(self) -> bool: return self.interface.emissionOn() - # ShutterControl + # ShutterCapability def doOpenShutter(self): self.interface.setShutter(True) @@ -189,7 +189,7 @@ def doCloseShutter(self): def doGetShutterState(self) -> bool: return self.interface.shutterOpen() - # PowerControl + # PowerCapability def doSetPower(self, power: float): upper = self.maxPower if self.maxPower is not None else float("inf") if not (0.0 <= power <= upper): @@ -199,7 +199,7 @@ def doSetPower(self, power: float): def doGetPower(self) -> float: return self.interface.getPower() - # InterlockControl (native raises HOPSInterface.NotSupported, by design) + # InterlockCapability (native raises HOPSInterface.NotSupported, by design) def doGetInterlockState(self) -> bool: return self.interface.interlockOk() diff --git a/hardwarelibrary/tests/testFieldMasterDevice.py b/hardwarelibrary/tests/testFieldMasterDevice.py index 4bda9ac..33cdfac 100644 --- a/hardwarelibrary/tests/testFieldMasterDevice.py +++ b/hardwarelibrary/tests/testFieldMasterDevice.py @@ -3,7 +3,7 @@ from hardwarelibrary.powermeters import FieldMasterDevice, DebugFieldMasterDevice from hardwarelibrary.powermeters import ( - WavelengthCalibratable, AutoScalable, ScaleAdjustable, + WavelengthCalibrationCapability, AutoScaleCapability, ScaleCapability, ) from hardwarelibrary.powermeters.powermeterdevice import PowerMeterNotification from hardwarelibrary.notificationcenter import NotificationCenter @@ -51,15 +51,15 @@ class TestFieldMasterCapabilities(unittest.TestCase): def setUp(self): self.device = DebugFieldMasterDevice() - def testDeclaresWavelengthCalibratable(self): - self.assertTrue(self.device.hasCapability(WavelengthCalibratable)) + def testDeclaresWavelengthCalibrationCapability(self): + self.assertTrue(self.device.hasCapability(WavelengthCalibrationCapability)) def testDoesNotDeclareScaleCapabilities(self): - self.assertFalse(self.device.hasCapability(AutoScalable)) - self.assertFalse(self.device.hasCapability(ScaleAdjustable)) + self.assertFalse(self.device.hasCapability(AutoScaleCapability)) + self.assertFalse(self.device.hasCapability(ScaleCapability)) def testCapabilitiesListsOnlyDeclaredMixins(self): - self.assertEqual(self.device.capabilities(), [WavelengthCalibratable]) + self.assertEqual(self.device.capabilities(), [WavelengthCalibrationCapability]) def testCapabilitiesExcludeDeviceClass(self): # The driver is itself a Capability subclass, but capabilities() must diff --git a/hardwarelibrary/tests/testLaserCapabilities.py b/hardwarelibrary/tests/testLaserCapabilities.py index 3e152c2..48def17 100644 --- a/hardwarelibrary/tests/testLaserCapabilities.py +++ b/hardwarelibrary/tests/testLaserCapabilities.py @@ -3,9 +3,9 @@ from hardwarelibrary.physicaldevice import DeviceState from hardwarelibrary.sources.cobolt import CoboltDevice -from hardwarelibrary.sources.capabilities import ( - Capability, OnOffControl, PowerControl, InterlockControl, - AutostartControl, WavelengthControl, DispersionControl) +from hardwarelibrary.capabilities import ( + Capability, OnOffCapability, PowerCapability, InterlockCapability, + AutostartCapability, WavelengthCapability, DispersionCapability) class TestLaserCapabilities(unittest.TestCase): @@ -18,21 +18,21 @@ def tearDown(self): def testCoboltAdvertisesItsFourCapabilities(self): self.assertEqual(set(self.device.capabilities()), - {OnOffControl, PowerControl, InterlockControl, AutostartControl}) + {OnOffCapability, PowerCapability, InterlockCapability, AutostartCapability}) def testCapabilitiesExcludeTheMarkerAndUnsupportedOnes(self): self.assertNotIn(Capability, self.device.capabilities()) - self.assertNotIn(WavelengthControl, self.device.capabilities()) - self.assertNotIn(DispersionControl, self.device.capabilities()) + self.assertNotIn(WavelengthCapability, self.device.capabilities()) + self.assertNotIn(DispersionCapability, self.device.capabilities()) def testTypedDiscovery(self): - self.assertIsInstance(self.device, PowerControl) - self.assertNotIsInstance(self.device, WavelengthControl) + self.assertIsInstance(self.device, PowerCapability) + self.assertNotIsInstance(self.device, WavelengthCapability) def testHasCapability(self): - self.assertTrue(self.device.hasCapability(PowerControl)) - self.assertTrue(self.device.hasCapability(AutostartControl)) - self.assertFalse(self.device.hasCapability(WavelengthControl)) + self.assertTrue(self.device.hasCapability(PowerCapability)) + self.assertTrue(self.device.hasCapability(AutostartCapability)) + self.assertFalse(self.device.hasCapability(WavelengthCapability)) def testDynamicAvailabilityOfTurnOn(self): self.device.initializeDevice() # the debug Cobolt boots with autostart on diff --git a/hardwarelibrary/tests/testMatisse.py b/hardwarelibrary/tests/testMatisse.py index 5db5f6b..982d96e 100644 --- a/hardwarelibrary/tests/testMatisse.py +++ b/hardwarelibrary/tests/testMatisse.py @@ -6,7 +6,7 @@ import time from hardwarelibrary.sources.matisse import MatisseDevice, DebugMatisseDevice, MatisseCommanderError -from hardwarelibrary.sources.capabilities import WavelengthControl +from hardwarelibrary.capabilities import WavelengthCapability from hardwarelibrary.physicaldevice import PhysicalDevice, DeviceState MATISSE_HOST = "172.16.8.57" @@ -73,7 +73,7 @@ def tearDown(self): def testIsPhysicalDeviceWithWavelengthCapability(self): self.assertIsInstance(self.matisse, PhysicalDevice) - self.assertIsInstance(self.matisse, WavelengthControl) + self.assertIsInstance(self.matisse, WavelengthCapability) def testInitializeReadsIdn(self): self.assertEqual(self.matisse.state, DeviceState.Ready) diff --git a/hardwarelibrary/tests/testMillennia.py b/hardwarelibrary/tests/testMillennia.py index 77fd1bc..65b7661 100644 --- a/hardwarelibrary/tests/testMillennia.py +++ b/hardwarelibrary/tests/testMillennia.py @@ -8,8 +8,8 @@ MillenniaEv25Device, DebugMillenniaEv25Device, MillenniaDevice, DebugMillenniaDevice, ) -from hardwarelibrary.sources.capabilities import ( - OnOffControl, ShutterControl, PowerControl, InterlockControl, WavelengthControl) +from hardwarelibrary.capabilities import ( + OnOffCapability, ShutterCapability, PowerCapability, InterlockCapability, WavelengthCapability) from hardwarelibrary.sources.lasersourcedevice import LaserSourceDevice # Set to the serial port of an attached Millennia eV to exercise TestMillenniaEv25Device; @@ -34,16 +34,16 @@ def tearDown(self): def testIsLaserSourceWithOnOffShutterAndPowerCapabilities(self): self.assertIsInstance(self.laser, LaserSourceDevice) - self.assertIsInstance(self.laser, OnOffControl) - self.assertIsInstance(self.laser, ShutterControl) - self.assertIsInstance(self.laser, PowerControl) + self.assertIsInstance(self.laser, OnOffCapability) + self.assertIsInstance(self.laser, ShutterCapability) + self.assertIsInstance(self.laser, PowerCapability) def testAdvertisesOnOffShutterAndPower(self): self.assertEqual(set(self.laser.capabilities()), - {OnOffControl, ShutterControl, PowerControl}) + {OnOffCapability, ShutterCapability, PowerCapability}) def testDoesNotAdvertiseUnsupportedCapabilities(self): - for unsupported in (InterlockControl, WavelengthControl): + for unsupported in (InterlockCapability, WavelengthCapability): self.assertFalse(self.laser.hasCapability(unsupported)) def testStartsOffWithShutterClosed(self): diff --git a/hardwarelibrary/tests/testSR830.py b/hardwarelibrary/tests/testSR830.py index de1e501..7ef7bb7 100644 --- a/hardwarelibrary/tests/testSR830.py +++ b/hardwarelibrary/tests/testSR830.py @@ -6,8 +6,8 @@ from hardwarelibrary.communication import PrologixGPIBPort from hardwarelibrary.communication.communicationport import CommunicationReadTimeout from hardwarelibrary.daq import ( - AnalogInputDevice, AnalogOutputDevice, AnalogInputStreamDevice, - PhaseLockedDetectionDevice, TriggerableDevice, + AnalogInputCapability, AnalogOutputCapability, AnalogInputStreamCapability, + PhaseLockedDetectionCapability, TriggerCapability, InputSource, AuxInput, AuxOutput, StreamChannel, TriggerSource, SampleClock, SR830Device, DebugSR830Device, DebugPrologixGPIBPort, ) @@ -221,14 +221,14 @@ def testSnapReadsAuxAndFrequencyCodes(self): class TestPhaseLockedDetectionContract(unittest.TestCase): def testDeclaresAllCapabilities(self): device = DebugSR830Device() - self.assertIsInstance(device, AnalogInputDevice) - self.assertIsInstance(device, AnalogOutputDevice) - self.assertIsInstance(device, AnalogInputStreamDevice) - self.assertIsInstance(device, PhaseLockedDetectionDevice) - self.assertIsInstance(device, TriggerableDevice) + self.assertIsInstance(device, AnalogInputCapability) + self.assertIsInstance(device, AnalogOutputCapability) + self.assertIsInstance(device, AnalogInputStreamCapability) + self.assertIsInstance(device, PhaseLockedDetectionCapability) + self.assertIsInstance(device, TriggerCapability) -class _MinimalLockIn(PhaseLockedDetectionDevice, TriggerableDevice): +class _MinimalLockIn(PhaseLockedDetectionCapability, TriggerCapability): """A bare capability implementation that supplies only the abstract hooks, so the base-class optional hooks and the base getDemodulatedValues are exercised (SR830Device overrides all of these).""" diff --git a/hardwarelibrary/tests/testVerdiG.py b/hardwarelibrary/tests/testVerdiG.py index c570b87..3f9cdcb 100644 --- a/hardwarelibrary/tests/testVerdiG.py +++ b/hardwarelibrary/tests/testVerdiG.py @@ -6,8 +6,8 @@ VerdiGDevice, DebugVerdiGDevice, HOPSInterface, DebugHOPSInterface) from hardwarelibrary.sources.hopsnative import ( HOPSNativeInterface, HOPSNativeI2C, MockHOPSBus) -from hardwarelibrary.sources.capabilities import ( - OnOffControl, ShutterControl, PowerControl, InterlockControl, WavelengthControl) +from hardwarelibrary.capabilities import ( + OnOffCapability, ShutterCapability, PowerCapability, InterlockCapability, WavelengthCapability) from hardwarelibrary.sources.lasersourcedevice import LaserSourceDevice # Point at a real Verdi-G / HOPS laser on this host to exercise the hardware class. @@ -26,8 +26,8 @@ def tearDown(self): def testAdvertisesFullCapabilitySet(self): self.assertIsInstance(self.laser, LaserSourceDevice) self.assertEqual(set(self.laser.capabilities()), - {OnOffControl, ShutterControl, PowerControl, InterlockControl}) - self.assertFalse(self.laser.hasCapability(WavelengthControl)) + {OnOffCapability, ShutterCapability, PowerCapability, InterlockCapability}) + self.assertFalse(self.laser.hasCapability(WavelengthCapability)) def testReadsIdentity(self): self.assertEqual(self.laser.laserModel, "Genesis CX-Vis")