Merge branch 'flash-led-sim' into 'v3'

Illumination Thing

See merge request openflexure/openflexure-microscope-server!468
This commit is contained in:
Joe Knapper 2026-02-12 16:37:53 +00:00
commit 704fc5569f
8 changed files with 219 additions and 24 deletions

View file

@ -140,6 +140,8 @@ class SimulatedCamera(BaseCamera):
self._capture_thread: Optional[Thread] = None
self._capture_enabled = False
self.generate_sprites()
# Whether the LED is on
self.led_on = True
repeating: bool = lt.property(default=False)
@ -339,8 +341,16 @@ class SimulatedCamera(BaseCamera):
pl_img = Image.fromarray(np_img.astype("uint8"))
return pl_img.resize((self.shape[1], self.shape[0]), Image.Resampling.BILINEAR)
def set_led(self, led_on: bool = True) -> None:
"""Set the simulated LED to on or off."""
self.led_on = led_on
def generate_frame(self) -> Image.Image:
"""Generate a frame with blobs based on the stage coordinates."""
# Simulate LED turning off by setting all channels to 0
if not self.led_on:
return Image.new(mode="RGB", size=(self.shape[1], self.shape[0]), color=0)
# Otherwise, generate a frame from current position
pos = self._stage.instantaneous_position
return self.generate_image((pos["y"], pos["x"], pos["z"]))

View file

@ -0,0 +1,59 @@
"""A module to handle the illumination in LabThings."""
import time
from typing import Literal
import labthings_fastapi as lt
from .camera.simulation import SimulatedCamera
from .stage.sangaboard import SangaboardThing
class Illumination(lt.Thing):
"""Base class for an illumination controller."""
@lt.action
def set_led(self, led_on: bool = True) -> None:
"""Set the LED to on or off."""
raise NotImplementedError(
"Turning the LED on and off should be implemented by child classes."
)
@lt.action
def flash(self, number_of_flashes: int = 10, dt: float = 0.5) -> None:
"""Flash the illumination source.
:param number_of_flashes: Number of times to flash the LED.
:param dt: Flash duration in seconds.
"""
for _ in range(number_of_flashes):
self.set_led(False)
time.sleep(dt)
self.set_led(True)
time.sleep(dt)
class SangaIllumination(Illumination):
"""Illumination driven by a Sangaboard."""
_stage: SangaboardThing = lt.thing_slot()
@lt.action
def set_led(
self,
led_on: bool = True,
led_channel: Literal["cc"] = "cc",
) -> None:
"""Set the LED to on or off."""
self._stage.set_led(led_on, led_channel)
class SimulatorIllumination(Illumination):
"""Illumination control in the simulator."""
_cam: SimulatedCamera = lt.thing_slot()
@lt.action
def set_led(self, led_on: bool = True) -> None:
"""Set the LED to on or off."""
self._cam.set_led(led_on)

View file

@ -3,7 +3,6 @@
from __future__ import annotations
import threading
import time
from contextlib import contextmanager
from copy import copy
from types import TracebackType
@ -174,11 +173,9 @@ class SangaboardThing(BaseStage):
sb.zero_position()
self.update_position()
@lt.action
def flash_led(
def set_led(
self,
number_of_flashes: int = 10,
dt: float = 0.5,
led_on: bool = True,
led_channel: Literal["cc"] = "cc",
) -> None:
"""Flash the LED to identify the board.
@ -196,14 +193,8 @@ class SangaboardThing(BaseStage):
# 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.
intended_brightness = float(return_value[7:])
on_brightness = 0.32
self.logger.warning(
"Brightness control is not yet implemented. Desired brightness: "
f"{intended_brightness}. Set brightness: {on_brightness}"
)
for _i in range(number_of_flashes):
sb.query(f"{led_command} 0")
time.sleep(dt)
if led_on:
on_brightness = 0.32
sb.query(f"{led_command} {on_brightness}")
time.sleep(dt)
else:
sb.query(f"{led_command} 0")