Changelog#
All notable changes to QInstrument are documented here. The format follows Keep a Changelog.
3.0.2 — 2026-04-29#
Fixed#
lib/QInstrumentWidget,lib/QInstrumentTree:_firstShow()now calls_syncProperties()before_startDeviceThread(). Previously the sync ran after the device was moved to its worker thread; because_syncProperties()uses direct Python method calls (not Qt queued slots), the serial I/O executed on the main thread while theQSerialPortwas owned by the worker thread. This triggered Qt’s “QSocketNotifier: cannot be enabled from another thread” warning, causedwaitForReadyRead()to fail, and left every property getter returningNone— producing errors such as “Could not set harmonic to None” and locking the UI. Running the sync on the main thread (before the move) eliminates the cross-thread access entirely.
3.0.1 — 2026-04-29#
Fixed#
lib/QInstrumentWidget,lib/QInstrumentTree: removed automaticstartPolling()call from_firstShow(). Auto-polling caused a race condition when SR830/SR844 instruments were embedded in scan-driven applications that also calledreport()from the main thread. Callers that need continuous updates must now invokestartPolling()explicitly (viaQMetaObject.invokeMethod(device, 'startPolling', QueuedConnection)so it executes on the worker thread).
Changed#
instruments/StanfordResearch/SR830,instruments/StanfordResearch/SR844: removedx,y,r, andthetafrom the property registry and droppedQPollingMixinfrom the class hierarchy. Both instruments are now pure control panels (sensitivity, time constant, frequency, etc.), consistent with their widgets and trees. Thereport()method remains as the measurement API for embedding applications. SR844 additionally dropsreference_frequencyandif_frequency.instruments/PriorScientific/Proscan:QProscannow inheritsQPollingMixinand implements_poll()— each cycle queriesposition()andactive_limits(), emittingpositionChangedand a newlimitsChangedsignal.POLL_INTERVALdefaults to 200 ms.QProscanWidgetdrops its main-threadQTimerand cross-thread direct calls; it now connects to the device signals and starts polling viainvokeMethod(QueuedConnection)in_firstShow(), keeping all serial I/O on the worker thread.lib/QPollingMixin: updated docstring to reflect thatstartPolling()must be called explicitly; it is no longer started automatically byQInstrumentWidgetorQInstrumentTree.
3.0.0 — 2026-04-29#
Changed#
lib/QSerialInterface.py: non-blocking mode removed entirely. Theblockingproperty,dataReadysignal,_buffer, and_handleReadyReadslot have been deleted.receive()andreadn()usewaitForReadyRead()directly and are intended to run in a dedicated worker thread rather than on the GUI thread. TheQEventLoop-based approach introduced in 2.4.0 has been reverted;waitForReadyRead()is safe in a worker thread and avoids reentrancy hazards.lib/QSerialInstrument.py:QSerialInterfaceis now constructed as a Qt child of the instrument (parent=self), so it migrates automatically when the instrument is moved to a worker thread.lib/QInstrumentWidget.py: on first show, the device is moved to a dedicatedQThreadso serial I/O no longer competes with the GUI event loop. Property sync is now fire-and-forget (device.get()); values are delivered asynchronously via thepropertyValuesignal to a new_onPropertyValueslot that applies them with signals blocked.closeEventstops the thread before saving settings.lib/QInstrumentTree.py: full parity withQInstrumentWidget— same first-show threading lifecycle,_restoreSettings()withQReconcileDialog, andcloseEventsave.HARDWARE_DOMINANTclass attribute added.lib/Configure.py:save()accepts an optionalsettingsparameter to avoid cross-thread reads when the device lives in a worker thread.lib/lazy.py:values_differ()extracted fromQInstrumentWidget(was duplicated) and exported in__all__.find_fake_cls()added to__all__.lib/QAbstractInstrument.py:handshake(),expect(), andgetValue()moved toQSerialInstrument.QAbstractInstrumentnow has no concept of hardware communication; it models only instrument state (property and method registry). A future transport subclass (e.g.QGPIBInstrument) would provide the same communication helpers over a different physical layer.lib/QAbstractInstrument.py:persistmetadata flag removed fromsettingsgetter and setter. The base class now treats all writable properties as persistent. Instruments that need to exclude specific properties from save/restore should overridesettingsin the instrument subclass (seeQProscan).instruments/PriorScientific/Proscan/instrument.py:speedandzspeedare excluded from save/restore via asettingsoverride and a_VOLATILEclass attribute, replacing the formerpersist=Falseargument toregisterProperty().lib/QInstrumentWidget.py:_firstShow()now invokesstartPollingvia a queuedQMetaObject.invokeMethodcall after moving the device to its worker thread, if the device inheritsQPollingMixin.closeEvent()callsstopPolling()before stopping the thread when the device supports it.lib/QInstrumentTree.py: same polling integration asQInstrumentWidget.instruments/StanfordResearch/SR830/instrument.py:QSR830now inheritsQPollingMixinand overrides_poll()to use theSNAP?9,3,4batch command, emittingfrequency,r, andthetaaspropertyValuesignals on every tick.instruments/StanfordResearch/SR844/instrument.py: same as SR830.
Added#
lib/QPollingMixin.py: new mixin class that adds a self-scheduling poll loop to any instrument.startPolling()begins the loop;stopPolling()ends it (safe to call from any thread). The loop usesQTimer.singleShotso the next query starts only after the current one completes, preventing query backup under any load.POLL_INTERVAL(default0) sets the delay between the end of one response and the start of the next. The default_poll()callsget()for every registered property; instruments that can batch multiple properties into a single query should override it.
Removed#
lib/QInstrumentWorker.py:QInstrumentWorkerremoved. UseQPollingMixinon the instrument class instead.instruments/StanfordResearch/SR830/worker.py:QSR830Workerremoved. Polling is now handled byQSR830._poll().instruments/StanfordResearch/SR844/worker.py:QSR844Workerremoved. Polling is now handled byQSR844._poll().
2.4.1#
Fixed#
lib/QSerialInterface.py:receive()now decodes bytes witherrors='replace'instead of the strict default. Prevents aUnicodeDecodeErrorwhenfind()scans a port occupied by an instrument that responds to*IDN?with non-UTF-8 bytes.
2.4.0#
Changed#
lib/QSerialInterface.py:receive()andreadn()switched fromwaitForReadyRead()to a scopedQEventLoopdriven byreadyReadand aQTimer. (This approach was reverted in the following release; see Unreleased above.)
Deprecated#
lib/QInstrumentWorker.py:QInstrumentWorkeris deprecated and will be removed in a future release.
2.3.2#
Fixed#
lib/QInstrumentWidget.py:showEventnow defers first-show reconciliation viaQTimer.singleShot(0, ...)instead of calling_restoreSettings()synchronously. Opening aQReconcileDialog(nestedexec()event loop) from inside Qt’s show-event sequence caused a segfault on all platforms; the deferred call runs after the show sequence completes, eliminating the re-entrancy.
2.3.1#
Fixed#
lib/lazy.py: newmake_getattr(lazy, package)factory replaces the hand-written__getattr__boilerplate in every instrument__init__.py. The resolved value is cached back into the package__dict__viasys.modules[package].__dict__, preventing Python’s import machinery from shadowing it with the submodule object on subsequent accesses. All eleven leaf instrument packages updated to use the factory.
2.3.0#
Added#
instruments/__init__.py: dynamic__getattr__aggregates all instrument classes from subpackages viapkgutil.walk_packages. Instrument classes can now be imported directly fromQInstrument.instruments(e.g.from QInstrument.instruments import QDS345Widget, QFakeSR830) without specifying the full subpackage path. Adding a new instrument requires no changes toinstruments/__init__.py.
2.2.0#
Added#
QAbstractInstrument.registerPropertyaccepts apersistkeyword (defaultTrue). Properties withpersist=Falseare excluded fromsettingsand never written to or restored from configuration files.Proscan:speedandzspeedset topersist=False.Configure.read()reads the saved JSON without applying it, enabling comparison before commit.QInstrumentWidget._restoreSettings(): on first show, hardware state is compared against the saved configuration. If no file exists the hardware state is saved; if values match nothing happens; if they differ aQReconcileDialogis shown so the user can choose which values to adopt.HARDWARE_DOMINANT = TrueonQProscanWidgetmakes “Keep Hardware” the default button.QInstrumentWorker: runs an instrument in a dedicatedQThreadwith a zero-interval poll loop.QSR830WorkerandQSR844Workeremit[frequency, R, theta]viaSNAP?9,3,4.Opus laser:
STATUS?property returnsTrue(ENABLED) orFalse(DISABLED); logs aWARNINGwhenDISABLEDis received.Opus laser:
CONTROL=POWERsent inidentify()to establish power-control mode on every connection.
Fixed#
QLedWidgetrewritten to useQPainterinstead ofQSvgRenderer. Eliminates theQtSvgdependency and fixes a silent packaging bug whereQLedWidget.svgwas never included in the distributed wheel, causingFileNotFoundErroron construction for PyPI installs.Opus laser:
POWER=,CURRENT=,ON, andOFFsetters now callhandshake()instead oftransmit(), consuming the acknowledgement response and preventing read desynchronisation.QInstrumentWidget.set(): replacedblockSignals(True/False)pair withQSignalBlockerso signals are correctly restored even if the setter raises.
2.1.0#
Added#
QInstrumentRacknow supports drag-to-reorder instrument slots. A⋮drag handle appears on each slot; dragging highlights the target with a colored bar and commits the move on release.QInstrumentRackclose button (×) overlaid on each slot.QInstrumentRack.editableproperty (defaultTrue) hides or shows the toolbar, drag handles, and close buttons as a unit. Seteditable=Falsefor embedded rack contexts where the instrument set should be fixed.-f/--fakecommand-line flag for theqinstrumentCLI andQInstrumentRack.example(): loads all instruments in fake (simulated) mode without probing hardware. The flag is remembered by the rack, so instruments added interactively via the picker dialog also use fake devices.QLedWidget: disabled widgets now render asWHITE/OFF(grayed out) regardless of their current color. Re-enabling restores the original color and state.Tests:
QInstrumentRack,QLedWidget, andQRotaryEncoderSpinBoxnow have pytest coverage.
Fixed#
Opus widget: poll timer is no longer started when the device port is not open, preventing spurious “Cannot send data: device is not open” log messages at startup.
Opus widget:
maximum_poweris now initialized from the model subclass constant (MAXIMUM_POWER) rather than a hard-coded default.
2.0.0#
Added#
Instruments are now organized in a two-level manufacturer hierarchy:
instruments/<Manufacturer>/<Name>/. The instrument picker inQInstrumentRackdiscovers all packages that contain awidget.pyat this depth.Novanta Opus family:
Opusbase class plusOpus532,Opus660, andOpus1064model subclasses. Each subclass setsWAVELENGTH,MINIMUM_POWER, andMAXIMUM_POWERas class attributes — no code duplication.QInstrumentRack: “Add instrument…” toolbar button that opens a picker dialog listing all discovered instrument widgets.QInstrumentRack: right-click context menu on each instrument slot to remove it at runtime.
Changed#
All existing instrument packages moved to their manufacturer subdirectory. Import paths change from
QInstrument.instruments.<Name>toQInstrument.instruments.<Manufacturer>.<Name>.
1.3.0#
Added#
QInstrumentTree: apyqtgraphParameterTree-based inspector that presents all registered properties and methods for any instrument without requiring a.uifile. Properties are editable live; methods appear as buttons. Read-only properties are displayed but cannot be edited.tree.pymodule added to each instrument package (QDS345Tree,QSR830Tree,QOpusTree, etc.).[tree]optional dependency (pip install 'QInstrument[tree]') pulls inpyqtgraph>=0.13.QInstrumentTree.fields/FIELDS: restrict the parameter tree to a named subset of properties and methods.
1.2.0#
Added#
registerPropertyaccepts adebouncekeyword (milliseconds).QInstrumentWidgetcoalesces rapid UI changes and sends only the final value to the device after the user pauses. Designed for controls (e.g. laser power) that must not receive a rapid stream of intermediate values.QJoystick:padColorandknobColorstyle properties,setRange()method, and several paint-event improvements.
Changed#
IPGLaser widget overhauled: LED fault indicator, aiming-beam toggle, emission toggle, and improved status polling.
1.1.0#
Added#
QInstrumentRack: top-level widget that holds multipleQInstrumentWidgetinstances in a vertical layout.qinstrumentCLI entry point (also invokable aspython -m QInstrument). With no arguments it restores the last session; with instrument names it loads those instruments.Instrument list is persisted to
~/.QInstrument/QInstrumentRack.jsonon close and restored on next launch.
1.0.2#
Fixed#
QSerialInterface: corrected PyQt6 compatibility — integer enum values are now accessed via the long-form scoped path (BaudRate.Baud9600, etc.) throughout.
1.0.1#
Added#
GitHub Actions CI workflow: runs the full test suite on push/PR.
1.0.0#
Added#
PiezoDrive PDUS210 ultrasonic amplifier: fully migrated to the
registerProperty()API with fake device and widget.Configure: JSON-based save/restore gated on the_shownflag so test widgets closed during teardown do not write config files.Expanded pytest suite covering
QSerialInterfaceandQInstrumentWidgetauto-binding.
0.4.0#
Added#
pyproject.toml-based packaging; installable from PyPI aspip install QInstrument.Initial Sphinx documentation published to Read the Docs.
pytesttest suite.LICENSE.md(GPLv3).