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.
|
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 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
|
## Getting started
|
||||||
|
|
||||||
|
|
@ -62,18 +62,19 @@ To set up a development version of the software (most likely using emulated came
|
||||||
* `cd openflexure-microscope-server`
|
* `cd openflexure-microscope-server`
|
||||||
|
|
||||||
### Set up the Python environment and run a test server
|
### Set up the Python environment and run a test server
|
||||||
* (Optional) Set local Python version to match what is available on the Pi
|
* (Optional) Set local Python version to 3.11
|
||||||
* `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`
|
|
||||||
* Create a virtual environment and activate it:
|
* Create a virtual environment and activate it:
|
||||||
* `python -m venv .venv`
|
* `python -m venv .venv`
|
||||||
* `source .venv/bin/activate` (on Linux) or `.venv/Scripts/activate` (on Windows)
|
* `source .venv/bin/activate` (on Linux) or `.venv/Scripts/activate` (on Windows)
|
||||||
* `pip install --upgrade pip wheel pipenv`
|
* `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.)
|
||||||
* `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: 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.
|
||||||
* Finally, run the server:
|
|
||||||
* You can use `ofm serve` or `ofm restart` on the Raspberry Pi to manage the server.
|
### Run the server manually on a Raspberry Pi
|
||||||
* 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`
|
```
|
||||||
|
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
|
### 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/).
|
* 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 appContent from "./components/appContent.vue";
|
||||||
import loadingContent from "./components/loadingContent.vue";
|
import loadingContent from "./components/loadingContent.vue";
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
var Mousetrap = require("mousetrap");
|
var Mousetrap = require("mousetrap");
|
||||||
|
|
||||||
Mousetrap.prototype.stopCallback = function(e, element) {
|
Mousetrap.prototype.stopCallback = function(e, element) {
|
||||||
|
|
@ -305,24 +304,31 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
checkConnection: function() {
|
async checkConnection() {
|
||||||
var baseUri = this.$store.getters.baseUri;
|
var baseUri = this.$store.getters.baseUri;
|
||||||
this.$store.commit("changeWaiting", true);
|
this.$store.commit("changeWaiting", true);
|
||||||
axios
|
// TODO: more robust check - e.g. use a microscope Thing
|
||||||
// TODO: more robust check - e.g. use a microscope Thing
|
// TODO: should we purge existing consumedThings?
|
||||||
.get(`${baseUri}/stage/`)
|
try {
|
||||||
.then(() => {
|
await this.$store.dispatch(
|
||||||
this.$store.commit("setConnected");
|
"wot/fetchThingDescriptions",
|
||||||
this.$store.commit("setErrorMessage", null);
|
`${baseUri}/thing_descriptions/`
|
||||||
})
|
);
|
||||||
.catch(error => {
|
for (let requiredThing of ["camera", "stage"]) {
|
||||||
this.$store.commit("setErrorMessage", error);
|
if (!this.$store.getters["wot/thingAvailable"](requiredThing)) {
|
||||||
})
|
throw new Error(
|
||||||
.finally(() => {
|
`No ${requiredThing} found, the GUI won't work without one.`
|
||||||
this.$store.commit("changeWaiting", false);
|
);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
this.$store.commit("setConnected");
|
||||||
|
this.$store.commit("setErrorMessage", null);
|
||||||
|
} catch (error) {
|
||||||
|
this.$store.commit("setErrorMessage", error);
|
||||||
|
} finally {
|
||||||
|
this.$store.commit("changeWaiting", false);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleExit: function() {
|
handleExit: function() {
|
||||||
this.$root.$emit("globalTogglePreview", false);
|
this.$root.$emit("globalTogglePreview", false);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -231,10 +231,6 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
pluginsUri: function() {
|
|
||||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
|
||||||
},
|
|
||||||
|
|
||||||
pluginsGuiList: function() {
|
pluginsGuiList: function() {
|
||||||
// List of plugin GUIs, obtained from this.plugins values
|
// List of plugin GUIs, obtained from this.plugins values
|
||||||
var pluginGuis = [];
|
var pluginGuis = [];
|
||||||
|
|
@ -267,34 +263,26 @@ export default {
|
||||||
icon: "visibility",
|
icon: "visibility",
|
||||||
component: viewContent
|
component: viewContent
|
||||||
},
|
},
|
||||||
{
|
/*{
|
||||||
id: "gallery",
|
id: "gallery",
|
||||||
icon: "photo_library",
|
icon: "photo_library",
|
||||||
component: galleryContent,
|
component: galleryContent,
|
||||||
divide: true // Add a divider after this tab icon
|
divide: true // Add a divider after this tab icon
|
||||||
},
|
},*/
|
||||||
{
|
{
|
||||||
id: "navigate",
|
id: "navigate",
|
||||||
icon: "gamepad",
|
icon: "gamepad",
|
||||||
component: navigateContent
|
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",
|
id: "slidescan",
|
||||||
icon: "settings_overscan",
|
icon: "settings_overscan",
|
||||||
component: slideScanContent,
|
component: slideScanContent,
|
||||||
divide: true
|
divide: true
|
||||||
});
|
}
|
||||||
|
];
|
||||||
|
if (!this.$store.state.galleryEnabled) {
|
||||||
|
tabs = tabs.filter(tab => tab.id != "gallery");
|
||||||
}
|
}
|
||||||
if (this.$store.state.imjoyEnabled) {
|
if (this.$store.state.imjoyEnabled) {
|
||||||
tabs.push({
|
tabs.push({
|
||||||
|
|
@ -351,9 +339,9 @@ export default {
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.modalError(error); // Let mixin handle error
|
this.modalError(error); // Let mixin handle error
|
||||||
});*/
|
});*/
|
||||||
return new Promise((resolve) => {
|
return new Promise(resolve => {
|
||||||
resolve({});
|
resolve({});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setTab: function(event, tab) {
|
setTab: function(event, tab) {
|
||||||
if (!(this.currentTab == tab)) {
|
if (!(this.currentTab == tab)) {
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,10 @@ export default {
|
||||||
if (task.status == "pending" || task.status == "running") {
|
if (task.status == "pending" || task.status == "running") {
|
||||||
this.taskStarted = true;
|
this.taskStarted = true;
|
||||||
this.$emit("taskStarted", this.taskId);
|
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) => {
|
var checkCondition = (resolve, reject) => {
|
||||||
// If the condition is met, we're done!
|
// If the condition is met, we're done!
|
||||||
axios.get(
|
axios
|
||||||
this.taskUrl,
|
.get(this.taskUrl, { baseURL: this.$store.getters.baseUri })
|
||||||
{baseURL: this.$store.getters.baseUri}
|
.then(response => {
|
||||||
).then(response => {
|
var result = response.data.status;
|
||||||
var result = response.data.status;
|
// If the task ends with success
|
||||||
// If the task ends with success
|
if (result == "completed") {
|
||||||
if (result == "completed") {
|
resolve(response.data);
|
||||||
resolve(response.data);
|
}
|
||||||
}
|
// If task ends with an error
|
||||||
// If task ends with an error
|
else if (result == "error") {
|
||||||
else if (result == "error") {
|
// Pass the error string back with reject
|
||||||
// Pass the error string back with reject
|
|
||||||
|
|
||||||
reject(new Error(response.data.output));
|
reject(new Error(response.data.output));
|
||||||
}
|
}
|
||||||
// If task ends with termination
|
// If task ends with termination
|
||||||
else if (result == "cancelled") {
|
else if (result == "cancelled") {
|
||||||
// Pass a generic termination error back with reject
|
// Pass a generic termination error back with reject
|
||||||
|
|
||||||
reject(new Error("Task cancelled"));
|
reject(new Error("Task cancelled"));
|
||||||
} else {
|
} else {
|
||||||
// Since the task is still running, we can update the progress bar
|
// Since the task is still running, we can update the progress bar
|
||||||
this.progress = response.data.progress;
|
this.progress = response.data.progress;
|
||||||
// Check again after timeout
|
// Check again after timeout
|
||||||
setTimeout(checkCondition, interval, resolve, reject);
|
setTimeout(checkCondition, interval, resolve, reject);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Promise(checkCondition);
|
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
|
<cameraCalibrationSettings
|
||||||
:show-extra-settings="false"
|
:show-extra-settings="false"
|
||||||
|
:camera-uri="cameraUri"
|
||||||
></cameraCalibrationSettings>
|
></cameraCalibrationSettings>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -146,8 +147,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
import cameraCalibrationSettings from "../tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue";
|
import cameraCalibrationSettings from "../tabContentComponents/settingsComponents/cameraSettingsComponents/cameraCalibrationSettings.vue";
|
||||||
import CSMCalibrationSettings from "../tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue";
|
import CSMCalibrationSettings from "../tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue";
|
||||||
import miniStreamDisplay from "../genericComponents/miniStreamDisplay.vue";
|
import miniStreamDisplay from "../genericComponents/miniStreamDisplay.vue";
|
||||||
|
|
@ -172,62 +171,29 @@ export default {
|
||||||
return {
|
return {
|
||||||
ready: false,
|
ready: false,
|
||||||
stepValue: 0,
|
stepValue: 0,
|
||||||
settings: {},
|
isCSMCalibrated: undefined,
|
||||||
config: {}
|
isLSTCalibrated: undefined
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
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() {
|
canCSMCalibrated: function() {
|
||||||
// Assert CSM extension is enabled
|
// Assert CSM extension is enabled
|
||||||
var extensionEnabled = this.availablePluginTitles.includes(
|
return this.thingAvailable("camera_stage_mapping");
|
||||||
"org.openflexure.camera_stage_mapping"
|
|
||||||
);
|
|
||||||
// Assert real stage is connected
|
|
||||||
var stageConnected = this.config.stage.type !== "MissingStage";
|
|
||||||
// Combine
|
|
||||||
return stageConnected && extensionEnabled;
|
|
||||||
},
|
},
|
||||||
canLSTCalibrated: function() {
|
canLSTCalibrated: function() {
|
||||||
// Assert LST extension is enabled
|
// Assert LST extension is enabled
|
||||||
var extensionEnabled = this.availablePluginTitles.includes(
|
return (
|
||||||
"org.openflexure.calibration.picamera"
|
"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() {
|
isUseful: function() {
|
||||||
var CSMUseful = this.canCSMCalibrated && !this.isCSMCalibrated;
|
var CSMUseful = this.canCSMCalibrated && !this.isCSMCalibrated;
|
||||||
var LSTUseful = this.canLSTCalibrated && !this.isLSTCalibrated;
|
var LSTUseful = this.canLSTCalibrated && !this.isLSTCalibrated;
|
||||||
return CSMUseful || LSTUseful;
|
return CSMUseful || LSTUseful;
|
||||||
|
},
|
||||||
|
cameraUri: function() {
|
||||||
|
return `${this.$store.getters.baseUri}/camera/`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -236,25 +202,33 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
show: function() {
|
show: async function() {
|
||||||
// Get current settings
|
// Check if the camera and stage are calibrated, if they can be
|
||||||
this.getSettings()
|
if (this.canCSMCalibrated) {
|
||||||
.then(() => {
|
let csm = await this.readThingProperty(
|
||||||
return this.getConfig();
|
"camera_stage_mapping",
|
||||||
})
|
"image_to_stage_displacement_matrix",
|
||||||
.then(() => {
|
true
|
||||||
// Check if this calibration wizard can actually do anything useful
|
);
|
||||||
if (this.isUseful) {
|
this.isCSMCalibrated = Boolean(csm);
|
||||||
this.ready = true;
|
}
|
||||||
this.stepValue = 0;
|
if (this.canLSTCalibrated) {
|
||||||
// Show the modal
|
this.isLSTCalibrated = await this.readThingProperty(
|
||||||
var el = this.$refs["calibrationModalEl"];
|
"camera",
|
||||||
this.showModalElement(el); // Calls the mixin
|
"lens_shading_is_static"
|
||||||
} else {
|
);
|
||||||
// If not useful, we just return the onClose event immediately
|
}
|
||||||
this.onHide();
|
// 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() {
|
hide: function() {
|
||||||
|
|
@ -268,28 +242,6 @@ export default {
|
||||||
this.$emit("onClose");
|
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() {
|
decrement: function() {
|
||||||
if (this.stepValue > 0) {
|
if (this.stepValue > 0) {
|
||||||
this.stepValue = this.stepValue - 1;
|
this.stepValue = this.stepValue - 1;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="host-input">
|
<div class="host-input">
|
||||||
<div v-if="configuration">
|
<div v-if="$store.state.available">
|
||||||
<div>
|
<div>
|
||||||
<div class="uk-margin-small-bottom">
|
<div class="uk-margin-small-bottom">
|
||||||
<b>API origin:</b>
|
<b>API origin:</b>
|
||||||
|
|
@ -11,13 +11,9 @@
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<div v-if="settings">
|
|
||||||
<b>Device name:</b> <br />
|
|
||||||
{{ settings.name }}
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<b>Server version:</b> <br />
|
<b>Server version:</b> <br />
|
||||||
{{ configuration.application.version }}
|
TODO
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
@ -25,18 +21,18 @@
|
||||||
<div class="uk-margin-small-bottom">
|
<div class="uk-margin-small-bottom">
|
||||||
<b>Camera:</b>
|
<b>Camera:</b>
|
||||||
<br />
|
<br />
|
||||||
<div v-if="configuration.camera.type != 'MissingCamera'">
|
<div v-if="'camera' in things">
|
||||||
{{ configuration.camera.type }}
|
{{ things.camera.title }}
|
||||||
</div>
|
</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>
|
||||||
<div>
|
<div>
|
||||||
<b>Stage:</b>
|
<b>Stage:</b>
|
||||||
<br />
|
<br />
|
||||||
<div v-if="configuration.stage.type != 'MissingStage'">
|
<div v-if="'stage' in things">
|
||||||
{{ configuration.stage.type }}
|
{{ things.stage.title }}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
@ -44,9 +40,9 @@
|
||||||
<div class="uk-grid-small uk-child-width-1-2" uk-grid>
|
<div class="uk-grid-small uk-child-width-1-2" uk-grid>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<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"
|
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
|
Shutdown
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -54,9 +50,9 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button
|
<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"
|
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
|
Restart
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -79,103 +75,22 @@ import axios from "axios";
|
||||||
export default {
|
export default {
|
||||||
name: "StatusPane",
|
name: "StatusPane",
|
||||||
|
|
||||||
components: {},
|
|
||||||
|
|
||||||
data: function() {
|
|
||||||
return {
|
|
||||||
configuration: null,
|
|
||||||
settings: null,
|
|
||||||
systemActionLinks: {}
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
settingsUri: function() {
|
things: function() {
|
||||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
return this.$store.getters["wot/thingDescriptions"];
|
||||||
},
|
|
||||||
configurationUri: function() {
|
|
||||||
return `${this.$store.getters.baseUri}/api/v2/instrument/configuration`;
|
|
||||||
},
|
|
||||||
rootUri: function() {
|
|
||||||
return `${this.$store.getters.baseUri}/api/v2`;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted: function() {
|
|
||||||
// Watch for host 'ready', then update configuration
|
|
||||||
this.updateConfiguration();
|
|
||||||
this.updateSettings();
|
|
||||||
this.updateSystemActions();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
updateConfiguration: function() {
|
systemRequest: function(action) {
|
||||||
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() {
|
|
||||||
this.modalConfirm("Restart microscope?").then(
|
this.modalConfirm("Restart microscope?").then(
|
||||||
() => {
|
() => {
|
||||||
if ("reboot" in this.systemActionLinks) {
|
this.$store.commit("resetState");
|
||||||
this.$store.commit("resetState");
|
this.$store.commit("wot/deleteAllThingDescriptions");
|
||||||
// Post and silence errors
|
// Post and silence errors
|
||||||
axios.post(this.systemActionLinks.reboot).catch(() => {});
|
axios
|
||||||
}
|
.post(this.thingActionUrl("system_control", action))
|
||||||
|
.catch(() => {});
|
||||||
},
|
},
|
||||||
() => {}
|
() => {}
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@
|
||||||
class="uk-link"
|
class="uk-link"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://gitlab.com/openflexure/openflexure-microscope-server/-/issues"
|
href="https://gitlab.com/openflexure/openflexure-microscope-server/-/issues"
|
||||||
>Report an issue</a
|
|
||||||
>
|
>
|
||||||
|
Report an issue
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-padding-small">
|
<div class="uk-padding-small">
|
||||||
<h2>Developer tools</h2>
|
<h2>Developer tools</h2>
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@
|
||||||
|
|
||||||
<ul uk-accordion="multiple: true">
|
<ul uk-accordion="multiple: true">
|
||||||
<!--Show stack and scan if scan plugin is enabled-->
|
<!--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>
|
<a class="uk-accordion-title" href="#">Stack and Scan</a>
|
||||||
<div class="uk-accordion-content">
|
<div class="uk-accordion-content">
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
|
|
@ -221,7 +221,10 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<label class="uk-form-label" for="form-stacked-text">
|
||||||
<input
|
<input
|
||||||
v-model="detectEmptyFieldsAndSkipAutofocus"
|
v-model="detectEmptyFieldsAndSkipAutofocus"
|
||||||
|
|
@ -256,7 +259,10 @@
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<!--Show stack and scan if scan plugin is enabled-->
|
<!--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>
|
<a class="uk-accordion-title" href="#">Smart Stack</a>
|
||||||
<div
|
<div
|
||||||
class="uk-accordion-content"
|
class="uk-accordion-content"
|
||||||
|
|
@ -401,10 +407,7 @@ export default {
|
||||||
data: function() {
|
data: function() {
|
||||||
return {
|
return {
|
||||||
...defaultCaptureSettings(),
|
...defaultCaptureSettings(),
|
||||||
scanCapture: false, // Don't remember the "scan" tickbox in local storage.
|
scanCapture: false // Don't remember the "scan" tickbox in local storage.
|
||||||
scanUri: null,
|
|
||||||
smartScanUri: null,
|
|
||||||
backgroundDetectUri: null
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -415,10 +418,13 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
captureActionUri: function() {
|
captureActionUri: function() {
|
||||||
return `${this.$store.getters.baseUri}/api/v2/actions/camera/capture`;
|
return this.thingActionUrl("camera", "capture");
|
||||||
},
|
},
|
||||||
pluginsUri: function() {
|
scanUri: function() {
|
||||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
return this.thingActionUrl("scan", "scan");
|
||||||
|
},
|
||||||
|
smartScanUri: function() {
|
||||||
|
return this.thingActionUrl("scan", "scan");
|
||||||
},
|
},
|
||||||
basePayload: function() {
|
basePayload: function() {
|
||||||
var payload = {};
|
var payload = {};
|
||||||
|
|
@ -475,7 +481,8 @@ export default {
|
||||||
namemode: this.namingStyle.toLowerCase(),
|
namemode: this.namingStyle.toLowerCase(),
|
||||||
autofocus_dz: afDeltas[this.scanDeltaZ],
|
autofocus_dz: afDeltas[this.scanDeltaZ],
|
||||||
fast_autofocus: this.scanDeltaZ == "Fast",
|
fast_autofocus: this.scanDeltaZ == "Fast",
|
||||||
detect_empty_fields_and_skip_autofocus: this.detectEmptyFieldsAndSkipAutofocus
|
detect_empty_fields_and_skip_autofocus: this
|
||||||
|
.detectEmptyFieldsAndSkipAutofocus
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
smartScanPayload: function() {
|
smartScanPayload: function() {
|
||||||
|
|
@ -493,7 +500,6 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.updateScanUri();
|
|
||||||
// Load settings if they have been saved, and set up watchers to sync with local storage
|
// Load settings if they have been saved, and set up watchers to sync with local storage
|
||||||
syncDataWithLocalStorage("captureSettings", this, defaultCaptureSettings());
|
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() {
|
onScanResponse: function() {
|
||||||
this.modalNotify("Finished scan.");
|
this.modalNotify("Finished scan.");
|
||||||
// Emit signal to update capture list
|
// 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() {
|
mounted: function() {
|
||||||
this.updateZipperUri();
|
this.updateZipperUri();
|
||||||
},
|
},
|
||||||
|
|
@ -84,7 +78,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
updateZipperUri: function() {
|
updateZipperUri: function() {
|
||||||
if (this.$store.state.available) {
|
if (this.$store.state.available) {
|
||||||
axios
|
/*axios
|
||||||
.get(this.pluginsUri) // Get a list of plugins
|
.get(this.pluginsUri) // Get a list of plugins
|
||||||
.then(response => {
|
.then(response => {
|
||||||
var plugins = response.data;
|
var plugins = response.data;
|
||||||
|
|
@ -100,7 +94,7 @@ export default {
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
this.modalError(error); // Let mixin handle error
|
this.modalError(error); // Let mixin handle error
|
||||||
});
|
});*/
|
||||||
} else {
|
} else {
|
||||||
this.zipBuilderUri = null;
|
this.zipBuilderUri = null;
|
||||||
this.zipGetterUri = null;
|
this.zipGetterUri = null;
|
||||||
|
|
|
||||||
|
|
@ -42,15 +42,15 @@
|
||||||
<div class="uk-width-xlarge uk-align-center">
|
<div class="uk-width-xlarge uk-align-center">
|
||||||
<div
|
<div
|
||||||
v-for="item in pagedItems"
|
v-for="item in pagedItems"
|
||||||
:key="item.timestamp"
|
:key="item.sequence"
|
||||||
uk-alert
|
uk-alert
|
||||||
class="logging-entry"
|
class="logging-entry"
|
||||||
:class="{
|
:class="{
|
||||||
'uk-alert-warning uk-alert': item.data.levelname == 'WARNING',
|
'uk-alert-warning uk-alert': item.level == 'WARNING',
|
||||||
'uk-alert-danger uk-alert': item.data.levelname == 'ERROR'
|
'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 class="logging-message">{{ formatMessage(item) }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -102,18 +102,15 @@ export default {
|
||||||
var items = [];
|
var items = [];
|
||||||
for (var item of this.logs) {
|
for (var item of this.logs) {
|
||||||
// Add to capture list if matched
|
// Add to capture list if matched
|
||||||
if (this.filteredLevels.includes(item.data.levelname)) {
|
if (this.filteredLevels.includes(item.level)) {
|
||||||
items.push(item);
|
items.push(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
},
|
},
|
||||||
loggingUri: function() {
|
|
||||||
return `${this.$store.getters.baseUri}/api/v2/events/logging`;
|
|
||||||
},
|
|
||||||
logFileURI: function() {
|
logFileURI: function() {
|
||||||
return `${this.$store.getters.baseUri}/api/v2/log`;
|
return `${this.$store.getters.baseUri}/log/`;
|
||||||
},
|
},
|
||||||
pagedItems: function() {
|
pagedItems: function() {
|
||||||
let startIndex = (this.page - 1) * this.maxitems;
|
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: {
|
methods: {
|
||||||
scrollToTop() {
|
scrollToTop() {
|
||||||
const el = document.querySelector("#container-left");
|
const el = document.querySelector("#container-left");
|
||||||
|
|
@ -141,22 +133,30 @@ export default {
|
||||||
this.updateLogs();
|
this.updateLogs();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateLogs: function() {
|
async updateLogs() {
|
||||||
axios
|
let response = await axios.get(this.logFileURI);
|
||||||
.get(this.loggingUri)
|
let lines = response.data.split("\n").reverse();
|
||||||
.then(response => {
|
this.logs = [];
|
||||||
this.logs = response.data.reverse();
|
let regexp = /\[(.+)\] \[(.+)\] (.*)$/;
|
||||||
})
|
for (let line of lines) {
|
||||||
.catch(error => {
|
if (line.length > 0) {
|
||||||
this.modalError(error); // Let mixin handle error
|
let m = line.match(regexp);
|
||||||
});
|
this.logs.push({
|
||||||
|
timestamp: m[1],
|
||||||
|
level: m[2],
|
||||||
|
message: m[3],
|
||||||
|
sequence: this.logs.length
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
formatDateTime: function(isoDateTimeString) {
|
formatDateTime: function(isoDateTimeString) {
|
||||||
|
isoDateTimeString = isoDateTimeString.replace(",", ".");
|
||||||
let date = new Date(isoDateTimeString);
|
let date = new Date(isoDateTimeString);
|
||||||
return date.toLocaleDateString() + " " + date.toLocaleTimeString();
|
return date.toLocaleDateString() + " " + date.toLocaleTimeString();
|
||||||
},
|
},
|
||||||
formatMessage: function(item) {
|
formatMessage: function(item) {
|
||||||
return item.data.levelname + ": " + item.data.message;
|
return item.level + ": " + item.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -68,66 +68,46 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<taskSubmitter
|
||||||
class="uk-button uk-button-default uk-margin uk-width-1-1"
|
:submit-url="zeroActionUri"
|
||||||
@click="zeroRequest()"
|
:submit-label="'Zero Coordinates'"
|
||||||
>
|
:canTerminate="false"
|
||||||
Zero coordinates
|
@finished="updatePosition"
|
||||||
</button>
|
@error="modalError"
|
||||||
|
></taskSubmitter>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="uk-open">
|
<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">
|
<div class="uk-accordion-content">
|
||||||
<form @submit.prevent="handleSubmit">
|
<form>
|
||||||
<!-- Text boxes to set and view position -->
|
<!-- Text boxes to set and view position -->
|
||||||
|
<div class="input-and-buttons-container">
|
||||||
<div class="uk-grid-small uk-child-width-1-3" uk-grid>
|
<input
|
||||||
<div>
|
v-for="(_v, key) in setPosition"
|
||||||
<label class="uk-form-label" for="form-stacked-text">x</label>
|
:key="`setPosition_${key}`"
|
||||||
<div class="uk-form-controls">
|
v-model="setPosition[key]"
|
||||||
<input
|
class="uk-form-small numeric-setting-line-input"
|
||||||
v-model="setPosition.x"
|
type="number"
|
||||||
class="uk-input uk-form-small"
|
@keyup.enter="startMoveTask"
|
||||||
type="number"
|
/>
|
||||||
name="inputPositionX"
|
<a class="button-next-to-input" @click="updatePosition">
|
||||||
/>
|
<i class="material-icons">refresh</i>
|
||||||
</div>
|
</a>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<button
|
<taskSubmitter
|
||||||
type="submit"
|
ref="moveTaskSubmitter"
|
||||||
class="uk-button uk-button-default uk-float-right uk-width-1-1"
|
:submit-url="absoluteMoveUri"
|
||||||
>
|
:submit-data="setPosition"
|
||||||
Move
|
:submit-label="'Move'"
|
||||||
</button>
|
:canTerminate="false"
|
||||||
|
:pollInterval="0.05"
|
||||||
|
@taskStarted="moveLock = true"
|
||||||
|
@finished="moveLock = false"
|
||||||
|
@error="modalError"
|
||||||
|
></taskSubmitter>
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -143,7 +123,7 @@
|
||||||
v-if="fastAutofocusUri"
|
v-if="fastAutofocusUri"
|
||||||
:submit-url="fastAutofocusUri"
|
:submit-url="fastAutofocusUri"
|
||||||
:submit-data="{ dz: 2000 }"
|
:submit-data="{ dz: 2000 }"
|
||||||
:submit-label="'Fast'"
|
:submit-label="'Autofocus'"
|
||||||
:button-primary="false"
|
:button-primary="false"
|
||||||
:submit-on-event="'globalFastAutofocusEvent'"
|
:submit-on-event="'globalFastAutofocusEvent'"
|
||||||
@taskStarted="isAutofocusing = 1"
|
@taskStarted="isAutofocusing = 1"
|
||||||
|
|
@ -151,32 +131,34 @@
|
||||||
@error="modalError"
|
@error="modalError"
|
||||||
></taskSubmitter>
|
></taskSubmitter>
|
||||||
</div>
|
</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">
|
<div class="uk-grid-small uk-child-width-expand" uk-grid>
|
||||||
<taskSubmitter
|
<taskSubmitter
|
||||||
v-if="normalAutofocusUri"
|
v-if="captureUri"
|
||||||
:submit-url="normalAutofocusUri"
|
:submit-url="captureUri"
|
||||||
:submit-data="{ dz: [-90, -60, -30, 0, 30, 60, 90] }"
|
:submit-data="{ resolution: 'full' }"
|
||||||
:submit-label="'Medium'"
|
submit-label="Full Resolution"
|
||||||
:button-primary="false"
|
submit-on-event="globalCaptureEvent"
|
||||||
@taskStarted="isAutofocusing = 2"
|
@response="handleCaptureResponse"
|
||||||
@finished="isAutofocusing = 0"
|
@error="modalError"
|
||||||
@error="modalError"
|
></taskSubmitter>
|
||||||
></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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -220,10 +202,7 @@ export default {
|
||||||
},
|
},
|
||||||
setPosition: null,
|
setPosition: null,
|
||||||
isAutofocusing: 0,
|
isAutofocusing: 0,
|
||||||
moveLock: false,
|
moveLock: false
|
||||||
fastAutofocusUri: null,
|
|
||||||
normalAutofocusUri: null,
|
|
||||||
moveInImageCoordinatesUri: null
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -231,17 +210,26 @@ export default {
|
||||||
baseUri: function() {
|
baseUri: function() {
|
||||||
return this.$store.getters.baseUri;
|
return this.$store.getters.baseUri;
|
||||||
},
|
},
|
||||||
moveActionUri: function() {
|
absoluteMoveUri: function() {
|
||||||
return `${this.$store.getters.baseUri}/stage/move_relative`;
|
return this.thingActionUrl("stage", "move_absolute");
|
||||||
},
|
},
|
||||||
zeroActionUri: function() {
|
zeroActionUri: function() {
|
||||||
return `${this.$store.getters.baseUri}/api/v2/actions/stage/zero`;
|
return this.thingActionUrl("stage", "set_zero_position");
|
||||||
},
|
},
|
||||||
positionStatusUri: function() {
|
positionStatusUri: function() {
|
||||||
return `${this.baseUri}/stage/position`;
|
return `${this.baseUri}/stage/position`;
|
||||||
},
|
},
|
||||||
pluginsUri: function() {
|
fastAutofocusUri: function() {
|
||||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
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.stepSize =
|
||||||
this.getLocalStorageObj("navigation_stepSize") || this.stepSize;
|
this.getLocalStorageObj("navigation_stepSize") || this.stepSize;
|
||||||
this.invert = this.getLocalStorageObj("navigation_invert") || this.invert;
|
this.invert = this.getLocalStorageObj("navigation_invert") || this.invert;
|
||||||
|
let self = this;
|
||||||
// A global signal listener to perform a move action
|
// A global signal listener to perform a move action
|
||||||
this.$root.$on("globalMoveEvent", (x, y, z, absolute) => {
|
this.$root.$on("globalMoveEvent", self.move);
|
||||||
this.moveRequest(x, y, z, absolute);
|
|
||||||
});
|
|
||||||
// A global signal listener to perform a move action in pixels
|
// A global signal listener to perform a move action in pixels
|
||||||
this.$root.$on("globalMoveInImageCoordinatesEvent", (x, y, absolute) => {
|
this.$root.$on("globalMoveInImageCoordinatesEvent", (x, y, absolute) => {
|
||||||
this.moveInImageCoordinatesRequest(x, y, absolute);
|
this.moveInImageCoordinatesRequest(x, y, absolute);
|
||||||
});
|
});
|
||||||
// A global signal listener to perform a move in multiples of a step size
|
// 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.$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),
|
x_steps * this.stepSize.x * (this.invert.x ? -1 : 1),
|
||||||
y_steps * this.stepSize.y * (this.invert.y ? -1 : 1),
|
y_steps * this.stepSize.y * (this.invert.y ? -1 : 1),
|
||||||
z_steps * this.stepSize.z * (this.invert.z ? -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
|
// Update the current position in text boxes
|
||||||
this.updatePosition();
|
this.updatePosition();
|
||||||
// Look for autofocus plugin
|
|
||||||
this.updateAutofocusUri();
|
|
||||||
this.updateMoveInImageCoordinatesUri();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
|
@ -297,40 +282,32 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
handleSubmit: function() {
|
timeout(ms) {
|
||||||
this.moveRequest(
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
this.setPosition.x,
|
|
||||||
this.setPosition.y,
|
|
||||||
this.setPosition.z,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
|
async move(x, y, z, absolute) {
|
||||||
moveRequest: function(x, y, z, absolute) {
|
// Move the stage, by updating the controls and starting a move task
|
||||||
// If not movement-locked
|
// This is equivalent to clicking the "move" button.
|
||||||
if (!this.moveLock) {
|
if (this.moveLock) return; // Discard move requests if we're already moving
|
||||||
// Lock move requests
|
// NB moveLock is just boolean flag - it's not as safe as a "proper" lock.
|
||||||
this.moveLock = true;
|
this.moveLock = true; // This will also be set by the task submitter, but
|
||||||
let move_type = absolute ? "absolute" : "relative";
|
// setting it here avoids multiple moves being requested simultaneously.
|
||||||
// Send move request
|
if (absolute){
|
||||||
axios
|
this.setPosition = {"x": x, "y": y, "z": z}
|
||||||
.post(`${this.baseUri}/stage/move_${move_type}`, {
|
}else{
|
||||||
x: x,
|
await this.updatePosition();
|
||||||
y: y,
|
this.setPosition = {
|
||||||
z: z
|
"x": this.setPosition.x + x,
|
||||||
})
|
"y": this.setPosition.y + y,
|
||||||
.then(() => {
|
"z": this.setPosition.z + z
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
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) {
|
moveInImageCoordinatesRequest: function(x, y) {
|
||||||
// If not movement-locked
|
// If not movement-locked
|
||||||
if (!this.moveLock) {
|
if (!this.moveLock) {
|
||||||
|
|
@ -373,24 +350,58 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updatePosition: function() {
|
async updatePosition() {
|
||||||
axios
|
this.setPosition = await this.readThingProperty("stage", "position")
|
||||||
.get(this.positionStatusUri)
|
|
||||||
.then(response => {
|
|
||||||
this.setPosition = response.data;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.modalError(error); // Let mixin handle error
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateAutofocusUri: function() {
|
handleCaptureResponse: async function(response) {
|
||||||
this.fastAutofocusUri = `${this.baseUri}/autofocus/fast_autofocus`;
|
// Retrieve the captured image and save it
|
||||||
},
|
let imageUri = response.output.href;
|
||||||
|
if (!imageUri) {
|
||||||
updateMoveInImageCoordinatesUri: function() {
|
this.modalError("No image URI returned from capture task.");
|
||||||
this.moveInImageCoordinatesUri = `${this.baseUri}/camera_stage_mapping/move_in_image_coordinates`;
|
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>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
|
||||||
import CSMCalibrationSettings from "./CSMSettingsComponents/CSMCalibrationSettings.vue";
|
import CSMCalibrationSettings from "./CSMSettingsComponents/CSMCalibrationSettings.vue";
|
||||||
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
|
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
|
||||||
|
|
||||||
|
|
@ -31,122 +30,6 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
CSMCalibrationSettings,
|
CSMCalibrationSettings,
|
||||||
miniStreamDisplay
|
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>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="CSMCalibrationSettings">
|
<div id="CSMCalibrationSettings">
|
||||||
<!--Show auto calibrate if default plugin is enabled-->
|
<!--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
|
<taskSubmitter
|
||||||
:button-primary="true"
|
:button-primary="true"
|
||||||
:can-terminate="false"
|
:can-terminate="false"
|
||||||
|
|
@ -9,16 +9,15 @@
|
||||||
:confirmation-message="
|
:confirmation-message="
|
||||||
'Start recalibration of the stage to the camera? This may take a while, and the microscope will be locked during this time.'
|
'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'"
|
:submit-label="'Auto-Calibrate using camera'"
|
||||||
@response="onRecalibrateResponse"
|
@response="onRecalibrateResponse"
|
||||||
@error="modalError"
|
@error="modalError"
|
||||||
>
|
/>
|
||||||
</taskSubmitter>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="'get_calibration' in recalibrationLinks"
|
v-if="'last_calibration' in properties"
|
||||||
v-show="dataAvailable && showExtraSettings"
|
v-show="showExtraSettings"
|
||||||
type="button"
|
type="button"
|
||||||
class="uk-button uk-button-default uk-width-1-1"
|
class="uk-button uk-button-default uk-width-1-1"
|
||||||
@click="getCalibrationData()"
|
@click="getCalibrationData()"
|
||||||
|
|
@ -29,7 +28,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
|
||||||
import taskSubmitter from "../../../genericComponents/taskSubmitter";
|
import taskSubmitter from "../../../genericComponents/taskSubmitter";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
|
|
@ -48,107 +46,44 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
|
||||||
return {
|
|
||||||
settings: null,
|
|
||||||
recalibrationLinks: {},
|
|
||||||
isCalibrating: false,
|
|
||||||
dataAvailable: false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
settingsUri: function() {
|
actions() {
|
||||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
return this.$store.getters["wot/thingDescription"]("camera_stage_mapping")
|
||||||
|
.actions;
|
||||||
},
|
},
|
||||||
pluginsUri: function() {
|
properties() {
|
||||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
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: {
|
methods: {
|
||||||
updateSettings: function() {
|
getCalibrationData: async function() {
|
||||||
// Update links
|
try {
|
||||||
axios
|
let data = await this.readThingProperty(
|
||||||
.get(this.settingsUri)
|
"camera_stage_mapping",
|
||||||
.then(response => {
|
"last_calibration"
|
||||||
this.settings =
|
);
|
||||||
response.data.extensions["org.openflexure.camera_stage_mapping"];
|
if (data == {}) {
|
||||||
})
|
throw "No calibration data available.";
|
||||||
.catch(error => {
|
}
|
||||||
this.modalError(error); // Let mixin handle error
|
const dataStr = JSON.stringify(data);
|
||||||
this.settings = {};
|
const url = window.URL.createObjectURL(new Blob([dataStr]));
|
||||||
});
|
const link = document.createElement("a");
|
||||||
},
|
link.href = url;
|
||||||
|
link.setAttribute("download", "csm_calibration.json");
|
||||||
updateCalibrationDataAvailability: function() {
|
document.body.appendChild(link);
|
||||||
if ("get_calibration" in this.recalibrationLinks) {
|
link.click();
|
||||||
axios
|
} catch (error) {
|
||||||
.get(this.recalibrationLinks.get_calibration.href)
|
this.modalError(error); // Let mixin handle error
|
||||||
.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() {
|
onRecalibrateResponse: function() {
|
||||||
this.modalNotify("Finished stage-to-camera calibration.");
|
this.modalNotify("Finished stage-to-camera calibration.");
|
||||||
// Update local settings
|
|
||||||
this.updateSettings();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@
|
||||||
<div id="cameraSettings">
|
<div id="cameraSettings">
|
||||||
<div class="uk-grid uk-grid-divider uk-child-width-expand" uk-grid>
|
<div class="uk-grid uk-grid-divider uk-child-width-expand" uk-grid>
|
||||||
<div class="uk-width-large">
|
<div class="uk-width-large">
|
||||||
|
<h3>Automatic calibration</h3>
|
||||||
|
<cameraCalibrationSettings :camera-uri="cameraUri" />
|
||||||
|
|
||||||
<h3>Manual camera settings</h3>
|
<h3>Manual camera settings</h3>
|
||||||
<form @submit.prevent="applySettingsRequest">
|
<form @submit.prevent="applySettingsRequest">
|
||||||
<div class="uk-margin-small-bottom">
|
<div class="uk-margin-small-bottom">
|
||||||
|
|
@ -9,139 +12,47 @@
|
||||||
<li class="uk-open">
|
<li class="uk-open">
|
||||||
<a class="uk-accordion-title" href="#">Pi Camera Settings</a>
|
<a class="uk-accordion-title" href="#">Pi Camera Settings</a>
|
||||||
<div class="uk-accordion-content">
|
<div class="uk-accordion-content">
|
||||||
<div v-if="picamera.shutter_speed !== undefined">
|
<PropertyControl
|
||||||
<label class="uk-form-label" for="form-stacked-text"
|
label="Exposure time"
|
||||||
>Exposure time</label
|
property-name="exposure_time"
|
||||||
>
|
thing-name="camera"
|
||||||
<div class="uk-form-controls">
|
:read-back-delay="1000"
|
||||||
<input
|
/>
|
||||||
v-model="picamera.shutter_speed"
|
<PropertyControl
|
||||||
class="uk-input uk-form-small"
|
label="Analogue gain"
|
||||||
type="number"
|
property-name="analogue_gain"
|
||||||
/>
|
thing-name="camera"
|
||||||
</div>
|
:read-back-delay="1000"
|
||||||
</div>
|
/>
|
||||||
|
<PropertyControl
|
||||||
<div v-if="picamera.analog_gain !== undefined">
|
label="Colour gains"
|
||||||
<label class="uk-form-label" for="form-stacked-text"
|
property-name="colour_gains"
|
||||||
>Analogue gain</label
|
thing-name="camera"
|
||||||
>
|
:read-back-delay="1000"
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="uk-open">
|
<li class="uk-open">
|
||||||
<a class="uk-accordion-title" href="#">Image Quality</a>
|
<a class="uk-accordion-title" href="#">Image Quality</a>
|
||||||
<div class="uk-accordion-content">
|
<div class="uk-accordion-content">
|
||||||
<div class="uk-child-width-1-2" uk-grid>
|
<PropertyControl
|
||||||
<div v-if="jpeg_quality !== undefined">
|
label="MJPEG stream bit rate"
|
||||||
<label class="uk-form-label" for="form-stacked-text"
|
property-name="mjpeg_bitrate"
|
||||||
>JPEG capture quality (%)</label
|
thing-name="camera"
|
||||||
>
|
:read-back-delay="100"
|
||||||
<div class="uk-form-controls">
|
/>
|
||||||
<input
|
<PropertyControl
|
||||||
v-model="jpeg_quality"
|
label="MJPEG stream resolution"
|
||||||
class="uk-input uk-form-small"
|
property-name="stream_resolution"
|
||||||
type="number"
|
thing-name="camera"
|
||||||
step="1"
|
:read-back-delay="100"
|
||||||
/>
|
/>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
<!--
|
||||||
<li>
|
<li>
|
||||||
<a class="uk-accordion-title" href="#">Advanced</a>
|
<a class="uk-accordion-title" href="#">Advanced</a>
|
||||||
<div class="uk-accordion-content">
|
<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
|
<div
|
||||||
v-if="picamera.framerate !== undefined"
|
v-if="picamera.framerate !== undefined"
|
||||||
|
|
@ -166,27 +77,14 @@
|
||||||
This option sets the framerate of the Pi Camera video
|
This option sets the framerate of the Pi Camera video
|
||||||
encoder. The actual stream framerate is determined by
|
encoder. The actual stream framerate is determined by
|
||||||
network speed, and is limited by the encoder framerate.<br />
|
network speed, and is limited by the encoder framerate.<br />
|
||||||
<b
|
<b>Lowering framerate may impact fast-autofocus accuracy.</b>
|
||||||
>Lowering framerate may impact fast-autofocus
|
|
||||||
accuracy.</b
|
|
||||||
>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>-->
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="uk-button uk-button-primary uk-margin-small uk-width-1-1"
|
|
||||||
>
|
|
||||||
Apply Settings
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h3>Automatic calibration</h3>
|
|
||||||
<cameraCalibrationSettings></cameraCalibrationSettings>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="mini-stream">
|
<div id="mini-stream">
|
||||||
|
|
@ -197,9 +95,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
|
||||||
import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue";
|
import cameraCalibrationSettings from "./cameraSettingsComponents/cameraCalibrationSettings.vue";
|
||||||
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
|
import miniStreamDisplay from "../../genericComponents/miniStreamDisplay.vue";
|
||||||
|
import PropertyControl from "../../labThingsComponents/propertyControl.vue";
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -207,7 +105,8 @@ export default {
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
cameraCalibrationSettings,
|
cameraCalibrationSettings,
|
||||||
miniStreamDisplay
|
miniStreamDisplay,
|
||||||
|
PropertyControl
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
data: function() {
|
||||||
|
|
@ -242,74 +141,8 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
settingsUri: function() {
|
cameraUri: function() {
|
||||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
return `${this.$store.getters.baseUri}/camera/`;
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,52 +1,43 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<!--Show auto calibrate if default plugin is enabled-->
|
<!--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
|
<taskSubmitter
|
||||||
:can-terminate="false"
|
:can-terminate="false"
|
||||||
:requires-confirmation="true"
|
:requires-confirmation="true"
|
||||||
:confirmation-message="
|
:confirmation-message="
|
||||||
'Start recalibration? This may take a while, and the microscope will be locked during this time.'
|
'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'"
|
:submit-label="'Full Auto-Calibrate'"
|
||||||
@response="onRecalibrateResponse"
|
@response="onRecalibrateResponse"
|
||||||
@error="modalError"
|
@error="modalError"
|
||||||
>
|
>
|
||||||
</taskSubmitter>
|
</taskSubmitter>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="'auto_expose_from_minimum' in actions" class="uk-margin-small">
|
||||||
v-if="'auto_exposure_from_raw' in recalibrationLinks"
|
|
||||||
class="uk-margin-small"
|
|
||||||
>
|
|
||||||
<taskSubmitter
|
<taskSubmitter
|
||||||
:can-terminate="false"
|
:can-terminate="false"
|
||||||
:requires-confirmation="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'"
|
:submit-label="'Auto gain & shutter speed'"
|
||||||
@response="onRecalibrateResponse"
|
@response="onRecalibrateResponse"
|
||||||
@error="modalError"
|
@error="modalError"
|
||||||
>
|
>
|
||||||
</taskSubmitter>
|
</taskSubmitter>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="'calibrate_white_balance' in actions" class="uk-margin-small">
|
||||||
v-if="'auto_white_balance_from_raw' in recalibrationLinks"
|
|
||||||
class="uk-margin-small"
|
|
||||||
>
|
|
||||||
<taskSubmitter
|
<taskSubmitter
|
||||||
:can-terminate="false"
|
:can-terminate="false"
|
||||||
:requires-confirmation="false"
|
:requires-confirmation="false"
|
||||||
:submit-url="recalibrationLinks.auto_white_balance_from_raw.href"
|
:submit-url="cameraUri + 'calibrate_white_balance'"
|
||||||
:submit-label="'Auto white balance'"
|
:submit-label="'Auto white balance'"
|
||||||
@response="onRecalibrateResponse"
|
@response="onRecalibrateResponse"
|
||||||
@error="modalError"
|
@error="modalError"
|
||||||
>
|
>
|
||||||
</taskSubmitter>
|
</taskSubmitter>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div v-if="'calibrate_lens_shading' in actions" class="uk-margin-small">
|
||||||
v-if="'auto_lens_shading_table' in recalibrationLinks"
|
|
||||||
class="uk-margin-small"
|
|
||||||
>
|
|
||||||
<taskSubmitter
|
<taskSubmitter
|
||||||
:can-terminate="false"
|
:can-terminate="false"
|
||||||
:requires-confirmation="true"
|
:requires-confirmation="true"
|
||||||
|
|
@ -54,7 +45,7 @@
|
||||||
'Is the microscope looking at an evenly illuminated, empty field of view? ' +
|
'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.'
|
'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'"
|
:submit-label="'Auto flat field correction'"
|
||||||
@response="onRecalibrateResponse"
|
@response="onRecalibrateResponse"
|
||||||
@error="modalError"
|
@error="modalError"
|
||||||
|
|
@ -62,23 +53,35 @@
|
||||||
</taskSubmitter>
|
</taskSubmitter>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="showExtraSettings" class="uk-child-width-expand">
|
<div
|
||||||
<button
|
v-show="showExtraSettings"
|
||||||
v-if="'flatten_lens_shading_table' in recalibrationLinks"
|
v-if="'flatten_lens_shading_table' in actions"
|
||||||
class="uk-button uk-button-danger uk-width-1-1"
|
class="uk-child-width-expand"
|
||||||
@click="flattenLensShadingTableRequest"
|
>
|
||||||
|
<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
|
</taskSubmitter>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
<div v-if="LstDownloadEnabled">
|
v-show="showExtraSettings"
|
||||||
<a
|
v-if="'reset_lens_shading' in actions"
|
||||||
class="uk-button uk-button-default uk-width-large uk-margin-small-top uk-align-center"
|
class="uk-child-width-expand"
|
||||||
:href="LstDownloadUri"
|
>
|
||||||
download
|
<taskSubmitter
|
||||||
>Download Lens-Shading Table</a
|
: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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -100,66 +103,34 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
cameraUri: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
data: function() {
|
data: function() {
|
||||||
return {
|
return {
|
||||||
recalibrationLinks: {},
|
actions: {},
|
||||||
isCalibrating: false,
|
isCalibrating: false,
|
||||||
LstDownloadEnabled: 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() {
|
mounted() {
|
||||||
this.updateRecalibrationLinks();
|
this.updateActions();
|
||||||
this.checkLstDownload();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
updateRecalibrationLinks: function() {
|
updateActions: async function() {
|
||||||
axios
|
try {
|
||||||
.get(this.pluginsUri) // Get a list of plugins
|
let response = await axios.get(this.cameraUri); // Get the thing description
|
||||||
.then(response => {
|
let td = response.data;
|
||||||
var plugins = response.data;
|
this.actions = td.actions;
|
||||||
var foundExtension = plugins.find(
|
} catch (error) {
|
||||||
e => e.title === "org.openflexure.calibration.picamera"
|
this.modalError(error); // Let mixin handle error
|
||||||
);
|
}
|
||||||
// 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
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onRecalibrateResponse: function() {
|
onRecalibrateResponse: function() {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="appSettings" class="uk-width-large">
|
<div id="appSettings" class="uk-width-large">
|
||||||
<h3>Additional features</h3>
|
<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 }">
|
<p class="uk-margin-small" :class="{ 'uk-text-muted': !imjoyPermitted }">
|
||||||
<label :disabled="!imjoyPermitted">
|
<label :disabled="!imjoyPermitted">
|
||||||
<input
|
<input
|
||||||
|
|
@ -57,14 +44,6 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
IHIEnabled: {
|
|
||||||
get() {
|
|
||||||
return this.$store.state.IHIEnabled;
|
|
||||||
},
|
|
||||||
set(value) {
|
|
||||||
this.$store.commit("changeIHIEnabled", value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
imjoyEnabled: {
|
imjoyEnabled: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.state.imjoyEnabled;
|
return this.$store.state.imjoyEnabled;
|
||||||
|
|
@ -90,9 +69,6 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
IHIEnabled: function() {
|
|
||||||
this.setLocalStorageObj("IHIEnabled", this.IHIEnabled);
|
|
||||||
},
|
|
||||||
imjoyEnabled: function() {
|
imjoyEnabled: function() {
|
||||||
this.setLocalStorageObj("imjoyEnabled", this.imjoyEnabled);
|
this.setLocalStorageObj("imjoyEnabled", this.imjoyEnabled);
|
||||||
},
|
},
|
||||||
|
|
@ -103,7 +79,6 @@ export default {
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
// Try loading settings from localStorage. If null, don't change.
|
// Try loading settings from localStorage. If null, don't change.
|
||||||
this.IHIEnabled = this.getLocalStorageObj("IHIEnabled") || this.IHIEnabled;
|
|
||||||
this.imjoyEnabled =
|
this.imjoyEnabled =
|
||||||
this.getLocalStorageObj("imjoyEnabled") || this.imjoyEnabled;
|
this.getLocalStorageObj("imjoyEnabled") || this.imjoyEnabled;
|
||||||
this.galleryEnabled =
|
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>
|
<template>
|
||||||
<div id="stageSettings" class="uk-width-large">
|
<div id="stageSettings" class="uk-width-large">
|
||||||
<div v-if="stageType === 'MissingStage'" class="uk-text-danger">
|
The microscope stage is a <b>{{ stageType }}</b
|
||||||
<b>No stage connected</b>
|
>. There are no settings to adjust here: to set the geometry of your stage
|
||||||
</div>
|
(i.e. switch between delta and cartesian), you will need to adjust the
|
||||||
<div v-else>
|
configuration of your microscope, which cannot be done while it is running.
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "StageSettings",
|
name: "StageSettings",
|
||||||
|
|
||||||
components: {},
|
components: {},
|
||||||
|
|
||||||
data: function() {
|
data: function() {
|
||||||
return {
|
return {};
|
||||||
stageType: "MissingStage",
|
|
||||||
backlash: {
|
|
||||||
x: undefined,
|
|
||||||
y: undefined,
|
|
||||||
z: undefined
|
|
||||||
},
|
|
||||||
settle_time: undefined
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
settingsUri: function() {
|
stageType: function() {
|
||||||
return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
|
return this.thingDescription("stage").title;
|
||||||
},
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -70,19 +70,6 @@
|
||||||
Camera/stage mapping
|
Camera/stage mapping
|
||||||
</tabIcon>
|
</tabIcon>
|
||||||
</li>
|
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="view-component uk-width-expand uk-padding-small">
|
<div class="view-component uk-width-expand uk-padding-small">
|
||||||
|
|
@ -136,24 +123,12 @@
|
||||||
<CSMSettings />
|
<CSMSettings />
|
||||||
</div>
|
</div>
|
||||||
</tabContent>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import streamSettings from "./settingsComponents/streamSettings.vue";
|
import streamSettings from "./settingsComponents/streamSettings.vue";
|
||||||
import microscopeSettings from "./settingsComponents/microscopeSettings.vue";
|
|
||||||
import cameraSettings from "./settingsComponents/cameraSettings.vue";
|
import cameraSettings from "./settingsComponents/cameraSettings.vue";
|
||||||
import appSettings from "./settingsComponents/appSettings.vue";
|
import appSettings from "./settingsComponents/appSettings.vue";
|
||||||
import featuresSettings from "./settingsComponents/featuresSettings.vue";
|
import featuresSettings from "./settingsComponents/featuresSettings.vue";
|
||||||
|
|
@ -171,7 +146,6 @@ export default {
|
||||||
streamSettings,
|
streamSettings,
|
||||||
cameraSettings,
|
cameraSettings,
|
||||||
stageSettings,
|
stageSettings,
|
||||||
microscopeSettings,
|
|
||||||
CSMSettings,
|
CSMSettings,
|
||||||
appSettings,
|
appSettings,
|
||||||
featuresSettings,
|
featuresSettings,
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import axios from "axios";
|
|
||||||
import taskSubmitter from "../../genericComponents/taskSubmitter";
|
import taskSubmitter from "../../genericComponents/taskSubmitter";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -166,9 +165,6 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
pluginsUri: function() {
|
|
||||||
return `${this.$store.getters.baseUri}/api/v2/extensions`;
|
|
||||||
},
|
|
||||||
currentTimeForm: {
|
currentTimeForm: {
|
||||||
get() {
|
get() {
|
||||||
// Chop the timezone information from the end
|
// Chop the timezone information from the end
|
||||||
|
|
@ -210,30 +206,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted: function() {
|
|
||||||
this.updateScanUri();
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
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) {
|
onScanError: function(error) {
|
||||||
this.scanRunning = false;
|
this.scanRunning = false;
|
||||||
this.modalError(error);
|
this.modalError(error);
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,6 @@ export default {
|
||||||
},
|
},
|
||||||
streamImgUri: function() {
|
streamImgUri: function() {
|
||||||
return `${this.$store.getters.baseUri}/camera/mjpeg_stream`;
|
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 Vue from "vue";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import store from "./store";
|
import store from "./store";
|
||||||
|
import axios from "axios";
|
||||||
import UIkit from "uikit";
|
import UIkit from "uikit";
|
||||||
import VueTour from "vue-tour";
|
import VueTour from "vue-tour";
|
||||||
import VueFriendlyIframe from "vue-friendly-iframe";
|
import VueFriendlyIframe from "vue-friendly-iframe";
|
||||||
|
|
@ -34,6 +35,49 @@ Vue.config.productionTip = false;
|
||||||
|
|
||||||
Vue.mixin({
|
Vue.mixin({
|
||||||
methods: {
|
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) {
|
modalConfirm: function(modalText) {
|
||||||
var context = this;
|
var context = this;
|
||||||
|
|
||||||
|
|
@ -86,6 +130,7 @@ Vue.mixin({
|
||||||
modalError: function(error) {
|
modalError: function(error) {
|
||||||
var errormsg = this.getErrorMessage(error);
|
var errormsg = this.getErrorMessage(error);
|
||||||
this.$store.commit("setErrorMessage", errormsg);
|
this.$store.commit("setErrorMessage", errormsg);
|
||||||
|
console.log("Modal error:", error);
|
||||||
UIkit.notification({
|
UIkit.notification({
|
||||||
message: `${errormsg}`,
|
message: `${errormsg}`,
|
||||||
status: "danger"
|
status: "danger"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import Vuex from "vuex";
|
import Vuex from "vuex";
|
||||||
|
import wotStoreModule from "./wot-client";
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
|
|
@ -76,7 +77,8 @@ function getOriginFromLocation() {
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
imjoy: moduleImjoy
|
imjoy: moduleImjoy,
|
||||||
|
wot: wotStoreModule
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
origin: getOriginFromLocation(),
|
origin: getOriginFromLocation(),
|
||||||
|
|
@ -86,7 +88,6 @@ export default new Vuex.Store({
|
||||||
disableStream: false,
|
disableStream: false,
|
||||||
autoGpuPreview: false,
|
autoGpuPreview: false,
|
||||||
trackWindow: true,
|
trackWindow: true,
|
||||||
IHIEnabled: false,
|
|
||||||
imjoyEnabled: false,
|
imjoyEnabled: false,
|
||||||
galleryEnabled: true,
|
galleryEnabled: true,
|
||||||
appTheme: "system",
|
appTheme: "system",
|
||||||
|
|
@ -112,9 +113,6 @@ export default new Vuex.Store({
|
||||||
changeAppTheme(state, theme) {
|
changeAppTheme(state, theme) {
|
||||||
state.appTheme = theme;
|
state.appTheme = theme;
|
||||||
},
|
},
|
||||||
changeIHIEnabled(state, enabled) {
|
|
||||||
state.IHIEnabled = enabled;
|
|
||||||
},
|
|
||||||
changeImjoyEnabled(state, enabled) {
|
changeImjoyEnabled(state, enabled) {
|
||||||
if (process.env.VUE_APP_ENABLE_IMJOY === "true") {
|
if (process.env.VUE_APP_ENABLE_IMJOY === "true") {
|
||||||
state.imjoyEnabled = enabled;
|
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 = {
|
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