diff --git a/picamera_coverage.zip b/picamera_coverage.zip index 7133573c..da2039b1 100644 Binary files a/picamera_coverage.zip and b/picamera_coverage.zip differ diff --git a/src/openflexure_microscope_server/things/autofocus.py b/src/openflexure_microscope_server/things/autofocus.py index 02175d36..bff91e81 100644 --- a/src/openflexure_microscope_server/things/autofocus.py +++ b/src/openflexure_microscope_server/things/autofocus.py @@ -69,7 +69,7 @@ class StackParams(BaseModel): # Using docstrings under variables as this is how pdoc would expect # attributed to be documented - settling_time: float = Field(default=0.05, ge=0) + settling_time: float = Field(default=0.3, ge=0) """Time (in seconds) between moving and capturing an image""" origin: StackOrigin = StackOrigin.START @@ -345,14 +345,12 @@ class JPEGSharpnessMonitor: async def monitor_sharpness(self) -> None: """Start monitoring sharpness metrics.""" self.running = True - while self.running: - i = await self.camera.lores_mjpeg_stream.next_frame() - entry = await self.camera.lores_mjpeg_stream.ringbuffer_entry(i) - self._jpeg_times.append(entry.timestamp.timestamp()) + async for frame in self.camera.lores_mjpeg_stream.frame_async_generator(): + self._jpeg_times.append(time.time()) # JPEG sharpness metric if self.record & SharpnessMethod.JPEG: - self._jpeg_sizes.append(len(entry.frame)) + self._jpeg_sizes.append(len(frame)) # FocusFoM metric if self.record & SharpnessMethod.FOCUS_FOM: diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index e93fb938..a4fe8fce 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -16,7 +16,7 @@ from abc import ABC, abstractmethod from copy import deepcopy from datetime import datetime from types import TracebackType -from typing import Any, Literal, Mapping, Optional, Self, Union, overload +from typing import Any, Literal, Mapping, Optional, Self import numpy as np import piexif @@ -216,70 +216,6 @@ class CaptureGalleryInfo(BaseModel): card_type: Literal["Capture"] = "Capture" -class MJPEGStreamWithTimestamp(lt.outputs.MJPEGStream): - """An MJPEG stream where the frame time can be recorded with the frame. - - This can be used to capture when the image was taken by the sensor if known - rather than take the time it is added into the ring buffer. - """ - - def add_frame(self, frame: bytes, timestamp: Optional[datetime] = None) -> None: - """Add a JPEG to the MJPEG stream. - - Modify the standard function to have an option to send in the capture time. - - :param frame: The frame to add - :param timestamp: The time the frame was captured. - - :raise ValueError: if the supplied frame does not start with the JPEG - start bytes and end with the end bytes. - """ - if not ( - frame[0] == 0xFF - and frame[1] == 0xD8 - and frame[-2] == 0xFF - and frame[-1] == 0xD9 - ): - raise ValueError("Invalid JPEG") - with self._lock: - entry = self._ringbuffer[(self.last_frame_i + 1) % len(self._ringbuffer)] - entry.timestamp = timestamp if timestamp is not None else datetime.now() - entry.frame = frame - entry.index = self.last_frame_i + 1 - self._thing_server_interface.start_async_task_soon( - self.notify_new_frame, entry.index - ) - - -class MJPEGStreamDescriptorWithTimestamp(lt.outputs.MJPEGStreamDescriptor): - """Subclass the Labthings descriptor to return our own class.""" - - @overload - def __get__(self, obj: Literal[None], type: type | None = None) -> Self: ... - - @overload - def __get__( - self, obj: lt.Thing, type: type | None = None - ) -> MJPEGStreamWithTimestamp: ... - - def __get__( - self, - obj: Optional[lt.Thing], - type: type[lt.Thing] | None = None, # noqa: A002 - ) -> Union[MJPEGStreamWithTimestamp | Self]: - """Return our own MJPEG Stream with timestamp.""" - if obj is None: - return self - try: - return obj.__dict__[self.name] - except KeyError: - obj.__dict__[self.name] = MJPEGStreamWithTimestamp( - **self._kwargs, - thing_server_interface=obj._thing_server_interface, - ) - return obj.__dict__[self.name] - - class BaseCamera(OFMThing, ABC): """The base class for all cameras. All cameras must directly inherit from this class. @@ -291,8 +227,8 @@ class BaseCamera(OFMThing, ABC): _all_background_detectors: Mapping[str, BackgroundDetectAlgorithm] = lt.thing_slot() - mjpeg_stream = MJPEGStreamDescriptorWithTimestamp() - lores_mjpeg_stream = MJPEGStreamDescriptorWithTimestamp() + mjpeg_stream = lt.outputs.MJPEGStreamDescriptor() + lores_mjpeg_stream = lt.outputs.MJPEGStreamDescriptor() _memory_buffer = CameraMemoryBuffer() supports_focus_fom: bool = False diff --git a/src/openflexure_microscope_server/things/camera/picamera.py b/src/openflexure_microscope_server/things/camera/picamera.py index b8de8442..c769947d 100644 --- a/src/openflexure_microscope_server/things/camera/picamera.py +++ b/src/openflexure_microscope_server/things/camera/picamera.py @@ -24,7 +24,6 @@ import tempfile import time from abc import ABC, abstractmethod from contextlib import contextmanager -from datetime import datetime from threading import RLock from types import TracebackType from typing import ( @@ -55,7 +54,7 @@ from openflexure_microscope_server.ui import ( property_control_for, ) -from . import BaseCamera, CaptureMode, MJPEGStreamWithTimestamp, StreamingMode +from . import BaseCamera, CaptureMode, StreamingMode from . import picamera_recalibrate_utils as recalibrate_utils from . import picamera_tuning_file_utils as tf_utils @@ -76,37 +75,24 @@ class MissingCalibrationError(RuntimeError): class PicameraStreamOutput(Output): """An Output class that sends frames to a stream.""" - def __init__(self, stream: MJPEGStreamWithTimestamp, encoder: MJPEGEncoder) -> None: + def __init__(self, stream: lt.outputs.MJPEGStream) -> None: """Create an output that puts frames in an MJPEGStream. :param stream: The labthings MJPEGStream to send frames to. - :param encoder: The encoder that is encoding the frames. This is needed to get - the full timestamp. """ Output.__init__(self) self.stream = stream - self.encoder = encoder def outputframe( self, frame: bytes, _keyframe: Optional[bool] = True, - timestamp: Optional[int] = None, + _timestamp: Optional[int] = None, _packet: Any = None, _audio: bool = False, ) -> None: """Add a frame to the stream's ringbuffer.""" - if timestamp is None or self.encoder.firsttimestamp is None: - frame_timestamp = None - else: - # The difference between unix time and the CPU time. Do not cache this - # number as it can change with clock slew or other time adjustments. - cpu_dt = time.time() - time.monotonic() - # Reconstruct full CPU time of the frame in us - timestamp += self.encoder.firsttimestamp - # Convert to datetime - frame_timestamp = datetime.fromtimestamp(cpu_dt + timestamp / 1e6) - self.stream.add_frame(frame, frame_timestamp) + self.stream.add_frame(frame) class PiCamera2StreamingMode(StreamingMode): @@ -501,16 +487,14 @@ class StreamingPiCamera2(BaseCamera, ABC): picam.configure(stream_config) LOGGER.info("Starting picamera MJPEG stream...") stream_name = "lores" if mode_info.use_lores_as_preview else "main" - encoder = MJPEGEncoder(self.mjpeg_bitrate) picam.start_recording( - encoder, - PicameraStreamOutput(self.mjpeg_stream, encoder), + MJPEGEncoder(self.mjpeg_bitrate), + PicameraStreamOutput(self.mjpeg_stream), name=stream_name, ) - encoder = MJPEGEncoder(100000000) picam.start_encoder( - encoder, - PicameraStreamOutput(self.lores_mjpeg_stream, encoder), + MJPEGEncoder(100000000), + PicameraStreamOutput(self.lores_mjpeg_stream), name="lores", ) except Exception as e: diff --git a/src/openflexure_microscope_server/things/camera/simulation.py b/src/openflexure_microscope_server/things/camera/simulation.py index 4c71ed39..e5fe90c1 100644 --- a/src/openflexure_microscope_server/things/camera/simulation.py +++ b/src/openflexure_microscope_server/things/camera/simulation.py @@ -12,7 +12,6 @@ import io import logging import re import time -from datetime import datetime from threading import Thread from types import TracebackType from typing import Optional, Self, overload @@ -468,10 +467,9 @@ class SimulatedCamera(BaseCamera): last_frame_t = time.time() frame = self.generate_frame() - timestamp = datetime.fromtimestamp(last_frame_t) - self.mjpeg_stream.add_frame(_frame2bytes(frame), timestamp) + self.mjpeg_stream.add_frame(_frame2bytes(frame)) ds_frame = frame.resize((320, 240), resample=Image.Resampling.NEAREST) - self.lores_mjpeg_stream.add_frame(_frame2bytes(ds_frame), timestamp) + self.lores_mjpeg_stream.add_frame(_frame2bytes(ds_frame)) @lt.action def discard_frames(self) -> None: diff --git a/webapp/src/assets/less/theme.less b/webapp/src/assets/less/theme.less index e9cac9ad..2a767a76 100644 --- a/webapp/src/assets/less/theme.less +++ b/webapp/src/assets/less/theme.less @@ -228,11 +228,3 @@ a:hover { .uk-pagination > .uk-active > * { color: @global-primary-background; } - -/* - * Checkboxes - */ - -.uk-checkbox { - margin-right: 4px; -} \ No newline at end of file diff --git a/webapp/src/components/genericComponents/multiSelectDropdown.vue b/webapp/src/components/genericComponents/multiSelectDropdown.vue index 0709e35c..a8294895 100644 --- a/webapp/src/components/genericComponents/multiSelectDropdown.vue +++ b/webapp/src/components/genericComponents/multiSelectDropdown.vue @@ -20,7 +20,8 @@ :value="option" :checked="modelValue.includes(option)" @change="toggleOption(option)" - />{{ option }} + /> + {{ option }} diff --git a/webapp/src/components/labThingsComponents/actionLogDisplay.vue b/webapp/src/components/labThingsComponents/actionLogDisplay.vue index 61977ff9..f4a4d84f 100644 --- a/webapp/src/components/labThingsComponents/actionLogDisplay.vue +++ b/webapp/src/components/labThingsComponents/actionLogDisplay.vue @@ -198,9 +198,7 @@ export default { .log-summary-text { text-align: center; font-size: large; - - /* Reserve two lines even when message is short. line-clamp handles truncation */ - min-height: calc(2 * 1.4em); + height: calc(2 * 1.4em); /* height of element is 2 lines */ line-height: 1.4em; padding: 0 5px; /* Give some padding so there's room for ... to not expand too far */ overflow: hidden; diff --git a/webapp/src/components/tabContentComponents/controlComponents/positionControl.vue b/webapp/src/components/tabContentComponents/controlComponents/positionControl.vue index b0d101ae..70cac330 100644 --- a/webapp/src/components/tabContentComponents/controlComponents/positionControl.vue +++ b/webapp/src/components/tabContentComponents/controlComponents/positionControl.vue @@ -11,31 +11,33 @@ and zero position buttons. It also includes the d-pad.
  • Position
    - -
    - - -
    -

    - -

    +
    + +
    + + +
    +

    + +

    +

    Stream Settings

    This will disable the embedded web stream of the camera.