diff --git a/README.md b/README.md index 78777586..15734630 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The "server" is the main component of the OpenFlexure Microscope's software. It is responsible for controlling microscope hardware, data management, and allowing it to be controlled locally and over a network. This repository now includes the web client, which is served from the root of the Python web server. -This software runs on [Python-LabThings](https://github.com/labthings/python-labthings/), and so most non-microscope functionality is handled by that library. +This software runs on [LabThings-FastAPI](https://github.com/rwb27/labthings-fastapi/), and so most non-microscope functionality is handled by that library. ## Getting started @@ -62,18 +62,19 @@ To set up a development version of the software (most likely using emulated came * `cd openflexure-microscope-server` ### Set up the Python environment and run a test server -* (Optional) Set local Python version to match what is available on the Pi - * `pyenv init` (this may or may not be required, depending on how you installed `pyenv`) - * `pyenv install 3.7.3` - * `pyenv local 3.7.3` +* (Optional) Set local Python version to 3.11 * Create a virtual environment and activate it: * `python -m venv .venv` * `source .venv/bin/activate` (on Linux) or `.venv/Scripts/activate` (on Windows) - * `pip install --upgrade pip wheel pipenv` - * `pipenv install --dev` (This will install development dependencies. If you don't need these, `pipenv install` will get you just the dependencies needed to run the server, which takes about half the time.) -* Finally, run the server: - * You can use `ofm serve` or `ofm restart` on the Raspberry Pi to manage the server. - * To run the server locally, with dummy hardware, you can use `python -m openflexure_microscope.api.app` to start a development-mode Flask server on `localhost:5000` + * `pip install -e .[dev]` (This will install development dependencies. If you don't need these, there is probably a simpler way to run the server than cloning this repo.) +* Finally, run the server: currently you can do this with `sudo systemctl start openflexure-microscope-server` if it's pre-installed on a Raspberry Pi, or `uvicorn --port 5000 openflexure_microscope_server.server:app` to run locally. + +### Run the server manually on a Raspberry Pi +``` +cd /var/openflexure +sudo -u openflexure-ws PATH="/var/openflexure/application/openflexure-microscope-server/.venv/bin/:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin" /var/openflexure/application/openflexure-microscope-server/.venv/bin/uvicorn --reload --reload-dir=./application --host 0.0.0.0 --port 5000 openflexure_microscope_server.server:app +``` + ### Set up the Javascript environment and build * The Flask web application, written in Python, serves a web application written in `Vue.js`. This is distributed as part of the built version of the server, hosted on our [build server](https://build.openflexure.org/openflexure-microscope-server/). diff --git a/webapp/src/App.vue b/webapp/src/App.vue index 6e63fcd6..e1234a92 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -42,7 +42,6 @@ import appContent from "./components/appContent.vue"; import loadingContent from "./components/loadingContent.vue"; -import axios from "axios"; var Mousetrap = require("mousetrap"); Mousetrap.prototype.stopCallback = function(e, element) { @@ -305,24 +304,31 @@ export default { }, methods: { - checkConnection: function() { + async checkConnection() { var baseUri = this.$store.getters.baseUri; this.$store.commit("changeWaiting", true); - axios - // TODO: more robust check - e.g. use a microscope Thing - .get(`${baseUri}/stage/`) - .then(() => { - this.$store.commit("setConnected"); - this.$store.commit("setErrorMessage", null); - }) - .catch(error => { - this.$store.commit("setErrorMessage", error); - }) - .finally(() => { - this.$store.commit("changeWaiting", false); - }); + // TODO: more robust check - e.g. use a microscope Thing + // TODO: should we purge existing consumedThings? + try { + await this.$store.dispatch( + "wot/fetchThingDescriptions", + `${baseUri}/thing_descriptions/` + ); + for (let requiredThing of ["camera", "stage"]) { + if (!this.$store.getters["wot/thingAvailable"](requiredThing)) { + throw new Error( + `No ${requiredThing} found, the GUI won't work without one.` + ); + } + } + this.$store.commit("setConnected"); + this.$store.commit("setErrorMessage", null); + } catch (error) { + this.$store.commit("setErrorMessage", error); + } finally { + this.$store.commit("changeWaiting", false); + } }, - handleExit: function() { this.$root.$emit("globalTogglePreview", false); }, diff --git a/webapp/src/components/appContent.vue b/webapp/src/components/appContent.vue index 20def127..63cebb0a 100644 --- a/webapp/src/components/appContent.vue +++ b/webapp/src/components/appContent.vue @@ -231,10 +231,6 @@ export default { }, computed: { - pluginsUri: function() { - return `${this.$store.getters.baseUri}/api/v2/extensions`; - }, - pluginsGuiList: function() { // List of plugin GUIs, obtained from this.plugins values var pluginGuis = []; @@ -267,34 +263,26 @@ export default { icon: "visibility", component: viewContent }, - { + /*{ id: "gallery", icon: "photo_library", component: galleryContent, divide: true // Add a divider after this tab icon - }, + },*/ { id: "navigate", icon: "gamepad", component: navigateContent }, { - id: "capture", - icon: "camera_alt", - component: captureContent, - divide: true // Add a divider after this tab icon - } - ]; - if (!this.$store.state.galleryEnabled) { - tabs = tabs.filter(tab => tab.id != "gallery"); - } - if (this.$store.state.IHIEnabled) { - tabs.push({ id: "slidescan", icon: "settings_overscan", component: slideScanContent, divide: true - }); + } + ]; + if (!this.$store.state.galleryEnabled) { + tabs = tabs.filter(tab => tab.id != "gallery"); } if (this.$store.state.imjoyEnabled) { tabs.push({ @@ -351,9 +339,9 @@ export default { .catch(error => { this.modalError(error); // Let mixin handle error });*/ - return new Promise((resolve) => { - resolve({}); - }); + return new Promise(resolve => { + resolve({}); + }); }, setTab: function(event, tab) { if (!(this.currentTab == tab)) { diff --git a/webapp/src/components/genericComponents/taskSubmitter.vue b/webapp/src/components/genericComponents/taskSubmitter.vue index 0c7ae4d2..ca316568 100644 --- a/webapp/src/components/genericComponents/taskSubmitter.vue +++ b/webapp/src/components/genericComponents/taskSubmitter.vue @@ -142,7 +142,10 @@ export default { if (task.status == "pending" || task.status == "running") { this.taskStarted = true; this.$emit("taskStarted", this.taskId); - this.startPolling(task.id, task.links.find(t => t.rel==self).href); + this.startPolling( + task.id, + task.links.find(t => t.rel == self).href + ); } } }); @@ -219,33 +222,32 @@ export default { var checkCondition = (resolve, reject) => { // If the condition is met, we're done! - axios.get( - this.taskUrl, - {baseURL: this.$store.getters.baseUri} - ).then(response => { - var result = response.data.status; - // If the task ends with success - if (result == "completed") { - resolve(response.data); - } - // If task ends with an error - else if (result == "error") { - // Pass the error string back with reject + axios + .get(this.taskUrl, { baseURL: this.$store.getters.baseUri }) + .then(response => { + var result = response.data.status; + // If the task ends with success + if (result == "completed") { + resolve(response.data); + } + // If task ends with an error + else if (result == "error") { + // Pass the error string back with reject - reject(new Error(response.data.output)); - } - // If task ends with termination - else if (result == "cancelled") { - // Pass a generic termination error back with reject + reject(new Error(response.data.output)); + } + // If task ends with termination + else if (result == "cancelled") { + // Pass a generic termination error back with reject - reject(new Error("Task cancelled")); - } else { - // Since the task is still running, we can update the progress bar - this.progress = response.data.progress; - // Check again after timeout - setTimeout(checkCondition, interval, resolve, reject); - } - }); + reject(new Error("Task cancelled")); + } else { + // Since the task is still running, we can update the progress bar + this.progress = response.data.progress; + // Check again after timeout + setTimeout(checkCondition, interval, resolve, reject); + } + }); }; return new Promise(checkCondition); diff --git a/webapp/src/components/labThingsComponents/propertyControl.vue b/webapp/src/components/labThingsComponents/propertyControl.vue new file mode 100644 index 00000000..d809f791 --- /dev/null +++ b/webapp/src/components/labThingsComponents/propertyControl.vue @@ -0,0 +1,238 @@ + + + + + diff --git a/webapp/src/components/modalComponents/calibrationModal.vue b/webapp/src/components/modalComponents/calibrationModal.vue index fab205bf..6ecc9d55 100644 --- a/webapp/src/components/modalComponents/calibrationModal.vue +++ b/webapp/src/components/modalComponents/calibrationModal.vue @@ -48,6 +48,7 @@ @@ -146,8 +147,6 @@ + + + diff --git a/webapp/src/components/tabContentComponents/settingsComponents/CSMSettings.vue b/webapp/src/components/tabContentComponents/settingsComponents/CSMSettings.vue index d8c4be79..adf0c863 100644 --- a/webapp/src/components/tabContentComponents/settingsComponents/CSMSettings.vue +++ b/webapp/src/components/tabContentComponents/settingsComponents/CSMSettings.vue @@ -20,7 +20,6 @@ diff --git a/webapp/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue b/webapp/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue index a7280c1f..db5fc10b 100644 --- a/webapp/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue +++ b/webapp/src/components/tabContentComponents/settingsComponents/CSMSettingsComponents/CSMCalibrationSettings.vue @@ -1,7 +1,7 @@