Restructured text fixes so that pydoctor would return without an error
This commit is contained in:
parent
58b056988a
commit
a84a916719
31 changed files with 269 additions and 256 deletions
|
|
@ -43,7 +43,6 @@ class RecentringThing(lt.Thing):
|
|||
much between these sites, making the procedure more sensitive
|
||||
to noise or a failed autofocus.
|
||||
"""
|
||||
|
||||
max_steps = 20
|
||||
dx = lateral_distance
|
||||
|
||||
|
|
|
|||
|
|
@ -33,10 +33,10 @@ class StackParams:
|
|||
:param stack_dz: The number of motor steps between images
|
||||
:param images_to_save: The number of images to save to disk
|
||||
:param min_images_to_test: The minimum number of images in the stack before, the
|
||||
stack is evaluated for focus. As more images are captured evaluation of the focus
|
||||
is always evaluated with the same number of images. i.e. if min_images_to_test=9,
|
||||
then 9 images are captured, if the stack is not well focused, a 10th image is
|
||||
captured and images 2 to 10 are evaluated for focus
|
||||
stack is evaluated for focus. As more images are captured evaluation of the
|
||||
focus is always evaluated with the same number of images. i.e. if
|
||||
``min_images_to_test=9``, then 9 images are captured, if the stack is not well
|
||||
focused, a 10th image is captured and images 2 to 10 are evaluated for focus
|
||||
:param autofocus_dz: The number of steps in a full autofocus (when required)
|
||||
:param images_dir: The directory to save images to disk
|
||||
:param save_resolution: The resolution to save the captures to disk with
|
||||
|
|
@ -101,7 +101,8 @@ class StackParams:
|
|||
|
||||
Note that this is the range of the minimum number of images captured,
|
||||
which is also the range of the images stored in memory that can be
|
||||
saved."""
|
||||
saved.
|
||||
"""
|
||||
return self.stack_dz * (self.min_images_to_test - 1)
|
||||
|
||||
@property
|
||||
|
|
@ -109,7 +110,6 @@ class StackParams:
|
|||
"""
|
||||
The distance to deliberately undershoot the estimated optimal starting point
|
||||
"""
|
||||
|
||||
# Starting too low by "steps_undershoot" makes smart stacking faster.
|
||||
# Starting a stack too high requires it to move to the start,
|
||||
# autofocus and then re-stack. Starting slightly too low only
|
||||
|
|
@ -157,7 +157,7 @@ def _get_capture_by_id(captures: list[CaptureInfo], buffer_id: int) -> CaptureIn
|
|||
|
||||
:returns: the CaptureInfo object of the capture with matching id
|
||||
|
||||
:raises: ValueError if buffer_id does not match the buffer_id of any captures
|
||||
:raises ValueError: if buffer_id does not match the buffer_id of any captures
|
||||
"""
|
||||
return captures[_get_capture_index_by_id(captures, buffer_id)]
|
||||
|
||||
|
|
@ -171,7 +171,7 @@ def _get_capture_index_by_id(captures: list[CaptureInfo], buffer_id: int) -> int
|
|||
|
||||
:returns: the list index of the capture with matching id
|
||||
|
||||
:raises: ValueError if buffer_id does not match the buffer_id of any captures
|
||||
:raises ValueError: if buffer_id does not match the buffer_id of any captures
|
||||
"""
|
||||
ids = [capture.buffer_id for capture in captures]
|
||||
if buffer_id not in ids:
|
||||
|
|
@ -290,7 +290,8 @@ class AutofocusThing(lt.Thing):
|
|||
|
||||
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)"""
|
||||
field of view to assess focus (autofocus and testing the success of a z-stack)
|
||||
"""
|
||||
|
||||
@lt.thing_action
|
||||
def fast_autofocus(
|
||||
|
|
@ -336,9 +337,9 @@ class AutofocusThing(lt.Thing):
|
|||
for the moves. This can be used to calibrate autofocus.
|
||||
|
||||
Each move is relative to the last one, i.e. we will finish at
|
||||
`sum(dz)` relative to the starting position.
|
||||
``sum(dz)`` relative to the starting position.
|
||||
|
||||
If `wait` is specified, we will wait for that many seconds
|
||||
If ``wait`` is specified, we will wait for that many seconds
|
||||
between moves.
|
||||
"""
|
||||
with sharpness_monitor.run():
|
||||
|
|
@ -358,9 +359,9 @@ class AutofocusThing(lt.Thing):
|
|||
):
|
||||
"""Repeatedly autofocus the stage until it looks focused.
|
||||
|
||||
This action will run the `fast_autofocus` action until it settles on a point
|
||||
This action will run the ``fast_autofocus`` action until it settles on a point
|
||||
in the middle 3/5 of its range. Such logic can be helpful if the microscope
|
||||
is close to focus, but not quite within `dz/2`. It will attempt to autofocus
|
||||
is close to focus, but not quite within ``dz/2``. It will attempt to autofocus
|
||||
up to 10 times.
|
||||
"""
|
||||
repeat = True
|
||||
|
|
@ -444,17 +445,18 @@ class AutofocusThing(lt.Thing):
|
|||
:param cam: Camera Dependency supplied by LabThings dependency injection
|
||||
:param stage: Stage Dependency supplied by LabThings dependency injection
|
||||
:param sharpness_monitor: Sharpness Monitor Dependency (for focus detection)
|
||||
supplied by LabThings dependency injection
|
||||
supplied by LabThings dependency injection
|
||||
:param images_dir: the folder to save all images
|
||||
:param autofocus_dz: the range to autofocus over if a stack fails
|
||||
:param save_resolution: The resolution the images should be saved at, the
|
||||
images will be resampled if this doesn't match the camera's capture resolution
|
||||
images will be resampled if this doesn't match the camera's capture
|
||||
resolution
|
||||
|
||||
:returns: A tuple containing:
|
||||
- A boolean, True if stack was successfully
|
||||
- The z position of the sharpest image
|
||||
"""
|
||||
|
||||
* A boolean, True if stack was successfully
|
||||
* The z position of the sharpest image
|
||||
"""
|
||||
# Set the variables to prevent changes from the GUI or other windows
|
||||
stack_parameters = StackParams(
|
||||
stack_dz=self.stack_dz,
|
||||
|
|
@ -511,11 +513,12 @@ class AutofocusThing(lt.Thing):
|
|||
"""Return to the initial height of the current stack, and run
|
||||
a looping autofocus.
|
||||
|
||||
Arguments:
|
||||
initial_z_pos: The initial z positions of previous captures
|
||||
autofocus_dz: the range in steps to autofocus
|
||||
variables stage and sharpness_monitor are Thing dependencies passed through from
|
||||
the calling action
|
||||
:param initial_z_pos: The initial z positions of previous captures
|
||||
:param autofocus_dz: the range in steps to autofocus
|
||||
|
||||
``stage`` and ``sharpness_monitor`` are Thing dependencies passed through
|
||||
from the calling action
|
||||
|
||||
"""
|
||||
stage.move_absolute(z=initial_z_pos)
|
||||
self.looping_autofocus(
|
||||
|
|
@ -534,14 +537,13 @@ class AutofocusThing(lt.Thing):
|
|||
"""Save the required captures to disk. Will save the sharpest image,
|
||||
and any images either side of focus.
|
||||
|
||||
Arguments:
|
||||
sharpest_id: the buffer id index of the sharpest image
|
||||
captures: a list of captures, including file name, image data and metadata
|
||||
stack_parameters: a StackParams object holding stack parameters
|
||||
variables logger and capture are Thing dependencies passed through from the
|
||||
calling action
|
||||
"""
|
||||
:param sharpest_id: the buffer id index of the sharpest image
|
||||
:param captures: a list of captures, including file name, image data and
|
||||
metadata
|
||||
:param stack_parameters: a StackParams object holding stack parameters
|
||||
:param cam: is a Thing dependency passed through from the calling action
|
||||
|
||||
"""
|
||||
sharpest_index = _get_capture_index_by_id(captures, sharpest_id)
|
||||
slice_to_save = stack_parameters.slice_to_save(sharpest_index)
|
||||
|
||||
|
|
@ -569,9 +571,10 @@ class AutofocusThing(lt.Thing):
|
|||
:param stage: Stage Dependency to be passed through from the calling action
|
||||
|
||||
:returns: A tuple of
|
||||
- the stack result (True for successful stack, False for failed stack),
|
||||
- a list of CaptureInfo objects,
|
||||
- the buffer_id of the shapest image (or None if the stack failed)
|
||||
|
||||
* the stack result (True for successful stack, False for failed stack),
|
||||
* a list of CaptureInfo objects,
|
||||
* the buffer_id of the shapest image (or None if the stack failed)
|
||||
"""
|
||||
# Move down by the height of the z stack, plus an overshoot
|
||||
# Better to start too low and take too many images than too high and need to refocus
|
||||
|
|
@ -628,11 +631,11 @@ class AutofocusThing(lt.Thing):
|
|||
|
||||
:param cam: Camera Dependency to be passed through from the calling action
|
||||
:param stage: Stage Dependency to be passed through from the calling action
|
||||
:buffer_max: The maximum number of images to tell the camera to keep in memory
|
||||
for saving once the stack is complete
|
||||
:param buffer_max: The maximum number of images to tell the camera to keep in memory
|
||||
for saving once the stack is complete
|
||||
|
||||
:return: A CaptureInfo object containing the capture information including its
|
||||
camera buffer_id needed for saving.
|
||||
:returns: A CaptureInfo object containing the capture information including its
|
||||
camera buffer_id needed for saving.
|
||||
"""
|
||||
stage_location = stage.position
|
||||
buffer_id = cam.capture_to_memory(buffer_max=buffer_max)
|
||||
|
|
@ -649,14 +652,19 @@ class AutofocusThing(lt.Thing):
|
|||
stack is centrally enough in the stack
|
||||
|
||||
:param captures: a list of the capture objects to for testing if the
|
||||
sharpeness has converged in the centre
|
||||
sharpness has converged in the centre
|
||||
|
||||
:return: A tuple with two values:
|
||||
- result - which is one of three literal values:
|
||||
'success' if the sharpest image is towards the centre
|
||||
'continue' if the sharpest image is in the final two images of the list
|
||||
'restart' if the sharpest image is in the first two images of the list
|
||||
- capture_id - the buffer id of the sharpest image
|
||||
:returns: A tuple with two values:
|
||||
|
||||
* result - which is one of three literal values:
|
||||
|
||||
* ``success`` if the sharpest image is towards the centre
|
||||
* ``continue`` if the sharpest image is in the final two images of the
|
||||
list
|
||||
* ``restart`` if the sharpest image is in the first two images of the
|
||||
list
|
||||
|
||||
* capture_id - the buffer id of the sharpest image
|
||||
"""
|
||||
sharpest_index = np.argmax([capture.sharpness for capture in captures])
|
||||
# The buffer id of the sharpest image
|
||||
|
|
|
|||
|
|
@ -70,12 +70,12 @@ class CameraMemoryBuffer:
|
|||
every time an image is added.
|
||||
|
||||
:param image: The image to add. A PIL image is recommended, but cameras
|
||||
can choose to use other formats
|
||||
can choose to use other formats
|
||||
:param metadata: Optional, a dictionary of the image metadata.
|
||||
:param buffer_max: The maximum number of images that should be in the buffer
|
||||
once this images is added. Default is 1.
|
||||
once this images is added. Default is 1.
|
||||
|
||||
:return buffer_id: The id in the buffer for this image
|
||||
:returns: The id in the buffer for this image
|
||||
"""
|
||||
self._latest_id += 1
|
||||
self._create_space(buffer_max)
|
||||
|
|
@ -94,9 +94,8 @@ class CameraMemoryBuffer:
|
|||
|
||||
:param buffer_id: The buffer id of the image to retrieve
|
||||
:param remove: True (default) to remove this image from the buffer, False
|
||||
to leave the image in the buffer.
|
||||
to leave the image in the buffer.
|
||||
"""
|
||||
|
||||
# No id given
|
||||
if buffer_id is None:
|
||||
# Get the latest image and metadata tuple from storage
|
||||
|
|
@ -128,7 +127,7 @@ class CameraMemoryBuffer:
|
|||
Create space to add an image.
|
||||
|
||||
:param buffer_max: The maximum number of images that should be in the buffer
|
||||
once another images is added.
|
||||
once another images is added.
|
||||
"""
|
||||
# If only one image to be stored just clear the storage and return
|
||||
if buffer_max <= 1:
|
||||
|
|
@ -164,7 +163,8 @@ class BaseCamera(lt.Thing):
|
|||
self, main_resolution: tuple[int, int], buffer_count: int
|
||||
) -> None:
|
||||
"""Start (or stop and restart) the camera with the given resolution
|
||||
for the main stream, and buffer_count number of images in the buffer"""
|
||||
for the main stream, and buffer_count number of images in the buffer
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"CameraThings must define their own start_streaming method"
|
||||
)
|
||||
|
|
@ -175,9 +175,9 @@ class BaseCamera(lt.Thing):
|
|||
|
||||
This is called when uvicorn gets the a shutdown signal. As this is called from
|
||||
the event loop it cannot interact with the our ThingProperties or run
|
||||
`self.mjpeg_stream.stop()` as the portal cannot be called from this loop.
|
||||
``self.mjpeg_stream.stop()`` as the portal cannot be called from this loop.
|
||||
|
||||
Instead we just set the `_streaming` value to False. This stops the async frame
|
||||
Instead we just set the ``_streaming`` value to False. This stops the async frame
|
||||
generator when the next frame notifies.
|
||||
"""
|
||||
if self.stream_active:
|
||||
|
|
@ -186,7 +186,7 @@ class BaseCamera(lt.Thing):
|
|||
|
||||
@lt.thing_property
|
||||
def stream_active(self) -> bool:
|
||||
"Whether the MJPEG stream is active"
|
||||
"""Whether the MJPEG stream is active"""
|
||||
raise NotImplementedError(
|
||||
"CameraThings must define their own stream_active method"
|
||||
)
|
||||
|
|
@ -221,7 +221,7 @@ class BaseCamera(lt.Thing):
|
|||
) -> JPEGBlob:
|
||||
"""Acquire one image from the preview stream and return as an array
|
||||
|
||||
This differs from `capture_jpeg` in that it does not pause the MJPEG
|
||||
This differs from ``capture_jpeg`` in that it does not pause the MJPEG
|
||||
preview stream. Instead, we simply return the next frame from that
|
||||
stream (either "main" for the preview stream, or "lores" for the low
|
||||
resolution preview). No metadata is returned.
|
||||
|
|
@ -267,11 +267,11 @@ class BaseCamera(lt.Thing):
|
|||
|
||||
:param jpeg_path: The path to save the file to
|
||||
:param logger: This should be injected automatically by Labthings FastAPI
|
||||
when calling the action
|
||||
when calling the action
|
||||
:param metadata_getter: This should be injected automatically by Labthings
|
||||
FastAPI when calling the action
|
||||
FastAPI when calling the action
|
||||
:param save_resolution: can be set to resize the image before saving. By
|
||||
default this is None meaning that the image is saved at original resolution.
|
||||
default this is None meaning that the image is saved at original resolution.
|
||||
"""
|
||||
image, metadata = self._robust_image_capture(
|
||||
metadata_getter,
|
||||
|
|
@ -294,19 +294,19 @@ class BaseCamera(lt.Thing):
|
|||
buffer_max: int = 1,
|
||||
) -> None:
|
||||
"""
|
||||
Capture an image to memory. This can be saved later with `save_from_memory`
|
||||
Capture an image to memory. This can be saved later with ``save_from_memory``
|
||||
|
||||
Note that only one image is held in memory so this will overwrite any image
|
||||
in memory.
|
||||
|
||||
:param logger: This should be injected automatically by Labthings FastAPI
|
||||
when calling the action
|
||||
when calling the action
|
||||
:param metadata_getter: This should be injected automatically by Labthings
|
||||
FastAPI when calling the action
|
||||
FastAPI when calling the action
|
||||
:param buffer_max: The maximum number of images that should be in the buffer
|
||||
once this images is added. Default is 1.
|
||||
once this images is added. Default is 1.
|
||||
|
||||
:return: the buffer id of the image captured
|
||||
:returns: the buffer id of the image captured
|
||||
"""
|
||||
image, metadata = self._robust_image_capture(metadata_getter, logger)
|
||||
return self._memory_buffer.add_image(image, metadata, buffer_max=buffer_max)
|
||||
|
|
@ -324,11 +324,12 @@ class BaseCamera(lt.Thing):
|
|||
|
||||
:param jpeg_path: The path to save the file to
|
||||
:param logger: This should be injected automatically by Labthings FastAPI
|
||||
when calling the action
|
||||
when calling the action
|
||||
:param save_resolution: can be set to resize the image before saving. By
|
||||
default this is None meaning that the image is saved at original resolution.
|
||||
default this is None meaning that the image is saved at original
|
||||
resolution.
|
||||
:param buffer_id: The buffer id of the image to save, this was returned by
|
||||
`capture_to_memory`
|
||||
``capture_to_memory``
|
||||
"""
|
||||
image, metadata = self._memory_buffer.get_image(buffer_id)
|
||||
|
||||
|
|
@ -379,7 +380,8 @@ class BaseCamera(lt.Thing):
|
|||
"""Saving the captured image and metadata to disk
|
||||
logger warning (via InvocationLogger) is raised if metadata is failed to be added
|
||||
IOError is raised if the file cannot be saved
|
||||
nothing is returned on success"""
|
||||
nothing is returned on success
|
||||
"""
|
||||
if save_resolution is not None and image.size != save_resolution:
|
||||
image = image.resize(save_resolution, Image.BOX)
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""OpenFlexure Microscope OpenCV Camera
|
||||
|
||||
This module defines a camera Thing that uses OpenCV's
|
||||
`VideoCapture`.
|
||||
``VideoCapture``.
|
||||
|
||||
See repository root for licensing information.
|
||||
"""
|
||||
|
|
@ -45,7 +45,7 @@ class OpenCVCamera(BaseCamera):
|
|||
|
||||
@lt.thing_property
|
||||
def stream_active(self) -> bool:
|
||||
"Whether the MJPEG stream is active"
|
||||
"""Whether the MJPEG stream is active"""
|
||||
if self._capture_enabled and self._capture_thread:
|
||||
return self._capture_thread.is_alive()
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -264,7 +264,6 @@ class StreamingPiCamera2(BaseCamera):
|
|||
@sensor_mode.setter
|
||||
def sensor_mode(self, new_mode: Optional[SensorModeSelector | dict]):
|
||||
"""Change the sensor mode used"""
|
||||
|
||||
if new_mode is None:
|
||||
self._sensor_mode = None
|
||||
elif isinstance(new_mode, SensorModeSelector):
|
||||
|
|
@ -287,9 +286,9 @@ class StreamingPiCamera2(BaseCamera):
|
|||
tuning = lt.ThingSetting(Optional[dict], None, readonly=True)
|
||||
|
||||
def _initialise_picamera(self):
|
||||
"""Acquire the picamera device and store it as `self._picamera`.
|
||||
"""Acquire the picamera device and store it as ``self._picamera``.
|
||||
|
||||
This duplicates logic in `Picamera2.__init__` to provide a tuning file that
|
||||
This duplicates logic in ``Picamera2.__init__`` to provide a tuning file that
|
||||
will be read when the camera system initialises.
|
||||
"""
|
||||
if self._picamera_lock is not None:
|
||||
|
|
@ -332,16 +331,17 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
@contextmanager
|
||||
def _streaming_picamera(self, pause_stream=False) -> Iterator[Picamera2]:
|
||||
"""Lock access to picamera and return the underlying `Picamera2` instance.
|
||||
"""Lock access to picamera and return the underlying ``Picamera2`` instance.
|
||||
|
||||
Optionally the stream can be paused to allow updating the camera settings.
|
||||
|
||||
:param pause_stream: If False the `Picamera2` instance is simply yielded.
|
||||
If True:
|
||||
* Stop the MJPEG Stream
|
||||
* Yield the `Picamera2` instance for function calling the context manager to
|
||||
make changes.
|
||||
* On closing of the context manager the stream will restart.
|
||||
:param pause_stream: If False the ``Picamera2`` instance is simply yielded.
|
||||
If True:
|
||||
|
||||
* Stop the MJPEG Stream
|
||||
* Yield the ``Picamera2`` instance for function calling the context manager to
|
||||
make changes.
|
||||
* On closing of the context manager the stream will restart.
|
||||
"""
|
||||
already_streaming = self.stream_active
|
||||
with self._picamera_lock:
|
||||
|
|
@ -372,8 +372,9 @@ class StreamingPiCamera2(BaseCamera):
|
|||
manual.
|
||||
|
||||
Create two streams:
|
||||
- `lores_mjpeg_stream` for autofocus at low-res resolution
|
||||
- `mjpeg_stream` for preview. This is the `main_resolution` if this is less
|
||||
|
||||
* ``lores_mjpeg_stream`` for autofocus at low-res resolution
|
||||
* ``mjpeg_stream`` for preview. This is the ``main_resolution`` if this is less
|
||||
than (1280, 960), or the low-res resolution if above. This allows for
|
||||
high resolution capture without streaming high resolution video.
|
||||
|
||||
|
|
@ -489,7 +490,6 @@ class StreamingPiCamera2(BaseCamera):
|
|||
A TimeoutError is raised if this time is exceeded during capture.
|
||||
Default = 0.9s, lower than the 1s timeout default in picamera yaml settings
|
||||
"""
|
||||
|
||||
# This was slower than capture_image for our use case, but directly returning
|
||||
# an image as an array is still a useful feature
|
||||
if stream_name == "full":
|
||||
|
|
@ -523,13 +523,13 @@ class StreamingPiCamera2(BaseCamera):
|
|||
) -> JPEGBlob:
|
||||
"""Acquire one image from the camera as a JPEG
|
||||
|
||||
The JPEG will be acquired using `Picamera2.capture_file`. If the
|
||||
`resolution` parameter is `main` or `lores`, it will be captured
|
||||
The JPEG will be acquired using ``Picamera2.capture_file``. If the
|
||||
``resolution`` parameter is ``main`` or ``lores``, it will be captured
|
||||
from the main preview stream, or the low-res preview stream,
|
||||
respectively. This means the camera won't be reconfigured, and
|
||||
the stream will not pause (though it may miss one frame).
|
||||
|
||||
If `full` resolution is requested, we will briefly pause the
|
||||
If ``full`` resolution is requested, we will briefly pause the
|
||||
MJPEG stream and reconfigure the camera to capture a full
|
||||
resolution image.
|
||||
|
||||
|
|
@ -588,11 +588,12 @@ class StreamingPiCamera2(BaseCamera):
|
|||
the image reaches the specified white level.
|
||||
|
||||
:param target_white_level: The target 10bit white level. 10-bit data has a
|
||||
theoretical maximum of 1023, but with black level correction the true maxiumum
|
||||
is about 950. Default is 700 as this is approximately 70% saturated.
|
||||
theoretical maximum of 1023, but with black level correction the true
|
||||
maximum is about 950. Default is 700 as this is approximately 70%
|
||||
saturated.
|
||||
:param percentile: The percentile to use instead of maximum. Default 99.9. When
|
||||
calculating the brightest pixel, a percentile is used rather than the
|
||||
maximum in order to be robust to a small number of noisy/bright pixels.
|
||||
calculating the brightest pixel, a percentile is used rather than the
|
||||
maximum in order to be robust to a small number of noisy/bright pixels.
|
||||
"""
|
||||
with self._streaming_picamera(pause_stream=True) as cam:
|
||||
recalibrate_utils.adjust_shutter_and_gain_from_raw(
|
||||
|
|
@ -615,7 +616,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
image with the lens shading correction applied, which should mean
|
||||
that the image is uniform, rather than weighted towards the centre.
|
||||
|
||||
If `method` is `"centre"`, we will correct the mean of the central 10%
|
||||
If ``method`` is ``"centre"``, we will correct the mean of the central 10%
|
||||
of the image.
|
||||
"""
|
||||
with self._streaming_picamera(pause_stream=True) as cam:
|
||||
|
|
@ -657,7 +658,7 @@ class StreamingPiCamera2(BaseCamera):
|
|||
def colour_correction_matrix(
|
||||
self,
|
||||
) -> tuple[float, float, float, float, float, float, float, float, float]:
|
||||
"""The `colour_correction_matrix` from the tuning file.
|
||||
"""The ``colour_correction_matrix`` from the tuning file.
|
||||
|
||||
This is broken out into its own property for convenience and compatibility with
|
||||
the micromanager API
|
||||
|
|
@ -720,11 +721,11 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
This function will call the other calibration actions in sequence:
|
||||
|
||||
* `flat_lens_shading` to disable flat-field
|
||||
* `auto_expose_from_minimum`
|
||||
* `set_static_green_equalisation` to set geq offset to max
|
||||
* `calibrate_lens_shading`
|
||||
* `calibrate_white_balance`
|
||||
* ``flat_lens_shading`` to disable flat-field
|
||||
* ``auto_expose_from_minimum``
|
||||
* ``set_static_green_equalisation`` to set geq offset to max
|
||||
* ``calibrate_lens_shading``
|
||||
* ``calibrate_white_balance``
|
||||
"""
|
||||
self.flat_lens_shading()
|
||||
self.auto_expose_from_minimum()
|
||||
|
|
@ -804,15 +805,15 @@ class StreamingPiCamera2(BaseCamera):
|
|||
|
||||
The white balance algorithm we use assumes the brightest pixels
|
||||
should be white, and that the only thing affecting the colour of
|
||||
said pixels is the `colour_gains`.
|
||||
said pixels is the ``colour_gains``.
|
||||
|
||||
The lens shading correction is normalised such that the *minimum*
|
||||
gain in the `Cr` and `Cb` channels is 1. The white balance
|
||||
gain in the ``Cr`` and ``Cb`` channels is 1. The white balance
|
||||
assumption above requires that the gain for the brightest pixels
|
||||
is 1. The solution might be that, when calibrating, we note which
|
||||
pixels are brightest (usually the centre) and explicitly use
|
||||
the LST values for there. However, for now I will assume that we
|
||||
need to normalise by the **maximum** of the `Cr` and `Cb`
|
||||
need to normalise by the **maximum** of the ``Cr`` and ``Cb``
|
||||
channels, which is correct the majority of the time.
|
||||
"""
|
||||
if not self.lens_shading_is_static:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ Functions to set up a Raspberry Pi Camera v2 for scientific use
|
|||
|
||||
This module provides slower, simpler functions to set the
|
||||
gain, exposure, and white balance of a Raspberry Pi camera, using
|
||||
the `picamera2` Python library. It's mostly used by the OpenFlexure
|
||||
the ``picamera2`` Python library. It's mostly used by the OpenFlexure
|
||||
Microscope, though it deliberately has no hard dependencies on
|
||||
said software, so that it's useful on its own.
|
||||
|
||||
|
|
@ -20,14 +20,15 @@ to "memory" or nonlinearities in the camera's image processing
|
|||
pipeline, is to use raw images. This is quite slow, but very
|
||||
reliable. The three steps above can be accomplished by:
|
||||
|
||||
```
|
||||
picamera = picamera2.Picamera2()
|
||||
.. code-block:: python
|
||||
|
||||
picamera = picamera2.Picamera2()
|
||||
|
||||
adjust_shutter_and_gain_from_raw(picamera)
|
||||
adjust_white_balance_from_raw(picamera)
|
||||
lst = lst_from_camera(picamera)
|
||||
picamera.lens_shading_table = lst
|
||||
|
||||
adjust_shutter_and_gain_from_raw(picamera)
|
||||
adjust_white_balance_from_raw(picamera)
|
||||
lst = lst_from_camera(picamera)
|
||||
picamera.lens_shading_table = lst
|
||||
```
|
||||
"""
|
||||
|
||||
# Disable N806 & 803, which checks that all variables and args are lowercase.
|
||||
|
|
@ -55,7 +56,7 @@ def load_default_tuning(cam: Picamera2) -> dict:
|
|||
"""Load the default tuning file for the camera
|
||||
|
||||
This will open and close the camera to determine its model. If you are
|
||||
using a model that's supported by `picamera2` it should have a tuning
|
||||
using a model that's supported by ``picamera2`` it should have a tuning
|
||||
file built in. If not, this will probably crash with an error.
|
||||
|
||||
Error handling for unsupported cameras is not something we are likely
|
||||
|
|
@ -152,25 +153,19 @@ def adjust_shutter_and_gain_from_raw(
|
|||
This routine is slow but effective. It uses raw images, so we
|
||||
are not affected by white balance or digital gain.
|
||||
|
||||
:param camera: A Picamera2 object.
|
||||
:param target_white_level: The raw, 10-bit value we aim for. The brightest pixels
|
||||
should be approximately this bright. Maximum possible is about 900, 700 is
|
||||
reasonable.
|
||||
:param max_iterations: We will terminate once we perform this many iterations,
|
||||
whether or not we converge. More than 10 shouldn't happen.
|
||||
:param tolerance: How close to the target value we consider "done". Expressed as a
|
||||
fraction of the ``target_white_level`` so 0.05 means +/- 5%
|
||||
:param percentile: Rather then use the maximum value for each channel, we calculate
|
||||
a percentile. This makes us robust to single pixels that are bright/noisy.
|
||||
99.9% still picks the top of the brightness range, but seems much more reliable
|
||||
than just ``np.max()``.
|
||||
|
||||
Arguments:
|
||||
target_white_level:
|
||||
The raw, 10-bit value we aim for. The brightest pixels
|
||||
should be approximately this bright. Maximum possible
|
||||
is about 900, 700 is reasonable.
|
||||
max_iterations:
|
||||
We will terminate once we perform this many iterations,
|
||||
whether or not we converge. More than 10 shouldn't happen.
|
||||
tolerance:
|
||||
How close to the target value we consider "done". Expressed
|
||||
as a fraction of the ``target_white_level`` so 0.05 means
|
||||
+/- 5%
|
||||
percentile:
|
||||
Rather then use the maximum value for each channel, we
|
||||
calculate a percentile. This makes us robust to single
|
||||
pixels that are bright/noisy. 99.9% still picks the top
|
||||
of the brightness range, but seems much more reliable
|
||||
than just ``np.max()``.
|
||||
"""
|
||||
# TODO: read black level and bit depth from camera?
|
||||
if target_white_level * (tolerance + 1) >= 959:
|
||||
|
|
@ -351,7 +346,7 @@ def get_16x12_grid(chan: np.ndarray, dx: int, dy: int) -> np.ndarray:
|
|||
def upsample_channels(grids: np.ndarray, shape: tuple[int]) -> np.ndarray:
|
||||
"""Zoom an image in the last two dimensions
|
||||
|
||||
This is effectively the inverse operation of `get_16x12_grid`
|
||||
This is effectively the inverse operation of ``get_16x12_grid``
|
||||
"""
|
||||
zoom_factors = [
|
||||
1,
|
||||
|
|
@ -381,7 +376,7 @@ def downsampled_channels(channels: np.ndarray, blacklevel=64) -> list[np.ndarray
|
|||
def lst_from_channels(channels: np.ndarray) -> LensShadingTables:
|
||||
"""Given the 4 Bayer colour channels from a white image, generate a LST.
|
||||
|
||||
Internally, is just calls `downsampled_channels` and `lst_from_grids`.
|
||||
Internally, is just calls ``downsampled_channels`` and ``lst_from_grids``.
|
||||
"""
|
||||
grids = downsampled_channels(channels)
|
||||
return lst_from_grids(grids)
|
||||
|
|
@ -392,11 +387,10 @@ def lst_from_grids(grids: np.ndarray) -> LensShadingTables:
|
|||
|
||||
The grids are the 4 BAYER channels RGGB
|
||||
|
||||
The LST format has changed with `picamera2` and now uses a fixed resolution,
|
||||
The LST format has changed with ``picamera2`` and now uses a fixed resolution,
|
||||
and is in luminance, Cr, Cb format. This function returns three ndarrays of
|
||||
luminance, Cr, Cb, each with shape (12, 16).
|
||||
"""
|
||||
|
||||
# Calculated red, green, and blue channels from Bayer data
|
||||
r: np.ndarray = grids[3, ...]
|
||||
g: np.ndarray = np.mean(grids[1:3, ...], axis=0)
|
||||
|
|
@ -419,7 +413,7 @@ def grids_from_lst(lum: np.ndarray, Cr: np.ndarray, Cb: np.ndarray) -> np.ndarra
|
|||
|
||||
Note that these will be normalised - the maximum green value is always 1.
|
||||
Also, note that the channels are BGGR, to be consistent with the
|
||||
`channels_from_raw_image` function. This should probably change in the
|
||||
``channels_from_raw_image`` function. This should probably change in the
|
||||
future.
|
||||
"""
|
||||
G = 1 / np.array(lum)
|
||||
|
|
@ -434,9 +428,9 @@ def set_static_lst(
|
|||
cr: np.ndarray,
|
||||
cb: np.ndarray,
|
||||
) -> None:
|
||||
"""Update the `rpi.alsc` section of a camera tuning dict to use a static correcton.
|
||||
"""Update the ``rpi.alsc`` section of a camera tuning dict to use a static correcton.
|
||||
|
||||
`tuning` will be updated in-place to set its shading to static, and disable any
|
||||
``tuning`` will be updated in-place to set its shading to static, and disable any
|
||||
adaptive tweaking by the algorithm.
|
||||
"""
|
||||
for table in luminance, cr, cb:
|
||||
|
|
@ -459,9 +453,9 @@ def set_static_ccm(
|
|||
float, float, float, float, float, float, float, float, float
|
||||
],
|
||||
) -> None:
|
||||
"""Update the `rpi.alsc` section of a camera tuning dict to use a static correcton.
|
||||
"""Update the ``rpi.alsc`` section of a camera tuning dict to use a static correcton.
|
||||
|
||||
`tuning` will be updated in-place to set its shading to static, and disable any
|
||||
``tuning`` will be updated in-place to set its shading to static, and disable any
|
||||
adaptive tweaking by the algorithm.
|
||||
"""
|
||||
ccm = Picamera2.find_tuning_algo(tuning, "rpi.ccm")
|
||||
|
|
@ -469,7 +463,7 @@ def set_static_ccm(
|
|||
|
||||
|
||||
def get_static_ccm(tuning: dict) -> None:
|
||||
"""Get the `rpi.ccm` section of a camera tuning dict"""
|
||||
"""Get the ``rpi.ccm`` section of a camera tuning dict"""
|
||||
ccm = Picamera2.find_tuning_algo(tuning, "rpi.ccm")
|
||||
return ccm["ccms"]
|
||||
|
||||
|
|
@ -484,14 +478,13 @@ def set_static_geq(
|
|||
tuning: dict,
|
||||
offset: int = 65535,
|
||||
) -> None:
|
||||
"""Update the `rpi.geq` section of a camera tuning dict to always use green
|
||||
"""Update the ``rpi.geq`` section of a camera tuning dict to always use green
|
||||
equalisation that averages the green pixels in the red and blue rows.
|
||||
|
||||
`tuning` will be updated in-place to set the geq offest to the given value.
|
||||
``tuning`` will be updated in-place to set the geq offest to the given value.
|
||||
The default 65535 is the maximum allowed value. This means
|
||||
the brightness will always be below the threshold where averaging is used.
|
||||
"""
|
||||
|
||||
geq = Picamera2.find_tuning_algo(tuning, "rpi.geq")
|
||||
geq["offset"] = offset # max out offset to disable the adaptive green equalisation
|
||||
|
||||
|
|
@ -511,7 +504,7 @@ def index_of_algorithm(algorithms: list[dict], algorithm: str) -> int:
|
|||
|
||||
|
||||
def copy_alsc_section(from_tuning: dict, to_tuning: dict) -> None:
|
||||
"""Copy the `rpi.alsc` algorithm from one tuning to another.
|
||||
"""Copy the ``rpi.alsc`` algorithm from one tuning to another.
|
||||
|
||||
This is done in-place, i.e. modifying to_tuning.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ class SimulatedCamera(BaseCamera):
|
|||
|
||||
@lt.thing_property
|
||||
def stream_active(self) -> bool:
|
||||
"Whether the MJPEG stream is active"
|
||||
"""Whether the MJPEG stream is active"""
|
||||
if self._capture_enabled and self._capture_thread:
|
||||
return self._capture_thread.is_alive()
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ def downsample(factor: int, image: np.ndarray) -> np.ndarray:
|
|||
"""Downsample an image by taking the mean of each nxn region
|
||||
|
||||
This should be very efficient: we calculate the mean of each
|
||||
`factor * factor` square, no interpolation. If the image is
|
||||
``factor * factor`` square, no interpolation. If the image is
|
||||
not an integer multiple of the resampling factor, we discard
|
||||
the left-over pixels. This avoids odd edge effects and keeps
|
||||
performance quick.
|
||||
|
|
@ -265,14 +265,16 @@ class CameraStageMapper(lt.Thing):
|
|||
that conversion.
|
||||
|
||||
It is often helpful to give a concrete example: to make a move in image coordinates
|
||||
(`dy`, `dx`), where `dx` is horizontal, i.e. the longer dimension of the image, you
|
||||
(``dy``, ``dx``), where ``dx`` is horizontal, i.e. the longer dimension of the image, you
|
||||
should move the stage by:
|
||||
```
|
||||
stage_disp = np.dot(
|
||||
np.array(image_to_stage_displacement_matrix),
|
||||
np.array([dy,dx]),
|
||||
)
|
||||
```
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
stage_disp = np.dot(
|
||||
np.array(image_to_stage_displacement_matrix),
|
||||
np.array([dy,dx]),
|
||||
)
|
||||
|
||||
"""
|
||||
if self.last_calibration is None:
|
||||
return None
|
||||
|
|
@ -316,8 +318,8 @@ class CameraStageMapper(lt.Thing):
|
|||
swap the order of these coordinates. This includes opencv and PIL. So, don't be
|
||||
surprised if you find it necessary to swap x and y around.
|
||||
|
||||
As a general rule, `x` usually corresponds to the longer dimension of the image,
|
||||
and `y` to the shorter one. Checking what shape your chosen toolkit reports for
|
||||
As a general rule, ``x`` usually corresponds to the longer dimension of the image,
|
||||
and ``y`` to the shorter one. Checking what shape your chosen toolkit reports for
|
||||
an image usually helps resolve any ambiguity.
|
||||
"""
|
||||
self.assert_calibrated()
|
||||
|
|
|
|||
|
|
@ -66,9 +66,9 @@ class SettingsManager(lt.Thing):
|
|||
recursively, i.e. if a key exists, it will be added to rather than
|
||||
replaced.
|
||||
|
||||
If a key is supplied, we will treat `data` as being relative to that
|
||||
key, i.e. calling this action with `data={"a":1 }, key="foo/bar"` is
|
||||
equivalent to calling it with `data={"foo": {"bar": {"a": 1}}}`.
|
||||
If a key is supplied, we will treat ``data`` as being relative to that
|
||||
key, i.e. calling this action with ``data={"a":1 }, key="foo/bar"`` is
|
||||
equivalent to calling it with ``data={"foo": {"bar": {"a": 1}}}``.
|
||||
"""
|
||||
metadata = self.external_metadata
|
||||
subdict = metadata
|
||||
|
|
@ -84,8 +84,8 @@ class SettingsManager(lt.Thing):
|
|||
"""Delete a key from the stored metadata.
|
||||
|
||||
The key may contain forward slashes, which are understood to separate
|
||||
levels of the dictionary - i.e. `'a/c'` will remove the `c` key from a
|
||||
dictionary that looks like: `{'a': {'c': 1}, 'b': {'d': 2}}`
|
||||
levels of the dictionary - i.e. ``'a/c'`` will remove the ``c`` key from a
|
||||
dictionary that looks like: ``{'a': {'c': 1}, 'b': {'d': 2}}``
|
||||
"""
|
||||
metadata = self.external_metadata
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -114,7 +114,6 @@ class SmartScanThing(lt.Thing):
|
|||
stopping once it is surrounded by "background" (as detected by the
|
||||
background_detect Thing) or reaches the "max_range" measured in steps.
|
||||
"""
|
||||
|
||||
got_lock = self._scan_lock.acquire(timeout=0.1)
|
||||
if not got_lock:
|
||||
raise RuntimeError("Trying to run scan while scan is already running!")
|
||||
|
|
@ -214,7 +213,6 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
Returns the (x,y,z) with the chosen z_estimate
|
||||
"""
|
||||
|
||||
if z_estimate is None:
|
||||
z_estimate = self._stage.position["z"]
|
||||
|
||||
|
|
@ -233,9 +231,9 @@ class SmartScanThing(lt.Thing):
|
|||
Take a test image and use camera stage mapping to calculate x and y displacement
|
||||
|
||||
:param overlap: The desired overlap as a fraction of the image. i.e. 0.5 means
|
||||
that each image should overlap its nearest neighbour by 50%.
|
||||
that each image should overlap its nearest neighbour by 50%.
|
||||
|
||||
Return (dx, dy) - the x and y displacments in steps
|
||||
:returns: (dx, dy) - the x and y displacments in steps
|
||||
"""
|
||||
test_jpg = self._cam.grab_jpeg()
|
||||
test_image = np.array(Image.open(test_jpg.open()))
|
||||
|
|
@ -375,7 +373,6 @@ class SmartScanThing(lt.Thing):
|
|||
"""
|
||||
Manage the stitching threads, starting them if needed and not already running.
|
||||
"""
|
||||
|
||||
# Assume 4 images means at least one offset in x and y, making the stitching
|
||||
# well constrained.
|
||||
if self._scan_images_taken > 3:
|
||||
|
|
@ -390,7 +387,6 @@ class SmartScanThing(lt.Thing):
|
|||
determine whether the scan should be stitched and the microscope
|
||||
should return to the starting x,y,z position
|
||||
"""
|
||||
|
||||
# Used to check if finally was reached via exception (except
|
||||
# cancel by user)
|
||||
scan_successful = True
|
||||
|
|
@ -462,7 +458,6 @@ class SmartScanThing(lt.Thing):
|
|||
The loop to run through during a scan, until no more scan x,y positions
|
||||
are remaining.
|
||||
"""
|
||||
|
||||
# The initial plan for the scan should be a single x,y position. All future
|
||||
# moves will be planned around this point. In future, route planner could
|
||||
# have multiple starting positions, each of which will be visited before the
|
||||
|
|
@ -537,7 +532,6 @@ class SmartScanThing(lt.Thing):
|
|||
@_scan_running
|
||||
def _perform_final_stitch(self):
|
||||
"""Update the scan zip and perform final stitch of the data"""
|
||||
|
||||
if self._scan_images_taken <= 3:
|
||||
self._scan_logger.info("Not performing a stitch as 3 or fewer images taken")
|
||||
return
|
||||
|
|
@ -608,7 +602,7 @@ class SmartScanThing(lt.Thing):
|
|||
model=bool,
|
||||
description="""Whether to detect and skip empty fields of view
|
||||
|
||||
This uses the settings from the `background_detect` Thing.""",
|
||||
This uses the settings from the ``BackgroundDetectThing``.""",
|
||||
)
|
||||
|
||||
autofocus_dz = lt.ThingSetting(
|
||||
|
|
@ -638,7 +632,7 @@ class SmartScanThing(lt.Thing):
|
|||
|
||||
Each scan has a name (which can be used to access it), along with
|
||||
its modified and created times (according to the filesystem) and
|
||||
the number of items in the `images` folder. Note that image count
|
||||
the number of items in the ``images`` folder. Note that image count
|
||||
uses a regular expression, and changes to the naming scheme will
|
||||
break it.
|
||||
"""
|
||||
|
|
@ -660,8 +654,8 @@ class SmartScanThing(lt.Thing):
|
|||
"""Return the stitched image corresponding to a given scan name, if it exists.
|
||||
|
||||
Will only return a file ending in suffix STITCH_SUFFIX
|
||||
Note: when downloading this, the default filename will be `scan_name`.jpeg"""
|
||||
|
||||
Note: when downloading this, the default filename will be ``scan_name``.jpeg
|
||||
"""
|
||||
stitch_path = self._scan_dir_manager.get_final_stitch_path(scan_name)
|
||||
|
||||
if stitch_path is None:
|
||||
|
|
@ -709,7 +703,6 @@ class SmartScanThing(lt.Thing):
|
|||
"""
|
||||
Delete all scan folders containing no images at the top level
|
||||
"""
|
||||
|
||||
# JSON is ignored as it's created before any images are captured
|
||||
for scan_info in self._scan_dir_manager.all_scans_info():
|
||||
if scan_info.number_of_images == 0:
|
||||
|
|
@ -732,7 +725,6 @@ class SmartScanThing(lt.Thing):
|
|||
@property
|
||||
def latest_preview_stitch_path(self) -> Optional[str]:
|
||||
"""The path of the latest preview stitched image, or None if not available"""
|
||||
|
||||
if not self.latest_scan_name:
|
||||
return None
|
||||
|
||||
|
|
@ -744,9 +736,10 @@ class SmartScanThing(lt.Thing):
|
|||
def latest_preview_stitch_time(self) -> Optional[float]:
|
||||
"""The modification time of the latest preview image, to allow live updating
|
||||
|
||||
This will return None (`null` to JS) if there is no preview image to return.
|
||||
This will return None (``null`` to JS) if there is no preview image to return.
|
||||
|
||||
This is used for two reasons:
|
||||
|
||||
1. If all caching was turned off this stitch would be sent over the network
|
||||
repeatedly
|
||||
2. If caching was is on, then the stitch will not update when needed.
|
||||
|
|
@ -834,12 +827,14 @@ class SmartScanThing(lt.Thing):
|
|||
Raises:
|
||||
ChildProcessError if exit code is not zero
|
||||
InvocationCancelledError if the action is cancelled.
|
||||
|
||||
"""
|
||||
logger.info(f"Running command in subprocess: `{' '.join(cmd)}`")
|
||||
|
||||
def log_buffer(buffer):
|
||||
"""A short internal function to read everything in the buffer to
|
||||
the log"""
|
||||
the log
|
||||
"""
|
||||
while line := buffer.readline():
|
||||
logger.info(line)
|
||||
|
||||
|
|
@ -968,6 +963,7 @@ class SmartScanThing(lt.Thing):
|
|||
scan_name: str,
|
||||
):
|
||||
"""Update the zip to include the files left until the end, then return the
|
||||
zip file as a Blob"""
|
||||
zip file as a Blob
|
||||
"""
|
||||
zip_fname = self._scan_dir_manager.zip_scan(scan_name, final_version=True)
|
||||
return ZipBlob.from_file(zip_fname)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
from __future__ import annotations
|
||||
from typing import TypeAlias
|
||||
from collections.abc import Sequence, Mapping
|
||||
|
||||
import labthings_fastapi as lt
|
||||
|
|
@ -10,8 +9,8 @@ class BaseStage(lt.Thing):
|
|||
|
||||
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`.
|
||||
``move_relative`` and ``move_absolute`` actions, which update the
|
||||
``position`` property on completion, and provide ``set_zero_position``.
|
||||
"""
|
||||
|
||||
_axis_names = ("x", "y", "z")
|
||||
|
|
@ -79,6 +78,4 @@ class BaseStage(lt.Thing):
|
|||
)
|
||||
|
||||
|
||||
StageDependency: TypeAlias = lt.deps.direct_thing_client_dependency(
|
||||
BaseStage, "/stage/"
|
||||
)
|
||||
StageDependency = lt.deps.direct_thing_client_dependency(BaseStage, "/stage/")
|
||||
|
|
|
|||
|
|
@ -13,13 +13,26 @@ from . import BaseStage
|
|||
|
||||
|
||||
class SangaboardThing(BaseStage):
|
||||
def __init__(self, port: str = None, **kwargs):
|
||||
"""A Thing to manage a Sangaboard motor controller
|
||||
"""A Thing to manage a Sangaboard motor controller
|
||||
|
||||
Internally, this uses the ``pysangaboard`` package from PyPi. This imports
|
||||
as ``sangaboard``. As ``pysangaboard`` does not support some features added
|
||||
to the Sangaboard firmware v1 (LED flashing, aborting moves, etc) this
|
||||
functionality is accessed by directly querying the serial interface.
|
||||
"""
|
||||
|
||||
def __init__(self, port: str = None, **kwargs):
|
||||
"""Initialise SangaboardThing.
|
||||
|
||||
Initialise the "Thing", but do not initialise an underlying
|
||||
``Sangaboard`` object from ``pysangaboard`` until the Thing context
|
||||
manager is started.
|
||||
|
||||
:param port: The serial port for the Sangaboard. Optional, this is used
|
||||
to stop the Sangaboard object querying available devices.
|
||||
:param ``**kwargs``: Any other keyword arguments to be passed to the
|
||||
Sangaboard class
|
||||
|
||||
Internally, this uses the `pysangaboard` package from PyPi. This imports
|
||||
as `sangaboard`. As `pysangaboard` does not support some features added
|
||||
to the Sangaboard firmware v1 (LED flashing, aborting moves, etc) this
|
||||
functionality is accessed by directly querying the serial interface.
|
||||
"""
|
||||
self.sangaboard_kwargs = kwargs
|
||||
self.sangaboard_kwargs["port"] = port
|
||||
|
|
@ -41,9 +54,9 @@ class SangaboardThing(BaseStage):
|
|||
|
||||
@contextmanager
|
||||
def sangaboard(self) -> Iterator[sangaboard.Sangaboard]:
|
||||
"""Return the wrapped `sangaboard.Sangaboard` instance.
|
||||
"""Return the wrapped ``sangaboard.Sangaboard`` instance.
|
||||
|
||||
This is protected by a `threading.RLock`, which may change in future.
|
||||
This is protected by a ``threading.RLock``, which may change in future.
|
||||
"""
|
||||
with self._sangaboard_lock:
|
||||
yield self._sangaboard
|
||||
|
|
|
|||
|
|
@ -36,9 +36,7 @@ class SystemControlThing(lt.Thing):
|
|||
|
||||
@lt.thing_property
|
||||
def is_raspberrypi() -> bool:
|
||||
"""
|
||||
Checks if we are running on a Raspberry Pi.
|
||||
"""
|
||||
"""Return True if running on a Raspberry Pi."""
|
||||
return os.path.exists("/usr/bin/raspi-config")
|
||||
|
||||
@lt.thing_action
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue