Notable changes to PyHardwareLibrary, loosely following Keep a Changelog. Read this before upgrading: API changes can land even when the minor version is unchanged.
- A protocol is described, not performed (
communication/protocol.py). AFrameturns named values into the bytes to write and the bytes read back into named values; it owns no port and stores nothing of what happened.TextFramestates astr.formattemplate and the regular expression that reads it back;BinaryFramestates one struct format, whose constants are written on the way out and required on the way in. ACommandpairs two frames, aCommandDictionaryholds a device's commands and reads them from JSON. Nothing is inferred anywhere: a byte order must be explicit, and so must both notations of a text line.- Both directions come out of the same objects. A driver writes the request and
reads the reply; a debug port reads the request and writes the reply. That is
what the old
DataCommandneeded a second encoder and decoder for, and those could -- and did -- drift apart. CommandDictionary.usage()prints every command, its arguments and what it answers, with types read off the description rather than a comment.CommandDictionary.validate()refuses a description that is wrong about itself: a template and an expression that describe different lines, a struct that packs a different number of values than it names, a command whose request another command answers to first. It writes each command out with specimen values made from the declared types and reads it straight back.
- Both directions come out of the same objects. A driver writes the request and
reads the reply; a debug port reads the request and writes the reply. That is
what the old
ProtocolDebugPort(communication/debugport.py), a debug port with no code of its own: hand it aCommandDictionaryand it stands in for the instrument. Whatever a request carries is remembered by name and whatever a reply carries is answered from that memory. A command may add a"sets"clause for a state change its bytes cannot express --HOMEcarries nothing and still moves a stage.PhysicalDevice.protocol(defaultNone) andPhysicalDevice.performTransaction(), which performs one command of that protocol under the port'stransactionLock, reading byreadLengthfor a binary reply and up to the terminator for a line. A driver that setsprotocolneeds no send-and-receive code of its own.
SutterDevicespeaks through a description. Its protocol is data, it sends throughperformTransaction, and it no longer checks acknowledgements by hand: the description saysMOVE,HOMEandWORKanswerb"\r". Its nestedDebugSerialPortis gone;serialNumber="debug"now gets aProtocolDebugPort. Callers building the debug port themselves should useProtocolDebugPort(SutterDevice.protocol).
PhysicalDevice.sendCommand(). It looked a command up inself.commands, sent it throughself.portand handed theCommandobject back so a caller could read the reply off it -- the pattern the description above exists to replace. No driver in the library called it; the two tests that did now send through theCommanditself, which is all it ever did. UseperformTransaction()with aprotocol, or aCommand's ownsend()while a device still carries acommandsdict.
- Notifications on every capability. Each capability owns a
<Capability>Notificationenum, reachable as itsnotificationattribute, and every public method is wrapped in the new@notifies(will=..., did=...)decorator, so a driver gets notifications by implementing hooks and writing no notification code at all. An operation that changes the instrument postswill<Stem>thendid<Stem>; a read (doGet*,doReadStream) posts onlydid<Stem>, to keep a voltage sampled in a loop at one notification instead of two.SpectrometergetsSpectrometerNotification, whosegetSpectrumkeeps awillbecause an acquisition takes an integration time.did*is posted whether the operation succeeded or not, so awill*is always followed by itsdid*and there is no separate failure member to pair up. The exception is still re-raised untouched, since a driver's exception type is part of its contract, so a caller sees it exactly as before while an observer decides what to do from the payload.user_infois a dict of the public method's arguments by name, plus"result"and"error", exactly one of which is non-None.- Capabilities related by inheritance share one enum, so
notificationis the same object on all of them and the members are interchangeable:AnalogInputCapability,AnalogOutputCapability,AnalogIOCapabilityandAnalogInputStreamCapabilityall postAnalogNotification, and the digital trio postsDigitalNotification(17 enums for 22 capabilities). Members are keyed by identity, so without sharing an observer would have to know which variant a device mixed in. - Measured overhead on a read with no observer is ~1.6 us per call (~2.0 us with one observer), against millisecond-scale device I/O.
- Removes the unused nested
AnalogInputStreamCapability.Notification(willAcquire/didAcquire), which was never posted; the equivalent members are nowAnalogInputStreamNotification.willAcquireWaveform/didAcquireWaveform.
- The family bases that already posted notifications now follow the same scheme,
through the same
@notifiesdecorator, and name their enum in anotificationattribute like the capabilities do. Breaking for observers:PowerMeterNotification.didMeasureis nowdidGetAbsolutePower, named after its hook like everywhere else.LinearMotionNotificationandRotationMotionNotificationkeep their groupedwillMove/didMove(moveTo,moveByandhomeare one operation to an observer), but the payload changed:user_infois now a dict carrying the method's arguments by name plus"result"and"error", where it used to be the bare position, displacement or angle. A handler readingnotification.user_infoas a tuple must now readuser_info["position"],user_info["displacement"]oruser_info["angle"].- Every one of them now also reports failures: the
did*is posted even when the driver raised, with the exception underuser_info["error"]. Spectrometergained thenotificationattribute it was missing.CameraDeviceNotificationis left alone: a capture session is a different shape (imageCapturedfires per frame), not a will/did pair around one hook.
- Contract-level argument validation on the capability public methods, through a
new
validate=parameter on@notifiesand a newhardwarelibrary/validation.pyholding the sharedrequire*checks. The validator runs before anything is posted, so a refused call announces nothing and a will/did pair still means the driver was invoked. Breaking: calls that used to be accepted silently now raise.acquireWaveform(sampleCount=0)returned an empty acquisition; nowValueError. An emptychannelslist failed withmin() iterable argument is empty; now it names the parameter.configureStream(sampleRate=-100)was accepted; a rate must be positive, orNoneto say the clock is external.sampleRate=0no longer means "external": passNone, which is what the docstring always said.setDigitalValue("yes", channel)set the line True; a logic level must be a bool (0 and 1 accepted).setAnalogVoltage("2.5", channel)now raisesTypeError.setSensitivity(-1)andsetTimeConstant(0)were silently snapped to a step; both must be positive.setWavelengthandsetDispersionare checked against the range the driver reports, somatisse.setWavelength(50.0)no longer drives the birefringent filter outside the installed optics' 700-1000 nm.- Outlets are checked against
doGetOutletCount()inOutletSwitchingCapabilityandDefaultOutletCapability, soPwrUSBDevice._validateOutletis gone and every future strip inherits the rule. setInputSourceandsetTriggerSourceaccept anything their enum accepts (setInputSource("Differential")) and hand the driver a member.- Instrument-specific limits stay in the drivers, unchanged.
- Every notified operation now requires an initialized device.
@notifiescallsvalidateReady()before anything else, raisingPhysicalDevice.NotInitializedunless the device isReadyand naming the operation, the class and the actual state. Previously such a call either failed deep inside the driver withAttributeError: 'NoneType' object has no attribute ...on a port that was never opened, or -- on a debug device -- answered as though the hardware had done it and posted adid*claiming success. The check runs before thewillis posted, so a refused call announces nothing.validateReadyis defined once, onPhysicalDevice, where the device lifecycle belongs;capabilities.pyonly calls it, so a class mixing in a capability without being aPhysicalDeviceanswers for readiness itself rather than silently skipping the check. Methods that only report what a model supports (supportedInputSources,supportedSensitivities,supportedTimeConstants,supportedTriggerSources,outletCount) are exempt viarequiresReady=False, since a UI populates its menus before connecting. allCapabilities()inhardwarelibrary/capabilities.py: returns every capability mixin the library defines, in declaration order. It answers the library-wide question ("what can be expressed?"), wherePhysicalDevice.capabilities()answers the per-device one ("what does this instrument support?"). Enumerating the module rather than walkingCapability.__subclasses__()keeps the answer independent of which device modules happen to be imported, and excludes the drivers, which areCapabilitysubclasses themselves.capabilityInterface()inhardwarelibrary/capabilities.py: describes one capability asextends/publicAPI/hookslists ofCapabilityMember(name, signature, isAbstract)tuples. Thedoprefix is what separates a hook from the public API, not abstractness: a hook that is optional, or that defaults to a composition of the others, is concrete. Members a parent capability declares are left to that parent.python -m hardwarelibrary --capabilities(-c): prints every capability with the methods it defines and the hooks a driver must implement, so the list never has to be maintained by hand.hardwarelibrary/tests/testCapabilities.py: coversallCapabilities()andcapabilityInterface(), and enforces the invariant that every capability mixin is declared incapabilities.py, by comparing the module listing against a full walk of theCapabilitysubclass graph.
- The public/
do*template method pattern is now uniform across every capability. The DAQ, lock-in and trigger capabilities used to declare their public method itself as the@abstractmethod; they now follow the same rule as every other family:getXxx()is concrete and callsdoGetXxx(), and only the hook is abstract. This keeps the public method free for the argument validation, notifications and error handling to be added there. Affected:AnalogInputCapability,AnalogOutputCapability,AnalogIOCapability,AnalogInputStreamCapability,PhaseLockedDetectionCapability,TriggerCapability,DigitalInputCapability,DigitalOutputCapability,DigitalIOCapability.- Callers are unaffected: every public name and signature is unchanged.
- Driver authors must rename their implementations to the
do*hook, e.g.getAnalogVoltage->doGetAnalogVoltage,setDigitalValue->doSetDigitalValue,configureStream->doConfigureStream,softwareTrigger->doSoftwareTrigger,supportedSensitivities->doGetSupportedSensitivities. A driver that misses one fails loudly at instantiation withTypeError, naming the missing hook.LabjackDeviceandSR830Device(and their debug counterparts) were migrated. configureStream(channels, sampleRate=None, **parameters)forwards extra keyword arguments todoConfigureStream, so instrument-specific options (the SR830'ssampleClock, the LabJack's deprecatedscanRate) still reach the driver through the shared public method.
Spectrometerfollows the same pattern:getSpectrum()andgetSerialNumber()are now concrete and delegate to the abstractdoGetSpectrum()/doGetSerialNumber(), which is the last place in the library where the public method was itself the hook.getSpectrum(**parameters)forwards keywords to the driver, sogetSpectrum(maxRequests=2, maxWait=0.05)still reachesOISpectrometer.OISpectrometerwas migrated;DebugSpectrois unaffected because it does not subclassSpectrometer.- ACTION REQUIRED for the licenced StellarNet driver, which is distributed
encrypted and is not in this repository: rename its
getSpectrumandgetSerialNumbertodoGetSpectrumanddoGetSerialNumber. Until thenStellarNet()raisesTypeErrorfor the missing hooks.
- ACTION REQUIRED for the licenced StellarNet driver, which is distributed
encrypted and is not in this repository: rename its
hardwarelibrary/tests/testCapabilities.pynow also asserts that noPhysicalDevicesubclass in the library declares an abstract method outside itsdo*hooks, so the pattern is enforced for family base classes, not just mixins.HOPSInterface(sources/verdig.py) is deliberately exempt: it is a transport strategy behindVerdiGDevice, closer toCommunicationPortthan to a device API.- README: the supported-hardware table now lists every driver in the tree. It was
missing
VerdiGDevice,FieldMasterDevice,SR830Device,PwrUSBDeviceandStellarNet, and carried the Millennia without its USB identity (0x0483:0x5740). Added a "Capabilities" section explaining the mixin pattern from first principles, and refreshed the class-hierarchy diagram, which was stale in the same way.
Recorded after the fact: this release was tagged without a changelog entry.
NotificationCenternow comes from the standalonenotifcenterpackage on PyPI, added as a dependency, rather than living in the library.hardwarelibrary/notificationcenter.pyis deleted, and the 19 imports across the tree point atfrom notificationcenter import ....hardwarelibrary/__init__.pyre-exports the external package, sofrom hardwarelibrary import NotificationCenterkeeps working.- The notification API is snake_case, a clean break with no aliases, across
roughly 286 call sites:
addObserver->add_observer,postNotification->post_notification,removeObserver->remove_observer,observersCount->observers_count, and the keyword arguments and attributesnotificationName,observedObject,userInfo,notifyingObject->notification_name,observed_object,user_info,notifying_object. Any code observing a device must be updated; this is what made the release a major one. Note that the rest of the library remains camelCase -- the snake_case is the external package's convention, not a change of style here.
- 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.