Complete UI for arbitrary dectector settings. Required better method for invoking actions.
Error handling was fixed in the process of getting the above correct.
This commit is contained in:
parent
92d15d921b
commit
33413a591a
5 changed files with 102 additions and 25 deletions
|
|
@ -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
|
||||
|
|
@ -153,13 +153,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
|
||||
|
|
|
|||
|
|
@ -516,6 +516,20 @@ 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
|
||||
|
||||
@lt.thing_setting
|
||||
def background_detector_data(self) -> dict:
|
||||
"""The data for each background detector, used to save to disk."""
|
||||
|
|
|
|||
|
|
@ -51,20 +51,21 @@
|
|||
</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="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 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"
|
||||
|
|
@ -112,6 +113,16 @@ export default {
|
|||
}
|
||||
},
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
v-model="backgroundDetectorStatus.settings"
|
||||
:data-schema="backgroundDetectorStatus.settings_schema"
|
||||
label=""
|
||||
@requestUpdate="readSettings"
|
||||
@sendValue="writeSettings"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
|
|
@ -72,6 +74,19 @@ export default {
|
|||
alertImageLabel(r) {
|
||||
let label = r.output[0] ? "sample" : "background";
|
||||
this.modalNotify(`Current image is ${label} (${r.output[1]})`);
|
||||
},
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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