From b2e91f9b950a78a9169bd08ed74fdc46181ca3d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20C=C3=B4t=C3=A9?= Date: Sat, 27 Jun 2026 17:08:37 -0400 Subject: [PATCH] Millennia eV: status snapshot + preserve init error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small gaps on the eV driver that pushed work into downstream callers: * doInitializeDevice caught the failure and raised a bare PhysicalDevice.UnableToInitialize(), discarding the underlying serial error. Callers could no longer tell a busy port ("[Errno 16] Resource busy") from a missing one. Now re-raises UnableToInitialize(error) from error so the cause is preserved. * MillenniaEv25Device did not override doGetStatusUserInfo(), so the base returned None and PhysicalDevice.startBackgroundStatusUpdates() posted empty status notifications for the eV. Added an override returning {power, isLaserOn, isShutterOpen} — a one-call snapshot for background monitoring and for GUI polling. Adds a debug-device test for the status snapshot. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01K6LVhfM3d9CcJdkuSMMqag --- hardwarelibrary/sources/millennia.py | 21 +++++++++++++++++++-- hardwarelibrary/tests/testMillennia.py | 9 +++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/hardwarelibrary/sources/millennia.py b/hardwarelibrary/sources/millennia.py index 8f5750e..5f5b9cd 100644 --- a/hardwarelibrary/sources/millennia.py +++ b/hardwarelibrary/sources/millennia.py @@ -80,10 +80,13 @@ def doInitializeDevice(self): self.port = SerialPort(portPath=self.portPath) self.port.open(baudRate=self.defaultBaudRate) self.readIdentity() - except Exception: + except Exception as error: if self.port is not None and self.port.isOpen: self.port.close() - raise PhysicalDevice.UnableToInitialize() + # Preserve the underlying error (e.g. pyserial's "[Errno 16] + # Resource busy") so callers can tell a busy port from a missing + # one instead of seeing a bare, info-less UnableToInitialize. + raise PhysicalDevice.UnableToInitialize(error) from error def readIdentity(self): # The eV identifies via the SCPI-style *IDN? query. The ?IDN form in @@ -197,6 +200,20 @@ def doGetPower(self) -> float: # token. return float(self.queryString("?P").split()[0]) + # Status snapshot + + def doGetStatusUserInfo(self) -> dict: + # Single-call snapshot for PhysicalDevice.startBackgroundStatusUpdates() + # (posted as PhysicalDeviceNotification.status) and for GUI polling: the + # output power plus the diode (emission) and shutter states. Without this + # override the base returns None, so the monitoring loop posts nothing + # useful for the eV. + return { + "power": self.doGetPower(), + "isLaserOn": self.doGetOnOffState(), + "isShutterOpen": self.doGetShutterState(), + } + class DebugMillenniaEv25Device(MillenniaEv25Device): classIdVendor = 0xFFFF diff --git a/hardwarelibrary/tests/testMillennia.py b/hardwarelibrary/tests/testMillennia.py index cb80f67..77fd1bc 100644 --- a/hardwarelibrary/tests/testMillennia.py +++ b/hardwarelibrary/tests/testMillennia.py @@ -104,6 +104,15 @@ def testShutterIsIndependentOfOnOff(self): self.assertFalse(self.laser.isLaserOn()) self.assertTrue(self.laser.isShutterOpen()) # off, shutter unchanged + def testStatusUserInfoSnapshotsPowerOnOffAndShutter(self): + self.laser.setPower(7.5) + self.laser.turnOn() + self.laser.openShutter() + self.assertEqual( + self.laser.doGetStatusUserInfo(), + {"power": 7.5, "isLaserOn": True, "isShutterOpen": True}, + ) + class TestMillenniaEv25Device(unittest.TestCase): def setUp(self):