From 5682b4d28d35aa8cf32819c227e496f7290359ec Mon Sep 17 00:00:00 2001 From: jaknapper Date: Tue, 10 Feb 2026 18:42:59 +0000 Subject: [PATCH] Illumination Things for sangaboard and simulator --- .../things/camera/simulation.py | 13 +++-- .../things/illumination.py | 49 ++++++++++--------- .../things/stage/sangaboard.py | 30 +++++++++++- 3 files changed, 65 insertions(+), 27 deletions(-) diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py index 521fc7cc..c22b32e6 100644 --- a/src/openflexure_microscope_server/things/camera/simulation.py +++ b/src/openflexure_microscope_server/things/camera/simulation.py @@ -140,6 +140,7 @@ class SimulatedCamera(BaseCamera): self._capture_thread: Optional[Thread] = None self._capture_enabled = False self.generate_sprites() + self.mult = 1 repeating: bool = lt.property(default=False) @@ -339,14 +340,20 @@ class SimulatedCamera(BaseCamera): pl_img = Image.fromarray(np_img.astype("uint8")) return pl_img.resize((self.shape[1], self.shape[0]), Image.Resampling.BILINEAR) + @lt.action + def set_led(self, led_on: bool = True, led_channel=None): + if led_on: + self.mult = 1 + else: + self.mult = 0 + + def generate_frame(self) -> Image.Image: """Generate a frame with blobs based on the stage coordinates.""" pos = self._stage.instantaneous_position frame = self.generate_image((pos["y"], pos["x"], pos["z"])) # Simulate LED turning off by setting all channels to 0 - if not self._stage.led_on: - frame = np.full((self.shape[0], self.shape[1], 3), 0, dtype=np.uint8) - return frame + return frame * self.mult def __enter__(self) -> Self: """Start the capture thread when the Thing context manager is opened.""" diff --git a/src/openflexure_microscope_server/things/illumination.py b/src/openflexure_microscope_server/things/illumination.py index 39d752e8..7cfddf50 100644 --- a/src/openflexure_microscope_server/things/illumination.py +++ b/src/openflexure_microscope_server/things/illumination.py @@ -3,19 +3,12 @@ from typing import Literal import labthings_fastapi as lt +from .stage import BaseStage +from .camera import BaseCam class Illumination(lt.Thing): """Abstract illumination controller.""" - brightness: float = lt.property( - default=0.0, - description="Illumination brightness (0-1)", - ) - - @lt.action - def set_brightness(self, brightness: float) -> None: - self.brightness = brightness - @lt.action def flash( self, @@ -23,22 +16,14 @@ class Illumination(lt.Thing): dt: float = 0.5, ) -> None: """Flash the illumination source.""" - for _ in range(number_of_flashes): - self.set_brightness(1.0) - time.sleep(dt) - self.set_brightness(0.0) - time.sleep(dt) - + raise NotImplementedError( + 'Flashing the LED can only be done from the simulator or sangaboard' + ) class SangaIllumination(Illumination): """Illumination driven by a Sangaboard.""" - sangaboard = lt.thing_slot(description="Sangaboard providing LED control") - - @lt.action - def set_brightness(self, brightness: float) -> None: - self.sangaboard.set_led_brightness(brightness) - self.brightness = brightness + _stage: BaseStage = lt.thing_slot() @lt.action def flash( @@ -48,7 +33,25 @@ class SangaIllumination(Illumination): led_channel: Literal["cc"] = "cc", ) -> None: for _ in range(number_of_flashes): - self.sangaboard.set_led(True, led_channel) + self._stage.set_led(False, led_channel) time.sleep(dt) - self.sangaboard.set_led(False, led_channel) + self._stage.set_led(True, led_channel) + time.sleep(dt) + +class SimulatorIllumination(Illumination): + """Illumination control in the simulator.""" + + _cam: BaseCam = lt.thing_slot() + + @lt.action + def flash( + self, + number_of_flashes: int = 10, + dt: float = 0.5, + led_channel: Literal["cc"] = "cc", + ) -> None: + for _ in range(number_of_flashes): + self._cam.set_led(False, led_channel) + time.sleep(dt) + self._cam.set_led(True, led_channel) time.sleep(dt) diff --git a/src/openflexure_microscope_server/things/stage/sangaboard.py b/src/openflexure_microscope_server/things/stage/sangaboard.py index 800a0e31..fb313337 100644 --- a/src/openflexure_microscope_server/things/stage/sangaboard.py +++ b/src/openflexure_microscope_server/things/stage/sangaboard.py @@ -6,7 +6,7 @@ import threading from contextlib import contextmanager from copy import copy from types import TracebackType -from typing import Any, Iterator, Optional, Self +from typing import Any, Iterator, Literal, Optional, Self import semver @@ -172,3 +172,31 @@ class SangaboardThing(BaseStage): with self.sangaboard() as sb: sb.zero_position() self.update_position() + + @lt.action + def set_led( + self, + led_on: bool = True, + led_channel: Literal["cc"] = "cc", + ) -> None: + """Flash the LED to identify the board. + + This is intended to be useful in situations where there are multiple + Sangaboards in use, and it is necessary to identify which one is + being addressed. + """ + led_command = f"led_{led_channel}" + with self.sangaboard() as sb: + return_value = sb.query(f"{led_command}?") + if not return_value.startswith("CC LED:"): + raise IOError("The sangaboard does not support LED control") + + # Reading and setting LED brightness suffers from repeated reads and writes + # decreasing the value. Rather than use the value the code warns that the value + # cannot be used. + if led_on: + on_brightness = 0.32 + sb.query(f"{led_command} {on_brightness}") + else: + sb.query(f"{led_command} 0") +