First images on labthings-fastapi
We now have very basic camera and stage support.
This commit is contained in:
parent
ac45423739
commit
081654533f
100 changed files with 60 additions and 10149 deletions
|
|
@ -1,12 +0,0 @@
|
|||
HTTP API
|
||||
========
|
||||
|
||||
Live documentation
|
||||
------------------
|
||||
|
||||
Full, interactive Swagger documentation for your microscopes web API is available from the microscope itself. From any browser, go to ``http://{your microscope IP address}/api/v2/docs/swagger-ui``.
|
||||
|
||||
The API is described in an OpenAPI description, available at ``http://{your microscope IP address}/api/v2/docs/openapi.yaml``. It is also available from our `build server`_. It can be conveniently viewed using `Redoc's online preview`_.
|
||||
|
||||
.. _`build server`: https://build.openflexure.org/openflexure-microscope-server/
|
||||
.. _`Redoc's online preview`: https://redocly.github.io/redoc/?url=https://build.openflexure.org/openflexure-microscope-server/latest-api.yaml
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
Base Streaming Camera
|
||||
=======================================================
|
||||
|
||||
.. automodule:: openflexure_microscope.camera.base
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
Base Microscope Stage
|
||||
=====================
|
||||
|
||||
.. automodule:: openflexure_microscope.stage.base
|
||||
:members:
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
Camera Class
|
||||
============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
picamera.rst
|
||||
basecamera.rst
|
||||
capture.rst
|
||||
|
||||
Capturing from a camera object
|
||||
------------------------------
|
||||
|
||||
In the cases of both a Raspberry Pi Streaming Camera, and a Mock Camera (attached if no real camera can be found), the camera's ``capture`` method takes as it's first positional argument either a string describing a file path to save to, or any Python file-like object.
|
||||
|
||||
The :class:`openflexure_microscope.camera.capture.CaptureObject` class works by providing a file path string, but adds additional functionality around storing and retreiving EXIF metadata in compatible files.
|
||||
|
||||
If, for your application, you do not require this functionality, you can pass a simple string or file-like object. For example, to take an image that will be stored in-memory, processed rapidly, and then discarded, you could use a BytesIO stream:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import io
|
||||
from PIL import Image
|
||||
...
|
||||
|
||||
with microscope.camera.lock, io.BytesIO() as stream:
|
||||
|
||||
microscope.camera.capture(
|
||||
stream,
|
||||
use_video_port=True,
|
||||
bayer=False,
|
||||
)
|
||||
|
||||
stream.seek(0)
|
||||
image = Image.open(stream)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
Capture Object
|
||||
=======================================================
|
||||
|
||||
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.
|
||||
|
||||
Below are details of available methods and attributes.
|
||||
|
||||
.. automodule:: openflexure_microscope.captures.capture
|
||||
:members:
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file does only contain a selection of the most common options. For a
|
||||
# full list see the documentation:
|
||||
# http://www.sphinx-doc.org/en/master/config
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Workaround to install and execute git-lfs on Read the Docs
|
||||
if os.system("git lfs env > /dev/null") == 0:
|
||||
os.system("git lfs fetch")
|
||||
os.system("git lfs checkout")
|
||||
else:
|
||||
print(
|
||||
"Warning: it seems git lfs is not installed. Bodging our way around the problem to retrieve images..."
|
||||
)
|
||||
os.system(
|
||||
"wget https://github.com/git-lfs/git-lfs/releases/download/v2.7.1/git-lfs-linux-amd64-v2.7.1.tar.gz"
|
||||
)
|
||||
os.system("tar xvfz git-lfs-linux-amd64-v2.7.1.tar.gz")
|
||||
os.system("./git-lfs install") # make lfs available in current repository
|
||||
os.system("./git-lfs fetch") # download content from remote
|
||||
os.system("./git-lfs checkout") # make local files to have the real content on them
|
||||
|
||||
# Load module from relative imports by modifying the path
|
||||
|
||||
module_path = os.path.abspath("../..")
|
||||
sys.path.insert(0, module_path)
|
||||
|
||||
# Handle mock imports for non-platform-agnostic modules
|
||||
# This allows modules to load that depend on hardware that's not present, e.g.
|
||||
# the `picamera` related modules. They won't *work* but if they load, we can
|
||||
# extract the docstrings, which is what we care about here.
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
class Mock(MagicMock):
|
||||
@classmethod
|
||||
def __getattr__(cls, name):
|
||||
return MagicMock()
|
||||
|
||||
|
||||
mock_imports = ["picamerax", "picamerax.array", "picamerax.mmalobj"]
|
||||
|
||||
sys.modules.update((mod_name, Mock()) for mod_name in mock_imports)
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "OpenFlexure Microscope Software"
|
||||
copyright = "2018, Bath Open Instrumentation Group" # pylint: disable=redefined-builtin
|
||||
author = "Bath Open Instrumentation Group"
|
||||
|
||||
# TODO: extract version from ../setup.py
|
||||
# The short X.Y version
|
||||
version = ""
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = ""
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
"sphinx.ext.autodoc",
|
||||
"sphinx.ext.napoleon",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx.ext.todo",
|
||||
"sphinx.ext.viewcode",
|
||||
"sphinx.ext.githubpages",
|
||||
"sphinx.ext.ifconfig",
|
||||
]
|
||||
|
||||
# Override ordering
|
||||
autodoc_member_order = "bysource"
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = "index"
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = []
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = None
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ["_static"]
|
||||
|
||||
# Custom sidebar templates, must be a dictionary that maps document names
|
||||
# to template names.
|
||||
#
|
||||
# The default sidebars (for documents that don't match any pattern) are
|
||||
# defined by theme itself. Builtin themes are using these templates by
|
||||
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
|
||||
# 'searchbox.html']``.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ---------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = "OpenFlexureMicroscopeSoftwaredoc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(
|
||||
master_doc,
|
||||
"OpenFlexureMicroscopeSoftware.tex",
|
||||
"OpenFlexure Microscope Software Documentation",
|
||||
"Bath Open Instrumentation Group",
|
||||
"manual",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(
|
||||
master_doc,
|
||||
"openflexuremicroscopesoftware",
|
||||
"OpenFlexure Microscope Software Documentation",
|
||||
[author],
|
||||
1,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output ----------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(
|
||||
master_doc,
|
||||
"OpenFlexureMicroscopeSoftware",
|
||||
"OpenFlexure Microscope Software Documentation",
|
||||
author,
|
||||
"OpenFlexureMicroscopeSoftware",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Epub output -------------------------------------------------
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
epub_title = project
|
||||
|
||||
# The unique identifier of the text. This can be a ISBN number
|
||||
# or the project homepage.
|
||||
#
|
||||
# epub_identifier = ''
|
||||
|
||||
# A unique identification for the text.
|
||||
#
|
||||
# epub_uid = ''
|
||||
|
||||
# A list of files that should not be packed into the epub file.
|
||||
epub_exclude_files = ["search.html"]
|
||||
|
||||
|
||||
# -- Extension configuration -------------------------------------------------
|
||||
|
||||
# -- Options for intersphinx extension ---------------------------------------
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
# TODO: update with pysangaboard?
|
||||
intersphinx_mapping = {
|
||||
"openflexure_stage": ("https://openflexure-stage.readthedocs.io/en/latest/", None),
|
||||
"picamerax": ("https://picamerax.readthedocs.io/en/latest//", None),
|
||||
"marshmallow": ("https://marshmallow.readthedocs.io/en/stable/", None),
|
||||
"webargs": ("https://webargs.readthedocs.io/en/latest/", None),
|
||||
}
|
||||
|
||||
# -- Options for todo extension ----------------------------------------------
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = True
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
Microscope settings
|
||||
===================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
.. _MicroscopeRC:
|
||||
|
||||
Microscope settings file
|
||||
------------------------
|
||||
|
||||
Microscope settings are made persistent via a microscope settings file. By default, this
|
||||
file exists at ``~/.openflexure/microscope_settings.json``.
|
||||
|
||||
The class :class:`openflexure_microscope.config.OpenflexureSettingsFile` provides functionality for loading a JSON-format settings file as a Python dictionary, and merging changed settings back into the file.
|
||||
|
||||
The default settings are loaded by the :attr:`openflexure_microscope.config.user_settings`, which can be imported anywhere in the microscope server application to allow reading and writing of persistent settings.
|
||||
|
||||
.. automodule:: openflexure_microscope.config
|
||||
:members:
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
Thing Actions
|
||||
=============
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
As well as properties, the OpenFlexure Microscope Server also supports Thing Actions.
|
||||
Thing Actions "invoke a function of the Thing, which manipulates state (e.g., toggling a lamp on or off) or triggers a process on the Thing (e.g., dim a lamp over time)." For the microscope, this would include moving the stage or taking a capture. Both of these require internal logic, and cannot be performed by changing a simple property.
|
||||
|
||||
Actions should be *triggered* with POST requests *only*. Ideally, a view corresponding to an action should only support POST requests.
|
||||
|
||||
Like properties, we use a special view class to identify a view as an action: ``ActionView``. For example, a view to perform a "quick-capture" action may look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class QuickCaptureAPI(ActionView):
|
||||
"""
|
||||
Take an image capture and return it without saving
|
||||
"""
|
||||
# Expect a "use_video_port" boolean, which defaults to True if none is given
|
||||
args = {"use_video_port": fields.Boolean(load_default=True)}
|
||||
|
||||
# Our success response (200) returns an image (image/jpeg mimetype)
|
||||
responses = {
|
||||
200: {
|
||||
"content": { "image/jpeg": {} },
|
||||
}
|
||||
}
|
||||
|
||||
def post(self, args):
|
||||
"""
|
||||
Take a non-persistant image capture.
|
||||
"""
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Open a BytesIO stream to be destroyed once request has returned
|
||||
with io.BytesIO() as stream:
|
||||
|
||||
# Capture to our stream object
|
||||
microscope.camera.capture(stream, use_video_port=args.get("use_video_port"))
|
||||
|
||||
# Rewind the stream
|
||||
stream.seek(0)
|
||||
|
||||
# Return our image data using Flasks send_file function
|
||||
return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg")
|
||||
|
||||
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
|
||||
----------------
|
||||
|
||||
Adding this new view into our example extension, we now have:
|
||||
|
||||
.. literalinclude:: ./example_extension/05_actions.py
|
||||
|
|
@ -1,223 +0,0 @@
|
|||
OpenFlexure eV GUI
|
||||
==================
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
The main client application for the OpenFlexure Microscope, OpenFlexure eV, can render simple GUIs (graphical user interfaces) for extensions.
|
||||
|
||||
We define our user interface by making use of the extensions general metadata, added using the ``add_meta`` function. This function adds arbitrary additional data to your extensions web API description, for example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Create your extension object
|
||||
my_extension = BaseExtension("com.myname.myextension", version="0.0.0")
|
||||
|
||||
...
|
||||
|
||||
my_extension.add_meta("myKey", "My metadata value")
|
||||
|
||||
OpenFlexure eV will recognise the ``gui`` metadata key, and render properly structured descriptions of a GUI in the format described below. The ``gui`` data essentially describes HTML forms, which it is up to the client to render. The form is constructed by specifying a set of components, and their values.
|
||||
|
||||
Each component in the form has a ``name`` property, which must match up to a property your API route expects in JSON POST requests, and returns in JSON GET requests.
|
||||
|
||||
|
||||
Structure of ``gui``
|
||||
---------------------------
|
||||
|
||||
Root level
|
||||
++++++++++
|
||||
|
||||
The root of your ``gui`` dictionary expects 2 properties:
|
||||
|
||||
``icon`` - The name of a Material Design icon to use for your plugin
|
||||
|
||||
``viewPanel`` *(optional)* - Content to display to the right of the extension form. Either ``stream`` (default), ``gallery``, or ``settings``.
|
||||
|
||||
``forms`` - An array of forms as described below
|
||||
|
||||
Form level
|
||||
++++++++++
|
||||
|
||||
Your extension can contain multiple forms. For example, if your extension creates several API routes, you will need a separate form for each route.
|
||||
|
||||
Each form is described by a JSON object, with the following properties:
|
||||
|
||||
``name`` - A human-readable name for the form
|
||||
|
||||
``route`` - String of the corresponding API route. *Must* match a route defined in your ``api_views`` dictionary
|
||||
|
||||
``isTask`` *(optional)* - Whether the client should treat your API route as a long-running task
|
||||
|
||||
``isCollapsible`` *(optional)* - Whether the form can be collapsed into an accordion
|
||||
|
||||
``submitLabel`` *(optional)* - String to place inside of the form's submit button
|
||||
|
||||
``schema`` - List of dictionaries. Each dictionary element describes a form component.
|
||||
|
||||
``emitOnResponse`` *(optional)* - OpenFlexure eV event to emit when a response is recieved from the extension (generally avoid unless you know you need this.)
|
||||
|
||||
Component level
|
||||
+++++++++++++++
|
||||
|
||||
Each form can (and probably should) contain multiple components. For example, if your API route expects several parameters in a POST requests, each parameter can be bound to a form component.
|
||||
|
||||
Upon form submission, the form data will be converted into a JSON object of key-value pairs, where the key is the components ``name``, and the value is it's current value.
|
||||
|
||||
An overview of available components, and their properties, can be found below.
|
||||
|
||||
Arranging components
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can request that the client render several components in a horizontal grid by placing them in an array. You cannot nest arrays however. Each component in the array will be rendered with equal width as far as possible.
|
||||
|
||||
Overview of components
|
||||
----------------------
|
||||
|
||||
.. list-table::
|
||||
:widths: 10 10 40 20
|
||||
:header-rows: 1
|
||||
:stub-columns: 1
|
||||
|
||||
* - fieldType
|
||||
- Data type
|
||||
- Properties
|
||||
- Example
|
||||
* - checkList
|
||||
- [str, str,...]
|
||||
- **name** (str) Unique name of the component
|
||||
|
||||
**label** (str) Friendly label for the component
|
||||
|
||||
**value** ([str, str,...]) List of selected options
|
||||
|
||||
**options** ([str, str,...]) List of all options
|
||||
|
||||
- .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/checkList.png
|
||||
* - htmlBlock
|
||||
- N/A
|
||||
- **name** (str) Unique name of the component
|
||||
|
||||
**label** (str) Friendly label for the component
|
||||
|
||||
**content** (str) HTML string to be rendered
|
||||
|
||||
- .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/htmlBlock.png
|
||||
* - keyvalList
|
||||
- dict
|
||||
- **name** (str) Unique name of the component
|
||||
|
||||
**value** (dict) Dictionary of key-value pairs
|
||||
|
||||
- .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/keyvalList.png
|
||||
* - labelInput
|
||||
- str
|
||||
- **name** (str) Unique name of the component
|
||||
|
||||
**label** (str) Friendly label for the component
|
||||
|
||||
**value** (str) Value of the editable label text
|
||||
|
||||
- .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/labelInput.png
|
||||
* - numberInput
|
||||
- int
|
||||
- **name** (str) Unique name of the component
|
||||
|
||||
**label** (str) Friendly label for the component
|
||||
|
||||
**value** (int) Value of the input
|
||||
|
||||
**placeholder** (int) Placeholder value
|
||||
|
||||
- .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/numberInput.png
|
||||
* - radioList
|
||||
- String
|
||||
- **name** (str) Unique name of the component
|
||||
|
||||
**label** (str) Friendly label for the component
|
||||
|
||||
**value** (str) Currently selected option
|
||||
|
||||
**options** ([str, str,...]) List of all options
|
||||
|
||||
- .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/radioList.png
|
||||
* - selectList
|
||||
- str
|
||||
- **name** (str) Unique name of the component
|
||||
|
||||
**label** (str) Friendly label for the component
|
||||
|
||||
**value** (str) Currently selected option
|
||||
|
||||
**options** ([str, str,...]) List of all options
|
||||
|
||||
- .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/selectList.png
|
||||
* - tagList
|
||||
- [str, str,...]
|
||||
- **name** (str) Unique name of the component
|
||||
|
||||
**value** ([str, str,...]) List of tag strings
|
||||
|
||||
- .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/tagList.png
|
||||
* - textInput
|
||||
- str
|
||||
- **name** (str) Unique name of the component
|
||||
|
||||
**label** (str) Friendly label for the component
|
||||
|
||||
**value** (int) Value of the input
|
||||
|
||||
**placeholder** (str) Placeholder value
|
||||
|
||||
- .. figure:: https://openflexure.gitlab.io/assets/plugin-form-components/textInput.png
|
||||
|
||||
**Note:** Basic input types (``textInput``, ``numberInput``) can also include additional attributes for HTML input elements inputs (e.g. ``placeholder``, ``required``, ``min``, ``max``). These additional attributes will be forwarded to the rendered HTML elements.
|
||||
|
||||
Building the GUI
|
||||
----------------
|
||||
|
||||
Once you have a dictionary describing your GUI, use the :py:meth:`openflexure_microscope.api.utilities.gui.build_gui` function to fill in and expand any information required to have it properly function. This function expands your ``route`` values to include your extensions full URI, and handles returning dynamic GUIs.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
my_gui = {...}
|
||||
|
||||
# Create your extension object
|
||||
my_extension = BaseExtension("com.myname.myextension", version="0.0.0")
|
||||
|
||||
...
|
||||
|
||||
my_extension.add_meta("gui", build_gui(my_gui, my_extension))
|
||||
|
||||
Dynamic GUIs
|
||||
------------
|
||||
|
||||
Instead of passing a static dictionary to :py:meth:`openflexure_microscope.api.utilities.gui.build_gui`, you can instead pass a callable function which returns a dictionary. This function is then called every time a client requests a description of active extensions.
|
||||
|
||||
Using a callable has the advantage of allowing your extensions GUI to be updated as it is used. This could be as simple as changing ``value`` parameters of components (to show up-to-date default form values), but could be used to entirely change the GUI form as it is used, for example dynamically changing options in select boxes.
|
||||
|
||||
For example, this could take the form:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def create_dynamic_form():
|
||||
...
|
||||
generated_form_dict = {...}
|
||||
return generated_form_dict
|
||||
|
||||
# Create your extension object
|
||||
my_extension = BaseExtension("com.myname.myextension", version="0.0.0")
|
||||
|
||||
...
|
||||
|
||||
my_extension.add_meta("gui", build_gui(create_dynamic_form, my_extension))
|
||||
|
||||
|
||||
Complete example
|
||||
----------------
|
||||
|
||||
Adding a GUI to our previous timelapse example extension becomes:
|
||||
|
||||
.. literalinclude:: ./example_extension/07_ev_gui.py
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
from labthings import find_component
|
||||
from labthings.extensions import BaseExtension
|
||||
|
||||
|
||||
# Create the extension class
|
||||
class MyExtension(BaseExtension):
|
||||
def __init__(self):
|
||||
# Superclass init function
|
||||
super().__init__("com.myname.myextension", version="0.0.0")
|
||||
|
||||
def identify(self):
|
||||
"""
|
||||
Demonstrate access to Microscope.camera, and Microscope.stage
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
response = (
|
||||
f"My name is {microscope.name}. "
|
||||
f"My parent camera is {microscope.camera}, "
|
||||
f"and my parent stage is {microscope.stage}."
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def rename(self, new_name):
|
||||
"""
|
||||
Rename the microscope
|
||||
"""
|
||||
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
microscope.name = new_name
|
||||
microscope.save_settings()
|
||||
|
||||
|
||||
LABTHINGS_EXTENSIONS = (MyExtension,)
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
from labthings import fields, find_component
|
||||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import View
|
||||
|
||||
|
||||
# Create the extension class
|
||||
class MyExtension(BaseExtension):
|
||||
def __init__(self):
|
||||
# Superclass init function
|
||||
super().__init__("com.myname.myextension", version="0.0.0")
|
||||
|
||||
# Add our API Views (defined below MyExtension)
|
||||
self.add_view(ExampleIdentifyView, "/identify")
|
||||
self.add_view(ExampleRenameView, "/rename")
|
||||
|
||||
def identify(self, microscope):
|
||||
"""
|
||||
Demonstrate access to Microscope.camera, and Microscope.stage
|
||||
"""
|
||||
response = (
|
||||
f"My name is {microscope.name}. "
|
||||
f"My parent camera is {microscope.camera}, "
|
||||
f"and my parent stage is {microscope.stage}."
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def rename(self, microscope, new_name):
|
||||
"""
|
||||
Rename the microscope
|
||||
"""
|
||||
microscope.name = new_name
|
||||
microscope.save_settings()
|
||||
|
||||
|
||||
## Extension views
|
||||
class ExampleIdentifyView(View):
|
||||
def get(self):
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Return our identify function's output
|
||||
return self.extension.identify(microscope)
|
||||
|
||||
|
||||
class ExampleRenameView(View):
|
||||
# Expect a request parameter called "name", which is a string.
|
||||
# Passed to the argument "args".
|
||||
args = fields.String(required=True, metadata={"example": "My Example Microscope"})
|
||||
|
||||
def post(self, args):
|
||||
# Look for our new name in the request body
|
||||
new_name = args
|
||||
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Pass microscope and new name to our rename function
|
||||
self.extension.rename(microscope, new_name)
|
||||
|
||||
# Return our identify function's output
|
||||
return self.extension.identify(microscope)
|
||||
|
||||
|
||||
LABTHINGS_EXTENSIONS = (MyExtension,)
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
from labthings import Schema, fields, find_component
|
||||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import View
|
||||
|
||||
|
||||
# Create the extension class
|
||||
class MyExtension(BaseExtension):
|
||||
def __init__(self):
|
||||
# Superclass init function
|
||||
super().__init__("com.myname.myextension", version="0.0.0")
|
||||
|
||||
# Add our API Views (defined below MyExtension)
|
||||
self.add_view(ExampleIdentifyView, "/identify")
|
||||
self.add_view(ExampleRenameView, "/rename")
|
||||
|
||||
def rename(self, microscope, new_name):
|
||||
"""
|
||||
Rename the microscope
|
||||
"""
|
||||
microscope.name = new_name
|
||||
microscope.save_settings()
|
||||
|
||||
|
||||
# Define which properties of a Microscope object we care about,
|
||||
# and what types they should be converted to
|
||||
class MicroscopeIdentifySchema(Schema):
|
||||
name = fields.String() # Microscopes name
|
||||
id = fields.UUID() # Microscopes unique ID
|
||||
state = fields.Dict() # Status dictionary
|
||||
camera = fields.String() # Camera object (represented as a string)
|
||||
stage = fields.String() # Stage object (represented as a string)
|
||||
|
||||
|
||||
## Extension views
|
||||
class ExampleIdentifyView(View):
|
||||
# Format our returned object using MicroscopeIdentifySchema
|
||||
schema = MicroscopeIdentifySchema()
|
||||
|
||||
def get(self):
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Return our microscope object,
|
||||
# let schema handle formatting the output
|
||||
return microscope
|
||||
|
||||
|
||||
class ExampleRenameView(View):
|
||||
# Format our returned object using MicroscopeIdentifySchema
|
||||
schema = MicroscopeIdentifySchema()
|
||||
# Expect a request parameter called "name", which is a string. Pass to argument "args".
|
||||
args = {
|
||||
"name": fields.String(
|
||||
required=True, metadata={"example": "My Example Microscope"}
|
||||
)
|
||||
}
|
||||
|
||||
def post(self, args):
|
||||
# Look for our "name" parameter in the request arguments
|
||||
new_name = args.get("name")
|
||||
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Pass microscope and new name to our rename function
|
||||
self.extension.rename(microscope, new_name)
|
||||
|
||||
# Return our microscope object,
|
||||
# let schema handle formatting the output
|
||||
return microscope
|
||||
|
||||
|
||||
LABTHINGS_EXTENSIONS = (MyExtension,)
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
from labthings import Schema, fields, find_component
|
||||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import PropertyView
|
||||
|
||||
|
||||
# Create the extension class
|
||||
class MyExtension(BaseExtension):
|
||||
def __init__(self):
|
||||
# Superclass init function
|
||||
super().__init__("com.myname.myextension", version="0.0.0")
|
||||
|
||||
# Add our API Views (defined below MyExtension)
|
||||
self.add_view(ExampleIdentifyView, "/identify")
|
||||
self.add_view(ExampleRenameView, "/rename")
|
||||
|
||||
def rename(self, microscope, new_name):
|
||||
"""
|
||||
Rename the microscope
|
||||
"""
|
||||
microscope.name = new_name
|
||||
microscope.save_settings()
|
||||
|
||||
|
||||
# Define which properties of a Microscope object we care about,
|
||||
# and what types they should be converted to
|
||||
class MicroscopeIdentifySchema(Schema):
|
||||
name = fields.String() # Microscopes name
|
||||
id = fields.UUID() # Microscopes unique ID
|
||||
state = fields.Dict() # Status dictionary
|
||||
camera = fields.String() # Camera object (represented as a string)
|
||||
stage = fields.String() # Stage object (represented as a string)
|
||||
|
||||
|
||||
## Extension viewss
|
||||
|
||||
# Since we only have a GET method here, it'll register as a read-only property
|
||||
class ExampleIdentifyView(PropertyView):
|
||||
# Format our returned object using MicroscopeIdentifySchema
|
||||
schema = MicroscopeIdentifySchema()
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Show identifying information about the current microscope object
|
||||
"""
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Return our microscope object,
|
||||
# let schema handle formatting the output
|
||||
return microscope
|
||||
|
||||
|
||||
# We can use a single schema as the input and output will be formatted identically
|
||||
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
|
||||
class ExampleRenameView(PropertyView):
|
||||
schema = {
|
||||
"name": fields.String(
|
||||
required=True, metadata={"example": "My Example Microscope"}
|
||||
)
|
||||
}
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Show the current microscope name
|
||||
"""
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
return microscope
|
||||
|
||||
def post(self, args):
|
||||
"""
|
||||
Change the current microscope name
|
||||
"""
|
||||
# Look for our "name" parameter in the request arguments
|
||||
new_name = args.get("name")
|
||||
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Pass microscope and new name to our rename function
|
||||
self.extension.rename(microscope, new_name)
|
||||
|
||||
# Return our microscope object,
|
||||
# let schema handle formatting the output
|
||||
return microscope
|
||||
|
||||
|
||||
LABTHINGS_EXTENSIONS = (MyExtension,)
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
import io # Used in our capture action
|
||||
|
||||
from flask import send_file # Used to send images from our server
|
||||
from labthings import Schema, fields, find_component
|
||||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import ActionView, PropertyView
|
||||
|
||||
|
||||
# Create the extension class
|
||||
class MyExtension(BaseExtension):
|
||||
def __init__(self):
|
||||
# Superclass init function
|
||||
super().__init__("com.myname.myextension", version="0.0.0")
|
||||
|
||||
# Add our API Views (defined below MyExtension)
|
||||
self.add_view(ExampleIdentifyView, "/identify")
|
||||
self.add_view(ExampleRenameView, "/rename")
|
||||
|
||||
def rename(self, microscope, new_name):
|
||||
"""
|
||||
Rename the microscope
|
||||
"""
|
||||
microscope.name = new_name
|
||||
microscope.save_settings()
|
||||
|
||||
|
||||
# Define which properties of a Microscope object we care about,
|
||||
# and what types they should be converted to
|
||||
class MicroscopeIdentifySchema(Schema):
|
||||
name = fields.String() # Microscopes name
|
||||
id = fields.UUID() # Microscopes unique ID
|
||||
state = fields.Dict() # Status dictionary
|
||||
camera = fields.String() # Camera object (represented as a string)
|
||||
stage = fields.String() # Stage object (represented as a string)
|
||||
|
||||
|
||||
## Extension views
|
||||
|
||||
# Since we only have a GET method here, it'll register as a read-only property
|
||||
class ExampleIdentifyView(PropertyView):
|
||||
# Format our returned object using MicroscopeIdentifySchema
|
||||
schema = MicroscopeIdentifySchema()
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Show identifying information about the current microscope object
|
||||
"""
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Return our microscope object,
|
||||
# let schema handle formatting the output
|
||||
return microscope
|
||||
|
||||
|
||||
# We can use a single schema as the input and output will be formatted identically
|
||||
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
|
||||
class ExampleRenameView(PropertyView):
|
||||
schema = {
|
||||
"name": fields.String(
|
||||
required=True, metadata={"example": "My Example Microscope"}
|
||||
)
|
||||
}
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Show the current microscope name
|
||||
"""
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
return microscope
|
||||
|
||||
def post(self, args):
|
||||
"""
|
||||
Change the current microscope name
|
||||
"""
|
||||
# Look for our "name" parameter in the request arguments
|
||||
new_name = args.get("name")
|
||||
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Pass microscope and new name to our rename function
|
||||
self.extension.rename(microscope, new_name)
|
||||
|
||||
# Return our microscope object,
|
||||
# let schema handle formatting the output
|
||||
return microscope
|
||||
|
||||
|
||||
class QuickCaptureAPI(ActionView):
|
||||
"""
|
||||
Take an image capture and return it without saving
|
||||
"""
|
||||
|
||||
# Expect a "use_video_port" boolean, which defaults to True if none is given
|
||||
args = {"use_video_port": fields.Boolean(load_default=True)}
|
||||
|
||||
# Our success response (200) returns an image (image/jpeg mimetype)
|
||||
responses = {200: {"content": {"image/jpeg": {}}}}
|
||||
|
||||
def post(self, args):
|
||||
"""
|
||||
Take a non-persistant image capture.
|
||||
"""
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Open a BytesIO stream to be destroyed once request has returned
|
||||
with io.BytesIO() as stream:
|
||||
|
||||
# Capture to our stream object
|
||||
microscope.camera.capture(stream, use_video_port=args.get("use_video_port"))
|
||||
|
||||
# Rewind the stream
|
||||
stream.seek(0)
|
||||
|
||||
# Return our image data using Flasks send_file function
|
||||
return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg")
|
||||
|
||||
|
||||
LABTHINGS_EXTENSIONS = (MyExtension,)
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
import time # Used in our timelapse function
|
||||
|
||||
from labthings import current_action, fields, find_component, update_action_progress
|
||||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import ActionView
|
||||
|
||||
# Used in our timelapse function
|
||||
from openflexure_microscope.captures.capture_manager import generate_basename
|
||||
|
||||
|
||||
# Create the extension class
|
||||
class TimelapseExtension(BaseExtension):
|
||||
def __init__(self):
|
||||
# Superclass init function
|
||||
super().__init__("org.openflexure.timelapse-extension", version="0.0.0")
|
||||
|
||||
# Add our API views
|
||||
self.add_view(TimelapseAPIView, "/timelapse")
|
||||
|
||||
def timelapse(self, microscope, n_images, t_between):
|
||||
"""
|
||||
Save a set of images in a timelapse
|
||||
|
||||
Args:
|
||||
microscope: Microscope object
|
||||
n_images (int): Number of images to take
|
||||
t_between (int/float): Time, in seconds, between sequential captures
|
||||
"""
|
||||
base_file_name = generate_basename()
|
||||
folder = "TIMELAPSE_{}".format(base_file_name)
|
||||
|
||||
# Take exclusive control over both the camera and stage
|
||||
with microscope.camera.lock, microscope.stage.lock:
|
||||
for n in range(n_images):
|
||||
# Elegantly handle action cancellation
|
||||
if current_action() and current_action().stopped:
|
||||
return
|
||||
# Generate a filename
|
||||
filename = f"{base_file_name}_image{n}"
|
||||
# Create a file to save the image to
|
||||
output = microscope.camera.new_image(
|
||||
filename=filename, folder=folder, temporary=False
|
||||
)
|
||||
|
||||
# Capture
|
||||
microscope.camera.capture(output)
|
||||
|
||||
# Add system metadata
|
||||
output.put_metadata(microscope.metadata, system=True)
|
||||
|
||||
# Update task progress (only does anyting if the function is running in a LabThings task)
|
||||
progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent
|
||||
update_action_progress(progress_pct)
|
||||
|
||||
# Wait for the specified time
|
||||
time.sleep(t_between)
|
||||
|
||||
|
||||
## Extension views
|
||||
|
||||
|
||||
class TimelapseAPIView(ActionView):
|
||||
"""
|
||||
Take a series of images in a timelapse
|
||||
"""
|
||||
|
||||
args = {
|
||||
"n_images": fields.Integer(
|
||||
required=True, metadata={"example": 5, "description": "Number of images"}
|
||||
),
|
||||
"t_between": fields.Number(
|
||||
load_default=1,
|
||||
metadata={"example": 1, "description": "Time (seconds) between images"},
|
||||
),
|
||||
}
|
||||
|
||||
def post(self, args):
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Start "timelapse"
|
||||
return self.extension.timelapse(
|
||||
microscope, args.get("n_images"), args.get("t_between")
|
||||
)
|
||||
|
||||
|
||||
LABTHINGS_EXTENSIONS = (TimelapseExtension,)
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
import time # Used in our timelapse function
|
||||
|
||||
from labthings import current_action, fields, find_component, update_action_progress
|
||||
from labthings.extensions import BaseExtension
|
||||
from labthings.views import ActionView
|
||||
|
||||
# Used to convert our GUI dictionary into a complete eV extension GUI
|
||||
from openflexure_microscope.api.utilities.gui import build_gui
|
||||
|
||||
# Used in our timelapse function
|
||||
from openflexure_microscope.captures.capture_manager import generate_basename
|
||||
|
||||
|
||||
# Create the extension class
|
||||
class TimelapseExtension(BaseExtension):
|
||||
def __init__(self):
|
||||
# Superclass init function
|
||||
super().__init__("org.openflexure.timelapse-extension", version="0.0.0")
|
||||
|
||||
# Add our API views
|
||||
self.add_view(TimelapseAPIView, "/timelapse")
|
||||
|
||||
# Add our GUI description
|
||||
gui_description = {
|
||||
"icon": "timelapse", # Name of an icon from https://material.io/resources/icons/
|
||||
"forms": [ # List of forms. Each form is a collapsible accordion panel
|
||||
{
|
||||
"name": "Start a timelapse", # Form title
|
||||
"route": "/timelapse", # The URL rule (as given by "add_view") of your submission view
|
||||
"isTask": True, # This forms submission starts a background task
|
||||
"isCollapsible": False, # This form cannot be collapsed into an accordion
|
||||
"submitLabel": "Start", # Label for the form submit button
|
||||
"schema": [ # List of dictionaries. Each element is a form component.
|
||||
{
|
||||
"fieldType": "numberInput",
|
||||
"name": "n_images", # Name of the view arg this value corresponds to
|
||||
"label": "Number of images",
|
||||
"min": 1, # HTML number input attribute
|
||||
"default": 5, # HTML number input attribute
|
||||
},
|
||||
{
|
||||
"fieldType": "numberInput",
|
||||
"name": "t_between",
|
||||
"label": "Time (seconds) between images",
|
||||
"min": 0.1, # HTML number input attribute
|
||||
"step": 0.1, # HTML number input attribute
|
||||
"default": 1, # HTML number input attribute
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
self.add_meta("gui", build_gui(gui_description, self))
|
||||
|
||||
def timelapse(self, microscope, n_images, t_between):
|
||||
"""
|
||||
Save a set of images in a timelapse
|
||||
|
||||
Args:
|
||||
microscope: Microscope object
|
||||
n_images (int): Number of images to take
|
||||
t_between (int/float): Time, in seconds, between sequential captures
|
||||
"""
|
||||
base_file_name = generate_basename()
|
||||
folder = "TIMELAPSE_{}".format(base_file_name)
|
||||
|
||||
# Take exclusive control over both the camera and stage
|
||||
with microscope.camera.lock, microscope.stage.lock:
|
||||
for n in range(n_images):
|
||||
# Elegantly handle action cancellation
|
||||
if current_action() and current_action().stopped:
|
||||
return
|
||||
# Generate a filename
|
||||
filename = f"{base_file_name}_image{n}"
|
||||
# Create a file to save the image to
|
||||
output = microscope.camera.new_image(
|
||||
filename=filename, folder=folder, temporary=False
|
||||
)
|
||||
|
||||
# Capture
|
||||
microscope.camera.capture(output)
|
||||
|
||||
# Add system metadata
|
||||
output.put_metadata(microscope.metadata, system=True)
|
||||
|
||||
# Update task progress (only does anyting if the function is running in a LabThings task)
|
||||
progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent
|
||||
update_action_progress(progress_pct)
|
||||
|
||||
# Wait for the specified time
|
||||
time.sleep(t_between)
|
||||
|
||||
|
||||
## Extension views
|
||||
class TimelapseAPIView(ActionView):
|
||||
"""
|
||||
Take a series of images in a timelapse
|
||||
"""
|
||||
|
||||
args = {
|
||||
"n_images": fields.Integer(
|
||||
required=True, metadata={"example": 5, "description": "Number of images"}
|
||||
),
|
||||
"t_between": fields.Number(
|
||||
load_default=1,
|
||||
metadata={"example": 1, "description": "Time (seconds) between images"},
|
||||
),
|
||||
}
|
||||
|
||||
def post(self, args):
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Start "timelapse"
|
||||
return self.extension.timelapse(
|
||||
microscope, args.get("n_images"), args.get("t_between")
|
||||
)
|
||||
|
||||
|
||||
LABTHINGS_EXTENSIONS = (TimelapseExtension,)
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
Introduction
|
||||
============
|
||||
|
||||
Extensions allow functionality to be added to the OpenFlexure Microscope web API without having to modify the base code.
|
||||
They have full access to the :py:class:`openflexure_microscope.Microscope` object,
|
||||
including direct access to any attached :py:class:`openflexure_microscope.camera.base.BaseCamera` and :py:class:`openflexure_stage.stage.OpenFlexureStage` objects.
|
||||
This also allows access to the :py:class:`picamerax.PiCamera` object.
|
||||
|
||||
Extensions can either be loaded from a single Python file, or as a Python package installed to the environment being used.
|
||||
|
||||
Single-file extensions
|
||||
----------------------
|
||||
For adding simple functionality, such as a few basic functions and API routes, a single Python file can be loaded as a extension. This Python file must contain all of your extension objects, and be located in the applications extensions directory (by default ``/var/openflexure/extensions/microscope_extensions``).
|
||||
|
||||
Package extensions
|
||||
------------------
|
||||
Generally, for adding anything other than very simple functionality, extensions should be written as `package distributions <https://packaging.python.org/tutorials/packaging-projects/>`_. This has the advantage of allowing relative imports, so functionality can be easily split over several files. For example, class definitions associated with API routes can be separated from class definitions associated with the microscope extension.
|
||||
|
||||
Your module must be a folder within the extensions folder (by default ``/var/openflexure/extensions/microscope_extensions``), and include a top-level ``__init__.py`` file which includes (or imports) all of your extension classes, and includes them in a global constant `LABTHINGS_EXTENSIONS` list.
|
||||
|
||||
For example, if your extension classes are defined in a file ``my_extension.py``, your adjascent ``__init__.py`` file may look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from .my_extension import MyExtensionClass, MyOtherExtensionClass
|
||||
|
||||
LABTHINGS_EXTENSIONS = (MyExtensionClass, MyOtherExtensionClass)
|
||||
|
||||
|
||||
In order to enable a globally installed, packaged extension, create a file in the applications extensions directory (by default ``/var/openflexure/extensions/microscope_extensions``) which imports your extension object(s) from your module.
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
Lifecycle Hooks
|
||||
===============
|
||||
|
||||
Introduction
|
||||
------------
|
||||
In some cases it is useful to have functions triggered by events in an extensions lifecycle. Currently two such lifecycle events can be used, ``on_register``, and ``on_component``.
|
||||
|
||||
``on_register``
|
||||
---------------
|
||||
|
||||
The ``on_register`` method can be used to have a function call as soon as the extension has been successfully registered to the microscope. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyExtension(BaseExtension):
|
||||
def __init__(self):
|
||||
|
||||
# Track if the extension has been registered
|
||||
self.registered = False
|
||||
|
||||
# Add lifecycle hooks
|
||||
self.on_register(self.on_register_handler, args=(), kwargs={})
|
||||
|
||||
# Superclass init function
|
||||
super().__init__("com.myname.myextension", version="0.0.0")
|
||||
|
||||
def on_register_handler(self, *args, **kwargs):
|
||||
self.registered = True
|
||||
print("Extension has been registered!")
|
||||
|
||||
|
||||
``on_component``
|
||||
----------------
|
||||
|
||||
The ``on_component`` method can be used to have a function call as soon as a particular LabThings component has been added. This can be used, for example, to get information about the microscope instance as soon as it is available. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyExtension(BaseExtension):
|
||||
def __init__(self):
|
||||
|
||||
# Hold a reference to the microscope object as soon as it is available
|
||||
self.microscope = None
|
||||
|
||||
# Add lifecycle hooks
|
||||
self.on_component("com.myname.myextension", self.on_microscope_handler)
|
||||
|
||||
# Superclass init function
|
||||
super().__init__("org.openflexure.microscope", version="0.0.0")
|
||||
|
||||
def on_microscope_handler(self, microscope_object):
|
||||
print("Microscope object has been found!")
|
||||
self.microscope = microscope_object
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
Marshaling data
|
||||
===============
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
The OpenFlexure Microscope Server makes use of the `Marshmallow library <https://github.com/marshmallow-code/marshmallow/>`_ for both response and argument marshaling. From the Marshmallow documentation:
|
||||
|
||||
**marshmallow** is an ORM/ODM/framework-agnostic library for converting complex datatypes, such as objects, to and from native Python datatypes.
|
||||
|
||||
In short, marshmallow schemas can be used to:
|
||||
|
||||
- **Validate** input data.
|
||||
- **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.
|
||||
|
||||
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
|
||||
++++++++++++++++++
|
||||
|
||||
A **field** describes the data type of a single parameter, as well as any other properties of that parameter for use in parsing, and documentation. For example, a String-type field, with a default value in case no actual value is passed, and extra documentation, may look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
fields.String(required=False, load_default="Default value", metadata={"example: "Example value"})
|
||||
|
||||
A **schema** is a collection of keys and fields describing how an object should be serialized/deserialized. Schemas can be created in several ways, either by creating a ``Schema`` class, or by passing a dictionary of key-field pairs. Both methods will be discussed in the following examples.
|
||||
|
||||
|
||||
Argument parsing
|
||||
++++++++++++++++
|
||||
|
||||
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:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from labthings.schema import Schema
|
||||
from labthings import fields
|
||||
|
||||
class UserSchema(Schema):
|
||||
name = fields.String(required=True)
|
||||
age = fields.Integer(required=True)
|
||||
job = fields.String(required=False, load_default="Unknown")
|
||||
|
||||
To inform your POST method to expect these arguments, use the ``args`` class attribute:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyView(View):
|
||||
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:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyView(View):
|
||||
args = {
|
||||
"name": fields.String(required=True),
|
||||
"age": fields.Integer(required=True),
|
||||
"job": fields.String(required=False, load_default="Unknown")
|
||||
}
|
||||
|
||||
def post(self, args):
|
||||
...
|
||||
|
||||
A compatible request body, in JSON format, may look like:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"name": "John Doe",
|
||||
"age": 45,
|
||||
"job": "Python developer"
|
||||
}
|
||||
|
||||
|
||||
This JSON data is the parsed, converted into a Python dictionary, and passed as an argument. Retreiving the data from within your view function may therefore look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyView(View):
|
||||
args = {
|
||||
"name": fields.String(required=True),
|
||||
"age": fields.Integer(required=True),
|
||||
"job": fields.String(required=False, load_default="Unknown")
|
||||
}
|
||||
|
||||
def post(self, args):
|
||||
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
|
||||
++++++++++++++++++++
|
||||
|
||||
Schemas can also be used to format our data so that it is suitable for an API response. Our API expects JSON formatted data both in, and out. It is therefore important that your API views respond with valid JSON where possible.
|
||||
|
||||
Continuing with our example in the previous pages, we will enhance our ``identify`` method to provide more, better formatted information about our current microscope.
|
||||
|
||||
We start by creating a schema to describe how to serialise a :py:class:`openflexure_microscope.Microscope` object.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Define which properties of a Microscope object we care about,
|
||||
# and what types they should be converted to
|
||||
class MicroscopeIdentifySchema(Schema):
|
||||
name = fields.String() # Microscopes name
|
||||
id = fields.UUID() # Microscopes unique ID
|
||||
state = fields.Dict() # Status dictionary
|
||||
camera = fields.String() # Camera object (represented as a string)
|
||||
stage = fields.String() # Stage object (represented as a string)
|
||||
|
||||
|
||||
We use this new schema in our ``identify`` view like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ExampleIdentifyView(View):
|
||||
# Format our returned object using MicroscopeIdentifySchema
|
||||
schema = MicroscopeIdentifySchema()
|
||||
|
||||
def get(self):
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Return our microscope object,
|
||||
# let schema handle formatting the output
|
||||
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 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.
|
||||
|
||||
Our ``rename`` view class may now look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class ExampleRenameView(View):
|
||||
# Format our returned object using MicroscopeIdentifySchema
|
||||
schema = MicroscopeIdentifySchema()
|
||||
# Expect a request parameter called "name", which is a string. Pass to argument "args".
|
||||
args = {"name": fields.String(required=True, metadata={"example": "My Example Microscope"})}
|
||||
|
||||
def post(self, args):
|
||||
# Look for our "name" parameter in the request arguments
|
||||
new_name = args.get("name")
|
||||
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Pass microscope and new name to our rename function
|
||||
rename(microscope, new_name)
|
||||
|
||||
# Return our microscope object,
|
||||
# let schema handle formatting the output
|
||||
return microscope
|
||||
|
||||
|
||||
Complete example
|
||||
++++++++++++++++
|
||||
|
||||
Combining both of these into our example extension, we now have:
|
||||
|
||||
.. literalinclude:: ./example_extension/03_marshaling_data.py
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
Thing Properties
|
||||
================
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
As well as generating Swagger documentation, the server will generate a draft `W3C Thing Description <https://www.w3.org/TR/wot-thing-description/>`_ . This description allows the microscope's features to be understood in a common "Web of Things" language.
|
||||
|
||||
Thing Properties "expose state of the Thing. This state can then be retrieved (read) and optionally updated (write)." For the microscope, this includes the current read-only state, such as if the microscope has real camera or stage hardware attached, as well as read-write states like camera settings, and the microscope name.
|
||||
|
||||
The property description for a view will be generated automatically from your available view methods, any schema decorators used, and any docstrings added to the view.
|
||||
|
||||
|
||||
Defining Thing Properties
|
||||
-------------------------
|
||||
|
||||
In order to register a view as a Thing property, we use the ``PropertyView`` class, like so:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# Since we only have a GET method here, it'll register as a read-only property
|
||||
class ExampleIdentifyView(PropertyView):
|
||||
# Format our returned object using MicroscopeIdentifySchema
|
||||
schema = MicroscopeIdentifySchema()
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Show identifying information about the current microscope object
|
||||
"""
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Return our microscope object,
|
||||
# let schemah handle formatting the output
|
||||
return microscope
|
||||
|
||||
|
||||
Property schema
|
||||
---------------
|
||||
|
||||
For read-write properties, it is best practice for the expected request arguments, and the views responses, to follow the same format. In this way, by looking at the response of a GET request, one can know the type of data expected in by a PUT request.
|
||||
|
||||
For example, if your GET request returns the JSON:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"name": "John Doe",
|
||||
"age": 45,
|
||||
"job": "Python developer"
|
||||
}
|
||||
|
||||
and your property supports PUT requests (for updating data), then a valid PUT request could contain the data:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"age": 46,
|
||||
"job": "Landscape gardener"
|
||||
}
|
||||
|
||||
This request would update the property, such that a GET request would *now* return:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"name": "John Doe",
|
||||
"age": 46,
|
||||
"job": "Landscape gardener"
|
||||
}
|
||||
|
||||
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 ``schema`` attribute in our ``ExampleRenameView`` view from our previous example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# We can use a single schema as the input and output will be formatted identically
|
||||
# Eg. We always expect a "name" string argument, and always return a "name" string attribute
|
||||
class ExampleRenameView(PropertyView):
|
||||
schema = {"name": fields.String(required=True, metadata={"example": "My Example Microscope"})}
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
Show the current microscope name
|
||||
"""
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
return microscope
|
||||
|
||||
def post(self, args):
|
||||
"""
|
||||
Change the current microscope name
|
||||
"""
|
||||
# Look for our "name" parameter in the request arguments
|
||||
new_name = args.get("name")
|
||||
|
||||
# Find our microscope component
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
||||
# Pass microscope and new name to our rename function
|
||||
rename(microscope, new_name)
|
||||
|
||||
# Return our microscope object,
|
||||
# let schema handle formatting the output
|
||||
return microscope
|
||||
|
||||
Complete example
|
||||
----------------
|
||||
|
||||
Combining these into our example extension, we now have:
|
||||
|
||||
.. literalinclude:: ./example_extension/04_properties.py
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
Basic extension structure
|
||||
=========================
|
||||
|
||||
An extension starts as a subclass of :py:class:`labthings.extensions.BaseExtension`.
|
||||
Each extension is described by a single ``BaseExtension`` instance, containing any number of methods, API views, and additional hardware components.
|
||||
|
||||
You will build your extension by subclassing :py:class:`labthings.extensions.BaseExtension`, and adding the class to a top-level `LABTHINGS_EXTENSIONS` list.
|
||||
|
||||
In order to access the currently running microscope object, use the :py:func:`labthings.find_component` function, with the argument ``"org.openflexure.microscope"``. Likewise, any new components attached by other extensions can be found using their full name, as above.
|
||||
|
||||
A simple extension file, with no API views but application-available methods may look like:
|
||||
|
||||
.. literalinclude:: ./example_extension/01_basic_structure.py
|
||||
|
||||
|
||||
Once this extension is loaded, any other extensions will have access to your methods:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from labthings import find_extension
|
||||
|
||||
def test_extension_method():
|
||||
# Find your extension. Returns None if it hasn't been found.
|
||||
my_found_extension = find_extension("com.myname.myextension")
|
||||
|
||||
# Call a function from your extension
|
||||
if my_found_extension:
|
||||
my_found_extension.identify()
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
Threads and Locks
|
||||
=================
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
Some actions in your extension may perform tasks that take a long time (compared to the expected response time of a web request). For example, if you were to implement a timelapse feature, this inherently runs over a long time.
|
||||
|
||||
This introduces a couple of problems. Firstly, a request that triggers a long function will, by default, block the Python interpreter for the duration of the function. This usually causes the connection to timeout, and the response will never be revieved.
|
||||
|
||||
Similarly, if your functionality takes a long time, it may be possible for other requests to interfere with your function. For example, in our hypothetical timelapse extension, while the timelapse is running, another user could open a connection and start moving the stage around, ruining the timelapse.
|
||||
|
||||
We get around these issues by making use of action threads, and component locks.
|
||||
|
||||
Action threads
|
||||
--------------
|
||||
|
||||
Action threads are introduced to manage long-running functions in a way that does not block HTTP requests. Any API Action will automatically run as a background thread.
|
||||
|
||||
Internally, the :class:`labthings.LabThing` object stores a list of all requested actions, and their states. This state stores the running status of the action (if itis idle, running, error, or success), information about the start and end times, a unique ID, and, upon completion, the return value of the long-running function.
|
||||
|
||||
By using threads, a function can be started in the background, and it's return value fetched at a later time once it has reported success. If a long-running action is started by some client, it should note the ID returned in the action state JSON, and use this to periodically check on the status of that particular action.
|
||||
|
||||
API routes have been created to allow checking the state of all actions (GET ``/actions``), a particular action by ID (GET ``/actions/<action_id>``), and stopping or removing individual actions (DELETE ``/actions/<action_id>``).
|
||||
|
||||
All actions will return a serialized representation of the action state when your POST request returns. If the action completes within a default timeout period (usually 1 second) then the completed action representation will be returned. If the action is still running after this timeout period, the "in-progress" action representation will be returned. The final output value can then be retrieved at a later time.
|
||||
|
||||
Most users will not need to create instances of this class. Instead, they will be created automatically when a function is started by an API Action view.
|
||||
|
||||
An example of a long running task may look like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
...
|
||||
from labthings import ActionView
|
||||
|
||||
class SlowAPI(ActionView):
|
||||
def post(self):
|
||||
# Return the task object.
|
||||
return long_running_function(function_argument_1, function_argument_2)
|
||||
|
||||
After some time, once the task has completed, it could be retreived using:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
...
|
||||
from labthings import current_labthing
|
||||
|
||||
def get_result(action_id):
|
||||
matching_action = current_labthing().actions.get(task_id)
|
||||
return matching_action.state
|
||||
|
||||
or by making GET requests to the ``http://microscope.local/api/v2/tasks/<task_id>`` view.
|
||||
|
||||
|
||||
Accessing the current action instance
|
||||
+++++++++++++++++++++++++++++++++++++
|
||||
|
||||
Every time a user requests your action, a new :class:`labthings.actions.ActionThread` instance is created to hold the state of your action. This object holds return values, errors, action progress and status, and handles action cancellation.
|
||||
|
||||
In some cases, your action function will need to access the currently running :class:`labthings.actions.ActionThread` instance. The :func:`labthings.current_action` function will return the currently running :class:`labthings.actions.ActionThread` instance if it's called from within an ``ActonThread``, and will return ``None`` if running outside of an `ActionThread`.
|
||||
|
||||
|
||||
Handling action cancellation
|
||||
++++++++++++++++++++++++++++
|
||||
|
||||
Users always have the option to stop an action while it's running. Your action function has the option to support an elegant cancellation by watching for cancellation requests on the running :class:`labthings.actions.ActionThread` instance.
|
||||
|
||||
The ``labthings.current_action().stopped`` attribute will return ``True`` if the Action has been requested to stop, and ``False`` otherwise. If your action runs a loop, this can be checked at each iteration, and used to return early if the action has been stopped.
|
||||
|
||||
If a stop request is sent and your action does not return within a timeout (by default 5 seconds), then the thread will be forcefully terminated. This is to ensure that actions can be stopped even if they have become stuck, or would otherwise take an unexpected amount of time. However, every effort should be made to handle action cancellation elegantly from within the action.
|
||||
|
||||
An example of elegant action cancellation is included in the example later on this page.
|
||||
|
||||
The ``ActionView.default_stop_timeout`` class attribute can be used to increase or descrease the forced cancellation timeout. Developers should carefully consider how long their action should take to elegantly stop, and avoid abusing this timeout override to simply prevent forceful cancelltion.
|
||||
|
||||
|
||||
Updating action progress
|
||||
++++++++++++++++++++++++
|
||||
|
||||
Some applications such as OpenFlexure eV are able to display progress bars showing the progress of an action thread. Implementing progress updates in your extension is made easy with the :py:meth:`labthings.update_action_progress` function. This function takes a single argument, which is the action progress as an integer percent (0 - 100).
|
||||
|
||||
If your long running function was started within a background thread, this function will update the state of the corresponding action thread object. If your function is called outside of a long-running task (e.g. by another extension, directly), then this function will silently do nothing.
|
||||
|
||||
An example of task progress is included in the example later on this page.
|
||||
|
||||
|
||||
Component Locks
|
||||
---------------
|
||||
|
||||
Locks have been implemented to solve a distinct issue, most obvious when considering long-running actions. During a long action such as a tile-scan or autofocus, it is absolutely necesarry to block any completing interaction with the microscope hardware. For example, even if the stage is not actively moving (for example during a capture phase within a tile scan), another user should not be able to move the microscope, interrupting the action. Thread locks act to prevent this.
|
||||
|
||||
The camera and stage both contain an instance of :py:class:`labthings.lock.StrictLock`, named ``lock``. Built-in functions such as capture and move will always acquire this lock for the duration of the function. This ensures that, for example, simultaneous attemps to move do not occur.
|
||||
|
||||
More importantly, however, threads can hold on to these locks for longer periods of time, blocking any other calls to the hardware.
|
||||
|
||||
Locks are acquired using context managers, i.e. ``with component.lock: ...``
|
||||
|
||||
|
||||
Complete example
|
||||
----------------
|
||||
|
||||
Implementing both action threads and locks in a new timelapse extension may look like:
|
||||
|
||||
.. literalinclude:: ./example_extension/06_tasks_locks.py
|
||||
|
||||
Notice that even though we never use the stage here, our ``timelapse`` function still acquires the stage lock. This means that during the timelapse, no other user is able to move the stage, or take separate captures. Control of the microscope is handed exclusively to the thread that obtains the lock, which in this case is the thread spawned when handling the POST request.
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
Adding web API views
|
||||
====================
|
||||
|
||||
Key terminology
|
||||
---------------
|
||||
|
||||
API View (or View)
|
||||
++++++++++++++++++
|
||||
|
||||
*"A view function is the code you write to respond to requests to your application [...] For RESTful APIs it’s especially helpful to execute a different function for each HTTP method. With the [View class] you can easily do that. Each HTTP method maps to a function with the same name (just in lowercase)"* - `Flask documentation <https://flask.palletsprojects.com/en/1.1.x/views/>`_
|
||||
|
||||
Introduction
|
||||
------------
|
||||
Extensions can create views to expose extension functionality via the web API. Creating API views for your extension is strongly recommended, as this is the primary way we encourage interaction with the microscope device.
|
||||
|
||||
As with most HTTP APIs, we make use of basic HTTP request methods. GET requests return data without modifying any state. POST requests completely replace data with data passed as request arguments. PUT requests update data with new data passed as request arguments. DELETE requests delete a particular object from the server. Your API views need not implement all of these methods.
|
||||
|
||||
Continuing our example on the previous page, and discussed below, adding API views may look like:
|
||||
|
||||
.. literalinclude:: ./example_extension/02_adding_views.py
|
||||
|
||||
Note that we are now passing our microscope object as an argument to our API methods. Finding the microscope component is performed by the API view at request-time, and passed onto the functions.
|
||||
|
||||
Your extension functions can be accessed from within an API View by using ``self.extension``. Once your view has been added to your extension, this will point to the extension object, allowing your API views to use your extension functionality.
|
||||
|
||||
In this case, our extension will have two new API views at `/identify` and `/rename`. The `/identify` view only accepts GET requests, and the `/rename` view only accepts POST requests.
|
||||
|
||||
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 ``args`` class attribute to provide this functionality.
|
||||
|
||||
``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.
|
||||
|
||||
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
|
||||
+++++++++++++++++++++
|
||||
|
||||
At this point, it is useful to introduce the automatically generated Swagger documentation. From any web browser, go to ``http://microscope.local/api/v2/docs/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 the view ``schema``.
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
Welcome to OpenFlexure Microscope Software's documentation!
|
||||
===========================================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
quickstart.rst
|
||||
webapp/index.rst
|
||||
config.rst
|
||||
microscope.rst
|
||||
camera.rst
|
||||
stage.rst
|
||||
plugins.rst
|
||||
api.rst
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
Microscope class
|
||||
=======================================================
|
||||
|
||||
The main microscope class handles microscope settings, passing these between the settings file and their appropriate components (camera, stage), basic metadata about the current device status, and interfacing with the separate camera and stage components.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
.. automodule:: openflexure_microscope.microscope
|
||||
:members:
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
Raspberry Pi Streaming Camera
|
||||
=======================================================
|
||||
|
||||
.. automodule:: openflexure_microscope.camera.pi
|
||||
:members:
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
Developing API Extensions
|
||||
=========================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
./extensions/introduction.rst
|
||||
./extensions/structure.rst
|
||||
./extensions/views.rst
|
||||
./extensions/marshaling.rst
|
||||
./extensions/properties.rst
|
||||
./extensions/actions.rst
|
||||
./extensions/tasks_locks.rst
|
||||
./extensions/ev_gui.rst
|
||||
./extensions/lifecycle_hooks.rst
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
Quickstart
|
||||
=======================================================
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
Stable installation
|
||||
+++++++++++++++++++
|
||||
The OpenFlexure Microscope software is designed to be run on the embedded Raspberry Pi, in an OpenFlexure Microscope. For most users, our `pre-built Raspbian SD card image. <https://openflexure.org/projects/microscope/install>`_ is the easiest way to get started. This SD card image is based on Raspberry Pi OS and includes both the microscope server and OpenFlexure Connect. A desktop shortcut will directly start OpenFlexure Connect if you are using the Raspberry Pi directly, or the microscope can be controlled over the network with its default hostname `raspberrypi.local`.
|
||||
|
||||
|
||||
Manual installation
|
||||
+++++++++++++++++++
|
||||
|
||||
To install the server on a Raspberry Pi without using the pre-built OpenFlexure Raspbian image, or to install the server on a different system (this is useful for development), follow the instructions in the README file at the top level of the project's repository.
|
||||
|
||||
Usage
|
||||
-----
|
||||
The easiest way to use the microscope is through OpenFlexure Connect, our cross-platform application that handles discovering and connecting to the microscope. It is detailed on the `instruction page on our website <https://openflexure.org/projects/microscope/control>`_ including a download link. OpenFlexure Connect is pre-installed on the full SD card image (not the "lite" image, as this does not have support for a graphical desktop).
|
||||
|
||||
If you know the hostname or IP address of your microscope, you can also connect to the same interface using a web browser by entering `http://microscope.local:5000/` as the address. `microscope.local` is the default hostname of the microscope if you use our pre-built SD card image. If you know the IP address or have customised the hostname, you can use that instead. Note that the hostname is announced via mDNS, which is usually reliable if the microscope is connected via a network cable directly to your client computer but may not work if both devices are connected to a more complicated network. As support for mDNS varies between operating systems, OpenFlexure Connect often detects microscopes even if you cannot resolve the microscope using the mDNS hostname.
|
||||
|
||||
Whether you connect with OpenFlexure Connect, or through a web browser, the web application interface is the same. See the "web application" section of this manual for more details.
|
||||
|
||||
Managing the server
|
||||
-------------------
|
||||
|
||||
Managing the server through the installer script's CLI is documented `on our website <https://openflexure.org/projects/microscope/install#managing-the-microscope-server>`_.
|
||||
|
||||
This includes starting the server as a background service, as well as starting a development server with real-time debug logging.
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
Sangaboard Microscope Stage
|
||||
===========================
|
||||
|
||||
.. automodule:: openflexure_microscope.stage.sanga
|
||||
:members:
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
Stage Class
|
||||
===========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
sangastage.rst
|
||||
basestage.rst
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
Web Application interface
|
||||
=========================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
pane_navigate
|
||||
pane_capture
|
||||
pane_settings
|
||||
pane_gallery
|
||||
|
||||
The main graphical interface for the OpenFlexure Microscope is implemented as a web application, which allows it to be accessed either through OpenFlexure Connect or a web browser. See the :doc:`../quickstart` page for connection instructions. A "tour" should guide users through the interface when they connect for the first time, and introduce the key interface elements.
|
||||
|
||||
Interface structure
|
||||
-------------------
|
||||
The main interface has a tab bar on the left, which allows the operator to select different controls. By default, the "view" pane does not show any additional controls, and a video feed from the camera fills the window.
|
||||
|
||||
.. image:: pane_view.png
|
||||
|
||||
Selecting one of the tab icons on the left will bring up the corresponding interface. Most of the pages display the image on the right hand side, and add additional controls between the image display and the tab bar.
|
||||
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0dcbbfbc4d5ba77824083b497b015b4178ec47f375f8e0457cb103da5befd2cf
|
||||
size 1971891
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
Capture pane
|
||||
============
|
||||
The capture pane allows images to be acquired through the interface. By default, a single image is captured to the microscope's internal SD card, and can be downloaded from the gallery. Various settings are available to control the resolution of the image captured: the "full resolution" checkbox will save the image at native resolution, and the "store raw data" checkbox saves raw pixel data as an EXIF annotation.
|
||||
|
||||
Due to the underlying ``picamera`` library, images are always saved as JPEG files, and raw data is simply appended to the file for later extraction. If an image is saved with raw data, the JPEG image is still the processed, compressed version; an external tool must be used to extract and process the raw Bayer data.
|
||||
|
||||
.. image:: pane_capture.png
|
||||
|
||||
It is also possible to acquire a grid of images for stitching together into a mosaic, by expanding the "Stack and Scan" section and enabling the "Scan capture" checkbox. Scans are 3D by default (i.e. a mosaic of images in X and Y, with a Z stack at each position) but 2D or linear scans can be performed by setting the number of steps in the unused axes to 1. This allows XY mosaics or Z stacks to be performed.
|
||||
|
||||
When scanning is enabled, the "capture" button is replaced by a "start scan" button, which will start the scan and display a progress indicator until scanning has finished.
|
||||
|
||||
.. image:: scan_dialog.png
|
||||
|
||||
The step sizes (in motor steps) for each axis specify the number of motor steps to move between images, then the "steps" fields specify the number of images to acquire along each axis. If "steps" is set to 1 for any axis, no scanning happens along that axis.
|
||||
|
||||
The scan routine will move through XY coordinates, and at each XY position will optionally run an autofocus routine, then acquire either a single image or a Z stack depending on the value of "z steps". The autofocus options correspond to those available in the "move" pane, allowing "fast" autofocus, or conventional autofocus with coarse, medium, or fine steps to be used. Selecting "none" disables autofocus. The vast majority of the time, "fast" autofocus is both quicker and more accurate than the other methods.
|
||||
|
||||
Various scan patterns are available for XY scanning. Raster scanning is the default, which scans columns (i.e. from lowest to highest Y coordinate) and works from low to high X coordinates as the "slow" scan axis. Snake scanning reverses every other column, such that there is a smaller distance from the end of one column to the beginning of the next. This can be helpful if the sample is not perfectly flat, as it avoids losing focus between columns. Finally, "spiral" scanning starts by taking an image at the current position, then works outwards in concentric squares. Spiral scans use the "x steps" value to set the number of rings, and ignore the "y steps" value.
|
||||
|
||||
Images acquired during the scan will be saved to a folder on the Raspberry Pi. They can be named according to their coordinates in the scan (default) or numbered sequentially (in case the latter is easier to process).
|
||||
|
||||
To retrieve images acquired during a scan, or captured individually, you can use the :doc:`pane_gallery`.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4c5dc30ff335f782d9a5c9d5568e13787f63be7e5d0a46c73afd5d525e79d557
|
||||
size 1370769
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
Gallery pane
|
||||
============
|
||||
|
||||
.. image:: pane_gallery.png
|
||||
|
||||
The gallery displays all the images currently stored on the microscope. Scans are grouped together into folders. Clicking on an image will display it in a "lightbox" view that allows scrolling through all images in the current view. When an image is displayed in the lightbox view, it may be right-clicked to download it. Multiple images can also be downloaded as a zip archive.
|
||||
|
||||
Bulk transfer of images is often easier using SCP, and images are stored by default in ``/var/openflexure/data/micrographs/`` on the Raspberry Pi.
|
||||
|
||||
Saving of images to external storage is possible - this can be configured using the "autostorage" plugin, which currently displays an SD card icon in the navigation bar at the left of the screen.
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:400d4f25aef0fecddbab296d982fb881367f1c5b6cdbd83b592bbbd142f3ef04
|
||||
size 3184560
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
Navigate pane
|
||||
=============
|
||||
|
||||
The navigate pane displays the current stage position. Editing the values for X, Y, and Z and then hitting "enter" or clicking "move" will move the stage to the specified coordinates. Using the arrow keys (or page up/down) when not editing a text box will also move the stage, and the step size used can be set in the "configure" section at the top of the pane (which is collapsed by default). Using the mouse scroll wheel on the image will also move in Z, using the same configured step size. Double clicking on the image will bring the point clicked to the centre of the field of view, if the camera-stage mapping has been calibrated (see :doc:`pane_settings`).
|
||||
|
||||
Autofocus can also be run from the navigate pane, by clicking the "fast", "medium" or "fine" buttons. Fast autofocus moves the stage up and down in a continuous motion, using the size of images in the MJPEG stream from the camera to determine the sharpest point. This is usually both faster and more accurate than the other methods, however the other two options use a different metric, and stop the stage for each measurement. This can lead to them being more reliable in some circumstances.
|
||||
|
||||
.. image:: pane_navigate.png
|
||||
|
||||
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ac21613d68c30c3e9038f71d738d41a266e6d99f3f93c9b92f833f1564ccd179
|
||||
size 668472
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
Settings pane
|
||||
=============
|
||||
|
||||
The settings for the microscope are gathered together into a settings pane, which is further subdivided into sections. This page does not provide an exhaustive list, but a few of the notable controls are:
|
||||
|
||||
* Adjusting exposure time and gain of the camera, including automatic adjustment.
|
||||
* Automatic white balance and flat-field correction for the camera.
|
||||
* Enabling or disabling certain features of the software (e.g. ImJoy integration).
|
||||
* Enabling or disabling the video stream (this allows the native low-latency preview on the Raspberry Pi to be used instead).
|
||||
* Calibrating the relationship between stage coordinates and pixel coordinates in the video stream, allowing click-to-move to function.
|
||||
|
||||
Important calibration tasks (in particular camera settings adjustment and click-to-move calibration) will be prompted in a "wizard" dialogue when the software is first run, to help first time users set up their microscope. All of the auto calibration routines are also available from the settings pane.
|
||||
|
||||
.. image:: pane_settings.png
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aca1cddcdd8038920748196f95307d8d0110ed702990dd4831b1566afe01dabd
|
||||
size 1930201
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5bc4944c89be5f43b178145fc7ff8c414dac1716275dc83e02a4bc23493aa2fb
|
||||
size 32284
|
||||
Loading…
Add table
Add a link
Reference in a new issue