update action, property, and setting syntax for labthings-fastapi 0.0.12

This commit is contained in:
Julian Stirling 2025-12-14 11:52:07 +00:00
parent bbbfaf8602
commit 5eea78cac7
14 changed files with 139 additions and 192 deletions

View file

@ -161,7 +161,7 @@ ignore = [
[tool.ruff.lint.pydocstyle] [tool.ruff.lint.pydocstyle]
# This lets the D401 checker understand that decorated thing properties and thing # This lets the D401 checker understand that decorated thing properties and thing
# settings act like properties so should be documented as such. # settings act like properties so should be documented as such.
property-decorators = ["labthings_fastapi.thing_property", "labthings_fastapi.thing_setting"] property-decorators = ["labthings_fastapi.property", "labthings_fastapi.setting"]
[tool.ruff.lint.pylint] [tool.ruff.lint.pylint]
max-args = 7 max-args = 7

View file

@ -350,7 +350,7 @@ class AutofocusThing(lt.Thing):
field of view to assess focus (autofocus and testing the success of a z-stack) field of view to assess focus (autofocus and testing the success of a z-stack)
""" """
@lt.thing_action @lt.action
def fast_autofocus( def fast_autofocus(
self, self,
sharpness_monitor: SharpnessMonitorDep, sharpness_monitor: SharpnessMonitorDep,
@ -381,7 +381,7 @@ class AutofocusThing(lt.Thing):
# Return all focus data # Return all focus data
return sharpness_monitor.data_dict() return sharpness_monitor.data_dict()
@lt.thing_action @lt.action
def z_move_and_measure_sharpness( def z_move_and_measure_sharpness(
self, self,
sharpness_monitor: SharpnessMonitorDep, sharpness_monitor: SharpnessMonitorDep,
@ -407,7 +407,7 @@ class AutofocusThing(lt.Thing):
sharpness_monitor.focus_rel(current_dz) sharpness_monitor.focus_rel(current_dz)
return sharpness_monitor.data_dict() return sharpness_monitor.data_dict()
@lt.thing_action @lt.action
def looping_autofocus( def looping_autofocus(
self, self,
stage: Stage, stage: Stage,
@ -455,19 +455,13 @@ class AutofocusThing(lt.Thing):
"Looping autofocus couldn't converge on a focus location." "Looping autofocus couldn't converge on a focus location."
) )
stack_images_to_save = lt.ThingSetting( stack_images_to_save: int = lt.setting(default=1)
initial_value=1,
model=int,
)
"""The number of images to save in a stack. """The number of images to save in a stack.
Defaults to 1 unless you need to see either side of focus Defaults to 1 unless you need to see either side of focus
""" """
stack_min_images_to_test = lt.ThingSetting( stack_min_images_to_test: int = lt.setting(default=9)
initial_value=9,
model=int,
)
"""The minimum number of images to capture in a stack. """The minimum number of images to capture in a stack.
This many images are captures and tested for focus, if the focus is not central This many images are captures and tested for focus, if the focus is not central
@ -477,7 +471,7 @@ class AutofocusThing(lt.Thing):
Defaults to 9 which balances reliability and speed. Defaults to 9 which balances reliability and speed.
""" """
stack_dz = lt.ThingSetting(initial_value=50, model=int) stack_dz: int = lt.setting(default=50)
"""Distance in steps between images in a z-stack. """Distance in steps between images in a z-stack.
Suggested values: Suggested values:
@ -487,7 +481,7 @@ class AutofocusThing(lt.Thing):
* 200 for 20x * 200 for 20x
""" """
@lt.thing_action @lt.action
def create_stack_params( def create_stack_params(
self, self,
images_dir: str, images_dir: str,
@ -560,7 +554,7 @@ class AutofocusThing(lt.Thing):
save_resolution=save_resolution, save_resolution=save_resolution,
) )
@lt.thing_action @lt.action
def run_smart_stack( def run_smart_stack(
self, self,
cam: CameraClient, cam: CameraClient,

View file

@ -203,7 +203,7 @@ class BaseCamera(lt.Thing):
"""Close hardware connection when the Thing context manager is closed.""" """Close hardware connection when the Thing context manager is closed."""
raise NotImplementedError("CameraThings must define their own __exit__ method") raise NotImplementedError("CameraThings must define their own __exit__ method")
@lt.thing_property @lt.property
def calibration_required(self) -> bool: def calibration_required(self) -> bool:
"""Whether the camera needs calibrating. """Whether the camera needs calibrating.
@ -212,7 +212,7 @@ class BaseCamera(lt.Thing):
""" """
return False return False
@lt.thing_action @lt.action
def start_streaming( def start_streaming(
self, main_resolution: tuple[int, int], buffer_count: int self, main_resolution: tuple[int, int], buffer_count: int
) -> None: ) -> None:
@ -239,21 +239,21 @@ class BaseCamera(lt.Thing):
self.mjpeg_stream._streaming = False self.mjpeg_stream._streaming = False
self.lores_mjpeg_stream._streaming = False self.lores_mjpeg_stream._streaming = False
@lt.thing_property @lt.property
def stream_active(self) -> bool: def stream_active(self) -> bool:
"""Whether the MJPEG stream is active.""" """Whether the MJPEG stream is active."""
raise NotImplementedError( raise NotImplementedError(
"CameraThings must define their own stream_active method" "CameraThings must define their own stream_active method"
) )
@lt.thing_action @lt.action
def discard_frames(self) -> None: def discard_frames(self) -> None:
"""Discard frames so that the next frame captured is fresh.""" """Discard frames so that the next frame captured is fresh."""
raise NotImplementedError( raise NotImplementedError(
"CameraThings must define their own discard_frames method" "CameraThings must define their own discard_frames method"
) )
@lt.thing_action @lt.action
def capture_array( def capture_array(
self, self,
stream_name: Literal["main", "lores", "raw", "full"] = "main", stream_name: Literal["main", "lores", "raw", "full"] = "main",
@ -264,10 +264,10 @@ class BaseCamera(lt.Thing):
"CameraThings must define their own capture_array method" "CameraThings must define their own capture_array method"
) )
downsampled_array_factor = lt.ThingProperty(int, 2) downsampled_array_factor: int = lt.property(default=2)
"""The downsampling factor when calling capture_downsampled_array.""" """The downsampling factor when calling capture_downsampled_array."""
@lt.thing_action @lt.action
def capture_downsampled_array(self) -> ArrayModel: def capture_downsampled_array(self) -> ArrayModel:
"""Acquire one image from the camera, downsample, and return as an array. """Acquire one image from the camera, downsample, and return as an array.
@ -279,7 +279,7 @@ class BaseCamera(lt.Thing):
img = self.capture_array() img = self.capture_array()
return downsample(self.downsampled_array_factor, img) return downsample(self.downsampled_array_factor, img)
@lt.thing_action @lt.action
def capture_jpeg( def capture_jpeg(
self, self,
metadata_getter: lt.deps.GetThingStates, metadata_getter: lt.deps.GetThingStates,
@ -316,7 +316,7 @@ class BaseCamera(lt.Thing):
return JPEGBlob.from_temporary_directory(directory, fname) return JPEGBlob.from_temporary_directory(directory, fname)
@lt.thing_action @lt.action
def grab_jpeg( def grab_jpeg(
self, self,
portal: lt.deps.BlockingPortal, portal: lt.deps.BlockingPortal,
@ -340,7 +340,7 @@ class BaseCamera(lt.Thing):
frame = portal.call(stream.grab_frame) frame = portal.call(stream.grab_frame)
return JPEGBlob.from_bytes(frame) return JPEGBlob.from_bytes(frame)
@lt.thing_action @lt.action
def grab_as_array( def grab_as_array(
self, self,
portal: lt.deps.BlockingPortal, portal: lt.deps.BlockingPortal,
@ -367,7 +367,7 @@ class BaseCamera(lt.Thing):
tries += 1 tries += 1
raise OSError("Could not open frames from MJPEG stream.") raise OSError("Could not open frames from MJPEG stream.")
@lt.thing_action @lt.action
def grab_jpeg_size( def grab_jpeg_size(
self, self,
portal: lt.deps.BlockingPortal, portal: lt.deps.BlockingPortal,
@ -389,7 +389,7 @@ class BaseCamera(lt.Thing):
"CameraThings must define their own capture_image method" "CameraThings must define their own capture_image method"
) )
@lt.thing_action @lt.action
def capture_and_save( def capture_and_save(
self, self,
jpeg_path: str, jpeg_path: str,
@ -420,7 +420,7 @@ class BaseCamera(lt.Thing):
save_resolution, save_resolution,
) )
@lt.thing_action @lt.action
def capture_to_memory( def capture_to_memory(
self, self,
logger: lt.deps.InvocationLogger, logger: lt.deps.InvocationLogger,
@ -444,7 +444,7 @@ class BaseCamera(lt.Thing):
image, metadata = self._robust_image_capture(metadata_getter, logger) image, metadata = self._robust_image_capture(metadata_getter, logger)
return self._memory_buffer.add_image(image, metadata, buffer_max=buffer_max) return self._memory_buffer.add_image(image, metadata, buffer_max=buffer_max)
@lt.thing_action @lt.action
def save_from_memory( def save_from_memory(
self, self,
jpeg_path: str, jpeg_path: str,
@ -473,7 +473,7 @@ class BaseCamera(lt.Thing):
save_resolution=save_resolution, save_resolution=save_resolution,
) )
@lt.thing_action @lt.action
def clear_buffers(self) -> None: def clear_buffers(self) -> None:
"""Clear all images in memory.""" """Clear all images in memory."""
self._memory_buffer.clear() self._memory_buffer.clear()
@ -600,10 +600,10 @@ class BaseCamera(lt.Thing):
except Exception as e: except Exception as e:
raise IOError(f"An error occurred while saving {jpeg_path}") from e raise IOError(f"An error occurred while saving {jpeg_path}") from e
settling_time = lt.ThingSetting(float, 0.2) settling_time: float = lt.setting(default=0.2)
"""The settling time when calling the ``settle()`` method.""" """The settling time when calling the ``settle()`` method."""
@lt.thing_action @lt.action
def settle(self) -> None: def settle(self) -> None:
"""Sleep for the settling time, ready to provide a fresh frame. """Sleep for the settling time, ready to provide a fresh frame.
@ -616,24 +616,24 @@ class BaseCamera(lt.Thing):
time.sleep(self.settling_time) time.sleep(self.settling_time)
self.discard_frames() self.discard_frames()
@lt.thing_property @lt.property
def primary_calibration_actions(self) -> list[ActionButton]: def primary_calibration_actions(self) -> list[ActionButton]:
"""The calibration actions for both calibration wizard and settings panel.""" """The calibration actions for both calibration wizard and settings panel."""
return [] return []
@lt.thing_property @lt.property
def secondary_calibration_actions(self) -> list[ActionButton]: def secondary_calibration_actions(self) -> list[ActionButton]:
"""The calibration actions that appear only in settings panel.""" """The calibration actions that appear only in settings panel."""
return [] return []
@lt.thing_property @lt.property
def manual_camera_settings(self) -> list[PropertyControl]: def manual_camera_settings(self) -> list[PropertyControl]:
"""The camera settings to expose as property controls in the settings panel.""" """The camera settings to expose as property controls in the settings panel."""
return [] return []
# Note that the default detector name is set at init. This is over written if # Note that the default detector name is set at init. This is over written if
# setting is loaded from disk. # setting is loaded from disk.
@lt.thing_setting @lt.setting
def detector_name(self) -> str: def detector_name(self) -> str:
"""The name of the active background selector.""" """The name of the active background selector."""
return self._detector_name return self._detector_name
@ -650,12 +650,12 @@ class BaseCamera(lt.Thing):
"""The active background detector instance.""" """The active background detector instance."""
return self.background_detectors[self.detector_name] return self.background_detectors[self.detector_name]
@lt.thing_property @lt.property
def background_detector_status(self) -> BackgroundDetectorStatus: def background_detector_status(self) -> BackgroundDetectorStatus:
"""The status of the active detector for the UI.""" """The status of the active detector for the UI."""
return self.active_detector.status return self.active_detector.status
@lt.thing_action @lt.action
def update_detector_settings(self, data: dict[str, Any]) -> None: def update_detector_settings(self, data: dict[str, Any]) -> None:
"""Update the settings of the current detector. """Update the settings of the current detector.
@ -671,7 +671,7 @@ class BaseCamera(lt.Thing):
# Manually save settings as the setter is not called. # Manually save settings as the setter is not called.
self.save_settings() self.save_settings()
@lt.thing_setting @lt.setting
def background_detector_data(self) -> dict: def background_detector_data(self) -> dict:
"""The data for each background detector, used to save to disk.""" """The data for each background detector, used to save to disk."""
data = {} data = {}
@ -704,13 +704,13 @@ class BaseCamera(lt.Thing):
f"No background detector named {name}, settings will be discarded." f"No background detector named {name}, settings will be discarded."
) )
@lt.thing_action @lt.action
def image_is_sample(self, portal: lt.deps.BlockingPortal) -> tuple[bool, str]: def image_is_sample(self, portal: lt.deps.BlockingPortal) -> tuple[bool, str]:
"""Label the current image as either background or sample.""" """Label the current image as either background or sample."""
current_image = self.grab_as_array(portal, stream_name="lores") current_image = self.grab_as_array(portal, stream_name="lores")
return self.active_detector.image_is_sample(current_image) return self.active_detector.image_is_sample(current_image)
@lt.thing_action @lt.action
def set_background(self, portal: lt.deps.BlockingPortal) -> None: def set_background(self, portal: lt.deps.BlockingPortal) -> None:
"""Grab an image, and use its statistics to set the background. """Grab an image, and use its statistics to set the background.

View file

@ -60,7 +60,7 @@ class OpenCVCamera(BaseCamera):
self._capture_thread.join() self._capture_thread.join()
self.cap.release() self.cap.release()
@lt.thing_property @lt.property
def stream_active(self) -> bool: def stream_active(self) -> bool:
"""Whether the MJPEG stream is active.""" """Whether the MJPEG stream is active."""
if self._capture_enabled and self._capture_thread: if self._capture_enabled and self._capture_thread:
@ -81,12 +81,12 @@ class OpenCVCamera(BaseCamera):
].tobytes() ].tobytes()
self.lores_mjpeg_stream.add_frame(jpeg_lores, portal) self.lores_mjpeg_stream.add_frame(jpeg_lores, portal)
@lt.thing_action @lt.action
def discard_frames(self) -> None: def discard_frames(self) -> None:
"""Discard frames so that the next frame captured is fresh.""" """Discard frames so that the next frame captured is fresh."""
self.capture_array() self.capture_array()
@lt.thing_action @lt.action
def capture_array( def capture_array(
self, self,
stream_name: Literal["main", "full"] = "full", stream_name: Literal["main", "full"] = "full",

View file

@ -169,30 +169,19 @@ class StreamingPiCamera2(BaseCamera):
# trigger a NotConnectedToServerError # trigger a NotConnectedToServerError
self._colour_gains = tf_utils.get_colour_gains_from_lst(self.tuning) self._colour_gains = tf_utils.get_colour_gains_from_lst(self.tuning)
stream_resolution = lt.ThingProperty( stream_resolution: tuple[int, int] = lt.property(default=(820, 616))
tuple[int, int],
initial_value=(820, 616),
)
"""Resolution to use for the MJPEG stream.""" """Resolution to use for the MJPEG stream."""
mjpeg_bitrate = lt.ThingProperty( mjpeg_bitrate: Optional[int] = lt.property(default=100000000)
Optional[int],
initial_value=100000000,
)
"""Bitrate for MJPEG stream (None for default).""" """Bitrate for MJPEG stream (None for default)."""
stream_active = lt.ThingProperty( stream_active: bool = lt.property(default=False, observable=True, readonly=True)
bool,
initial_value=False,
observable=True,
readonly=True,
)
"""Whether the MJPEG stream is active.""" """Whether the MJPEG stream is active."""
def save_settings(self) -> None: def save_settings(self) -> None:
"""Override save_settings to ensure that camera properties don't recurse. """Override save_settings to ensure that camera properties don't recurse.
This method is run by any Thing when a ThingSetting is saved. However, the This method is run by any Thing when a setting is saved. However, the
method reads the thing_setting. As reading the thing setting talks to the method reads the thing_setting. As reading the thing setting talks to the
camera and calls save_settings if the value is not as expected, this could camera and calls save_settings if the value is not as expected, this could
cause recursion. Also this means that saving one setting causes all others cause recursion. Also this means that saving one setting causes all others
@ -204,7 +193,7 @@ class StreamingPiCamera2(BaseCamera):
finally: finally:
self._setting_save_in_progress = False self._setting_save_in_progress = False
@lt.thing_property @lt.property
def calibration_required(self) -> bool: def calibration_required(self) -> bool:
"""Whether the camera needs calibrating.""" """Whether the camera needs calibrating."""
# Check if the lens shading table is calibrated. # Check if the lens shading table is calibrated.
@ -214,7 +203,7 @@ class StreamingPiCamera2(BaseCamera):
_analogue_gain: float = 1.0 _analogue_gain: float = 1.0
@lt.thing_setting @lt.setting
def analogue_gain(self) -> float: def analogue_gain(self) -> float:
"""The Analogue gain applied by the camera sensor.""" """The Analogue gain applied by the camera sensor."""
if not self._setting_save_in_progress and self.streaming: if not self._setting_save_in_progress and self.streaming:
@ -234,7 +223,7 @@ class StreamingPiCamera2(BaseCamera):
_colour_gains: tuple[float, float] = (1.0, 1.0) _colour_gains: tuple[float, float] = (1.0, 1.0)
@lt.thing_setting @lt.setting
def colour_gains(self) -> tuple[float, float]: def colour_gains(self) -> tuple[float, float]:
"""The red and blue colour gains, must be between 0.0 and 32.0.""" """The red and blue colour gains, must be between 0.0 and 32.0."""
if not self._setting_save_in_progress and self.streaming: if not self._setting_save_in_progress and self.streaming:
@ -254,7 +243,7 @@ class StreamingPiCamera2(BaseCamera):
_exposure_time: int = 500 _exposure_time: int = 500
@lt.thing_setting @lt.setting
def exposure_time(self) -> int: def exposure_time(self) -> int:
"""The camera exposure time in microseconds. """The camera exposure time in microseconds.
@ -296,7 +285,7 @@ class StreamingPiCamera2(BaseCamera):
_sensor_modes = None _sensor_modes = None
@lt.thing_property @lt.property
def sensor_modes(self) -> list[SensorMode]: def sensor_modes(self) -> list[SensorMode]:
"""All the available modes the current sensor supports.""" """All the available modes the current sensor supports."""
if not self._sensor_modes: if not self._sensor_modes:
@ -306,7 +295,7 @@ class StreamingPiCamera2(BaseCamera):
_sensor_mode: Optional[dict] = None _sensor_mode: Optional[dict] = None
@lt.thing_property @lt.property
def sensor_mode(self) -> Optional[SensorModeSelector]: def sensor_mode(self) -> Optional[SensorModeSelector]:
"""The intended sensor mode of the camera.""" """The intended sensor mode of the camera."""
if self._sensor_mode is None: if self._sensor_mode is None:
@ -329,13 +318,13 @@ class StreamingPiCamera2(BaseCamera):
with self._streaming_picamera(pause_stream=True): with self._streaming_picamera(pause_stream=True):
pass pass
@lt.thing_property @lt.property
def sensor_resolution(self) -> Optional[tuple[int, int]]: def sensor_resolution(self) -> Optional[tuple[int, int]]:
"""The native resolution of the camera's sensor.""" """The native resolution of the camera's sensor."""
with self._streaming_picamera() as cam: with self._streaming_picamera() as cam:
return cam.sensor_resolution return cam.sensor_resolution
tuning = lt.ThingSetting(Optional[dict], None, readonly=True) tuning: Optional[dict] = lt.setting(default=None, readonly=True)
"""The Raspberry PiCamera Tuning File JSON.""" """The Raspberry PiCamera Tuning File JSON."""
def _initialise_picamera(self, check_sensor_model: bool = False) -> None: def _initialise_picamera(self, check_sensor_model: bool = False) -> None:
@ -437,7 +426,7 @@ class StreamingPiCamera2(BaseCamera):
cam.close() cam.close()
del self._picamera del self._picamera
@lt.thing_action @lt.action
def start_streaming( def start_streaming(
self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 6 self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 6
) -> None: ) -> None:
@ -509,7 +498,7 @@ class StreamingPiCamera2(BaseCamera):
"Started MJPEG stream at %s on port %s", self.stream_resolution, 1 "Started MJPEG stream at %s on port %s", self.stream_resolution, 1
) )
@lt.thing_action @lt.action
def stop_streaming(self, stop_web_stream: bool = True) -> None: def stop_streaming(self, stop_web_stream: bool = True) -> None:
"""Stop the MJPEG stream.""" """Stop the MJPEG stream."""
with self._streaming_picamera() as picam: with self._streaming_picamera() as picam:
@ -529,7 +518,7 @@ class StreamingPiCamera2(BaseCamera):
# Adding a sleep to prevent camera getting confused by rapid commands # Adding a sleep to prevent camera getting confused by rapid commands
time.sleep(self._sensor_info.short_pause) time.sleep(self._sensor_info.short_pause)
@lt.thing_action @lt.action
def discard_frames(self) -> None: def discard_frames(self) -> None:
"""Discard frames so that the next frame captured is fresh.""" """Discard frames so that the next frame captured is fresh."""
with self._streaming_picamera() as cam: with self._streaming_picamera() as cam:
@ -587,7 +576,7 @@ class StreamingPiCamera2(BaseCamera):
else: else:
raise ValueError(f'Unknown stream name "{stream_name}"') raise ValueError(f'Unknown stream name "{stream_name}"')
@lt.thing_action @lt.action
def capture_array( def capture_array(
self, self,
stream_name: Literal["main", "lores", "raw", "full"] = "main", stream_name: Literal["main", "lores", "raw", "full"] = "main",
@ -620,7 +609,7 @@ class StreamingPiCamera2(BaseCamera):
# as array # as array
return np.array(self.capture_image(stream_name, wait)) return np.array(self.capture_image(stream_name, wait))
@lt.thing_property @lt.property
def camera_configuration(self) -> Mapping: def camera_configuration(self) -> Mapping:
"""The "configuration" dictionary of the picamera2 object. """The "configuration" dictionary of the picamera2 object.
@ -635,13 +624,13 @@ class StreamingPiCamera2(BaseCamera):
with self._streaming_picamera() as cam: with self._streaming_picamera() as cam:
return cam.camera_configuration() return cam.camera_configuration()
@lt.thing_property @lt.property
def capture_metadata(self) -> dict: def capture_metadata(self) -> dict:
"""Return the metadata from the camera.""" """Return the metadata from the camera."""
with self._streaming_picamera() as cam: with self._streaming_picamera() as cam:
return cam.capture_metadata() return cam.capture_metadata()
@lt.thing_action @lt.action
def auto_expose_from_minimum( def auto_expose_from_minimum(
self, self,
target_white_level: Optional[int] = None, target_white_level: Optional[int] = None,
@ -671,7 +660,7 @@ class StreamingPiCamera2(BaseCamera):
percentile=percentile, percentile=percentile,
) )
@lt.thing_action @lt.action
def calibrate_lens_shading(self) -> None: def calibrate_lens_shading(self) -> None:
"""Take an image and use it for flat-field correction. """Take an image and use it for flat-field correction.
@ -699,7 +688,7 @@ class StreamingPiCamera2(BaseCamera):
self.colour_gains = tf_utils.get_colour_gains_from_lst(self.tuning) self.colour_gains = tf_utils.get_colour_gains_from_lst(self.tuning)
@lt.thing_property @lt.property
def colour_correction_matrix( def colour_correction_matrix(
self, self,
) -> tuple[float, float, float, float, float, float, float, float, float]: ) -> tuple[float, float, float, float, float, float, float, float, float]:
@ -724,7 +713,7 @@ class StreamingPiCamera2(BaseCamera):
with self._streaming_picamera(pause_stream=True): with self._streaming_picamera(pause_stream=True):
self._initialise_picamera() self._initialise_picamera()
@lt.thing_action @lt.action
def reset_ccm(self) -> None: def reset_ccm(self) -> None:
"""Overwrite the colour correction matrix in camera tuning with default values.""" """Overwrite the colour correction matrix in camera tuning with default values."""
self.tuning = tf_utils.copy_algo_from_other_tuning( self.tuning = tf_utils.copy_algo_from_other_tuning(
@ -733,7 +722,7 @@ class StreamingPiCamera2(BaseCamera):
copy_from=self.default_tuning, copy_from=self.default_tuning,
) )
@lt.thing_action @lt.action
def set_static_green_equalisation(self, offset: int = 65535) -> None: def set_static_green_equalisation(self, offset: int = 65535) -> None:
"""Set the green equalisation to a static value. """Set the green equalisation to a static value.
@ -748,7 +737,7 @@ class StreamingPiCamera2(BaseCamera):
self.tuning = tf_utils.set_static_geq(self.tuning, offset) self.tuning = tf_utils.set_static_geq(self.tuning, offset)
self._initialise_picamera() self._initialise_picamera()
@lt.thing_action @lt.action
def set_ce_enable_to_off(self) -> None: def set_ce_enable_to_off(self) -> None:
"""Set the contrast enhancement to disabled. """Set the contrast enhancement to disabled.
@ -759,7 +748,7 @@ class StreamingPiCamera2(BaseCamera):
self.tuning = tf_utils.set_ce_to_disabled(self.tuning) self.tuning = tf_utils.set_ce_to_disabled(self.tuning)
self._initialise_picamera() self._initialise_picamera()
@lt.thing_action @lt.action
def full_auto_calibrate(self, portal: lt.deps.BlockingPortal) -> None: def full_auto_calibrate(self, portal: lt.deps.BlockingPortal) -> None:
"""Perform a full auto-calibration. """Perform a full auto-calibration.
@ -787,7 +776,7 @@ class StreamingPiCamera2(BaseCamera):
pass pass
raise RuntimeError("Couldn't set background") raise RuntimeError("Couldn't set background")
@lt.thing_property @lt.property
def primary_calibration_actions(self) -> list[ActionButton]: def primary_calibration_actions(self) -> list[ActionButton]:
"""The calibration actions for both calibration wizard and settings panel.""" """The calibration actions for both calibration wizard and settings panel."""
return [ return [
@ -805,7 +794,7 @@ class StreamingPiCamera2(BaseCamera):
), ),
] ]
@lt.thing_property @lt.property
def secondary_calibration_actions(self) -> list[ActionButton]: def secondary_calibration_actions(self) -> list[ActionButton]:
"""The calibration actions that appear only in settings panel.""" """The calibration actions that appear only in settings panel."""
return [ return [
@ -847,7 +836,7 @@ class StreamingPiCamera2(BaseCamera):
), ),
] ]
@lt.thing_property @lt.property
def manual_camera_settings(self) -> list[PropertyControl]: def manual_camera_settings(self) -> list[PropertyControl]:
"""The camera settings to expose as property controls in the settings panel.""" """The camera settings to expose as property controls in the settings panel."""
return [ return [
@ -874,7 +863,7 @@ class StreamingPiCamera2(BaseCamera):
), ),
] ]
@lt.thing_property @lt.property
def lens_shading_tables(self) -> Optional[tf_utils.LensShading]: def lens_shading_tables(self) -> Optional[tf_utils.LensShading]:
"""The current lens shading (i.e. flat-field correction). """The current lens shading (i.e. flat-field correction).
@ -901,7 +890,7 @@ class StreamingPiCamera2(BaseCamera):
) )
self._initialise_picamera() self._initialise_picamera()
@lt.thing_action @lt.action
def flat_lens_shading(self) -> None: def flat_lens_shading(self) -> None:
"""Disable flat-field correction. """Disable flat-field correction.
@ -916,7 +905,7 @@ class StreamingPiCamera2(BaseCamera):
self.tuning = tf_utils.flatten_lst(self.tuning) self.tuning = tf_utils.flatten_lst(self.tuning)
self._initialise_picamera() self._initialise_picamera()
@lt.thing_action @lt.action
def flat_lens_shading_chrominance(self) -> None: def flat_lens_shading_chrominance(self) -> None:
"""Disable flat-field correction for colour only. """Disable flat-field correction for colour only.
@ -928,7 +917,7 @@ class StreamingPiCamera2(BaseCamera):
self.tuning = tf_utils.flatten_lst(self.tuning, keep_luminance=True) self.tuning = tf_utils.flatten_lst(self.tuning, keep_luminance=True)
self._initialise_picamera() self._initialise_picamera()
@lt.thing_action @lt.action
def reset_lens_shading(self) -> None: def reset_lens_shading(self) -> None:
"""Revert to default lens shading settings. """Revert to default lens shading settings.

View file

@ -86,7 +86,7 @@ class SimulatedCamera(BaseCamera):
self.generate_blobs() self.generate_blobs()
self.generate_canvas() self.generate_canvas()
@lt.thing_property @lt.property
def calibration_required(self) -> bool: def calibration_required(self) -> bool:
"""Whether the camera needs calibrating.""" """Whether the camera needs calibrating."""
return not self.background_detector_status.ready return not self.background_detector_status.ready
@ -274,7 +274,7 @@ class SimulatedCamera(BaseCamera):
self._capture_enabled = False self._capture_enabled = False
self._capture_thread.join() self._capture_thread.join()
@lt.thing_action @lt.action
def start_streaming( def start_streaming(
self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 1 self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 1
) -> None: ) -> None:
@ -300,14 +300,14 @@ class SimulatedCamera(BaseCamera):
self._capture_thread = Thread(target=self._capture_frames) self._capture_thread = Thread(target=self._capture_frames)
self._capture_thread.start() self._capture_thread.start()
@lt.thing_property @lt.property
def stream_active(self) -> bool: def stream_active(self) -> bool:
"""Whether the MJPEG stream is active.""" """Whether the MJPEG stream is active."""
if self._capture_enabled and self._capture_thread: if self._capture_enabled and self._capture_thread:
return self._capture_thread.is_alive() return self._capture_thread.is_alive()
return False return False
noise_level = lt.ThingProperty(float, 2.0) noise_level: float = lt.property(default=2.0)
def _capture_frames(self) -> None: def _capture_frames(self) -> None:
portal = lt.get_blocking_portal(self) portal = lt.get_blocking_portal(self)
@ -326,14 +326,14 @@ class SimulatedCamera(BaseCamera):
except Exception as e: except Exception as e:
LOGGER.exception(f"Failed to capture frame: {e}, retrying...") LOGGER.exception(f"Failed to capture frame: {e}, retrying...")
@lt.thing_action @lt.action
def discard_frames(self) -> None: def discard_frames(self) -> None:
"""Discard frames so that the next frame captured is fresh. """Discard frames so that the next frame captured is fresh.
There is nothing to do as this is a simulation! There is nothing to do as this is a simulation!
""" """
@lt.thing_action @lt.action
def capture_array( def capture_array(
self, self,
stream_name: Literal["main", "full"] = "full", stream_name: Literal["main", "full"] = "full",
@ -370,7 +370,7 @@ class SimulatedCamera(BaseCamera):
LOGGER.warning(f"Simulation camera camera doesn't respect {stream_name=}") LOGGER.warning(f"Simulation camera camera doesn't respect {stream_name=}")
return self.generate_frame() return self.generate_frame()
@lt.thing_action @lt.action
def full_auto_calibrate(self, portal: lt.deps.BlockingPortal) -> None: def full_auto_calibrate(self, portal: lt.deps.BlockingPortal) -> None:
"""Perform a full auto-calibration. """Perform a full auto-calibration.
@ -386,21 +386,21 @@ class SimulatedCamera(BaseCamera):
time.sleep(0.2) time.sleep(0.2)
self.load_sample() self.load_sample()
@lt.thing_action @lt.action
def remove_sample(self) -> None: def remove_sample(self) -> None:
"""Show the simulated background with no sample.""" """Show the simulated background with no sample."""
if not self._show_sample: if not self._show_sample:
raise RuntimeError("Sample is already removed.") raise RuntimeError("Sample is already removed.")
self._show_sample = False self._show_sample = False
@lt.thing_action @lt.action
def load_sample(self) -> None: def load_sample(self) -> None:
"""Show the simulated sample.""" """Show the simulated sample."""
if self._show_sample: if self._show_sample:
raise RuntimeError("Sample is already in place.") raise RuntimeError("Sample is already in place.")
self._show_sample = True self._show_sample = True
@lt.thing_property @lt.property
def primary_calibration_actions(self) -> list[ActionButton]: def primary_calibration_actions(self) -> list[ActionButton]:
"""The calibration actions for both calibration wizard and settings panel.""" """The calibration actions for both calibration wizard and settings panel."""
return [ return [
@ -409,7 +409,7 @@ class SimulatedCamera(BaseCamera):
), ),
] ]
@lt.thing_property @lt.property
def secondary_calibration_actions(self) -> list[ActionButton]: def secondary_calibration_actions(self) -> list[ActionButton]:
"""The calibration actions that appear only in settings panel.""" """The calibration actions that appear only in settings panel."""
return [ return [
@ -417,7 +417,7 @@ class SimulatedCamera(BaseCamera):
action_button_for(self.remove_sample, submit_label="Remove Sample"), action_button_for(self.remove_sample, submit_label="Remove Sample"),
] ]
@lt.thing_property @lt.property
def manual_camera_settings(self) -> list[PropertyControl]: def manual_camera_settings(self) -> list[PropertyControl]:
"""The camera settings to expose as property controls in the settings panel.""" """The camera settings to expose as property controls in the settings panel."""
return [property_control_for(self, "noise_level", label="Noise Level")] return [property_control_for(self, "noise_level", label="Noise Level")]

View file

@ -118,7 +118,7 @@ class CameraStageMapper(lt.Thing):
override the ``get_xyz_position()`` and ``move_to_xyz_position()`` methods. override the ``get_xyz_position()`` and ``move_to_xyz_position()`` methods.
""" """
@lt.thing_action @lt.action
def calibrate_1d( def calibrate_1d(
self, self,
camera: CameraClient, camera: CameraClient,
@ -154,7 +154,7 @@ class CameraStageMapper(lt.Thing):
result["image_resolution"] = camera.capture_downsampled_array().shape[:2] result["image_resolution"] = camera.capture_downsampled_array().shape[:2]
return result return result
@lt.thing_action @lt.action
def calibrate_xy( def calibrate_xy(
self, camera: CameraClient, stage: Stage, logger: lt.deps.InvocationLogger self, camera: CameraClient, stage: Stage, logger: lt.deps.InvocationLogger
) -> DenumpifyingDict: ) -> DenumpifyingDict:
@ -200,7 +200,7 @@ class CameraStageMapper(lt.Thing):
return data return data
@lt.thing_property @lt.property
def image_to_stage_displacement_matrix( def image_to_stage_displacement_matrix(
self, self,
) -> Optional[List[List[float]]]: # 2x2 integer array ) -> Optional[List[List[float]]]: # 2x2 integer array
@ -230,19 +230,17 @@ class CameraStageMapper(lt.Thing):
] ]
return np.array(displacement_matrix).tolist() return np.array(displacement_matrix).tolist()
last_calibration = lt.ThingSetting( last_calibration: Optional[dict] = lt.setting(default=None, readonly=True)
initial_value=None, model=Optional[dict], readonly=True
)
"""The most recent CSM calibration.""" """The most recent CSM calibration."""
@lt.thing_property @lt.property
def image_resolution(self) -> Optional[Tuple[float, float]]: def image_resolution(self) -> Optional[Tuple[float, float]]:
"""The image size used to calibrate the image_to_stage_displacement_matrix.""" """The image size used to calibrate the image_to_stage_displacement_matrix."""
if self.last_calibration is None: if self.last_calibration is None:
return None return None
return self.last_calibration["image_resolution"] return self.last_calibration["image_resolution"]
@lt.thing_property @lt.property
def calibration_required(self) -> bool: def calibration_required(self) -> bool:
"""Whether the camera stage mapper needs calibrating.""" """Whether the camera stage mapper needs calibrating."""
return self.image_to_stage_displacement_matrix is None return self.image_to_stage_displacement_matrix is None
@ -254,7 +252,7 @@ class CameraStageMapper(lt.Thing):
# added by CSMUncalibratedError # added by CSMUncalibratedError
raise CSMUncalibratedError() # noqa: RSE102 raise CSMUncalibratedError() # noqa: RSE102
@lt.thing_action @lt.action
def move_in_image_coordinates( def move_in_image_coordinates(
self, self,
stage: Stage, stage: Stage,
@ -275,7 +273,7 @@ class CameraStageMapper(lt.Thing):
self.assert_calibrated() self.assert_calibrated()
stage.move_relative(**self.convert_image_to_stage_coordinates(x=x, y=y)) stage.move_relative(**self.convert_image_to_stage_coordinates(x=x, y=y))
@lt.thing_action @lt.action
def convert_image_to_stage_coordinates( def convert_image_to_stage_coordinates(
self, x: float, y: float, **_kwargs: float self, x: float, y: float, **_kwargs: float
) -> Mapping[str, int]: ) -> Mapping[str, int]:
@ -283,7 +281,7 @@ class CameraStageMapper(lt.Thing):
self.assert_calibrated() self.assert_calibrated()
return csm_img_to_stage(self.image_to_stage_displacement_matrix, x=x, y=y) return csm_img_to_stage(self.image_to_stage_displacement_matrix, x=x, y=y)
@lt.thing_action @lt.action
def convert_stage_to_image_coordinates( def convert_stage_to_image_coordinates(
self, x: int, y: int, **_kwargs: int self, x: int, y: int, **_kwargs: int
) -> Mapping[str, float]: ) -> Mapping[str, float]:
@ -291,7 +289,7 @@ class CameraStageMapper(lt.Thing):
self.assert_calibrated() self.assert_calibrated()
return csm_stage_to_img(self.image_to_stage_displacement_matrix, x=x, y=y) return csm_stage_to_img(self.image_to_stage_displacement_matrix, x=x, y=y)
@lt.thing_property @lt.property
def thing_state(self) -> Mapping[str, Any]: def thing_state(self) -> Mapping[str, Any]:
"""Summary metadata describing the current state of the Thing.""" """Summary metadata describing the current state of the Thing."""
return { return {

View file

@ -117,7 +117,7 @@ class SmartScanThing(lt.Thing):
self._latest_scan_name: Optional[str] = None self._latest_scan_name: Optional[str] = None
# Scan logger is the invocation logger labthings-fastapi creates # Scan logger is the invocation logger labthings-fastapi creates
# when the `sample_scan` lt.thing_action is called. It is saved as # when the `sample_scan` lt.action is called. It is saved as
# private class variable along with many others here. # private class variable along with many others here.
# Access to these variables requires a scan to be running, # Access to these variables requires a scan to be running,
# any method that calls these should be decorated with # any method that calls these should be decorated with
@ -133,7 +133,7 @@ class SmartScanThing(lt.Thing):
self._scan_data: Optional[scan_directories.ScanData] = None self._scan_data: Optional[scan_directories.ScanData] = None
self._preview_stitcher: Optional[stitching.PreviewStitcher] = None self._preview_stitcher: Optional[stitching.PreviewStitcher] = None
@lt.thing_action @lt.action
def sample_scan( def sample_scan(
self, self,
cancel: lt.deps.CancelHook, cancel: lt.deps.CancelHook,
@ -231,7 +231,7 @@ class SmartScanThing(lt.Thing):
"of motion." "of motion."
) )
@lt.thing_property @lt.property
def latest_scan_name(self) -> Optional[str]: def latest_scan_name(self) -> Optional[str]:
"""The name of the last scan to be started.""" """The name of the last scan to be started."""
return self._latest_scan_name return self._latest_scan_name
@ -546,48 +546,27 @@ class SmartScanThing(lt.Thing):
raise HTTPException(404, "File not found") raise HTTPException(404, "File not found")
return FileResponse(preview_path) return FileResponse(preview_path)
save_resolution = lt.ThingSetting( save_resolution: tuple[int, int] = lt.setting(default=(1640, 1232))
initial_value=(1640, 1232),
model=tuple[int, int],
)
"""A tuple of the image resolution to capture.""" """A tuple of the image resolution to capture."""
max_range = lt.ThingSetting( max_range: int = lt.setting(default=45000)
initial_value=45000,
model=int,
)
"""The maximum distance in steps from the centre of the scan.""" """The maximum distance in steps from the centre of the scan."""
stitch_tiff = lt.ThingSetting( stitch_tiff: bool = lt.setting(default=False)
initial_value=False,
model=bool,
)
"""Whether or not to also produce a pyramidal tiff at the end of a scan.""" """Whether or not to also produce a pyramidal tiff at the end of a scan."""
skip_background = lt.ThingSetting( skip_background: bool = lt.setting(default=True)
initial_value=True,
model=bool,
)
"""Whether to detect and skip empty fields of view. """Whether to detect and skip empty fields of view.
This uses the settings from the ``BackgroundDetectThing``.""" This uses the settings from the ``BackgroundDetectThing``."""
autofocus_dz = lt.ThingSetting( autofocus_dz: int = lt.setting(default=1000)
initial_value=1000,
model=int,
)
"""The z distance to perform an autofocus in steps.""" """The z distance to perform an autofocus in steps."""
overlap = lt.ThingSetting( overlap: float = lt.setting(default=0.45)
initial_value=0.45,
model=float,
)
"""The fraction (0-1) that adjacent images should overlap in x or y.""" """The fraction (0-1) that adjacent images should overlap in x or y."""
stitch_automatically = lt.ThingSetting( stitch_automatically: bool = lt.setting(default=True)
initial_value=True,
model=bool,
)
"""Whether to run a final stitch at the end of a successful scan.""" """Whether to run a final stitch at the end of a successful scan."""
def _get_all_scan_info(self) -> list[scan_directories.ScanInfo]: def _get_all_scan_info(self) -> list[scan_directories.ScanInfo]:
@ -600,7 +579,7 @@ class SmartScanThing(lt.Thing):
ongoing_name = None if self._ongoing_scan is None else self._ongoing_scan.name ongoing_name = None if self._ongoing_scan is None else self._ongoing_scan.name
return self._scan_dir_manager.all_scans_info(ongoing=ongoing_name) return self._scan_dir_manager.all_scans_info(ongoing=ongoing_name)
@lt.thing_property @lt.property
def scans(self) -> ScanListInfo: def scans(self) -> ScanListInfo:
"""All the available scans. """All the available scans.
@ -675,7 +654,7 @@ class SmartScanThing(lt.Thing):
for scan_name in self._scan_dir_manager.all_scans: for scan_name in self._scan_dir_manager.all_scans:
self._delete_scan(scan_name, logger) self._delete_scan(scan_name, logger)
@lt.thing_action @lt.action
def purge_empty_scans(self, logger: lt.deps.InvocationLogger) -> None: def purge_empty_scans(self, logger: lt.deps.InvocationLogger) -> None:
"""Delete all scan folders containing no images at the top level.""" """Delete all scan folders containing no images at the top level."""
# JSON is ignored as it's created before any images are captured # JSON is ignored as it's created before any images are captured
@ -712,7 +691,7 @@ class SmartScanThing(lt.Thing):
scan_name=self.latest_scan_name, filename="preview.jpg", check_exists=True scan_name=self.latest_scan_name, filename="preview.jpg", check_exists=True
) )
@lt.thing_property @lt.property
def latest_preview_stitch_time(self) -> Optional[float]: def latest_preview_stitch_time(self) -> Optional[float]:
"""The modification time of the latest preview image, to allow live updating. """The modification time of the latest preview image, to allow live updating.
@ -746,7 +725,7 @@ class SmartScanThing(lt.Thing):
raise HTTPException(404, "File not found") raise HTTPException(404, "File not found")
return FileResponse(preview_path) return FileResponse(preview_path)
@lt.thing_action @lt.action
def stitch_scan( def stitch_scan(
self, self,
logger: lt.deps.InvocationLogger, logger: lt.deps.InvocationLogger,
@ -757,7 +736,7 @@ class SmartScanThing(lt.Thing):
) -> None: ) -> None:
"""Generate a stitched image based on stage position metadata. """Generate a stitched image based on stage position metadata.
Note that as this is a lt.thing_action it needs the logger passed as Note that as this is a lt.action it needs the logger passed as
a variable if called from another thing action a variable if called from another thing action
""" """
scan_data_dict = self._scan_dir_manager.get_scan_data_dict(scan_name) scan_data_dict = self._scan_dir_manager.get_scan_data_dict(scan_name)
@ -780,7 +759,7 @@ class SmartScanThing(lt.Thing):
except SubprocessError as e: except SubprocessError as e:
self._scan_logger.error(f"Stitching failed: {e}", exc_info=e) self._scan_logger.error(f"Stitching failed: {e}", exc_info=e)
@lt.thing_action @lt.action
def download_zip( def download_zip(
self, self,
scan_name: str, scan_name: str,
@ -792,7 +771,7 @@ class SmartScanThing(lt.Thing):
zip_fname = self._scan_dir_manager.zip_scan(scan_name, final_version=True) zip_fname = self._scan_dir_manager.zip_scan(scan_name, final_version=True)
return ZipBlob.from_file(zip_fname) return ZipBlob.from_file(zip_fname)
@lt.thing_action @lt.action
def stitch_all_scans( def stitch_all_scans(
self, logger: lt.deps.InvocationLogger, cancel: lt.deps.CancelHook self, logger: lt.deps.InvocationLogger, cancel: lt.deps.CancelHook
) -> None: ) -> None:

View file

@ -68,28 +68,21 @@ class BaseStage(lt.Thing):
"_hardware_move_relative and/or _hardware_move_absolute instead." "_hardware_move_relative and/or _hardware_move_absolute instead."
) )
@lt.thing_property @lt.property
def axis_names(self) -> Sequence[str]: def axis_names(self) -> Sequence[str]:
"""The names of the stage's axes, in order.""" """The names of the stage's axes, in order."""
return self._axis_names return self._axis_names
@lt.thing_property @lt.property
def position(self) -> Mapping[str, int]: def position(self) -> Mapping[str, int]:
"""Current position of the stage.""" """Current position of the stage."""
return self._apply_axis_direction(self._hardware_position) return self._apply_axis_direction(self._hardware_position)
moving = lt.ThingProperty( moving: bool = lt.property(bool, default=False, readonly=True, observable=True)
bool,
False,
readonly=True,
observable=True,
)
"""Whether the stage is in motion.""" """Whether the stage is in motion."""
axis_inverted = lt.ThingSetting( axis_inverted: Mapping[str, bool] = lt.setting(
initial_value={"x": False, "y": False, "z": False}, default={"x": False, "y": False, "z": False}, readonly=True
model=Mapping[str, bool],
readonly=True,
) )
"""Used to convert coordinates between the program frame and the hardware frame.""" """Used to convert coordinates between the program frame and the hardware frame."""
@ -124,7 +117,7 @@ class BaseStage(lt.Thing):
"""Summary metadata describing the current state of the stage.""" """Summary metadata describing the current state of the stage."""
return {"position": self.position} return {"position": self.position}
@lt.thing_action @lt.action
def invert_axis_direction(self, axis: Literal["x", "y", "z"]) -> None: def invert_axis_direction(self, axis: Literal["x", "y", "z"]) -> None:
"""Invert the direction setting of the given axis. """Invert the direction setting of the given axis.
@ -138,7 +131,7 @@ class BaseStage(lt.Thing):
raise KeyError(f"The axis {axis} is not defined.") from e raise KeyError(f"The axis {axis} is not defined.") from e
self.axis_inverted = direction self.axis_inverted = direction
@lt.thing_action @lt.action
def move_relative( def move_relative(
self, self,
cancel: lt.deps.CancelHook, cancel: lt.deps.CancelHook,
@ -166,7 +159,7 @@ class BaseStage(lt.Thing):
"StageThings must define their own _hardware_move_relative method" "StageThings must define their own _hardware_move_relative method"
) )
@lt.thing_action @lt.action
def move_absolute( def move_absolute(
self, self,
cancel: lt.deps.CancelHook, cancel: lt.deps.CancelHook,
@ -194,7 +187,7 @@ class BaseStage(lt.Thing):
"StageThings must define their own move_absolute method" "StageThings must define their own move_absolute method"
) )
@lt.thing_action @lt.action
def set_zero_position(self) -> None: def set_zero_position(self) -> None:
"""Make the current position zero in all axes. """Make the current position zero in all axes.
@ -206,7 +199,7 @@ class BaseStage(lt.Thing):
"StageThings must define their own set_zero_position method" "StageThings must define their own set_zero_position method"
) )
@lt.thing_action @lt.action
def get_xyz_position(self) -> tuple[int, int, int]: def get_xyz_position(self) -> tuple[int, int, int]:
"""Return a tuple containing (x, y, z) position. """Return a tuple containing (x, y, z) position.
@ -217,7 +210,7 @@ class BaseStage(lt.Thing):
position_dict = self.position position_dict = self.position
return (position_dict["x"], position_dict["y"], position_dict["z"]) return (position_dict["x"], position_dict["y"], position_dict["z"])
@lt.thing_action @lt.action
def move_to_xyz_position( def move_to_xyz_position(
self, cancel: lt.deps.CancelHook, xyz_pos: tuple[int, int, int] self, cancel: lt.deps.CancelHook, xyz_pos: tuple[int, int, int]
) -> None: ) -> None:

View file

@ -43,10 +43,8 @@ class DummyStage(BaseStage):
) -> None: ) -> None:
"""Nothing to do when the Thing context manager is closed.""" """Nothing to do when the Thing context manager is closed."""
axis_inverted = lt.ThingSetting( axis_inverted: Mapping[str, bool] = lt.setting(
initial_value={"x": True, "y": False, "z": False}, default={"x": True, "y": False, "z": False}, readonly=True
model=Mapping[str, bool],
readonly=True,
) )
"""Used to convert coordinates between the program frame and the hardware frame.""" """Used to convert coordinates between the program frame and the hardware frame."""
@ -104,7 +102,7 @@ class DummyStage(BaseStage):
cancel, block_cancellation=block_cancellation, **displacement cancel, block_cancellation=block_cancellation, **displacement
) )
@lt.thing_action @lt.action
def set_zero_position(self) -> None: def set_zero_position(self) -> None:
"""Make the current position zero in all axes. """Make the current position zero in all axes.

View file

@ -78,10 +78,8 @@ class SangaboardThing(BaseStage):
with self._sangaboard_lock: with self._sangaboard_lock:
yield self._sangaboard yield self._sangaboard
axis_inverted = lt.ThingSetting( axis_inverted: Mapping[str, bool] = lt.setting(
initial_value={"x": True, "y": False, "z": True}, default={"x": True, "y": False, "z": True}, readonly=True
model=Mapping[str, bool],
readonly=True,
) )
"""Used to convert coordinates between the program frame and the hardware frame.""" """Used to convert coordinates between the program frame and the hardware frame."""
@ -164,7 +162,7 @@ class SangaboardThing(BaseStage):
cancel, block_cancellation=block_cancellation, **displacement cancel, block_cancellation=block_cancellation, **displacement
) )
@lt.thing_action @lt.action
def set_zero_position(self) -> None: def set_zero_position(self) -> None:
"""Make the current position zero in all axes. """Make the current position zero in all axes.
@ -176,7 +174,7 @@ class SangaboardThing(BaseStage):
sb.zero_position() sb.zero_position()
self.update_position() self.update_position()
@lt.thing_action @lt.action
def flash_led( def flash_led(
self, self,
number_of_flashes: int = 10, number_of_flashes: int = 10,

View file

@ -136,9 +136,7 @@ class ParasiticMotionError(Exception):
class RangeofMotionThing(lt.Thing): class RangeofMotionThing(lt.Thing):
"""A class used to measure the range of motion of the stage in X and Y.""" """A class used to measure the range of motion of the stage in X and Y."""
calibrated_range = lt.ThingSetting( calibrated_range: int = lt.setting(default=None, readonly=True)
initial_value=None, model=Optional[list[int, int]], readonly=True
)
def __init__(self) -> None: def __init__(self) -> None:
"""Initialise and create the lock.""" """Initialise and create the lock."""
@ -147,7 +145,7 @@ class RangeofMotionThing(lt.Thing):
self._stream_resolution: Optional[tuple[int, int]] = None self._stream_resolution: Optional[tuple[int, int]] = None
self._rom_data = RomDataTracker() self._rom_data = RomDataTracker()
@lt.thing_action @lt.action
def perform_rom_test( def perform_rom_test(
self, self,
autofocus: AutofocusDep, autofocus: AutofocusDep,
@ -214,7 +212,7 @@ class RangeofMotionThing(lt.Thing):
"Step Range": step_range, "Step Range": step_range,
} }
@lt.thing_action @lt.action
def perform_recentre( def perform_recentre(
self, self,
autofocus: AutofocusDep, autofocus: AutofocusDep,

View file

@ -43,7 +43,7 @@ class OpenFlexureSystem(lt.Thing):
_microscope_id: Optional[str] = None _microscope_id: Optional[str] = None
@lt.thing_setting @lt.setting
def microscope_id(self) -> UUID: def microscope_id(self) -> UUID:
"""A unique identifier for this microscope.""" """A unique identifier for this microscope."""
if self._microscope_id is None: if self._microscope_id is None:
@ -54,14 +54,14 @@ class OpenFlexureSystem(lt.Thing):
def microscope_id(self, uuid: UUID) -> None: def microscope_id(self, uuid: UUID) -> None:
self._microscope_id = uuid self._microscope_id = uuid
@lt.thing_property @lt.property
def hostname(self) -> str: def hostname(self) -> str:
"""The hostname of the microscope, as reported by its operating system.""" """The hostname of the microscope, as reported by its operating system."""
return socket.gethostname() return socket.gethostname()
_version_data: Optional[VersionData] = None _version_data: Optional[VersionData] = None
@lt.thing_property @lt.property
def version_data(self) -> VersionData: def version_data(self) -> VersionData:
"""The version string and version source for the server. """The version string and version source for the server.
@ -76,12 +76,12 @@ class OpenFlexureSystem(lt.Thing):
self._version_data = robust_version_strings() self._version_data = robust_version_strings()
return self._version_data return self._version_data
@lt.thing_property @lt.property
def is_raspberrypi(self) -> bool: def is_raspberrypi(self) -> bool:
"""Return True if running on a Raspberry Pi.""" """Return True if running on a Raspberry Pi."""
return os.path.exists("/usr/bin/raspi-config") return os.path.exists("/usr/bin/raspi-config")
@lt.thing_action @lt.action
def shutdown(self) -> CommandOutput: def shutdown(self) -> CommandOutput:
"""Attempt to shutdown the device.""" """Attempt to shutdown the device."""
if not self.is_raspberrypi: if not self.is_raspberrypi:
@ -103,7 +103,7 @@ class OpenFlexureSystem(lt.Thing):
out, err = p.communicate() out, err = p.communicate()
return CommandOutput(output=out, error=err) return CommandOutput(output=out, error=err)
@lt.thing_action @lt.action
def reboot(self) -> CommandOutput: def reboot(self) -> CommandOutput:
"""Attempt to reboot the device.""" """Attempt to reboot the device."""
if not self.is_raspberrypi: if not self.is_raspberrypi:
@ -120,7 +120,7 @@ class OpenFlexureSystem(lt.Thing):
out, err = p.communicate() out, err = p.communicate()
return CommandOutput(output=out, error=err) return CommandOutput(output=out, error=err)
@lt.thing_action @lt.action
def get_things_state(self, metadata_getter: lt.deps.GetThingStates) -> Mapping: def get_things_state(self, metadata_getter: lt.deps.GetThingStates) -> Mapping:
"""Metadata summarising the current state of all Things in the server.""" """Metadata summarising the current state of all Things in the server."""
return metadata_getter() return metadata_getter()

View file

@ -63,7 +63,7 @@ def test_override_base_movement():
""" """
class BadStage1(BaseStage): class BadStage1(BaseStage):
@lt.thing_action @lt.action
def move_relative( def move_relative(
self, self,
cancel: lt.deps.CancelHook, cancel: lt.deps.CancelHook,
@ -76,7 +76,7 @@ def test_override_base_movement():
BadStage1() BadStage1()
class BadStage2(BaseStage): class BadStage2(BaseStage):
@lt.thing_action @lt.action
def move_absolute( def move_absolute(
self, self,
cancel: lt.deps.CancelHook, cancel: lt.deps.CancelHook,