Convert Stage to a Protocol

The Stage is now implemented as a runtime-checkable Protocol.

Part of this led to the realisation that we can't create a DirectThingClient
based on a Protocol, only on a concrete Thing. While it might be
a good idea to address that in LabThings in due course, for now
we work around it by creating both a Protocol and a stub class. The stub class implements the protocol, and
can be used to declare a dependency.
This commit is contained in:
Richard Bowman 2024-11-29 12:53:04 +00:00
parent 2e2e877d75
commit a0fbbc3e40
7 changed files with 77 additions and 25 deletions

View file

@ -4,11 +4,10 @@ import logging
from labthings_fastapi.thing import Thing from labthings_fastapi.thing import Thing
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
from labthings_fastapi.decorators import thing_action from labthings_fastapi.decorators import thing_action
from .stage import Stage from .stage import StageDependency as StageDep
from openflexure_microscope_server.things.autofocus import AutofocusThing from openflexure_microscope_server.things.autofocus import AutofocusThing
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
StageDep = direct_thing_client_dependency(Stage, "/stage/")
CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/") CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/")
AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/") AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/")

View file

@ -21,11 +21,10 @@ from labthings_fastapi.decorators import thing_action
from labthings_fastapi.types.numpy import NDArray from labthings_fastapi.types.numpy import NDArray
from .camera import RawCameraDependency as Camera from .camera import RawCameraDependency as Camera
from .camera import CameraDependency as WrappedCamera from .camera import CameraDependency as WrappedCamera
from .stage import Stage as StageThing from .stage import StageDependency as Stage
import numpy as np import numpy as np
from pydantic import BaseModel from pydantic import BaseModel
Stage = direct_thing_client_dependency(StageThing, "/stage/")
### Autofocus utilities ### Autofocus utilities

View file

@ -25,7 +25,7 @@ from labthings_fastapi.types.numpy import NDArray
from labthings_fastapi.server import ThingServer from labthings_fastapi.server import ThingServer
from . import BaseCamera, JPEGBlob from . import BaseCamera, JPEGBlob
from ..stage import Stage from ..stage import StageProtocol as Stage
class SimulatedCamera(BaseCamera): class SimulatedCamera(BaseCamera):

View file

@ -21,17 +21,13 @@ from camera_stage_mapping.camera_stage_calibration_1d import (
calibrate_backlash_1d, calibrate_backlash_1d,
image_to_stage_displacement_from_1d, image_to_stage_displacement_from_1d,
) )
from camera_stage_mapping.camera_stage_tracker import Tracker
from .camera import CameraDependency as Camera
from .stage import Stage as StageThing
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
from labthings_fastapi.dependencies.invocation import InvocationCancelledError, InvocationLogger from labthings_fastapi.dependencies.invocation import InvocationCancelledError, InvocationLogger
from labthings_fastapi.types.numpy import NDArray, denumpify, DenumpifyingDict from labthings_fastapi.types.numpy import NDArray, denumpify, DenumpifyingDict
from labthings_fastapi.decorators import thing_action, thing_property from labthings_fastapi.decorators import thing_action, thing_property
from labthings_fastapi.thing import Thing from labthings_fastapi.thing import Thing
from camera_stage_mapping.camera_stage_tracker import Tracker
Stage = direct_thing_client_dependency(StageThing, "/stage/") from .camera import CameraDependency as Camera
from .stage import StageDependency as Stage
CoordinateType = Tuple[float, float, float] CoordinateType = Tuple[float, float, float]
XYCoordinateType = Tuple[float, float] XYCoordinateType = Tuple[float, float]

View file

@ -29,13 +29,12 @@ from labthings_fastapi.dependencies.invocation import CancelHook, InvocationLogg
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 labthings_fastapi.outputs.blob import blob_type
from .camera import CameraDependency as CamDep from .camera import CameraDependency as CamDep
from .stage import Stage from .stage import StageDependency as StageDep
from openflexure_microscope_server.things.autofocus import AutofocusThing from openflexure_microscope_server.things.autofocus import AutofocusThing
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
from openflexure_microscope_server.things.auto_recentre_stage import RecentringThing from openflexure_microscope_server.things.auto_recentre_stage import RecentringThing
StageDep = direct_thing_client_dependency(Stage, "/stage/")
CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/") CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/")
AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/") AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/")
RecentreStage = direct_thing_client_dependency(RecentringThing, "/auto_recentre_stage/") RecentreStage = direct_thing_client_dependency(RecentringThing, "/auto_recentre_stage/")

