Skip to content

Add capability mixins to the power-meter family#105

Merged
dccote merged 2 commits into
masterfrom
powermeter-capabilities
Jul 6, 2026
Merged

Add capability mixins to the power-meter family#105
dccote merged 2 commits into
masterfrom
powermeter-capabilities

Conversation

@dccote

@dccote dccote commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Problem

PowerMeterDevice baked wavelength-calibration hooks directly into the base class even though not every meter calibrates by wavelength, and there was no way to introspect what a given meter can do. The laser-source family (hardwarelibrary/sources/) already models optional features as interface-segregated Capability mixins, but the power meters did not mirror that structure.

Solution

Introduce hardwarelibrary/powermeters/capabilities.py, a mirror of sources/capabilities.py, with a Capability ABC and three orthogonal mixins:

Mixin Public API do* hooks
WavelengthCalibratable getCalibrationWavelength / setCalibrationWavelength doGetCalibrationWavelength, doSetCalibrationWavelength
AutoScalable autoScaleIsOn / turnAutoScaleOn / turnAutoScaleOff doGetAutoScale, doTurnAutoScaleOn, doTurnAutoScaleOff
ScaleAdjustable getScale / setScale / availableScales doGetScale, doSetScale, doGetAvailableScales
  • PowerMeterDevice now keeps only the always-present doGetAbsolutePower hook and gains capabilities() / hasCapability() introspection, copied from LaserSourceDevice.
  • Wavelength hooks moved out of the base into WavelengthCalibratable.
  • IntegraDevice and FieldMasterDevice declare WavelengthCalibratable; their existing hooks satisfy the contract unchanged. AutoScalable / ScaleAdjustable are left for future auto-ranging meters — no driver falsely advertises them.
  • AutoScalable and ScaleAdjustable are kept fully orthogonal; a driver that needs "auto off before manual scale" handles it in its own doSetScale.

MRO / cooperative init (2nd commit)

Adding the mixins surfaced a latent init bug shared by the whole library. PhysicalDevice.__init__ did not call super().__init__(), and in the MRO of a device that mixes in a capability it sits ahead of the mixins:

IntegraDevice -> PowerMeterDevice -> PhysicalDevice -> WavelengthCalibratable -> Capability -> ABC -> object

So the init chain terminated at PhysicalDevice and a mixin's __init__ was never reached — mixins could only fake instance defaults with class attributes, and any future mixin holding real per-instance state would be silently skipped.

Fix:

  • PhysicalDevice.__init__ now ends with super().__init__(), making it a cooperative base. It still consumes the device-identity arguments and forwards nothing, so the contract (documented on Capability) is: a mixin __init__ takes no required arguments and calls super().__init__() itself.
  • WavelengthCalibratable / ScaleAdjustable now set their per-instance state (calibrationWavelength, scale) in a cooperative __init__ instead of shared class attributes.
  • DebugFieldMasterDevice routes through super().__init__() instead of calling PhysicalDevice.__init__ directly, so it no longer skips PowerMeterDevice (absolutePower is now initialized to 0).

Verified safe for every multiply-inheriting device (Matisse, Millennia, Labjack, Cobolt, power meters) — none of their mixins define an __init__ to collide with.

Tests

Adds TestFieldMasterCapabilities covering capabilities() / hasCapability(), including that the driver class does not leak into the reported list. Existing wavelength tests now exercise the mixin. Full suite: 398 passed, 235 skipped, 0 failures.

🤖 Generated with Claude Code

dccote and others added 2 commits July 6, 2026 15:11
Problem: PowerMeterDevice baked wavelength-calibration hooks directly into
the base class even though not every meter calibrates by wavelength, and
there was no way to introspect what a given meter can do. The laser-source
family already solves this with interface-segregated Capability mixins, but
the power meters did not mirror that structure.

Solution: Introduce hardwarelibrary/powermeters/capabilities.py with a
Capability ABC and three orthogonal mixins matching the sources/ pattern:
WavelengthCalibratable (wavelength hooks moved out of the base),
AutoScalable (toggle automatic ranging), and ScaleAdjustable (read/set the
manual full-scale range and enumerate available scales). PowerMeterDevice
now keeps only the always-present doGetAbsolutePower hook and gains
capabilities()/hasCapability() introspection copied from LaserSourceDevice.
IntegraDevice and FieldMasterDevice declare WavelengthCalibratable; their
existing hooks satisfy the contract unchanged. Adds TestFieldMasterCapabilities
covering the introspection API.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ialize

Problem: PhysicalDevice.__init__ did not call super().__init__(), and in the
MRO of a device that mixes in capabilities (e.g.
IntegraDevice(PowerMeterDevice, WavelengthCalibratable)) PhysicalDevice sits
ahead of the mixins. The init chain therefore terminated at PhysicalDevice and
never reached a mixin's __init__. This forced mixins to fake instance defaults
with class attributes, and any future mixin that actually initialized
per-instance state would be silently skipped. DebugFieldMasterDevice made it
worse by calling PhysicalDevice.__init__ directly, also skipping
PowerMeterDevice (so absolutePower was never set).

Solution: End PhysicalDevice.__init__ with super().__init__(). It still
consumes the device-identity arguments and forwards nothing, so a mixin
__init__ must take no required arguments and call super().__init__() itself;
this contract is documented on the Capability base. WavelengthCalibratable and
ScaleAdjustable now set their per-instance state (calibrationWavelength, scale)
in a cooperative __init__ instead of shared class attributes.
DebugFieldMasterDevice goes through super().__init__() so it runs the full
chain. Verified across families (Matisse, Labjack, power meters); full suite
passes (398 passed, 235 skipped).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dccote
dccote merged commit cdc76ce into master Jul 6, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant