Skip to content

Commit 163e202

Browse files
dccoteclaude
andauthored
Uniformize capability mixins: single *Capability convention, single module, single base (#114)
* Rename DAQ capability mixins from *Device to *Capability Problem: The DAQ family named its interface-segregated capability mixins with a *Device suffix (AnalogInputDevice, TriggerableDevice, ...), while the same objects are features a device *has*, not devices themselves. This collided conceptually with instantiable hardware drivers, which are also named *Device (LabjackDevice, SR830Device), making it unclear from a name whether a class is a mixin or a real device. Solution: Rename all nine DAQ mixins to the *Capability suffix, reserving *Device for instantiable hardware drivers. Public methods and behavior are unchanged; only the mixin class names change. AnalogInputDevice -> AnalogInputCapability AnalogOutputDevice -> AnalogOutputCapability AnalogIODevice -> AnalogIOCapability AnalogInputStreamDevice -> AnalogInputStreamCapability DigitalInputDevice -> DigitalInputCapability DigitalOutputDevice -> DigitalOutputCapability DigitalIODevice -> DigitalIOCapability PhaseLockedDetectionDevice -> PhaseLockedDetectionCapability TriggerableDevice -> TriggerCapability Updates the LabjackDevice and SR830Device base-class lists and imports, the daq package exports, and the SR830 tests. Documents the rename in CHANGELOG (Unreleased, marked Breaking) and the *Capability/*Device rule in CLAUDE.md and the skill doc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Rename source and power-meter capability mixins to *Capability Problem: After the DAQ rename, two families still used their own conventions for capability mixins: laser sources used a *Control suffix (OnOffControl, PowerControl, ...) and power meters used adjective forms (WavelengthCalibratable, AutoScalable, ScaleAdjustable). This left three naming styles for the same concept -- a feature a device may have -- and made it inconsistent which classes are mixins versus instantiable devices. Solution: Rename these mixins to the same *Capability suffix adopted for DAQ, completing the uniform rule: *Capability for mixins, *Device for instantiable hardware. Public methods and behavior are unchanged. Laser sources (sources/capabilities.py): OnOffControl -> OnOffCapability ShutterControl -> ShutterCapability PowerControl -> PowerCapability InterlockControl -> InterlockCapability AutostartControl -> AutostartCapability WavelengthControl -> WavelengthCapability DispersionControl -> DispersionCapability Power meters (powermeters/capabilities.py): WavelengthCalibratable -> WavelengthCalibrationCapability AutoScalable -> AutoScaleCapability ScaleAdjustable -> ScaleCapability Updates the CoboltDevice, MillenniaEv25Device, MatisseDevice, VerdiGDevice, FieldMasterDevice, and IntegraDevice base-class lists and imports, the sources/powermeters package exports, the source/powermeter tests, and the physicaldevice.py doc comment. Documents the rename in CHANGELOG (Unreleased, folded into the Breaking capability-rename entry) and CLAUDE.md / the skill doc, and aligns the HOPS and Millennia manuals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Consolidate capability mixins into one module with a single Capability base Problem: Capability mixins were spread across three modules with two independent Capability base classes: sources/capabilities.py and powermeters/capabilities.py each defined their own Capability(ABC), while the DAQ mixins in daq/daqdevice.py inherited bare ABC with no shared base at all. Capability introspection (capabilities() / hasCapability()) was duplicated verbatim on LaserSourceDevice and PowerMeterDevice, each bound to its family's local Capability, and DAQ devices had no introspection. Solution: Move every capability mixin into a single top-level module, hardwarelibrary/capabilities.py, all subclassing one Capability base. The DAQ enums (InputSource, TriggerSource, SampleClock, DAQNotification) move with their mixins. Hoist capabilities() / hasCapability() onto PhysicalDevice, bound to the single Capability, so every device -- DAQ included -- gets capability introspection, and the duplicated methods are removed (LaserSourceDevice becomes a pure marker; PowerMeterDevice keeps only its power hooks). The per-family capabilities.py / daqdevice.py files are deleted. All internal imports point at hardwarelibrary.capabilities; the family package __init__s still re-export their own mixins, so existing consumers such as `from hardwarelibrary.daq import AnalogIOCapability` keep working. Docs updated: capabilities are documented once on the Core page; the per-family automodule sections and their file-path references are removed. Full suite: 470 passed, 247 skipped, no regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Nest the acquisition notification inside AnalogInputStreamCapability Problem: DAQNotification (willAcquire/didAcquire) sat as a free-floating module-level enum in capabilities.py, unattached to the capability that posts it. Solution: Nest it as AnalogInputStreamCapability.Notification, inside the acquisition capability it belongs to, so the notification is discoverable from the capability that raises it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ce03dde commit 163e202

31 files changed

Lines changed: 472 additions & 418 deletions

.claude/skills/pyhardwarelibrary/SKILL.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,11 @@ the capabilities it declares — check the table.
131131

132132
| Capability | Methods |
133133
|---|---|
134-
| `OnOffControl` | `turnOn()`, `turnOff()`, `isLaserOn()`, `canTurnOn()` |
135-
| `ShutterControl` | `openShutter()`, `closeShutter()`, `isShutterOpen()` |
136-
| `PowerControl` | `setPower(watts)`, `power()` |
137-
| `InterlockControl` | `interlock()` |
138-
| `WavelengthControl` | `setWavelength(nm)`, `wavelength()`, `wavelengthRange()` |
134+
| `OnOffCapability` | `turnOn()`, `turnOff()`, `isLaserOn()`, `canTurnOn()` |
135+
| `ShutterCapability` | `openShutter()`, `closeShutter()`, `isShutterOpen()` |
136+
| `PowerCapability` | `setPower(watts)`, `power()` |
137+
| `InterlockCapability` | `interlock()` |
138+
| `WavelengthCapability` | `setWavelength(nm)`, `wavelength()`, `wavelengthRange()` |
139139

140140
```python
141141
from hardwarelibrary.sources.millennia import MillenniaEv25Device
@@ -153,7 +153,7 @@ laser.shutdownDevice()
153153

154154
- Cobolt: `CoboltDevice(portPath="COM3")` — OnOff + Power (+ autostart constraints; it
155155
may refuse `turnOn()` when autostart is on, raising `CoboltCantTurnOnWithAutostartOn`).
156-
- Matisse: `MatisseDevice(...)` over TCP — `WavelengthControl` (`setWavelength`/`wavelength`)
156+
- Matisse: `MatisseDevice(...)` over TCP — `WavelengthCapability` (`setWavelength`/`wavelength`)
157157
plus BiFi/etalon/piezo/scan methods.
158158

159159
### Power meters — Gentec-EO Integra
@@ -181,7 +181,7 @@ v = daq.getAnalogVoltage(channel=0) # read
181181
daq.setAnalogVoltage(2.5, channel=1) # write (U3 DACs are slow PWM)
182182
bit = daq.getDigitalValue(channel=4)
183183
daq.setDigitalValue(1, channel=5)
184-
# Hardware-timed acquisition: daq.acquireWaveform(...) (AnalogInputStreamDevice)
184+
# Hardware-timed acquisition: daq.acquireWaveform(...) (AnalogInputStreamCapability)
185185
daq.shutdownDevice()
186186
```
187187

CHANGELOG.md

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ API changes can land even when the minor version is unchanged.
1212
lab Genesis CX-Vis (head `G532`). A HOPS supply is not a serial device: its
1313
FTDI FT2232 (`0x0403:0x6010`) is driven as bit-banged I2C, with power DAC, ADC,
1414
shutter/enable GPIO, and the head identity/calibration EEPROM all on one I2C
15-
bus (see `manuals/Coherent-HOPS-*`). `VerdiGDevice` combines `OnOffControl`,
16-
`ShutterControl`, `PowerControl`, and `InterlockControl`, and drives the bus
15+
bus (see `manuals/Coherent-HOPS-*`). `VerdiGDevice` combines `OnOffCapability`,
16+
`ShutterCapability`, `PowerCapability`, and `InterlockCapability`, and drives the bus
1717
through an interchangeable `HOPSInterface`:
1818
- `HOPSNativeInterface` (`sources/hopsnative.py`): **pure-Python** pyftdi I2C,
1919
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.
2828
decode in `manuals/Coherent-HOPS-2-USB-and-DLL-Protocol.md` and
2929
`manuals/Coherent-HOPS-3-I2C-Wire-Protocol.md`.
3030

31+
### Changed
32+
- **Breaking:** capability mixins across all families now use a uniform
33+
`*Capability` suffix, reserving `*Device` for instantiable hardware drivers.
34+
Public methods and behavior are unchanged; only the mixin class names change.
35+
Drivers subclassing these must update their base-class lists and imports.
36+
- DAQ: `AnalogInputDevice` -> `AnalogInputCapability`, `AnalogOutputDevice` ->
37+
`AnalogOutputCapability`, `AnalogIODevice` -> `AnalogIOCapability`,
38+
`AnalogInputStreamDevice` -> `AnalogInputStreamCapability`,
39+
`DigitalInputDevice` -> `DigitalInputCapability`, `DigitalOutputDevice` ->
40+
`DigitalOutputCapability`, `DigitalIODevice` -> `DigitalIOCapability`,
41+
`PhaseLockedDetectionDevice` -> `PhaseLockedDetectionCapability`,
42+
`TriggerableDevice` -> `TriggerCapability`.
43+
- Laser sources: `OnOffControl` -> `OnOffCapability`, `ShutterControl` ->
44+
`ShutterCapability`, `PowerControl` -> `PowerCapability`, `InterlockControl`
45+
-> `InterlockCapability`, `AutostartControl` -> `AutostartCapability`,
46+
`WavelengthControl` -> `WavelengthCapability`, `DispersionControl` ->
47+
`DispersionCapability`.
48+
- Power meters: `WavelengthCalibratable` -> `WavelengthCalibrationCapability`,
49+
`AutoScalable` -> `AutoScaleCapability`, `ScaleAdjustable` ->
50+
`ScaleCapability`.
51+
- **Breaking:** all capability mixins are consolidated into a single module,
52+
`hardwarelibrary/capabilities.py`, and share one `Capability` base class (the
53+
per-family `sources/capabilities.py`, `powermeters/capabilities.py`, and
54+
`daq/daqdevice.py` are removed; the DAQ enums `InputSource`, `TriggerSource`,
55+
`SampleClock` move there too, and the acquisition notification enum is now
56+
nested as `AnalogInputStreamCapability.Notification`). Imports must point at
57+
`hardwarelibrary.capabilities` (the family package `__init__`s still re-export
58+
their own mixins, so `from hardwarelibrary.daq import AnalogIOCapability` and
59+
the like keep working). `capabilities()` / `hasCapability()` are hoisted onto
60+
`PhysicalDevice`, so every device -- including DAQ drivers -- now supports
61+
capability introspection; the duplicated methods on `LaserSourceDevice` and
62+
`PowerMeterDevice` are gone (`LaserSourceDevice` is now a pure marker).
63+
3164
## [1.4.0] - 2026-07-08
3265

3366
### Added

CLAUDE.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,16 +59,17 @@ Each family has an **abstract base class**. A driver subclasses it and implement
5959

6060
**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]`).
6161

62-
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.
62+
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.
6363

64-
- 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)`.
65-
- 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)`.
64+
- 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)`.
65+
- 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)`.
66+
- Power meters: `WavelengthCalibrationCapability`, `AutoScaleCapability`, `ScaleCapability` (on top of the base `doGetAbsolutePower` every meter implements).
6667

6768
## Adding a new device
6869

6970
Reference implementation: `hardwarelibrary/daq/labjackdevice.py`. The pattern:
7071

71-
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)`.
72+
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)`.
7273
2. Set class attributes `classIdVendor` and `classIdProduct` (USB VID/PID, or the equivalent for serial-only devices).
7374
3. Implement `doInitializeDevice` and `doShutdownDevice`. Keep them minimal — open the port, close the port.
7475
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.

docs/source/api/core.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@ PhysicalDevice
99

1010
.. automodule:: hardwarelibrary.physicaldevice
1111

12+
Capabilities
13+
------------
14+
15+
Interface-segregated capability mixins shared by every device family. Each
16+
subclasses the single ``Capability`` base; ``PhysicalDevice`` introspects them
17+
through ``capabilities()`` / ``hasCapability()``.
18+
19+
.. automodule:: hardwarelibrary.capabilities
20+
1221
DeviceManager
1322
-------------
1423

docs/source/api/daq.rst

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
Data acquisition (DAQ)
22
======================
33

4-
Capability mixins and DAQ drivers.
5-
6-
DAQDevice
7-
---------
8-
9-
.. automodule:: hardwarelibrary.daq.daqdevice
4+
DAQ drivers. The DAQ capability mixins (``AnalogIOCapability``,
5+
``DigitalIOCapability``, ``AnalogInputStreamCapability``, ...) live in
6+
:mod:`hardwarelibrary.capabilities`; see the Core page.
107

118
LabJack
129
-------

docs/source/api/sources.rst

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@ LaserSourceDevice
88

99
.. automodule:: hardwarelibrary.sources.lasersourcedevice
1010

11-
Capabilities
12-
------------
13-
14-
.. automodule:: hardwarelibrary.sources.capabilities
11+
The laser-source capability mixins (``OnOffCapability``, ``ShutterCapability``,
12+
...) live in :mod:`hardwarelibrary.capabilities`; see the Core page.
1513

1614
Cobolt
1715
------

0 commit comments

Comments
 (0)