View file

@ -1,16 +1,63 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Protocol, TypeAlias, runtime_checkable
from labthings_fastapi.descriptors.property import PropertyDescriptor from labthings_fastapi.descriptors.property import PropertyDescriptor
from labthings_fastapi.thing import Thing from labthings_fastapi.thing import Thing
from labthings_fastapi.decorators import thing_action, thing_property from labthings_fastapi.decorators import thing_action, thing_property
from labthings_fastapi.dependencies.invocation import CancelHook from labthings_fastapi.dependencies.invocation import CancelHook
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
from collections.abc import Sequence, Mapping from collections.abc import Sequence, Mapping
@runtime_checkable
class StageProtocol(Protocol):
"""A protocol for the OpenFlexure translation stage
"""
_axis_names: Sequence[str]
class Stage(Thing): @property
"""A dummy stage for testing purposes def axis_names(self) -> Sequence[str]:
"""The names of the stage's axes, in order."""
...
@property
def position(self) -> Mapping[str, int]:
"""Current position of the stage"""
...
@property
def moving() -> bool:
"Whether the stage is in motion"
...
@property
def thing_state(self) -> Mapping[str, Any]:
"""Summary metadata describing the current state of the stage"""
...
This stage should work similarly to a Sangaboard stage, but without any def move_relative(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]):
hardware attached. """Make a relative move. Keyword arguments should be axis names."""
...
def move_absolute(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]):
"""Make an absolute move. Keyword arguments should be axis names."""
...
def set_zero_position(self):
"""Make the current position zero in all axes
This action does not move the stage, but resets the position to zero.
It is intended for use after manually or automatically recentring the
stage.
"""
...
class BaseStage(Thing):
"""A base stage class for OpenFlexure translation stages
This can't be used directly but should reduce boilerplate code when
implementing new stages. A minimal working stage must implement
`move_relative` and `move_absolute` actions, which update the
`position` property on completion, and provide `set_zero_position`.
""" """
_axis_names = ("x", "y", "z") _axis_names = ("x", "y", "z")
@ -41,17 +88,26 @@ class Stage(Thing):
return { return {
"position": self.position "position": self.position
} }
class StageStub(BaseStage):
"""A Stub Thing for the OpenFlexure translation stage
As of LabThings-FastAPI 0.0.7, we can't make a thing client dependency
based on a protocol, because the protocol is not a Thing, and its
methods/properties are not decorated as Affordances. This stub class
is a workaround for that limitation, and should not be used directly.
"""
@thing_action @thing_action
def move_relative(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]): def move_relative(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]):
"""Make a relative move. Keyword arguments should be axis names.""" """Make a relative move. Keyword arguments should be axis names."""
raise NotImplementedError("Subclasses should implement this method") raise NotImplementedError("StageStub should not be used directly")
@thing_action @thing_action
def move_absolute(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]): def move_absolute(self, cancel: CancelHook, block_cancellation: bool=False, **kwargs: Mapping[str, int]):
"""Make an absolute move. Keyword arguments should be axis names.""" """Make an absolute move. Keyword arguments should be axis names."""
raise NotImplementedError("Subclasses should implement this method") raise NotImplementedError("StageStub should not be used directly")
@thing_action @thing_action
def set_zero_position(self): def set_zero_position(self):
"""Make the current position zero in all axes """Make the current position zero in all axes
@ -60,4 +116,7 @@ class Stage(Thing):
It is intended for use after manually or automatically recentring the It is intended for use after manually or automatically recentring the
stage. stage.
""" """
raise NotImplementedError("Subclasses should implement this method") raise NotImplementedError("StageStub should not be used directly")
StageDependency: TypeAlias = direct_thing_client_dependency(StageStub, "/stage/")

View file

@ -4,10 +4,10 @@ from labthings_fastapi.dependencies.invocation import CancelHook, InvocationCanc
from collections.abc import Mapping from collections.abc import Mapping
import time import time
from . import Stage from . import BaseStage
class DummyStage(Stage): class DummyStage(BaseStage):
"""A dummy stage for testing purposes """A dummy stage for testing purposes
This stage should work similarly to a Sangaboard stage, but without any This stage should work similarly to a Sangaboard stage, but without any