And adjust for new as lt import
This commit is contained in:
parent
840ed7f20e
commit
52a80ef66b
17 changed files with 240 additions and 293 deletions
|
|
@ -3,14 +3,16 @@ from __future__ import annotations
|
|||
from typing import Optional
|
||||
from copy import copy
|
||||
|
||||
from labthings_fastapi.server import cli, ThingServer
|
||||
import labthings_fastapi as lt
|
||||
import uvicorn
|
||||
from .serve_static_files import add_static_files
|
||||
from .legacy_api import add_v2_endpoints
|
||||
from ..logging import configure_logging, retrieve_log, retrieve_log_from_file
|
||||
|
||||
|
||||
def customise_server(server: ThingServer, log_folder: str, scans_folder: Optional[str]):
|
||||
def customise_server(
|
||||
server: lt.ThingServer, log_folder: str, scans_folder: Optional[str]
|
||||
):
|
||||
"""Customise the server with additional endpoints, etc."""
|
||||
configure_logging(log_folder)
|
||||
add_v2_endpoints(server)
|
||||
|
|
@ -39,7 +41,7 @@ def _get_scans_dir(config: dict) -> Optional[str]:
|
|||
|
||||
def serve_from_cli(argv: Optional[list[str]] = None):
|
||||
"""Start the server from the command line"""
|
||||
args = cli.parse_args(argv)
|
||||
args = lt.cli.parse_args(argv)
|
||||
|
||||
log_config = copy(uvicorn.config.LOGGING_CONFIG)
|
||||
log_config["loggers"]["uvicorn"]["propagate"] = True
|
||||
|
|
@ -50,10 +52,10 @@ def serve_from_cli(argv: Optional[list[str]] = None):
|
|||
config = None
|
||||
server = None
|
||||
try:
|
||||
config = cli.config_from_args(args)
|
||||
config = lt.cli.config_from_args(args)
|
||||
log_folder = config.get("log_folder", "./openflexure/logs")
|
||||
scans_folder = _get_scans_dir(config)
|
||||
server = cli.server_from_config(config)
|
||||
server = lt.cli.server_from_config(config)
|
||||
customise_server(server, log_folder, scans_folder)
|
||||
uvicorn.run(
|
||||
server.app,
|
||||
|
|
@ -68,7 +70,7 @@ def serve_from_cli(argv: Optional[list[str]] = None):
|
|||
print(f"Error: {e}")
|
||||
fallback_server = "labthings_fastapi.server.fallback:app"
|
||||
print(f"Starting fallback server {fallback_server}.")
|
||||
app = cli.object_reference_to_object(fallback_server)
|
||||
app = lt.cli.object_reference_to_object(fallback_server)
|
||||
app.labthings_config = config
|
||||
app.labthings_server = server
|
||||
app.labthings_error = e
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
from . import ThingServer
|
||||
import labthings_fastapi as lt
|
||||
from fastapi import Response
|
||||
from socket import gethostname
|
||||
|
||||
|
||||
def add_v2_endpoints(thing_server: ThingServer):
|
||||
def add_v2_endpoints(thing_server: lt.ThingServer):
|
||||
app = thing_server.app
|
||||
|
||||
# TODO: update openflexure connect to make this unnecessary!!
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
import numpy as np
|
||||
import logging
|
||||
|
||||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
||||
from labthings_fastapi.decorators import thing_action
|
||||
import labthings_fastapi as lt
|
||||
|
||||
from .stage import StageDependency as StageDep
|
||||
from openflexure_microscope_server.things.autofocus import AutofocusThing
|
||||
from openflexure_microscope_server.things.camera_stage_mapping import CameraStageMapper
|
||||
|
||||
CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/")
|
||||
AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/")
|
||||
CSMDep = lt.deps.direct_thing_client_dependency(
|
||||
CameraStageMapper, "/camera_stage_mapping/"
|
||||
)
|
||||
AutofocusDep = lt.deps.direct_thing_client_dependency(AutofocusThing, "/autofocus/")
|
||||
|
||||
|
||||
class RecentringThing(Thing):
|
||||
@thing_action
|
||||
class RecentringThing(lt.Thing):
|
||||
@lt.thing_action
|
||||
def recentre(
|
||||
self,
|
||||
autofocus: AutofocusDep,
|
||||
|
|
|
|||
|
|
@ -17,10 +17,7 @@ from fastapi import Depends
|
|||
import numpy as np
|
||||
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
|
||||
from labthings_fastapi.descriptors import ThingSetting
|
||||
import labthings_fastapi as lt
|
||||
from labthings_fastapi.types.numpy import NDArray
|
||||
|
||||
from .camera import RawCameraDependency as Camera
|
||||
|
|
@ -190,7 +187,7 @@ class SharpnessDataArrays(BaseModel):
|
|||
|
||||
|
||||
class JPEGSharpnessMonitor:
|
||||
def __init__(self, stage: Stage, camera: Camera, portal: BlockingPortal):
|
||||
def __init__(self, stage: Stage, camera: Camera, portal: lt.deps.BlockingPortal):
|
||||
self.camera = camera
|
||||
self.stage = stage
|
||||
self.portal = portal
|
||||
|
|
@ -288,14 +285,14 @@ class JPEGSharpnessMonitor:
|
|||
SharpnessMonitorDep = Annotated[JPEGSharpnessMonitor, Depends()]
|
||||
|
||||
|
||||
class AutofocusThing(Thing):
|
||||
class AutofocusThing(lt.Thing):
|
||||
"""The Thing concerned with combinations of z axis movements and the camera.
|
||||
|
||||
Actions here involve moving a stage in z, and using the camera to either
|
||||
capture images (generally, z-stacking) and measuring the sharpness of the
|
||||
field of view to assess focus (autofocus and testing the success of a z-stack)"""
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def fast_autofocus(
|
||||
self,
|
||||
sharpness_monitor: SharpnessMonitorDep,
|
||||
|
|
@ -325,7 +322,7 @@ class AutofocusThing(Thing):
|
|||
# Return all focus data
|
||||
return sharpness_monitor.data_dict()
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def z_move_and_measure_sharpness(
|
||||
self,
|
||||
sharpness_monitor: SharpnessMonitorDep,
|
||||
|
|
@ -351,7 +348,7 @@ class AutofocusThing(Thing):
|
|||
sharpness_monitor.focus_rel(current_dz)
|
||||
return sharpness_monitor.data_dict()
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def looping_autofocus(
|
||||
self,
|
||||
stage: Stage,
|
||||
|
|
@ -397,7 +394,7 @@ class AutofocusThing(Thing):
|
|||
stage.move_absolute(z=peak_height)
|
||||
return heights.tolist(), sizes.tolist()
|
||||
|
||||
stack_images_to_save = ThingSetting(
|
||||
stack_images_to_save = lt.ThingSetting(
|
||||
initial_value=1,
|
||||
model=int,
|
||||
description="""The number of images to save in a stack.
|
||||
|
|
@ -405,7 +402,7 @@ class AutofocusThing(Thing):
|
|||
Defaults to 1 unless you need to see either side of focus""",
|
||||
)
|
||||
|
||||
stack_min_images_to_test = ThingSetting(
|
||||
stack_min_images_to_test = lt.ThingSetting(
|
||||
initial_value=9,
|
||||
model=int,
|
||||
description="""The minimum number of images to capture in a stack.
|
||||
|
|
@ -419,7 +416,7 @@ class AutofocusThing(Thing):
|
|||
""",
|
||||
)
|
||||
|
||||
stack_dz = ThingSetting(
|
||||
stack_dz = lt.ThingSetting(
|
||||
initial_value=50,
|
||||
model=int,
|
||||
description="""Space in steps between images in a z-stack
|
||||
|
|
@ -428,7 +425,7 @@ class AutofocusThing(Thing):
|
|||
200 for 20x""",
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def run_smart_stack(
|
||||
self,
|
||||
cam: WrappedCamera,
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ from PIL import Image
|
|||
from pydantic import BaseModel
|
||||
from scipy.stats import norm
|
||||
|
||||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.decorators import thing_action, thing_setting
|
||||
from labthings_fastapi.descriptors import ThingSetting
|
||||
import labthings_fastapi as lt
|
||||
from .camera import CameraDependency as CamDep
|
||||
|
||||
|
||||
|
|
@ -17,12 +15,12 @@ class ChannelDistributions(BaseModel):
|
|||
colorspace: str = "LUV"
|
||||
|
||||
|
||||
class BackgroundDetectThing(Thing):
|
||||
class BackgroundDetectThing(lt.Thing):
|
||||
# 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
|
||||
@lt.thing_setting
|
||||
def background_distributions(self) -> Optional[ChannelDistributions]:
|
||||
"""The statistics of the background image"""
|
||||
bd = self._background_distributions
|
||||
|
|
@ -45,13 +43,13 @@ class BackgroundDetectThing(Thing):
|
|||
f"Cannot set background_distributions with an object of type {type(value)}"
|
||||
)
|
||||
|
||||
tolerance = ThingSetting(
|
||||
tolerance = lt.ThingSetting(
|
||||
initial_value=7.0,
|
||||
model=float,
|
||||
description="How many standard deviations to allow for the background",
|
||||
)
|
||||
|
||||
fraction = ThingSetting(
|
||||
fraction = lt.ThingSetting(
|
||||
initial_value=25.0,
|
||||
model=float,
|
||||
description="How much of the image needs to be not background to label as sample",
|
||||
|
|
@ -78,7 +76,7 @@ class BackgroundDetectThing(Thing):
|
|||
axis=2,
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def background_fraction(self, cam: CamDep) -> float:
|
||||
"""Determine what fraction of the current image is background
|
||||
|
||||
|
|
@ -96,7 +94,7 @@ class BackgroundDetectThing(Thing):
|
|||
mask = self.background_mask(current_image_luv)
|
||||
return np.count_nonzero(mask) / np.prod(mask.shape) * 100
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def image_is_sample(self, cam: CamDep) -> bool:
|
||||
"""Label the current image as either background or sample"""
|
||||
b_fraction = self.background_fraction(cam)
|
||||
|
|
@ -104,7 +102,7 @@ class BackgroundDetectThing(Thing):
|
|||
|
||||
return (100 - b_fraction) > fraction_threshold
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def set_background(self, cam: CamDep):
|
||||
"""Grab an image, and use its statistics to set the background
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"""OpenFlexure Microscope Camera
|
||||
|
||||
This module defines the interface for cameras. Any compatible Thing
|
||||
This module defines the interface for cameras. Any compatible lt.Thing
|
||||
should enabe the server to work.
|
||||
|
||||
See repository root for licensing information.
|
||||
|
|
@ -14,23 +14,15 @@ from pydantic import RootModel
|
|||
from PIL import Image
|
||||
import piexif
|
||||
|
||||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
from labthings_fastapi.dependencies.metadata import GetThingStates
|
||||
from labthings_fastapi.dependencies.blocking_portal import BlockingPortal
|
||||
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
||||
from labthings_fastapi.dependencies.raw_thing import raw_thing_dependency
|
||||
from labthings_fastapi.dependencies.invocation import InvocationLogger
|
||||
from labthings_fastapi.outputs.mjpeg_stream import MJPEGStreamDescriptor
|
||||
from labthings_fastapi.outputs.blob import Blob
|
||||
import labthings_fastapi as lt
|
||||
from labthings_fastapi.types.numpy import NDArray
|
||||
|
||||
|
||||
class JPEGBlob(Blob):
|
||||
class JPEGBlob(lt.blob.Blob):
|
||||
media_type: str = "image/jpeg"
|
||||
|
||||
|
||||
class PNGBlob(Blob):
|
||||
class PNGBlob(lt.blob.Blob):
|
||||
"""A class representing a PNG image as a LabThings FastAPI Blob"""
|
||||
|
||||
media_type: str = "image/png"
|
||||
|
|
@ -154,11 +146,11 @@ class CameraMemoryBuffer:
|
|||
del self._storage[key]
|
||||
|
||||
|
||||
class BaseCamera(Thing):
|
||||
class BaseCamera(lt.Thing):
|
||||
"""The base class for all cameras. All cameras must directly inherit from this class"""
|
||||
|
||||
mjpeg_stream = MJPEGStreamDescriptor()
|
||||
lores_mjpeg_stream = MJPEGStreamDescriptor()
|
||||
mjpeg_stream = lt.outputs.MJPEGStreamDescriptor()
|
||||
lores_mjpeg_stream = lt.outputs.MJPEGStreamDescriptor()
|
||||
_memory_buffer = CameraMemoryBuffer()
|
||||
|
||||
def __enter__(self) -> None:
|
||||
|
|
@ -167,7 +159,7 @@ class BaseCamera(Thing):
|
|||
def __exit__(self, _exc_type, _exc_value, _traceback) -> None:
|
||||
raise NotImplementedError("CameraThings must define their own __exit__ method")
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def start_streaming(
|
||||
self, main_resolution: tuple[int, int], buffer_count: int
|
||||
) -> None:
|
||||
|
|
@ -177,14 +169,14 @@ class BaseCamera(Thing):
|
|||
"CameraThings must define their own start_streaming method"
|
||||
)
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def stream_active(self) -> bool:
|
||||
"Whether the MJPEG stream is active"
|
||||
raise NotImplementedError(
|
||||
"CameraThings must define their own stream_active method"
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def capture_array(
|
||||
self,
|
||||
stream_name: Literal["main", "lores", "raw", "full"] = "main",
|
||||
|
|
@ -194,10 +186,10 @@ class BaseCamera(Thing):
|
|||
"CameraThings must define their own capture_array method"
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def capture_jpeg(
|
||||
self,
|
||||
metadata_getter: GetThingStates,
|
||||
metadata_getter: lt.deps.GetThingStates,
|
||||
resolution: Literal["lores", "main", "full"] = "main",
|
||||
wait: Optional[float] = 5,
|
||||
) -> JPEGBlob:
|
||||
|
|
@ -206,10 +198,10 @@ class BaseCamera(Thing):
|
|||
"CameraThings must define their own capture_jpeg method"
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def grab_jpeg(
|
||||
self,
|
||||
portal: BlockingPortal,
|
||||
portal: lt.deps.BlockingPortal,
|
||||
stream_name: Literal["main", "lores"] = "main",
|
||||
) -> JPEGBlob:
|
||||
"""Acquire one image from the preview stream and return as an array
|
||||
|
|
@ -225,10 +217,10 @@ class BaseCamera(Thing):
|
|||
frame = portal.call(stream.grab_frame)
|
||||
return JPEGBlob.from_bytes(frame)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def grab_jpeg_size(
|
||||
self,
|
||||
portal: BlockingPortal,
|
||||
portal: lt.deps.BlockingPortal,
|
||||
stream_name: Literal["main", "lores"] = "main",
|
||||
) -> int:
|
||||
"""Acquire one image from the preview stream and return its size"""
|
||||
|
|
@ -237,7 +229,7 @@ class BaseCamera(Thing):
|
|||
)
|
||||
return portal.call(stream.next_frame_size)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def capture_image(
|
||||
self,
|
||||
stream_name: Literal["main", "lores", "raw"],
|
||||
|
|
@ -248,12 +240,12 @@ class BaseCamera(Thing):
|
|||
"CameraThings must define their own capture_image method"
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def capture_and_save(
|
||||
self,
|
||||
jpeg_path: str,
|
||||
logger: InvocationLogger,
|
||||
metadata_getter: GetThingStates,
|
||||
logger: lt.deps.InvocationLogger,
|
||||
metadata_getter: lt.deps.GetThingStates,
|
||||
save_resolution: Optional[Tuple[int, int]] = None,
|
||||
) -> None:
|
||||
"""Capture an image and save it to disk
|
||||
|
|
@ -279,11 +271,11 @@ class BaseCamera(Thing):
|
|||
save_resolution,
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def capture_to_memory(
|
||||
self,
|
||||
logger: InvocationLogger,
|
||||
metadata_getter: GetThingStates,
|
||||
logger: lt.deps.InvocationLogger,
|
||||
metadata_getter: lt.deps.GetThingStates,
|
||||
buffer_max: int = 1,
|
||||
) -> None:
|
||||
"""
|
||||
|
|
@ -304,11 +296,11 @@ class BaseCamera(Thing):
|
|||
image, metadata = self._robust_image_capture(metadata_getter, logger)
|
||||
return self._memory_buffer.add_image(image, metadata, buffer_max=buffer_max)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def save_from_memory(
|
||||
self,
|
||||
jpeg_path: str,
|
||||
logger: InvocationLogger,
|
||||
logger: lt.deps.InvocationLogger,
|
||||
save_resolution: Optional[Tuple[int, int]] = None,
|
||||
buffer_id: Optional[int] = None,
|
||||
) -> None:
|
||||
|
|
@ -333,15 +325,15 @@ class BaseCamera(Thing):
|
|||
save_resolution=save_resolution,
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def clear_buffers(self) -> None:
|
||||
"""Clear all images in memory"""
|
||||
self._memory_buffer.clear()
|
||||
|
||||
def _robust_image_capture(
|
||||
self,
|
||||
metadata_getter: GetThingStates,
|
||||
logger: InvocationLogger,
|
||||
metadata_getter: lt.deps.GetThingStates,
|
||||
logger: lt.deps.InvocationLogger,
|
||||
) -> Image:
|
||||
"""Capture an image in memory and return it with metadata
|
||||
CaptureError raised if the capture fails for any reason
|
||||
|
|
@ -366,7 +358,7 @@ class BaseCamera(Thing):
|
|||
jpeg_path: str,
|
||||
image: Image,
|
||||
metadata: dict,
|
||||
logger: InvocationLogger,
|
||||
logger: lt.deps.InvocationLogger,
|
||||
save_resolution: Optional[Tuple[int, int]] = None,
|
||||
) -> None:
|
||||
"""Saving the captured image and metadata to disk
|
||||
|
|
@ -399,5 +391,5 @@ class BaseCamera(Thing):
|
|||
raise IOError(f"An error occurred while saving {jpeg_path}") from e
|
||||
|
||||
|
||||
CameraDependency = direct_thing_client_dependency(BaseCamera, "/camera/")
|
||||
RawCameraDependency = raw_thing_dependency(BaseCamera)
|
||||
CameraDependency = lt.deps.direct_thing_client_dependency(BaseCamera, "/camera/")
|
||||
RawCameraDependency = lt.deps.raw_thing_dependency(BaseCamera)
|
||||
|
|
|
|||
|
|
@ -16,9 +16,7 @@ from threading import Thread
|
|||
import cv2
|
||||
import piexif
|
||||
|
||||
from labthings_fastapi.utilities import get_blocking_portal
|
||||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
from labthings_fastapi.dependencies.metadata import GetThingStates
|
||||
import labthings_fastapi as lt
|
||||
from labthings_fastapi.types.numpy import NDArray
|
||||
|
||||
from . import BaseCamera, JPEGBlob
|
||||
|
|
@ -45,7 +43,7 @@ class OpenCVCamera(BaseCamera):
|
|||
self._capture_thread.join()
|
||||
self.cap.release()
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def stream_active(self) -> bool:
|
||||
"Whether the MJPEG stream is active"
|
||||
if self._capture_enabled and self._capture_thread:
|
||||
|
|
@ -53,7 +51,7 @@ class OpenCVCamera(BaseCamera):
|
|||
return False
|
||||
|
||||
def _capture_frames(self):
|
||||
portal = get_blocking_portal(self)
|
||||
portal = lt.get_blocking_portal(self)
|
||||
while self._capture_enabled:
|
||||
ret, frame = self.cap.read()
|
||||
if not ret:
|
||||
|
|
@ -68,7 +66,7 @@ class OpenCVCamera(BaseCamera):
|
|||
].tobytes()
|
||||
self.lores_mjpeg_stream.add_frame(jpeg_lores, portal)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def capture_array(
|
||||
self,
|
||||
resolution: Literal["main", "full"] = "full",
|
||||
|
|
@ -87,10 +85,10 @@ class OpenCVCamera(BaseCamera):
|
|||
)
|
||||
return frame
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def capture_jpeg(
|
||||
self,
|
||||
metadata_getter: GetThingStates,
|
||||
metadata_getter: lt.deps.GetThingStates,
|
||||
resolution: Literal["main", "full"] = "main",
|
||||
) -> JPEGBlob:
|
||||
"""Acquire one image from the camera and return as a JPEG blob
|
||||
|
|
|
|||
|
|
@ -16,37 +16,31 @@ https://datasheets.raspberrypi.com/camera/raspberry-pi-camera-guide.pdf
|
|||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Annotated, Any, Iterator, Literal, Mapping, Optional
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
from tempfile import TemporaryDirectory
|
||||
from contextlib import contextmanager
|
||||
from threading import RLock
|
||||
|
||||
from pydantic import BaseModel, BeforeValidator
|
||||
|
||||
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
|
||||
from labthings_fastapi.dependencies.metadata import GetThingStates
|
||||
from labthings_fastapi.dependencies.blocking_portal import BlockingPortal
|
||||
from typing import Annotated, Any, Iterator, Literal, Mapping, Optional
|
||||
from contextlib import contextmanager
|
||||
import piexif
|
||||
from threading import RLock
|
||||
import picamera2
|
||||
import numpy as np
|
||||
from picamera2 import Picamera2
|
||||
from picamera2.encoders import MJPEGEncoder
|
||||
from picamera2.outputs import Output
|
||||
import numpy as np
|
||||
from . import picamera_recalibrate_utils as recalibrate_utils
|
||||
|
||||
import labthings_fastapi as lt
|
||||
|
||||
from . import picamera_recalibrate_utils as recalibrate_utils
|
||||
from . import BaseCamera, JPEGBlob, ArrayModel
|
||||
|
||||
|
||||
class PicameraControl(ThingProperty):
|
||||
class PicameraControl(lt.ThingProperty):
|
||||
def __init__(
|
||||
self, control_name: str, model: type = float, description: Optional[str] = None
|
||||
):
|
||||
|
|
@ -66,7 +60,7 @@ class PicameraControl(ThingProperty):
|
|||
class PicameraStreamOutput(Output):
|
||||
"""An Output class that sends frames to a stream"""
|
||||
|
||||
def __init__(self, stream: MJPEGStream, portal: BlockingPortal):
|
||||
def __init__(self, stream: lt.outputs.MJPEGStream, portal: lt.deps.BlockingPortal):
|
||||
"""Create an output that puts frames in an MJPEGStream
|
||||
|
||||
We need to pass the stream object, and also the blocking portal, because
|
||||
|
|
@ -211,19 +205,19 @@ class StreamingPiCamera2(BaseCamera):
|
|||
except KeyError:
|
||||
pass # If controls are missing, leave at default
|
||||
|
||||
stream_resolution = ThingProperty(
|
||||
stream_resolution = lt.ThingProperty(
|
||||
tuple[int, int],
|
||||
initial_value=(820, 616),
|
||||
description="Resolution to use for the MJPEG stream",
|
||||
)
|
||||
|
||||
mjpeg_bitrate = ThingProperty(
|
||||
mjpeg_bitrate = lt.ThingProperty(
|
||||
Optional[int],
|
||||
initial_value=100000000,
|
||||
description="Bitrate for MJPEG stream (None for default)",
|
||||
)
|
||||
|
||||
stream_active = ThingProperty(
|
||||
stream_active = lt.ThingProperty(
|
||||
bool,
|
||||
initial_value=False,
|
||||
description="Whether the MJPEG stream is active",
|
||||
|
|
@ -255,7 +249,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
_sensor_modes = None
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def sensor_modes(self) -> list[SensorMode]:
|
||||
"""All the available modes the current sensor supports"""
|
||||
if not self._sensor_modes:
|
||||
|
|
@ -263,7 +257,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
self._sensor_modes = cam.sensor_modes
|
||||
return self._sensor_modes
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def sensor_mode(self) -> Optional[SensorModeSelector]:
|
||||
"""The intended sensor mode of the camera"""
|
||||
return self.thing_settings["sensor_mode"]
|
||||
|
|
@ -276,13 +270,13 @@ class StreamingPiCamera2(BaseCamera):
|
|||
with self.picamera(pause_stream=True):
|
||||
self.thing_settings["sensor_mode"] = new_mode
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def sensor_resolution(self) -> tuple[int, int]:
|
||||
"""The native resolution of the camera's sensor"""
|
||||
with self.picamera() as cam:
|
||||
return cam.sensor_resolution
|
||||
|
||||
tuning = ThingProperty(Optional[dict], None, readonly=True)
|
||||
tuning = lt.ThingProperty(Optional[dict], None, readonly=True)
|
||||
|
||||
def settings_to_properties(self):
|
||||
"""Set the values of properties based on the settings dict"""
|
||||
|
|
@ -400,7 +394,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
cam.close()
|
||||
del self._picamera
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def start_streaming(
|
||||
self, main_resolution: tuple[int, int] = (820, 616), buffer_count: int = 6
|
||||
) -> None:
|
||||
|
|
@ -451,7 +445,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
MJPEGEncoder(self.mjpeg_bitrate),
|
||||
PicameraStreamOutput(
|
||||
self.mjpeg_stream,
|
||||
get_blocking_portal(self),
|
||||
lt.get_blocking_portal(self),
|
||||
),
|
||||
name=stream_name,
|
||||
)
|
||||
|
|
@ -459,7 +453,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
MJPEGEncoder(100000000),
|
||||
PicameraStreamOutput(
|
||||
self.lores_mjpeg_stream,
|
||||
get_blocking_portal(self),
|
||||
lt.get_blocking_portal(self),
|
||||
),
|
||||
name="lores",
|
||||
)
|
||||
|
|
@ -472,7 +466,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
"Started MJPEG stream at %s on port %s", self.stream_resolution, 1
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def stop_streaming(self, stop_web_stream: bool = True) -> None:
|
||||
"""
|
||||
Stop the MJPEG stream
|
||||
|
|
@ -493,7 +487,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
# Adding a sleep to prevent camera getting confused by rapid commands
|
||||
time.sleep(0.2)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def capture_image(
|
||||
self,
|
||||
stream_name: Literal["main", "lores", "raw"] = "main",
|
||||
|
|
@ -511,7 +505,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
with self.picamera() as cam:
|
||||
return cam.capture_image(stream_name, wait=wait)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def capture_array(
|
||||
self,
|
||||
stream_name: Literal["main", "lores", "raw", "full"] = "main",
|
||||
|
|
@ -538,7 +532,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
with self.picamera() as cam:
|
||||
return cam.capture_array(stream_name, wait=wait)
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def camera_configuration(self) -> Mapping:
|
||||
"""The "configuration" dictionary of the picamera2 object
|
||||
|
||||
|
|
@ -553,10 +547,10 @@ class StreamingPiCamera2(BaseCamera):
|
|||
with self.picamera() as cam:
|
||||
return cam.camera_configuration()
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def capture_jpeg(
|
||||
self,
|
||||
metadata_getter: GetThingStates,
|
||||
metadata_getter: lt.deps.GetThingStates,
|
||||
resolution: Literal["lores", "main", "full"] = "main",
|
||||
wait: Optional[float] = 0.9,
|
||||
) -> JPEGBlob:
|
||||
|
|
@ -580,7 +574,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
bypass this, you must use a raw capture.
|
||||
"""
|
||||
fname = datetime.now().strftime("%Y-%m-%d-%H%M%S.jpeg")
|
||||
folder = TemporaryDirectory()
|
||||
folder = tempfile.TemporaryDirectory()
|
||||
path = os.path.join(folder.name, fname)
|
||||
config = self.camera_configuration
|
||||
# Low-res and main streams are running already - so we don't need
|
||||
|
|
@ -609,13 +603,13 @@ class StreamingPiCamera2(BaseCamera):
|
|||
piexif.insert(piexif.dump(exif_dict), path)
|
||||
return JPEGBlob.from_temporary_directory(folder, fname)
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def capture_metadata(self) -> dict:
|
||||
"""Return the metadata from the camera"""
|
||||
with self.picamera() as cam:
|
||||
return cam.capture_metadata()
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def auto_expose_from_minimum(
|
||||
self,
|
||||
target_white_level: int = 700,
|
||||
|
|
@ -641,7 +635,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
)
|
||||
self.update_persistent_controls()
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def calibrate_white_balance(
|
||||
self,
|
||||
method: Literal["percentile", "centre"] = "centre",
|
||||
|
|
@ -676,7 +670,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
)
|
||||
self.update_persistent_controls()
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def calibrate_lens_shading(self) -> None:
|
||||
"""Take an image and use it for flat-field correction.
|
||||
|
||||
|
|
@ -694,7 +688,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
recalibrate_utils.set_static_lst(self.tuning, L, Cr, Cb)
|
||||
self.initialise_picamera()
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def colour_correction_matrix(
|
||||
self,
|
||||
) -> tuple[float, float, float, float, float, float, float, float, float]:
|
||||
|
|
@ -709,7 +703,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
self.thing_settings["colour_correction_matrix"] = value
|
||||
self.calibrate_colour_correction(value)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def reset_ccm(self):
|
||||
"""
|
||||
Overwrite the colour correction matrix in camera tuning with default values.
|
||||
|
|
@ -731,7 +725,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
]
|
||||
self.colour_correction_matrix = col_corr_matrix
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def calibrate_colour_correction(
|
||||
self,
|
||||
col_corr_matrix: tuple[
|
||||
|
|
@ -750,7 +744,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
recalibrate_utils.set_static_ccm(self.tuning, col_corr_matrix)
|
||||
self.initialise_picamera()
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def set_static_green_equalisation(self, offset: int = 65535) -> None:
|
||||
"""Set the green equalisation to a static value.
|
||||
|
||||
|
|
@ -765,7 +759,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
recalibrate_utils.set_static_geq(self.tuning, offset)
|
||||
self.initialise_picamera()
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def full_auto_calibrate(self) -> None:
|
||||
"""Perform a full auto-calibration
|
||||
|
||||
|
|
@ -783,7 +777,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
self.calibrate_lens_shading()
|
||||
self.calibrate_white_balance()
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def flat_lens_shading(self) -> None:
|
||||
"""Disable flat-field correction
|
||||
|
||||
|
|
@ -802,7 +796,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
)
|
||||
self.initialise_picamera()
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def lens_shading_tables(self) -> Optional[LensShading]:
|
||||
"""The current lens shading (i.e. flat-field correction)
|
||||
|
||||
|
|
@ -879,7 +873,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
float(gain_b / np.max(lst.Cb)),
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def flat_lens_shading_chrominance(self) -> None:
|
||||
"""Disable flat-field correction
|
||||
|
||||
|
|
@ -894,7 +888,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
recalibrate_utils.set_static_lst(self.tuning, luminance, flat, flat)
|
||||
self.initialise_picamera()
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def reset_lens_shading(self) -> None:
|
||||
"""Revert to default lens shading settings
|
||||
|
||||
|
|
@ -905,7 +899,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
recalibrate_utils.copy_alsc_section(self.default_tuning, self.tuning)
|
||||
self.initialise_picamera()
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def lens_shading_is_static(self) -> bool:
|
||||
"""Whether the lens shading is static
|
||||
|
||||
|
|
|
|||
|
|
@ -19,10 +19,7 @@ import numpy as np
|
|||
import piexif
|
||||
from scipy.ndimage import gaussian_filter
|
||||
|
||||
from labthings_fastapi.utilities import get_blocking_portal
|
||||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
from labthings_fastapi.dependencies.metadata import GetThingStates
|
||||
from labthings_fastapi.server import ThingServer
|
||||
import labthings_fastapi as lt
|
||||
|
||||
from . import BaseCamera, JPEGBlob, ArrayModel
|
||||
from ..stage import BaseStage
|
||||
|
|
@ -36,7 +33,7 @@ class SimulatedCamera(BaseCamera):
|
|||
"""A Thing representing an OpenCV camera"""
|
||||
|
||||
_stage: Optional[BaseStage] = None
|
||||
_server: Optional[ThingServer] = None
|
||||
_server: Optional[lt.ThingServer] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -116,7 +113,7 @@ class SimulatedCamera(BaseCamera):
|
|||
return image
|
||||
|
||||
def attach_to_server(
|
||||
self, server: ThingServer, path: str, setting_storage_path: str
|
||||
self, server: lt.ThingServer, path: str, setting_storage_path: str
|
||||
):
|
||||
self._server = server
|
||||
return super().attach_to_server(server, path, setting_storage_path)
|
||||
|
|
@ -146,7 +143,7 @@ class SimulatedCamera(BaseCamera):
|
|||
self._capture_enabled = False
|
||||
self._capture_thread.join()
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def stream_active(self) -> bool:
|
||||
"Whether the MJPEG stream is active"
|
||||
if self._capture_enabled and self._capture_thread:
|
||||
|
|
@ -154,7 +151,7 @@ class SimulatedCamera(BaseCamera):
|
|||
return False
|
||||
|
||||
def _capture_frames(self):
|
||||
portal = get_blocking_portal(self)
|
||||
portal = lt.get_blocking_portal(self)
|
||||
while self._capture_enabled:
|
||||
time.sleep(self.frame_interval)
|
||||
try:
|
||||
|
|
@ -168,7 +165,7 @@ class SimulatedCamera(BaseCamera):
|
|||
except Exception as e:
|
||||
logging.error(f"Failed to capture frame: {e}, retrying...")
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def capture_array(
|
||||
self,
|
||||
resolution: Literal["main", "full"] = "full",
|
||||
|
|
@ -182,10 +179,10 @@ class SimulatedCamera(BaseCamera):
|
|||
logging.warning(f"Simulation camera doen't respect {resolution} setting")
|
||||
return self.generate_frame()
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def capture_jpeg(
|
||||
self,
|
||||
metadata_getter: GetThingStates,
|
||||
metadata_getter: lt.deps.GetThingStates,
|
||||
resolution: Literal["main", "full"] = "main",
|
||||
) -> JPEGBlob:
|
||||
"""Acquire one image from the camera and return as a JPEG blob
|
||||
|
|
|
|||
|
|
@ -33,14 +33,10 @@ from camera_stage_mapping.camera_stage_calibration_1d import (
|
|||
image_to_stage_displacement_from_1d,
|
||||
)
|
||||
from camera_stage_mapping.exceptions import MappingError
|
||||
from labthings_fastapi.dependencies.invocation import (
|
||||
InvocationCancelledError,
|
||||
InvocationLogger,
|
||||
)
|
||||
|
||||
import labthings_fastapi as lt
|
||||
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
|
||||
from .stage import StageDependency as Stage
|
||||
|
|
@ -179,15 +175,15 @@ class CSMUncalibratedError(HTTPException):
|
|||
)
|
||||
|
||||
|
||||
class CameraStageMapper(Thing):
|
||||
class CameraStageMapper(lt.Thing):
|
||||
"""A Thing to manage mapping between image and stage coordinates"""
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def calibrate_1d(
|
||||
self,
|
||||
hw: HardwareInterfaceDep,
|
||||
stage: Stage,
|
||||
logger: InvocationLogger,
|
||||
logger: lt.deps.InvocationLogger,
|
||||
direction: Tuple[float, float, float],
|
||||
) -> DenumpifyingDict:
|
||||
"""Move a microscope's stage in 1D, and figure out the relationship with the camera"""
|
||||
|
|
@ -202,7 +198,7 @@ class CameraStageMapper(Thing):
|
|||
result: dict = calibrate_backlash_1d(
|
||||
tracker, move, direction_array, logger=logger
|
||||
)
|
||||
except InvocationCancelledError as e:
|
||||
except lt.exceptions.InvocationCancelledError as e:
|
||||
logger.info("User cancelled the camera stage mapping calibration")
|
||||
logger.info("Returning to starting position")
|
||||
stage.move_absolute(**starting_position, block_cancellation=True)
|
||||
|
|
@ -215,9 +211,9 @@ class CameraStageMapper(Thing):
|
|||
result["image_resolution"] = hw.grab_image().shape[:2]
|
||||
return result
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def calibrate_xy(
|
||||
self, hw: HardwareInterfaceDep, stage: Stage, logger: InvocationLogger
|
||||
self, hw: HardwareInterfaceDep, stage: Stage, logger: lt.deps.InvocationLogger
|
||||
) -> DenumpifyingDict:
|
||||
"""Move the microscope's stage in X and Y, to calibrate its relationship to the camera
|
||||
|
||||
|
|
@ -257,7 +253,7 @@ class CameraStageMapper(Thing):
|
|||
|
||||
return data
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def image_to_stage_displacement_matrix(
|
||||
self,
|
||||
) -> Optional[List[List[float]]]: # 2x2 integer array
|
||||
|
|
@ -285,21 +281,19 @@ class CameraStageMapper(Thing):
|
|||
]
|
||||
return np.array(displacement_matrix).tolist()
|
||||
|
||||
last_calibration = ThingSetting(
|
||||
last_calibration = lt.ThingSetting(
|
||||
initial_value=None,
|
||||
model=Optional[dict],
|
||||
readonly=True,
|
||||
description="The most recent CSM calibration",
|
||||
)
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def image_resolution(self) -> Optional[Tuple[float, float]]:
|
||||
"""The image size used to calibrate the image_to_stage_displacement_matrix"""
|
||||
if self.last_calibration is None:
|
||||
return None
|
||||
return self.last_calibration["camera_stage_mapping_calibration"][
|
||||
"image_resolution"
|
||||
]
|
||||
return self.last_calibration["image_resolution"]
|
||||
|
||||
def assert_calibrated(self):
|
||||
"""Raise an exception if the image_to_stage_displacement matrix is not set"""
|
||||
|
|
@ -308,7 +302,7 @@ class CameraStageMapper(Thing):
|
|||
# added by CSMUncalibratedError
|
||||
raise CSMUncalibratedError() # noqa: RSE102
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def move_in_image_coordinates(
|
||||
self,
|
||||
stage: Stage,
|
||||
|
|
@ -332,7 +326,7 @@ class CameraStageMapper(Thing):
|
|||
)
|
||||
stage.move_relative(x=relative_move[0], y=relative_move[1])
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def thing_state(self) -> dict[str, Any]:
|
||||
"""Summary metadata describing the current state of the Thing"""
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -11,19 +11,14 @@ from socket import gethostname
|
|||
from typing import Annotated, Any, MutableMapping, Optional, Sequence
|
||||
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, thing_setting
|
||||
from labthings_fastapi.descriptors import ThingSetting
|
||||
from labthings_fastapi.dependencies.thing_server import find_thing_server
|
||||
from labthings_fastapi.server import ThingServer
|
||||
import labthings_fastapi as lt
|
||||
|
||||
|
||||
def thing_server_from_request(request: Request) -> ThingServer:
|
||||
return find_thing_server(request.app)
|
||||
def thing_server_from_request(request: Request) -> lt.ThingServer:
|
||||
return lt.find_thing_server(request.app)
|
||||
|
||||
|
||||
ThingServerDep = Annotated[ThingServer, Depends(thing_server_from_request)]
|
||||
ThingServerDep = Annotated[lt.ThingServer, Depends(thing_server_from_request)]
|
||||
|
||||
|
||||
def recursive_update(old_dict: MutableMapping, update: Mapping):
|
||||
|
|
@ -48,20 +43,20 @@ def nested_dict_get(data: dict, key: Sequence[str], create=False) -> Any:
|
|||
return subdict
|
||||
|
||||
|
||||
class SettingsManager(Thing):
|
||||
external_metadata = ThingSetting(
|
||||
class SettingsManager(lt.Thing):
|
||||
external_metadata = lt.ThingSetting(
|
||||
initial_value={},
|
||||
model=Mapping,
|
||||
description="External metadata stored in the server's settings",
|
||||
)
|
||||
|
||||
external_metadata_in_state = ThingSetting(
|
||||
external_metadata_in_state = lt.ThingSetting(
|
||||
initial_value=[],
|
||||
model=Sequence[str],
|
||||
description='A list of strings that are included in the "state" metadata',
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def update_external_metadata(
|
||||
self, data: Mapping, key: Optional[str] = None
|
||||
) -> None:
|
||||
|
|
@ -84,7 +79,7 @@ class SettingsManager(Thing):
|
|||
recursive_update(subdict, data)
|
||||
self.external_metadata = metadata
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def delete_external_metadata(self, key: str) -> None:
|
||||
"""Delete a key from the stored metadata.
|
||||
|
||||
|
|
@ -104,7 +99,7 @@ class SettingsManager(Thing):
|
|||
|
||||
_microscope_id: Optional[str] = None
|
||||
|
||||
@thing_setting
|
||||
@lt.thing_setting
|
||||
def microscope_id(self) -> UUID:
|
||||
"""A unique identifier for this microscope"""
|
||||
if self._microscope_id is None:
|
||||
|
|
@ -116,13 +111,13 @@ class SettingsManager(Thing):
|
|||
# TODO make read only but still settable from disk
|
||||
self._microscope_id = uuid
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def hostname(self) -> str:
|
||||
"""The hostname of the microscope, as reported by its operating system."""
|
||||
return gethostname()
|
||||
|
||||
@thing_action
|
||||
def get_things_state(self, metadata_getter: GetThingStates) -> Mapping:
|
||||
@lt.thing_action
|
||||
def get_things_state(self, metadata_getter: lt.deps.GetThingStates) -> Mapping:
|
||||
"""Metadata summarising the current state of all Things in the server"""
|
||||
return metadata_getter()
|
||||
|
||||
|
|
|
|||
|
|
@ -11,21 +11,7 @@ from fastapi.responses import FileResponse
|
|||
import numpy as np
|
||||
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.outputs.blob import blob_type
|
||||
import labthings_fastapi as lt
|
||||
|
||||
from openflexure_microscope_server.utilities import ErrorCapturingThread
|
||||
from openflexure_microscope_server import scan_directories
|
||||
|
|
@ -38,14 +24,16 @@ from .background_detect import BackgroundDetectThing
|
|||
from .camera import CameraDependency as CamDep
|
||||
from .stage import StageDependency as StageDep
|
||||
|
||||
CSMDep = direct_thing_client_dependency(CameraStageMapper, "/camera_stage_mapping/")
|
||||
AutofocusDep = direct_thing_client_dependency(AutofocusThing, "/autofocus/")
|
||||
BackgroundDep = direct_thing_client_dependency(
|
||||
CSMDep = lt.deps.direct_thing_client_dependency(
|
||||
CameraStageMapper, "/camera_stage_mapping/"
|
||||
)
|
||||
AutofocusDep = lt.deps.direct_thing_client_dependency(AutofocusThing, "/autofocus/")
|
||||
BackgroundDep = lt.deps.direct_thing_client_dependency(
|
||||
BackgroundDetectThing, "/background_detect/"
|
||||
)
|
||||
|
||||
JPEGBlob = blob_type("image/jpeg")
|
||||
ZipBlob = blob_type("application/zip")
|
||||
JPEGBlob = lt.blob.blob_type("image/jpeg")
|
||||
ZipBlob = lt.blob.blob_type("application/zip")
|
||||
|
||||
SCAN_DATA_FILENAME = "scan_data.json"
|
||||
STITCHING_CMD = "openflexure-stitch"
|
||||
|
|
@ -76,7 +64,7 @@ def _scan_running(method):
|
|||
return scan_running_wrapper
|
||||
|
||||
|
||||
class SmartScanThing(Thing):
|
||||
class SmartScanThing(lt.Thing):
|
||||
def __init__(self, scans_folder):
|
||||
self._scan_dir_manager = scan_directories.ScanDirectoryManager(scans_folder)
|
||||
self._preview_stitch_popen = None
|
||||
|
|
@ -87,17 +75,17 @@ class SmartScanThing(Thing):
|
|||
self._latest_scan_name: Optional[str] = None
|
||||
|
||||
# Scan logger is the invocation logger labthings-fastapi creates
|
||||
# when the `sample_scan` thing_action is called. It is saved as
|
||||
# when the `sample_scan` lt.thing_action is called. It is saved as
|
||||
# private class variable along with many others here.
|
||||
# Access to these variables requires a scan to be running,
|
||||
# any method that calls these should be decorrected with
|
||||
# @_scan_running
|
||||
self._scan_logger: Optional[InvocationLogger] = None
|
||||
self._cancel: Optional[CancelHook] = None
|
||||
self._scan_logger: Optional[lt.deps.InvocationLogger] = None
|
||||
self._cancel: Optional[lt.deps.CancelHook] = None
|
||||
self._autofocus: Optional[AutofocusDep] = None
|
||||
self._stage: Optional[StageDep] = None
|
||||
self._cam: Optional[CamDep] = None
|
||||
self._metadata_getter: Optional[GetThingStates] = None
|
||||
self._metadata_getter: Optional[lt.deps.GetThingStates] = None
|
||||
self._csm: Optional[CSMDep] = None
|
||||
self._background_detect: Optional[BackgroundDep] = None
|
||||
self._ongoing_scan: Optional[scan_directories.ScanDirectory] = None
|
||||
|
|
@ -107,15 +95,15 @@ class SmartScanThing(Thing):
|
|||
# TODO Scan data is a dict during refactoring, should become a dataclass
|
||||
self._scan_data: Optional[dict] = None
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def sample_scan(
|
||||
self,
|
||||
cancel: CancelHook,
|
||||
logger: InvocationLogger,
|
||||
cancel: lt.deps.CancelHook,
|
||||
logger: lt.deps.InvocationLogger,
|
||||
autofocus: AutofocusDep,
|
||||
stage: StageDep,
|
||||
cam: CamDep,
|
||||
metadata_getter: GetThingStates,
|
||||
metadata_getter: lt.deps.GetThingStates,
|
||||
csm: CSMDep,
|
||||
background_detect: BackgroundDep,
|
||||
scan_name: str = "",
|
||||
|
|
@ -210,7 +198,7 @@ class SmartScanThing(Thing):
|
|||
"of motion."
|
||||
)
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def latest_scan_name(self) -> Optional[str]:
|
||||
"""The name of the last scan to be started."""
|
||||
return self._latest_scan_name
|
||||
|
|
@ -419,7 +407,7 @@ class SmartScanThing(Thing):
|
|||
self._main_scan_loop()
|
||||
self._update_scan_data_json(scan_result="success")
|
||||
|
||||
except InvocationCancelledError:
|
||||
except lt.exceptions.InvocationCancelledError:
|
||||
scan_successful = False
|
||||
# Reset the cancel event so it can be thrown again
|
||||
self._cancel.clear()
|
||||
|
|
@ -572,7 +560,7 @@ class SmartScanThing(Thing):
|
|||
except SubprocessError as e:
|
||||
self._scan_logger.error(f"Stitching failed: {e}", exc_info=e)
|
||||
|
||||
@fastapi_endpoint(
|
||||
@lt.fastapi_endpoint(
|
||||
"get",
|
||||
"scans/stitched_thumbnail.jpg",
|
||||
responses={
|
||||
|
|
@ -595,13 +583,13 @@ class SmartScanThing(Thing):
|
|||
raise HTTPException(404, "File not found")
|
||||
return FileResponse(preview_path)
|
||||
|
||||
save_resolution = ThingSetting(
|
||||
save_resolution = lt.ThingSetting(
|
||||
initial_value=(1640, 1232),
|
||||
model=tuple[int, int],
|
||||
description=("A tuple of the image resolution to capture."),
|
||||
)
|
||||
|
||||
max_range = ThingSetting(
|
||||
max_range = lt.ThingSetting(
|
||||
initial_value=45000,
|
||||
model=int,
|
||||
description=(
|
||||
|
|
@ -609,13 +597,13 @@ class SmartScanThing(Thing):
|
|||
),
|
||||
)
|
||||
|
||||
stitch_tiff = ThingSetting(
|
||||
stitch_tiff = lt.ThingSetting(
|
||||
initial_value=False,
|
||||
model=bool,
|
||||
description="Whether or not to also produce a pyramidal tiff",
|
||||
)
|
||||
|
||||
skip_background = ThingSetting(
|
||||
skip_background = lt.ThingSetting(
|
||||
initial_value=True,
|
||||
model=bool,
|
||||
description="""Whether to detect and skip empty fields of view
|
||||
|
|
@ -623,19 +611,19 @@ class SmartScanThing(Thing):
|
|||
This uses the settings from the `background_detect` Thing.""",
|
||||
)
|
||||
|
||||
autofocus_dz = ThingSetting(
|
||||
autofocus_dz = lt.ThingSetting(
|
||||
initial_value=1000,
|
||||
model=int,
|
||||
description="The z distance to perform an autofocus in steps",
|
||||
)
|
||||
|
||||
overlap = ThingSetting(
|
||||
overlap = lt.ThingSetting(
|
||||
initial_value=0.45,
|
||||
model=float,
|
||||
description="The fraction (0-1) that adjacent images should overlap in x or y",
|
||||
)
|
||||
|
||||
stitch_automatically = ThingSetting(
|
||||
stitch_automatically = lt.ThingSetting(
|
||||
initial_value=True,
|
||||
model=bool,
|
||||
description=(
|
||||
|
|
@ -644,7 +632,7 @@ class SmartScanThing(Thing):
|
|||
),
|
||||
)
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def scans(self) -> list[scan_directories.ScanInfo]:
|
||||
"""All the available scans
|
||||
|
||||
|
|
@ -656,7 +644,7 @@ class SmartScanThing(Thing):
|
|||
"""
|
||||
return self._scan_dir_manager.all_scans_info()
|
||||
|
||||
@fastapi_endpoint(
|
||||
@lt.fastapi_endpoint(
|
||||
"get",
|
||||
"get_stitch/{scan_name}",
|
||||
responses={
|
||||
|
|
@ -680,7 +668,7 @@ class SmartScanThing(Thing):
|
|||
raise HTTPException(404, "File not found")
|
||||
return FileResponse(stitch_path)
|
||||
|
||||
@fastapi_endpoint(
|
||||
@lt.fastapi_endpoint(
|
||||
"delete",
|
||||
"scans/{scan_name}",
|
||||
responses={
|
||||
|
|
@ -688,7 +676,7 @@ class SmartScanThing(Thing):
|
|||
400: {"description": "An error occurred while trying to delete scan"},
|
||||
},
|
||||
)
|
||||
def delete_scan(self, scan_name: str, logger: InvocationLogger) -> None:
|
||||
def delete_scan(self, scan_name: str, logger: lt.deps.InvocationLogger) -> None:
|
||||
"""Delete the folder for the specified scan.
|
||||
|
||||
This endpoint allows scans to be deleted from disk.
|
||||
|
|
@ -702,11 +690,11 @@ class SmartScanThing(Thing):
|
|||
if not deleted_scan_success:
|
||||
raise HTTPException(400, "Couldn't delete scan, check log for details")
|
||||
|
||||
@fastapi_endpoint(
|
||||
@lt.fastapi_endpoint(
|
||||
"delete",
|
||||
"scans",
|
||||
)
|
||||
def delete_all_scans(self, logger: InvocationLogger) -> None:
|
||||
def delete_all_scans(self, logger: lt.deps.InvocationLogger) -> None:
|
||||
"""Delete all the scans on the microscope
|
||||
|
||||
**This will irreversibly remove all scanned data from the
|
||||
|
|
@ -716,8 +704,8 @@ class SmartScanThing(Thing):
|
|||
for scan_name in self._scan_dir_manager.all_scans:
|
||||
self._delete_scan(scan_name, logger)
|
||||
|
||||
@thing_action
|
||||
def purge_empty_scans(self, logger: InvocationLogger) -> None:
|
||||
@lt.thing_action
|
||||
def purge_empty_scans(self, logger: lt.deps.InvocationLogger) -> None:
|
||||
"""
|
||||
Delete all scan folders containing no images at the top level
|
||||
"""
|
||||
|
|
@ -727,7 +715,7 @@ class SmartScanThing(Thing):
|
|||
if scan_info.number_of_images == 0:
|
||||
self._delete_scan(scan_info.name, logger)
|
||||
|
||||
def _delete_scan(self, scan_name, logger: InvocationLogger) -> bool:
|
||||
def _delete_scan(self, scan_name, logger: lt.deps.InvocationLogger) -> bool:
|
||||
"""
|
||||
A wrapper around scan manager's delete_scan that logs to the invocation logger
|
||||
"""
|
||||
|
|
@ -752,7 +740,7 @@ class SmartScanThing(Thing):
|
|||
scan_name=self.latest_scan_name, filename="preview.jpg", check_exists=True
|
||||
)
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def latest_preview_stitch_time(self) -> Optional[float]:
|
||||
"""The modification time of the latest preview image, to allow live updating
|
||||
|
||||
|
|
@ -767,7 +755,7 @@ class SmartScanThing(Thing):
|
|||
return None
|
||||
return os.path.getmtime(self.latest_preview_stitch_path)
|
||||
|
||||
@fastapi_endpoint(
|
||||
@lt.fastapi_endpoint(
|
||||
"get",
|
||||
"latest_preview_stitch.jpg",
|
||||
responses={
|
||||
|
|
@ -836,8 +824,8 @@ class SmartScanThing(Thing):
|
|||
|
||||
def run_subprocess(
|
||||
self,
|
||||
logger: InvocationLogger,
|
||||
cancel: CancelHook,
|
||||
logger: lt.deps.InvocationLogger,
|
||||
cancel: lt.deps.CancelHook,
|
||||
cmd: list[str],
|
||||
) -> CompletedProcess:
|
||||
"""
|
||||
|
|
@ -873,7 +861,7 @@ class SmartScanThing(Thing):
|
|||
# Note that using cancel.sleep allows the InvocationCancelledError to
|
||||
# be thrown
|
||||
cancel.sleep(0.2)
|
||||
except InvocationCancelledError as e:
|
||||
except lt.exceptions.InvocationCancelledError as e:
|
||||
logger.info("Stitching cancelled by user")
|
||||
process.kill()
|
||||
raise e
|
||||
|
|
@ -886,18 +874,18 @@ class SmartScanThing(Thing):
|
|||
else:
|
||||
raise ChildProcessError(f"Subprocess {cmd[0]} exited with an error.")
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def stitch_scan(
|
||||
self,
|
||||
logger: InvocationLogger,
|
||||
cancel: CancelHook,
|
||||
logger: lt.deps.InvocationLogger,
|
||||
cancel: lt.deps.CancelHook,
|
||||
scan_name: str,
|
||||
stitch_resize: Optional[float] = None,
|
||||
overlap: float = 0.0,
|
||||
) -> None:
|
||||
"""Generate a stitched image based on stage position metadata
|
||||
|
||||
Note that as this is a thing_action it needs the logger passed as
|
||||
Note that as this is a lt.thing_action it needs the logger passed as
|
||||
a variable if called from another thing action
|
||||
"""
|
||||
json_fpath = self._scan_dir_manager.get_file_path_from_img_dir(
|
||||
|
|
@ -969,12 +957,12 @@ class SmartScanThing(Thing):
|
|||
self._scan_dir_manager.img_dir_for(scan_name),
|
||||
],
|
||||
)
|
||||
except InvocationCancelledError:
|
||||
except lt.exceptions.InvocationCancelledError:
|
||||
# Sleep for 1 second just to allow invocation logs to pass to user.
|
||||
time.sleep(1)
|
||||
pass
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def download_zip(
|
||||
self,
|
||||
scan_name: str,
|
||||
|
|
|
|||
|
|
@ -2,14 +2,10 @@ from __future__ import annotations
|
|||
from typing import TypeAlias
|
||||
from collections.abc import Sequence, Mapping
|
||||
|
||||
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
|
||||
from labthings_fastapi.dependencies.thing import direct_thing_client_dependency
|
||||
import labthings_fastapi as lt
|
||||
|
||||
|
||||
class BaseStage(Thing):
|
||||
class BaseStage(lt.Thing):
|
||||
"""A base stage class for OpenFlexure translation stages
|
||||
|
||||
This can't be used directly but should reduce boilerplate code when
|
||||
|
|
@ -20,12 +16,12 @@ class BaseStage(Thing):
|
|||
|
||||
_axis_names = ("x", "y", "z")
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def axis_names(self) -> Sequence[str]:
|
||||
"""The names of the stage's axes, in order."""
|
||||
return self._axis_names
|
||||
|
||||
position = ThingProperty(
|
||||
position = lt.ThingProperty(
|
||||
Mapping[str, int],
|
||||
dict.fromkeys(_axis_names, 0),
|
||||
description="Current position of the stage",
|
||||
|
|
@ -33,7 +29,7 @@ class BaseStage(Thing):
|
|||
observable=True,
|
||||
)
|
||||
|
||||
moving = ThingProperty(
|
||||
moving = lt.ThingProperty(
|
||||
bool,
|
||||
False,
|
||||
description="Whether the stage is in motion",
|
||||
|
|
@ -46,10 +42,10 @@ class BaseStage(Thing):
|
|||
"""Summary metadata describing the current state of the stage"""
|
||||
return {"position": self.position}
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def move_relative(
|
||||
self,
|
||||
cancel: CancelHook,
|
||||
cancel: lt.deps.CancelHook,
|
||||
block_cancellation: bool = False,
|
||||
**kwargs: Mapping[str, int],
|
||||
):
|
||||
|
|
@ -58,10 +54,10 @@ class BaseStage(Thing):
|
|||
"StageThings must define their own move_relative method"
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def move_absolute(
|
||||
self,
|
||||
cancel: CancelHook,
|
||||
cancel: lt.deps.CancelHook,
|
||||
block_cancellation: bool = False,
|
||||
**kwargs: Mapping[str, int],
|
||||
):
|
||||
|
|
@ -70,7 +66,7 @@ class BaseStage(Thing):
|
|||
"StageThings must define their own move_absolute method"
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def set_zero_position(self):
|
||||
"""Make the current position zero in all axes
|
||||
|
||||
|
|
@ -83,4 +79,6 @@ class BaseStage(Thing):
|
|||
)
|
||||
|
||||
|
||||
StageDependency: TypeAlias = direct_thing_client_dependency(BaseStage, "/stage/")
|
||||
StageDependency: TypeAlias = lt.deps.direct_thing_client_dependency(
|
||||
BaseStage, "/stage/"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
from __future__ import annotations
|
||||
from labthings_fastapi.decorators import thing_action
|
||||
from labthings_fastapi.dependencies.invocation import (
|
||||
CancelHook,
|
||||
InvocationCancelledError,
|
||||
)
|
||||
|
||||
from collections.abc import Mapping
|
||||
import time
|
||||
|
||||
import labthings_fastapi as lt
|
||||
|
||||
from . import BaseStage
|
||||
|
||||
|
||||
|
|
@ -27,10 +25,10 @@ class DummyStage(BaseStage):
|
|||
def __exit__(self, _exc_type, _exc_value, _traceback):
|
||||
pass
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def move_relative(
|
||||
self,
|
||||
cancel: CancelHook,
|
||||
cancel: lt.deps.CancelHook,
|
||||
block_cancellation: bool = False,
|
||||
**kwargs: Mapping[str, int],
|
||||
):
|
||||
|
|
@ -53,7 +51,7 @@ class DummyStage(BaseStage):
|
|||
for k, v in zip(self.axis_names, displacement)
|
||||
}
|
||||
fraction_complete = 1.0
|
||||
except InvocationCancelledError as e:
|
||||
except lt.exceptions.InvocationCancelledError as e:
|
||||
# If the move has been cancelled, stop it but don't handle the exception.
|
||||
# We need the exception to propagate in order to stop any calling tasks,
|
||||
# and to mark the invocation as "cancelled" rather than stopped.
|
||||
|
|
@ -66,10 +64,10 @@ class DummyStage(BaseStage):
|
|||
}
|
||||
self.instantaneous_position = self.position
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def move_absolute(
|
||||
self,
|
||||
cancel: CancelHook,
|
||||
cancel: lt.deps.CancelHook,
|
||||
block_cancellation: bool = False,
|
||||
**kwargs: Mapping[str, int],
|
||||
):
|
||||
|
|
@ -83,7 +81,7 @@ class DummyStage(BaseStage):
|
|||
cancel, block_cancellation=block_cancellation, **displacement
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def set_zero_position(self):
|
||||
"""Make the current position zero in all axes
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@ from contextlib import contextmanager
|
|||
from collections.abc import Mapping
|
||||
|
||||
import sangaboard
|
||||
from labthings_fastapi.decorators import thing_action
|
||||
from labthings_fastapi.dependencies.invocation import (
|
||||
CancelHook,
|
||||
InvocationCancelledError,
|
||||
)
|
||||
import labthings_fastapi as lt
|
||||
|
||||
from . import BaseStage
|
||||
|
||||
|
|
@ -57,10 +53,10 @@ class SangaboardThing(BaseStage):
|
|||
with self.sangaboard() as sb:
|
||||
self.position = dict(zip(self.axis_names, sb.position))
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def move_relative(
|
||||
self,
|
||||
cancel: CancelHook,
|
||||
cancel: lt.deps.CancelHook,
|
||||
block_cancellation: bool = False,
|
||||
**kwargs: Mapping[str, int],
|
||||
) -> None:
|
||||
|
|
@ -75,7 +71,7 @@ class SangaboardThing(BaseStage):
|
|||
else:
|
||||
while sb.query("moving?") == "true":
|
||||
cancel.sleep(0.1)
|
||||
except InvocationCancelledError as e:
|
||||
except lt.exceptions.InvocationCancelledError as e:
|
||||
# If the move has been cancelled, stop it but don't handle the exception.
|
||||
# We need the exception to propagate in order to stop any calling tasks,
|
||||
# and to mark the invocation as "cancelled" rather than stopped.
|
||||
|
|
@ -85,10 +81,10 @@ class SangaboardThing(BaseStage):
|
|||
self.moving = False
|
||||
self.update_position()
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def move_absolute(
|
||||
self,
|
||||
cancel: CancelHook,
|
||||
cancel: lt.deps.CancelHook,
|
||||
block_cancellation: bool = False,
|
||||
**kwargs: Mapping[str, int],
|
||||
) -> None:
|
||||
|
|
@ -104,7 +100,7 @@ class SangaboardThing(BaseStage):
|
|||
cancel, block_cancellation=block_cancellation, **displacement
|
||||
)
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def set_zero_position(self) -> None:
|
||||
"""Make the current position zero in all axes
|
||||
|
||||
|
|
@ -116,7 +112,7 @@ class SangaboardThing(BaseStage):
|
|||
sb.zero_position()
|
||||
self.update_position()
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def flash_led(
|
||||
self,
|
||||
number_of_flashes: int = 10,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ This module defines a Thing that can shut down or restart the host computer.
|
|||
|
||||
import subprocess
|
||||
import os
|
||||
from labthings_fastapi.thing import Thing
|
||||
from labthings_fastapi.decorators import thing_action, thing_property
|
||||
import labthings_fastapi as lt
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
|
|
@ -16,12 +15,12 @@ class CommandOutput(BaseModel):
|
|||
error: str
|
||||
|
||||
|
||||
class SystemControlThing(Thing):
|
||||
class SystemControlThing(lt.Thing):
|
||||
"""
|
||||
Attempt to shutdown the device
|
||||
"""
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def shutdown(self) -> CommandOutput:
|
||||
"""
|
||||
Attempt to shutdown the device
|
||||
|
|
@ -35,14 +34,14 @@ class SystemControlThing(Thing):
|
|||
out, err = p.communicate()
|
||||
return CommandOutput(output=out, error=err)
|
||||
|
||||
@thing_property
|
||||
@lt.thing_property
|
||||
def is_raspberrypi() -> bool:
|
||||
"""
|
||||
Checks if we are running on a Raspberry Pi.
|
||||
"""
|
||||
return os.path.exists("/usr/bin/raspi-config")
|
||||
|
||||
@thing_action
|
||||
@lt.thing_action
|
||||
def reboot(self) -> CommandOutput:
|
||||
"""Attempt to reboot the device"""
|
||||
p = subprocess.Popen(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue