Update Thing Settings to use the new format provided by PR #110 of Labthings FastAPI
This commit is contained in:
parent
fd4d51401f
commit
840ed7f20e
10 changed files with 169 additions and 203 deletions
|
|
@ -19,7 +19,8 @@ from pydantic import BaseModel
|
|||
|
||||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.dependencies.blocking_portal import BlockingPortal
|
||||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
from labthings_fastapi.decorators import thing_action
|
||||
from labthings_fastapi.descriptors import ThingSetting
|
||||
from labthings_fastapi.types.numpy import NDArray
|
||||
|
||||
from .camera import RawCameraDependency as Camera
|
||||
|
|
@ -396,37 +397,36 @@ class AutofocusThing(Thing):
|
|||
stage.move_absolute(z=peak_height)
|
||||
return heights.tolist(), sizes.tolist()
|
||||
|
||||
@thing_property
|
||||
def stack_images_to_save(self) -> int:
|
||||
"""The number of images to capture and save in a stack
|
||||
Defaults to 1 unless you need to see either side of focus"""
|
||||
return self.thing_settings.get("stack_images_to_save", 1)
|
||||
stack_images_to_save = ThingSetting(
|
||||
initial_value=1,
|
||||
model=int,
|
||||
description="""The number of images to save in a stack.
|
||||
|
||||
@stack_images_to_save.setter
|
||||
def stack_images_to_save(self, value: int) -> None:
|
||||
self.thing_settings["stack_images_to_save"] = value
|
||||
Defaults to 1 unless you need to see either side of focus""",
|
||||
)
|
||||
|
||||
@thing_property
|
||||
def stack_min_images_to_test(self) -> int:
|
||||
"""The number of images to test for successful focusing in a stack
|
||||
Defaults to 9, which balances reliability and speed"""
|
||||
return self.thing_settings.get("stack_min_images_to_test", 9)
|
||||
stack_min_images_to_test = ThingSetting(
|
||||
initial_value=9,
|
||||
model=int,
|
||||
description="""The minimum number of images to capture in a stack.
|
||||
|
||||
@stack_min_images_to_test.setter
|
||||
def stack_min_images_to_test(self, value: int) -> None:
|
||||
self.thing_settings["stack_min_images_to_test"] = value
|
||||
This many images are captures and tested for focus, if the focus
|
||||
is not central enough more images may be captured. After new images
|
||||
are captured the number sets the number of images used for checking
|
||||
if focus is central.
|
||||
|
||||
@thing_property
|
||||
def stack_dz(self) -> int:
|
||||
"""Space in steps between images in a z-stack
|
||||
Suggested is 50 for 60-100x
|
||||
100 for 40x
|
||||
200 for 20x"""
|
||||
return self.thing_settings.get("stack_dz", 50)
|
||||
Defaults to 9 which balances reliability and speed
|
||||
""",
|
||||
)
|
||||
|
||||
@stack_dz.setter
|
||||
def stack_dz(self, value: int) -> None:
|
||||
self.thing_settings["stack_dz"] = value
|
||||
stack_dz = ThingSetting(
|
||||
initial_value=50,
|
||||
model=int,
|
||||
description="""Space in steps between images in a z-stack
|
||||
Suggested is 50 for 60-100x
|
||||
100 for 40x
|
||||
200 for 20x""",
|
||||
)
|
||||
|
||||
@thing_action
|
||||
def run_smart_stack(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ from pydantic import BaseModel
|
|||
from scipy.stats import norm
|
||||
|
||||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
from labthings_fastapi.decorators import thing_action, thing_setting
|
||||
from labthings_fastapi.descriptors import ThingSetting
|
||||
from .camera import CameraDependency as CamDep
|
||||
|
||||
|
||||
|
|
@ -17,38 +18,44 @@ class ChannelDistributions(BaseModel):
|
|||
|
||||
|
||||
class BackgroundDetectThing(Thing):
|
||||
@thing_property
|
||||
# Requires a getter and a setter to support being a BaseModel but being
|
||||
# saved to file as a dict
|
||||
_background_distributions: Optional[ChannelDistributions] = None
|
||||
|
||||
@thing_setting
|
||||
def background_distributions(self) -> Optional[ChannelDistributions]:
|
||||
"""The statistics of the background image"""
|
||||
bd = self.thing_settings.get("background_distributions", None)
|
||||
if bd:
|
||||
return ChannelDistributions(**bd)
|
||||
return None
|
||||
bd = self._background_distributions
|
||||
if bd is None:
|
||||
return None
|
||||
return ChannelDistributions(**bd)
|
||||
|
||||
@background_distributions.setter
|
||||
def background_distributions(self, value: Optional[ChannelDistributions]) -> None:
|
||||
try:
|
||||
self.thing_settings["background_distributions"] = value.model_dump()
|
||||
except AttributeError:
|
||||
self.thing_settings["background_distributions"] = None
|
||||
def background_distributions(
|
||||
self, value: Optional[ChannelDistributions | dict]
|
||||
) -> None:
|
||||
if value is None:
|
||||
self._background_distributions = None
|
||||
elif isinstance(value, ChannelDistributions):
|
||||
self._background_distributions = value.model_dump()
|
||||
elif isinstance(value, dict):
|
||||
self._background_distributions = value
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Cannot set background_distributions with an object of type {type(value)}"
|
||||
)
|
||||
|
||||
@thing_property
|
||||
def tolerance(self) -> float:
|
||||
"""How many standard deviations to allow for the background"""
|
||||
return self.thing_settings.get("tolerance", 7)
|
||||
tolerance = ThingSetting(
|
||||
initial_value=7.0,
|
||||
model=float,
|
||||
description="How many standard deviations to allow for the background",
|
||||
)
|
||||
|
||||
@tolerance.setter
|
||||
def tolerance(self, value: float) -> None:
|
||||
self.thing_settings["tolerance"] = value
|
||||
|
||||
@thing_property
|
||||
def fraction(self) -> float:
|
||||
"""How much of the image needs to be not background to label as sample"""
|
||||
return self.thing_settings.get("fraction", 25)
|
||||
|
||||
@fraction.setter
|
||||
def fraction(self, value: float) -> None:
|
||||
self.thing_settings["fraction"] = value
|
||||
fraction = ThingSetting(
|
||||
initial_value=25.0,
|
||||
model=float,
|
||||
description="How much of the image needs to be not background to label as sample",
|
||||
)
|
||||
|
||||
def background_mask(self, image: np.ndarray) -> np.ndarray:
|
||||
"""Calculate a binary image, showing whether each pixel is background
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ from tempfile import TemporaryDirectory
|
|||
|
||||
from pydantic import BaseModel, BeforeValidator
|
||||
|
||||
from labthings_fastapi.descriptors.property import PropertyDescriptor
|
||||
from labthings_fastapi.descriptors.property import ThingProperty
|
||||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
from labthings_fastapi.outputs.mjpeg_stream import MJPEGStream
|
||||
from labthings_fastapi.utilities import get_blocking_portal
|
||||
|
|
@ -46,14 +46,12 @@ from . import picamera_recalibrate_utils as recalibrate_utils
|
|||
from . import BaseCamera, JPEGBlob, ArrayModel
|
||||
|
||||
|
||||
class PicameraControl(PropertyDescriptor):
|
||||
class PicameraControl(ThingProperty):
|
||||
def __init__(
|
||||
self, control_name: str, model: type = float, description: Optional[str] = None
|
||||
):
|
||||
"""A property descriptor controlling a picamera control"""
|
||||
PropertyDescriptor.__init__(
|
||||
self, model, observable=False, description=description
|
||||
)
|
||||
super().__init__(model, observable=False, description=description)
|
||||
self.control_name = control_name
|
||||
|
||||
def _getter(self, obj: StreamingPiCamera2):
|
||||
|
|
@ -213,19 +211,19 @@ class StreamingPiCamera2(BaseCamera):
|
|||
except KeyError:
|
||||
pass # If controls are missing, leave at default
|
||||
|
||||
stream_resolution = PropertyDescriptor(
|
||||
stream_resolution = ThingProperty(
|
||||
tuple[int, int],
|
||||
initial_value=(820, 616),
|
||||
description="Resolution to use for the MJPEG stream",
|
||||
)
|
||||
|
||||
mjpeg_bitrate = PropertyDescriptor(
|
||||
mjpeg_bitrate = ThingProperty(
|
||||
Optional[int],
|
||||
initial_value=100000000,
|
||||
description="Bitrate for MJPEG stream (None for default)",
|
||||
)
|
||||
|
||||
stream_active = PropertyDescriptor(
|
||||
stream_active = ThingProperty(
|
||||
bool,
|
||||
initial_value=False,
|
||||
description="Whether the MJPEG stream is active",
|
||||
|
|
@ -284,7 +282,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
with self.picamera() as cam:
|
||||
return cam.sensor_resolution
|
||||
|
||||
tuning = PropertyDescriptor(Optional[dict], None, readonly=True)
|
||||
tuning = ThingProperty(Optional[dict], None, readonly=True)
|
||||
|
||||
def settings_to_properties(self):
|
||||
"""Set the values of properties based on the settings dict"""
|
||||
|
|
|
|||
|
|
@ -115,9 +115,11 @@ class SimulatedCamera(BaseCamera):
|
|||
)
|
||||
return image
|
||||
|
||||
def attach_to_server(self, server: ThingServer, path: str):
|
||||
def attach_to_server(
|
||||
self, server: ThingServer, path: str, setting_storage_path: str
|
||||
):
|
||||
self._server = server
|
||||
return super().attach_to_server(server, path)
|
||||
return super().attach_to_server(server, path, setting_storage_path)
|
||||
|
||||
def get_stage_position(self):
|
||||
if not self._stage and self._server:
|
||||
|
|
|
|||
|
|
@ -37,8 +37,9 @@ from labthings_fastapi.dependencies.invocation import (
|
|||
InvocationCancelledError,
|
||||
InvocationLogger,
|
||||
)
|
||||
from labthings_fastapi.types.numpy import NDArray, denumpify, DenumpifyingDict
|
||||
from labthings_fastapi.types.numpy import NDArray, DenumpifyingDict
|
||||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
from labthings_fastapi.descriptors import ThingSetting
|
||||
from labthings_fastapi.thing import Thing
|
||||
from camera_stage_mapping.camera_stage_tracker import Tracker
|
||||
from .camera import CameraDependency as Camera
|
||||
|
|
@ -181,12 +182,6 @@ class CSMUncalibratedError(HTTPException):
|
|||
class CameraStageMapper(Thing):
|
||||
"""A Thing to manage mapping between image and stage coordinates"""
|
||||
|
||||
def __enter__(self):
|
||||
pass
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.thing_settings.write_to_file()
|
||||
|
||||
@thing_action
|
||||
def calibrate_1d(
|
||||
self,
|
||||
|
|
@ -244,8 +239,6 @@ class CameraStageMapper(Thing):
|
|||
corrected_resolution = tuple(
|
||||
r * hw.grab_image_downsampling for r in cal_x["image_resolution"]
|
||||
)
|
||||
self.thing_settings.update(denumpify(cal_xy))
|
||||
self.thing_settings["image_resolution"] = corrected_resolution
|
||||
|
||||
csm_matrix = cal_xy["image_to_stage_displacement"]
|
||||
csm_as_string = f"[{round(csm_matrix[0][0], 2)}, {round(csm_matrix[0][1], 2)},],[{round(csm_matrix[1][0], 2)}, {round(csm_matrix[1][1], 2)}]"
|
||||
|
|
@ -260,7 +253,7 @@ class CameraStageMapper(Thing):
|
|||
"downsampling": hw.grab_image_downsampling,
|
||||
}
|
||||
|
||||
self.thing_settings["last_calibration"] = DenumpifyingDict(data).model_dump()
|
||||
self.last_calibration = DenumpifyingDict(data).model_dump()
|
||||
|
||||
return data
|
||||
|
||||
|
|
@ -285,15 +278,28 @@ class CameraStageMapper(Thing):
|
|||
)
|
||||
```
|
||||
"""
|
||||
displacement_matrix = self.thing_settings.get("image_to_stage_displacement")
|
||||
if not displacement_matrix:
|
||||
if self.last_calibration is None:
|
||||
return None
|
||||
displacement_matrix = self.last_calibration["camera_stage_mapping_calibration"][
|
||||
"image_to_stage_displacement"
|
||||
]
|
||||
return np.array(displacement_matrix).tolist()
|
||||
|
||||
last_calibration = ThingSetting(
|
||||
initial_value=None,
|
||||
model=Optional[dict],
|
||||
readonly=True,
|
||||
description="The most recent CSM calibration",
|
||||
)
|
||||
|
||||
@thing_property
|
||||
def image_resolution(self) -> Optional[Tuple[float, float]]:
|
||||
"""The image size used to calibrate the image_to_stage_displacement_matrix"""
|
||||
return self.thing_settings.get("image_resolution", None)
|
||||
if self.last_calibration is None:
|
||||
return None
|
||||
return self.last_calibration["camera_stage_mapping_calibration"][
|
||||
"image_resolution"
|
||||
]
|
||||
|
||||
def assert_calibrated(self):
|
||||
"""Raise an exception if the image_to_stage_displacement matrix is not set"""
|
||||
|
|
@ -302,11 +308,6 @@ class CameraStageMapper(Thing):
|
|||
# added by CSMUncalibratedError
|
||||
raise CSMUncalibratedError() # noqa: RSE102
|
||||
|
||||
@thing_property
|
||||
def last_calibration(self) -> Optional[Dict]:
|
||||
"""The results of the last calibration that was run"""
|
||||
return self.thing_settings.get("last_calibration", None)
|
||||
|
||||
@thing_action
|
||||
def move_in_image_coordinates(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ from uuid import UUID, uuid4
|
|||
from fastapi import Depends, HTTPException, Request
|
||||
from labthings_fastapi.dependencies.metadata import GetThingStates
|
||||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
from labthings_fastapi.decorators import thing_action, thing_property, thing_setting
|
||||
from labthings_fastapi.descriptors import ThingSetting
|
||||
from labthings_fastapi.dependencies.thing_server import find_thing_server
|
||||
from labthings_fastapi.server import ThingServer
|
||||
from labthings_fastapi.dependencies.invocation import InvocationLogger
|
||||
|
||||
|
||||
def thing_server_from_request(request: Request) -> ThingServer:
|
||||
|
|
@ -49,10 +49,17 @@ def nested_dict_get(data: dict, key: Sequence[str], create=False) -> Any:
|
|||
|
||||
|
||||
class SettingsManager(Thing):
|
||||
@thing_property
|
||||
def external_metadata(self) -> Mapping:
|
||||
"""External metadata stored in the server's settings"""
|
||||
return self.thing_settings.get("external_metadata", {})
|
||||
external_metadata = ThingSetting(
|
||||
initial_value={},
|
||||
model=Mapping,
|
||||
description="External metadata stored in the server's settings",
|
||||
)
|
||||
|
||||
external_metadata_in_state = ThingSetting(
|
||||
initial_value=[],
|
||||
model=Sequence[str],
|
||||
description='A list of strings that are included in the "state" metadata',
|
||||
)
|
||||
|
||||
@thing_action
|
||||
def update_external_metadata(
|
||||
|
|
@ -75,7 +82,7 @@ class SettingsManager(Thing):
|
|||
if key:
|
||||
subdict = nested_dict_get(subdict, key.split("/"), create=True)
|
||||
recursive_update(subdict, data)
|
||||
self.thing_settings["external_metadata"] = metadata
|
||||
self.external_metadata = metadata
|
||||
|
||||
@thing_action
|
||||
def delete_external_metadata(self, key: str) -> None:
|
||||
|
|
@ -93,14 +100,21 @@ class SettingsManager(Thing):
|
|||
raise HTTPException(
|
||||
status_code=404, detail="The specified key '{key}' was not found"
|
||||
)
|
||||
self.thing_settings["external_metadata"] = metadata
|
||||
self.external_metadata = metadata
|
||||
|
||||
@thing_property
|
||||
_microscope_id: Optional[str] = None
|
||||
|
||||
@thing_setting
|
||||
def microscope_id(self) -> UUID:
|
||||
"""A unique identifier for this microscope"""
|
||||
if "microscope_id" not in self.thing_settings:
|
||||
self.thing_settings["microscope_id"] = str(uuid4())
|
||||
return UUID(self.thing_settings["microscope_id"])
|
||||
if self._microscope_id is None:
|
||||
self._microscope_id = str(uuid4())
|
||||
return UUID(self._microscope_id)
|
||||
|
||||
@microscope_id.setter
|
||||
def microscope_id(self, uuid: UUID):
|
||||
# TODO make read only but still settable from disk
|
||||
self._microscope_id = uuid
|
||||
|
||||
@thing_property
|
||||
def hostname(self) -> str:
|
||||
|
|
@ -112,16 +126,6 @@ class SettingsManager(Thing):
|
|||
"""Metadata summarising the current state of all Things in the server"""
|
||||
return metadata_getter()
|
||||
|
||||
@thing_property
|
||||
def external_metadata_in_state(self) -> Sequence[str]:
|
||||
"""A list of strings that are included in the "state" metadata"""
|
||||
return self.thing_settings.get("external_metadata_in_state", [])
|
||||
|
||||
@external_metadata_in_state.setter
|
||||
def external_metadata_in_state(self, keys: Sequence[str]):
|
||||
"""Set the keys from external metadata that are returned in state"""
|
||||
self.thing_settings["external_metadata_in_state"] = keys
|
||||
|
||||
@property
|
||||
def thing_state(self) -> Mapping:
|
||||
state = {
|
||||
|
|
@ -138,29 +142,3 @@ class SettingsManager(Thing):
|
|||
except KeyError:
|
||||
continue
|
||||
return state
|
||||
|
||||
@thing_action
|
||||
def save_all_thing_settings(
|
||||
self, thing_server: ThingServerDep, logger: InvocationLogger
|
||||
) -> None:
|
||||
"""Ensure all the Things sync their settings to disk.
|
||||
|
||||
Normally, each Thing has a `thing_settings` attribute that can be
|
||||
used to save settings. That dictionary is saved to a JSON file
|
||||
when the server shuts down cleanly.
|
||||
|
||||
This action causes all the `thing_settings` dictionaries to be
|
||||
saved to files immediately, meaning that settings will not be
|
||||
lost in the event of a server crash, power outage, etc.
|
||||
"""
|
||||
for name, thing in thing_server.things.items():
|
||||
try:
|
||||
if thing._labthings_thing_settings:
|
||||
thing._labthings_thing_settings.write_to_file()
|
||||
logger.info(f"Wrote {name} settings to disk.")
|
||||
except PermissionError:
|
||||
logger.warning(
|
||||
f"Could not write {name} settings to disk: permission error."
|
||||
)
|
||||
except FileNotFoundError:
|
||||
logger.warning(f"Could not write {name} settings, folder not found")
|
||||
|
|
|
|||
|
|
@ -14,12 +14,17 @@ from PIL import Image
|
|||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.dependencies.metadata import GetThingStates
|
||||
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
||||
from labthings_fastapi.descriptors import ThingSetting
|
||||
from labthings_fastapi.dependencies.invocation import (
|
||||
CancelHook,
|
||||
InvocationLogger,
|
||||
InvocationCancelledError,
|
||||
)
|
||||
from labthings_fastapi.decorators import thing_action, thing_property, fastapi_endpoint
|
||||
from labthings_fastapi.decorators import (
|
||||
thing_action,
|
||||
thing_property,
|
||||
fastapi_endpoint,
|
||||
)
|
||||
from labthings_fastapi.outputs.blob import blob_type
|
||||
|
||||
from openflexure_microscope_server.utilities import ErrorCapturingThread
|
||||
|
|
@ -590,72 +595,54 @@ class SmartScanThing(Thing):
|
|||
raise HTTPException(404, "File not found")
|
||||
return FileResponse(preview_path)
|
||||
|
||||
@thing_property
|
||||
def save_resolution(self) -> tuple[int, int]:
|
||||
"""A tuple of the image resolution to capture. Should be in a
|
||||
4:3 aspect ratio"""
|
||||
return self.thing_settings.get("save_resolution", ((1640, 1232)))
|
||||
save_resolution = ThingSetting(
|
||||
initial_value=(1640, 1232),
|
||||
model=tuple[int, int],
|
||||
description=("A tuple of the image resolution to capture."),
|
||||
)
|
||||
|
||||
@save_resolution.setter
|
||||
def save_resolution(self, value: tuple[int, int]) -> None:
|
||||
self.thing_settings["save_resolution"] = value
|
||||
max_range = ThingSetting(
|
||||
initial_value=45000,
|
||||
model=int,
|
||||
description=(
|
||||
"The maximum distance from the centre of the scan before we break in steps"
|
||||
),
|
||||
)
|
||||
|
||||
@thing_property
|
||||
def max_range(self) -> int:
|
||||
"""The maximum distance from the centre of the scan before we break in steps"""
|
||||
return self.thing_settings.get("max_range", 45000)
|
||||
stitch_tiff = ThingSetting(
|
||||
initial_value=False,
|
||||
model=bool,
|
||||
description="Whether or not to also produce a pyramidal tiff",
|
||||
)
|
||||
|
||||
@max_range.setter
|
||||
def max_range(self, value: int) -> None:
|
||||
self.thing_settings["max_range"] = value
|
||||
skip_background = ThingSetting(
|
||||
initial_value=True,
|
||||
model=bool,
|
||||
description="""Whether to detect and skip empty fields of view
|
||||
|
||||
@thing_property
|
||||
def stitch_tiff(self) -> bool:
|
||||
"""Whether or not to also produce a pyramidal tiff"""
|
||||
return self.thing_settings.get("stitch_tiff", False)
|
||||
This uses the settings from the `background_detect` Thing.""",
|
||||
)
|
||||
|
||||
@stitch_tiff.setter
|
||||
def stitch_tiff(self, value: bool) -> None:
|
||||
self.thing_settings["stitch_tiff"] = value
|
||||
autofocus_dz = ThingSetting(
|
||||
initial_value=1000,
|
||||
model=int,
|
||||
description="The z distance to perform an autofocus in steps",
|
||||
)
|
||||
|
||||
@thing_property
|
||||
def skip_background(self) -> bool:
|
||||
"""Whether to detect and skip empty fields of view
|
||||
overlap = ThingSetting(
|
||||
initial_value=0.45,
|
||||
model=float,
|
||||
description="The fraction (0-1) that adjacent images should overlap in x or y",
|
||||
)
|
||||
|
||||
This uses the settings from the `background_detect` Thing.
|
||||
"""
|
||||
return self.thing_settings.get("skip_background", True)
|
||||
|
||||
@skip_background.setter
|
||||
def skip_background(self, value: bool) -> None:
|
||||
self.thing_settings["skip_background"] = value
|
||||
|
||||
@thing_property
|
||||
def autofocus_dz(self) -> int:
|
||||
"""The z distance to perform an autofocus in steps"""
|
||||
return self.thing_settings.get("autofocus_dz", 1000)
|
||||
|
||||
@autofocus_dz.setter
|
||||
def autofocus_dz(self, value: int) -> None:
|
||||
self.thing_settings["autofocus_dz"] = value
|
||||
|
||||
@thing_property
|
||||
def overlap(self) -> float:
|
||||
"""The fraction (0-1) that adjacent images should overlap in x or y"""
|
||||
return self.thing_settings.get("overlap", 0.45)
|
||||
|
||||
@overlap.setter
|
||||
def overlap(self, value: float) -> None:
|
||||
self.thing_settings["overlap"] = value
|
||||
|
||||
@thing_property
|
||||
def stitch_automatically(self) -> bool:
|
||||
"""Whether to run a final stitch at the end of the scan (assuming scan success)"""
|
||||
return self.thing_settings.get("stitch_automatically", True)
|
||||
|
||||
@stitch_automatically.setter
|
||||
def stitch_automatically(self, value: bool) -> None:
|
||||
self.thing_settings["stitch_automatically"] = value
|
||||
stitch_automatically = ThingSetting(
|
||||
initial_value=True,
|
||||
model=bool,
|
||||
description=(
|
||||
"Whether to run a final stitch at the end of the scan (assuming scan "
|
||||
"success)"
|
||||
),
|
||||
)
|
||||
|
||||
@thing_property
|
||||
def scans(self) -> list[scan_directories.ScanInfo]:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
|||
from typing import TypeAlias
|
||||
from collections.abc import Sequence, Mapping
|
||||
|
||||
from labthings_fastapi.descriptors.property import PropertyDescriptor
|
||||
from labthings_fastapi.descriptors.property import ThingProperty
|
||||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
from labthings_fastapi.dependencies.invocation import CancelHook
|
||||
|
|
@ -25,7 +25,7 @@ class BaseStage(Thing):
|
|||
"""The names of the stage's axes, in order."""
|
||||
return self._axis_names
|
||||
|
||||
position = PropertyDescriptor(
|
||||
position = ThingProperty(
|
||||
Mapping[str, int],
|
||||
dict.fromkeys(_axis_names, 0),
|
||||
description="Current position of the stage",
|
||||
|
|
@ -33,7 +33,7 @@ class BaseStage(Thing):
|
|||
observable=True,
|
||||
)
|
||||
|
||||
moving = PropertyDescriptor(
|
||||
moving = ThingProperty(
|
||||
bool,
|
||||
False,
|
||||
description="Whether the stage is in motion",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue