From db4a0822716eb81bc87840b3be9f69f338b68c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 18 May 2026 10:34:25 -0400 Subject: [PATCH 01/10] Fix inverted serial-number match in USBDeviceDescriptor Problem: USBDeviceDescriptor.matchesPhysicalDevice() returned False when the serialNumber regex *matched* the device's serial number, which is the opposite of the intended semantics. As a result, DeviceManager.usbDeviceDisconnected could not identify and remove the matching PhysicalDevice from its set when a USB device was unplugged. Solution: invert the regex condition (is not None -> is None) so that a successful regex match falls through to return True, and a non-match returns False. Co-Authored-By: Claude Opus 4.7 (1M context) --- hardwarelibrary/devicemanager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardwarelibrary/devicemanager.py b/hardwarelibrary/devicemanager.py index 05b3c988..46036ae6 100644 --- a/hardwarelibrary/devicemanager.py +++ b/hardwarelibrary/devicemanager.py @@ -93,7 +93,7 @@ def matchesPhysicalDevice(self, device): return False if self.idVendor != device.idVendor: return False - if re.match(self.serialNumber, device.serialNumber, re.IGNORECASE) is not None: + if re.match(self.serialNumber, device.serialNumber, re.IGNORECASE) is None: return False return True From f08ff2d5e52c55c5d5c1cf491f361d1e033a3e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 18 May 2026 10:34:30 -0400 Subject: [PATCH 02/10] Fix NameError in DebugRotationDevice.doMoveBy Problem: DebugRotationDevice.doMoveBy(displacement) incremented self._debugOrientation by 'angle', which is not defined in this method. Any call to doMoveBy would raise NameError. Solution: reference the actual parameter, 'displacement'. Co-Authored-By: Claude Opus 4.7 (1M context) --- hardwarelibrary/motion/rotationdevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardwarelibrary/motion/rotationdevice.py b/hardwarelibrary/motion/rotationdevice.py index f7486443..46899568 100644 --- a/hardwarelibrary/motion/rotationdevice.py +++ b/hardwarelibrary/motion/rotationdevice.py @@ -54,7 +54,7 @@ def doMoveTo(self, angle): self._debugOrientation = angle def doMoveBy(self, displacement): - self._debugOrientation += angle + self._debugOrientation += displacement def doHome(self): self._debugOrientation = 0 From 58a4509eb3b44be2ff10394f1af27a1260eb8b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 18 May 2026 10:34:36 -0400 Subject: [PATCH 03/10] Fix TypeError in Spectrometer.showHelp Problem: Spectrometer.showHelp formatted its help string with ', '.join(Spectrometer.supportedClassNames) -- without parentheses. supportedClassNames is a classmethod, so the join was attempting to iterate a bound method object, which would raise TypeError whenever showHelp was invoked (i.e. when guiding a user through a startup error). Solution: add the missing () so the classmethod is actually called, returning the list of supported class names. Co-Authored-By: Claude Opus 4.7 (1M context) --- hardwarelibrary/spectrometers/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardwarelibrary/spectrometers/base.py b/hardwarelibrary/spectrometers/base.py index 6ebf95bb..b9429a94 100644 --- a/hardwarelibrary/spectrometers/base.py +++ b/hardwarelibrary/spectrometers/base.py @@ -147,7 +147,7 @@ def showHelp(cls, err=None): speed, etc...). More spectrometers will be supported in the future. Look at the class USB2000 to see what you have to provide to support a new spectrometer (it is not that much work, but you need one to test). -""".format(__file__, ', '.join(Spectrometer.supportedClassNames))) +""".format(__file__, ', '.join(Spectrometer.supportedClassNames()))) # Well, how about that? This does not work in Windows # https://stackoverflow.com/questions/2330245/python-change-text-color-in-shell From 33c64df4c98ff4891809bccdd90b27a7f5ede2b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 18 May 2026 10:34:43 -0400 Subject: [PATCH 04/10] Send the correct init opcode to Ocean Insight spectrometers Problem: OISpectrometer.doInitializeDevice sent the four ASCII bytes b'0x01' (i.e. the characters '0','x','0','1') to the spectrometer instead of the single byte 0x01. The spectrometer protocol expects a one-byte initialization opcode, so the device was almost certainly receiving an unrecognized command. Every other sendCommand call in the file uses the proper escape form (b'\\x05', b'\\xfe', b'\\x02'). Solution: replace b'0x01' with b'\\x01' so the single init byte is transmitted. Co-Authored-By: Claude Opus 4.7 (1M context) --- hardwarelibrary/spectrometers/oceaninsight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardwarelibrary/spectrometers/oceaninsight.py b/hardwarelibrary/spectrometers/oceaninsight.py index b051d94f..7595def4 100644 --- a/hardwarelibrary/spectrometers/oceaninsight.py +++ b/hardwarelibrary/spectrometers/oceaninsight.py @@ -275,7 +275,7 @@ def doInitializeDevice(self): self.epStatus = self.inputEndpoints[self.epStatusIdx] self.flushEndpoints() - self.sendCommand(b'0x01') + self.sendCommand(b'\x01') time.sleep(0.1) self.getCalibration() except Exception as err: From 1f25ea9089d633a748119f713ac1adcbc0120f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 18 May 2026 10:34:49 -0400 Subject: [PATCH 05/10] Bind the utils module name in physicaldevice Problem: physicaldevice.py used 'import hardwarelibrary.utils', which only binds the dotted name 'hardwarelibrary.utils' and not the short name 'utils'. Every call to utils.getAllUSBIds(...), utils.connectedUSBDevices(...), and utils.getCandidateDeviceClasses(...) in PhysicalDevice.any, .connectedDevices, and .anyDevice would therefore raise NameError. Solution: switch to 'from hardwarelibrary import utils', which binds the short name and matches the existing call sites. Co-Authored-By: Claude Opus 4.7 (1M context) --- hardwarelibrary/physicaldevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardwarelibrary/physicaldevice.py b/hardwarelibrary/physicaldevice.py index 4fec4c1c..15ad4483 100644 --- a/hardwarelibrary/physicaldevice.py +++ b/hardwarelibrary/physicaldevice.py @@ -1,7 +1,7 @@ from enum import Enum, IntEnum from threading import Thread, RLock -import hardwarelibrary.utils +from hardwarelibrary import utils from hardwarelibrary.notificationcenter import NotificationCenter import typing import time From b4948158a1a980c70354f15c8b1db5ffdf64318b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 18 May 2026 10:34:54 -0400 Subject: [PATCH 06/10] Remove duplicate measureAbsolutePower in PowerMeterDevice Problem: PowerMeterDevice defined measureAbsolutePower twice with identical bodies. Python silently retained only the second definition; the first was dead code, and the duplication invited divergence on future edits. Solution: delete the second copy, keeping the original definition that sits next to __init__ and the rest of the public API. Co-Authored-By: Claude Opus 4.7 (1M context) --- hardwarelibrary/powermeters/powermeterdevice.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/hardwarelibrary/powermeters/powermeterdevice.py b/hardwarelibrary/powermeters/powermeterdevice.py index d118b6a5..5b9208cf 100644 --- a/hardwarelibrary/powermeters/powermeterdevice.py +++ b/hardwarelibrary/powermeters/powermeterdevice.py @@ -28,11 +28,5 @@ def setCalibrationWavelength(self, wavelength): self.doSetCalibrationWavelength(wavelength) self.doGetCalibrationWavelength() - def measureAbsolutePower(self): - self.doGetAbsolutePower() - power = self.absolutePower - NotificationCenter().postNotification(PowerMeterNotification.didMeasure, notifyingObject=self, userInfo=power) - return power - def doGetStatusUserInfo(self): return self.measureAbsolutePower() From 60649d71e5bf2d7e90956b0f4134d1803ecae970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 18 May 2026 10:35:01 -0400 Subject: [PATCH 07/10] Always bind endPointIndex in DebugPort read/write Problem: DebugPort.readData and DebugPort.writeData set endPointIndex = 0 only when their endPoint argument was None. Any caller passing an explicit endPoint left endPointIndex unbound, so the subsequent reference (self.outputBuffers[endPointIndex] / self.inputBuffers[endPointIndex]) would raise UnboundLocalError. Solution: collapse the conditional into a single expression -- 'endPointIndex = 0 if endPoint is None else endPoint' -- so the variable is always bound. Mirrors the default-to-0 pattern already used by bytesAvailable. Co-Authored-By: Claude Opus 4.7 (1M context) --- hardwarelibrary/communication/debugport.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/hardwarelibrary/communication/debugport.py b/hardwarelibrary/communication/debugport.py index b7ebe45f..8839134b 100644 --- a/hardwarelibrary/communication/debugport.py +++ b/hardwarelibrary/communication/debugport.py @@ -59,8 +59,7 @@ def flush(self): self.buffers = [bytearray(),bytearray()] def readData(self, length, endPoint=None): - if endPoint is None: - endPointIndex = 0 + endPointIndex = 0 if endPoint is None else endPoint with self.portLock: time.sleep(self.delay*random.random()) @@ -75,8 +74,7 @@ def readData(self, length, endPoint=None): return data def writeData(self, data, endPoint=None): - if endPoint is None: - endPointIndex = 0 + endPointIndex = 0 if endPoint is None else endPoint with self.portLock: self.inputBuffers[endPointIndex].extend(data) From 20fcad61307566e608a33c0e5005f37d93aafceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 18 May 2026 10:35:06 -0400 Subject: [PATCH 08/10] Fix AttributeError in SutterDevice initialization error message Problem: SutterDevice.doInitializeDevice formatted its 'Cannot allocate port {0}' error with self.portPath, but SutterDevice never sets self.portPath -- the port path is only a local variable inside the non-debug branch. If the 'self.port is None' guard ever fired, the error message would raise AttributeError and mask the real failure. Solution: format the message with self.serialNumber, which is always defined on the device and conveys the relevant identifier. Co-Authored-By: Claude Opus 4.7 (1M context) --- hardwarelibrary/motion/sutterdevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardwarelibrary/motion/sutterdevice.py b/hardwarelibrary/motion/sutterdevice.py index 5625bf13..8cb0a47c 100644 --- a/hardwarelibrary/motion/sutterdevice.py +++ b/hardwarelibrary/motion/sutterdevice.py @@ -49,7 +49,7 @@ def doInitializeDevice(self): self.port.open(baudRate=128000, timeout=10) if self.port is None: - raise PhysicalDevice.UnableToInitialize("Cannot allocate port {0}".format(self.portPath)) + raise PhysicalDevice.UnableToInitialize("Cannot allocate port for serial '{0}'".format(self.serialNumber)) self.positionInMicrosteps() From ef7c7c0b7c1d4a0dc2aae40f0ba7defd2c6e7eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 18 May 2026 10:35:25 -0400 Subject: [PATCH 09/10] Define globalLock used by CoboltDebugSerial Problem: CoboltDebugSerial.open/close/read/write and the helper increasePowerSlowlyInBackground all used 'with globalLock:' to serialize access from a background power-ramp thread, but globalLock was never defined or imported in cobolt.py (the only real definition lived in tests/testCommunicationPorts.py). Any debug-mode operation would raise NameError. Solution: define a module-level 'globalLock = RLock()' near the top of cobolt.py. RLock was already imported. Co-Authored-By: Claude Opus 4.7 (1M context) --- hardwarelibrary/sources/cobolt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hardwarelibrary/sources/cobolt.py b/hardwarelibrary/sources/cobolt.py index fa877129..8a478a67 100644 --- a/hardwarelibrary/sources/cobolt.py +++ b/hardwarelibrary/sources/cobolt.py @@ -6,6 +6,8 @@ import time from threading import Thread, RLock +globalLock = RLock() + class CoboltCantTurnOnWithAutostartOn(Exception): pass From 842edb5b14555c30217076fb7237c2483cd0ff10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Mon, 18 May 2026 10:35:38 -0400 Subject: [PATCH 10/10] Preserve UnableToInitialize message in CoboltDevice init Problem: CoboltDevice.doInitializeDevice had two broken issues in its except clauses. The first 'except Exception as error' caught *everything* -- including the explicit 'PhysicalDevice.UnableToInitialize(\"Cannot allocate port {0}\"...)' raised on the previous line -- and re-raised a blank UnableToInitialize() with no message, discarding the diagnostic text. The second clause referenced 'PhysicalDeviceUnableToInitialize' which is not defined anywhere, and was unreachable in any case because of the broad except above it. Solution: reorder so the specific exception is caught first and re-raised unchanged ('raise'), preserving its message; the generic 'except Exception' then wraps anything else as UnableToInitialize. Also flattened the nested 'is not None / isOpen' checks for readability. Co-Authored-By: Claude Opus 4.7 (1M context) --- hardwarelibrary/sources/cobolt.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/hardwarelibrary/sources/cobolt.py b/hardwarelibrary/sources/cobolt.py index 8a478a67..7b6e2255 100644 --- a/hardwarelibrary/sources/cobolt.py +++ b/hardwarelibrary/sources/cobolt.py @@ -66,13 +66,14 @@ def doInitializeDevice(self): self.doTurnAutostartOn() self.doGetInterlockState() self.doGetPower() - except Exception as error: - if self.port is not None: - if self.port.isOpen: - self.port.close() + except PhysicalDevice.UnableToInitialize: + if self.port is not None and self.port.isOpen: + self.port.close() + raise + except Exception: + if self.port is not None and self.port.isOpen: + self.port.close() raise PhysicalDevice.UnableToInitialize() - except PhysicalDeviceUnableToInitialize as error: - raise error def doShutdownDevice(self):