Merge branch 'labthings-070' into 'master'
Convert to LabThings 0.7 style See merge request openflexure/openflexure-microscope-server!67
This commit is contained in:
commit
369af1649e
42 changed files with 541 additions and 459 deletions
|
|
@ -1,11 +1,11 @@
|
||||||
Capture Object
|
Capture Object
|
||||||
=======================================================
|
=======================================================
|
||||||
|
|
||||||
By default, all image and video capture data are stored to instances of :py:class:`openflexure_microscope.camera.capture.CaptureObject`. This class mostly wraps up complexity associated with moving data between disk and memory.
|
By default, all image and video capture data are stored to instances of :py:class:`openflexure_microscope.captures.CaptureObject`. This class mostly wraps up complexity associated with moving data between disk and memory.
|
||||||
|
|
||||||
The class also includes some convenience features such as handling metadata tags and file names, and generating image thumbnails. Additionally, the class handles storing capture metadata to Exif tags in supported formats.
|
The class also includes some convenience features such as handling metadata tags and file names, and generating image thumbnails. Additionally, the class handles storing capture metadata to Exif tags in supported formats.
|
||||||
|
|
||||||
Below are details of available methods and attributes.
|
Below are details of available methods and attributes.
|
||||||
|
|
||||||
.. automodule:: openflexure_microscope.camera.capture
|
.. automodule:: openflexure_microscope.captures.capture
|
||||||
:members:
|
:members:
|
||||||
|
|
@ -17,11 +17,14 @@ Like properties, we use a special view class to identify a view as an action: ``
|
||||||
"""
|
"""
|
||||||
Take an image capture and return it without saving
|
Take an image capture and return it without saving
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Expect a "use_video_port" boolean, which defaults to True if none is given
|
# Expect a "use_video_port" boolean, which defaults to True if none is given
|
||||||
@use_args({"use_video_port": fields.Boolean(missing=True)})
|
args = {"use_video_port": fields.Boolean(missing=True)}
|
||||||
|
|
||||||
# Our success response (200) returns an image (image/jpeg mimetype)
|
# Our success response (200) returns an image (image/jpeg mimetype)
|
||||||
@doc_response(200, mimetype="image/jpeg")
|
responses = {
|
||||||
|
200: {"content_type": "image/jpeg"}
|
||||||
|
}
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
"""
|
"""
|
||||||
Take a non-persistant image capture.
|
Take a non-persistant image capture.
|
||||||
|
|
@ -41,7 +44,7 @@ Like properties, we use a special view class to identify a view as an action: ``
|
||||||
# Return our image data using Flasks send_file function
|
# Return our image data using Flasks send_file function
|
||||||
return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg")
|
return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg")
|
||||||
|
|
||||||
In this example, we are also making use of the ``@doc_response`` decorator, to document that our successful response (HTTP code 200) will return data with a mimetype ``image/jpeg``, as well as ``@use_args`` to accept optional parameters with POST requests.
|
In this example, we are also making use of the ``responses`` attribute, to document that our successful response (HTTP code 200) will return data with a mimetype ``image/jpeg``, as well as ``args`` to accept optional parameters with POST requests.
|
||||||
|
|
||||||
Complete example
|
Complete example
|
||||||
----------------
|
----------------
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ from labthings.server.extensions import BaseExtension
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
from labthings.server.view import View
|
from labthings.server.view import View
|
||||||
|
|
||||||
from labthings.server.decorators import use_body
|
|
||||||
from labthings.server import fields
|
from labthings.server import fields
|
||||||
|
|
||||||
## Extension methods
|
## Extension methods
|
||||||
|
|
@ -44,11 +43,13 @@ class ExampleIdentifyView(View):
|
||||||
|
|
||||||
|
|
||||||
class ExampleRenameView(View):
|
class ExampleRenameView(View):
|
||||||
# Expect a request parameter called "name", which is a string. Pass to argument "args".
|
# Expect a request parameter called "name", which is a string.
|
||||||
@use_body(fields.String(required=True, example="My Example Microscope"))
|
# Passed to the argument "args".
|
||||||
def post(self, body):
|
args = fields.String(required=True, example="My Example Microscope")
|
||||||
|
|
||||||
|
def post(self, args):
|
||||||
# Look for our new name in the request body
|
# Look for our new name in the request body
|
||||||
new_name = body
|
new_name = args
|
||||||
|
|
||||||
# Find our microscope component
|
# Find our microscope component
|
||||||
microscope = find_component("org.openflexure.microscope")
|
microscope = find_component("org.openflexure.microscope")
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ from labthings.server.extensions import BaseExtension
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
from labthings.server.view import View
|
from labthings.server.view import View
|
||||||
|
|
||||||
from labthings.server.decorators import use_args, marshal_with
|
|
||||||
from labthings.server.schema import Schema
|
from labthings.server.schema import Schema
|
||||||
from labthings.server import fields
|
from labthings.server import fields
|
||||||
|
|
||||||
|
|
@ -30,24 +29,25 @@ def rename(microscope, new_name):
|
||||||
|
|
||||||
## Extension views
|
## Extension views
|
||||||
|
|
||||||
|
|
||||||
class ExampleIdentifyView(View):
|
class ExampleIdentifyView(View):
|
||||||
# Format our returned object using MicroscopeIdentifySchema
|
# Format our returned object using MicroscopeIdentifySchema
|
||||||
@marshal_with(MicroscopeIdentifySchema())
|
schema = MicroscopeIdentifySchema()
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
# Find our microscope component
|
# Find our microscope component
|
||||||
microscope = find_component("org.openflexure.microscope")
|
microscope = find_component("org.openflexure.microscope")
|
||||||
|
|
||||||
# Return our microscope object,
|
# Return our microscope object,
|
||||||
# let @marshal_with handle formatting the output
|
# let schema handle formatting the output
|
||||||
return microscope
|
return microscope
|
||||||
|
|
||||||
|
|
||||||
class ExampleRenameView(View):
|
class ExampleRenameView(View):
|
||||||
# Format our returned object using MicroscopeIdentifySchema
|
# Format our returned object using MicroscopeIdentifySchema
|
||||||
@marshal_with(MicroscopeIdentifySchema())
|
schema = MicroscopeIdentifySchema()
|
||||||
# Expect a request parameter called "name", which is a string. Pass to argument "args".
|
# Expect a request parameter called "name", which is a string. Pass to argument "args".
|
||||||
@use_args({"name": fields.String(required=True, example="My Example Microscope")})
|
args = {"name": fields.String(required=True, example="My Example Microscope")}
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
# Look for our "name" parameter in the request arguments
|
# Look for our "name" parameter in the request arguments
|
||||||
new_name = args.get("name")
|
new_name = args.get("name")
|
||||||
|
|
@ -59,7 +59,7 @@ class ExampleRenameView(View):
|
||||||
rename(microscope, new_name)
|
rename(microscope, new_name)
|
||||||
|
|
||||||
# Return our microscope object,
|
# Return our microscope object,
|
||||||
# let @marshal_with handle formatting the output
|
# let schema handle formatting the output
|
||||||
return microscope
|
return microscope
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,6 @@ from labthings.server.extensions import BaseExtension
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
from labthings.server.view import View, PropertyView
|
from labthings.server.view import View, PropertyView
|
||||||
|
|
||||||
from labthings.server.decorators import (
|
|
||||||
use_args,
|
|
||||||
marshal_with,
|
|
||||||
ThingProperty,
|
|
||||||
PropertySchema,
|
|
||||||
)
|
|
||||||
from labthings.server.schema import Schema
|
from labthings.server.schema import Schema
|
||||||
from labthings.server import fields
|
from labthings.server import fields
|
||||||
|
|
||||||
|
|
@ -38,7 +32,8 @@ def rename(microscope, new_name):
|
||||||
# Since we only have a GET method here, it'll register as a read-only property
|
# Since we only have a GET method here, it'll register as a read-only property
|
||||||
class ExampleIdentifyView(PropertyView):
|
class ExampleIdentifyView(PropertyView):
|
||||||
# Format our returned object using MicroscopeIdentifySchema
|
# Format our returned object using MicroscopeIdentifySchema
|
||||||
@marshal_with(MicroscopeIdentifySchema())
|
schema = MicroscopeIdentifySchema()
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
Show identifying information about the current microscope object
|
Show identifying information about the current microscope object
|
||||||
|
|
@ -47,14 +42,15 @@ class ExampleIdentifyView(PropertyView):
|
||||||
microscope = find_component("org.openflexure.microscope")
|
microscope = find_component("org.openflexure.microscope")
|
||||||
|
|
||||||
# Return our microscope object,
|
# Return our microscope object,
|
||||||
# let @marshal_with handle formatting the output
|
# let schema handle formatting the output
|
||||||
return microscope
|
return microscope
|
||||||
|
|
||||||
|
|
||||||
# We can use a single schema for all methods if the input and output will be formatted identically
|
# We can use a single schema as the input and output will be formatted identically
|
||||||
# Eg. Here, we will always expect a "name" string argument, and always return a "name" string attribute
|
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
|
||||||
@PropertySchema({"name": fields.String(required=True, example="My Example Microscope")})
|
|
||||||
class ExampleRenameView(PropertyView):
|
class ExampleRenameView(PropertyView):
|
||||||
|
schema = {"name": fields.String(required=True, example="My Example Microscope")}
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
Show the current microscope name
|
Show the current microscope name
|
||||||
|
|
@ -78,7 +74,7 @@ class ExampleRenameView(PropertyView):
|
||||||
rename(microscope, new_name)
|
rename(microscope, new_name)
|
||||||
|
|
||||||
# Return our microscope object,
|
# Return our microscope object,
|
||||||
# let @marshal_with handle formatting the output
|
# let schema handle formatting the output
|
||||||
return microscope
|
return microscope
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,6 @@ from labthings.server.extensions import BaseExtension
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
from labthings.server.view import View, ActionView, PropertyView
|
from labthings.server.view import View, ActionView, PropertyView
|
||||||
|
|
||||||
from labthings.server.decorators import (
|
|
||||||
use_args,
|
|
||||||
marshal_with,
|
|
||||||
ThingProperty,
|
|
||||||
PropertySchema,
|
|
||||||
ThingAction,
|
|
||||||
doc_response,
|
|
||||||
)
|
|
||||||
from labthings.server.schema import Schema
|
from labthings.server.schema import Schema
|
||||||
from labthings.server import fields
|
from labthings.server import fields
|
||||||
|
|
||||||
|
|
@ -43,7 +35,8 @@ def rename(microscope, new_name):
|
||||||
# Since we only have a GET method here, it'll register as a read-only property
|
# Since we only have a GET method here, it'll register as a read-only property
|
||||||
class ExampleIdentifyView(PropertyView):
|
class ExampleIdentifyView(PropertyView):
|
||||||
# Format our returned object using MicroscopeIdentifySchema
|
# Format our returned object using MicroscopeIdentifySchema
|
||||||
@marshal_with(MicroscopeIdentifySchema())
|
schema = MicroscopeIdentifySchema()
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
Show identifying information about the current microscope object
|
Show identifying information about the current microscope object
|
||||||
|
|
@ -52,14 +45,16 @@ class ExampleIdentifyView(PropertyView):
|
||||||
microscope = find_component("org.openflexure.microscope")
|
microscope = find_component("org.openflexure.microscope")
|
||||||
|
|
||||||
# Return our microscope object,
|
# Return our microscope object,
|
||||||
# let @marshal_with handle formatting the output
|
# let schema handle formatting the output
|
||||||
return microscope
|
return microscope
|
||||||
|
|
||||||
|
|
||||||
# We can use a single schema for all methods if the input and output will be formatted identically
|
|
||||||
# Eg. Here, we will always expect a "name" string argument, and always return a "name" string attribute
|
# We can use a single schema as the input and output will be formatted identically
|
||||||
@PropertySchema({"name": fields.String(required=True, example="My Example Microscope")})
|
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
|
||||||
class ExampleRenameView(PropertyView):
|
class ExampleRenameView(PropertyView):
|
||||||
|
schema = {"name": fields.String(required=True, example="My Example Microscope")}
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
Show the current microscope name
|
Show the current microscope name
|
||||||
|
|
@ -83,7 +78,7 @@ class ExampleRenameView(PropertyView):
|
||||||
rename(microscope, new_name)
|
rename(microscope, new_name)
|
||||||
|
|
||||||
# Return our microscope object,
|
# Return our microscope object,
|
||||||
# let @marshal_with handle formatting the output
|
# let schema handle formatting the output
|
||||||
return microscope
|
return microscope
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -91,11 +86,14 @@ class QuickCaptureAPI(ActionView):
|
||||||
"""
|
"""
|
||||||
Take an image capture and return it without saving
|
Take an image capture and return it without saving
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Expect a "use_video_port" boolean, which defaults to True if none is given
|
# Expect a "use_video_port" boolean, which defaults to True if none is given
|
||||||
@use_args({"use_video_port": fields.Boolean(missing=True)})
|
args = {"use_video_port": fields.Boolean(missing=True)}
|
||||||
|
|
||||||
# Our success response (200) returns an image (image/jpeg mimetype)
|
# Our success response (200) returns an image (image/jpeg mimetype)
|
||||||
@doc_response(200, mimetype="image/jpeg")
|
responses = {
|
||||||
|
200: {"content_type": "image/jpeg"}
|
||||||
|
}
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
"""
|
"""
|
||||||
Take a non-persistant image capture.
|
Take a non-persistant image capture.
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,6 @@ from labthings.server.extensions import BaseExtension
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
from labthings.server.view import View, ActionView
|
from labthings.server.view import View, ActionView
|
||||||
|
|
||||||
from labthings.server.decorators import (
|
|
||||||
use_args,
|
|
||||||
marshal_with,
|
|
||||||
ThingProperty,
|
|
||||||
PropertySchema,
|
|
||||||
ThingAction,
|
|
||||||
doc_response,
|
|
||||||
)
|
|
||||||
from labthings.server.schema import Schema
|
from labthings.server.schema import Schema
|
||||||
from labthings.server import fields
|
from labthings.server import fields
|
||||||
|
|
||||||
|
|
@ -18,10 +10,10 @@ import io # Used in our capture action
|
||||||
import time # Used in our timelapse function
|
import time # Used in our timelapse function
|
||||||
|
|
||||||
# Used in our timelapse function
|
# Used in our timelapse function
|
||||||
from openflexure_microscope.camera.base import generate_basename
|
from openflexure_microscope.captures.capture_manager import generate_basename
|
||||||
|
|
||||||
# Used to run our timelapse in a background thread
|
# Used to run our timelapse in a background thread
|
||||||
from labthings.core.tasks import taskify, update_task_progress
|
from labthings.core.tasks import update_task_progress
|
||||||
|
|
||||||
|
|
||||||
## Extension methods
|
## Extension methods
|
||||||
|
|
@ -70,16 +62,15 @@ class TimelapseAPI(ActionView):
|
||||||
"""
|
"""
|
||||||
Take a series of images in a timelapse
|
Take a series of images in a timelapse
|
||||||
"""
|
"""
|
||||||
@use_args(
|
args = {
|
||||||
{
|
"n_images": fields.Integer(
|
||||||
"n_images": fields.Integer(
|
required=True, example=5, description="Number of images"
|
||||||
required=True, example=5, description="Number of images"
|
),
|
||||||
),
|
"t_between": fields.Number(
|
||||||
"t_between": fields.Number(
|
missing=1, example=1, description="Time (seconds) between images"
|
||||||
missing=1, example=1, description="Time (seconds) between images"
|
),
|
||||||
),
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
# Find our microscope component
|
# Find our microscope component
|
||||||
microscope = find_component("org.openflexure.microscope")
|
microscope = find_component("org.openflexure.microscope")
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,6 @@ from labthings.server.extensions import BaseExtension
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
from labthings.server.view import View, ActionView
|
from labthings.server.view import View, ActionView
|
||||||
|
|
||||||
from labthings.server.decorators import (
|
|
||||||
use_args,
|
|
||||||
marshal_with,
|
|
||||||
ThingProperty,
|
|
||||||
PropertySchema,
|
|
||||||
ThingAction,
|
|
||||||
doc_response,
|
|
||||||
)
|
|
||||||
from labthings.server.schema import Schema
|
from labthings.server.schema import Schema
|
||||||
from labthings.server import fields
|
from labthings.server import fields
|
||||||
|
|
||||||
|
|
@ -18,10 +10,10 @@ import io # Used in our capture action
|
||||||
import time # Used in our timelapse function
|
import time # Used in our timelapse function
|
||||||
|
|
||||||
# Used in our timelapse function
|
# Used in our timelapse function
|
||||||
from openflexure_microscope.camera.base import generate_basename
|
from openflexure_microscope.captures.capture_manager import generate_basename
|
||||||
|
|
||||||
# Used to run our timelapse in a background thread
|
# Used to run our timelapse in a background thread
|
||||||
from labthings.core.tasks import taskify, update_task_progress
|
from labthings.core.tasks import update_task_progress
|
||||||
|
|
||||||
# Used to convert our GUI dictionary into a complete eV extension GUI
|
# Used to convert our GUI dictionary into a complete eV extension GUI
|
||||||
from openflexure_microscope.api.utilities.gui import build_gui
|
from openflexure_microscope.api.utilities.gui import build_gui
|
||||||
|
|
@ -72,16 +64,15 @@ class TimelapseAPI(ActionView):
|
||||||
"""
|
"""
|
||||||
Take a series of images in a timelapse, running as a background task
|
Take a series of images in a timelapse, running as a background task
|
||||||
"""
|
"""
|
||||||
@use_args(
|
args = {
|
||||||
{
|
"n_images": fields.Integer(
|
||||||
"n_images": fields.Integer(
|
required=True, example=5, description="Number of images"
|
||||||
required=True, example=5, description="Number of images"
|
),
|
||||||
),
|
"t_between": fields.Number(
|
||||||
"t_between": fields.Number(
|
missing=1, example=1, description="Time (seconds) between images"
|
||||||
missing=1, example=1, description="Time (seconds) between images"
|
),
|
||||||
),
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
# Find our microscope component
|
# Find our microscope component
|
||||||
microscope = find_component("org.openflexure.microscope")
|
microscope = find_component("org.openflexure.microscope")
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ The OpenFlexure Microscope Server makes use of the `Marshmallow library <https:/
|
||||||
- **Deserialize** input data to app-level objects.
|
- **Deserialize** input data to app-level objects.
|
||||||
- **Serialize** app-level objects to primitive Python types. The serialized objects can then be rendered to standard formats such as JSON for use in an HTTP API.
|
- **Serialize** app-level objects to primitive Python types. The serialized objects can then be rendered to standard formats such as JSON for use in an HTTP API.
|
||||||
|
|
||||||
When developing extensions, you are encouraged to make use of the ``@marshal_with`` and ``@use_args`` decorators, along with schemas, to handle serialisation of your API responses, and parsing of request parameters respectively.
|
When developing extensions, you are encouraged to make use of your View ``schema`` and ``args`` class attributes to handle serialisation of your API responses, and parsing of request parameters respectively.
|
||||||
|
|
||||||
Schemas and fields
|
Schemas and fields
|
||||||
++++++++++++++++++
|
++++++++++++++++++
|
||||||
|
|
@ -31,7 +31,7 @@ A **schema** is a collection of keys and fields describing how an object should
|
||||||
Argument parsing
|
Argument parsing
|
||||||
++++++++++++++++
|
++++++++++++++++
|
||||||
|
|
||||||
In the previous section we saw how to use fields and ``@use_body`` to get simple arguments from requests, in which a single parameter is required. By making use of Marshmallow schemas, and the `Webargs library <https://github.com/marshmallow-code/webargs>`_, we can allow for more complex requests containing many parameters of different types. The parsed request parameters are then passed to the view function as a positional argument (as before), in the form of a dictionary.
|
In the previous section we saw how to use fields and ``args`` to get simple arguments from requests, in which a single parameter is required. By making use of Marshmallow schemas, and the `Webargs library <https://github.com/marshmallow-code/webargs>`_, we can allow for more complex requests containing many parameters of different types. The parsed request parameters are then passed to the view function as a positional argument (as before), in the form of a dictionary.
|
||||||
|
|
||||||
For example, if you are creating an API route, in which you expect parameters ``name``, ``age``, and optionally, ``job``, your schema class may look like:
|
For example, if you are creating an API route, in which you expect parameters ``name``, ``age``, and optionally, ``job``, your schema class may look like:
|
||||||
|
|
||||||
|
|
@ -45,25 +45,29 @@ For example, if you are creating an API route, in which you expect parameters ``
|
||||||
age = fields.Integer(required=True)
|
age = fields.Integer(required=True)
|
||||||
job = fields.String(required=False, missing="Unknown")
|
job = fields.String(required=False, missing="Unknown")
|
||||||
|
|
||||||
To inform your POST method to expect these arguments, use the ``@use_args`` decorator:
|
To inform your POST method to expect these arguments, use the ``args`` class attribute:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@use_args(UserSchema())
|
class MyView(View):
|
||||||
def post(self, args):
|
args = UserSchema()
|
||||||
|
|
||||||
|
def post(self, args):
|
||||||
..
|
..
|
||||||
|
|
||||||
Alternatively, if your schema is only used in a single location, it may be simpler to create a dictionary schema only where it is used, for example:
|
Alternatively, if your schema is only used in a single location, it may be simpler to create a dictionary schema only where it is used, for example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@use_args({
|
class MyView(View):
|
||||||
"name": fields.String(required=True),
|
args = {
|
||||||
"age": fields.Integer(required=True),
|
"name": fields.String(required=True),
|
||||||
"job": fields.String(required=False, missing="Unknown")
|
"age": fields.Integer(required=True),
|
||||||
})
|
"job": fields.String(required=False, missing="Unknown")
|
||||||
def post(self, args):
|
}
|
||||||
...
|
|
||||||
|
def post(self, args):
|
||||||
|
...
|
||||||
|
|
||||||
A compatible request body, in JSON format, may look like:
|
A compatible request body, in JSON format, may look like:
|
||||||
|
|
||||||
|
|
@ -80,15 +84,17 @@ This JSON data is the parsed, converted into a Python dictionary, and passed as
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@use_args({
|
class MyView(View):
|
||||||
"name": fields.String(required=True),
|
args = {
|
||||||
"age": fields.Integer(required=True),
|
"name": fields.String(required=True),
|
||||||
"job": fields.String(required=False, missing="Unknown")
|
"age": fields.Integer(required=True),
|
||||||
})
|
"job": fields.String(required=False, missing="Unknown")
|
||||||
def post(self, args):
|
}
|
||||||
name = args.get("name") # Returns "John Doe", type str
|
|
||||||
age = args.get("age") # Returns 45, type int
|
def post(self, args):
|
||||||
job = args.get("job") # Returns "Python developer", type str
|
name = args.get("name") # Returns "John Doe", type str
|
||||||
|
age = args.get("age") # Returns 45, type int
|
||||||
|
job = args.get("job") # Returns "Python developer", type str
|
||||||
|
|
||||||
|
|
||||||
Object serialization
|
Object serialization
|
||||||
|
|
@ -118,16 +124,17 @@ We use this new schema in our ``identify`` view like so:
|
||||||
|
|
||||||
class ExampleIdentifyView(View):
|
class ExampleIdentifyView(View):
|
||||||
# Format our returned object using MicroscopeIdentifySchema
|
# Format our returned object using MicroscopeIdentifySchema
|
||||||
@marshal_with(MicroscopeIdentifySchema())
|
schema = MicroscopeIdentifySchema()
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
# Find our microscope component
|
# Find our microscope component
|
||||||
microscope = find_component("org.openflexure.microscope")
|
microscope = find_component("org.openflexure.microscope")
|
||||||
|
|
||||||
# Return our microscope object,
|
# Return our microscope object,
|
||||||
# let @marshal_with handle formatting the output
|
# let schema handle formatting the output
|
||||||
return microscope
|
return microscope
|
||||||
|
|
||||||
Note that our ``get`` method now returns the :py:class:`openflexure_microscope.Microscope` object itself. No formatting is done by the function, it is entirely handled by ``@marshal_with(MicroscopeIdentifySchema())``. Additionally, since we defined our schema as a class, it can be re-used elsewhere.
|
Note that our ``get`` method now returns the :py:class:`openflexure_microscope.Microscope` object itself. No formatting is done by the function, it is entirely handled by the view class, and its `schema` attribute. Additionally, since we defined our schema as a class, it can be re-used elsewhere.
|
||||||
|
|
||||||
For our ``rename`` view, we will use a simpler schema for our input arguments, defined by a dictionary (since we are only expecting a single parameter in, and it will likely not be re-used elsewhere). Our response, however, will use our ``MicroscopeIdentifySchema`` class. This means that the *response* of our ``identify`` and ``rename`` views will be identically formatted.
|
For our ``rename`` view, we will use a simpler schema for our input arguments, defined by a dictionary (since we are only expecting a single parameter in, and it will likely not be re-used elsewhere). Our response, however, will use our ``MicroscopeIdentifySchema`` class. This means that the *response* of our ``identify`` and ``rename`` views will be identically formatted.
|
||||||
|
|
||||||
|
|
@ -137,9 +144,10 @@ Our ``rename`` view class may now look like:
|
||||||
|
|
||||||
class ExampleRenameView(View):
|
class ExampleRenameView(View):
|
||||||
# Format our returned object using MicroscopeIdentifySchema
|
# Format our returned object using MicroscopeIdentifySchema
|
||||||
@marshal_with(MicroscopeIdentifySchema())
|
schema = MicroscopeIdentifySchema()
|
||||||
# Expect a request parameter called "name", which is a string. Pass to argument "args".
|
# Expect a request parameter called "name", which is a string. Pass to argument "args".
|
||||||
@use_args({"name": fields.String(required=True, example="My Example Microscope")})
|
args = {"name": fields.String(required=True, example="My Example Microscope")}
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
# Look for our "name" parameter in the request arguments
|
# Look for our "name" parameter in the request arguments
|
||||||
new_name = args.get("name")
|
new_name = args.get("name")
|
||||||
|
|
@ -151,7 +159,7 @@ Our ``rename`` view class may now look like:
|
||||||
rename(microscope, new_name)
|
rename(microscope, new_name)
|
||||||
|
|
||||||
# Return our microscope object,
|
# Return our microscope object,
|
||||||
# let @marshal_with handle formatting the output
|
# let schema handle formatting the output
|
||||||
return microscope
|
return microscope
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ In order to register a view as a Thing property, we use the ``PropertyView`` cla
|
||||||
# Since we only have a GET method here, it'll register as a read-only property
|
# Since we only have a GET method here, it'll register as a read-only property
|
||||||
class ExampleIdentifyView(PropertyView):
|
class ExampleIdentifyView(PropertyView):
|
||||||
# Format our returned object using MicroscopeIdentifySchema
|
# Format our returned object using MicroscopeIdentifySchema
|
||||||
@marshal_with(MicroscopeIdentifySchema())
|
schema = MicroscopeIdentifySchema()
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
Show identifying information about the current microscope object
|
Show identifying information about the current microscope object
|
||||||
|
|
@ -30,12 +31,9 @@ In order to register a view as a Thing property, we use the ``PropertyView`` cla
|
||||||
microscope = find_component("org.openflexure.microscope")
|
microscope = find_component("org.openflexure.microscope")
|
||||||
|
|
||||||
# Return our microscope object,
|
# Return our microscope object,
|
||||||
# let @marshal_with handle formatting the output
|
# let schemah handle formatting the output
|
||||||
return microscope
|
return microscope
|
||||||
|
|
||||||
This decorator serves only to add documentation: The view will be tagged with ``property`` in Swagger documentation, and will appear as a property in the microscopes Thing Description.
|
|
||||||
|
|
||||||
However, the additional ``@PropertySchema`` decorator allows for simplified argument parsing and object marshaling.
|
|
||||||
|
|
||||||
Property schema
|
Property schema
|
||||||
---------------
|
---------------
|
||||||
|
|
@ -71,16 +69,17 @@ This request would update the property, such that a GET request would *now* retu
|
||||||
"job": "Landscape gardener"
|
"job": "Landscape gardener"
|
||||||
}
|
}
|
||||||
|
|
||||||
The ``@PropertySchema`` decorator can be applied to your view *class*, and essentially acts as shorthand for applying ``@marshal_with`` to all methods, and ``@use_args`` to all POST and PUT methods (or ``@use_body`` if only a single field is given).
|
In Property Views the ``schema`` class attribute acts as the schema for both marshalling responses *and* parsing arguments. This is because property requests and responses should be identically formatted.
|
||||||
|
|
||||||
We will implement the ``@PropertySchema`` decorator in our ``ExampleRenameView`` view from our previous example:
|
We will implement the ``schema`` attribute in our ``ExampleRenameView`` view from our previous example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
# We can use a single schema for all methods if the input and output will be formatted identically
|
# We can use a single schema as the input and output will be formatted identically
|
||||||
# Eg. Here, we will always expect a "name" string argument, and always return a "name" string attribute
|
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
|
||||||
@PropertySchema({"name": fields.String(required=True, example="My Example Microscope")})
|
|
||||||
class ExampleRenameView(PropertyView):
|
class ExampleRenameView(PropertyView):
|
||||||
|
schema = {"name": fields.String(required=True, example="My Example Microscope")}
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
Show the current microscope name
|
Show the current microscope name
|
||||||
|
|
@ -104,7 +103,7 @@ We will implement the ``@PropertySchema`` decorator in our ``ExampleRenameView``
|
||||||
rename(microscope, new_name)
|
rename(microscope, new_name)
|
||||||
|
|
||||||
# Return our microscope object,
|
# Return our microscope object,
|
||||||
# let @marshal_with handle formatting the output
|
# let schema handle formatting the output
|
||||||
return microscope
|
return microscope
|
||||||
|
|
||||||
Complete example
|
Complete example
|
||||||
|
|
|
||||||
|
|
@ -18,17 +18,17 @@ In this case, our extension will have two new API views at `/identify` and `/ren
|
||||||
Request arguments
|
Request arguments
|
||||||
+++++++++++++++++
|
+++++++++++++++++
|
||||||
|
|
||||||
For POST and PUT requests, data usually needs to be provided to the view in order to perform its function. In this example, our ``rename`` view requires a new microscope name to be passed. We make use of the ``@use_body`` decorator to provide this functionality.
|
For POST and PUT requests, data usually needs to be provided to the view in order to perform its function. In this example, our ``rename`` view requires a new microscope name to be passed. We make use of the ``args`` class attribute to provide this functionality.
|
||||||
|
|
||||||
``@use_body`` defines the type of data expected in the request body. In this example, we use ``String`` type data. The arguments of ``fields.String`` allow us to provide additional information, such as the parameter being required, and example values to appear in API documentation.
|
``args`` defines the type of data expected in the request body. In this example, we use ``String`` type data. The arguments of ``fields.String`` allow us to provide additional information, such as the parameter being required, and example values to appear in API documentation.
|
||||||
|
|
||||||
Adding additional fields, and the meaning of the field types, will be discussed further in the next section.
|
Adding additional fields, and the meaning of the field types, will be discussed further in the next section.
|
||||||
|
|
||||||
When a POST request is made to our API view, the ``@use_body`` converts the body of the request into a ``String``, and passes it as a positional argument to our ``post`` function.
|
When a POST request is made to our API view, the server converts the body of the request into a ``String``, and passes it as a positional argument to our ``post`` function.
|
||||||
|
|
||||||
Swagger documentation
|
Swagger documentation
|
||||||
+++++++++++++++++++++
|
+++++++++++++++++++++
|
||||||
|
|
||||||
At this point, it is useful to introduce the automatically generated Swagger documentation. From any web browser, go to ``http://microscope.local/api/v2/swagger-ui`` (or replace ``microscope.local`` with your microscope's IP address if ``microscope.local`` doesn't work for your system).
|
At this point, it is useful to introduce the automatically generated Swagger documentation. From any web browser, go to ``http://microscope.local/api/v2/swagger-ui`` (or replace ``microscope.local`` with your microscope's IP address if ``microscope.local`` doesn't work for your system).
|
||||||
|
|
||||||
This page uses `SwaggerUI <https://swagger.io/tools/swagger-ui/>`_ to provide visual, interactive API documentation. Find your extensions URL in the documentation under the ``extensions`` group. Basic documentation about the parameters required for your POST method should be visible, as well as an interactive example filled out with the example request given in ``@use_body``.
|
This page uses `SwaggerUI <https://swagger.io/tools/swagger-ui/>`_ to provide visual, interactive API documentation. Find your extensions URL in the documentation under the ``extensions`` group. Basic documentation about the parameters required for your POST method should be visible, as well as an interactive example filled out with the example request given in the view ``schema``.
|
||||||
|
|
|
||||||
|
|
@ -7,5 +7,5 @@ The main microscope class handles microscope settings, passing these between the
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Contents:
|
:caption: Contents:
|
||||||
|
|
||||||
.. automodule:: openflexure_microscope
|
.. automodule:: openflexure_microscope.microscope
|
||||||
:members:
|
:members:
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
from labthings.server.extensions import BaseExtension
|
from labthings.server.extensions import BaseExtension
|
||||||
from labthings.server.view import View, ActionView, PropertyView
|
from labthings.server.view import View, ActionView, PropertyView
|
||||||
from labthings.server.decorators import ThingAction, ThingProperty
|
from labthings.server import fields
|
||||||
|
|
||||||
from openflexure_microscope.devel import JsonResponse, request, abort
|
from openflexure_microscope.devel import JsonResponse, request, abort
|
||||||
from openflexure_microscope.utilities import set_properties
|
from openflexure_microscope.utilities import set_properties
|
||||||
|
|
@ -336,7 +336,9 @@ class AutofocusAPI(ActionView):
|
||||||
"""
|
"""
|
||||||
Run a standard autofocus
|
Run a standard autofocus
|
||||||
"""
|
"""
|
||||||
def post(self):
|
args = {"dz": fields.List(fields.Int())}
|
||||||
|
|
||||||
|
def post(self, args):
|
||||||
payload = JsonResponse(request)
|
payload = JsonResponse(request)
|
||||||
microscope = find_component("org.openflexure.microscope")
|
microscope = find_component("org.openflexure.microscope")
|
||||||
|
|
||||||
|
|
@ -344,7 +346,7 @@ class AutofocusAPI(ActionView):
|
||||||
abort(503, "No microscope connected. Unable to autofocus.")
|
abort(503, "No microscope connected. Unable to autofocus.")
|
||||||
|
|
||||||
# Figure out the range of z values to use
|
# Figure out the range of z values to use
|
||||||
dz = payload.param("dz", default=np.linspace(-300, 300, 7), convert=np.array)
|
dz = np.array(args.get("dz", np.linspace(-300, 300, 7)))
|
||||||
|
|
||||||
if microscope.has_real_stage():
|
if microscope.has_real_stage():
|
||||||
logging.debug("Running autofocus...")
|
logging.debug("Running autofocus...")
|
||||||
|
|
@ -360,18 +362,20 @@ class FastAutofocusAPI(ActionView):
|
||||||
"""
|
"""
|
||||||
Run a fast autofocus
|
Run a fast autofocus
|
||||||
"""
|
"""
|
||||||
def post(self):
|
args = {
|
||||||
payload = JsonResponse(request)
|
"dz": fields.Int(missing=2000),
|
||||||
|
"backlash": fields.Int(missing=25, minimum=0)
|
||||||
|
}
|
||||||
|
|
||||||
|
def post(self, args):
|
||||||
microscope = find_component("org.openflexure.microscope")
|
microscope = find_component("org.openflexure.microscope")
|
||||||
|
|
||||||
if not microscope:
|
if not microscope:
|
||||||
abort(503, "No microscope connected. Unable to autofocus.")
|
abort(503, "No microscope connected. Unable to autofocus.")
|
||||||
|
|
||||||
# Figure out the parameters to use
|
# Figure out the parameters to use
|
||||||
dz = payload.param("dz", default=2000, convert=int)
|
dz = args.get("dz")
|
||||||
backlash = payload.param("backlash", default=25, convert=int)
|
backlash = args.get("backlash")
|
||||||
if backlash < 0:
|
|
||||||
backlash = 0
|
|
||||||
|
|
||||||
if microscope.has_real_stage():
|
if microscope.has_real_stage():
|
||||||
logging.debug("Running autofocus...")
|
logging.debug("Running autofocus...")
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
from labthings.server.extensions import BaseExtension
|
from labthings.server.extensions import BaseExtension
|
||||||
from labthings.server.view import View, PropertyView
|
from labthings.server.view import View, PropertyView
|
||||||
from labthings.server.decorators import ThingProperty, PropertySchema, use_args
|
|
||||||
from labthings.server import fields
|
from labthings.server import fields
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
|
|
||||||
|
|
@ -198,8 +197,9 @@ class GetLocationsView(PropertyView):
|
||||||
return autostorage_extension_v2.get_locations()
|
return autostorage_extension_v2.get_locations()
|
||||||
|
|
||||||
|
|
||||||
@PropertySchema(fields.String(required=True, example="Default"))
|
|
||||||
class PreferredLocationView(PropertyView):
|
class PreferredLocationView(PropertyView):
|
||||||
|
schema = fields.String(required=True, example="Default")
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
global autostorage_extension_v2
|
global autostorage_extension_v2
|
||||||
|
|
||||||
|
|
@ -219,7 +219,8 @@ class PreferredLocationView(PropertyView):
|
||||||
|
|
||||||
|
|
||||||
class PreferredLocationGUIView(View):
|
class PreferredLocationGUIView(View):
|
||||||
@use_args({"new_path_title": fields.String(required=True)})
|
args = {"new_path_title": fields.String(required=True)}
|
||||||
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
global autostorage_extension_v2
|
global autostorage_extension_v2
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from labthings.server.view import View, ActionView
|
from labthings.server.view import View, ActionView
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
from labthings.server.extensions import BaseExtension
|
from labthings.server.extensions import BaseExtension
|
||||||
from labthings.server.decorators import ThingAction
|
|
||||||
|
|
||||||
from flask import abort
|
from flask import abort
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ from functools import reduce
|
||||||
from openflexure_microscope.captures.capture_manager import generate_basename
|
from openflexure_microscope.captures.capture_manager import generate_basename
|
||||||
from labthings.server.find import find_component, find_extension
|
from labthings.server.find import find_component, find_extension
|
||||||
from labthings.server.extensions import BaseExtension
|
from labthings.server.extensions import BaseExtension
|
||||||
from labthings.server.decorators import use_args
|
|
||||||
from labthings.server import fields
|
from labthings.server import fields
|
||||||
|
|
||||||
from openflexure_microscope.devel import abort, update_task_progress
|
from openflexure_microscope.devel import abort, update_task_progress
|
||||||
|
|
@ -92,7 +91,7 @@ class ScanExtension(BaseExtension):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._images_to_be_captured: int = 1
|
self._images_to_be_captured: int = 1
|
||||||
self._images_captured_so_far: int = 0
|
self._images_captured_so_far: int = 0
|
||||||
return BaseExtension.__init__(self, "org.openflexure.scan", version="2.0.0")
|
BaseExtension.__init__(self, "org.openflexure.scan", version="2.0.0")
|
||||||
|
|
||||||
def progress(self):
|
def progress(self):
|
||||||
progress = (self._images_captured_so_far / self._images_to_be_captured) * 100
|
progress = (self._images_captured_so_far / self._images_to_be_captured) * 100
|
||||||
|
|
@ -290,24 +289,23 @@ class ScanExtension(BaseExtension):
|
||||||
scan_extension_v2 = ScanExtension()
|
scan_extension_v2 = ScanExtension()
|
||||||
|
|
||||||
class TileScanAPI(ActionView):
|
class TileScanAPI(ActionView):
|
||||||
@use_args(
|
args = {
|
||||||
{
|
"filename": fields.String(missing=None, example=None),
|
||||||
"filename": fields.String(missing=None, example=None),
|
"temporary": fields.Boolean(missing=False),
|
||||||
"temporary": fields.Boolean(missing=False),
|
"stride_size": fields.List(
|
||||||
"stride_size": fields.List(
|
fields.Integer, missing=[2000, 1500, 100], example=[2000, 1500, 100]
|
||||||
fields.Integer, missing=[2000, 1500, 100], example=[2000, 1500, 100]
|
),
|
||||||
),
|
"grid": fields.List(fields.Integer, missing=[3, 3, 3], example=[3, 3, 3]),
|
||||||
"grid": fields.List(fields.Integer, missing=[3, 3, 3], example=[3, 3, 3]),
|
"style": fields.String(missing="raster"),
|
||||||
"style": fields.String(missing="raster"),
|
"autofocus_dz": fields.Integer(missing=50),
|
||||||
"autofocus_dz": fields.Integer(missing=50),
|
"fast_autofocus": fields.Boolean(missing=False),
|
||||||
"fast_autofocus": fields.Boolean(missing=False),
|
"use_video_port": fields.Boolean(missing=False),
|
||||||
"use_video_port": fields.Boolean(missing=False),
|
"bayer": fields.Boolean(missing=False),
|
||||||
"bayer": fields.Boolean(missing=False),
|
"annotations": fields.Dict(missing={}, example={"Foo": "Bar"}),
|
||||||
"annotations": fields.Dict(missing={}, example={"Foo": "Bar"}),
|
"tags": fields.List(fields.String, missing=[]),
|
||||||
"tags": fields.List(fields.String, missing=[]),
|
"resize": fields.Dict(missing=None), # TODO: Validate keys
|
||||||
"resize": fields.Dict(missing=None), # TODO: Validate keys
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
microscope = find_component("org.openflexure.microscope")
|
microscope = find_component("org.openflexure.microscope")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,10 @@ import logging
|
||||||
|
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
from labthings.server.view import View, ActionView, PropertyView
|
from labthings.server.view import View, ActionView, PropertyView
|
||||||
from labthings.server.schema import Schema
|
from labthings.server.schema import Schema, pre_dump
|
||||||
from labthings.server import fields
|
from labthings.server import fields
|
||||||
from labthings.server.extensions import BaseExtension
|
from labthings.server.extensions import BaseExtension
|
||||||
from labthings.server.utilities import description_from_view
|
from labthings.server.utilities import description_from_view
|
||||||
from labthings.server.decorators import (
|
|
||||||
ThingAction,
|
|
||||||
ThingProperty,
|
|
||||||
marshal_with,
|
|
||||||
pre_dump,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ZipObjectSchema(Schema):
|
class ZipObjectSchema(Schema):
|
||||||
|
|
@ -140,7 +134,6 @@ default_zip_manager = ZipManager()
|
||||||
|
|
||||||
class ZipBuilderAPIView(ActionView):
|
class ZipBuilderAPIView(ActionView):
|
||||||
def post(self):
|
def post(self):
|
||||||
|
|
||||||
ids = list(JsonResponse(request).json)
|
ids = list(JsonResponse(request).json)
|
||||||
microscope = find_component("org.openflexure.microscope")
|
microscope = find_component("org.openflexure.microscope")
|
||||||
|
|
||||||
|
|
@ -151,7 +144,8 @@ class ZipBuilderAPIView(ActionView):
|
||||||
|
|
||||||
|
|
||||||
class ZipListAPIView(PropertyView):
|
class ZipListAPIView(PropertyView):
|
||||||
@marshal_with(ZipObjectSchema(many=True))
|
schema = ZipObjectSchema(many=True)
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
return default_zip_manager.session_zips.values()
|
return default_zip_manager.session_zips.values()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ from labthings.server.extensions import BaseExtension
|
||||||
from labthings.server.view import ActionView
|
from labthings.server.view import ActionView
|
||||||
|
|
||||||
from labthings.server import fields
|
from labthings.server import fields
|
||||||
from labthings.server.decorators import use_args, marshal_with
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
@ -12,12 +11,9 @@ class RaiseException(ActionView):
|
||||||
raise Exception("The developer raised an exception")
|
raise Exception("The developer raised an exception")
|
||||||
|
|
||||||
class SleepFor(ActionView):
|
class SleepFor(ActionView):
|
||||||
@marshal_with({
|
schema = {"TimeAsleep": fields.Float()}
|
||||||
"TimeAsleep": fields.Float()
|
args = {"time": fields.Float(description="Time to sleep, in seconds", example=0.5)}
|
||||||
})
|
|
||||||
@use_args({
|
|
||||||
"time": fields.Float(description="Time to sleep, in seconds", example=0.5)
|
|
||||||
})
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
sleep_time = args.get("time")
|
sleep_time = args.get("time")
|
||||||
logging.info(f"Going to sleep for {sleep_time}...")
|
logging.info(f"Going to sleep for {sleep_time}...")
|
||||||
|
|
|
||||||
|
|
@ -10,21 +10,11 @@ To prevent the editor from interfering with ESLint, add to your project `setting
|
||||||
{
|
{
|
||||||
"editor.tabSize": 2,
|
"editor.tabSize": 2,
|
||||||
"cSpell.enabled": false,
|
"cSpell.enabled": false,
|
||||||
"eslint.validate": [{
|
"eslint.validate": ["vue","javascript", "javascriptreact"],
|
||||||
"language": "vue",
|
|
||||||
"autoFix": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"language": "javascript",
|
|
||||||
"autoFix": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"language": "javascriptreact",
|
|
||||||
"autoFix": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"eslint.autoFixOnSave": true,
|
|
||||||
"editor.formatOnSave": false,
|
"editor.formatOnSave": false,
|
||||||
"vetur.validation.template": false
|
"vetur.validation.template": false,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
49
openflexure_microscope/api/static/package-lock.json
generated
49
openflexure_microscope/api/static/package-lock.json
generated
|
|
@ -6918,7 +6918,8 @@
|
||||||
"ansi-regex": {
|
"ansi-regex": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"aproba": {
|
"aproba": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
|
|
@ -6939,12 +6940,14 @@
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
|
|
@ -6959,17 +6962,20 @@
|
||||||
"code-point-at": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"console-control-strings": {
|
"console-control-strings": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"core-util-is": {
|
"core-util-is": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
|
@ -7086,7 +7092,8 @@
|
||||||
"inherits": {
|
"inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"ini": {
|
"ini": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
|
|
@ -7098,6 +7105,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"number-is-nan": "^1.0.0"
|
"number-is-nan": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -7112,6 +7120,7 @@
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
|
|
@ -7119,12 +7128,14 @@
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"minipass": {
|
"minipass": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "^5.1.2",
|
"safe-buffer": "^5.1.2",
|
||||||
"yallist": "^3.0.0"
|
"yallist": "^3.0.0"
|
||||||
|
|
@ -7143,6 +7154,7 @@
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
|
|
@ -7232,7 +7244,8 @@
|
||||||
"number-is-nan": {
|
"number-is-nan": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"object-assign": {
|
"object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
|
|
@ -7244,6 +7257,7 @@
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
|
|
@ -7329,7 +7343,8 @@
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"safer-buffer": {
|
"safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
|
|
@ -7365,6 +7380,7 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"code-point-at": "^1.0.0",
|
"code-point-at": "^1.0.0",
|
||||||
"is-fullwidth-code-point": "^1.0.0",
|
"is-fullwidth-code-point": "^1.0.0",
|
||||||
|
|
@ -7384,6 +7400,7 @@
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ansi-regex": "^2.0.0"
|
"ansi-regex": "^2.0.0"
|
||||||
}
|
}
|
||||||
|
|
@ -7427,12 +7444,14 @@
|
||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"yallist": {
|
"yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -9191,6 +9210,11 @@
|
||||||
"minimist": "^1.2.5"
|
"minimist": "^1.2.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mousetrap": {
|
||||||
|
"version": "1.6.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz",
|
||||||
|
"integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA=="
|
||||||
|
},
|
||||||
"move-concurrently": {
|
"move-concurrently": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||||
|
|
@ -9904,7 +9928,8 @@
|
||||||
"version": "2.2.2",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
|
||||||
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
|
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
},
|
},
|
||||||
"pify": {
|
"pify": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@
|
||||||
"serve": "vue-cli-service serve --mode development"
|
"serve": "vue-cli-service serve --mode development"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"material-design-icons": "^3.0.1"
|
"material-design-icons": "^3.0.1",
|
||||||
|
"mousetrap": "^1.6.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^3.12.1",
|
"@vue/cli-plugin-babel": "^3.12.1",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,26 @@
|
||||||
<loadingContent v-if="!$store.getters.ready" />
|
<loadingContent v-if="!$store.getters.ready" />
|
||||||
<div v-if="$store.getters.ready" id="tour-header"></div>
|
<div v-if="$store.getters.ready" id="tour-header"></div>
|
||||||
<appContent v-if="$store.getters.ready" />
|
<appContent v-if="$store.getters.ready" />
|
||||||
|
<!-- Runtime modals -->
|
||||||
|
<div
|
||||||
|
id="modal-center"
|
||||||
|
ref="keyboardManualModal"
|
||||||
|
class="uk-flex-top"
|
||||||
|
uk-modal
|
||||||
|
>
|
||||||
|
<div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical">
|
||||||
|
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||||
|
<div
|
||||||
|
v-for="shortcut in keyboardManual"
|
||||||
|
:key="shortcut.shortcut"
|
||||||
|
class="uk-margin-small"
|
||||||
|
uk-grid
|
||||||
|
>
|
||||||
|
<div class="uk-width-small">{{ shortcut.shortcut }}</div>
|
||||||
|
<div class="uk-width-expand">{{ shortcut.description }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<v-tour
|
<v-tour
|
||||||
v-show="$store.getters.ready"
|
v-show="$store.getters.ready"
|
||||||
name="guidedTour"
|
name="guidedTour"
|
||||||
|
|
@ -23,20 +43,26 @@ import appContent from "./components/appContent.vue";
|
||||||
import loadingContent from "./components/loadingContent.vue";
|
import loadingContent from "./components/loadingContent.vue";
|
||||||
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
var Mousetrap = require("mousetrap");
|
||||||
|
|
||||||
// Key Codes
|
Mousetrap.prototype.stopCallback = function(e, element, combo) {
|
||||||
const keyCodes = {
|
// if the element has the class "mousetrap" then no need to stop
|
||||||
pgup: 33,
|
if ((" " + element.className + " ").indexOf(" mousetrap ") > -1) {
|
||||||
pgdn: 34,
|
return false;
|
||||||
left: 37,
|
}
|
||||||
up: 38,
|
|
||||||
right: 39,
|
// if we're in a lightbox, stop mousetrap
|
||||||
down: 40,
|
if (element.classList.contains("lightbox-link")) {
|
||||||
enter: 13,
|
return true;
|
||||||
esc: 27,
|
}
|
||||||
shift: 16,
|
|
||||||
alt: 18,
|
// stop for input, select, and textarea
|
||||||
t: 84
|
return (
|
||||||
|
element.tagName == "INPUT" ||
|
||||||
|
element.tagName == "SELECT" ||
|
||||||
|
element.tagName == "TEXTAREA" ||
|
||||||
|
(element.contentEditable && element.contentEditable == "true")
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Export main app
|
// Export main app
|
||||||
|
|
@ -51,7 +77,8 @@ export default {
|
||||||
data: function() {
|
data: function() {
|
||||||
return {
|
return {
|
||||||
appAvailable: false,
|
appAvailable: false,
|
||||||
keysDown: {},
|
arrowKeysDown: {},
|
||||||
|
keyboardManual: [],
|
||||||
systemDark: undefined,
|
systemDark: undefined,
|
||||||
themeObserver: undefined,
|
themeObserver: undefined,
|
||||||
tourCallbacks: {
|
tourCallbacks: {
|
||||||
|
|
@ -166,9 +193,7 @@ export default {
|
||||||
|
|
||||||
created: function() {
|
created: function() {
|
||||||
window.addEventListener("beforeunload", this.handleExit);
|
window.addEventListener("beforeunload", this.handleExit);
|
||||||
// Key events
|
// Scrollwheel listener
|
||||||
window.addEventListener("keydown", this.keyDownMonitor);
|
|
||||||
window.addEventListener("keyup", this.keyUpMonitor);
|
|
||||||
window.addEventListener("wheel", this.wheelMonitor);
|
window.addEventListener("wheel", this.wheelMonitor);
|
||||||
// Watch for origin changes
|
// Watch for origin changes
|
||||||
this.unwatchOriginFunction = this.$store.watch(
|
this.unwatchOriginFunction = this.$store.watch(
|
||||||
|
|
@ -180,6 +205,82 @@ export default {
|
||||||
console.log(uriV2);
|
console.log(uriV2);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
|
||||||
|
// TODO: Shortcut guide
|
||||||
|
Mousetrap.bind("?", () => {
|
||||||
|
console.log(this.keyboardManual);
|
||||||
|
this.toggleModalElement(this.$refs["keyboardManualModal"]); // Calls the mixin
|
||||||
|
});
|
||||||
|
|
||||||
|
// Arrow keys
|
||||||
|
Mousetrap.bind(
|
||||||
|
["up", "down", "left", "right"],
|
||||||
|
event => {
|
||||||
|
this.arrowKeysDown[event.keyCode] = true; //Add key to array
|
||||||
|
this.navigateKeyHandler();
|
||||||
|
},
|
||||||
|
"keydown"
|
||||||
|
);
|
||||||
|
Mousetrap.bind(
|
||||||
|
["up", "down", "left", "right"],
|
||||||
|
event => {
|
||||||
|
delete this.arrowKeysDown[event.keyCode]; //Remove key from array
|
||||||
|
},
|
||||||
|
"keyup"
|
||||||
|
);
|
||||||
|
this.keyboardManual.push({
|
||||||
|
shortcut: "←↑→↓",
|
||||||
|
description: "Move the microscope stage"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus keys
|
||||||
|
Mousetrap.bind("pageup", () => {
|
||||||
|
this.$root.$emit("globalMoveStepEvent", 0, 0, 1);
|
||||||
|
});
|
||||||
|
Mousetrap.bind("pagedown", () => {
|
||||||
|
this.$root.$emit("globalMoveStepEvent", 0, 0, -1);
|
||||||
|
});
|
||||||
|
this.keyboardManual.push({
|
||||||
|
shortcut: "pgup / pgdn",
|
||||||
|
description: "Move the microscope focus"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Capture
|
||||||
|
Mousetrap.bind("c", () => {
|
||||||
|
this.$root.$emit("globalCaptureEvent");
|
||||||
|
});
|
||||||
|
this.keyboardManual.push({
|
||||||
|
shortcut: "c",
|
||||||
|
description: "Take a capture"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Autofocus
|
||||||
|
Mousetrap.bind("a", () => {
|
||||||
|
this.$root.$emit("globalFastAutofocusEvent");
|
||||||
|
});
|
||||||
|
this.keyboardManual.push({
|
||||||
|
shortcut: "a",
|
||||||
|
description: "Fast autofocus"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Increment/decrement tab
|
||||||
|
Mousetrap.bind("shift+down", () => {
|
||||||
|
this.$root.$emit("globalIncrementTab");
|
||||||
|
});
|
||||||
|
Mousetrap.bind("shift+up", () => {
|
||||||
|
this.$root.$emit("globalDecrementTab");
|
||||||
|
});
|
||||||
|
this.keyboardManual.push({
|
||||||
|
shortcut: "shift+↑ / shift+↓",
|
||||||
|
description: "Switch tab"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-run tour
|
||||||
|
Mousetrap.bind("alt+t", () => {
|
||||||
|
this.$tours["guidedTour"].start();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy: function() {
|
beforeDestroy: function() {
|
||||||
|
|
@ -187,12 +288,13 @@ export default {
|
||||||
if (this.themeObserver) {
|
if (this.themeObserver) {
|
||||||
this.themeObserver.disconnect();
|
this.themeObserver.disconnect();
|
||||||
}
|
}
|
||||||
// Remove key listeners
|
// Remove scrollwheel listener
|
||||||
window.removeEventListener("keydown", this.keyDownMonitor);
|
|
||||||
window.removeEventListener("keyup", this.keyUpMonitor);
|
|
||||||
window.removeEventListener("wheel", this.wheelMonitor);
|
window.removeEventListener("wheel", this.wheelMonitor);
|
||||||
// Remove origin watcher
|
// Remove origin watcher
|
||||||
this.unwatchOriginFunction();
|
this.unwatchOriginFunction();
|
||||||
|
// Remove key listeners
|
||||||
|
console.log("Resetting Mousetrap");
|
||||||
|
Mousetrap.reset();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -231,84 +333,26 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Handle global key press events to be associated with navigation
|
navigateKeyHandler: function() {
|
||||||
keyDownMonitor: function(event) {
|
// Calculate movement array
|
||||||
this.keysDown[event.keyCode] = true; //Add key to array
|
var x_rel = 0;
|
||||||
|
var y_rel = 0;
|
||||||
// Convert keyCode dict into a list of key codes
|
var z_rel = 0;
|
||||||
var keyCodeList = Object.keys(keyCodes).map(function(key) {
|
if (37 in this.arrowKeysDown) {
|
||||||
return keyCodes[key];
|
x_rel = x_rel + 1;
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
// If not inside an element we want to ignore
|
|
||||||
!(event.target instanceof HTMLInputElement) &&
|
|
||||||
!event.target.classList.contains("lightbox-link") &&
|
|
||||||
// If it's a recognised key
|
|
||||||
keyCodeList.includes(event.keyCode)
|
|
||||||
) {
|
|
||||||
this.navigateKeyHandler(keyCodes);
|
|
||||||
this.captureKeyHandler(keyCodes);
|
|
||||||
this.letterKeyHandler(keyCodes);
|
|
||||||
}
|
}
|
||||||
},
|
if (39 in this.arrowKeysDown) {
|
||||||
|
x_rel = x_rel - 1;
|
||||||
keyUpMonitor: function(event) {
|
|
||||||
delete this.keysDown[event.keyCode]; //Remove key from array
|
|
||||||
},
|
|
||||||
|
|
||||||
navigateKeyHandler: function(keyCodes) {
|
|
||||||
const moveKeys = [
|
|
||||||
keyCodes.left,
|
|
||||||
keyCodes.right,
|
|
||||||
keyCodes.up,
|
|
||||||
keyCodes.down,
|
|
||||||
keyCodes.pgup,
|
|
||||||
keyCodes.pgdn
|
|
||||||
];
|
|
||||||
|
|
||||||
if (
|
|
||||||
moveKeys.some(r => Object.keys(this.keysDown).includes(r.toString()))
|
|
||||||
) {
|
|
||||||
// Calculate movement array
|
|
||||||
var x_rel = 0;
|
|
||||||
var y_rel = 0;
|
|
||||||
var z_rel = 0;
|
|
||||||
if (keyCodes.left in this.keysDown) {
|
|
||||||
x_rel = x_rel + 1;
|
|
||||||
}
|
|
||||||
if (keyCodes.right in this.keysDown) {
|
|
||||||
x_rel = x_rel - 1;
|
|
||||||
}
|
|
||||||
if (keyCodes.up in this.keysDown) {
|
|
||||||
y_rel = y_rel + 1;
|
|
||||||
}
|
|
||||||
if (keyCodes.down in this.keysDown) {
|
|
||||||
y_rel = y_rel - 1;
|
|
||||||
}
|
|
||||||
if (keyCodes.pgup in this.keysDown) {
|
|
||||||
z_rel = z_rel - 1;
|
|
||||||
}
|
|
||||||
if (keyCodes.pgdn in this.keysDown) {
|
|
||||||
z_rel = z_rel + 1;
|
|
||||||
}
|
|
||||||
// Make a position request
|
|
||||||
// Emit a signal to move, acted on by panelNavigate.vue
|
|
||||||
this.$root.$emit("globalMoveStepEvent", x_rel, y_rel, z_rel);
|
|
||||||
}
|
}
|
||||||
},
|
if (38 in this.arrowKeysDown) {
|
||||||
|
y_rel = y_rel + 1;
|
||||||
captureKeyHandler: function(keyCodes) {
|
|
||||||
if (keyCodes.shift in this.keysDown && keyCodes.enter in this.keysDown) {
|
|
||||||
console.log("Capturing");
|
|
||||||
this.$root.$emit("globalCaptureEvent");
|
|
||||||
}
|
}
|
||||||
},
|
if (40 in this.arrowKeysDown) {
|
||||||
|
y_rel = y_rel - 1;
|
||||||
letterKeyHandler: function(keyCodes) {
|
|
||||||
if (keyCodes.alt in this.keysDown && keyCodes.t in this.keysDown) {
|
|
||||||
this.$tours["guidedTour"].start();
|
|
||||||
}
|
}
|
||||||
|
// Make a position request
|
||||||
|
// Emit a signal to move, acted on by panelNavigate.vue
|
||||||
|
this.$root.$emit("globalMoveStepEvent", x_rel, y_rel, z_rel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,12 @@
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Decorations
|
||||||
|
//
|
||||||
|
@small-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||||
|
@big-shadow: 0 14px 25px rgba(0, 0, 0, 0.16);
|
||||||
|
|
||||||
//
|
//
|
||||||
// Container
|
// Container
|
||||||
//
|
//
|
||||||
|
|
@ -148,6 +154,7 @@
|
||||||
// Paper
|
// Paper
|
||||||
//
|
//
|
||||||
@paper-border-radius: 4px;
|
@paper-border-radius: 4px;
|
||||||
|
@button-border-radius: 3px;
|
||||||
|
|
||||||
|
|
||||||
/* ========================================================================
|
/* ========================================================================
|
||||||
|
|
@ -187,9 +194,8 @@
|
||||||
|
|
||||||
.uk-card {
|
.uk-card {
|
||||||
border-radius: @paper-border-radius;
|
border-radius: @paper-border-radius;
|
||||||
border: 1px solid rgba(180, 180, 180, 0.25);
|
//border: 1px solid rgba(180, 180, 180, 0.25);
|
||||||
box-shadow: none;
|
box-shadow: @small-shadow;
|
||||||
-webkit-box-shabox-shadow: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.uk-card-media-top img {
|
.uk-card-media-top img {
|
||||||
|
|
@ -228,7 +234,7 @@
|
||||||
*/
|
*/
|
||||||
.uk-modal-dialog {
|
.uk-modal-dialog {
|
||||||
border-radius: @paper-border-radius;
|
border-radius: @paper-border-radius;
|
||||||
box-shadow: 0 14px 25px rgba(0, 0, 0, 0.16);
|
box-shadow: @big-shadow;
|
||||||
transition: 0.3s ease !important;
|
transition: 0.3s ease !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,7 +251,7 @@
|
||||||
*/
|
*/
|
||||||
.uk-notification-message {
|
.uk-notification-message {
|
||||||
border-radius: @paper-border-radius;
|
border-radius: @paper-border-radius;
|
||||||
box-shadow: 0 14px 25px rgba(0, 0, 0, 0.16);
|
box-shadow: @big-shadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uk-notification-message-danger {
|
.uk-notification-message-danger {
|
||||||
|
|
@ -287,7 +293,7 @@ a:hover {
|
||||||
* Buttons
|
* Buttons
|
||||||
*/
|
*/
|
||||||
.uk-button {
|
.uk-button {
|
||||||
border-radius: 2px;
|
border-radius: @button-border-radius;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,6 +302,12 @@ a:hover {
|
||||||
border-color: @global-border;
|
border-color: @global-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uk-button-primary {
|
||||||
|
background-color: rgba(180, 180, 180, 0.10);
|
||||||
|
border-color: @global-primary-background;
|
||||||
|
color: @button-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
.uk-button-danger {
|
.uk-button-danger {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: #f0506e;
|
color: #f0506e;
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,18 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pluginGuis;
|
return pluginGuis;
|
||||||
|
},
|
||||||
|
tabOrder: function() {
|
||||||
|
// TODO: There must be a better way to do this, somehow reading the order of the HTML elements?
|
||||||
|
var ind = ["view", "gallery", "navigate", "capture", "settings"];
|
||||||
|
for (const plugin of this.pluginsGuiList) {
|
||||||
|
ind.push(plugin.id);
|
||||||
|
}
|
||||||
|
ind.push("about");
|
||||||
|
return ind;
|
||||||
|
},
|
||||||
|
currentTabIndex: function() {
|
||||||
|
return this.tabOrder.indexOf(this.currentTab);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -246,6 +258,14 @@ export default {
|
||||||
this.$root.$on("globalSwitchTab", tabID => {
|
this.$root.$on("globalSwitchTab", tabID => {
|
||||||
this.currentTab = tabID;
|
this.currentTab = tabID;
|
||||||
});
|
});
|
||||||
|
// A global signal listener to increment tab
|
||||||
|
this.$root.$on("globalIncrementTab", () => {
|
||||||
|
this.incrementTabBy(1);
|
||||||
|
});
|
||||||
|
// A global signal listener to decrement tab
|
||||||
|
this.$root.$on("globalDecrementTab", () => {
|
||||||
|
this.incrementTabBy(-1);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
|
@ -274,6 +294,14 @@ export default {
|
||||||
this.currentTab = tab;
|
this.currentTab = tab;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
incrementTabBy: function(n) {
|
||||||
|
const newIndex =
|
||||||
|
(((this.currentTabIndex + n) % this.tabOrder.length) +
|
||||||
|
this.tabOrder.length) %
|
||||||
|
this.tabOrder.length;
|
||||||
|
const newId = this.tabOrder[newIndex];
|
||||||
|
this.currentTab = newId;
|
||||||
|
},
|
||||||
startModals: function() {
|
startModals: function() {
|
||||||
this.$refs["calibrationModal"].show();
|
this.$refs["calibrationModal"].show();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,8 @@
|
||||||
:submit-url="fastAutofocusUri"
|
:submit-url="fastAutofocusUri"
|
||||||
:submit-data="{ dz: 2000 }"
|
:submit-data="{ dz: 2000 }"
|
||||||
:submit-label="'Fast'"
|
:submit-label="'Fast'"
|
||||||
|
:button-primary="false"
|
||||||
|
:submit-on-event="'globalFastAutofocusEvent'"
|
||||||
@taskStarted="isAutofocusing = 1"
|
@taskStarted="isAutofocusing = 1"
|
||||||
@finished="isAutofocusing = 0"
|
@finished="isAutofocusing = 0"
|
||||||
></taskSubmitter>
|
></taskSubmitter>
|
||||||
|
|
@ -122,6 +124,7 @@
|
||||||
:submit-url="normalAutofocusUri"
|
:submit-url="normalAutofocusUri"
|
||||||
:submit-data="{ dz: [-60, -30, 0, 30, 60] }"
|
:submit-data="{ dz: [-60, -30, 0, 30, 60] }"
|
||||||
:submit-label="'Medium'"
|
:submit-label="'Medium'"
|
||||||
|
:button-primary="false"
|
||||||
@taskStarted="isAutofocusing = 2"
|
@taskStarted="isAutofocusing = 2"
|
||||||
@finished="isAutofocusing = 0"
|
@finished="isAutofocusing = 0"
|
||||||
></taskSubmitter>
|
></taskSubmitter>
|
||||||
|
|
@ -133,6 +136,7 @@
|
||||||
:submit-url="normalAutofocusUri"
|
:submit-url="normalAutofocusUri"
|
||||||
:submit-data="{ dz: [-20, -10, 0, 10, 20] }"
|
:submit-data="{ dz: [-20, -10, 0, 10, 20] }"
|
||||||
:submit-label="'Fine'"
|
:submit-label="'Fine'"
|
||||||
|
:button-primary="false"
|
||||||
@taskStarted="isAutofocusing = 3"
|
@taskStarted="isAutofocusing = 3"
|
||||||
@finished="isAutofocusing = 0"
|
@finished="isAutofocusing = 0"
|
||||||
></taskSubmitter>
|
></taskSubmitter>
|
||||||
|
|
@ -201,6 +205,7 @@ export default {
|
||||||
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
|
||||||
this.$root.$on("globalMoveStepEvent", (x_steps, y_steps, z_steps) => {
|
this.$root.$on("globalMoveStepEvent", (x_steps, y_steps, z_steps) => {
|
||||||
this.moveRequest(
|
this.moveRequest(
|
||||||
x_steps * this.stepXy,
|
x_steps * this.stepXy,
|
||||||
|
|
@ -219,6 +224,8 @@ export default {
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
// Remove global signal listener to perform a move action
|
// Remove global signal listener to perform a move action
|
||||||
this.$root.$off("globalMoveEvent");
|
this.$root.$off("globalMoveEvent");
|
||||||
|
this.$root.$off("globalMoveInImageCoordinatesEvent");
|
||||||
|
this.$root.$off("globalMoveStepEvent");
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
||||||
|
|
@ -93,13 +93,9 @@ export default {
|
||||||
@import "../../assets/less/theme.less";
|
@import "../../assets/less/theme.less";
|
||||||
|
|
||||||
.tabicon-active {
|
.tabicon-active {
|
||||||
color: @global-primary-background !important;
|
color: #fff !important;
|
||||||
}
|
background-color: @global-primary-background !important;
|
||||||
|
box-shadow: @small-shadow;
|
||||||
.hook-inverse() {
|
|
||||||
.tabicon-active {
|
|
||||||
color: @inverse-primary-muted-color !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabtitle {
|
.tabtitle {
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,12 @@ export default {
|
||||||
buttonPrimary: {
|
buttonPrimary: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: true
|
||||||
|
},
|
||||||
|
submitOnEvent: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -101,7 +106,20 @@ export default {
|
||||||
|
|
||||||
created() {},
|
created() {},
|
||||||
|
|
||||||
beforeDestroy() {},
|
mounted() {
|
||||||
|
// A global signal listener to perform a move action
|
||||||
|
if (this.submitOnEvent) {
|
||||||
|
this.$root.$on(this.submitOnEvent, () => {
|
||||||
|
this.bootstrapTask();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.submitOnEvent) {
|
||||||
|
this.$root.$off(this.submitOnEvent);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
bootstrapTask: function() {
|
bootstrapTask: function() {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
<a
|
<a
|
||||||
class="uk-link"
|
class="uk-link"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href="https://gitlab.com/openflexure/openflexure-microscope-jsclient/-/issues"
|
href="https://gitlab.com/openflexure/openflexure-microscope-server/-/issues"
|
||||||
>Report an issue</a
|
>Report an issue</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -153,11 +153,11 @@ export default {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less" scoped>
|
||||||
|
// Custom UIkit CSS modifications
|
||||||
|
@import "../../assets/less/theme.less";
|
||||||
|
|
||||||
.settings-pane {
|
.settings-pane {
|
||||||
border-width: 0 1px 0 0;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: rgba(180, 180, 180, 0.25);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-nav {
|
.settings-nav {
|
||||||
|
|
@ -170,4 +170,9 @@ export default {
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-color: rgba(180, 180, 180, 0.25);
|
border-color: rgba(180, 180, 180, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-nav li > a {
|
||||||
|
padding-left: 6px !important;
|
||||||
|
border-radius: @button-border-radius;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,10 @@ Vue.mixin({
|
||||||
UIkit.modal(element).hide();
|
UIkit.modal(element).hide();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleModalElement: function(element) {
|
||||||
|
UIkit.modal(element).toggle();
|
||||||
|
},
|
||||||
|
|
||||||
getLocalStorageObj: function(keyName) {
|
getLocalStorageObj: function(keyName) {
|
||||||
if (localStorage.getItem(keyName)) {
|
if (localStorage.getItem(keyName)) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ from . import camera, stage, system
|
||||||
from labthings.server.view import View
|
from labthings.server.view import View
|
||||||
from labthings.server.find import current_labthing
|
from labthings.server.find import current_labthing
|
||||||
from labthings.server.utilities import description_from_view
|
from labthings.server.utilities import description_from_view
|
||||||
from labthings.server.decorators import Tag
|
|
||||||
|
|
||||||
_actions = {
|
_actions = {
|
||||||
"capture": {
|
"capture": {
|
||||||
|
|
@ -58,7 +57,6 @@ def enabled_root_actions():
|
||||||
return {k: v for k, v in _actions.items() if v["conditions"]}
|
return {k: v for k, v in _actions.items() if v["conditions"]}
|
||||||
|
|
||||||
|
|
||||||
# @Tag("actions")
|
|
||||||
class ActionsView(View):
|
class ActionsView(View):
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,11 @@
|
||||||
from openflexure_microscope.api.utilities import get_bool, JsonResponse
|
from openflexure_microscope.api.utilities import get_bool, JsonResponse
|
||||||
from labthings.server.view import View, ActionView
|
from labthings.server.view import View, ActionView
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
from labthings.server.decorators import (
|
|
||||||
use_args,
|
|
||||||
marshal_with,
|
|
||||||
doc,
|
|
||||||
tag,
|
|
||||||
ThingAction,
|
|
||||||
doc_response,
|
|
||||||
)
|
|
||||||
|
|
||||||
from labthings.server import fields
|
from labthings.server import fields
|
||||||
from openflexure_microscope.utilities import filter_dict
|
from openflexure_microscope.utilities import filter_dict
|
||||||
|
|
||||||
from openflexure_microscope.api.v2.views.captures import capture_schema
|
from openflexure_microscope.api.v2.views.captures import CaptureSchema
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import io
|
import io
|
||||||
|
|
@ -25,24 +17,23 @@ class CaptureAPI(ActionView):
|
||||||
Create a new image capture.
|
Create a new image capture.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@use_args(
|
args = {
|
||||||
{
|
"filename": fields.String(example="MyFileName"),
|
||||||
"filename": fields.String(example="MyFileName"),
|
"temporary": fields.Boolean(
|
||||||
"temporary": fields.Boolean(
|
missing=False, description="Delete capture on shutdown"
|
||||||
missing=False, description="Delete capture on shutdown"
|
),
|
||||||
),
|
"use_video_port": fields.Boolean(missing=False),
|
||||||
"use_video_port": fields.Boolean(missing=False),
|
"bayer": fields.Boolean(
|
||||||
"bayer": fields.Boolean(
|
missing=False, description="Store raw bayer data in file"
|
||||||
missing=False, description="Store raw bayer data in file"
|
),
|
||||||
),
|
"annotations": fields.Dict(missing={}, example={"Client": "SwaggerUI"}),
|
||||||
"annotations": fields.Dict(missing={}, example={"Client": "SwaggerUI"}),
|
"tags": fields.List(fields.String, missing=[], example=["docs"]),
|
||||||
"tags": fields.List(fields.String, missing=[], example=["docs"]),
|
"resize": fields.Dict(
|
||||||
"resize": fields.Dict(
|
missing=None, example={"width": 640, "height": 480}
|
||||||
missing=None, example={"width": 640, "height": 480}
|
), # TODO: Validate keys
|
||||||
), # TODO: Validate keys
|
}
|
||||||
}
|
schema = CaptureSchema()
|
||||||
)
|
|
||||||
@marshal_with(capture_schema)
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
"""
|
"""
|
||||||
Create a new capture
|
Create a new capture
|
||||||
|
|
@ -78,18 +69,22 @@ class RAMCaptureAPI(ActionView):
|
||||||
Take a non-persistant image capture.
|
Take a non-persistant image capture.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@use_args(
|
args = {
|
||||||
{
|
"use_video_port": fields.Boolean(missing=True),
|
||||||
"use_video_port": fields.Boolean(missing=True),
|
"bayer": fields.Boolean(
|
||||||
"bayer": fields.Boolean(
|
missing=False, description="Return with raw bayer data"
|
||||||
missing=False, description="Return with raw bayer data"
|
),
|
||||||
),
|
"resize": fields.Dict(
|
||||||
"resize": fields.Dict(
|
missing=None, example={"width": 640, "height": 480}
|
||||||
missing=None, example={"width": 640, "height": 480}
|
), # TODO: Validate keys
|
||||||
), # TODO: Validate keys
|
}
|
||||||
|
|
||||||
|
responses = {
|
||||||
|
200: {
|
||||||
|
"content_type": "image/jpeg"
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
@doc_response(200, mimetype="image/jpeg")
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
"""
|
"""
|
||||||
Take a non-persistant image capture.
|
Take a non-persistant image capture.
|
||||||
|
|
@ -128,9 +123,8 @@ class GPUPreviewStartAPI(ActionView):
|
||||||
in the format ``[x, y, width, height]``.
|
in the format ``[x, y, width, height]``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@use_args(
|
args = {"window": fields.List(fields.Integer, missing=[], example=[0, 0, 640, 480])}
|
||||||
{"window": fields.List(fields.Integer, missing=[], example=[0, 0, 640, 480])}
|
|
||||||
)
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
"""
|
"""
|
||||||
Start the onboard GPU preview.
|
Start the onboard GPU preview.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from openflexure_microscope.api.utilities import JsonResponse
|
from openflexure_microscope.api.utilities import JsonResponse
|
||||||
from labthings.server.view import View, ActionView
|
from labthings.server.view import View, ActionView
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
from labthings.server.decorators import use_args, marshal_with, doc, ThingAction
|
|
||||||
from labthings.server import fields
|
from labthings.server import fields
|
||||||
|
|
||||||
from openflexure_microscope.utilities import axes_to_array, filter_dict
|
from openflexure_microscope.utilities import axes_to_array, filter_dict
|
||||||
|
|
@ -12,16 +11,15 @@ import logging
|
||||||
|
|
||||||
|
|
||||||
class MoveStageAPI(ActionView):
|
class MoveStageAPI(ActionView):
|
||||||
@use_args(
|
args = {
|
||||||
{
|
"absolute": fields.Boolean(
|
||||||
"absolute": fields.Boolean(
|
missing=False, example=False, description="Move to an absolute position"
|
||||||
default=False, example=False, description="Move to an absolute position"
|
),
|
||||||
),
|
"x": fields.Int(missing=0, example=100),
|
||||||
"x": fields.Int(default=0, example=100),
|
"y": fields.Int(missing=0, example=100),
|
||||||
"y": fields.Int(default=0, example=100),
|
"z": fields.Int(missing=0, example=20),
|
||||||
"z": fields.Int(default=0, example=20),
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
def post(self, args):
|
def post(self, args):
|
||||||
"""
|
"""
|
||||||
Move the microscope stage in x, y, z
|
Move the microscope stage in x, y, z
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@ import subprocess
|
||||||
import os
|
import os
|
||||||
from sys import platform
|
from sys import platform
|
||||||
|
|
||||||
from labthings.server.decorators import ThingAction, doc_response
|
|
||||||
|
|
||||||
|
|
||||||
def is_raspberrypi(raise_on_errors=False):
|
def is_raspberrypi(raise_on_errors=False):
|
||||||
"""
|
"""
|
||||||
|
|
@ -19,7 +17,6 @@ class ShutdownAPI(ActionView):
|
||||||
Attempt to shutdown the device
|
Attempt to shutdown the device
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@doc_response(201)
|
|
||||||
def post(self):
|
def post(self):
|
||||||
"""
|
"""
|
||||||
Attempt to shutdown the device
|
Attempt to shutdown the device
|
||||||
|
|
@ -31,7 +28,7 @@ class ShutdownAPI(ActionView):
|
||||||
)
|
)
|
||||||
|
|
||||||
out, err = p.communicate()
|
out, err = p.communicate()
|
||||||
return {"out": out, "err": err}, 201
|
return {"out": out, "err": err}
|
||||||
|
|
||||||
|
|
||||||
class RebootAPI(ActionView):
|
class RebootAPI(ActionView):
|
||||||
|
|
@ -39,7 +36,6 @@ class RebootAPI(ActionView):
|
||||||
Attempt to reboot the device
|
Attempt to reboot the device
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@doc_response(201)
|
|
||||||
def post(self):
|
def post(self):
|
||||||
"""
|
"""
|
||||||
Attempt to reboot the device
|
Attempt to reboot the device
|
||||||
|
|
@ -51,4 +47,4 @@ class RebootAPI(ActionView):
|
||||||
)
|
)
|
||||||
|
|
||||||
out, err = p.communicate()
|
out, err = p.communicate()
|
||||||
return {"out": out, "err": err}, 201
|
return {"out": out, "err": err}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ from labthings.server.schema import Schema
|
||||||
from labthings.server import fields
|
from labthings.server import fields
|
||||||
from labthings.server.view import View, PropertyView
|
from labthings.server.view import View, PropertyView
|
||||||
from labthings.server.utilities import description_from_view
|
from labthings.server.utilities import description_from_view
|
||||||
from labthings.server.decorators import marshal_with, doc_response, Tag, ThingProperty
|
|
||||||
|
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
|
|
||||||
|
|
@ -84,16 +83,10 @@ class CaptureSchema(Schema):
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
capture_schema = CaptureSchema()
|
|
||||||
capture_list_schema = CaptureSchema(many=True)
|
|
||||||
|
|
||||||
|
|
||||||
from pprint import pprint
|
|
||||||
|
|
||||||
|
|
||||||
@Tag("captures")
|
|
||||||
class CaptureList(PropertyView):
|
class CaptureList(PropertyView):
|
||||||
@marshal_with(CaptureSchema(many=True))
|
tags = ["captures"]
|
||||||
|
schema = CaptureSchema(many=True)
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
List all image captures
|
List all image captures
|
||||||
|
|
@ -103,9 +96,10 @@ class CaptureList(PropertyView):
|
||||||
return image_list
|
return image_list
|
||||||
|
|
||||||
|
|
||||||
@Tag("captures")
|
|
||||||
class CaptureView(View):
|
class CaptureView(View):
|
||||||
@marshal_with(CaptureSchema())
|
tags = ["captures"]
|
||||||
|
schema = CaptureSchema()
|
||||||
|
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
"""
|
"""
|
||||||
Description of a single image capture
|
Description of a single image capture
|
||||||
|
|
@ -136,9 +130,14 @@ class CaptureView(View):
|
||||||
return "", 204
|
return "", 204
|
||||||
|
|
||||||
|
|
||||||
@Tag("captures")
|
|
||||||
class CaptureDownload(View):
|
class CaptureDownload(View):
|
||||||
@doc_response(200, mimetype="image/jpeg")
|
tags = ["captures"]
|
||||||
|
responses = {
|
||||||
|
200: {
|
||||||
|
"content_type": "image/jpeg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def get(self, id, filename):
|
def get(self, id, filename):
|
||||||
"""
|
"""
|
||||||
Image data for a single image capture
|
Image data for a single image capture
|
||||||
|
|
@ -172,8 +171,9 @@ class CaptureDownload(View):
|
||||||
return send_file(img, mimetype="image/jpeg")
|
return send_file(img, mimetype="image/jpeg")
|
||||||
|
|
||||||
|
|
||||||
@Tag("captures")
|
|
||||||
class CaptureTags(View):
|
class CaptureTags(View):
|
||||||
|
tags = ["captures"]
|
||||||
|
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
"""
|
"""
|
||||||
Get tags associated with a single image capture
|
Get tags associated with a single image capture
|
||||||
|
|
@ -227,8 +227,9 @@ class CaptureTags(View):
|
||||||
return capture_obj.tags
|
return capture_obj.tags
|
||||||
|
|
||||||
|
|
||||||
@Tag("captures")
|
|
||||||
class CaptureAnnotations(View):
|
class CaptureAnnotations(View):
|
||||||
|
tags = ["captures"]
|
||||||
|
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
"""
|
"""
|
||||||
Get annotations associated with a single image capture
|
Get annotations associated with a single image capture
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ from labthings.core.utilities import get_by_path, set_by_path, create_from_path
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
from labthings.server.view import View, PropertyView
|
from labthings.server.view import View, PropertyView
|
||||||
|
|
||||||
from labthings.server.decorators import ThingProperty, Tag, doc_response
|
|
||||||
|
|
||||||
from flask import request, abort
|
from flask import request, abort
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
@ -35,9 +33,12 @@ class SettingsProperty(PropertyView):
|
||||||
return self.get()
|
return self.get()
|
||||||
|
|
||||||
|
|
||||||
@Tag("properties")
|
|
||||||
class NestedSettingsProperty(View):
|
class NestedSettingsProperty(View):
|
||||||
@doc_response(404, description="Settings key cannot be found")
|
tags = ["properties"]
|
||||||
|
responses = {
|
||||||
|
404: {"description": "Settings key cannot be found"}
|
||||||
|
}
|
||||||
|
|
||||||
def get(self, route):
|
def get(self, route):
|
||||||
"""
|
"""
|
||||||
Show a nested section of the current microscope settings
|
Show a nested section of the current microscope settings
|
||||||
|
|
@ -52,7 +53,6 @@ class NestedSettingsProperty(View):
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@doc_response(404, description="Settings key cannot be found")
|
|
||||||
def put(self, route):
|
def put(self, route):
|
||||||
"""
|
"""
|
||||||
Update a nested section of the current microscope settings
|
Update a nested section of the current microscope settings
|
||||||
|
|
@ -79,9 +79,12 @@ class StateProperty(PropertyView):
|
||||||
return microscope.state
|
return microscope.state
|
||||||
|
|
||||||
|
|
||||||
@Tag("properties")
|
|
||||||
class NestedStateProperty(View):
|
class NestedStateProperty(View):
|
||||||
@doc_response(404, description="Status key cannot be found")
|
tags = ["properties"]
|
||||||
|
responses = {
|
||||||
|
404: {"description": "Status key cannot be found"}
|
||||||
|
}
|
||||||
|
|
||||||
def get(self, route):
|
def get(self, route):
|
||||||
"""
|
"""
|
||||||
Show a nested section of the current microscope state
|
Show a nested section of the current microscope state
|
||||||
|
|
@ -106,9 +109,12 @@ class ConfigurationProperty(PropertyView):
|
||||||
return microscope.configuration
|
return microscope.configuration
|
||||||
|
|
||||||
|
|
||||||
@Tag("properties")
|
|
||||||
class NestedConfigurationProperty(View):
|
class NestedConfigurationProperty(View):
|
||||||
@doc_response(404, description="Configuration key cannot be found")
|
tags = ["properties"]
|
||||||
|
responses = {
|
||||||
|
404: {"description": "Status key cannot be found"}
|
||||||
|
}
|
||||||
|
|
||||||
def get(self, route):
|
def get(self, route):
|
||||||
"""
|
"""
|
||||||
Show a nested section of the current microscope state
|
Show a nested section of the current microscope state
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,19 @@ from labthings.core.utilities import get_by_path, set_by_path, create_from_path
|
||||||
|
|
||||||
from labthings.server.find import find_component
|
from labthings.server.find import find_component
|
||||||
from labthings.server.view import View, PropertyView
|
from labthings.server.view import View, PropertyView
|
||||||
from labthings.server.decorators import doc_response, ThingProperty
|
from labthings.server.responses import Response
|
||||||
|
|
||||||
from flask import Response
|
|
||||||
|
|
||||||
|
|
||||||
class MjpegStream(PropertyView):
|
class MjpegStream(PropertyView):
|
||||||
"""
|
"""
|
||||||
Real-time MJPEG stream from the microscope camera
|
Real-time MJPEG stream from the microscope camera
|
||||||
"""
|
"""
|
||||||
|
responses = {
|
||||||
|
200: {
|
||||||
|
"content_type": "multipart/x-mixed-replace"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@doc_response(200, mimetype="multipart/x-mixed-replace")
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
MJPEG stream from the microscope camera.
|
MJPEG stream from the microscope camera.
|
||||||
|
|
@ -42,7 +44,13 @@ class SnapshotStream(PropertyView):
|
||||||
Single JPEG snapshot from the camera stream
|
Single JPEG snapshot from the camera stream
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@doc_response(200, description="Snapshot taken", mimetype="image/jpeg")
|
responses = {
|
||||||
|
200: {
|
||||||
|
"content_type": "image/jpeg",
|
||||||
|
"description": "Snapshot taken"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
"""
|
"""
|
||||||
Single snapshot from the camera stream
|
Single snapshot from the camera stream
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
|
|
||||||
self.stream_active = False
|
self.stream_active = False
|
||||||
self.record_active = False
|
self.record_active = False
|
||||||
|
self.preview_active = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ def remove(src, new_file=None):
|
||||||
new_data = _webp.remove(src_data)
|
new_data = _webp.remove(src_data)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
new_data = src_data
|
new_data = src_data
|
||||||
except e:
|
except Exception as e:
|
||||||
print(e.args)
|
print(e)
|
||||||
raise ValueError("Error occurred.")
|
raise ValueError("Error occurred.")
|
||||||
|
|
||||||
if isinstance(new_file, io.BytesIO):
|
if isinstance(new_file, io.BytesIO):
|
||||||
|
|
|
||||||
|
|
@ -215,21 +215,6 @@ class Microscope:
|
||||||
settings_current_camera = self.camera.read_settings()
|
settings_current_camera = self.camera.read_settings()
|
||||||
settings_current["camera"] = settings_current_camera
|
settings_current["camera"] = settings_current_camera
|
||||||
|
|
||||||
# Store an encoded copy of the PiCamera lens shading table, if it exists
|
|
||||||
if hasattr(self.camera, "read_lens_shading_table"):
|
|
||||||
# Read LST. Returns None if no LST is active
|
|
||||||
lst_arr = self.camera.read_lens_shading_table()
|
|
||||||
|
|
||||||
if lst_arr is not None:
|
|
||||||
b64_string, dtype, shape = serialise_array_b64(lst_arr)
|
|
||||||
|
|
||||||
settings_current["camera"]["lens_shading_table"] = {
|
|
||||||
"@type": "ndarray",
|
|
||||||
"b64_string": b64_string,
|
|
||||||
"dtype": dtype,
|
|
||||||
"shape": shape,
|
|
||||||
}
|
|
||||||
|
|
||||||
# If attached to a stage
|
# If attached to a stage
|
||||||
if self.stage:
|
if self.stage:
|
||||||
settings_current_stage = self.stage.read_settings()
|
settings_current_stage = self.stage.read_settings()
|
||||||
|
|
|
||||||
44
poetry.lock
generated
44
poetry.lock
generated
|
|
@ -104,17 +104,6 @@ numpy = ">=1.17,<2.0"
|
||||||
all = ["opencv-python-headless (>=4.1,<5.0)", "scipy (>=1.4,<2.0)"]
|
all = ["opencv-python-headless (>=4.1,<5.0)", "scipy (>=1.4,<2.0)"]
|
||||||
correlation = ["opencv-python-headless (>=4.1,<5.0)", "scipy (>=1.4,<2.0)"]
|
correlation = ["opencv-python-headless (>=4.1,<5.0)", "scipy (>=1.4,<2.0)"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Pure Python CBOR (de)serializer with extensive tag support"
|
|
||||||
name = "cbor2"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "5.1.0"
|
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
test = ["pytest", "pytest-cov"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "Python package for providing Mozilla's CA Bundle."
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
|
@ -322,23 +311,26 @@ i18n = ["Babel (>=0.8)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
description = "Python implementation of LabThings, based on the Flask microframework"
|
description = ""
|
||||||
name = "labthings"
|
name = "labthings"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6,<4.0"
|
python-versions = "^3.6"
|
||||||
version = "0.6.7"
|
version = "0.7.0"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
Flask = ">=1.1.1,<2.0.0"
|
Flask = "^1.1.1"
|
||||||
apispec = ">=3.2.0,<4.0.0"
|
apispec = "^3.2.0"
|
||||||
cbor2 = ">=5.1.0,<6.0.0"
|
flask-cors = "^3.0.8"
|
||||||
flask-cors = ">=3.0.8,<4.0.0"
|
|
||||||
gevent = ">=1.4,<21.0"
|
gevent = ">=1.4,<21.0"
|
||||||
gevent-websocket = ">=0.10.1,<0.11.0"
|
gevent-websocket = "^0.10.1"
|
||||||
marshmallow = ">=3.4.0,<4.0.0"
|
marshmallow = "^3.4.0"
|
||||||
webargs = ">=6.0.0,<7.0.0"
|
webargs = "^6.0.0"
|
||||||
zeroconf = ">=0.24.5,<0.28.0"
|
zeroconf = ">=0.24.5,<0.28.0"
|
||||||
|
|
||||||
|
[package.source]
|
||||||
|
reference = "b718258fe219fbdca7f82d3987c0680f7157e738"
|
||||||
|
type = "git"
|
||||||
|
url = "https://github.com/labthings/python-labthings.git"
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "A fast and thorough lazy object proxy."
|
description = "A fast and thorough lazy object proxy."
|
||||||
|
|
@ -841,7 +833,7 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
||||||
rpi = ["RPi.GPIO"]
|
rpi = ["RPi.GPIO"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "94e35da71817800c617a54e54dc937df62953fa95caa2e2e05f69b1163582c4c"
|
content-hash = "fff0dba8c9a8ffe0a496148d68bedf1e12802e6700ab3e5f82788e10891eb774"
|
||||||
python-versions = "^3.6"
|
python-versions = "^3.6"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
|
|
@ -877,9 +869,6 @@ camera-stage-mapping = [
|
||||||
{file = "camera-stage-mapping-0.1.2.tar.gz", hash = "sha256:0d1a6f93b3a60054bc8057359464adbb40488b4daeb93c672b90e4fe525e7b69"},
|
{file = "camera-stage-mapping-0.1.2.tar.gz", hash = "sha256:0d1a6f93b3a60054bc8057359464adbb40488b4daeb93c672b90e4fe525e7b69"},
|
||||||
{file = "camera_stage_mapping-0.1.2-py3-none-any.whl", hash = "sha256:3f3da3e95792e811e6808173c368be67ebb00e6f7d7e37d68cb36fa765d13da9"},
|
{file = "camera_stage_mapping-0.1.2-py3-none-any.whl", hash = "sha256:3f3da3e95792e811e6808173c368be67ebb00e6f7d7e37d68cb36fa765d13da9"},
|
||||||
]
|
]
|
||||||
cbor2 = [
|
|
||||||
{file = "cbor2-5.1.0.tar.gz", hash = "sha256:43ce11e8c2fe4971d386d1a60cf83bfa0a4a667b97668ba76acbf5e6398821aa"},
|
|
||||||
]
|
|
||||||
certifi = [
|
certifi = [
|
||||||
{file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
|
{file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"},
|
||||||
{file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"},
|
{file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"},
|
||||||
|
|
@ -1021,10 +1010,7 @@ jinja2 = [
|
||||||
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
|
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
|
||||||
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
|
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
|
||||||
]
|
]
|
||||||
labthings = [
|
labthings = []
|
||||||
{file = "labthings-0.6.7-py3-none-any.whl", hash = "sha256:c39d3de301cc2c0b21e44bee49c84bdc912be84ace0168de7c16dc4966b4397b"},
|
|
||||||
{file = "labthings-0.6.7.tar.gz", hash = "sha256:5c70fde942436a21764f0ba7e5a77c0f080e51270dc8b4c69917b5b23e253c7b"},
|
|
||||||
]
|
|
||||||
lazy-object-proxy = [
|
lazy-object-proxy = [
|
||||||
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
|
{file = "lazy-object-proxy-1.4.3.tar.gz", hash = "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"},
|
||||||
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"},
|
{file = "lazy_object_proxy-1.4.3-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442"},
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ opencv-python-headless = [
|
||||||
{version = "4.1.0.25", python = "~3.7"}, # PiWheels build for RPi running Py37
|
{version = "4.1.0.25", python = "~3.7"}, # PiWheels build for RPi running Py37
|
||||||
{version = "4.2.0.34", python = "^3.8"} # Latest for Py38 systems
|
{version = "4.2.0.34", python = "^3.8"} # Latest for Py38 systems
|
||||||
]
|
]
|
||||||
labthings = "0.6.7"
|
labthings = {git = "https://github.com/labthings/python-labthings.git", rev = "100-restructure"}
|
||||||
pynpm = "^0.1.2"
|
pynpm = "^0.1.2"
|
||||||
sangaboard = "^0.2"
|
sangaboard = "^0.2"
|
||||||
expiringdict = "^1.2.1"
|
expiringdict = "^1.2.1"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue