Merge branch 'camera-and-bg-detect-setting-customisation' into 'v3'
Dynamic UI for camera and background detect settings See merge request openflexure/openflexure-microscope-server!331
This commit is contained in:
commit
b75af87b84
18 changed files with 880 additions and 353 deletions
Binary file not shown.
|
|
@ -8,7 +8,7 @@ current camera field of view contains sample.
|
|||
from typing import Optional, Any
|
||||
import cv2
|
||||
import numpy as np
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from pydantic.errors import PydanticUserError
|
||||
from scipy.stats import norm
|
||||
from labthings_fastapi.thing_description import type_to_dataschema
|
||||
|
|
@ -32,13 +32,19 @@ class BackgroundDetectorStatus(BaseModel):
|
|||
``ready`` is used in case more complex methods are added in the future, which
|
||||
need different initialisation.
|
||||
"""
|
||||
settings: BaseModel
|
||||
"""The settings for this this background detect Algorithm"""
|
||||
settings: dict[str, Any]
|
||||
"""The settings for the current background detect Algorithm. These are a dictionary
|
||||
dumped from the base model."""
|
||||
|
||||
# Setting schema is a dict until LabThings FastAPI issue #154 is fixed and
|
||||
# DataSchema can be used directly. For now `model_dump()` must be used to dump schema
|
||||
# to a dict.
|
||||
settings_schema: dict[str, Any]
|
||||
"""The schema for the settings for the current background detect Algorithm.
|
||||
|
||||
This is reported so that the UI can dynamically create a UI for any background detector
|
||||
algorithm.
|
||||
"""
|
||||
|
||||
|
||||
class BackgroundDetectAlgorithm:
|
||||
|
|
@ -63,7 +69,7 @@ class BackgroundDetectAlgorithm:
|
|||
"""The status information needed for the GUI. Read only."""
|
||||
return BackgroundDetectorStatus(
|
||||
ready=self.background_data is not None,
|
||||
settings=self.settings,
|
||||
settings=self.settings.model_dump(),
|
||||
# Dump model with `model_dump()` for reason explained when defining
|
||||
# BackgroundDetectorStatus
|
||||
settings_schema=type_to_dataschema(self.settings_data_model).model_dump(),
|
||||
|
|
@ -152,13 +158,18 @@ class ChannelDistributions(BaseModel):
|
|||
class ColourChannelDetectSettings(BaseModel):
|
||||
"""A BaseModel for storing the settings for colour channel detectors."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
channel_tolerance: float = 7.0
|
||||
"""Channel Tolerance
|
||||
|
||||
The number of standard deviations a pixel value must be from the background mean
|
||||
to be considered sample.
|
||||
"""
|
||||
min_sample_coverage: float = 25.0
|
||||
|
||||
# Use Field to set Title reported to UI. By default Pydantic will convert the name
|
||||
# from snake_case to Title Case.
|
||||
min_sample_coverage: float = Field(25, title="Sample Coverage Required (%)")
|
||||
"""Sample Coverage Required (%)
|
||||
|
||||
The minimum percentage of the image that needs to be identified as sample for the
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import piexif
|
|||
import labthings_fastapi as lt
|
||||
from labthings_fastapi.types.numpy import NDArray
|
||||
|
||||
from openflexure_microscope_server.ui import ActionButton, PropertyControl
|
||||
from openflexure_microscope_server.background_detect import (
|
||||
ColourChannelDetectLUV,
|
||||
BackgroundDetectAlgorithm,
|
||||
|
|
@ -476,6 +477,21 @@ class BaseCamera(lt.Thing):
|
|||
time.sleep(self.settling_time)
|
||||
self.discard_frames()
|
||||
|
||||
@lt.thing_property
|
||||
def primary_calibration_actions(self) -> list[ActionButton]:
|
||||
"""The calibration actions for both calibration wizard and settings panel."""
|
||||
return []
|
||||
|
||||
@lt.thing_property
|
||||
def secondary_calibration_actions(self) -> list[ActionButton]:
|
||||
"""The calibration actions that appear only in settings panel."""
|
||||
return []
|
||||
|
||||
@lt.thing_property
|
||||
def manual_camera_settings(self) -> list[PropertyControl]:
|
||||
"""The camera settings to expose as property controls in the settings panel."""
|
||||
return []
|
||||
|
||||
# Note that the default detector name is set at init. This is over written if
|
||||
# setting is loaded from disk.
|
||||
@lt.thing_setting
|
||||
|
|
@ -500,6 +516,22 @@ class BaseCamera(lt.Thing):
|
|||
"""The status of the active detector for the UI."""
|
||||
return self.active_detector.status
|
||||
|
||||
@lt.thing_action
|
||||
def update_detector_settings(self, data) -> None:
|
||||
"""Update the settings of the current detector.
|
||||
|
||||
This is an action not a setting/property as the data model depends on the
|
||||
selected detector. As such, it cannot be specified with the necessary precision
|
||||
to be included in a ThingDescription as a setting/property, while retaining
|
||||
enough useful information to communicate to the UI how it is set and read.
|
||||
|
||||
The information on how to read the settings is exposed in
|
||||
``background_detector_status``.
|
||||
"""
|
||||
self.active_detector.settings = data
|
||||
# Manually save settings as the setter is not called.
|
||||
self.save_settings()
|
||||
|
||||
@lt.thing_setting
|
||||
def background_detector_data(self) -> dict:
|
||||
"""The data for each background detector, used to save to disk."""
|
||||
|
|
|
|||
|
|
@ -35,6 +35,12 @@ from picamera2.outputs import Output
|
|||
import labthings_fastapi as lt
|
||||
from labthings_fastapi.exceptions import NotConnectedToServerError
|
||||
|
||||
from openflexure_microscope_server.ui import (
|
||||
ActionButton,
|
||||
PropertyControl,
|
||||
action_button_for,
|
||||
property_control_for,
|
||||
)
|
||||
from . import picamera_recalibrate_utils as recalibrate_utils
|
||||
from . import BaseCamera, JPEGBlob, ArrayModel
|
||||
|
||||
|
|
@ -777,6 +783,88 @@ class StreamingPiCamera2(BaseCamera):
|
|||
)
|
||||
self._initialise_picamera()
|
||||
|
||||
@lt.thing_property
|
||||
def primary_calibration_actions(self) -> list[ActionButton]:
|
||||
"""The calibration actions for both calibration wizard and settings panel."""
|
||||
return [
|
||||
action_button_for(
|
||||
self.full_auto_calibrate,
|
||||
submit_label="Full Auto-Calibrate",
|
||||
can_terminate=False,
|
||||
requires_confirmation=True,
|
||||
confirmation_message=(
|
||||
"Start recalibration? This may take a while, and the microscope "
|
||||
"will be locked during this time."
|
||||
),
|
||||
notify_on_success=True,
|
||||
success_message="Finished recalibration.",
|
||||
),
|
||||
action_button_for(
|
||||
self.auto_expose_from_minimum,
|
||||
submit_label="Auto Gain & Shutter Speed",
|
||||
can_terminate=False,
|
||||
),
|
||||
action_button_for(
|
||||
self.calibrate_white_balance,
|
||||
submit_label="Auto White Balance",
|
||||
can_terminate=False,
|
||||
),
|
||||
action_button_for(
|
||||
self.calibrate_lens_shading,
|
||||
submit_label="Auto Flat Field Correction",
|
||||
can_terminate=False,
|
||||
requires_confirmation=True,
|
||||
confirmation_message=(
|
||||
"Is the microscope looking at an evenly illuminated, empty field "
|
||||
"of view? If not, the current image will show through in any "
|
||||
"images captured afterwards."
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@lt.thing_property
|
||||
def secondary_calibration_actions(self) -> list[ActionButton]:
|
||||
"""The calibration actions that appear only in settings panel."""
|
||||
return [
|
||||
action_button_for(
|
||||
self.flat_lens_shading,
|
||||
submit_label="Disable Flat Field Correction",
|
||||
can_terminate=False,
|
||||
),
|
||||
action_button_for(
|
||||
self.reset_lens_shading,
|
||||
submit_label="Reset Flat Field Correction",
|
||||
can_terminate=False,
|
||||
),
|
||||
]
|
||||
|
||||
@lt.thing_property
|
||||
def manual_camera_settings(self) -> list[PropertyControl]:
|
||||
"""The camera settings to expose as property controls in the settings panel."""
|
||||
return [
|
||||
property_control_for(
|
||||
self,
|
||||
"exposure_time",
|
||||
label="Exposure Time (0-33251)",
|
||||
read_back=True,
|
||||
read_back_delay=1000,
|
||||
),
|
||||
property_control_for(
|
||||
self,
|
||||
"analogue_gain",
|
||||
label="Analogue Gain",
|
||||
read_back=True,
|
||||
read_back_delay=1000,
|
||||
),
|
||||
property_control_for(
|
||||
self,
|
||||
"colour_gains",
|
||||
label="Colour Gains",
|
||||
read_back=True,
|
||||
read_back_delay=1000,
|
||||
),
|
||||
]
|
||||
|
||||
@lt.thing_property
|
||||
def lens_shading_tables(self) -> Optional[LensShading]:
|
||||
"""The current lens shading (i.e. flat-field correction).
|
||||
|
|
|
|||
|
|
@ -21,6 +21,13 @@ from scipy.ndimage import gaussian_filter
|
|||
|
||||
import labthings_fastapi as lt
|
||||
|
||||
from openflexure_microscope_server.ui import (
|
||||
ActionButton,
|
||||
PropertyControl,
|
||||
action_button_for,
|
||||
property_control_for,
|
||||
)
|
||||
|
||||
from . import BaseCamera, JPEGBlob, ArrayModel
|
||||
from ..stage import BaseStage
|
||||
|
||||
|
|
@ -40,6 +47,7 @@ class SimulatedCamera(BaseCamera):
|
|||
|
||||
_stage: Optional[BaseStage] = None
|
||||
_server: Optional[lt.ThingServer] = None
|
||||
_show_sample: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
@ -124,10 +132,11 @@ class SimulatedCamera(BaseCamera):
|
|||
Canvas is int16 so that random noise can be added to simulation image before
|
||||
changing to unit8 to stop wrapping.
|
||||
"""
|
||||
self.canvas = np.ones(self.canvas_shape, dtype=np.int16)
|
||||
self.canvas[:, :, 0] *= BG_COLOR[0]
|
||||
self.canvas[:, :, 1] *= BG_COLOR[1]
|
||||
self.canvas[:, :, 2] *= BG_COLOR[2]
|
||||
self.blank_canvas = np.ones(self.canvas_shape, dtype=np.int16)
|
||||
self.blank_canvas[:, :, 0] *= BG_COLOR[0]
|
||||
self.blank_canvas[:, :, 1] *= BG_COLOR[1]
|
||||
self.blank_canvas[:, :, 2] *= BG_COLOR[2]
|
||||
self.canvas = self.blank_canvas.copy()
|
||||
w, h, _ = self.glyph_shape
|
||||
for x, y, sprite_size_index in self.blobs:
|
||||
self.canvas[
|
||||
|
|
@ -149,7 +158,8 @@ class SimulatedCamera(BaseCamera):
|
|||
x_slice = slice(top_left[0], top_left[0] + self.shape[0])
|
||||
y_slice = slice(top_left[1], top_left[1] + self.shape[1])
|
||||
z_slice = slice(None)
|
||||
focused_image = self.canvas[(x_slice, y_slice, z_slice)]
|
||||
canvas = self.canvas if self._show_sample else self.blank_canvas
|
||||
focused_image = canvas[(x_slice, y_slice, z_slice)]
|
||||
image = gaussian_filter(
|
||||
focused_image,
|
||||
sigma=np.abs(pos[2]) / 5,
|
||||
|
|
@ -161,7 +171,7 @@ class SimulatedCamera(BaseCamera):
|
|||
)
|
||||
|
||||
# Add noise and convert to uint8
|
||||
image += RNG.normal(scale=2, size=self.shape).astype("int16")
|
||||
image += RNG.normal(scale=self.noise_level, size=self.shape).astype("int16")
|
||||
image[image < 0] = 0
|
||||
image[image > 255] = 255
|
||||
return image.astype("uint8")
|
||||
|
|
@ -216,6 +226,8 @@ class SimulatedCamera(BaseCamera):
|
|||
return self._capture_thread.is_alive()
|
||||
return False
|
||||
|
||||
noise_level = lt.ThingProperty(float, 2.0)
|
||||
|
||||
def _capture_frames(self):
|
||||
portal = lt.get_blocking_portal(self)
|
||||
while self._capture_enabled:
|
||||
|
|
@ -279,3 +291,30 @@ class SimulatedCamera(BaseCamera):
|
|||
output = io.BytesIO()
|
||||
piexif.insert(piexif.dump(exif_dict), jpeg, output)
|
||||
return JPEGBlob.from_bytes(output.getvalue())
|
||||
|
||||
@lt.thing_action
|
||||
def remove_sample(self):
|
||||
"""Show the simulated background with no sample."""
|
||||
if not self._show_sample:
|
||||
raise RuntimeError("Sample is already removed.")
|
||||
self._show_sample = False
|
||||
|
||||
@lt.thing_action
|
||||
def load_sample(self):
|
||||
"""Show the simulated sample."""
|
||||
if self._show_sample:
|
||||
raise RuntimeError("Sample is already in place.")
|
||||
self._show_sample = True
|
||||
|
||||
@lt.thing_property
|
||||
def secondary_calibration_actions(self) -> list[ActionButton]:
|
||||
"""The calibration actions that appear only in settings panel."""
|
||||
return [
|
||||
action_button_for(self.load_sample, submit_label="Load Sample"),
|
||||
action_button_for(self.remove_sample, submit_label="Remove Sample"),
|
||||
]
|
||||
|
||||
@lt.thing_property
|
||||
def manual_camera_settings(self) -> list[PropertyControl]:
|
||||
"""The camera settings to expose as property controls in the settings panel."""
|
||||
return [property_control_for(self, "noise_level", label="Noise Level")]
|
||||
|
|
|
|||
94
src/openflexure_microscope_server/ui.py
Normal file
94
src/openflexure_microscope_server/ui.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
"""Functionality for communicating the required user interface for a thing."""
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class ActionButton(BaseModel):
|
||||
"""The data required for creating an actionButton in Vue.
|
||||
|
||||
Currently this cannot be used to submitData, or to submitOnEvent.
|
||||
"""
|
||||
|
||||
thing: str
|
||||
"""The Thing "path" for the Thing instance."""
|
||||
|
||||
action: str
|
||||
"""The name of the action to be triggered."""
|
||||
|
||||
poll_interval: int = 1
|
||||
"""The interval for polling in seconds."""
|
||||
|
||||
submit_label: str = "Submit"
|
||||
"""The label for the action button."""
|
||||
|
||||
can_terminate: bool = True
|
||||
"""Specify whether the action can be terminated."""
|
||||
|
||||
requires_confirmation: bool = False
|
||||
"""Specify whether a confirmation modal is needed."""
|
||||
|
||||
confirmation_message: str = "Start task?"
|
||||
"""The message for the confirmation modal."""
|
||||
|
||||
button_primary: bool = True
|
||||
"""Specify whether the button is styled as a primary button."""
|
||||
|
||||
modal_progress: bool = False
|
||||
"""Specify whether to show a progress modal."""
|
||||
|
||||
notify_on_success: bool = False
|
||||
"""Specify whether to a notification on successful completion."""
|
||||
|
||||
success_message: str = "Success!"
|
||||
"""The message to show on successful completion."""
|
||||
|
||||
|
||||
def action_button_for(action, **kwargs) -> ActionButton:
|
||||
"""Create a ActionButton data for the specified Thing Action.
|
||||
|
||||
:param action: The thing action to create a button for.
|
||||
:param kwargs: Any attribute of `ActionButton` except for ``thing`` or ``action``.
|
||||
"""
|
||||
thing = action.args[0]
|
||||
thing_path = thing.path.strip("/")
|
||||
action_name = action.func.__name__
|
||||
return ActionButton(thing=thing_path, action=action_name, **kwargs)
|
||||
|
||||
|
||||
class PropertyControl(BaseModel):
|
||||
"""The data required for creating an actionButton in Vue.
|
||||
|
||||
Currently this cannot be used to submitData, or to submitOnEvent.
|
||||
"""
|
||||
|
||||
thing: str
|
||||
"""The Thing "path" for the Thing instance."""
|
||||
|
||||
property_name: str
|
||||
"""The name of the property (or setting)."""
|
||||
|
||||
label: str
|
||||
"""The label to show in the UI"""
|
||||
|
||||
read_back: bool = False
|
||||
"""Whether or not to read back the property after setting.
|
||||
|
||||
This is useful for hardware settings that may be coerced to the closest value.
|
||||
"""
|
||||
|
||||
read_back_delay: int = 1000
|
||||
"""The delay in ms before reading back the property."""
|
||||
|
||||
|
||||
def property_control_for(thing, property_name, **kwargs) -> PropertyControl:
|
||||
"""Create an PropertyControl data for the specified Thing Property.
|
||||
|
||||
:param thing: The instance of the thing that has the property to be controlled.
|
||||
:param property_name: The name of the property to create a control for.
|
||||
:param kwargs: Any attribute of `PropertyControl` except for ``thing`` or
|
||||
``property_name``. If label is not set here it will be the property name.
|
||||
"""
|
||||
thing_path = thing.path.strip("/")
|
||||
if "label" not in kwargs:
|
||||
kwargs["label"] = property_name
|
||||
return PropertyControl(thing=thing_path, property_name=property_name, **kwargs)
|
||||
|
|
@ -23,6 +23,6 @@ class MockCameraThing:
|
|||
|
||||
background_detector_status = BackgroundDetectorStatus(
|
||||
ready=True,
|
||||
settings=ColourChannelDetectSettings(),
|
||||
settings=ColourChannelDetectSettings().model_dump(),
|
||||
settings_schema={"fake": "schema"},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -70,7 +70,8 @@ def test_partial_base_class(background_image):
|
|||
bad_algo1 = BadAlgo1()
|
||||
status = bad_algo1.status
|
||||
assert not status.ready
|
||||
assert isinstance(status.settings, ColourChannelDetectSettings)
|
||||
# Check the settings dictionary can be validated as ``ColourChannelDetectSettings``
|
||||
ColourChannelDetectSettings(**status.settings)
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
# Should error on any dictionary input. This simulates loading settings from
|
||||
|
|
|
|||
133
tests/test_cameras.py
Normal file
133
tests/test_cameras.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"""Generic tests for cameras, specific camera hardware reqipres hardware specific tests."""
|
||||
|
||||
import pytest
|
||||
|
||||
from openflexure_microscope_server.things.camera import BaseCamera
|
||||
from openflexure_microscope_server.things.camera.simulation import SimulatedCamera
|
||||
from openflexure_microscope_server.things.camera.opencv import OpenCVCamera
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_picam_thing(mocker):
|
||||
"""Import PiCamera without hardware well enough to get a ThingDescription."""
|
||||
dummy_cam = mocker.Mock()
|
||||
mock_picamera2 = mocker.MagicMock()
|
||||
mock_picamera2.return_value.__enter__.return_value = dummy_cam
|
||||
mock_picamera2.return_value.__exit__.return_value = None
|
||||
|
||||
mocker.patch.dict(
|
||||
"sys.modules",
|
||||
{
|
||||
"picamera2": mock_picamera2,
|
||||
"picamera2.encoders": mocker.Mock(),
|
||||
"picamera2.outputs": mocker.Mock(),
|
||||
},
|
||||
)
|
||||
mocker.patch(
|
||||
"openflexure_microscope_server.things.camera.picamera.recalibrate_utils.load_default_tuning",
|
||||
return_value={"mock": "tuning"},
|
||||
)
|
||||
from openflexure_microscope_server.things.camera.picamera import StreamingPiCamera2
|
||||
|
||||
return StreamingPiCamera2()
|
||||
|
||||
|
||||
def _get_clean_camera_description(camera_thing):
|
||||
"""Return actions and properties for a camera Thing, separating those exposed to the UI.
|
||||
|
||||
Return cleaned sets of actions and properties for camera_thing, excluding
|
||||
calibration actions exposed to the UI as primary or secondary calibration actions,
|
||||
and excluding the properties exposed in manual camera settings.
|
||||
|
||||
:returns: Dict with keys:
|
||||
|
||||
* "actions" - Actions not explicitly exposed to the UI by this camera.
|
||||
* "properties" - Properties not explicitly exposed to the UI as manual camera
|
||||
settings.
|
||||
* "calibration_actions" - Actions explicitly exposed to the UI by this camera.
|
||||
* "manual_properties" - Properties explicitly exposed to the UI as manual
|
||||
camera settings.
|
||||
"""
|
||||
camera_thing.path = "/mock/"
|
||||
|
||||
td = camera_thing.thing_description()
|
||||
actions = set(td.actions.keys())
|
||||
properties = set(td.properties.keys())
|
||||
|
||||
# Calibration actions are camera specific
|
||||
primary = [btn.action for btn in camera_thing.primary_calibration_actions]
|
||||
secondary = [btn.action for btn in camera_thing.secondary_calibration_actions]
|
||||
calibration_actions = set(primary + secondary)
|
||||
|
||||
actions -= calibration_actions
|
||||
|
||||
# Manual properties are also camera specific
|
||||
manual_props = {ctrl.property_name for ctrl in camera_thing.manual_camera_settings}
|
||||
properties -= manual_props
|
||||
|
||||
return {
|
||||
"actions": actions,
|
||||
"properties": properties,
|
||||
"calibration_actions": calibration_actions,
|
||||
"manual_properties": manual_props,
|
||||
}
|
||||
|
||||
|
||||
def test_thing_description_equivalence(mock_picam_thing):
|
||||
"""Ensure the built-in camera Thing subclasses retain consistent core functionality.
|
||||
|
||||
This test verifies that extra actions are not unintentionally introduced to
|
||||
camera child classes. Any addition of actions must be accompanied by an update
|
||||
to this test, prompting discussion of whether the action belongs in the subclass
|
||||
or the base camera class.
|
||||
"""
|
||||
base_td = BaseCamera().thing_description()
|
||||
base_actions = set(base_td.actions.keys())
|
||||
base_props = set(base_td.properties.keys())
|
||||
|
||||
sim_description = _get_clean_camera_description(SimulatedCamera())
|
||||
opencv_description = _get_clean_camera_description(OpenCVCamera())
|
||||
picamera_description = _get_clean_camera_description(mock_picam_thing)
|
||||
|
||||
# Note. These are the actions and properties that are not exposed to the UI via
|
||||
# `primary_calibration_actions`, `secondary_calibration_actions`, or
|
||||
# `manual_camera_settings`.
|
||||
sim_actions = sim_description["actions"]
|
||||
sim_props = sim_description["properties"]
|
||||
|
||||
opencv_actions = opencv_description["actions"]
|
||||
opencv_props = opencv_description["properties"]
|
||||
|
||||
picamera_actions = picamera_description["actions"]
|
||||
picamera_props = picamera_description["properties"]
|
||||
|
||||
# Camera actions and properties should generally be equivalent except for exposed
|
||||
# manual settings and calibration actions.
|
||||
assert opencv_actions == sim_actions == base_actions
|
||||
assert opencv_props == sim_props == base_props
|
||||
|
||||
# For now PiCamera has a number of extra actions and properties. These should be
|
||||
# reduced over time by creating a way to use the functionality in a way as clearly
|
||||
# defined as PiCamera specific, or by replicating the functionality for other
|
||||
# cameras.
|
||||
picamera_extra_actions = {
|
||||
"flat_lens_shading_chrominance",
|
||||
"set_static_green_equalisation",
|
||||
"stop_streaming",
|
||||
"reset_ccm",
|
||||
}
|
||||
picamera_extra_props = {
|
||||
"mjpeg_bitrate",
|
||||
"colour_correction_matrix",
|
||||
"lens_shading_tables",
|
||||
"sensor_resolution",
|
||||
"lens_shading_is_static",
|
||||
"capture_metadata",
|
||||
"camera_configuration",
|
||||
"stream_resolution",
|
||||
"tuning",
|
||||
"sensor_modes",
|
||||
"sensor_mode",
|
||||
}
|
||||
assert picamera_actions - base_actions == picamera_extra_actions
|
||||
assert picamera_props - base_props == picamera_extra_props
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
<div>
|
||||
<button
|
||||
type="button"
|
||||
:disabled="isDisabled"
|
||||
:hidden="taskStarted"
|
||||
class="uk-button uk-width-1-1"
|
||||
:class="[buttonPrimary ? 'uk-button-primary' : 'uk-button-default']"
|
||||
|
|
@ -132,7 +133,12 @@ export default {
|
|||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
isDisabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
},
|
||||
|
||||
data: function() {
|
||||
|
|
|
|||
227
webapp/src/components/labThingsComponents/inputFromSchema.vue
Normal file
227
webapp/src/components/labThingsComponents/inputFromSchema.vue
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
<template>
|
||||
<div>
|
||||
<label v-if="dataType == 'number'" class="uk-form-label"
|
||||
>{{ label }}
|
||||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
v-model="internalValue"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
type="number"
|
||||
@focusin="focusIn"
|
||||
@focusout="focusOut"
|
||||
@keydown="keyDown"
|
||||
/>
|
||||
<a class="button-next-to-input" @click="requestUpdate">
|
||||
<span class="material-symbols-outlined">refresh</span>
|
||||
</a>
|
||||
</div>
|
||||
</label>
|
||||
<div v-if="dataType == 'boolean'" class="input-and-buttons-container">
|
||||
<label class="uk-form-label numeric-setting-line-input">
|
||||
<input
|
||||
ref="checkbox"
|
||||
v-model="internalValue"
|
||||
class="uk-checkbox"
|
||||
type="checkbox"
|
||||
@change="sendValue"
|
||||
/>
|
||||
{{ label }}
|
||||
</label>
|
||||
<a class="button-next-to-input" @click="requestUpdate">
|
||||
<span class="material-symbols-outlined">refresh</span>
|
||||
</a>
|
||||
</div>
|
||||
<label v-if="dataType == 'number_array'" class="uk-form-label"
|
||||
>{{ label }}
|
||||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
v-for="i in valueLength"
|
||||
:key="i"
|
||||
v-model="internalValue[i - 1]"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
type="number"
|
||||
@focusin="focusIn"
|
||||
@focusout="focusOut"
|
||||
@keydown="keyDown"
|
||||
/>
|
||||
<a class="button-next-to-input" @click="requestUpdate">
|
||||
<span class="material-symbols-outlined">refresh</span>
|
||||
</a>
|
||||
</div>
|
||||
</label>
|
||||
<label v-if="dataType == 'number_object'" class="uk-form-label"
|
||||
>{{ label }}
|
||||
<div v-for="(val, key) in value" :key="key">
|
||||
<label>{{internalLabels[key]}}</label>
|
||||
<div class="input-and-buttons-container" >
|
||||
<input
|
||||
v-model="internalValue[key]"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
type="number"
|
||||
@focusin="focusIn"
|
||||
@focusout="focusOut"
|
||||
@keydown="keyDown"
|
||||
/>
|
||||
<a class="button-next-to-input" @click="requestUpdate">
|
||||
<span class="material-symbols-outlined">refresh</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label v-if="dataType == 'other'" class="uk-form-label"
|
||||
>{{ label }}
|
||||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
v-model="internalValue"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
type="text"
|
||||
disabled="true"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "InputFromSchema",
|
||||
|
||||
props: {
|
||||
dataSchema: {
|
||||
type: Object
|
||||
},
|
||||
value: {
|
||||
type: null
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
internalValue: this.value,
|
||||
valueOnEnter: undefined,
|
||||
focused: false
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(newValue) {
|
||||
this.internalValue = newValue;
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
internalLabels: function() {
|
||||
if (this.dataType == "number_object") {
|
||||
let labels = {};
|
||||
for (const key in this.internalValue){
|
||||
labels[key] = this.dataSchema.properties[key].title
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
valueLength: function() {
|
||||
if (this.dataType == "number_array") {
|
||||
if (this.internalValue == undefined) {
|
||||
return 0;
|
||||
}
|
||||
return this.internalValue.length;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
dataType: function() {
|
||||
let prop = this.dataSchema;
|
||||
if (prop == undefined) {
|
||||
return "undefined";
|
||||
}
|
||||
const num_types = ["integer", "float", "number"];
|
||||
if (num_types.includes(prop.type)) {
|
||||
return "number";
|
||||
}
|
||||
if (prop.type == "array") {
|
||||
if (num_types.includes(prop.items.type)) {
|
||||
return "number_array";
|
||||
}
|
||||
if (Array.isArray(prop.items)) {
|
||||
if (prop.items.every(t => num_types.includes(t.type))) {
|
||||
return "number_array";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (prop.type == "boolean") {
|
||||
return "boolean";
|
||||
}
|
||||
if (prop.type == "object") {
|
||||
let numeric = true;
|
||||
for (let key in prop.properties) {
|
||||
if (!num_types.includes(prop.properties[key].type)) {
|
||||
numeric = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (numeric) {
|
||||
return "number_object";
|
||||
}
|
||||
}
|
||||
return "other";
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
requestUpdate: async function() {
|
||||
this.$emit("requestUpdate")
|
||||
},
|
||||
sendValue: async function() {
|
||||
this.$emit("sendValue", this.internalValue)
|
||||
},
|
||||
checkboxUpdated: function() {
|
||||
if (this.internalValue != this.$refs.checkbox.checked) {
|
||||
this.internalValue = this.$refs.checkbox.checked;
|
||||
this.sendValue();
|
||||
}
|
||||
},
|
||||
focusIn: function(event) {
|
||||
this.valueOnEnter = event.target.value;
|
||||
},
|
||||
focusOut: function(event) {
|
||||
if (this.valueOnEnter != event.target.value) {
|
||||
this.sendValue(event.target.value);
|
||||
}
|
||||
},
|
||||
keyDown: function(event) {
|
||||
// Pressing enter should set the property, whether or not we think it's changed.
|
||||
if (event.keyCode == 13) {
|
||||
this.sendValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.input-and-buttons-container {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
align-content: stretch;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.numeric-setting-line-input {
|
||||
flex-grow: 1;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
width: 6em;
|
||||
}
|
||||
.button-next-to-input {
|
||||
flex-grow: 0;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,90 +1,23 @@
|
|||
<template>
|
||||
<div>
|
||||
<label v-if="dataType == 'number'" class="uk-form-label"
|
||||
>{{ label }}
|
||||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
v-model="value"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
type="number"
|
||||
@focusin="focusIn"
|
||||
@focusout="focusOut"
|
||||
@keydown="keyDown"
|
||||
/>
|
||||
<a class="button-next-to-input" @click="readProperty">
|
||||
<span class="material-symbols-outlined">refresh</span>
|
||||
</a>
|
||||
</div>
|
||||
</label>
|
||||
<div v-if="dataType == 'boolean'" class="input-and-buttons-container">
|
||||
<label class="uk-form-label numeric-setting-line-input">
|
||||
<input
|
||||
ref="checkbox"
|
||||
v-model="value"
|
||||
class="uk-checkbox"
|
||||
type="checkbox"
|
||||
@change="writeProperty"
|
||||
/>
|
||||
{{ label }}
|
||||
</label>
|
||||
<a class="button-next-to-input" @click="readProperty">
|
||||
<span class="material-symbols-outlined">refresh</span>
|
||||
</a>
|
||||
</div>
|
||||
<label v-if="dataType == 'number_array'" class="uk-form-label"
|
||||
>{{ label }}
|
||||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
v-for="i in valueLength"
|
||||
:key="i"
|
||||
v-model="value[i - 1]"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
type="number"
|
||||
@focusin="focusIn"
|
||||
@focusout="focusOut"
|
||||
@keydown="keyDown"
|
||||
/>
|
||||
<a class="button-next-to-input" @click="readProperty">
|
||||
<span class="material-symbols-outlined">refresh</span>
|
||||
</a>
|
||||
</div>
|
||||
</label>
|
||||
<label v-if="dataType == 'number_object'" class="uk-form-label"
|
||||
>{{ label }}
|
||||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
v-for="(_v, key) in value"
|
||||
:key="key"
|
||||
v-model="value[key]"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
type="number"
|
||||
@focusin="focusIn"
|
||||
@focusout="focusOut"
|
||||
@keydown="keyDown"
|
||||
/>
|
||||
<a class="button-next-to-input" @click="readProperty">
|
||||
<span class="material-symbols-outlined">refresh</span>
|
||||
</a>
|
||||
</div>
|
||||
</label>
|
||||
<label v-if="dataType == 'other'" class="uk-form-label"
|
||||
>{{ label }}
|
||||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
:value="value"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
type="text"
|
||||
disabled="true"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<input-from-schema
|
||||
v-model="value"
|
||||
:data-schema="propertyDescription"
|
||||
:label="label"
|
||||
@requestUpdate="readProperty"
|
||||
@sendValue="writeProperty"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import InputFromSchema from "./inputFromSchema.vue";
|
||||
|
||||
export default {
|
||||
name: "PropertyControl",
|
||||
|
||||
components: {
|
||||
InputFromSchema
|
||||
},
|
||||
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
|
|
@ -98,35 +31,25 @@ export default {
|
|||
type: String,
|
||||
required: true
|
||||
},
|
||||
readBack: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
readBackDelay: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
default: 1000,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
data: () => {
|
||||
data() {
|
||||
return {
|
||||
value: undefined,
|
||||
valueOnEnter: undefined,
|
||||
focused: false
|
||||
value: undefined
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
valueLength: function() {
|
||||
if (this.dataType == "number_array") {
|
||||
if (this.value == undefined) {
|
||||
return 0;
|
||||
}
|
||||
return this.value.length;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
},
|
||||
readBack: function() {
|
||||
return this.readBackDelay !== undefined;
|
||||
},
|
||||
propertyDescription: function() {
|
||||
try {
|
||||
return this.thingDescription(this.thingName).properties[
|
||||
|
|
@ -135,42 +58,6 @@ export default {
|
|||
} catch (error) {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
dataType: function() {
|
||||
let prop = this.propertyDescription;
|
||||
if (prop == undefined) {
|
||||
return "undefined";
|
||||
}
|
||||
const num_types = ["integer", "float", "number"];
|
||||
if (num_types.includes(prop.type)) {
|
||||
return "number";
|
||||
}
|
||||
if (prop.type == "array") {
|
||||
if (num_types.includes(prop.items.type)) {
|
||||
return "number_array";
|
||||
}
|
||||
if (Array.isArray(prop.items)) {
|
||||
if (prop.items.every(t => num_types.includes(t.type))) {
|
||||
return "number_array";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (prop.type == "boolean") {
|
||||
return "boolean";
|
||||
}
|
||||
if (prop.type == "object") {
|
||||
let numeric = true;
|
||||
for (let key in prop.properties) {
|
||||
if (!num_types.includes(prop.properties[key].type)) {
|
||||
numeric = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (numeric) {
|
||||
return "number_object";
|
||||
}
|
||||
}
|
||||
return "other";
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -199,10 +86,9 @@ export default {
|
|||
this.value = data;
|
||||
return data;
|
||||
},
|
||||
writeProperty: async function() {
|
||||
writeProperty: async function(requestedValue) {
|
||||
try {
|
||||
let requestedValue = this.value;
|
||||
console.log("writing", requestedValue);
|
||||
this.value=requestedValue;
|
||||
await this.writeThingProperty(
|
||||
this.thingName,
|
||||
this.propertyName,
|
||||
|
|
@ -224,51 +110,9 @@ export default {
|
|||
} catch (error) {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
}
|
||||
},
|
||||
checkboxUpdated: function() {
|
||||
if (this.value != this.$refs.checkbox.checked) {
|
||||
this.value = this.$refs.checkbox.checked;
|
||||
this.writeProperty();
|
||||
}
|
||||
},
|
||||
focusIn: function(event) {
|
||||
this.valueOnEnter = event.target.value;
|
||||
},
|
||||
focusOut: function(event) {
|
||||
if (this.valueOnEnter != event.target.value) {
|
||||
this.writeProperty(event.target.value);
|
||||
}
|
||||
},
|
||||
keyDown: function(event) {
|
||||
// Pressing enter should set the property, whether or not we think it's changed.
|
||||
if (event.keyCode == 13) {
|
||||
this.writeProperty();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.input-and-buttons-container {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
align-content: stretch;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.numeric-setting-line-input {
|
||||
flex-grow: 1;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
width: 6em;
|
||||
}
|
||||
.button-next-to-input {
|
||||
flex-grow: 0;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<action-button
|
||||
:thing="actionData.thing"
|
||||
:action="actionData.action"
|
||||
:poll-interval="actionData.poll_interval"
|
||||
:submit-label="actionData.submit_label"
|
||||
:can-terminate="actionData.can_terminate"
|
||||
:requires-confirmation="actionData.requires_confirmation"
|
||||
:confirmation-message="actionData.confirmation_message"
|
||||
:button-primary="actionData.button_primary"
|
||||
:modal-progress="actionData.modal_progress"
|
||||
@response="actionResponse"
|
||||
@error="modalError"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ActionButton from "./actionButton.vue";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "ServerSpecifiedActionButton",
|
||||
|
||||
components: {
|
||||
ActionButton
|
||||
},
|
||||
|
||||
|
||||
props: {
|
||||
actionData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
actionResponse: function() {
|
||||
if (this.actionData.notify_on_success) {
|
||||
this.modalNotify(this.actionData.success_message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<property-control
|
||||
:thing="propertyData.thing"
|
||||
:property-name="propertyData.property_name"
|
||||
:thing-name="propertyData.thing"
|
||||
:label="propertyData.label"
|
||||
:read-back="propertyData.read_back"
|
||||
:read-back-delay="propertyData.read_back_delay"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PropertyControl from "./propertyControl.vue";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "ServerSpecifiedPropertyControl",
|
||||
|
||||
components: {
|
||||
PropertyControl
|
||||
},
|
||||
|
||||
props: {
|
||||
propertyData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less"></style>
|
||||
|
|
@ -5,7 +5,14 @@
|
|||
<li>
|
||||
<a class="uk-accordion-title" href="#">Settings</a>
|
||||
<div class="uk-accordion-content">
|
||||
<p> TODO!</p>
|
||||
<input-from-schema
|
||||
v-if="backgroundDetectorStatus"
|
||||
v-model="backgroundDetectorStatus.settings"
|
||||
:data-schema="backgroundDetectorStatus.settings_schema"
|
||||
label=""
|
||||
@requestUpdate="readSettings"
|
||||
@sendValue="writeSettings"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
@ -17,6 +24,7 @@
|
|||
:can-terminate="false"
|
||||
:poll-interval="0.1"
|
||||
@response="alertBackgroundSet"
|
||||
@error="modalError"
|
||||
/>
|
||||
</div>
|
||||
<div class="uk-margin">
|
||||
|
|
@ -25,10 +33,11 @@
|
|||
thing="camera"
|
||||
action="image_is_sample"
|
||||
submit-label="Check Current Image"
|
||||
:isDisabled="!ready"
|
||||
:can-terminate="false"
|
||||
:poll-interval="0.1"
|
||||
@response="alertImageLabel"
|
||||
@error="backgroundDetectError"
|
||||
@error="modalError"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -37,10 +46,25 @@
|
|||
|
||||
<script>
|
||||
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
||||
import InputFromSchema from "../../labThingsComponents/inputFromSchema.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ActionButton
|
||||
ActionButton,
|
||||
InputFromSchema
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
backgroundDetectorStatus: undefined,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
ready() {
|
||||
const status = this.backgroundDetectorStatus;
|
||||
return status && status.ready === true;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
|
@ -51,12 +75,25 @@ export default {
|
|||
let label = r.output[0] ? "sample" : "background";
|
||||
this.modalNotify(`Current image is ${label} (${r.output[1]})`);
|
||||
},
|
||||
backgroundDetectError() {
|
||||
this.modalError(
|
||||
"Background detection failed, most likely you need to set a background image." +
|
||||
" There may be more information in the log."
|
||||
readSettings: async function() {
|
||||
this.backgroundDetectorStatus = await this.readThingProperty(
|
||||
"camera",
|
||||
"background_detector_status"
|
||||
);
|
||||
},
|
||||
writeSettings: async function(requestedValue) {
|
||||
await this.invokeAction(
|
||||
"camera",
|
||||
"update_detector_settings",
|
||||
{"data": requestedValue}
|
||||
);
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
this.backgroundDetectorStatus = await this.readThingProperty(
|
||||
"camera",
|
||||
"background_detector_status"
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -4,37 +4,14 @@
|
|||
<div class="uk-width-large">
|
||||
<h3>Automatic calibration</h3>
|
||||
<cameraCalibrationSettings :camera-uri="cameraUri" />
|
||||
|
||||
<h3>Manual camera settings</h3>
|
||||
<form @submit.prevent="applySettingsRequest">
|
||||
<div class="uk-margin-small-bottom">
|
||||
<ul uk-accordion="multiple: true">
|
||||
<li class="uk-open">
|
||||
<a class="uk-accordion-title" href="#">Pi Camera Settings</a>
|
||||
<div class="uk-accordion-content">
|
||||
<PropertyControl
|
||||
label="Exposure Time (0-33251)"
|
||||
property-name="exposure_time"
|
||||
thing-name="camera"
|
||||
:read-back-delay="1000"
|
||||
/>
|
||||
<PropertyControl
|
||||
label="Analogue Gain"
|
||||
property-name="analogue_gain"
|
||||
thing-name="camera"
|
||||
:read-back-delay="1000"
|
||||
/>
|
||||
<PropertyControl
|
||||
label="Colour Gains"
|
||||
property-name="colour_gains"
|
||||
thing-name="camera"
|
||||
:read-back-delay="1000"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
<div class="uk-margin-small-bottom">
|
||||
<server-specified-property-control
|
||||
v-for="(setting, index) in manualCameraSettings"
|
||||
:key="'cam_setting' + index"
|
||||
:property-data="setting"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="mini-stream">
|
||||
|
|
@ -47,7 +24,7 @@
|
|||
<script>
|
||||
import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue";
|
||||
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
|
||||
import PropertyControl from "../../labThingsComponents/propertyControl.vue";
|
||||
import ServerSpecifiedPropertyControl from "../../labThingsComponents/serverSpecifiedPropertyControl.vue";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
|
|
@ -56,37 +33,12 @@ export default {
|
|||
components: {
|
||||
cameraCalibrationSettings,
|
||||
miniStreamDisplay,
|
||||
PropertyControl
|
||||
ServerSpecifiedPropertyControl
|
||||
},
|
||||
|
||||
data: function() {
|
||||
data() {
|
||||
return {
|
||||
picamera: {
|
||||
shutter_speed: undefined,
|
||||
analog_gain: undefined,
|
||||
digital_gain: undefined,
|
||||
framerate: undefined,
|
||||
awb_gains: undefined
|
||||
},
|
||||
mjpeg_bitrate: undefined,
|
||||
stream_resolution: undefined,
|
||||
jpeg_quality: undefined,
|
||||
bitrateOptions: [
|
||||
{ text: "Maximum (unlimited)", value: -1 },
|
||||
{ text: "High (25Mbps)", value: 25000000 },
|
||||
{ text: "Normal (17Mbps)", value: 17000000 },
|
||||
{ text: "Low (5Mbps)", value: 5000000 },
|
||||
{ text: "Very low (2.5Mbps)", value: 2500000 }
|
||||
],
|
||||
resolutionOptions: [
|
||||
{ text: "Higher (832, 624)", value: [832, 624] },
|
||||
{ text: "Normal (640, 480)", value: [640, 480] }
|
||||
],
|
||||
framerateOptions: [
|
||||
{ text: "Normal (30fps)", value: 30 },
|
||||
{ text: "Low (15fps)", value: 15 },
|
||||
{ text: "Very low (10fps)", value: 10 }
|
||||
]
|
||||
manualCameraSettings: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -94,6 +46,13 @@ export default {
|
|||
cameraUri: function() {
|
||||
return `${this.$store.getters.baseUri}/camera/`;
|
||||
}
|
||||
},
|
||||
|
||||
async created() {
|
||||
this.manualCameraSettings = await this.readThingProperty(
|
||||
"camera",
|
||||
"manual_camera_settings"
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,100 +1,34 @@
|
|||
<template>
|
||||
<div>
|
||||
<!--Show auto calibrate if default plugin is enabled-->
|
||||
<div v-if="'full_auto_calibrate' in actions" class="uk-margin-small">
|
||||
<action-button
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="true"
|
||||
:confirmation-message="
|
||||
'Start recalibration? This may take a while, and the microscope will be locked during this time.'
|
||||
"
|
||||
thing="camera"
|
||||
action="full_auto_calibrate"
|
||||
:submit-label="'Full Auto-Calibrate'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="'auto_expose_from_minimum' in actions" class="uk-margin-small">
|
||||
<action-button
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="false"
|
||||
thing="camera"
|
||||
action="auto_expose_from_minimum"
|
||||
:submit-label="'Auto Gain & Shutter Speed'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="'calibrate_white_balance' in actions" class="uk-margin-small">
|
||||
<action-button
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="false"
|
||||
thing="camera"
|
||||
action="calibrate_white_balance"
|
||||
:submit-label="'Auto White Balance'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="'calibrate_lens_shading' in actions" class="uk-margin-small">
|
||||
<action-button
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="true"
|
||||
:confirmation-message="
|
||||
'Is the microscope looking at an evenly illuminated, empty field of view? ' +
|
||||
'If not, the current image will show through in any images captured afterwards.'
|
||||
"
|
||||
thing="camera"
|
||||
action="calibrate_lens_shading"
|
||||
:submit-label="'Auto Flat Field Correction'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!--Show calibration actions as specified by the camera.-->
|
||||
<div
|
||||
v-show="showExtraSettings"
|
||||
v-if="'flatten_lens_shading_table' in actions"
|
||||
v-for="(action, index) in primaryCalibrationActions"
|
||||
:key="'primary_cal' + index"
|
||||
class="uk-child-width-expand"
|
||||
>
|
||||
<action-button
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="false"
|
||||
thing="camera"
|
||||
action="flat_lens_shading"
|
||||
:submit-label="'Disable Flat Field Correction'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
/>
|
||||
<server-specified-action-button :action-data="action" />
|
||||
</div>
|
||||
<div
|
||||
v-show="showExtraSettings"
|
||||
v-if="'reset_lens_shading' in actions"
|
||||
class="uk-child-width-expand"
|
||||
>
|
||||
<action-button
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="false"
|
||||
thing="camera"
|
||||
action="reset_lens_shading"
|
||||
:submit-label="'Reset Flat Field Correction'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
/>
|
||||
<div v-if="showExtraSettings">
|
||||
<div
|
||||
v-for="(action, index) in secondaryCalibrationActions"
|
||||
:key="'secondary_cal' + index"
|
||||
class="uk-child-width-expand"
|
||||
>
|
||||
<server-specified-action-button :action-data="action" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ActionButton from "../../../labThingsComponents/actionButton.vue";
|
||||
import ServerSpecifiedActionButton from "../../../labThingsComponents/serverSpecifiedActionButton.vue";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "CameraCalibrationSettings",
|
||||
|
||||
components: {
|
||||
ActionButton
|
||||
ServerSpecifiedActionButton
|
||||
},
|
||||
|
||||
props: {
|
||||
|
|
@ -109,16 +43,28 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
primaryCalibrationActions: [],
|
||||
secondaryCalibrationActions: []
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
actions() {
|
||||
return this.$store.getters["wot/thingDescription"]("camera").actions;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onRecalibrateResponse: function() {
|
||||
this.modalNotify("Finished recalibration.");
|
||||
}
|
||||
async created() {
|
||||
this.primaryCalibrationActions = await this.readThingProperty(
|
||||
"camera",
|
||||
"primary_calibration_actions"
|
||||
);
|
||||
this.secondaryCalibrationActions = await this.readThingProperty(
|
||||
"camera",
|
||||
"secondary_calibration_actions"
|
||||
);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -67,6 +67,21 @@ Vue.mixin({
|
|||
}
|
||||
await axios.put(url, value);
|
||||
},
|
||||
async invokeAction(thing, action, data) {
|
||||
let url = this.$store.getters["wot/thingActionUrl"](
|
||||
thing,
|
||||
action,
|
||||
"invokeaction",
|
||||
false
|
||||
);
|
||||
try {
|
||||
let response = await axios.post(url, data);
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.modalError(error);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
thingActionUrl(thing, action, allow_missing = false) {
|
||||
let url = this.$store.getters["wot/thingActionUrl"](
|
||||
thing,
|
||||
|
|
@ -132,7 +147,6 @@ Vue.mixin({
|
|||
modalError: function(error) {
|
||||
var errormsg = this.getErrorMessage(error);
|
||||
this.$store.commit("setErrorMessage", errormsg);
|
||||
console.log("Modal error:", error);
|
||||
UIkit.notification({
|
||||
message: `${errormsg}`,
|
||||
status: "danger"
|
||||
|
|
@ -140,25 +154,43 @@ Vue.mixin({
|
|||
},
|
||||
|
||||
getErrorMessage: function(error) {
|
||||
// If a response was obtained, format it nicely and return it
|
||||
// Format the error.
|
||||
let data = this.getErrorData(error)
|
||||
// Get error data may get an object. Handle edge cases and if not try to use
|
||||
// JSON to get the best string. This stops "objectObject" showing as an error.
|
||||
if (data === null) return "null";
|
||||
if (data === undefined) return "undefined";
|
||||
if (typeof data === 'string') return data;
|
||||
try {
|
||||
return JSON.stringify(data, null, 2);
|
||||
} catch (err) {
|
||||
return String(data);
|
||||
}
|
||||
},
|
||||
getErrorData: function(error){
|
||||
// If a response was obtained, extract the most specific message
|
||||
if (error.response) {
|
||||
// If the response is a nicely formatted JSON response from the server
|
||||
if (error.response.data.message) {
|
||||
return `${error.response.data.message}`;
|
||||
return error.response.data.message;
|
||||
}
|
||||
if (error.response.data.detail) {
|
||||
return `${error.response.data.detail}`;
|
||||
try {
|
||||
return error.response.data.detail[0].msg
|
||||
} catch (err) {
|
||||
return error.response.data.detail;
|
||||
}
|
||||
}
|
||||
// If the response is just some generic error response
|
||||
if (error.response.data) {
|
||||
return `${error.response.data}`;
|
||||
return error.response.data;
|
||||
}
|
||||
return `${error.response}`;
|
||||
return error.response;
|
||||
}
|
||||
// If we have an error object with a message, use that
|
||||
if (error.message) return `${error.message}`;
|
||||
// Otherwise attempt to cast it to a string.
|
||||
return `${error}`;
|
||||
if (error.message) return error.message;
|
||||
// At this point just formatting the whole error object is the best we can do.
|
||||
return error;
|
||||
},
|
||||
|
||||
showModalElement: function(element) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue