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
|
from typing import Optional, Any
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
from pydantic.errors import PydanticUserError
|
from pydantic.errors import PydanticUserError
|
||||||
from scipy.stats import norm
|
from scipy.stats import norm
|
||||||
from labthings_fastapi.thing_description import type_to_dataschema
|
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
|
``ready`` is used in case more complex methods are added in the future, which
|
||||||
need different initialisation.
|
need different initialisation.
|
||||||
"""
|
"""
|
||||||
settings: BaseModel
|
settings: dict[str, Any]
|
||||||
"""The settings for this this background detect Algorithm"""
|
"""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
|
# 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
|
# DataSchema can be used directly. For now `model_dump()` must be used to dump schema
|
||||||
# to a dict.
|
# to a dict.
|
||||||
settings_schema: dict[str, Any]
|
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:
|
class BackgroundDetectAlgorithm:
|
||||||
|
|
@ -63,7 +69,7 @@ class BackgroundDetectAlgorithm:
|
||||||
"""The status information needed for the GUI. Read only."""
|
"""The status information needed for the GUI. Read only."""
|
||||||
return BackgroundDetectorStatus(
|
return BackgroundDetectorStatus(
|
||||||
ready=self.background_data is not None,
|
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
|
# Dump model with `model_dump()` for reason explained when defining
|
||||||
# BackgroundDetectorStatus
|
# BackgroundDetectorStatus
|
||||||
settings_schema=type_to_dataschema(self.settings_data_model).model_dump(),
|
settings_schema=type_to_dataschema(self.settings_data_model).model_dump(),
|
||||||
|
|
@ -152,13 +158,18 @@ class ChannelDistributions(BaseModel):
|
||||||
class ColourChannelDetectSettings(BaseModel):
|
class ColourChannelDetectSettings(BaseModel):
|
||||||
"""A BaseModel for storing the settings for colour channel detectors."""
|
"""A BaseModel for storing the settings for colour channel detectors."""
|
||||||
|
|
||||||
|
model_config = ConfigDict(extra="forbid")
|
||||||
|
|
||||||
channel_tolerance: float = 7.0
|
channel_tolerance: float = 7.0
|
||||||
"""Channel Tolerance
|
"""Channel Tolerance
|
||||||
|
|
||||||
The number of standard deviations a pixel value must be from the background mean
|
The number of standard deviations a pixel value must be from the background mean
|
||||||
to be considered sample.
|
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 (%)
|
"""Sample Coverage Required (%)
|
||||||
|
|
||||||
The minimum percentage of the image that needs to be identified as sample for the
|
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
|
import labthings_fastapi as lt
|
||||||
from labthings_fastapi.types.numpy import NDArray
|
from labthings_fastapi.types.numpy import NDArray
|
||||||
|
|
||||||
|
from openflexure_microscope_server.ui import ActionButton, PropertyControl
|
||||||
from openflexure_microscope_server.background_detect import (
|
from openflexure_microscope_server.background_detect import (
|
||||||
ColourChannelDetectLUV,
|
ColourChannelDetectLUV,
|
||||||
BackgroundDetectAlgorithm,
|
BackgroundDetectAlgorithm,
|
||||||
|
|
@ -476,6 +477,21 @@ class BaseCamera(lt.Thing):
|
||||||
time.sleep(self.settling_time)
|
time.sleep(self.settling_time)
|
||||||
self.discard_frames()
|
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
|
# Note that the default detector name is set at init. This is over written if
|
||||||
# setting is loaded from disk.
|
# setting is loaded from disk.
|
||||||
@lt.thing_setting
|
@lt.thing_setting
|
||||||
|
|
@ -500,6 +516,22 @@ class BaseCamera(lt.Thing):
|
||||||
"""The status of the active detector for the UI."""
|
"""The status of the active detector for the UI."""
|
||||||
return self.active_detector.status
|
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
|
@lt.thing_setting
|
||||||
def background_detector_data(self) -> dict:
|
def background_detector_data(self) -> dict:
|
||||||
"""The data for each background detector, used to save to disk."""
|
"""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
|
import labthings_fastapi as lt
|
||||||
from labthings_fastapi.exceptions import NotConnectedToServerError
|
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 picamera_recalibrate_utils as recalibrate_utils
|
||||||
from . import BaseCamera, JPEGBlob, ArrayModel
|
from . import BaseCamera, JPEGBlob, ArrayModel
|
||||||
|
|
||||||
|
|
@ -777,6 +783,88 @@ class StreamingPiCamera2(BaseCamera):
|
||||||
)
|
)
|
||||||
self._initialise_picamera()
|
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
|
@lt.thing_property
|
||||||
def lens_shading_tables(self) -> Optional[LensShading]:
|
def lens_shading_tables(self) -> Optional[LensShading]:
|
||||||
"""The current lens shading (i.e. flat-field correction).
|
"""The current lens shading (i.e. flat-field correction).
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,13 @@ from scipy.ndimage import gaussian_filter
|
||||||
|
|
||||||
import labthings_fastapi as lt
|
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 . import BaseCamera, JPEGBlob, ArrayModel
|
||||||
from ..stage import BaseStage
|
from ..stage import BaseStage
|
||||||
|
|
||||||
|
|
@ -40,6 +47,7 @@ class SimulatedCamera(BaseCamera):
|
||||||
|
|
||||||
_stage: Optional[BaseStage] = None
|
_stage: Optional[BaseStage] = None
|
||||||
_server: Optional[lt.ThingServer] = None
|
_server: Optional[lt.ThingServer] = None
|
||||||
|
_show_sample: bool = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
@ -124,10 +132,11 @@ class SimulatedCamera(BaseCamera):
|
||||||
Canvas is int16 so that random noise can be added to simulation image before
|
Canvas is int16 so that random noise can be added to simulation image before
|
||||||
changing to unit8 to stop wrapping.
|
changing to unit8 to stop wrapping.
|
||||||
"""
|
"""
|
||||||
self.canvas = np.ones(self.canvas_shape, dtype=np.int16)
|
self.blank_canvas = np.ones(self.canvas_shape, dtype=np.int16)
|
||||||
self.canvas[:, :, 0] *= BG_COLOR[0]
|
self.blank_canvas[:, :, 0] *= BG_COLOR[0]
|
||||||
self.canvas[:, :, 1] *= BG_COLOR[1]
|
self.blank_canvas[:, :, 1] *= BG_COLOR[1]
|
||||||
self.canvas[:, :, 2] *= BG_COLOR[2]
|
self.blank_canvas[:, :, 2] *= BG_COLOR[2]
|
||||||
|
self.canvas = self.blank_canvas.copy()
|
||||||
w, h, _ = self.glyph_shape
|
w, h, _ = self.glyph_shape
|
||||||
for x, y, sprite_size_index in self.blobs:
|
for x, y, sprite_size_index in self.blobs:
|
||||||
self.canvas[
|
self.canvas[
|
||||||
|
|
@ -149,7 +158,8 @@ class SimulatedCamera(BaseCamera):
|
||||||
x_slice = slice(top_left[0], top_left[0] + self.shape[0])
|
x_slice = slice(top_left[0], top_left[0] + self.shape[0])
|
||||||
y_slice = slice(top_left[1], top_left[1] + self.shape[1])
|
y_slice = slice(top_left[1], top_left[1] + self.shape[1])
|
||||||
z_slice = slice(None)
|
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(
|
image = gaussian_filter(
|
||||||
focused_image,
|
focused_image,
|
||||||
sigma=np.abs(pos[2]) / 5,
|
sigma=np.abs(pos[2]) / 5,
|
||||||
|
|
@ -161,7 +171,7 @@ class SimulatedCamera(BaseCamera):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add noise and convert to uint8
|
# 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 < 0] = 0
|
||||||
image[image > 255] = 255
|
image[image > 255] = 255
|
||||||
return image.astype("uint8")
|
return image.astype("uint8")
|
||||||
|
|
@ -216,6 +226,8 @@ class SimulatedCamera(BaseCamera):
|
||||||
return self._capture_thread.is_alive()
|
return self._capture_thread.is_alive()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
noise_level = lt.ThingProperty(float, 2.0)
|
||||||
|
|
||||||
def _capture_frames(self):
|
def _capture_frames(self):
|
||||||
portal = lt.get_blocking_portal(self)
|
portal = lt.get_blocking_portal(self)
|
||||||
while self._capture_enabled:
|
while self._capture_enabled:
|
||||||
|
|
@ -279,3 +291,30 @@ class SimulatedCamera(BaseCamera):
|
||||||
output = io.BytesIO()
|
output = io.BytesIO()
|
||||||
piexif.insert(piexif.dump(exif_dict), jpeg, output)
|
piexif.insert(piexif.dump(exif_dict), jpeg, output)
|
||||||
return JPEGBlob.from_bytes(output.getvalue())
|
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(
|
background_detector_status = BackgroundDetectorStatus(
|
||||||
ready=True,
|
ready=True,
|
||||||
settings=ColourChannelDetectSettings(),
|
settings=ColourChannelDetectSettings().model_dump(),
|
||||||
settings_schema={"fake": "schema"},
|
settings_schema={"fake": "schema"},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,8 @@ def test_partial_base_class(background_image):
|
||||||
bad_algo1 = BadAlgo1()
|
bad_algo1 = BadAlgo1()
|
||||||
status = bad_algo1.status
|
status = bad_algo1.status
|
||||||
assert not status.ready
|
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):
|
with pytest.raises(NotImplementedError):
|
||||||
# Should error on any dictionary input. This simulates loading settings from
|
# 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>
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
:disabled="isDisabled"
|
||||||
:hidden="taskStarted"
|
:hidden="taskStarted"
|
||||||
class="uk-button uk-width-1-1"
|
class="uk-button uk-width-1-1"
|
||||||
:class="[buttonPrimary ? 'uk-button-primary' : 'uk-button-default']"
|
:class="[buttonPrimary ? 'uk-button-primary' : 'uk-button-default']"
|
||||||
|
|
@ -132,7 +133,12 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
}
|
},
|
||||||
|
isDisabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
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>
|
<template>
|
||||||
<div>
|
<input-from-schema
|
||||||
<label v-if="dataType == 'number'" class="uk-form-label"
|
v-model="value"
|
||||||
>{{ label }}
|
:data-schema="propertyDescription"
|
||||||
<div class="input-and-buttons-container">
|
:label="label"
|
||||||
<input
|
@requestUpdate="readProperty"
|
||||||
v-model="value"
|
@sendValue="writeProperty"
|
||||||
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>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import InputFromSchema from "./inputFromSchema.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "PropertyControl",
|
name: "PropertyControl",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
InputFromSchema
|
||||||
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
@ -98,35 +31,25 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
|
readBack: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
readBackDelay: {
|
readBackDelay: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: undefined,
|
default: 1000,
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data: () => {
|
data() {
|
||||||
return {
|
return {
|
||||||
value: undefined,
|
value: undefined
|
||||||
valueOnEnter: undefined,
|
|
||||||
focused: false
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
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() {
|
propertyDescription: function() {
|
||||||
try {
|
try {
|
||||||
return this.thingDescription(this.thingName).properties[
|
return this.thingDescription(this.thingName).properties[
|
||||||
|
|
@ -135,42 +58,6 @@ export default {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return undefined;
|
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;
|
this.value = data;
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
writeProperty: async function() {
|
writeProperty: async function(requestedValue) {
|
||||||
try {
|
try {
|
||||||
let requestedValue = this.value;
|
this.value=requestedValue;
|
||||||
console.log("writing", requestedValue);
|
|
||||||
await this.writeThingProperty(
|
await this.writeThingProperty(
|
||||||
this.thingName,
|
this.thingName,
|
||||||
this.propertyName,
|
this.propertyName,
|
||||||
|
|
@ -224,51 +110,9 @@ export default {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.modalError(error); // Let mixin handle 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>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped></style>
|
||||||
.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>
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
<li>
|
||||||
<a class="uk-accordion-title" href="#">Settings</a>
|
<a class="uk-accordion-title" href="#">Settings</a>
|
||||||
<div class="uk-accordion-content">
|
<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>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -17,6 +24,7 @@
|
||||||
:can-terminate="false"
|
:can-terminate="false"
|
||||||
:poll-interval="0.1"
|
:poll-interval="0.1"
|
||||||
@response="alertBackgroundSet"
|
@response="alertBackgroundSet"
|
||||||
|
@error="modalError"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
|
|
@ -25,10 +33,11 @@
|
||||||
thing="camera"
|
thing="camera"
|
||||||
action="image_is_sample"
|
action="image_is_sample"
|
||||||
submit-label="Check Current Image"
|
submit-label="Check Current Image"
|
||||||
|
:isDisabled="!ready"
|
||||||
:can-terminate="false"
|
:can-terminate="false"
|
||||||
:poll-interval="0.1"
|
:poll-interval="0.1"
|
||||||
@response="alertImageLabel"
|
@response="alertImageLabel"
|
||||||
@error="backgroundDetectError"
|
@error="modalError"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -37,10 +46,25 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
import ActionButton from "../../labThingsComponents/actionButton.vue";
|
||||||
|
import InputFromSchema from "../../labThingsComponents/inputFromSchema.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ActionButton
|
ActionButton,
|
||||||
|
InputFromSchema
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
backgroundDetectorStatus: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
ready() {
|
||||||
|
const status = this.backgroundDetectorStatus;
|
||||||
|
return status && status.ready === true;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -51,12 +75,25 @@ export default {
|
||||||
let label = r.output[0] ? "sample" : "background";
|
let label = r.output[0] ? "sample" : "background";
|
||||||
this.modalNotify(`Current image is ${label} (${r.output[1]})`);
|
this.modalNotify(`Current image is ${label} (${r.output[1]})`);
|
||||||
},
|
},
|
||||||
backgroundDetectError() {
|
readSettings: async function() {
|
||||||
this.modalError(
|
this.backgroundDetectorStatus = await this.readThingProperty(
|
||||||
"Background detection failed, most likely you need to set a background image." +
|
"camera",
|
||||||
" There may be more information in the log."
|
"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>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -4,37 +4,14 @@
|
||||||
<div class="uk-width-large">
|
<div class="uk-width-large">
|
||||||
<h3>Automatic calibration</h3>
|
<h3>Automatic calibration</h3>
|
||||||
<cameraCalibrationSettings :camera-uri="cameraUri" />
|
<cameraCalibrationSettings :camera-uri="cameraUri" />
|
||||||
|
|
||||||
<h3>Manual camera settings</h3>
|
<h3>Manual camera settings</h3>
|
||||||
<form @submit.prevent="applySettingsRequest">
|
<div class="uk-margin-small-bottom">
|
||||||
<div class="uk-margin-small-bottom">
|
<server-specified-property-control
|
||||||
<ul uk-accordion="multiple: true">
|
v-for="(setting, index) in manualCameraSettings"
|
||||||
<li class="uk-open">
|
:key="'cam_setting' + index"
|
||||||
<a class="uk-accordion-title" href="#">Pi Camera Settings</a>
|
:property-data="setting"
|
||||||
<div class="uk-accordion-content">
|
/>
|
||||||
<PropertyControl
|
</div>
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<div id="mini-stream">
|
<div id="mini-stream">
|
||||||
|
|
@ -47,7 +24,7 @@
|
||||||
<script>
|
<script>
|
||||||
import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue";
|
import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue";
|
||||||
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
|
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
|
||||||
import PropertyControl from "../../labThingsComponents/propertyControl.vue";
|
import ServerSpecifiedPropertyControl from "../../labThingsComponents/serverSpecifiedPropertyControl.vue";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -56,37 +33,12 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
cameraCalibrationSettings,
|
cameraCalibrationSettings,
|
||||||
miniStreamDisplay,
|
miniStreamDisplay,
|
||||||
PropertyControl
|
ServerSpecifiedPropertyControl
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
data() {
|
||||||
return {
|
return {
|
||||||
picamera: {
|
manualCameraSettings: [],
|
||||||
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 }
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -94,6 +46,13 @@ export default {
|
||||||
cameraUri: function() {
|
cameraUri: function() {
|
||||||
return `${this.$store.getters.baseUri}/camera/`;
|
return `${this.$store.getters.baseUri}/camera/`;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async created() {
|
||||||
|
this.manualCameraSettings = await this.readThingProperty(
|
||||||
|
"camera",
|
||||||
|
"manual_camera_settings"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,100 +1,34 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!--Show auto calibrate if default plugin is enabled-->
|
<!--Show calibration actions as specified by the camera.-->
|
||||||
<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>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-show="showExtraSettings"
|
v-for="(action, index) in primaryCalibrationActions"
|
||||||
v-if="'flatten_lens_shading_table' in actions"
|
:key="'primary_cal' + index"
|
||||||
class="uk-child-width-expand"
|
class="uk-child-width-expand"
|
||||||
>
|
>
|
||||||
<action-button
|
<server-specified-action-button :action-data="action" />
|
||||||
:can-terminate="false"
|
|
||||||
:requires-confirmation="false"
|
|
||||||
thing="camera"
|
|
||||||
action="flat_lens_shading"
|
|
||||||
:submit-label="'Disable Flat Field Correction'"
|
|
||||||
@response="onRecalibrateResponse"
|
|
||||||
@error="modalError"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="showExtraSettings">
|
||||||
v-show="showExtraSettings"
|
<div
|
||||||
v-if="'reset_lens_shading' in actions"
|
v-for="(action, index) in secondaryCalibrationActions"
|
||||||
class="uk-child-width-expand"
|
:key="'secondary_cal' + index"
|
||||||
>
|
class="uk-child-width-expand"
|
||||||
<action-button
|
>
|
||||||
:can-terminate="false"
|
<server-specified-action-button :action-data="action" />
|
||||||
:requires-confirmation="false"
|
</div>
|
||||||
thing="camera"
|
|
||||||
action="reset_lens_shading"
|
|
||||||
:submit-label="'Reset Flat Field Correction'"
|
|
||||||
@response="onRecalibrateResponse"
|
|
||||||
@error="modalError"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ActionButton from "../../../labThingsComponents/actionButton.vue";
|
import ServerSpecifiedActionButton from "../../../labThingsComponents/serverSpecifiedActionButton.vue";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
name: "CameraCalibrationSettings",
|
name: "CameraCalibrationSettings",
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
ActionButton
|
ServerSpecifiedActionButton
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -109,16 +43,28 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
primaryCalibrationActions: [],
|
||||||
|
secondaryCalibrationActions: []
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
actions() {
|
actions() {
|
||||||
return this.$store.getters["wot/thingDescription"]("camera").actions;
|
return this.$store.getters["wot/thingDescription"]("camera").actions;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
async created() {
|
||||||
onRecalibrateResponse: function() {
|
this.primaryCalibrationActions = await this.readThingProperty(
|
||||||
this.modalNotify("Finished recalibration.");
|
"camera",
|
||||||
}
|
"primary_calibration_actions"
|
||||||
|
);
|
||||||
|
this.secondaryCalibrationActions = await this.readThingProperty(
|
||||||
|
"camera",
|
||||||
|
"secondary_calibration_actions"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,21 @@ Vue.mixin({
|
||||||
}
|
}
|
||||||
await axios.put(url, value);
|
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) {
|
thingActionUrl(thing, action, allow_missing = false) {
|
||||||
let url = this.$store.getters["wot/thingActionUrl"](
|
let url = this.$store.getters["wot/thingActionUrl"](
|
||||||
thing,
|
thing,
|
||||||
|
|
@ -132,7 +147,6 @@ Vue.mixin({
|
||||||
modalError: function(error) {
|
modalError: function(error) {
|
||||||
var errormsg = this.getErrorMessage(error);
|
var errormsg = this.getErrorMessage(error);
|
||||||
this.$store.commit("setErrorMessage", errormsg);
|
this.$store.commit("setErrorMessage", errormsg);
|
||||||
console.log("Modal error:", error);
|
|
||||||
UIkit.notification({
|
UIkit.notification({
|
||||||
message: `${errormsg}`,
|
message: `${errormsg}`,
|
||||||
status: "danger"
|
status: "danger"
|
||||||
|
|
@ -140,25 +154,43 @@ Vue.mixin({
|
||||||
},
|
},
|
||||||
|
|
||||||
getErrorMessage: function(error) {
|
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 (error.response) {
|
||||||
// If the response is a nicely formatted JSON response from the server
|
// If the response is a nicely formatted JSON response from the server
|
||||||
if (error.response.data.message) {
|
if (error.response.data.message) {
|
||||||
return `${error.response.data.message}`;
|
return error.response.data.message;
|
||||||
}
|
}
|
||||||
if (error.response.data.detail) {
|
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 the response is just some generic error response
|
||||||
if (error.response.data) {
|
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 we have an error object with a message, use that
|
||||||
if (error.message) return `${error.message}`;
|
if (error.message) return error.message;
|
||||||
// Otherwise attempt to cast it to a string.
|
// At this point just formatting the whole error object is the best we can do.
|
||||||
return `${error}`;
|
return error;
|
||||||
},
|
},
|
||||||
|
|
||||||
showModalElement: function(element) {
|
showModalElement: function(element) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue