Merge branch 'master' of https://gitlab.com/openflexure/openflexure-microscope-server
This commit is contained in:
commit
2ce58f2aee
10 changed files with 24 additions and 232 deletions
|
|
@ -37,8 +37,8 @@ For example, if you are creating an API route, in which you expect parameters ``
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from labthings.server.schema import Schema
|
from labthings.schema import Schema
|
||||||
from labthings.server import fields
|
from labthings import fields
|
||||||
|
|
||||||
class UserSchema(Schema):
|
class UserSchema(Schema):
|
||||||
name = fields.String(required=True)
|
name = fields.String(required=True)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
Basic extension structure
|
Basic extension structure
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
An extension starts as a simple instance of :py:class:`labthings.server.extensions.BaseExtension`.
|
An extension starts as a simple instance of :py:class:`labthings.extensions.BaseExtension`.
|
||||||
Each extension is described by a single ``BaseExtension`` instance, containing any number of methods, API views, and additional hardware components.
|
Each extension is described by a single ``BaseExtension`` instance, containing any number of methods, API views, and additional hardware components.
|
||||||
|
|
||||||
In order to access the currently running microscope object, use the :py:func:`labthings.server.find.find_component` function, with the argument ``"org.openflexure.microscope"``. Likewise, any new components attached by other extensions can be found using their full name, as above.
|
In order to access the currently running microscope object, use the :py:func:`labthings.find_component` function, with the argument ``"org.openflexure.microscope"``. Likewise, any new components attached by other extensions can be found using their full name, as above.
|
||||||
|
|
||||||
A simple extension file, with no API views but application-available methods may look like:
|
A simple extension file, with no API views but application-available methods may look like:
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ Once this extension is loaded, any other extensions will have access to your met
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from labthings.server.find import find_extension
|
from labthings import find_extension
|
||||||
|
|
||||||
def test_extension_method():
|
def test_extension_method():
|
||||||
# Find your extension. Returns None if it hasn't been found.
|
# Find your extension. Returns None if it hasn't been found.
|
||||||
|
|
@ -29,7 +29,7 @@ Once this extension is loaded, any other extensions will have access to your met
|
||||||
Subclassing ``BaseExtension``
|
Subclassing ``BaseExtension``
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
The syntax used above allows novice programmers to easily start building extensions, without having to deal with subclassing. However, for more complex extensions which require persistent state, subclassing :py:class:`labthings.server.extensions.BaseExtension` is recommended.
|
The syntax used above allows novice programmers to easily start building extensions, without having to deal with subclassing. However, for more complex extensions which require persistent state, subclassing :py:class:`labthings.extensions.BaseExtension` is recommended.
|
||||||
|
|
||||||
The same simple extension as seen above can be written using subclassing:
|
The same simple extension as seen above can be written using subclassing:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ An example of a long running task may look like:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
...
|
...
|
||||||
from labthings.server.view import ActionView
|
from labthings import ActionView
|
||||||
|
|
||||||
class SlowAPI(ActionView):
|
class SlowAPI(ActionView):
|
||||||
def post(self):
|
def post(self):
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,8 @@ class ZipObjectDescription:
|
||||||
def close(self):
|
def close(self):
|
||||||
logging.debug(self.fp.name)
|
logging.debug(self.fp.name)
|
||||||
self.fp.close()
|
self.fp.close()
|
||||||
os.unlink(self.fp.name)
|
if os.path.exists(self.fp.name):
|
||||||
|
os.unlink(self.fp.name)
|
||||||
|
|
||||||
assert not os.path.exists(self.fp.name)
|
assert not os.path.exists(self.fp.name)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,209 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
class="progress uk-margin-top uk-margin-horizontal-remove uk-padding-remove"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="progress"
|
|
||||||
class="determinate"
|
|
||||||
:style="barWidthFromProgress"
|
|
||||||
></div>
|
|
||||||
<div v-else class="indeterminate"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="uk-button uk-button-danger uk-form-small uk-float-right uk-width-1-1"
|
|
||||||
@click="terminateTask()"
|
|
||||||
>
|
|
||||||
Terminate
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "TaskProgress",
|
|
||||||
|
|
||||||
props: {
|
|
||||||
taskId: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
pollInterval: {
|
|
||||||
type: Number,
|
|
||||||
required: false,
|
|
||||||
default: 500
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data: function() {
|
|
||||||
return {
|
|
||||||
polling: null,
|
|
||||||
progress: null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
barWidthFromProgress: function() {
|
|
||||||
var progress = this.progress <= 100 ? this.progress : 100;
|
|
||||||
var styleString = `width: ${progress}%`;
|
|
||||||
return styleString;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
created() {
|
|
||||||
this.polling = setInterval(() => {
|
|
||||||
this.pollProgress();
|
|
||||||
}, this.pollInterval);
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeDestroy() {
|
|
||||||
clearInterval(this.polling);
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
pollProgress: function() {
|
|
||||||
console.log("Starting progress polling");
|
|
||||||
axios
|
|
||||||
.get(`${this.$store.getters.uriV2}/tasks/${this.taskId}`)
|
|
||||||
.then(response => {
|
|
||||||
console.log("PROGRESS RESPONSE: ", response.data.progress);
|
|
||||||
this.progress = response.data.progress;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
terminateTask: function() {
|
|
||||||
axios
|
|
||||||
.delete(`${this.$store.getters.uriV2}/tasks/${this.taskId}`)
|
|
||||||
.then(response => {
|
|
||||||
console.log("TERMINATION RESPONSE: ", response.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
@import "../../assets/less/theme.less";
|
|
||||||
|
|
||||||
.progress {
|
|
||||||
position: relative;
|
|
||||||
height: 5px;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
background-color: rgba(180, 180, 180, 0.15);
|
|
||||||
border-radius: 2px;
|
|
||||||
background-clip: padding-box;
|
|
||||||
margin: 0.5rem 0 1rem 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress .determinate {
|
|
||||||
position: absolute;
|
|
||||||
background-color: inherit;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
transition: width 0.3s linear;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress .indeterminate,
|
|
||||||
.progress .determinate {
|
|
||||||
background-color: @global-primary-background;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hook-inverse() {
|
|
||||||
.progress .indeterminate,
|
|
||||||
.progress .determinate {
|
|
||||||
background-color: @inverse-primary-muted-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress .indeterminate:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
background-color: inherit;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
will-change: left, right;
|
|
||||||
-webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395)
|
|
||||||
infinite;
|
|
||||||
animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress .indeterminate:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
background-color: inherit;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
will-change: left, right;
|
|
||||||
-webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)
|
|
||||||
infinite;
|
|
||||||
animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1)
|
|
||||||
infinite;
|
|
||||||
-webkit-animation-delay: 1.15s;
|
|
||||||
animation-delay: 1.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes indeterminate {
|
|
||||||
0% {
|
|
||||||
left: -35%;
|
|
||||||
right: 100%;
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
left: 100%;
|
|
||||||
right: -90%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
left: 100%;
|
|
||||||
right: -90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes indeterminate {
|
|
||||||
0% {
|
|
||||||
left: -35%;
|
|
||||||
right: 100%;
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
left: 100%;
|
|
||||||
right: -90%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
left: 100%;
|
|
||||||
right: -90%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@-webkit-keyframes indeterminate-short {
|
|
||||||
0% {
|
|
||||||
left: -200%;
|
|
||||||
right: 100%;
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
left: 107%;
|
|
||||||
right: -8%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
left: 107%;
|
|
||||||
right: -8%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes indeterminate-short {
|
|
||||||
0% {
|
|
||||||
left: -200%;
|
|
||||||
right: 100%;
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
left: 107%;
|
|
||||||
right: -8%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
left: 107%;
|
|
||||||
right: -8%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -90,6 +90,7 @@ export default {
|
||||||
data: function() {
|
data: function() {
|
||||||
return {
|
return {
|
||||||
taskId: null,
|
taskId: null,
|
||||||
|
taskUrl: null,
|
||||||
polling: null,
|
polling: null,
|
||||||
progress: null,
|
progress: null,
|
||||||
taskStarted: false,
|
taskStarted: false,
|
||||||
|
|
@ -150,6 +151,7 @@ export default {
|
||||||
// Fetch the task ID
|
// Fetch the task ID
|
||||||
console.log("Task ID: " + response.data.id);
|
console.log("Task ID: " + response.data.id);
|
||||||
this.taskId = response.data.id;
|
this.taskId = response.data.id;
|
||||||
|
this.taskUrl = response.data.href;
|
||||||
// Start the store polling TaskId for success
|
// Start the store polling TaskId for success
|
||||||
this.taskRunning = true;
|
this.taskRunning = true;
|
||||||
this.$emit("taskRunning", this.taskId);
|
this.$emit("taskRunning", this.taskId);
|
||||||
|
|
@ -192,12 +194,12 @@ 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
|
axios
|
||||||
.get(`${this.$store.getters.uriV2}/tasks/${taskId}`)
|
.get(this.taskUrl)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
console.log(response.data.status);
|
console.log(response.data.status);
|
||||||
var result = response.data.status;
|
var result = response.data.status;
|
||||||
// If the task ends with success
|
// If the task ends with success
|
||||||
if (result == "success") {
|
if (result == "completed") {
|
||||||
resolve(response.data);
|
resolve(response.data);
|
||||||
}
|
}
|
||||||
// If task ends with an error
|
// If task ends with an error
|
||||||
|
|
@ -206,16 +208,10 @@ export default {
|
||||||
reject(new Error(response.data.return));
|
reject(new Error(response.data.return));
|
||||||
}
|
}
|
||||||
// If task ends with termination
|
// If task ends with termination
|
||||||
else if (result == "terminated") {
|
else if (result == "cancelled") {
|
||||||
// Pass a generic termination error back with reject
|
// Pass a generic termination error back with reject
|
||||||
reject(new Error("Task terminated"));
|
reject(new Error("Task cancelled"));
|
||||||
}
|
}
|
||||||
// If task ends in any other way
|
|
||||||
else if (result != "running") {
|
|
||||||
// Pass status string as error
|
|
||||||
reject(new Error(result));
|
|
||||||
}
|
|
||||||
// Didn't match and too much time, reject!
|
|
||||||
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;
|
||||||
|
|
@ -232,7 +228,7 @@ export default {
|
||||||
console.log("Starting progress polling");
|
console.log("Starting progress polling");
|
||||||
|
|
||||||
axios
|
axios
|
||||||
.get(`${this.$store.getters.uriV2}/tasks/${this.taskId}`)
|
.get(this.taskUrl)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
console.log("PROGRESS RESPONSE: ", response.data.progress);
|
console.log("PROGRESS RESPONSE: ", response.data.progress);
|
||||||
this.progress = response.data.progress;
|
this.progress = response.data.progress;
|
||||||
|
|
@ -241,7 +237,7 @@ export default {
|
||||||
|
|
||||||
terminateTask: function() {
|
terminateTask: function() {
|
||||||
axios
|
axios
|
||||||
.delete(`${this.$store.getters.uriV2}/tasks/${this.taskId}`)
|
.delete(this.taskUrl)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
console.log("TERMINATION RESPONSE: ", response.data);
|
console.log("TERMINATION RESPONSE: ", response.data);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
onResponse: function(response) {
|
onResponse: function(response) {
|
||||||
this.lastSessionId = response.return.id;
|
this.lastSessionId = response.output.id;
|
||||||
this.downloadUrl = `${this.zipGetterUri}/${this.lastSessionId}`;
|
this.downloadUrl = `${this.zipGetterUri}/${this.lastSessionId}`;
|
||||||
this.downloadReady = true;
|
this.downloadReady = true;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,8 @@ class MissingCamera(BaseCamera):
|
||||||
|
|
||||||
if isinstance(output, str):
|
if isinstance(output, str):
|
||||||
output.close()
|
output.close()
|
||||||
|
else:
|
||||||
|
output.flush()
|
||||||
|
|
||||||
# HANDLE STREAM FRAMES
|
# HANDLE STREAM FRAMES
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ class CaptureObject(object):
|
||||||
|
|
||||||
# Store a nice ID
|
# Store a nice ID
|
||||||
self.id = uuid.uuid4() #: str: Unique capture ID
|
self.id = uuid.uuid4() #: str: Unique capture ID
|
||||||
logging.debug("Created StreamObject {}".format(self.id))
|
logging.debug("Created CaptureObject {}".format(self.id))
|
||||||
self.datetime = datetime.datetime.now()
|
self.datetime = datetime.datetime.now()
|
||||||
|
|
||||||
# Create file name. Default to UUID
|
# Create file name. Default to UUID
|
||||||
|
|
@ -151,6 +151,7 @@ class CaptureObject(object):
|
||||||
self.thumb_bytes = None
|
self.thumb_bytes = None
|
||||||
|
|
||||||
def write(self, s):
|
def write(self, s):
|
||||||
|
logging.debug(f"Writing to {self}")
|
||||||
self.stream.write(s)
|
self.stream.write(s)
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
|
|
|
||||||
|
|
@ -354,7 +354,8 @@ class Microscope:
|
||||||
)
|
)
|
||||||
|
|
||||||
# Capture to output object
|
# Capture to output object
|
||||||
logging.info(f"Starting microscope capture {filename}")
|
logging.info(f"Starting microscope capture {output.file}")
|
||||||
|
print(output)
|
||||||
self.camera.capture(
|
self.camera.capture(
|
||||||
output,
|
output,
|
||||||
use_video_port=use_video_port,
|
use_video_port=use_video_port,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue