Merge branch 'v3-webapp' into 'v3'
Web app compatibility with v3 API See merge request openflexure/openflexure-microscope-server!163
This commit is contained in:
commit
04321cac96
26 changed files with 868 additions and 1352 deletions
21
README.md
21
README.md
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
The "server" is the main component of the OpenFlexure Microscope's software. It is responsible for controlling microscope hardware, data management, and allowing it to be controlled locally and over a network.
|
||||
This repository now includes the web client, which is served from the root of the Python web server.
|
||||
This software runs on [Python-LabThings](https://github.com/labthings/python-labthings/), and so most non-microscope functionality is handled by that library.
|
||||
This software runs on [LabThings-FastAPI](https://github.com/rwb27/labthings-fastapi/), and so most non-microscope functionality is handled by that library.
|
||||
|
||||
## Getting started
|
||||
|
||||
|
|
@ -62,18 +62,19 @@ To set up a development version of the software (most likely using emulated came
|
|||
* `cd openflexure-microscope-server`
|
||||
|
||||
### Set up the Python environment and run a test server
|
||||
* (Optional) Set local Python version to match what is available on the Pi
|
||||
* `pyenv init` (this may or may not be required, depending on how you installed `pyenv`)
|
||||
* `pyenv install 3.7.3`
|
||||
* `pyenv local 3.7.3`
|
||||
* (Optional) Set local Python version to 3.11
|
||||
* Create a virtual environment and activate it:
|
||||
* `python -m venv .venv`
|
||||
* `source .venv/bin/activate` (on Linux) or `.venv/Scripts/activate` (on Windows)
|
||||
* `pip install --upgrade pip wheel pipenv`
|
||||
* `pipenv install --dev` (This will install development dependencies. If you don't need these, `pipenv install` will get you just the dependencies needed to run the server, which takes about half the time.)
|
||||
* Finally, run the server:
|
||||
* You can use `ofm serve` or `ofm restart` on the Raspberry Pi to manage the server.
|
||||
* To run the server locally, with dummy hardware, you can use `python -m openflexure_microscope.api.app` to start a development-mode Flask server on `localhost:5000`
|
||||
* `pip install -e .[dev]` (This will install development dependencies. If you don't need these, there is probably a simpler way to run the server than cloning this repo.)
|
||||
* Finally, run the server: currently you can do this with `sudo systemctl start openflexure-microscope-server` if it's pre-installed on a Raspberry Pi, or `uvicorn --port 5000 openflexure_microscope_server.server:app` to run locally.
|
||||
|
||||
### Run the server manually on a Raspberry Pi
|
||||
```
|
||||
cd /var/openflexure
|
||||
sudo -u openflexure-ws PATH="/var/openflexure/application/openflexure-microscope-server/.venv/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin" /var/openflexure/application/openflexure-microscope-server/.venv/bin/uvicorn --reload --reload-dir=./application --host 0.0.0.0 --port 5000 openflexure_microscope_server.server:app
|
||||
```
|
||||
|
||||
|
||||
### Set up the Javascript environment and build
|
||||
* The Flask web application, written in Python, serves a web application written in `Vue.js`. This is distributed as part of the built version of the server, hosted on our [build server](https://build.openflexure.org/openflexure-microscope-server/).
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@
|
|||
import appContent from "./components/appContent.vue";
|
||||
import loadingContent from "./components/loadingContent.vue";
|
||||
|
||||
import axios from "axios";
|
||||
var Mousetrap = require("mousetrap");
|
||||
|
||||
Mousetrap.prototype.stopCallback = function(e, element) {
|
||||
|
|
@ -305,24 +304,31 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
checkConnection: function() {
|
||||
async checkConnection() {
|
||||
var baseUri = this.$store.getters.baseUri;
|
||||
this.$store.commit("changeWaiting", true);
|
||||
axios
|
||||
// TODO: more robust check - e.g. use a microscope Thing
|
||||
.get(`${baseUri}/stage/`)
|
||||
.then(() => {
|
||||
this.$store.commit("setConnected");
|
||||
this.$store.commit("setErrorMessage", null);
|
||||
})
|
||||
.catch(error => {
|
||||
this.$store.commit("setErrorMessage", error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.$store.commit("changeWaiting", false);
|
||||
});
|
||||
// TODO: more robust check - e.g. use a microscope Thing
|
||||
// TODO: should we purge existing consumedThings?
|
||||
try {
|
||||
await this.$store.dispatch(
|
||||
"wot/fetchThingDescriptions",
|
||||
`${baseUri}/thing_descriptions/`
|
||||
);
|
||||
for (let requiredThing of ["camera", "stage"]) {
|
||||
if (!this.$store.getters["wot/thingAvailable"](requiredThing)) {
|
||||
throw new Error(
|
||||
`No ${requiredThing} found, the GUI won't work without one.`
|
||||
);
|
||||
}
|
||||
}
|
||||
this.$store.commit("setConnected");
|
||||
this.$store.commit("setErrorMessage", null);
|
||||
} catch (error) {
|
||||
this.$store.commit("setErrorMessage", error);
|
||||
} finally {
|
||||
this.$store.commit("changeWaiting", false);
|
||||
}
|
||||
},
|
||||
|
||||
handleExit: function() {
|
||||
this.$root.$emit("globalTogglePreview", false);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -231,10 +231,6 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
},
|
||||
|
||||
pluginsGuiList: function() {
|
||||
// List of plugin GUIs, obtained from this.plugins values
|
||||
var pluginGuis = [];
|
||||
|
|
@ -267,34 +263,26 @@ export default {
|
|||
icon: "visibility",
|
||||
component: viewContent
|
||||
},
|
||||
{
|
||||
/*{
|
||||
id: "gallery",
|
||||
icon: "photo_library",
|
||||
component: galleryContent,
|
||||
divide: true // Add a divider after this tab icon
|
||||
},
|
||||
},*/
|
||||
{
|
||||
id: "navigate",
|
||||
icon: "gamepad",
|
||||
component: navigateContent
|
||||
},
|
||||
{
|
||||
id: "capture",
|
||||
icon: "camera_alt",
|
||||
component: captureContent,
|
||||
divide: true // Add a divider after this tab icon
|
||||
}
|
||||
];
|
||||
if (!this.$store.state.galleryEnabled) {
|
||||
tabs = tabs.filter(tab => tab.id != "gallery");
|
||||
}
|
||||
if (this.$store.state.IHIEnabled) {
|
||||
tabs.push({
|
||||
id: "slidescan",
|
||||
icon: "settings_overscan",
|
||||
component: slideScanContent,
|
||||
divide: true
|
||||
});
|
||||
}
|
||||
];
|
||||
if (!this.$store.state.galleryEnabled) {
|
||||
tabs = tabs.filter(tab => tab.id != "gallery");
|
||||
}
|
||||
if (this.$store.state.imjoyEnabled) {
|
||||
tabs.push({
|
||||
|
|
@ -351,9 +339,9 @@ export default {
|
|||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});*/
|
||||
return new Promise((resolve) => {
|
||||
resolve({});
|
||||
});
|
||||
return new Promise(resolve => {
|
||||
resolve({});
|
||||
});
|
||||
},
|
||||
setTab: function(event, tab) {
|
||||
if (!(this.currentTab == tab)) {
|
||||
|
|
|
|||
|
|
@ -142,7 +142,10 @@ export default {
|
|||
if (task.status == "pending" || task.status == "running") {
|
||||
this.taskStarted = true;
|
||||
this.$emit("taskStarted", this.taskId);
|
||||
this.startPolling(task.id, task.links.find(t => t.rel==self).href);
|
||||
this.startPolling(
|
||||
task.id,
|
||||
task.links.find(t => t.rel == self).href
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -219,33 +222,32 @@ export default {
|
|||
|
||||
var checkCondition = (resolve, reject) => {
|
||||
// If the condition is met, we're done!
|
||||
axios.get(
|
||||
this.taskUrl,
|
||||
{baseURL: this.$store.getters.baseUri}
|
||||
).then(response => {
|
||||
var result = response.data.status;
|
||||
// If the task ends with success
|
||||
if (result == "completed") {
|
||||
resolve(response.data);
|
||||
}
|
||||
// If task ends with an error
|
||||
else if (result == "error") {
|
||||
// Pass the error string back with reject
|
||||
axios
|
||||
.get(this.taskUrl, { baseURL: this.$store.getters.baseUri })
|
||||
.then(response => {
|
||||
var result = response.data.status;
|
||||
// If the task ends with success
|
||||
if (result == "completed") {
|
||||
resolve(response.data);
|
||||
}
|
||||
// If task ends with an error
|
||||
else if (result == "error") {
|
||||
// Pass the error string back with reject
|
||||
|
||||
reject(new Error(response.data.output));
|
||||
}
|
||||
// If task ends with termination
|
||||
else if (result == "cancelled") {
|
||||
// Pass a generic termination error back with reject
|
||||
reject(new Error(response.data.output));
|
||||
}
|
||||
// If task ends with termination
|
||||
else if (result == "cancelled") {
|
||||
// Pass a generic termination error back with reject
|
||||
|
||||
reject(new Error("Task cancelled"));
|
||||
} else {
|
||||
// Since the task is still running, we can update the progress bar
|
||||
this.progress = response.data.progress;
|
||||
// Check again after timeout
|
||||
setTimeout(checkCondition, interval, resolve, reject);
|
||||
}
|
||||
});
|
||||
reject(new Error("Task cancelled"));
|
||||
} else {
|
||||
// Since the task is still running, we can update the progress bar
|
||||
this.progress = response.data.progress;
|
||||
// Check again after timeout
|
||||
setTimeout(checkCondition, interval, resolve, reject);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return new Promise(checkCondition);
|
||||
|
|
|
|||
238
webapp/src/components/labThingsComponents/propertyControl.vue
Normal file
238
webapp/src/components/labThingsComponents/propertyControl.vue
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
<template>
|
||||
<div>
|
||||
<label class="uk-form-label">{{ label }}</label>
|
||||
<div v-if="dataType == 'number'" 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">
|
||||
<i class="material-icons">refresh</i>
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="dataType == 'number_array'" 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">
|
||||
<i class="material-icons">refresh</i>
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="dataType == 'number_object'" 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">
|
||||
<i class="material-icons">refresh</i>
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="dataType == 'other'" class="input-and-buttons-container">
|
||||
<input
|
||||
:value="value"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
type="text"
|
||||
disabled="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "PropertyControl",
|
||||
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
propertyName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
thingName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
readBackDelay: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
|
||||
data: () => {
|
||||
return {
|
||||
value: undefined,
|
||||
valueOnEnter: undefined,
|
||||
focused: false
|
||||
};
|
||||
},
|
||||
|
||||
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[
|
||||
this.propertyName
|
||||
];
|
||||
} 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 == "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";
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
propertyDescription: function() {
|
||||
// Ensure we read the property once the URL is known
|
||||
this.readProperty();
|
||||
}
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
// Read the property when we're mounted - usually this won't
|
||||
// work because the URL isn't set yet. However, it's helpful if
|
||||
// the app is reloaded (e.g. from a dev server).
|
||||
if (this.value == undefined) {
|
||||
this.readProperty();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
readProperty: async function() {
|
||||
let data = await this.readThingProperty(
|
||||
this.thingName,
|
||||
this.propertyName
|
||||
);
|
||||
this.value = data;
|
||||
return data;
|
||||
},
|
||||
writeProperty: async function() {
|
||||
try {
|
||||
let requestedValue = this.value;
|
||||
await this.writeThingProperty(
|
||||
this.thingName,
|
||||
this.propertyName,
|
||||
requestedValue
|
||||
);
|
||||
if (this.readBack) {
|
||||
await new Promise(r => setTimeout(r, this.readBackDelay));
|
||||
let newVal = await this.readProperty();
|
||||
if (newVal == requestedValue) {
|
||||
await this.modalNotify(`Set ${this.label} to ${newVal}.`);
|
||||
} else {
|
||||
await this.modalNotify(
|
||||
`Set ${this.label} to ${newVal} (requested ${requestedValue}).`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await this.modalNotify(`Set ${this.label} to ${this.value}.`);
|
||||
}
|
||||
} catch (error) {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
}
|
||||
},
|
||||
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>
|
||||
|
|
@ -48,6 +48,7 @@
|
|||
|
||||
<cameraCalibrationSettings
|
||||
:show-extra-settings="false"
|
||||
:camera-uri="cameraUri"
|
||||
></cameraCalibrationSettings>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -146,8 +147,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
import cameraCalibrationSettings from "../tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue";
|
||||
import CSMCalibrationSettings from "../tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue";
|
||||
import miniStreamDisplay from "../genericComponents/miniStreamDisplay.vue";
|
||||
|
|
@ -172,62 +171,29 @@ export default {
|
|||
return {
|
||||
ready: false,
|
||||
stepValue: 0,
|
||||
settings: {},
|
||||
config: {}
|
||||
isCSMCalibrated: undefined,
|
||||
isLSTCalibrated: undefined
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
},
|
||||
configUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/configuration`;
|
||||
},
|
||||
availablePluginTitles: function() {
|
||||
return this.availablePlugins.map(a => a.title);
|
||||
},
|
||||
isCSMCalibrated: function() {
|
||||
if (this.settings.extensions["org.openflexure.camera_stage_mapping"]) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
isLSTCalibrated: function() {
|
||||
if (
|
||||
this.settings.camera.picamera &&
|
||||
this.settings.camera.picamera.lens_shading_table
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
canCSMCalibrated: function() {
|
||||
// Assert CSM extension is enabled
|
||||
var extensionEnabled = this.availablePluginTitles.includes(
|
||||
"org.openflexure.camera_stage_mapping"
|
||||
);
|
||||
// Assert real stage is connected
|
||||
var stageConnected = this.config.stage.type !== "MissingStage";
|
||||
// Combine
|
||||
return stageConnected && extensionEnabled;
|
||||
return this.thingAvailable("camera_stage_mapping");
|
||||
},
|
||||
canLSTCalibrated: function() {
|
||||
// Assert LST extension is enabled
|
||||
var extensionEnabled = this.availablePluginTitles.includes(
|
||||
"org.openflexure.calibration.picamera"
|
||||
return (
|
||||
"calibrate_lens_shading" in this.thingDescription("camera").actions
|
||||
);
|
||||
// Assert real camera is connected
|
||||
var cameraConnected = this.config.camera.type !== "MissingCamera";
|
||||
// Combine
|
||||
return cameraConnected && extensionEnabled;
|
||||
},
|
||||
isUseful: function() {
|
||||
var CSMUseful = this.canCSMCalibrated && !this.isCSMCalibrated;
|
||||
var LSTUseful = this.canLSTCalibrated && !this.isLSTCalibrated;
|
||||
return CSMUseful || LSTUseful;
|
||||
},
|
||||
cameraUri: function() {
|
||||
return `${this.$store.getters.baseUri}/camera/`;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -236,25 +202,33 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
show: function() {
|
||||
// Get current settings
|
||||
this.getSettings()
|
||||
.then(() => {
|
||||
return this.getConfig();
|
||||
})
|
||||
.then(() => {
|
||||
// Check if this calibration wizard can actually do anything useful
|
||||
if (this.isUseful) {
|
||||
this.ready = true;
|
||||
this.stepValue = 0;
|
||||
// Show the modal
|
||||
var el = this.$refs["calibrationModalEl"];
|
||||
this.showModalElement(el); // Calls the mixin
|
||||
} else {
|
||||
// If not useful, we just return the onClose event immediately
|
||||
this.onHide();
|
||||
}
|
||||
});
|
||||
show: async function() {
|
||||
// Check if the camera and stage are calibrated, if they can be
|
||||
if (this.canCSMCalibrated) {
|
||||
let csm = await this.readThingProperty(
|
||||
"camera_stage_mapping",
|
||||
"image_to_stage_displacement_matrix",
|
||||
true
|
||||
);
|
||||
this.isCSMCalibrated = Boolean(csm);
|
||||
}
|
||||
if (this.canLSTCalibrated) {
|
||||
this.isLSTCalibrated = await this.readThingProperty(
|
||||
"camera",
|
||||
"lens_shading_is_static"
|
||||
);
|
||||
}
|
||||
// Check if this calibration wizard can actually do anything useful
|
||||
if (this.isUseful) {
|
||||
this.ready = true;
|
||||
this.stepValue = 0;
|
||||
// Show the modal
|
||||
var el = this.$refs["calibrationModalEl"];
|
||||
this.showModalElement(el); // Calls the mixin
|
||||
} else {
|
||||
// If not useful, we just return the onClose event immediately
|
||||
this.onHide();
|
||||
}
|
||||
},
|
||||
|
||||
hide: function() {
|
||||
|
|
@ -268,28 +242,6 @@ export default {
|
|||
this.$emit("onClose");
|
||||
},
|
||||
|
||||
getSettings: function() {
|
||||
return axios
|
||||
.get(this.settingsUri)
|
||||
.then(response => {
|
||||
this.settings = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
getConfig: function() {
|
||||
return axios
|
||||
.get(this.configUri)
|
||||
.then(response => {
|
||||
this.config = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
decrement: function() {
|
||||
if (this.stepValue > 0) {
|
||||
this.stepValue = this.stepValue - 1;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="host-input">
|
||||
<div v-if="configuration">
|
||||
<div v-if="$store.state.available">
|
||||
<div>
|
||||
<div class="uk-margin-small-bottom">
|
||||
<b>API origin:</b>
|
||||
|
|
@ -11,13 +11,9 @@
|
|||
|
||||
<hr />
|
||||
|
||||
<div v-if="settings">
|
||||
<b>Device name:</b> <br />
|
||||
{{ settings.name }}
|
||||
</div>
|
||||
<div>
|
||||
<b>Server version:</b> <br />
|
||||
{{ configuration.application.version }}
|
||||
TODO
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
|
@ -25,18 +21,18 @@
|
|||
<div class="uk-margin-small-bottom">
|
||||
<b>Camera:</b>
|
||||
<br />
|
||||
<div v-if="configuration.camera.type != 'MissingCamera'">
|
||||
{{ configuration.camera.type }}
|
||||
<div v-if="'camera' in things">
|
||||
{{ things.camera.title }}
|
||||
</div>
|
||||
<div v-else class="uk-text-danger"><b>No camera connected</b></div>
|
||||
<div v-else class="uk-text-danger"><b>No camera configured</b></div>
|
||||
</div>
|
||||
<div>
|
||||
<b>Stage:</b>
|
||||
<br />
|
||||
<div v-if="configuration.stage.type != 'MissingStage'">
|
||||
{{ configuration.stage.type }}
|
||||
<div v-if="'stage' in things">
|
||||
{{ things.stage.title }}
|
||||
</div>
|
||||
<div v-else class="uk-text-danger"><b>No stage connected</b></div>
|
||||
<div v-else class="uk-text-danger"><b>No stage configured</b></div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
|
@ -44,9 +40,9 @@
|
|||
<div class="uk-grid-small uk-child-width-1-2" uk-grid>
|
||||
<div>
|
||||
<button
|
||||
v-show="'shutdown' in systemActionLinks"
|
||||
v-show="'shutdown' in things.system_control.actions"
|
||||
class="uk-button uk-button-danger uk-float-right uk-margin uk-margin-remove-top uk-width-1-1"
|
||||
@click="shutdownRequest"
|
||||
@click="systemRequest('shutdown')"
|
||||
>
|
||||
Shutdown
|
||||
</button>
|
||||
|
|
@ -54,9 +50,9 @@
|
|||
|
||||
<div>
|
||||
<button
|
||||
v-show="'reboot' in systemActionLinks"
|
||||
v-show="'reboot' in things.system_control.actions"
|
||||
class="uk-button uk-button-danger uk-float-right uk-margin uk-margin-remove-top uk-width-1-1"
|
||||
@click="rebootRequest"
|
||||
@click="systemRequest('reboot')"
|
||||
>
|
||||
Restart
|
||||
</button>
|
||||
|
|
@ -79,103 +75,22 @@ import axios from "axios";
|
|||
export default {
|
||||
name: "StatusPane",
|
||||
|
||||
components: {},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
configuration: null,
|
||||
settings: null,
|
||||
systemActionLinks: {}
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
},
|
||||
configurationUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/configuration`;
|
||||
},
|
||||
rootUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2`;
|
||||
things: function() {
|
||||
return this.$store.getters["wot/thingDescriptions"];
|
||||
}
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
// Watch for host 'ready', then update configuration
|
||||
this.updateConfiguration();
|
||||
this.updateSettings();
|
||||
this.updateSystemActions();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateConfiguration: function() {
|
||||
axios
|
||||
.get(this.configurationUri)
|
||||
.then(response => {
|
||||
this.configuration = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
updateSettings: function() {
|
||||
axios
|
||||
.get(this.settingsUri)
|
||||
.then(response => {
|
||||
this.settings = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
updateSystemActions: function() {
|
||||
axios
|
||||
.get(this.rootUri)
|
||||
.then(response => {
|
||||
if ("RebootAPI" in response.data.actions) {
|
||||
this.$set(
|
||||
this.systemActionLinks,
|
||||
"reboot",
|
||||
`${this.$store.getters.baseUri}/api/v2/actions/system/reboot/`
|
||||
);
|
||||
} else {
|
||||
delete this.systemActionLinks.reboot;
|
||||
}
|
||||
if ("ShutdownAPI" in response.data.actions) {
|
||||
this.$set(
|
||||
this.systemActionLinks,
|
||||
"shutdown",
|
||||
`${this.$store.getters.baseUri}/api/v2/actions/system/shutdown/`
|
||||
);
|
||||
} else {
|
||||
delete this.systemActionLinks.shutdown;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
shutdownRequest: function() {
|
||||
this.modalConfirm("Shut down microscope?").then(
|
||||
() => {
|
||||
if ("shutdown" in this.systemActionLinks) {
|
||||
this.$store.commit("resetState");
|
||||
// Post and silence errors
|
||||
axios.post(this.systemActionLinks.shutdown).catch(() => {});
|
||||
}
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
},
|
||||
rebootRequest: function() {
|
||||
systemRequest: function(action) {
|
||||
this.modalConfirm("Restart microscope?").then(
|
||||
() => {
|
||||
if ("reboot" in this.systemActionLinks) {
|
||||
this.$store.commit("resetState");
|
||||
// Post and silence errors
|
||||
axios.post(this.systemActionLinks.reboot).catch(() => {});
|
||||
}
|
||||
this.$store.commit("resetState");
|
||||
this.$store.commit("wot/deleteAllThingDescriptions");
|
||||
// Post and silence errors
|
||||
axios
|
||||
.post(this.thingActionUrl("system_control", action))
|
||||
.catch(() => {});
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@
|
|||
class="uk-link"
|
||||
target="_blank"
|
||||
href="https://gitlab.com/openflexure/openflexure-microscope-server/-/issues"
|
||||
>Report an issue</a
|
||||
>
|
||||
Report an issue
|
||||
</a>
|
||||
</div>
|
||||
<div class="uk-padding-small">
|
||||
<h2>Developer tools</h2>
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@
|
|||
|
||||
<ul uk-accordion="multiple: true">
|
||||
<!--Show stack and scan if scan plugin is enabled-->
|
||||
<li v-if="scanUri" :class="{ 'uk-open': scanCapture }">
|
||||
<li v-if="thingAvailable('scan')" :class="{ 'uk-open': scanCapture }">
|
||||
<a class="uk-accordion-title" href="#">Stack and Scan</a>
|
||||
<div class="uk-accordion-content">
|
||||
<div class="uk-margin">
|
||||
|
|
@ -221,7 +221,10 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div class="uk-margin-small uk-margin-remove-bottom" v-if="backgroundDetectUri">
|
||||
<div
|
||||
v-if="thingAvailable('background_detect')"
|
||||
class="uk-margin-small uk-margin-remove-bottom"
|
||||
>
|
||||
<label class="uk-form-label" for="form-stacked-text">
|
||||
<input
|
||||
v-model="detectEmptyFieldsAndSkipAutofocus"
|
||||
|
|
@ -256,7 +259,10 @@
|
|||
</div>
|
||||
</li>
|
||||
<!--Show stack and scan if scan plugin is enabled-->
|
||||
<li v-if="smartScanUri" :class="{ 'uk-open': smartStack && scanCapture }">
|
||||
<li
|
||||
v-if="thingAvailable('smart_scan')"
|
||||
:class="{ 'uk-open': smartStack && scanCapture }"
|
||||
>
|
||||
<a class="uk-accordion-title" href="#">Smart Stack</a>
|
||||
<div
|
||||
class="uk-accordion-content"
|
||||
|
|
@ -401,10 +407,7 @@ export default {
|
|||
data: function() {
|
||||
return {
|
||||
...defaultCaptureSettings(),
|
||||
scanCapture: false, // Don't remember the "scan" tickbox in local storage.
|
||||
scanUri: null,
|
||||
smartScanUri: null,
|
||||
backgroundDetectUri: null
|
||||
scanCapture: false // Don't remember the "scan" tickbox in local storage.
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -415,10 +418,13 @@ export default {
|
|||
};
|
||||
},
|
||||
captureActionUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/actions/camera/capture`;
|
||||
return this.thingActionUrl("camera", "capture");
|
||||
},
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
scanUri: function() {
|
||||
return this.thingActionUrl("scan", "scan");
|
||||
},
|
||||
smartScanUri: function() {
|
||||
return this.thingActionUrl("scan", "scan");
|
||||
},
|
||||
basePayload: function() {
|
||||
var payload = {};
|
||||
|
|
@ -475,7 +481,8 @@ export default {
|
|||
namemode: this.namingStyle.toLowerCase(),
|
||||
autofocus_dz: afDeltas[this.scanDeltaZ],
|
||||
fast_autofocus: this.scanDeltaZ == "Fast",
|
||||
detect_empty_fields_and_skip_autofocus: this.detectEmptyFieldsAndSkipAutofocus
|
||||
detect_empty_fields_and_skip_autofocus: this
|
||||
.detectEmptyFieldsAndSkipAutofocus
|
||||
};
|
||||
},
|
||||
smartScanPayload: function() {
|
||||
|
|
@ -493,7 +500,6 @@ export default {
|
|||
},
|
||||
|
||||
mounted() {
|
||||
this.updateScanUri();
|
||||
// Load settings if they have been saved, and set up watchers to sync with local storage
|
||||
syncDataWithLocalStorage("captureSettings", this, defaultCaptureSettings());
|
||||
|
||||
|
|
@ -521,61 +527,6 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
updateScanUri: function() {
|
||||
// Update URIs for the various functions in plugins.
|
||||
// This will only work if the plugins are present, so
|
||||
// checking if the URIs is null will determine which
|
||||
// extensions are available.
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.scan"
|
||||
);
|
||||
// if ScanPlugin is enabled
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.scanUri = foundExtension.links.tile.href;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.smart-stack"
|
||||
);
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.smartScanUri = foundExtension.links.tile.href;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.background-detect"
|
||||
);
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.backgroundDetectUri = foundExtension.links.grab_and_classify_image.href;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
onScanResponse: function() {
|
||||
this.modalNotify("Finished scan.");
|
||||
// Emit signal to update capture list
|
||||
|
|
|
|||
|
|
@ -56,12 +56,6 @@ export default {
|
|||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
this.updateZipperUri();
|
||||
},
|
||||
|
|
@ -84,7 +78,7 @@ export default {
|
|||
methods: {
|
||||
updateZipperUri: function() {
|
||||
if (this.$store.state.available) {
|
||||
axios
|
||||
/*axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
|
|
@ -100,7 +94,7 @@ export default {
|
|||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
});*/
|
||||
} else {
|
||||
this.zipBuilderUri = null;
|
||||
this.zipGetterUri = null;
|
||||
|
|
|
|||
|
|
@ -42,15 +42,15 @@
|
|||
<div class="uk-width-xlarge uk-align-center">
|
||||
<div
|
||||
v-for="item in pagedItems"
|
||||
:key="item.timestamp"
|
||||
:key="item.sequence"
|
||||
uk-alert
|
||||
class="logging-entry"
|
||||
:class="{
|
||||
'uk-alert-warning uk-alert': item.data.levelname == 'WARNING',
|
||||
'uk-alert-danger uk-alert': item.data.levelname == 'ERROR'
|
||||
'uk-alert-warning uk-alert': item.level == 'WARNING',
|
||||
'uk-alert-danger uk-alert': item.level == 'ERROR'
|
||||
}"
|
||||
>
|
||||
<b>{{ formatDateTime(item.data.created) }}</b>
|
||||
<b>{{ formatDateTime(item.timestamp) }}</b>
|
||||
<div class="logging-message">{{ formatMessage(item) }}</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -102,18 +102,15 @@ export default {
|
|||
var items = [];
|
||||
for (var item of this.logs) {
|
||||
// Add to capture list if matched
|
||||
if (this.filteredLevels.includes(item.data.levelname)) {
|
||||
if (this.filteredLevels.includes(item.level)) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
loggingUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/events/logging`;
|
||||
},
|
||||
logFileURI: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/log`;
|
||||
return `${this.$store.getters.baseUri}/log/`;
|
||||
},
|
||||
pagedItems: function() {
|
||||
let startIndex = (this.page - 1) * this.maxitems;
|
||||
|
|
@ -124,11 +121,6 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
// Update on mount (does nothing if not connected)
|
||||
this.updateLogs();
|
||||
},
|
||||
|
||||
methods: {
|
||||
scrollToTop() {
|
||||
const el = document.querySelector("#container-left");
|
||||
|
|
@ -141,22 +133,30 @@ export default {
|
|||
this.updateLogs();
|
||||
}
|
||||
},
|
||||
updateLogs: function() {
|
||||
axios
|
||||
.get(this.loggingUri)
|
||||
.then(response => {
|
||||
this.logs = response.data.reverse();
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
async updateLogs() {
|
||||
let response = await axios.get(this.logFileURI);
|
||||
let lines = response.data.split("\n").reverse();
|
||||
this.logs = [];
|
||||
let regexp = /\[(.+)\] \[(.+)\] (.*)$/;
|
||||
for (let line of lines) {
|
||||
if (line.length > 0) {
|
||||
let m = line.match(regexp);
|
||||
this.logs.push({
|
||||
timestamp: m[1],
|
||||
level: m[2],
|
||||
message: m[3],
|
||||
sequence: this.logs.length
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
formatDateTime: function(isoDateTimeString) {
|
||||
isoDateTimeString = isoDateTimeString.replace(",", ".");
|
||||
let date = new Date(isoDateTimeString);
|
||||
return date.toLocaleDateString() + " " + date.toLocaleTimeString();
|
||||
},
|
||||
formatMessage: function(item) {
|
||||
return item.data.levelname + ": " + item.data.message;
|
||||
return item.level + ": " + item.message;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -68,66 +68,46 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="uk-button uk-button-default uk-margin uk-width-1-1"
|
||||
@click="zeroRequest()"
|
||||
>
|
||||
Zero coordinates
|
||||
</button>
|
||||
<taskSubmitter
|
||||
:submit-url="zeroActionUri"
|
||||
:submit-label="'Zero Coordinates'"
|
||||
:canTerminate="false"
|
||||
@finished="updatePosition"
|
||||
@error="modalError"
|
||||
></taskSubmitter>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="uk-open">
|
||||
<a class="uk-accordion-title" href="#">Move-to</a>
|
||||
<a class="uk-accordion-title" href="#">Position</a>
|
||||
<div class="uk-accordion-content">
|
||||
<form @submit.prevent="handleSubmit">
|
||||
<form>
|
||||
<!-- Text boxes to set and view position -->
|
||||
|
||||
<div class="uk-grid-small uk-child-width-1-3" uk-grid>
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">x</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="setPosition.x"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
name="inputPositionX"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">y</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="setPosition.y"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
name="inputPositionY"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">z</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="setPosition.z"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
name="inputPositionZx"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-and-buttons-container">
|
||||
<input
|
||||
v-for="(_v, key) in setPosition"
|
||||
:key="`setPosition_${key}`"
|
||||
v-model="setPosition[key]"
|
||||
class="uk-form-small numeric-setting-line-input"
|
||||
type="number"
|
||||
@keyup.enter="startMoveTask"
|
||||
/>
|
||||
<a class="button-next-to-input" @click="updatePosition">
|
||||
<i class="material-icons">refresh</i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<button
|
||||
type="submit"
|
||||
class="uk-button uk-button-default uk-float-right uk-width-1-1"
|
||||
>
|
||||
Move
|
||||
</button>
|
||||
<taskSubmitter
|
||||
ref="moveTaskSubmitter"
|
||||
:submit-url="absoluteMoveUri"
|
||||
:submit-data="setPosition"
|
||||
:submit-label="'Move'"
|
||||
:canTerminate="false"
|
||||
:pollInterval="0.05"
|
||||
@taskStarted="moveLock = true"
|
||||
@finished="moveLock = false"
|
||||
@error="modalError"
|
||||
></taskSubmitter>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -143,7 +123,7 @@
|
|||
v-if="fastAutofocusUri"
|
||||
:submit-url="fastAutofocusUri"
|
||||
:submit-data="{ dz: 2000 }"
|
||||
:submit-label="'Fast'"
|
||||
:submit-label="'Autofocus'"
|
||||
:button-primary="false"
|
||||
:submit-on-event="'globalFastAutofocusEvent'"
|
||||
@taskStarted="isAutofocusing = 1"
|
||||
|
|
@ -151,32 +131,34 @@
|
|||
@error="modalError"
|
||||
></taskSubmitter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li v-show="captureUri" class="uk-open">
|
||||
<a class="uk-accordion-title" href="#">Image Capture</a>
|
||||
<div class="uk-accordion-content">
|
||||
<div class="uk-grid-small uk-child-width-expand" uk-grid>
|
||||
<taskSubmitter
|
||||
v-if="captureUri"
|
||||
:submit-url="captureUri"
|
||||
:submit-data="{ resolution: 'main' }"
|
||||
:submit-label="'Low Resolution'"
|
||||
:submit-on-event="'globalCaptureEvent'"
|
||||
@response="handleCaptureResponse"
|
||||
@error="modalError"
|
||||
></taskSubmitter>
|
||||
</div>
|
||||
|
||||
<div v-show="!isAutofocusing || isAutofocusing == 2">
|
||||
<taskSubmitter
|
||||
v-if="normalAutofocusUri"
|
||||
:submit-url="normalAutofocusUri"
|
||||
:submit-data="{ dz: [-90, -60, -30, 0, 30, 60, 90] }"
|
||||
:submit-label="'Medium'"
|
||||
:button-primary="false"
|
||||
@taskStarted="isAutofocusing = 2"
|
||||
@finished="isAutofocusing = 0"
|
||||
@error="modalError"
|
||||
></taskSubmitter>
|
||||
</div>
|
||||
|
||||
<div v-show="!isAutofocusing || isAutofocusing == 3">
|
||||
<taskSubmitter
|
||||
v-if="normalAutofocusUri"
|
||||
:submit-url="normalAutofocusUri"
|
||||
:submit-data="{ dz: [-30, -20, -10, 0, 10, 20, 30] }"
|
||||
:submit-label="'Fine'"
|
||||
:button-primary="false"
|
||||
@taskStarted="isAutofocusing = 3"
|
||||
@finished="isAutofocusing = 0"
|
||||
@error="modalError"
|
||||
></taskSubmitter>
|
||||
</div>
|
||||
<div class="uk-grid-small uk-child-width-expand" uk-grid>
|
||||
<taskSubmitter
|
||||
v-if="captureUri"
|
||||
:submit-url="captureUri"
|
||||
:submit-data="{ resolution: 'full' }"
|
||||
submit-label="Full Resolution"
|
||||
submit-on-event="globalCaptureEvent"
|
||||
@response="handleCaptureResponse"
|
||||
@error="modalError"
|
||||
></taskSubmitter>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
|
@ -220,10 +202,7 @@ export default {
|
|||
},
|
||||
setPosition: null,
|
||||
isAutofocusing: 0,
|
||||
moveLock: false,
|
||||
fastAutofocusUri: null,
|
||||
normalAutofocusUri: null,
|
||||
moveInImageCoordinatesUri: null
|
||||
moveLock: false
|
||||
};
|
||||
},
|
||||
|
||||
|
|
@ -231,17 +210,26 @@ export default {
|
|||
baseUri: function() {
|
||||
return this.$store.getters.baseUri;
|
||||
},
|
||||
moveActionUri: function() {
|
||||
return `${this.$store.getters.baseUri}/stage/move_relative`;
|
||||
absoluteMoveUri: function() {
|
||||
return this.thingActionUrl("stage", "move_absolute");
|
||||
},
|
||||
zeroActionUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/actions/stage/zero`;
|
||||
return this.thingActionUrl("stage", "set_zero_position");
|
||||
},
|
||||
positionStatusUri: function() {
|
||||
return `${this.baseUri}/stage/position`;
|
||||
},
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
fastAutofocusUri: function() {
|
||||
return this.thingActionUrl("autofocus", "fast_autofocus");
|
||||
},
|
||||
captureUri: function() {
|
||||
return this.thingActionUrl("camera", "capture_jpeg");
|
||||
},
|
||||
moveInImageCoordinatesUri: function() {
|
||||
return this.thingActionUrl(
|
||||
"camera_stage_mapping",
|
||||
"move_in_image_coordinates"
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -265,17 +253,17 @@ export default {
|
|||
this.stepSize =
|
||||
this.getLocalStorageObj("navigation_stepSize") || this.stepSize;
|
||||
this.invert = this.getLocalStorageObj("navigation_invert") || this.invert;
|
||||
let self = this;
|
||||
// A global signal listener to perform a move action
|
||||
this.$root.$on("globalMoveEvent", (x, y, z, absolute) => {
|
||||
this.moveRequest(x, y, z, absolute);
|
||||
});
|
||||
this.$root.$on("globalMoveEvent", self.move);
|
||||
// A global signal listener to perform a move action in pixels
|
||||
this.$root.$on("globalMoveInImageCoordinatesEvent", (x, y, absolute) => {
|
||||
this.moveInImageCoordinatesRequest(x, y, absolute);
|
||||
});
|
||||
// A global signal listener to perform a move in multiples of a step size
|
||||
this.$root.$on("globalMoveStepEvent", (x_steps, y_steps, z_steps) => {
|
||||
this.moveRequest(
|
||||
this.$root.$emit(
|
||||
"globalMoveEvent",
|
||||
x_steps * this.stepSize.x * (this.invert.x ? -1 : 1),
|
||||
y_steps * this.stepSize.y * (this.invert.y ? -1 : 1),
|
||||
z_steps * this.stepSize.z * (this.invert.z ? -1 : 1),
|
||||
|
|
@ -284,9 +272,6 @@ export default {
|
|||
});
|
||||
// Update the current position in text boxes
|
||||
this.updatePosition();
|
||||
// Look for autofocus plugin
|
||||
this.updateAutofocusUri();
|
||||
this.updateMoveInImageCoordinatesUri();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
|
|
@ -297,40 +282,32 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
handleSubmit: function() {
|
||||
this.moveRequest(
|
||||
this.setPosition.x,
|
||||
this.setPosition.y,
|
||||
this.setPosition.z,
|
||||
true
|
||||
);
|
||||
timeout(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
},
|
||||
|
||||
moveRequest: function(x, y, z, absolute) {
|
||||
// If not movement-locked
|
||||
if (!this.moveLock) {
|
||||
// Lock move requests
|
||||
this.moveLock = true;
|
||||
let move_type = absolute ? "absolute" : "relative";
|
||||
// Send move request
|
||||
axios
|
||||
.post(`${this.baseUri}/stage/move_${move_type}`, {
|
||||
x: x,
|
||||
y: y,
|
||||
z: z
|
||||
})
|
||||
.then(() => {
|
||||
this.updatePosition(); // Update the position in text boxes
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
})
|
||||
.then(() => {
|
||||
this.moveLock = false; // Release the move lock
|
||||
});
|
||||
async move(x, y, z, absolute) {
|
||||
// Move the stage, by updating the controls and starting a move task
|
||||
// This is equivalent to clicking the "move" button.
|
||||
if (this.moveLock) return; // Discard move requests if we're already moving
|
||||
// NB moveLock is just boolean flag - it's not as safe as a "proper" lock.
|
||||
this.moveLock = true; // This will also be set by the task submitter, but
|
||||
// setting it here avoids multiple moves being requested simultaneously.
|
||||
if (absolute){
|
||||
this.setPosition = {"x": x, "y": y, "z": z}
|
||||
}else{
|
||||
await this.updatePosition();
|
||||
this.setPosition = {
|
||||
"x": this.setPosition.x + x,
|
||||
"y": this.setPosition.y + y,
|
||||
"z": this.setPosition.z + z
|
||||
}
|
||||
}
|
||||
await this.timeout(1); // Wait for Vue to update the position
|
||||
await this.startMoveTask();
|
||||
},
|
||||
async startMoveTask() {
|
||||
await this.$refs.moveTaskSubmitter.startTask();
|
||||
},
|
||||
|
||||
moveInImageCoordinatesRequest: function(x, y) {
|
||||
// If not movement-locked
|
||||
if (!this.moveLock) {
|
||||
|
|
@ -373,24 +350,58 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
updatePosition: function() {
|
||||
axios
|
||||
.get(this.positionStatusUri)
|
||||
.then(response => {
|
||||
this.setPosition = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
async updatePosition() {
|
||||
this.setPosition = await this.readThingProperty("stage", "position")
|
||||
},
|
||||
|
||||
updateAutofocusUri: function() {
|
||||
this.fastAutofocusUri = `${this.baseUri}/autofocus/fast_autofocus`;
|
||||
},
|
||||
|
||||
updateMoveInImageCoordinatesUri: function() {
|
||||
this.moveInImageCoordinatesUri = `${this.baseUri}/camera_stage_mapping/move_in_image_coordinates`;
|
||||
handleCaptureResponse: async function(response) {
|
||||
// Retrieve the captured image and save it
|
||||
let imageUri = response.output.href;
|
||||
if (!imageUri) {
|
||||
this.modalError("No image URI returned from capture task.");
|
||||
console.log(`Capture resulted in response ${response}`);
|
||||
return;
|
||||
}
|
||||
// To save the returned data, we make a virtual link and click it
|
||||
let imageResponse = await axios.get(imageUri, { responseType: "blob" });
|
||||
const url = window.URL.createObjectURL(new Blob([imageResponse.data]));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", `OFM_${new Date().toISOString()}.jpeg`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
}
|
||||
}
|
||||
};
|
||||
</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: 3em;
|
||||
}
|
||||
/* Chrome, Safari, Edge, Opera */
|
||||
.numeric-setting-line-input::-webkit-outer-spin-button,
|
||||
.numeric-setting-line-input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
.button-next-to-input {
|
||||
flex-grow: 0;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
vertical-align: middle;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import CSMCalibrationSettings from "./CSMSettingsComponents/CSMCalibrationSettings.vue";
|
||||
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
|
||||
|
||||
|
|
@ -31,122 +30,6 @@ export default {
|
|||
components: {
|
||||
CSMCalibrationSettings,
|
||||
miniStreamDisplay
|
||||
},
|
||||
|
||||
props: {
|
||||
showExtraSettings: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
settings: null,
|
||||
recalibrationLinks: {},
|
||||
isCalibrating: false,
|
||||
dataAvailable: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
},
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateSettings();
|
||||
this.updateRecalibrationLinks();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateSettings: function() {
|
||||
// Update links
|
||||
axios
|
||||
.get(this.settingsUri)
|
||||
.then(response => {
|
||||
this.settings =
|
||||
response.data.extensions["org.openflexure.camera_stage_mapping"];
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
this.settings = {};
|
||||
});
|
||||
},
|
||||
|
||||
updateCalibrationDataAvailability: function() {
|
||||
if ("get_calibration" in this.recalibrationLinks) {
|
||||
axios
|
||||
.get(this.recalibrationLinks.get_calibration.href)
|
||||
.then(response => {
|
||||
if (Object.keys(response.data).length === 0) {
|
||||
this.dataAvailable = false;
|
||||
} else {
|
||||
this.dataAvailable = true;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
getCalibrationData: function() {
|
||||
axios
|
||||
.get(this.recalibrationLinks.get_calibration.href)
|
||||
.then(response => {
|
||||
if (response.data != {}) {
|
||||
const data = JSON.stringify(response.data);
|
||||
const url = window.URL.createObjectURL(new Blob([data]));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", "csm_calibration.json");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
updateRecalibrationLinks: function() {
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.camera_stage_mapping"
|
||||
);
|
||||
// if camera-stage mapping extension is enabled
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.recalibrationLinks = foundExtension.links;
|
||||
// Update whether calibration data is available
|
||||
this.updateCalibrationDataAvailability();
|
||||
} else {
|
||||
this.recalibrationLinks = {};
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
onRecalibrateResponse: function() {
|
||||
this.modalNotify("Finished stage-to-camera calibration.");
|
||||
// Update local settings
|
||||
this.updateSettings();
|
||||
},
|
||||
|
||||
onRecalibrateError: function(error) {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div id="CSMCalibrationSettings">
|
||||
<!--Show auto calibrate if default plugin is enabled-->
|
||||
<div v-if="'calibrate_xy' in recalibrationLinks" class="uk-margin-small">
|
||||
<div v-if="'calibrate_xy' in actions" class="uk-margin-small">
|
||||
<taskSubmitter
|
||||
:button-primary="true"
|
||||
:can-terminate="false"
|
||||
|
|
@ -9,16 +9,15 @@
|
|||
:confirmation-message="
|
||||
'Start recalibration of the stage to the camera? This may take a while, and the microscope will be locked during this time.'
|
||||
"
|
||||
:submit-url="recalibrationLinks.calibrate_xy.href"
|
||||
:submit-url="calibrateXYUri"
|
||||
:submit-label="'Auto-Calibrate using camera'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
v-if="'get_calibration' in recalibrationLinks"
|
||||
v-show="dataAvailable && showExtraSettings"
|
||||
v-if="'last_calibration' in properties"
|
||||
v-show="showExtraSettings"
|
||||
type="button"
|
||||
class="uk-button uk-button-default uk-width-1-1"
|
||||
@click="getCalibrationData()"
|
||||
|
|
@ -29,7 +28,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import taskSubmitter from "../../../genericComponents/taskSubmitter";
|
||||
|
||||
// Export main app
|
||||
|
|
@ -48,107 +46,44 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
settings: null,
|
||||
recalibrationLinks: {},
|
||||
isCalibrating: false,
|
||||
dataAvailable: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
actions() {
|
||||
return this.$store.getters["wot/thingDescription"]("camera_stage_mapping")
|
||||
.actions;
|
||||
},
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
properties() {
|
||||
return this.$store.getters["wot/thingDescription"]("camera_stage_mapping")
|
||||
.properties;
|
||||
},
|
||||
calibrateXYUri() {
|
||||
return this.thingActionUrl("camera_stage_mapping", "calibrate_xy");
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateSettings();
|
||||
this.updateRecalibrationLinks();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateSettings: function() {
|
||||
// Update links
|
||||
axios
|
||||
.get(this.settingsUri)
|
||||
.then(response => {
|
||||
this.settings =
|
||||
response.data.extensions["org.openflexure.camera_stage_mapping"];
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
this.settings = {};
|
||||
});
|
||||
},
|
||||
|
||||
updateCalibrationDataAvailability: function() {
|
||||
if ("get_calibration" in this.recalibrationLinks) {
|
||||
axios
|
||||
.get(this.recalibrationLinks.get_calibration.href)
|
||||
.then(response => {
|
||||
if (Object.keys(response.data).length === 0) {
|
||||
this.dataAvailable = false;
|
||||
} else {
|
||||
this.dataAvailable = true;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
getCalibrationData: async function() {
|
||||
try {
|
||||
let data = await this.readThingProperty(
|
||||
"camera_stage_mapping",
|
||||
"last_calibration"
|
||||
);
|
||||
if (data == {}) {
|
||||
throw "No calibration data available.";
|
||||
}
|
||||
const dataStr = JSON.stringify(data);
|
||||
const url = window.URL.createObjectURL(new Blob([dataStr]));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", "csm_calibration.json");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
} catch (error) {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
}
|
||||
},
|
||||
|
||||
getCalibrationData: function() {
|
||||
axios
|
||||
.get(this.recalibrationLinks.get_calibration.href)
|
||||
.then(response => {
|
||||
if (response.data != {}) {
|
||||
const data = JSON.stringify(response.data);
|
||||
const url = window.URL.createObjectURL(new Blob([data]));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", "csm_calibration.json");
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
updateRecalibrationLinks: function() {
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.camera_stage_mapping"
|
||||
);
|
||||
// if camera-stage mapping extension is enabled
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.recalibrationLinks = foundExtension.links;
|
||||
// Update whether calibration data is available
|
||||
this.updateCalibrationDataAvailability();
|
||||
} else {
|
||||
this.recalibrationLinks = {};
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
onRecalibrateResponse: function() {
|
||||
this.modalNotify("Finished stage-to-camera calibration.");
|
||||
// Update local settings
|
||||
this.updateSettings();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
<div id="cameraSettings">
|
||||
<div class="uk-grid uk-grid-divider uk-child-width-expand" uk-grid>
|
||||
<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">
|
||||
|
|
@ -9,139 +12,47 @@
|
|||
<li class="uk-open">
|
||||
<a class="uk-accordion-title" href="#">Pi Camera Settings</a>
|
||||
<div class="uk-accordion-content">
|
||||
<div v-if="picamera.shutter_speed !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Exposure time</label
|
||||
>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="picamera.shutter_speed"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="picamera.analog_gain !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Analogue gain</label
|
||||
>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="picamera.analog_gain"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.0000000000000001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="picamera.digital_gain !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Digital gain</label
|
||||
>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="picamera.digital_gain"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.0000000000000001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="picamera.awb_gains !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text">
|
||||
White Balance gains
|
||||
</label>
|
||||
<div class="uk-form-controls">
|
||||
<label class="uk-form-label">R:</label>
|
||||
<input
|
||||
v-model="picamera.awb_gains[0]"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.0000000000000001"
|
||||
/>
|
||||
<label class="uk-form-label">B:</label>
|
||||
<input
|
||||
v-model="picamera.awb_gains[1]"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.0000000000000001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<PropertyControl
|
||||
label="Exposure time"
|
||||
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>
|
||||
|
||||
<li class="uk-open">
|
||||
<a class="uk-accordion-title" href="#">Image Quality</a>
|
||||
<div class="uk-accordion-content">
|
||||
<div class="uk-child-width-1-2" uk-grid>
|
||||
<div v-if="jpeg_quality !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>JPEG capture quality (%)</label
|
||||
>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="jpeg_quality"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="stream_resolution !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Stream resolution</label
|
||||
>
|
||||
<select
|
||||
v-model="stream_resolution"
|
||||
class="uk-select uk-form-small"
|
||||
>
|
||||
<option
|
||||
v-for="option in resolutionOptions"
|
||||
:key="option.value[0]"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.text }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<PropertyControl
|
||||
label="MJPEG stream bit rate"
|
||||
property-name="mjpeg_bitrate"
|
||||
thing-name="camera"
|
||||
:read-back-delay="100"
|
||||
/>
|
||||
<PropertyControl
|
||||
label="MJPEG stream resolution"
|
||||
property-name="stream_resolution"
|
||||
thing-name="camera"
|
||||
:read-back-delay="100"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<!--
|
||||
<li>
|
||||
<a class="uk-accordion-title" href="#">Advanced</a>
|
||||
<div class="uk-accordion-content">
|
||||
<div v-if="mjpeg_bitrate !== undefined">
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Camera bitrate</label
|
||||
>
|
||||
<select
|
||||
v-model="mjpeg_bitrate"
|
||||
class="uk-select uk-form-small"
|
||||
>
|
||||
<option
|
||||
v-for="option in bitrateOptions"
|
||||
:key="option.value"
|
||||
:value="option.value"
|
||||
>
|
||||
{{ option.text }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="uk-margin-remove">
|
||||
This option sets the target bitrate for camera
|
||||
recordings.<br />
|
||||
<b
|
||||
>Limiting bitrate may impact fast-autofocus
|
||||
reliability.</b
|
||||
>
|
||||
However is guaranteed to fix the live stream disappearing
|
||||
for some highly detailed samples.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="picamera.framerate !== undefined"
|
||||
|
|
@ -166,27 +77,14 @@
|
|||
This option sets the framerate of the Pi Camera video
|
||||
encoder. The actual stream framerate is determined by
|
||||
network speed, and is limited by the encoder framerate.<br />
|
||||
<b
|
||||
>Lowering framerate may impact fast-autofocus
|
||||
accuracy.</b
|
||||
>
|
||||
<b>Lowering framerate may impact fast-autofocus accuracy.</b>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</li>-->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="uk-button uk-button-primary uk-margin-small uk-width-1-1"
|
||||
>
|
||||
Apply Settings
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<h3>Automatic calibration</h3>
|
||||
<cameraCalibrationSettings></cameraCalibrationSettings>
|
||||
</div>
|
||||
|
||||
<div id="mini-stream">
|
||||
|
|
@ -197,9 +95,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue";
|
||||
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
|
||||
import PropertyControl from "../../labThingsComponents/propertyControl.vue";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
|
|
@ -207,7 +105,8 @@ export default {
|
|||
|
||||
components: {
|
||||
cameraCalibrationSettings,
|
||||
miniStreamDisplay
|
||||
miniStreamDisplay,
|
||||
PropertyControl
|
||||
},
|
||||
|
||||
data: function() {
|
||||
|
|
@ -242,74 +141,8 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateSettings();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateSettings: function() {
|
||||
axios
|
||||
.get(this.settingsUri)
|
||||
.then(response => {
|
||||
const cameraSettings = response.data.camera;
|
||||
// Get base camera settings
|
||||
this.mjpeg_bitrate = cameraSettings.mjpeg_bitrate;
|
||||
this.jpeg_quality = cameraSettings.jpeg_quality;
|
||||
this.stream_resolution = cameraSettings.stream_resolution;
|
||||
// Get Pi Camera settings if they exist
|
||||
if (cameraSettings.picamera) {
|
||||
this.picamera.analog_gain = cameraSettings.picamera.analog_gain;
|
||||
this.picamera.digital_gain = cameraSettings.picamera.digital_gain;
|
||||
this.picamera.shutter_speed = cameraSettings.picamera.shutter_speed;
|
||||
this.picamera.framerate = cameraSettings.picamera.framerate;
|
||||
this.picamera.awb_gains = cameraSettings.picamera.awb_gains;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
applySettingsRequest: function() {
|
||||
// We have to use parseInt/parseFloat because JS sometimes seems to
|
||||
// make the numbers be strings... TypeScript would solve this...
|
||||
var payload = {
|
||||
camera: {
|
||||
mjpeg_bitrate: parseInt(this.mjpeg_bitrate),
|
||||
jpeg_quality: parseInt(this.jpeg_quality),
|
||||
stream_resolution: this.stream_resolution,
|
||||
picamera: {
|
||||
shutter_speed: parseFloat(this.picamera.shutter_speed),
|
||||
analog_gain: parseFloat(this.picamera.analog_gain),
|
||||
digital_gain: parseFloat(this.picamera.digital_gain),
|
||||
framerate: parseInt(this.picamera.framerate),
|
||||
awb_gains: [
|
||||
parseFloat(this.picamera.awb_gains[0]),
|
||||
parseFloat(this.picamera.awb_gains[1])
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
console.log(payload);
|
||||
// Send request
|
||||
axios
|
||||
.put(this.settingsUri, payload)
|
||||
.then(() => {
|
||||
return new Promise(r => setTimeout(r, 500));
|
||||
}) // why is there no built-in for this??!
|
||||
.then(() => {
|
||||
// Update local settings
|
||||
this.updateSettings();
|
||||
this.modalNotify("Camera settings applied.");
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
cameraUri: function() {
|
||||
return `${this.$store.getters.baseUri}/camera/`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,52 +1,43 @@
|
|||
<template>
|
||||
<div>
|
||||
<!--Show auto calibrate if default plugin is enabled-->
|
||||
<div v-if="'recalibrate' in recalibrationLinks" class="uk-margin-small">
|
||||
<div v-if="'full_auto_calibrate' in actions" class="uk-margin-small">
|
||||
<taskSubmitter
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="true"
|
||||
:confirmation-message="
|
||||
'Start recalibration? This may take a while, and the microscope will be locked during this time.'
|
||||
"
|
||||
:submit-url="recalibrationLinks.recalibrate.href"
|
||||
:submit-url="cameraUri + 'full_auto_calibrate'"
|
||||
:submit-label="'Full Auto-Calibrate'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
<div
|
||||
v-if="'auto_exposure_from_raw' in recalibrationLinks"
|
||||
class="uk-margin-small"
|
||||
>
|
||||
<div v-if="'auto_expose_from_minimum' in actions" class="uk-margin-small">
|
||||
<taskSubmitter
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="false"
|
||||
:submit-url="recalibrationLinks.auto_exposure_from_raw.href"
|
||||
:submit-url="cameraUri + 'auto_expose_from_minimum'"
|
||||
:submit-label="'Auto gain & shutter speed'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
<div
|
||||
v-if="'auto_white_balance_from_raw' in recalibrationLinks"
|
||||
class="uk-margin-small"
|
||||
>
|
||||
<div v-if="'calibrate_white_balance' in actions" class="uk-margin-small">
|
||||
<taskSubmitter
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="false"
|
||||
:submit-url="recalibrationLinks.auto_white_balance_from_raw.href"
|
||||
:submit-url="cameraUri + 'calibrate_white_balance'"
|
||||
:submit-label="'Auto white balance'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
<div
|
||||
v-if="'auto_lens_shading_table' in recalibrationLinks"
|
||||
class="uk-margin-small"
|
||||
>
|
||||
<div v-if="'calibrate_lens_shading' in actions" class="uk-margin-small">
|
||||
<taskSubmitter
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="true"
|
||||
|
|
@ -54,7 +45,7 @@
|
|||
'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.'
|
||||
"
|
||||
:submit-url="recalibrationLinks.auto_lens_shading_table.href"
|
||||
:submit-url="cameraUri + 'calibrate_lens_shading'"
|
||||
:submit-label="'Auto flat field correction'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
|
|
@ -62,23 +53,35 @@
|
|||
</taskSubmitter>
|
||||
</div>
|
||||
|
||||
<div v-show="showExtraSettings" class="uk-child-width-expand">
|
||||
<button
|
||||
v-if="'flatten_lens_shading_table' in recalibrationLinks"
|
||||
class="uk-button uk-button-danger uk-width-1-1"
|
||||
@click="flattenLensShadingTableRequest"
|
||||
<div
|
||||
v-show="showExtraSettings"
|
||||
v-if="'flatten_lens_shading_table' in actions"
|
||||
class="uk-child-width-expand"
|
||||
>
|
||||
<taskSubmitter
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="false"
|
||||
:submit-url="cameraUri + 'flat_lens_shading'"
|
||||
:submit-label="'Disable flat field correction'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
Disable flat field correction
|
||||
</button>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
|
||||
<div v-if="LstDownloadEnabled">
|
||||
<a
|
||||
class="uk-button uk-button-default uk-width-large uk-margin-small-top uk-align-center"
|
||||
:href="LstDownloadUri"
|
||||
download
|
||||
>Download Lens-Shading Table</a
|
||||
<div
|
||||
v-show="showExtraSettings"
|
||||
v-if="'reset_lens_shading' in actions"
|
||||
class="uk-child-width-expand"
|
||||
>
|
||||
<taskSubmitter
|
||||
:can-terminate="false"
|
||||
:requires-confirmation="false"
|
||||
:submit-url="cameraUri + 'reset_lens_shading'"
|
||||
:submit-label="'Reset flat field correction'"
|
||||
@response="onRecalibrateResponse"
|
||||
@error="modalError"
|
||||
>
|
||||
</taskSubmitter>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -100,66 +103,34 @@ export default {
|
|||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
},
|
||||
cameraUri: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
recalibrationLinks: {},
|
||||
actions: {},
|
||||
isCalibrating: false,
|
||||
LstDownloadEnabled: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
},
|
||||
LstDownloadUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/camera/lst`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateRecalibrationLinks();
|
||||
this.checkLstDownload();
|
||||
this.updateActions();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateRecalibrationLinks: function() {
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.calibration.picamera"
|
||||
);
|
||||
// if AutocalibrationPlugin is enabled
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.recalibrationLinks = foundExtension.links;
|
||||
} else {
|
||||
this.recalibrationLinks = {};
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
checkLstDownload: function() {
|
||||
axios
|
||||
.get(this.LstDownloadUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
if (response.status === 200) {
|
||||
this.LstDownloadEnabled = true;
|
||||
} else {
|
||||
this.LstDownloadEnabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
updateActions: async function() {
|
||||
try {
|
||||
let response = await axios.get(this.cameraUri); // Get the thing description
|
||||
let td = response.data;
|
||||
this.actions = td.actions;
|
||||
} catch (error) {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
}
|
||||
},
|
||||
|
||||
onRecalibrateResponse: function() {
|
||||
|
|
|
|||
|
|
@ -1,19 +1,6 @@
|
|||
<template>
|
||||
<div id="appSettings" class="uk-width-large">
|
||||
<h3>Additional features</h3>
|
||||
<p class="uk-margin-small">
|
||||
<label
|
||||
><input v-model="IHIEnabled" class="uk-checkbox" type="checkbox" />
|
||||
Enable IHI Slide Scan</label
|
||||
>
|
||||
</p>
|
||||
<p class="uk-margin-small">
|
||||
Enabling IHI Slide Scan will add a new tab designed to simplify acquiring
|
||||
blood smear tile scans. <br />
|
||||
While this may be useful for other applications, it is primarily designed
|
||||
for use in clinics acquiring blood smear samples using a 100x
|
||||
oil-immersion objective, with a Raspberry Pi Camera v2.
|
||||
</p>
|
||||
<p class="uk-margin-small" :class="{ 'uk-text-muted': !imjoyPermitted }">
|
||||
<label :disabled="!imjoyPermitted">
|
||||
<input
|
||||
|
|
@ -57,14 +44,6 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
IHIEnabled: {
|
||||
get() {
|
||||
return this.$store.state.IHIEnabled;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("changeIHIEnabled", value);
|
||||
}
|
||||
},
|
||||
imjoyEnabled: {
|
||||
get() {
|
||||
return this.$store.state.imjoyEnabled;
|
||||
|
|
@ -90,9 +69,6 @@ export default {
|
|||
},
|
||||
|
||||
watch: {
|
||||
IHIEnabled: function() {
|
||||
this.setLocalStorageObj("IHIEnabled", this.IHIEnabled);
|
||||
},
|
||||
imjoyEnabled: function() {
|
||||
this.setLocalStorageObj("imjoyEnabled", this.imjoyEnabled);
|
||||
},
|
||||
|
|
@ -103,7 +79,6 @@ export default {
|
|||
|
||||
mounted() {
|
||||
// Try loading settings from localStorage. If null, don't change.
|
||||
this.IHIEnabled = this.getLocalStorageObj("IHIEnabled") || this.IHIEnabled;
|
||||
this.imjoyEnabled =
|
||||
this.getLocalStorageObj("imjoyEnabled") || this.imjoyEnabled;
|
||||
this.galleryEnabled =
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
<template>
|
||||
<div v-if="settings" id="microscopeSettings" class="uk-width-large">
|
||||
<form @submit.prevent="applySettingsRequest">
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text"
|
||||
>Microscope name</label
|
||||
>
|
||||
<input
|
||||
v-model="settings.name"
|
||||
class="uk-input uk-width-1-1 uk-form-small"
|
||||
name="inputFilename"
|
||||
placeholder="Leave blank for default"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="uk-button uk-button-primary uk-margin-small uk-width-1-1"
|
||||
>
|
||||
Apply Settings
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
name: "MicroscopeSettings",
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
settings: null
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.updateSettings();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateSettings: function() {
|
||||
axios
|
||||
.get(this.settingsUri)
|
||||
.then(response => {
|
||||
this.settings = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
applySettingsRequest: function() {
|
||||
var payload = {
|
||||
name: this.settings.name
|
||||
};
|
||||
|
||||
// Send request to update config
|
||||
axios
|
||||
.put(this.settingsUri, payload)
|
||||
.then(() => {
|
||||
// Update local settings
|
||||
this.updateSettings();
|
||||
this.modalNotify("Microscope settings applied.");
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.center-spinner {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,192 +1,25 @@
|
|||
<template>
|
||||
<div id="stageSettings" class="uk-width-large">
|
||||
<div v-if="stageType === 'MissingStage'" class="uk-text-danger">
|
||||
<b>No stage connected</b>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h3>Stage settings</h3>
|
||||
<form @submit.prevent="setStageType">
|
||||
<div class="uk-margin-small-bottom">
|
||||
<h4>Stage geometry</h4>
|
||||
<select v-model="stageType" class="uk-select uk-margin-small-top">
|
||||
<option value="SangaStage">SangaStage (Standard)</option>
|
||||
<option value="SangaDeltaStage"> SangaStage (Delta)</option>
|
||||
</select>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="uk-button uk-button-primary uk-margin-small uk-width-1-1"
|
||||
>
|
||||
Change stage geometry
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<form @submit.prevent="applySettingsRequest">
|
||||
<div class="uk-margin-small-bottom">
|
||||
<h4>Backlash compensation</h4>
|
||||
<p class="uk-margin-remove">
|
||||
Backlash compentation causes movements to overshoot by that number
|
||||
of motor steps, reducing the effect of mechanical backlash.
|
||||
</p>
|
||||
<div
|
||||
class="uk-grid-small uk-child-width-1-3 uk-margin-small-bottom"
|
||||
uk-grid
|
||||
>
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">x</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="backlash.x"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">y</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="backlash.y"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="uk-form-label" for="form-stacked-text">z</label>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="backlash.z"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4>Settle time</h4>
|
||||
<div v-if="settle_time !== undefined">
|
||||
<p class="uk-margin-small-bottom">
|
||||
When moving or scanning, captures will be delayed by the settle
|
||||
time (in seconds) to reduce motion blur.
|
||||
</p>
|
||||
<div class="uk-form-controls">
|
||||
<input
|
||||
v-model="settle_time"
|
||||
class="uk-input uk-form-small"
|
||||
type="number"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="uk-button uk-button-primary uk-margin-small uk-width-1-1"
|
||||
>
|
||||
Apply Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
The microscope stage is a <b>{{ stageType }}</b
|
||||
>. There are no settings to adjust here: to set the geometry of your stage
|
||||
(i.e. switch between delta and cartesian), you will need to adjust the
|
||||
configuration of your microscope, which cannot be done while it is running.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
|
||||
export default {
|
||||
name: "StageSettings",
|
||||
|
||||
components: {},
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
stageType: "MissingStage",
|
||||
backlash: {
|
||||
x: undefined,
|
||||
y: undefined,
|
||||
z: undefined
|
||||
},
|
||||
settle_time: undefined
|
||||
};
|
||||
return {};
|
||||
},
|
||||
|
||||
computed: {
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
},
|
||||
stageTypeUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/stage/type`;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.getStageType();
|
||||
this.updateSettings();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateSettings: function() {
|
||||
axios
|
||||
.get(this.settingsUri)
|
||||
.then(response => {
|
||||
const stageSettings = response.data.stage;
|
||||
this.backlash.x = stageSettings.backlash.x;
|
||||
this.backlash.y = stageSettings.backlash.y;
|
||||
this.backlash.z = stageSettings.backlash.z;
|
||||
this.settle_time = stageSettings.settle_time;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
applySettingsRequest: function() {
|
||||
var payload = {
|
||||
stage: {
|
||||
backlash: this.backlash
|
||||
}
|
||||
};
|
||||
// Send request to update settings
|
||||
axios
|
||||
.put(this.settingsUri, payload)
|
||||
.then(() => {
|
||||
// Update local settings
|
||||
this.updateSettings();
|
||||
this.modalNotify("Stage settings applied.");
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
getStageType: function() {
|
||||
axios
|
||||
.get(this.stageTypeUri)
|
||||
.then(response => {
|
||||
this.stageType = response.data;
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error);
|
||||
});
|
||||
},
|
||||
setStageType: function() {
|
||||
axios
|
||||
.put(this.stageTypeUri, this.stageType, {
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
this.stageType = response.data;
|
||||
|
||||
this.modalNotify("Stage geometry changed.");
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error);
|
||||
});
|
||||
stageType: function() {
|
||||
return this.thingDescription("stage").title;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -70,19 +70,6 @@
|
|||
Camera/stage mapping
|
||||
</tabIcon>
|
||||
</li>
|
||||
<li>
|
||||
<tabIcon
|
||||
id="settings-microscope-icon"
|
||||
tab-i-d="microscope"
|
||||
:show-title="false"
|
||||
:show-tooltip="false"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
@set-tab="setTab"
|
||||
>
|
||||
General
|
||||
</tabIcon>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="view-component uk-width-expand uk-padding-small">
|
||||
|
|
@ -136,24 +123,12 @@
|
|||
<CSMSettings />
|
||||
</div>
|
||||
</tabContent>
|
||||
|
||||
<tabContent
|
||||
tab-i-d="microscope"
|
||||
:require-connection="true"
|
||||
:current-tab="currentTab"
|
||||
>
|
||||
<div class="settings-pane uk-padding-small">
|
||||
<h3>Microscope settings</h3>
|
||||
<microscopeSettings />
|
||||
</div>
|
||||
</tabContent>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import streamSettings from "./settingsComponents/streamSettings.vue";
|
||||
import microscopeSettings from "./settingsComponents/microscopeSettings.vue";
|
||||
import cameraSettings from "./settingsComponents/cameraSettings.vue";
|
||||
import appSettings from "./settingsComponents/appSettings.vue";
|
||||
import featuresSettings from "./settingsComponents/featuresSettings.vue";
|
||||
|
|
@ -171,7 +146,6 @@ export default {
|
|||
streamSettings,
|
||||
cameraSettings,
|
||||
stageSettings,
|
||||
microscopeSettings,
|
||||
CSMSettings,
|
||||
appSettings,
|
||||
featuresSettings,
|
||||
|
|
|
|||
|
|
@ -134,7 +134,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import taskSubmitter from "../../genericComponents/taskSubmitter";
|
||||
|
||||
export default {
|
||||
|
|
@ -166,9 +165,6 @@ export default {
|
|||
},
|
||||
|
||||
computed: {
|
||||
pluginsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
||||
},
|
||||
currentTimeForm: {
|
||||
get() {
|
||||
// Chop the timezone information from the end
|
||||
|
|
@ -210,30 +206,7 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
this.updateScanUri();
|
||||
},
|
||||
|
||||
methods: {
|
||||
updateScanUri: function() {
|
||||
axios
|
||||
.get(this.pluginsUri) // Get a list of plugins
|
||||
.then(response => {
|
||||
var plugins = response.data;
|
||||
var foundExtension = plugins.find(
|
||||
e => e.title === "org.openflexure.scan"
|
||||
);
|
||||
// if ScanPlugin is enabled
|
||||
if (foundExtension) {
|
||||
// Get plugin action link
|
||||
this.scanUri = foundExtension.links.tile.href;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
this.modalError(error); // Let mixin handle error
|
||||
});
|
||||
},
|
||||
|
||||
onScanError: function(error) {
|
||||
this.scanRunning = false;
|
||||
this.modalError(error);
|
||||
|
|
|
|||
|
|
@ -58,9 +58,6 @@ export default {
|
|||
},
|
||||
streamImgUri: function() {
|
||||
return `${this.$store.getters.baseUri}/camera/mjpeg_stream`;
|
||||
},
|
||||
settingsUri: function() {
|
||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from "vue";
|
||||
import App from "./App.vue";
|
||||
import store from "./store";
|
||||
import axios from "axios";
|
||||
import UIkit from "uikit";
|
||||
import VueTour from "vue-tour";
|
||||
import VueFriendlyIframe from "vue-friendly-iframe";
|
||||
|
|
@ -34,6 +35,49 @@ Vue.config.productionTip = false;
|
|||
|
||||
Vue.mixin({
|
||||
methods: {
|
||||
thingDescription(thing) {
|
||||
return this.$store.getters["wot/thingDescription"](thing);
|
||||
},
|
||||
thingAvailable(thing) {
|
||||
return this.$store.getters["wot/thingAvailable"](thing);
|
||||
},
|
||||
async readThingProperty(thing, property, silence_errors = false) {
|
||||
let url = this.$store.getters["wot/thingPropertyUrl"](
|
||||
thing,
|
||||
property,
|
||||
"readproperty",
|
||||
false
|
||||
);
|
||||
try {
|
||||
let response = await axios.get(url);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (!silence_errors) this.modalError(error);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
async writeThingProperty(thing, property, value) {
|
||||
let url = this.$store.getters["wot/thingPropertyUrl"](
|
||||
thing,
|
||||
property,
|
||||
"writeproperty",
|
||||
false
|
||||
);
|
||||
try {
|
||||
await axios.put(url, value);
|
||||
} catch (error) {
|
||||
this.modalError(error);
|
||||
}
|
||||
},
|
||||
thingActionUrl(thing, action) {
|
||||
let url = this.$store.getters["wot/thingActionUrl"](
|
||||
thing,
|
||||
action,
|
||||
"invokeaction",
|
||||
false
|
||||
);
|
||||
return url;
|
||||
},
|
||||
modalConfirm: function(modalText) {
|
||||
var context = this;
|
||||
|
||||
|
|
@ -86,6 +130,7 @@ 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"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import wotStoreModule from "./wot-client";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
|
|
@ -76,7 +77,8 @@ function getOriginFromLocation() {
|
|||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
imjoy: moduleImjoy
|
||||
imjoy: moduleImjoy,
|
||||
wot: wotStoreModule
|
||||
},
|
||||
state: {
|
||||
origin: getOriginFromLocation(),
|
||||
|
|
@ -86,7 +88,6 @@ export default new Vuex.Store({
|
|||
disableStream: false,
|
||||
autoGpuPreview: false,
|
||||
trackWindow: true,
|
||||
IHIEnabled: false,
|
||||
imjoyEnabled: false,
|
||||
galleryEnabled: true,
|
||||
appTheme: "system",
|
||||
|
|
@ -112,9 +113,6 @@ export default new Vuex.Store({
|
|||
changeAppTheme(state, theme) {
|
||||
state.appTheme = theme;
|
||||
},
|
||||
changeIHIEnabled(state, enabled) {
|
||||
state.IHIEnabled = enabled;
|
||||
},
|
||||
changeImjoyEnabled(state, enabled) {
|
||||
if (process.env.VUE_APP_ENABLE_IMJOY === "true") {
|
||||
state.imjoyEnabled = enabled;
|
||||
|
|
|
|||
129
webapp/src/wot-client.js
Normal file
129
webapp/src/wot-client.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import axios from "axios";
|
||||
|
||||
export const wotStoreModule = {
|
||||
namespaced: true,
|
||||
state: () => ({
|
||||
thingDescriptions: {},
|
||||
servient: null,
|
||||
helpers: null
|
||||
}),
|
||||
mutations: {
|
||||
addThingDescription(state, { thingName, thingDescription }) {
|
||||
state.thingDescriptions[thingName] = thingDescription;
|
||||
},
|
||||
removeThingDescription(state, thingName) {
|
||||
delete state.thingDescriptions[thingName];
|
||||
},
|
||||
removeAllThingDescriptions(state) {
|
||||
state.thingDescriptions = {};
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async start() {
|
||||
// Set up thing client - not currently used.
|
||||
},
|
||||
async fetchThingDescription({ commit }, { uri, name = null }) {
|
||||
// Fetch the thing description from the given URI and consume it
|
||||
// NB this should only be called once, or we'll duplicate effort.
|
||||
// Deduplication should be done elsewhere.
|
||||
let response = await axios.get(uri);
|
||||
let td = response.data;
|
||||
let thing_name =
|
||||
name |
|
||||
uri
|
||||
.replace(/\/$/, "")
|
||||
.split("/")
|
||||
.pop();
|
||||
commit("addThingDescription", {
|
||||
thingName: thing_name,
|
||||
thingDescription: td
|
||||
});
|
||||
},
|
||||
async fetchThingDescriptions({ commit }, uri) {
|
||||
// Fetch thing descriptions from the given URI
|
||||
let response = await axios.get(uri);
|
||||
if (response.status != 200) throw "Could not retrieve thing descriptions";
|
||||
for (const k in response.data) {
|
||||
let thing_name = k.replace(/\/$/, "").replace(/^\//, "");
|
||||
commit("addThingDescription", {
|
||||
thingName: thing_name,
|
||||
thingDescription: response.data[k]
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
thingDescriptions: state => {
|
||||
return state.thingDescriptions;
|
||||
},
|
||||
thingDescription: state => thingName => {
|
||||
return state.thingDescriptions[thingName];
|
||||
},
|
||||
thingAvailable: state => thingName => {
|
||||
return thingName in state.thingDescriptions;
|
||||
},
|
||||
thingFormUrl: state => (
|
||||
thing,
|
||||
affordanceType,
|
||||
affordance,
|
||||
op,
|
||||
allowUndefined = true
|
||||
) => {
|
||||
// Find the URL for a particular operation
|
||||
let td = state.thingDescriptions[thing];
|
||||
if (!td) {
|
||||
if (allowUndefined) return undefined;
|
||||
throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`;
|
||||
}
|
||||
let affordances = td[affordanceType];
|
||||
let href = findFormHref(affordances[affordance], op);
|
||||
if (href == undefined) {
|
||||
if (allowUndefined) return undefined;
|
||||
throw `Could not find form for ${affordanceType} ${thing}/${affordance} with op ${op}`;
|
||||
}
|
||||
// If we've found an href, prepend the `base` URL if appropriate
|
||||
if (href.startsWith("http")) return href;
|
||||
if ("base" in td) {
|
||||
let base = td.base;
|
||||
if (href.startsWith("/")) href = href.slice(1);
|
||||
if (!base.endsWith("/")) base += "/";
|
||||
return base + href;
|
||||
}
|
||||
return href;
|
||||
},
|
||||
thingPropertyUrl: (_state, getters) => (
|
||||
thing,
|
||||
property,
|
||||
op,
|
||||
allowUndefined
|
||||
) => {
|
||||
// Find the URL for a particular property
|
||||
return getters.thingFormUrl(
|
||||
thing,
|
||||
"properties",
|
||||
property,
|
||||
op,
|
||||
allowUndefined
|
||||
);
|
||||
},
|
||||
thingActionUrl: (_state, getters) => (
|
||||
thing,
|
||||
action,
|
||||
op,
|
||||
allowUndefined
|
||||
) => {
|
||||
// Find the URL for a particular action
|
||||
return getters.thingFormUrl(thing, "actions", action, op, allowUndefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function findFormHref(affordance, op) {
|
||||
// Find the form in the affordance that matches the given operation type
|
||||
let forms = affordance.forms;
|
||||
let matchingForm = forms.find(f => f.op == op || f.op.includes(op));
|
||||
if (matchingForm == undefined) return undefined;
|
||||
return matchingForm.href;
|
||||
}
|
||||
|
||||
export default wotStoreModule;
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
module.exports = {
|
||||
outputDir: '../openflexure_microscope/api/static/dist',
|
||||
outputDir: "../openflexure_microscope/api/static/dist"
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue