Notable changes to PyHardwareLibrary, loosely following Keep a Changelog. Read this before upgrading: API changes can land even when the minor version is unchanged.
- PowerStrip device family (
hardwarelibrary/powerstrips/) plus its first driverPwrUSBDevice(andDebugPwrUSBDevice) for the PwrUSB / PowerUSB controllable power strip (USB HID04d8:003f, enumerates as "Simple HID Device Demo"). The family follows the interface-segregated capability-mixin pattern used bysources/anddaq/:PowerStripDeviceis a thin marker base overPhysicalDevice, and behaviour comes fromOutletSwitchingCapability(turnOutletOn/turnOutletOff/setOutletState/isOutletOn/outletCount, outlets 1-based),DefaultOutletCapability(per-outlet power-on default state), andCurrentMeteringCapability(current()in A,accumulatedCharge()in Ah,resetAccumulatedCharge()), all in the sharedhardwarelibrary/capabilities.py. The strip speaks a single-byte HID report protocol driven through aHIDPort; the protocol was reverse-engineered publicly and cross-checked against aarossig/pwrusbctl (Apache-2.0) and pwrusb.com, but the implementation is our own. Outlet state is cached on write because live readback is unreliable on this firmware. HIDPort(hardwarelibrary/communication/hidport.py): aCommunicationPortover a USB HID device, backed by hidapi (IOKit on macOS), alongsideSerialPortandUSBPort. Needed because an HID device the OS claims has no/devnode (soSerialPortcannot reach it) and cannot be claimed by libusb (soUSBPortcannot either) -- notably on macOS, whereIOHIDFamilyowns the interface. hidapi is an optional dependency; install it with the newpwrusbextra (pip install -e .[pwrusb]), required to drive the strip.VerdiGDevice(andDebugVerdiGDevice): a laser-source driver for the Coherent "HOPS" (High Output Power Supply) laser -- Genesis heads / Verdi G-C, e.g. the lab Genesis CX-Vis (headG532). 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 (seemanuals/Coherent-HOPS-*).VerdiGDevicecombinesOnOffCapability,ShutterCapability,PowerCapability, andInterlockCapability, and drives the bus through an interchangeableHOPSInterface:HOPSNativeInterface(sources/hopsnative.py): pure-Python pyftdi I2C, no DLL (macOS/Linux). Hardware-confirmed end to end on the lab unit (identity, on/off, shutter, remote, power setpoint, temperature). Itsinterlock()/faults()raiseHOPSInterface.NotSupporteduntil the?FFdecode is reverse-engineered.HOPSDLLInterface(sources/hopsdll.py): Coherent'sCohrHOPS.dll(ASCII command set; Windows/Linux). Read +REM/PCMDwrite paths hardware- confirmed;KSWCMD/SHCMDper the DLL spec, not yet exercised. Selection:VerdiGDevice(interface="auto")tries native first, then the DLL; pass"native"/"dll"/an interface instance to force one. Protocol and I2C decode inmanuals/Coherent-HOPS-2-USB-and-DLL-Protocol.mdandmanuals/Coherent-HOPS-3-I2C-Wire-Protocol.md.
- Breaking: capability mixins across all families now use a uniform
*Capabilitysuffix, reserving*Devicefor 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.
- DAQ:
- Breaking: all capability mixins are consolidated into a single module,
hardwarelibrary/capabilities.py, and share oneCapabilitybase class (the per-familysources/capabilities.py,powermeters/capabilities.py, anddaq/daqdevice.pyare removed; the DAQ enumsInputSource,TriggerSource,SampleClockmove there too, and the acquisition notification enum is now nested asAnalogInputStreamCapability.Notification). Imports must point athardwarelibrary.capabilities(the family package__init__s still re-export their own mixins, sofrom hardwarelibrary.daq import AnalogIOCapabilityand the like keep working).capabilities()/hasCapability()are hoisted ontoPhysicalDevice, so every device -- including DAQ drivers -- now supports capability introspection; the duplicated methods onLaserSourceDeviceandPowerMeterDeviceare gone (LaserSourceDeviceis now a pure marker).
SR830Device(andDebugSR830Device): Stanford Research SR830 DSP lock-in amplifier over a Prologix GPIB-USB controller. It combines several capabilities:AnalogInputStreamDevice(the four rear-panel Aux A/D inputs viaOAUX?, plus hardware-timed buffered acquisition of the demodulated outputs from the internal data buffer),AnalogOutputDevice(the four rear-panel Aux D/A outputs viaAUXV),PhaseLockedDetectionDevice(X/Y/R/theta, reference frequency, signal input source, sensitivity, and time constant), andTriggerableDevice(the rear-panel TRIG IN). Enums:AuxInput,AuxOutput,StreamChannel,InputSource.doInitializeDeviceself-discovers the Prologix among the connected FTDI adaptors by confirming*IDN?, and pins the adaptor's serial.PrologixGPIBPort(hardwarelibrary/communication/): aSerialPortsubclass that speaks the Prologix GPIB-USB controller++protocol. GPIB instruments talk to it with the ordinaryreadString/writeStringprimitives; the++read eoihandshake is encapsulated in itsreadString.- New DAQ capability contracts in
daq/daqdevice.py:PhaseLockedDetectionDevice(lock-in / phase-locked detection),TriggerableDevicewith theTriggerSourceenum, and theSampleClockenum for stream sample clocking.
- Breaking:
AnalogInputStreamDevice: the sample-rate parameter ofconfigureStream/acquireWaveformis renamedscanRate->sampleRate. Callers passing it positionally are unaffected; callers passingscanRate=by keyword must switch tosampleRate=.LabjackDevice.configureStreamkeepsscanRateas a temporary deprecated synonym, so LabJack callers are unaffected for now. LabjackDevicenow importsu3(LabJackPython) lazily at point of use, soimport hardwarelibrary.daq(and the new SR830 driver) works on hosts that do not have LabJackPython installed.
- Heavy third-party modules are now imported lazily, at their point of use,
instead of at module load.
import hardwarelibraryno longer pulls inmatplotlibornumpy(import time drops from ~336 ms to ~59 ms); they load only when a plot is drawn or a spectrum is acquired. Affected: the spectrometers package (SpectraViewerdeferred intodisplay()/displayAny(),numpyinto the methods that use it;base.pyusesfrom __future__ import annotationsfor its-> np.arrayhint),OscilloscopeDevice.displayWaveforms, and the cameras module (cv2). Public APIs are unchanged. Two behavioral notes: importinghardwarelibrary.camerasno longer prints a warning when OpenCV is absent — a missingcv2now raisesModuleNotFoundErrorwhen a camera operation is invoked; and the unused matplotlib import block inoceaninsight.pywas removed.
- Dead
from pyftdi.ftdi import Ftdiimports inSutterDeviceandEchoDevice(both were unused and flagged# FIXME: should not be here). FTDI access still goes throughSerialPort, which owns thepyftdidependency.
MillenniaEv25Device(and itsMillenniaDevicealias) now discovers its port by USB identity when constructed without aportPath. The class carries the STM32 Virtual COM Port identityclassIdVendor = 0x0483/classIdProduct = 0x5740, anddoInitializeDevicematches it over pyserial's ports, raisingUnableToInitializenaming the identity when none is found. An explicitportPathstill takes precedence, and aserialNumbernarrows discovery when several STM32 USB-CDC ports are present. Note:0x0483:0x5740is STMicro's generic STM32 VCP identity shared by unrelated STM32 boards, so pinportPathon a host that has more than one.
- Power-meter capability mixins (
powermeters/capabilities.py), mirroring the laser-sourceCapabilitystructure:WavelengthCalibratable(getCalibrationWavelength/setCalibrationWavelength),AutoScalable(autoScaleIsOn/turnAutoScaleOn/turnAutoScaleOff), andScaleAdjustable(getScale/setScale/availableScales), each delegating todo*hooks the driver implements.PowerMeterDevicegainscapabilities()andhasCapability(capabilityClass)for introspection.
- The wavelength-calibration hooks (
doGetCalibrationWavelength,doSetCalibrationWavelength) and their public methods move offPowerMeterDeviceinto the newWavelengthCalibratablemixin. The base now requires onlydoGetAbsolutePower.IntegraDeviceandFieldMasterDevicedeclareWavelengthCalibratable, so their public API is unchanged; a new power meter that calibrates by wavelength must now mix inWavelengthCalibratableto expose those methods. PhysicalDevice.__init__is now a cooperative base: it callssuper().__init__()after consuming the device-identity arguments, so a capability mixin combined with a device (e.g.IntegraDevice(PowerMeterDevice, WavelengthCalibratable)) has its__init__run instead of being skipped by the MRO. A mixin__init__must therefore take no required arguments and callsuper().__init__()itself. No existing device changes behavior.
FieldMasterDeviceandDebugFieldMasterDevice(powermeters/): a driver for the Coherent FieldMaster GS laser power/energy meter over RS-232 via an FTDI adaptor (9600 8N1, LF terminator,pw?/en?/wv?/vcommands). The meter has no USB identity of its own, soclassIdVendor/classIdProductare the generic FTDI values (0x0403/0x6001); disambiguate multiple FTDI adaptors with the adaptorserialNumberor an explicitportPath. The message terminator is a front-panel Menu setting (LF/CR/CR-LF);initializeDeviceprobes with the configuredterminator(default LF) and falls through the other combinations until one replies, so a mismatched Menu self-heals. Note: the meter only answers RS-232 while on its Home or Trend screen, andinitializeDeviceraises with that hint when none of the terminators reply.SerialPort.genericSerialConverterPorts()andisGenericSerialConverter(), plus thegenericSerialConverterVendorstable: discover connected ports that come from a generic USB/RS-232 converter chip (FTDI, Prolific, Silicon Labs CP210x, WCH CH34x), so an instrument with no USB identity of its own can be located and disambiguated by the adaptor's serial number.PhysicalDevice.usesGenericSerialConverterflag (defaultFalse). A device behind a generic converter matches any converter vendor (itsvidpids()expands to the whole table, product id wildcarded), andDeviceManager.candidateClassesForAutoDiscovery()excludes such classes from automatic probing, since their VID/PID identifies only the cable. FieldMaster, oscilloscope, Echo and IntelliDrive are flagged and must be constructed explicitly; Thorlabs (custom-EEPROM FTDI PID) stays a specific identity.
PhysicalDevice.isCompatibleWithtreats aNoneproduct id in avidpids()pair as a wildcard that matches any product from that vendor. Concrete(vendor, product)pairs are unaffected.
OISpectrometer.getSpectrumno longer hangs. The wait for the "spectrum ready" flag is now bounded (maxRequests/maxWait) and raisesSpectrumRequestTimeoutErrorinstead of re-requesting a spectrum forever on a transient USB glitch; transientusb.core.USBError(incl.USBTimeoutError) during polling is absorbed and retried.integrationTimeremains the first optional argument, so all existing callers are unaffected.
CommunicationPort: the optional matching-method argumentalternatePatternis renamed toerrorPatternonwriteStringExpectMatchingString,writeStringReadMatchingGroups,writeStringReadFirstMatchingGroup, andreadMatchingGroups, and it now actually works on all of them (it was inverted in one method and silently ignored in the others). A reply matchingerrorPatternraisesCommunicationReadErrorcarrying that pattern's capture groups; a reply matching neither pattern still raisesCommunicationReadNoMatch. Callers that passed the argument positionally are unaffected; callers passingalternatePattern=by keyword must switch toerrorPattern=.CommunicationReadError.__init__now takes(reply, groups)instead of a single argument. Catching the exception is unaffected; only code that constructs or raises it directly must update.MatisseDevice.queryStringis renamed toquery. The high-level API (wavelength,setWavelength, the BiFi/thin-etalon/piezo/scan get/set/lock methods, andsendSetting) is unchanged.
CommunicationReadErrorexception (replacesCommunicationReadAlternateMatch).CommunicationPort.writeStringReadMatchandCommunicationPort.matchReply, the shared write-then-read-then-match and pure-match helpers the matching methods now delegate to.DebugMatissePort, a debug port that speaks the Matisse reply grammar soDebugMatisseDeviceruns the same port code path as the real device.
MatisseDevice.parseReply. Its job is now expressed as the port'sreplyPattern/errorPattern, with errors mapped toMatisseCommanderError.
CommunicationReadAlternateMatchis kept as an alias forCommunicationReadErrorand will be removed in a future release.