Merge branch 'master' into 'rwb27-plugin-comments'

# Conflicts:
#   docs/source/extensions/structure.rst
This commit is contained in:
Joel Collins 2020-03-24 11:17:11 +00:00
commit 7c59245753
65 changed files with 1829 additions and 844 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
openflexure_microscope/api/static/** filter=lfs diff=lfs merge=lfs -text

View file

@ -226,7 +226,7 @@ intersphinx_mapping = {
"openflexure_stage": ("https://openflexure-stage.readthedocs.io/en/latest/", None), "openflexure_stage": ("https://openflexure-stage.readthedocs.io/en/latest/", None),
"picamera": ("https://picamera.readthedocs.io/en/release-1.13/", None), "picamera": ("https://picamera.readthedocs.io/en/release-1.13/", None),
"marshmallow": ("https://marshmallow.readthedocs.io/en/stable/", None), "marshmallow": ("https://marshmallow.readthedocs.io/en/stable/", None),
"webargs": ("https://webargs.readthedocs.io/en/latest/", None) "webargs": ("https://webargs.readthedocs.io/en/latest/", None),
} }
# -- Options for todo extension ---------------------------------------------- # -- Options for todo extension ----------------------------------------------

View file

@ -0,0 +1,41 @@
from labthings.server.extensions import BaseExtension
from labthings.server.find import find_component
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Create some instance variable
self.state_variable = "An example of a persistant instance variable"
# 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()
# Create your extension object
my_extension = MyExtension()

View file

@ -14,7 +14,7 @@ from labthings.server import fields
class MicroscopeIdentifySchema(Schema): class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID id = fields.UUID() # Microscopes unique ID
status = fields.Dict() # Status dictionary state = fields.Dict() # Status dictionary
camera = fields.String() # Camera object (represented as a string) camera = fields.String() # Camera object (represented as a string)
stage = fields.String() # Stage object (represented as a string) stage = fields.String() # Stage object (represented as a string)

View file

@ -19,7 +19,7 @@ from labthings.server import fields
class MicroscopeIdentifySchema(Schema): class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID id = fields.UUID() # Microscopes unique ID
status = fields.Dict() # Status dictionary state = fields.Dict() # Status dictionary
camera = fields.String() # Camera object (represented as a string) camera = fields.String() # Camera object (represented as a string)
stage = fields.String() # Stage object (represented as a string) stage = fields.String() # Stage object (represented as a string)

View file

@ -24,7 +24,7 @@ import io # Used in our capture action
class MicroscopeIdentifySchema(Schema): class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID id = fields.UUID() # Microscopes unique ID
status = fields.Dict() # Status dictionary state = fields.Dict() # Status dictionary
camera = fields.String() # Camera object (represented as a string) camera = fields.String() # Camera object (represented as a string)
stage = fields.String() # Stage object (represented as a string) stage = fields.String() # Stage object (represented as a string)

View file

@ -0,0 +1,53 @@
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

View file

@ -107,7 +107,7 @@ We start by creating a schema to describe how to serialise a :py:class:`openflex
class MicroscopeIdentifySchema(Schema): class MicroscopeIdentifySchema(Schema):
name = fields.String() # Microscopes name name = fields.String() # Microscopes name
id = fields.UUID() # Microscopes unique ID id = fields.UUID() # Microscopes unique ID
status = fields.Dict() # Status dictionary state = fields.Dict() # Status dictionary
camera = fields.String() # Camera object (represented as a string) camera = fields.String() # Camera object (represented as a string)
stage = fields.String() # Stage object (represented as a string) stage = fields.String() # Stage object (represented as a string)

View file

@ -24,3 +24,13 @@ Once this extension is loaded, any other extensions will have access to your met
# Call a function from your extension # Call a function from your extension
if my_found_extension: if my_found_extension:
my_found_extension.identify() my_found_extension.identify()
Subclassing ``BaseExtension``
-------------------------------
The syntax used above allows novice programmers to easily start building extensions, without having to deal with subclassing. However, for more complex extensions which require persistent state, subclassing :py:class:`labthings.server.extensions.BaseExtension` is recommended.
The same simple extension as seen above can be written using subclassing:
.. literalinclude:: ./example_extension/01b_basic_structure_subclass.py

View file

@ -11,4 +11,5 @@ Developing API Extensions
./extensions/properties.rst ./extensions/properties.rst
./extensions/actions.rst ./extensions/actions.rst
./extensions/tasks_locks.rst ./extensions/tasks_locks.rst
./extensions/ev_gui.rst ./extensions/ev_gui.rst
./extensions/lifecycle_hooks.rst

View file

@ -1,6 +0,0 @@
__all__ = ["Microscope", "config", "utilities"]
__version__ = "0.1.0"
from .microscope import Microscope
from . import config
from . import utilities

View file

@ -2,7 +2,7 @@
import time import time
import atexit import atexit
import logging import logging, logging.handlers
import os import os
import pkg_resources import pkg_resources
@ -16,10 +16,10 @@ from openflexure_microscope.api.utilities import list_routes, init_default_exten
from openflexure_microscope.config import JSONEncoder from openflexure_microscope.config import JSONEncoder
from openflexure_microscope.paths import ( from openflexure_microscope.paths import (
OPENFLEXURE_ETC_PATH,
OPENFLEXURE_VAR_PATH, OPENFLEXURE_VAR_PATH,
OPENFLEXURE_EXTENSIONS_PATH, OPENFLEXURE_EXTENSIONS_PATH,
settings_file_path, settings_file_path,
logs_file_path,
) )
from labthings.server.quick import create_app from labthings.server.quick import create_app
@ -30,37 +30,29 @@ from openflexure_microscope.api.microscope import default_microscope as api_micr
from openflexure_microscope.api.v2 import views from openflexure_microscope.api.v2 import views
# Handle logging # Handle logging
is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "") DEFAULT_LOGFILE = logs_file_path("openflexure_microscope.log")
DEFAULT_LOGFILE = settings_file_path("openflexure_microscope.log")
logger = logging.getLogger() logger = logging.getLogger()
if (__name__ == "__main__") or (not is_gunicorn):
# If imported, but not by gunicorn
print("Letting sys handle logs")
logger.setLevel(logging.DEBUG)
else:
# Direct standard Python logging to file and console
error_formatter = logging.Formatter(
"[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
)
rotating_logfile = logging.handlers.RotatingFileHandler( error_formatter = logging.Formatter(
DEFAULT_LOGFILE, maxBytes=1_000_000, backupCount=7 "[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s"
) )
error_handlers = [rotating_logfile, logging.StreamHandler()] rotating_logfile = logging.handlers.RotatingFileHandler(
DEFAULT_LOGFILE, maxBytes=1_000_000, backupCount=7
)
for handler in error_handlers: error_handlers = [rotating_logfile, logging.StreamHandler()]
handler.setFormatter(error_formatter)
logger.addHandler(handler)
logger.setLevel(logging.getLogger("gunicorn.error").level) for handler in error_handlers:
handler.setFormatter(error_formatter)
logger.addHandler(handler)
logger.setLevel(logging.INFO)
# Log server paths being used # Log server paths being used
logging.info(f"Running with data path {OPENFLEXURE_VAR_PATH}") logging.info(f"Running with data path {OPENFLEXURE_VAR_PATH}")
logging.info(f"Running with server path {OPENFLEXURE_ETC_PATH}")
# Create flask app # Create flask app
app, labthing = create_app( app, labthing = create_app(
@ -69,6 +61,7 @@ app, labthing = create_app(
title=f"OpenFlexure Microscope {api_microscope.name}", title=f"OpenFlexure Microscope {api_microscope.name}",
description="Test LabThing-based API for OpenFlexure Microscope", description="Test LabThing-based API for OpenFlexure Microscope",
version=pkg_resources.get_distribution("openflexure_microscope").version, version=pkg_resources.get_distribution("openflexure_microscope").version,
flask_kwargs={"static_url_path": ""},
) )
# Enable CORS for some routes outside of LabThings # Enable CORS for some routes outside of LabThings
@ -93,15 +86,21 @@ labthing.add_root_link(views.CaptureList, "captures")
labthing.add_view(views.CaptureView, f"/captures/<id>") labthing.add_view(views.CaptureView, f"/captures/<id>")
labthing.add_view(views.CaptureDownload, f"/captures/<id>/download/<filename>") labthing.add_view(views.CaptureDownload, f"/captures/<id>/download/<filename>")
labthing.add_view(views.CaptureTags, f"/captures/<id>/tags") labthing.add_view(views.CaptureTags, f"/captures/<id>/tags")
labthing.add_view(views.CaptureMetadata, f"/captures/<id>/metadata") labthing.add_view(views.CaptureAnnotations, f"/captures/<id>/annotations")
# Attach settings and state resources
labthing.add_view(views.SettingsProperty, f"/instrument/settings")
labthing.add_root_link(views.SettingsProperty, "instrumentSettings")
labthing.add_view(views.NestedSettingsProperty, "/instrument/settings/<path:route>")
labthing.add_view(views.StateProperty, "/instrument/state")
labthing.add_view(views.NestedStateProperty, "/instrument/state/<path:route>")
labthing.add_root_link(views.StateProperty, "instrumentState")
labthing.add_view(views.ConfigurationProperty, "/instrument/configuration")
labthing.add_view(
views.NestedConfigurationProperty, "/instrument/configuration/<path:route>"
)
labthing.add_root_link(views.ConfigurationProperty, "instrumentConfiguration")
# Attach settings and status resources
labthing.add_view(views.SettingsProperty, f"/settings")
labthing.add_root_link(views.SettingsProperty, "settings")
labthing.add_view(views.NestedSettingsProperty, "/settings/<path:route>")
labthing.add_view(views.StatusProperty, "/status")
labthing.add_view(views.NestedStatusProperty, "/status/<path:route>")
labthing.add_root_link(views.StatusProperty, "status")
# Attach streams resources # Attach streams resources
labthing.add_view(views.MjpegStream, f"/streams/mjpeg") labthing.add_view(views.MjpegStream, f"/streams/mjpeg")
@ -109,12 +108,18 @@ labthing.add_view(views.SnapshotStream, f"/streams/snapshot")
# Attach microscope action resources # Attach microscope action resources
labthing.add_view(views.actions.ActionsView, "/actions") labthing.add_view(views.actions.ActionsView, "/actions")
labthing.add_root_link(views.actions.ActionsView, "actions")
for name, action in views.enabled_root_actions().items(): for name, action in views.enabled_root_actions().items():
view_class = action["view_class"] view_class = action["view_class"]
rule = action["rule"] rule = action["rule"]
labthing.add_view(view_class, f"/actions{rule}") labthing.add_view(view_class, f"/actions{rule}")
@app.route("/")
def openflexure_ev():
return app.send_static_file("index.html")
@app.route("/routes") @app.route("/routes")
@cross_origin() @cross_origin()
def routes(): def routes():

View file

@ -4,6 +4,7 @@ import traceback
from .autofocus import autofocus_extension_v2 from .autofocus import autofocus_extension_v2
from .scan import scan_extension_v2 from .scan import scan_extension_v2
from .zip_builder import zip_extension_v2 from .zip_builder import zip_extension_v2
from .autostorage import autostorage_extension_v2
# "Gracefully" handle cases where picamera cannot be imported (eg test server) # "Gracefully" handle cases where picamera cannot be imported (eg test server)
try: try:
@ -11,4 +12,4 @@ try:
except Exception as e: except Exception as e:
logging.error( logging.error(
f"Exception loading builtin extension picamera_autocalibrate: \n{traceback.format_exc()}" f"Exception loading builtin extension picamera_autocalibrate: \n{traceback.format_exc()}"
) )

View file

@ -1,10 +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 from labthings.server.view import View
from labthings.server.decorators import ( from labthings.server.decorators import ThingAction, ThingProperty, marshal_task
ThingAction,
ThingProperty,
)
from openflexure_microscope.devel import JsonResponse, request, jsonify, taskify, abort from openflexure_microscope.devel import JsonResponse, request, jsonify, taskify, abort
from openflexure_microscope.utilities import set_properties from openflexure_microscope.utilities import set_properties
@ -198,7 +195,7 @@ def move_and_find_focus(microscope, dz):
def fast_autofocus(microscope, dz=2000, backlash=None): def fast_autofocus(microscope, dz=2000, backlash=None):
"""Perform a down-up-down-up autofocus""" """Perform a down-up-down-up autofocus"""
with monitor_sharpness(microscope) as m: with monitor_sharpness(microscope) as m, microscope.camera.lock:
i, z = m.focus_rel(-dz / 2) i, z = m.focus_rel(-dz / 2)
i, z = m.focus_rel(dz) i, z = m.focus_rel(dz)
fz = m.sharpest_z_on_move(i) fz = m.sharpest_z_on_move(i)
@ -211,7 +208,7 @@ def fast_autofocus(microscope, dz=2000, backlash=None):
def fast_up_down_up_autofocus( def fast_up_down_up_autofocus(
microscope, dz=2000, target_z=0, initial_move_up=True, mini_backlash=150 microscope, dz=2000, target_z=0, initial_move_up=True, mini_backlash=25
): ):
"""Autofocus by measuring on the way down, and moving back up with feedback. """Autofocus by measuring on the way down, and moving back up with feedback.
@ -242,7 +239,7 @@ def fast_up_down_up_autofocus(
from the starting position. Mostly useful if you're able to combine from the starting position. Mostly useful if you're able to combine
the initial move with something else, e.g. moving to the next scan point. the initial move with something else, e.g. moving to the next scan point.
mini_backlash: (optional, default 50) is a small extra move made in step mini_backlash: (optional, default 25) is a small extra move made in step
3 to help counteract backlash. It should be small enough that you 3 to help counteract backlash. It should be small enough that you
would always expect there to be greater backlash than this. Too small would always expect there to be greater backlash than this. Too small
might slightly hurt accuracy, but is unlikely to be a big issue. Too big might slightly hurt accuracy, but is unlikely to be a big issue. Too big
@ -308,6 +305,7 @@ class AutofocusAPI(View):
Run a standard autofocus Run a standard autofocus
""" """
@marshal_task
def post(self): def post(self):
payload = JsonResponse(request) payload = JsonResponse(request)
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
@ -323,7 +321,7 @@ class AutofocusAPI(View):
task = taskify(autofocus)(microscope, dz) task = taskify(autofocus)(microscope, dz)
# return a handle on the autofocus task # return a handle on the autofocus task
return jsonify(task.state), 201 return task
else: else:
abort(503, "No stage connected. Unable to autofocus.") abort(503, "No stage connected. Unable to autofocus.")
@ -335,6 +333,7 @@ class FastAutofocusAPI(View):
Run a fast autofocus Run a fast autofocus
""" """
@marshal_task
def post(self): def post(self):
payload = JsonResponse(request) payload = JsonResponse(request)
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
@ -344,24 +343,27 @@ class FastAutofocusAPI(View):
# Figure out the parameters to use # Figure out the parameters to use
dz = payload.param("dz", default=2000, convert=int) dz = payload.param("dz", default=2000, convert=int)
backlash = payload.param("backlash", default=0, convert=int) backlash = payload.param("backlash", default=25, convert=int)
if backlash < 0: if backlash < 0:
backlash = 0 backlash = 0
if microscope.has_real_stage(): if microscope.has_real_stage():
logging.info("Running autofocus...") logging.info("Running autofocus...")
task = taskify(fast_autofocus)(microscope, dz, backlash=backlash) task = taskify(fast_up_down_up_autofocus)(microscope, dz=dz, mini_backlash=backlash)
# return a handle on the autofocus task # return a handle on the autofocus task
return jsonify(task.state), 201 return task
else: else:
abort(503, "No stage connected. Unable to autofocus.") abort(503, "No stage connected. Unable to autofocus.")
autofocus_extension_v2 = BaseExtension("org.openflexure.autofocus", version="2.0.0-beta.1") autofocus_extension_v2 = BaseExtension(
"org.openflexure.autofocus", version="2.0.0"
)
autofocus_extension_v2.add_method(fast_autofocus, "fast_autofocus") autofocus_extension_v2.add_method(fast_autofocus, "fast_autofocus")
autofocus_extension_v2.add_method(fast_up_down_up_autofocus, "fast_up_down_up_autofocus")
autofocus_extension_v2.add_method(autofocus, "autofocus") autofocus_extension_v2.add_method(autofocus, "autofocus")
autofocus_extension_v2.add_view(MeasureSharpnessAPI, "/measure_sharpness") autofocus_extension_v2.add_view(MeasureSharpnessAPI, "/measure_sharpness")

View file

@ -0,0 +1,271 @@
from labthings.server.extensions import BaseExtension
from labthings.server.view import View
from labthings.server.decorators import ThingProperty, PropertySchema, use_args
from labthings.server import fields
from labthings.server.find import find_component
from openflexure_microscope.paths import settings_file_path, check_rw
from openflexure_microscope.config import OpenflexureSettingsFile
from openflexure_microscope.camera.base import BASE_CAPTURE_PATH
from openflexure_microscope.camera.capture import build_captures_from_exif
from openflexure_microscope.api.utilities.gui import build_gui
from flask import abort
import logging
import os
import psutil
from sys import platform
AS_SETTINGS_PATH = settings_file_path("autostorage_settings.json")
def get_partitions():
return [disk.mountpoint for disk in psutil.disk_partitions() if "rw" in disk.opts]
def get_permissive_partitions():
return [partition for partition in get_partitions() if check_rw(partition)]
def get_permissive_locations():
return [
(partition, os.path.join(partition, "openflexure", "data", "micrographs"))
for partition in get_permissive_partitions()
]
def get_current_location(camera):
return camera.paths.get("default")
def set_current_location(camera, location: str):
if not os.path.isdir(location):
os.makedirs(location)
logging.debug("Updating location...")
camera.paths.update({"default": location})
logging.debug("Rebuilding captures...")
camera.rebuild_captures()
logging.debug("Capture location changed successfully.")
def get_default_location():
return BASE_CAPTURE_PATH
def get_all_locations():
locations = {}
# If default is not already listed (e.g. if it's currently set)
if get_default_location() not in locations.values():
locations["Default"] = get_default_location()
for ppartition, plocation in get_permissive_locations():
pdrive = os.path.splitdrive(plocation)[0]
if not (
pdrive # If path actually has a drive (basically just Windows?)
and any( # And shares a common drive with an existing location
[
pdrive == os.path.splitdrive(location)[0]
for location in locations.values()
]
)
):
locations[ppartition] = plocation
# Strip out Nones
return {k: v for k, v in locations.items() if v}
class CaptureStorageLocation:
def __init__(self, mountpoint):
pass
class AutostorageExtension(BaseExtension):
def __init__(self):
BaseExtension.__init__(
self,
"org.openflexure.autostorage",
version="2.0.0",
description="Handle switching capture storage devices",
)
# We'll store a reference to a camera object, who's capture paths will be modified
self.camera = None
self.initial_location = get_default_location()
# Register the on_microscope function to run when the microscope is attached
self.on_component("org.openflexure.microscope", self.on_microscope)
def on_microscope(self, microscope_obj):
"""Function to automatically call when the parent LabThing has a microscope attached."""
logging.debug(f"Autostorage extension found microscope {microscope_obj}")
if hasattr(microscope_obj, "camera"):
logging.debug(f"Autostorage extension bound to camera {self.camera}")
# Store a reference to the camera
self.camera = microscope_obj.camera
# Store the initial storage location
self.initial_location = get_current_location(self.camera)
# If preferred path does not exist, or cannot be written to
self.check_location(self.initial_location)
logging.debug(self.get_locations())
def check_location(self, location=None):
if not location:
location = get_current_location(self.camera)
# If preferred path does not exist, or cannot be written to
if not (os.path.isdir(location) and check_rw(location)):
logging.error(
f"Preferred capture path {location} is missing or cannot be written to. Restoring defaults."
)
# Reset the storage location to default
set_current_location(self.camera, get_default_location())
def get_locations(self):
if self.camera:
locations = get_all_locations()
current_location = get_current_location(self.camera)
if current_location not in locations.values():
locations.update({"Custom": current_location})
# Add location from the cameras settings file
return locations
else:
return {}
def get_preferred_key(self):
current = get_current_location(self.camera)
locations = self.get_locations()
matches = [k for k, v in locations.items() if v == current]
if len(matches) > 1:
logging.warning(
"Multiple path matches found. Weird, but carrying on using zeroth."
)
return matches[0]
def set_preferred_key(self, new_path_key: str):
if not new_path_key in self.get_locations().keys():
raise KeyError(f"No location named {new_path_key}")
location = self.get_locations().get(new_path_key)
set_current_location(self.camera, location)
def key_to_title(self, path_key: str):
if not path_key in self.get_locations().keys():
raise KeyError(f"No location named {path_key}")
return f"{path_key} ({self.get_locations().get(path_key)})"
def title_to_key(self, path_title: str):
matches = []
for loc_key in self.get_locations().keys():
if path_title.startswith(loc_key):
matches.append(loc_key)
if len(matches) > 1:
logging.warning(
"Multiple path matches found. Weird, but carrying on using zeroth."
)
return matches[0]
def get_titles(self):
return [self.key_to_title(key) for key in self.get_locations().keys()]
def get_preferred_title(self):
return self.key_to_title(self.get_preferred_key())
autostorage_extension_v2 = AutostorageExtension()
@ThingProperty
class GetLocationsView(View):
def get(self):
global autostorage_extension_v2
autostorage_extension_v2.check_location()
return autostorage_extension_v2.get_locations()
@ThingProperty
@PropertySchema(fields.String(required=True, example="Default"))
class PreferredLocationView(View):
def get(self):
global autostorage_extension_v2
autostorage_extension_v2.check_location()
return autostorage_extension_v2.get_preferred_key()
def post(self, new_path_key):
global autostorage_extension_v2
microscope = find_component("org.openflexure.microscope")
if not microscope:
abort(503, "No microscope connected. Unable to autofocus.")
autostorage_extension_v2.check_location()
autostorage_extension_v2.set_preferred_key(new_path_key)
microscope.save_settings()
class PreferredLocationGUIView(View):
@use_args({"new_path_title": fields.String(required=True)})
def post(self, args):
global autostorage_extension_v2
new_path_title = args.get("new_path_title")
logging.debug(new_path_title)
new_path_key = autostorage_extension_v2.title_to_key(new_path_title)
logging.debug(f"{new_path_key}")
autostorage_extension_v2.check_location()
autostorage_extension_v2.set_preferred_key(new_path_key)
return new_path_title
def dynamic_form():
global autostorage_extension_v2
autostorage_extension_v2.check_location()
return {
"icon": "sd_storage",
"title": "Storage",
"forms": [
{
"name": "Autostorage",
"isCollapsible": False,
"isTask": False,
"route": "/location-from-title",
"emitOnResponse": "globalUpdateCaptures",
"submitLabel": "Set path",
"schema": [
{
"fieldType": "selectList",
"name": "new_path_title",
"label": "Capture storage path",
"options": autostorage_extension_v2.get_titles(),
"value": autostorage_extension_v2.get_preferred_title(),
}
],
}
],
}
autostorage_extension_v2.add_view(GetLocationsView, "list-locations")
autostorage_extension_v2.add_view(PreferredLocationView, "location")
autostorage_extension_v2.add_view(PreferredLocationGUIView, "location-from-title")
autostorage_extension_v2.add_meta(
"gui", build_gui(dynamic_form, autostorage_extension_v2)
)

View file

@ -1,10 +1,7 @@
from labthings.server.view import View from labthings.server.view import View
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 ( from labthings.server.decorators import marshal_task, ThingAction
marshal_task,
ThingAction,
)
from labthings.core.tasks import taskify from labthings.core.tasks import taskify
@ -24,8 +21,8 @@ def recalibrate(microscope):
""" """
scamera = microscope.camera scamera = microscope.camera
with scamera.lock: with scamera.lock:
assert not scamera.status["record_active"], "Can't recalibrate while recording!" assert not scamera.record_active, "Can't recalibrate while recording!"
streaming = scamera.status["stream_active"] streaming = scamera.stream_active
if streaming: if streaming:
logging.info("Stopping stream before recalibration") logging.info("Stopping stream before recalibration")
scamera.stop_stream_recording(resolution=(640, 480)) scamera.stop_stream_recording(resolution=(640, 480))

View file

@ -1,20 +1,14 @@
import itertools import itertools
import logging import logging
import uuid import uuid
import datetime
from typing import Tuple from typing import Tuple
from functools import reduce from functools import reduce
from openflexure_microscope.camera.base import generate_basename from openflexure_microscope.camera.base import generate_basename
from labthings.server.find import ( from labthings.server.find import find_component, find_extension
find_component,
find_extension,
)
from labthings.server.extensions import BaseExtension from labthings.server.extensions import BaseExtension
from labthings.server.decorators import ( from labthings.server.decorators import marshal_task, use_args, ThingAction
marshal_task,
use_args,
ThingAction,
)
from labthings.server import fields from labthings.server import fields
from openflexure_microscope.devel import taskify, abort, update_task_progress from openflexure_microscope.devel import taskify, abort, update_task_progress
@ -78,12 +72,12 @@ def progress():
def capture( def capture(
microscope, microscope,
basename, basename,
scan_id,
temporary: bool = False, temporary: bool = False,
use_video_port: bool = False, use_video_port: bool = False,
resize: Tuple[int, int] = None, resize: Tuple[int, int] = None,
bayer: bool = False, bayer: bool = False,
metadata: dict = {}, metadata: dict = {},
annotations: dict = {},
tags: list = [], tags: list = [],
): ):
@ -101,16 +95,14 @@ def capture(
output.file, use_video_port=use_video_port, resize=resize, bayer=bayer output.file, use_video_port=use_video_port, resize=resize, bayer=bayer
) )
# Affix metadata
if "scan" not in tags:
tags.append("scan")
# Inject system metadata # Inject system metadata
output.put_metadata(microscope.metadata, system=True) output.put_metadata({"instrument": microscope.metadata})
# Insert custom metadata # Insert custom metadata
output.put_metadata(metadata) output.put_metadata(metadata)
# Insert custom metadata
output.put_annotations(annotations)
# Insert custom tags # Insert custom tags
output.put_tags(tags) output.put_tags(tags)
@ -122,7 +114,7 @@ def tile(
microscope, microscope,
basename: str = None, basename: str = None,
temporary: bool = False, temporary: bool = False,
step_size: int = [2000, 1500, 100], stride_size: int = [2000, 1500, 100],
grid: list = [3, 3, 5], grid: list = [3, 3, 5],
style="raster", style="raster",
autofocus_dz: int = 50, autofocus_dz: int = 50,
@ -131,13 +123,13 @@ def tile(
bayer: bool = False, bayer: bool = False,
fast_autofocus=False, fast_autofocus=False,
metadata: dict = {}, metadata: dict = {},
annotations: dict = {},
tags: list = [], tags: list = [],
): ):
global _images_to_be_captured global _images_to_be_captured
global _images_captured_so_far global _images_captured_so_far
# Keep task progress # Keep task progress
# TODO: Make this line not nasty
_images_to_be_captured = reduce((lambda x, y: x * y), grid) _images_to_be_captured = reduce((lambda x, y: x * y), grid)
_images_captured_so_far = 0 _images_captured_so_far = 0
@ -145,28 +137,22 @@ def tile(
if not basename: if not basename:
basename = generate_basename() basename = generate_basename()
# Generate a stack ID
scan_id = uuid.uuid4()
# Store initial position # Store initial position
initial_position = microscope.stage.position initial_position = microscope.stage.position
# Add scan metadata # Add dataset metadata
if "time" not in metadata: dataset_d = {
metadata["time"] = generate_basename() "dataset": {
"id": uuid.uuid4(),
metadata.update( "type": "xyzScan",
{ "name": basename,
"scan_id": scan_id, "acquisitionDate": datetime.datetime.now().isoformat(),
"basename": basename, "strideSize": stride_size,
"scan_parameters": { "grid": grid,
"step_size": step_size, "style": style,
"grid": grid, "autofocusDz": autofocus_dz,
"style": style,
"autofocus_dz": autofocus_dz,
},
} }
) }
# Check if autofocus is enabled # Check if autofocus is enabled
autofocus_extension = find_extension("org.openflexure.autofocus") autofocus_extension = find_extension("org.openflexure.autofocus")
@ -180,25 +166,19 @@ def tile(
else: else:
autofocus_enabled = False autofocus_enabled = False
z_stack_dz = (
grid[2] * step_size[2] if grid[2] > 1 else 0
) # shorthand for Z stack range
# Construct an x-y grid (worry about z later) # Construct an x-y grid (worry about z later)
x_y_grid = construct_grid(initial_position, step_size[:2], grid[:2], style=style) x_y_grid = construct_grid(initial_position, stride_size[:2], grid[:2], style=style)
# Keep the initial Z position the same as our current position # Keep the initial Z position the same as our current position
next_z = initial_position[2] initial_z = initial_position[2]
if fast_autofocus: # If fast autofocus is enabled, make next_z = initial_z # Save this value for use in raster scans
next_z += autofocus_dz / 2 # sure we start from the top of the range
initial_z = next_z # Save this value for use in raster scans
# Now step through each point in the x-y coordinate array # Now step through each point in the x-y coordinate array
for line in x_y_grid: for line in x_y_grid:
# If rastering, rather than snake (or eventually spiral) # If rastering, rather than snake (or eventually spiral)
# Return focus to initial position # Return focus to initial position
if style == "raster": if style == "raster":
next_z = initial_z next_z = initial_z # Reset z position at start of each new row
logging.debug("Returning to initial z position") logging.debug("Returning to initial z position")
microscope.stage.move_abs( microscope.stage.move_abs(
[line[0][0], line[0][1], next_z] [line[0][0], line[0][1], next_z]
@ -211,31 +191,29 @@ def tile(
# Refocus # Refocus
if autofocus_enabled: if autofocus_enabled:
if fast_autofocus: if fast_autofocus:
autofocus_extension.fast_autofocus( # Run fast autofocus. Client should provide dz ~ 2000
dz=autofocus_dz, autofocus_extension.fast_up_down_up_autofocus(
target_z=-z_stack_dz / 2.0, # Finish below the focus microscope, dz=autofocus_dz
initial_move_up=False, # We're already at the top of the scan
) )
# TODO: save the focus data for future reference? Use it for diagnostics?
else: else:
logging.debug("Running autofocus") # Run slow autofocus. Client should provide dz ~ 50
autofocus_extension.autofocus( autofocus_extension.autofocus(
range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz) range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz)
) )
logging.debug("Finished autofocus") logging.debug("Finished autofocus")
time.sleep(1) # TODO: Remove time.sleep(1)
# If we're not doing a z-stack, just capture # If we're not doing a z-stack, just capture
if grid[2] <= 1: if grid[2] <= 1:
capture( capture(
microscope, microscope,
basename, basename,
scan_id,
temporary=temporary, temporary=temporary,
use_video_port=use_video_port, use_video_port=use_video_port,
resize=resize, resize=resize,
bayer=bayer, bayer=bayer,
metadata=metadata, metadata=dataset_d,
annotations=annotations,
tags=tags, tags=tags,
) )
# Update task progress # Update task progress
@ -247,27 +225,17 @@ def tile(
microscope=microscope, microscope=microscope,
basename=basename, basename=basename,
temporary=temporary, temporary=temporary,
scan_id=scan_id, step_size=stride_size[2],
step_size=step_size[2],
steps=grid[2], steps=grid[2],
center=not fast_autofocus, # fast_autofocus does this for us!
return_to_start=not fast_autofocus,
use_video_port=use_video_port, use_video_port=use_video_port,
resize=resize, resize=resize,
bayer=bayer, bayer=bayer,
metadata=metadata, metadata=dataset_d,
annotations=annotations,
tags=tags, tags=tags,
) )
# Make sure we use our current best estimate of focus (i.e. the current position) next point # Make sure we use our current best estimate of focus (i.e. the current position) next point
next_z = microscope.stage.position[2] next_z = microscope.stage.position[2]
if fast_autofocus:
next_z += (
autofocus_dz / 2
) # Fast autofocus requires us to start at the top of the range
if grid[2] > 1:
next_z -= int(
grid[2] / 2.0 * step_size[2]
) # Z stacking means we're higher up to start with
logging.debug("Returning to {}".format(initial_position)) logging.debug("Returning to {}".format(initial_position))
microscope.stage.move_abs(initial_position) microscope.stage.move_abs(initial_position)
@ -277,52 +245,40 @@ def stack(
microscope, microscope,
basename: str = None, basename: str = None,
temporary: bool = False, temporary: bool = False,
scan_id: str = None,
step_size: int = 100, step_size: int = 100,
steps: int = 5, steps: int = 5,
center: bool = True,
return_to_start: bool = True, return_to_start: bool = True,
use_video_port: bool = False, use_video_port: bool = False,
resize: Tuple[int, int] = None, resize: Tuple[int, int] = None,
bayer: bool = False, bayer: bool = False,
metadata: dict = {}, metadata: dict = {},
annotations: dict = {},
tags: list = [], tags: list = [],
): ):
global _images_captured_so_far global _images_captured_so_far
# Generate a basename if none given
if not basename:
basename = generate_basename()
# Generate a stack ID
if not scan_id:
scan_id = uuid.uuid4()
# Add scan metadata
if not "time" in metadata:
metadata["time"] = generate_basename()
# Store initial position # Store initial position
initial_position = microscope.stage.position initial_position = microscope.stage.position
logging.debug(f"Starting z-stack from position {microscope.stage.position}")
with microscope.lock: with microscope.lock:
# Move to center scan # Move to center scan
if center: logging.debug("Moving to z-stack starting position")
logging.debug("Moving to starting position") microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)])
microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)]) logging.debug(f"Starting scan from position {microscope.stage.position}")
for i in range(steps): for i in range(steps):
time.sleep(0.1) time.sleep(0.1)
logging.debug("Capturing...") logging.debug(f"Capturing from position {microscope.stage.position}")
capture( capture(
microscope, microscope,
basename, basename,
scan_id,
temporary=temporary, temporary=temporary,
use_video_port=use_video_port, use_video_port=use_video_port,
resize=resize, resize=resize,
bayer=bayer, bayer=bayer,
metadata=metadata, metadata=metadata,
annotations=annotations,
tags=tags, tags=tags,
) )
# Update task progress # Update task progress
@ -344,16 +300,18 @@ def stack(
class TileScanAPI(View): class TileScanAPI(View):
@use_args( @use_args(
{ {
"filename": fields.String(), "filename": fields.String(missing=None, example=None),
"temporary": fields.Boolean(missing=False), "temporary": fields.Boolean(missing=False),
"step_size": fields.List(fields.Integer, missing=[2000, 1500, 100]), "stride_size": fields.List(
"grid": fields.List(fields.Integer, missing=[3, 3, 5]), fields.Integer, missing=[2000, 1500, 100], example=[2000, 1500, 100]
),
"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),
"metadata": fields.Dict(missing={}), "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
} }
@ -380,7 +338,7 @@ class TileScanAPI(View):
microscope, microscope,
basename=args.get("filename"), basename=args.get("filename"),
temporary=args.get("temporary"), temporary=args.get("temporary"),
step_size=args.get("step_size"), stride_size=args.get("stride_size"),
grid=args.get("grid"), grid=args.get("grid"),
style=args.get("style"), style=args.get("style"),
autofocus_dz=args.get("autofocus_dz"), autofocus_dz=args.get("autofocus_dz"),
@ -388,7 +346,7 @@ class TileScanAPI(View):
resize=resize, resize=resize,
bayer=args.get("bayer"), bayer=args.get("bayer"),
fast_autofocus=args.get("fast_autofocus"), fast_autofocus=args.get("fast_autofocus"),
metadata=args.get("metadata"), annotations=args.get("annotations"),
tags=args.get("tags"), tags=args.get("tags"),
) )
@ -396,6 +354,6 @@ class TileScanAPI(View):
return task return task
scan_extension_v2 = BaseExtension("org.openflexure.scan", version="2.0.0-beta.1") scan_extension_v2 = BaseExtension("org.openflexure.scan", version="2.0.0")
scan_extension_v2.add_view(TileScanAPI, "/tile") scan_extension_v2.add_view(TileScanAPI, "/tile")

View file

@ -6,7 +6,7 @@ from openflexure_microscope.devel import (
update_task_progress, update_task_progress,
) )
from flask import send_file, abort from flask import send_file, abort, url_for
import uuid import uuid
import os import os
@ -16,13 +16,56 @@ import logging
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.schema import Schema
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.decorators import ( from labthings.server.decorators import (
ThingAction, ThingAction,
ThingProperty, ThingProperty,
marshal_task,
marshal_with,
pre_dump,
) )
class ZipObjectSchema(Schema):
id = fields.String()
data_size = fields.Number()
zip_size = fields.Number()
links = fields.Dict()
@pre_dump
def generate_links(self, data, **kwargs):
data.links = {
"download": {
"href": url_for(
ZipGetterAPIView.endpoint, session_id=data.id, _external=True
),
**description_from_view(ZipGetterAPIView),
}
}
return data
class ZipObjectDescription:
def __init__(self, id, file_pointer, data_size=None):
self.id = id
self.fp = file_pointer
self.data_size = data_size
self.zip_size = os.path.getsize(self.fp.name) * 1e-6
def close(self):
logging.debug(self.fp.name)
self.fp.close()
os.unlink(self.fp.name)
assert not os.path.exists(self.fp.name)
def __del__(self):
self.close()
class ZipManager: class ZipManager:
""" """
ZIP-builder manager ZIP-builder manager
@ -38,8 +81,7 @@ class ZipManager:
# Get array of captures from IDs # Get array of captures from IDs
capture_list = [ capture_list = [
microscope.camera.image_from_id(capture_id) microscope.camera.images.get(capture_id) for capture_id in capture_id_list
for capture_id in capture_id_list
] ]
# Remove Nones from list (missing/invalid captures) # Remove Nones from list (missing/invalid captures)
capture_list = [capture for capture in capture_list if capture] capture_list = [capture for capture in capture_list if capture]
@ -76,20 +118,23 @@ class ZipManager:
# Update task progress # Update task progress
update_task_progress(int((index / n_files) * 100)) update_task_progress(int((index / n_files) * 100))
session_id = uuid.uuid4() session_id = str(uuid.uuid4())
session_key = str(session_id) session_description = ZipObjectDescription(
# self.session_zips[session_id] = fp session_id, fp, data_size=data_size_megabytes
self.session_zips[session_key] = { )
"id": session_id, self.session_zips[session_id] = session_description
"fp": fp,
"data_size": data_size_megabytes,
"zip_size": os.path.getsize(fp.name) * 1e-6,
}
return self.session_zips[session_key] return self.session_zips[session_id]
def zip_from_id(self, session_id): def marshaled_build_zip_from_capture_ids(self, *args, **kwargs):
return self.session_zips[session_id]["fp"] return ZipObjectSchema().dump(self.build_zip_from_capture_ids(*args, **kwargs))
def zip_fp_from_id(self, session_id):
return self.session_zips[session_id].fp
def __del__(self):
for zd in self.session_zips.values():
zd.close()
# Create a global ZIP manager # Create a global ZIP manager
@ -98,56 +143,68 @@ default_zip_manager = ZipManager()
@ThingAction @ThingAction
class ZipBuilderAPIView(View): class ZipBuilderAPIView(View):
@marshal_task
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")
task = taskify(default_zip_manager.build_zip_from_capture_ids)(microscope, ids) task = taskify(default_zip_manager.marshaled_build_zip_from_capture_ids)(
microscope, ids
)
# Return a handle on the autofocus task # Return a handle on the autofocus task
return jsonify(task.state), 201 return task
@ThingProperty @ThingProperty
class ZipListAPIView(View): class ZipListAPIView(View):
@marshal_with(ZipObjectSchema(many=True))
def get(self): def get(self):
return jsonify(default_zip_manager.session_zips) return default_zip_manager.session_zips.values()
class ZipGetterAPIView(View): class ZipGetterAPIView(View):
"""
Download or delete a particular capture collection ZIP file
"""
def get(self, session_id): def get(self, session_id):
"""
Download a particular capture collection ZIP file
"""
if not session_id in default_zip_manager.session_zips: if not session_id in default_zip_manager.session_zips:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
logging.info(f"Session ID: {session_id}") logging.info(f"Session ID: {session_id}")
return send_file( return send_file(
default_zip_manager.zip_from_id(session_id).name, default_zip_manager.zip_fp_from_id(session_id).name,
mimetype="application/zip", mimetype="application/zip",
as_attachment=True, as_attachment=True,
attachment_filename=f"{session_id}.zip", attachment_filename=f"{session_id}.zip",
) )
def delete(self, session_id): def delete(self, session_id):
"""
Close and delete a particular capture collection ZIP file
"""
if not session_id in default_zip_manager.session_zips: if not session_id in default_zip_manager.session_zips:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
logging.info(f"Session ID: {session_id}") # Close the file
default_zip_manager.session_zips[session_id].close()
fp = default_zip_manager.zip_from_id(session_id) # Delete the file reference
logging.debug(fp.name)
fp.close()
os.unlink(fp.name)
assert not os.path.exists(fp.name)
del default_zip_manager.session_zips[session_id] del default_zip_manager.session_zips[session_id]
return jsonify({"return": session_id}) return {"return": session_id}
zip_extension_v2 = BaseExtension("org.openflexure.zipbuilder", version="2.0.0-beta.1") zip_extension_v2 = BaseExtension(
"org.openflexure.zipbuilder",
version="2.0.0",
description="Build and download capture collections as ZIP files",
)
zip_extension_v2.add_view(ZipGetterAPIView, "/get/<string:session_id>") zip_extension_v2.add_view(ZipGetterAPIView, "/get/<string:session_id>")
zip_extension_v2.add_view(ZipListAPIView, "/get") zip_extension_v2.add_view(ZipListAPIView, "/get")

View file

@ -1,9 +1,6 @@
from labthings.server.view import View from labthings.server.view import View
from labthings.server.extensions import BaseExtension from labthings.server.extensions import BaseExtension
from labthings.server.decorators import ( from labthings.server.decorators import ThingAction, ThingProperty
ThingAction,
ThingProperty,
)
from openflexure_microscope.api.utilities.gui import build_gui from openflexure_microscope.api.utilities.gui import build_gui
@ -35,7 +32,7 @@ def dynamic_form():
"name": "val_int", "name": "val_int",
"label": "Number value", "label": "Number value",
"minValue": 0, "minValue": 0,
"value": val_int "value": val_int,
}, },
{ {
"fieldType": "htmlBlock", "fieldType": "htmlBlock",
@ -66,7 +63,7 @@ static_form = {
"name": "val_int", "name": "val_int",
"label": "Number value", "label": "Number value",
"minValue": 0, "minValue": 0,
"default": 1 "default": 1,
}, },
{ {
"fieldType": "htmlBlock", "fieldType": "htmlBlock",

View file

@ -1,44 +1,12 @@
from openflexure_microscope import Microscope from openflexure_microscope.microscope import Microscope
from openflexure_microscope.camera.capture import build_captures_from_exif
import logging import logging
# Import device modules
# NB this will eventually be handled by the RC file, so you can choose what device
# class should be attached.
try:
from openflexure_microscope.camera.pi import PiCameraStreamer
except ImportError:
logging.warning("Unable to import PiCameraStreamer")
from openflexure_microscope.camera.mock import MockStreamer
from openflexure_microscope.stage.sanga import SangaStage
from openflexure_microscope.stage.mock import MockStage
default_microscope = Microscope() default_microscope = Microscope()
# Initialise camera
logging.debug("Creating camera object...")
try:
api_camera = PiCameraStreamer()
except Exception as e:
logging.error(e)
logging.warning("No valid camera hardware found. Falling back to mock camera!")
api_camera = MockStreamer()
# Initialise stage
logging.debug("Creating stage object...")
api_stage = MockStage()
# Attach devices to microscope
logging.debug("Attaching devices to microscope...")
default_microscope.attach(api_camera, api_stage)
# Restore loaded capture array to camera object # Restore loaded capture array to camera object
logging.debug("Restoring captures...") logging.debug("Restoring captures...")
default_microscope.camera.images = build_captures_from_exif( default_microscope.camera.rebuild_captures()
default_microscope.camera.paths["default"]
)
logging.debug("Microscope successfully attached!") logging.debug("Microscope successfully attached!")

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8605f70737729f74f48d3183b30b4a2f50706f4f750cd208aeb8c136253f7d01
size 772

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e68506e72389393d35cc47e5184459fff9a23bedea8c9ee96491f66c88e71954
size 1375875

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ee944cc13be0a52ce48a81a09c179dac70729bd7580a516ccc8ac10b4cfa3f98
size 1310

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:710327f8b3d83c83019ed2c4b27de8caf8e4e923c9148f93c1c0a2662f683f26
size 1659

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0405ef60ab42df1c2e8721bf1f500c8c7d9e1afae130307101b8f77925ffdac1
size 1150

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c4a1baec300d09e03a8380b85918267ee80faae8e00c6c56b48e2e74b1d9b38d
size 57620

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a87d66c91b2e7dc5530aef76c03bd6a3d25ea5826110bf4803b561b811cc8726
size 44300

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b7f4a3ab562048f28dd1fa691601bc43363a61d0f876d16d8316c52e4f32d696
size 128180

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8c998b4a9c0acbb9fe5dd572c206a5a33fdd5ca2b58db87fc3b893beac85068d
size 143258

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:463bfdbd635ddff1d00df38b1149ffd3947b492d71cfb546549dffebd311b090
size 843

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a536bfa88fd1bf69284804285c5cf1da33cf05f164f175eb4085f7d057ff4f17
size 378133

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:996b19391845b7cf1810f1181d935aad524897763da4c36847c7715f762b63fa
size 1597950

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a77d7d2e5e4220883bc1491a627435cc80d0b2574d51972bcd6b12add7a4a94
size 116850

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1404c938e8f60b1694dd48357d486d919f5b1e32c9f226ca26fc0ee399019542
size 395811

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:187e61d8f6171eebf611502412556c707fab1d837f8763b2b60ce3915303f0ad
size 1734

View file

@ -3,6 +3,12 @@ import logging
from functools import wraps from functools import wraps
def clean_rule(rule: str):
while rule[0] == "/":
rule = rule[1:]
return f"{rule}"
def build_gui_from_dict(gui_description, extension_object): def build_gui_from_dict(gui_description, extension_object):
# Make a working copy of GUI description # Make a working copy of GUI description
api_gui = copy.deepcopy(gui_description) api_gui = copy.deepcopy(gui_description)
@ -10,6 +16,10 @@ def build_gui_from_dict(gui_description, extension_object):
# Expand shorthand routes into full relative URLs # Expand shorthand routes into full relative URLs
if "forms" in gui_description and isinstance(api_gui["forms"], list): if "forms" in gui_description and isinstance(api_gui["forms"], list):
for form in api_gui["forms"]: for form in api_gui["forms"]:
# Clean leading slashes from rule
if "route" in form:
form["route"] = clean_rule(form["route"])
# Match rule in extension object
if "route" in form and form["route"] in extension_object._rules.keys(): if "route" in form and form["route"] in extension_object._rules.keys():
form["route"] = extension_object._rules[form["route"]]["rule"] form["route"] = extension_object._rules[form["route"]]["rule"]
else: else:

View file

@ -1,4 +1,4 @@
from .actions import enabled_root_actions from .actions import enabled_root_actions
from .captures import * from .captures import *
from .state import * from .instrument import *
from .streams import * from .streams import *

View file

@ -1,7 +1,7 @@
""" """
Top-level representation of enabled actions Top-level representation of enabled actions
""" """
from flask import url_for
from . import camera, stage, system from . import camera, stage, system
from labthings.server.view import View from labthings.server.view import View
@ -74,9 +74,9 @@ class ActionsView(View):
d = { d = {
"links": { "links": {
"self": { "self": {
"href": current_labthing().url_for(action["view_class"]), "href": url_for(action["view_class"].endpoint, _external=True),
"mimetype": "application/json", "mimetype": "application/json",
**description_from_view(action["view_class"]) **description_from_view(action["view_class"]),
} }
}, },
"rule": action["rule"], "rule": action["rule"],

View file

@ -36,7 +36,7 @@ class CaptureAPI(View):
"bayer": fields.Boolean( "bayer": fields.Boolean(
missing=False, description="Store raw bayer data in file" missing=False, description="Store raw bayer data in file"
), ),
"metadata": 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}
@ -75,11 +75,10 @@ class CaptureAPI(View):
) )
# Inject system metadata # Inject system metadata
output.put_metadata(microscope.metadata, system=True) output.put_metadata({"instrument": microscope.metadata})
# Insert custom metadata # Insert custom metadata
output.put_metadata(args.get("metadata")) output.put_annotations(args.get("annotations"))
# Insert custom tags # Insert custom tags
output.put_tags(args.get("tags")) output.put_tags(args.get("tags"))
@ -132,10 +131,7 @@ class RAMCaptureAPI(View):
stream.seek(0) stream.seek(0)
return send_file( return send_file(io.BytesIO(stream.getbuffer()), mimetype="image/jpeg")
io.BytesIO(stream.getbuffer()),
mimetype="image/jpeg"
)
@ThingAction @ThingAction
@ -167,8 +163,8 @@ class GPUPreviewStartAPI(View):
microscope.camera.start_preview(fullscreen=fullscreen, window=window) microscope.camera.start_preview(fullscreen=fullscreen, window=window)
# TODO: Make schema for microscope status # TODO: Make schema for microscope state
return jsonify(microscope.status) return jsonify(microscope.state)
@ThingAction @ThingAction
@ -179,5 +175,5 @@ class GPUPreviewStopAPI(View):
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
microscope.camera.stop_preview() microscope.camera.stop_preview()
# TODO: Make schema for microscope status # TODO: Make schema for microscope state
return jsonify(microscope.status) return jsonify(microscope.state)

View file

@ -1,12 +1,7 @@
from openflexure_microscope.api.utilities import JsonResponse from openflexure_microscope.api.utilities import JsonResponse
from labthings.server.view import View from labthings.server.view import View
from labthings.server.find import find_component from labthings.server.find import find_component
from labthings.server.decorators import ( from labthings.server.decorators import use_args, marshal_with, doc, ThingAction
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
@ -57,8 +52,8 @@ class MoveStageAPI(View):
else: else:
logging.warning("Unable to move. No stage found.") logging.warning("Unable to move. No stage found.")
# TODO: Make schema for microscope status # TODO: Make schema for microscope state
return jsonify(microscope.status["stage"]["position"]) return jsonify(microscope.state["stage"]["position"])
@ThingAction @ThingAction
@ -71,5 +66,5 @@ class ZeroStageAPI(View):
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
microscope.stage.zero_position() microscope.stage.zero_position()
# TODO: Make schema for microscope status # TODO: Make schema for microscope state
return jsonify(microscope.status["stage"]) return jsonify(microscope.state["stage"])

View file

@ -3,10 +3,7 @@ import subprocess
import os import os
from sys import platform from sys import platform
from labthings.server.decorators import ( from labthings.server.decorators import ThingAction, doc_response
ThingAction,
doc_response,
)
def is_raspberrypi(raise_on_errors=False): def is_raspberrypi(raise_on_errors=False):
@ -28,9 +25,14 @@ class ShutdownAPI(View):
""" """
Attempt to shutdown the device Attempt to shutdown the device
""" """
subprocess.Popen(["sudo", "shutdown", "-h", "now"]) p = subprocess.Popen(
["sudo", "shutdown", "-h", "now"],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
)
return "{}", 201 out, err = p.communicate()
return {"out": out, "err": err}, 201
@ThingAction @ThingAction
@ -44,6 +46,11 @@ class RebootAPI(View):
""" """
Attempt to reboot the device Attempt to reboot the device
""" """
subprocess.Popen(["sudo", "shutdown", "-r", "now"]) p = subprocess.Popen(
["sudo", "shutdown", "-r", "now"],
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
)
return "{}", 201 out, err = p.communicate()
return {"out": out, "err": err}, 201

View file

@ -6,9 +6,7 @@ from openflexure_microscope.api.utilities import get_bool, JsonResponse
from labthings.server.schema import Schema from labthings.server.schema import Schema
from labthings.server import fields from labthings.server import fields
from labthings.server.view import View from labthings.server.view import View
from labthings.server.utilities import ( from labthings.server.utilities import description_from_view
description_from_view,
)
from labthings.server.decorators import marshal_with, doc_response, Tag, ThingProperty 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
@ -16,14 +14,38 @@ from labthings.server.find import find_component
from marshmallow import pre_dump from marshmallow import pre_dump
class InstrumentSchema(Schema):
id = fields.UUID()
configuration = fields.Dict()
settings = fields.Dict()
state = fields.Dict()
class CaptureMetadataImageSchema(Schema):
id = fields.UUID()
acquisitionDate = fields.String(format="date")
format = fields.String()
name = fields.String()
tags = fields.List(fields.String())
annotations = fields.Dict()
class CaptureMetadataSchema(Schema):
experimenter = fields.Dict() # TODO: Make schema
experimenterGroup = fields.Dict() # TODO: Make schema
dataset = fields.Dict() # TODO: Make schema
image = fields.Nested(CaptureMetadataImageSchema())
instrument = fields.Nested(InstrumentSchema())
class CaptureSchema(Schema): class CaptureSchema(Schema):
id = fields.String() id = fields.String()
file = fields.String( file = fields.String(
data_key="path", description="Path of file on microscope device" data_key="path", description="Path of file on microscope device"
) )
exists = fields.Bool(data_key="available") exists = fields.Bool(data_key="available")
filename = fields.String() name = fields.String()
metadata = fields.Dict() metadata = fields.Nested(CaptureMetadataSchema())
links = fields.Dict() links = fields.Dict()
@ -41,16 +63,18 @@ class CaptureSchema(Schema):
"mimetype": "application/json", "mimetype": "application/json",
**description_from_view(CaptureTags), **description_from_view(CaptureTags),
}, },
"metadata": { "annotations": {
"href": url_for(CaptureMetadata.endpoint, id=data.id, _external=True), "href": url_for(
CaptureAnnotations.endpoint, id=data.id, _external=True
),
"mimetype": "application/json", "mimetype": "application/json",
**description_from_view(CaptureMetadata), **description_from_view(CaptureAnnotations),
}, },
"download": { "download": {
"href": url_for( "href": url_for(
CaptureDownload.endpoint, CaptureDownload.endpoint,
id=data.id, id=data.id,
filename=data.filename, filename=data.name,
_external=True, _external=True,
), ),
"mimetype": "image/jpeg", "mimetype": "image/jpeg",
@ -64,6 +88,9 @@ capture_schema = CaptureSchema()
capture_list_schema = CaptureSchema(many=True) capture_list_schema = CaptureSchema(many=True)
from pprint import pprint
@ThingProperty @ThingProperty
@Tag("captures") @Tag("captures")
class CaptureList(View): class CaptureList(View):
@ -73,7 +100,7 @@ class CaptureList(View):
List all image captures List all image captures
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
image_list = microscope.camera.images image_list = microscope.camera.images.values()
return image_list return image_list
@ -85,7 +112,7 @@ class CaptureView(View):
Description of a single image capture Description of a single image capture
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id) capture_obj = microscope.camera.images.get(id)
if not capture_obj: if not capture_obj:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
@ -97,12 +124,15 @@ class CaptureView(View):
Delete a single image capture Delete a single image capture
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id) capture_obj = microscope.camera.images.get(id)
if not capture_obj: if not capture_obj:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
# Delete the capture file
capture_obj.delete() capture_obj.delete()
# Delete from capture list
del microscope.camera.images[id]
return "", 204 return "", 204
@ -115,7 +145,7 @@ class CaptureDownload(View):
Image data for a single image capture Image data for a single image capture
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id) capture_obj = microscope.camera.images.get(id)
if not capture_obj: if not capture_obj:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
@ -150,7 +180,7 @@ class CaptureTags(View):
Get tags associated with a single image capture Get tags associated with a single image capture
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id) capture_obj = microscope.camera.images.get(id)
if not capture_obj: if not capture_obj:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
@ -162,7 +192,7 @@ class CaptureTags(View):
Add tags to a single image capture Add tags to a single image capture
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id) capture_obj = microscope.camera.images.get(id)
if not capture_obj: if not capture_obj:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
@ -177,12 +207,12 @@ class CaptureTags(View):
return jsonify(capture_obj.tags) return jsonify(capture_obj.tags)
def delete(self, capture_id): def delete(self, id):
""" """
Delete tags from a single image capture Delete tags from a single image capture
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id) capture_obj = microscope.camera.images.get(id)
if not capture_obj: if not capture_obj:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
@ -199,25 +229,25 @@ class CaptureTags(View):
@Tag("captures") @Tag("captures")
class CaptureMetadata(View): class CaptureAnnotations(View):
def get(self, id): def get(self, id):
""" """
Get metadata associated with a single image capture Get annotations associated with a single image capture
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id) capture_obj = microscope.camera.images.get(id)
if not capture_obj: if not capture_obj:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
return jsonify(capture_obj.metadata) return jsonify(capture_obj.annotations)
def put(self, id): def put(self, id):
""" """
Update metadata for a single image capture Update metadata for a single image capture
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
capture_obj = microscope.camera.image_from_id(id) capture_obj = microscope.camera.images.get(id)
if not capture_obj: if not capture_obj:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
@ -228,7 +258,6 @@ class CaptureMetadata(View):
if type(data_dict) != dict: if type(data_dict) != dict:
return abort(400) return abort(400)
# TODO: Allow putting system metadata maybe? capture_obj.put_annotations(data_dict)
capture_obj.put_metadata(data_dict)
return jsonify(capture_obj.metadata) return jsonify(capture_obj.annotations)

View file

@ -1,10 +1,6 @@
from openflexure_microscope.api.utilities import JsonResponse from openflexure_microscope.api.utilities import JsonResponse
from labthings.core.utilities import ( from labthings.core.utilities import get_by_path, set_by_path, create_from_path
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 from labthings.server.view import View
@ -34,7 +30,7 @@ class SettingsProperty(View):
logging.debug("Updating settings from PUT request:") logging.debug("Updating settings from PUT request:")
logging.debug(payload.json) logging.debug(payload.json)
microscope.apply_settings(payload.json) microscope.update_settings(payload.json)
microscope.save_settings() microscope.save_settings()
return self.get() return self.get()
@ -69,24 +65,24 @@ class NestedSettingsProperty(View):
dictionary = create_from_path(keys) dictionary = create_from_path(keys)
set_by_path(dictionary, keys, payload.json) set_by_path(dictionary, keys, payload.json)
microscope.apply_settings(dictionary) microscope.update_settings(dictionary)
microscope.save_settings() microscope.save_settings()
return self.get(route) return self.get(route)
@ThingProperty @ThingProperty
class StatusProperty(View): class StateProperty(View):
def get(self): def get(self):
""" """
Show current read-only state of the microscope Show current read-only state of the microscope
""" """
microscope = find_component("org.openflexure.microscope") microscope = find_component("org.openflexure.microscope")
return jsonify(microscope.status) return jsonify(microscope.state)
@Tag("properties") @Tag("properties")
class NestedStatusProperty(View): class NestedStateProperty(View):
@doc_response(404, description="Status key cannot be found") @doc_response(404, description="Status key cannot be found")
def get(self, route): def get(self, route):
""" """
@ -96,7 +92,35 @@ class NestedStatusProperty(View):
keys = route.split("/") keys = route.split("/")
try: try:
value = get_by_path(microscope.status, keys) value = get_by_path(microscope.state, keys)
except KeyError:
return abort(404)
return jsonify(value)
@ThingProperty
class ConfigurationProperty(View):
def get(self):
"""
Show current read-only state of the microscope
"""
microscope = find_component("org.openflexure.microscope")
return jsonify(microscope.configuration)
@Tag("properties")
class NestedConfigurationProperty(View):
@doc_response(404, description="Configuration key cannot be found")
def get(self, route):
"""
Show a nested section of the current microscope state
"""
microscope = find_component("org.openflexure.microscope")
keys = route.split("/")
try:
value = get_by_path(microscope.configuration, keys)
except KeyError: except KeyError:
return abort(404) return abort(404)

View file

@ -1,10 +1,6 @@
from openflexure_microscope.api.utilities import gen, JsonResponse from openflexure_microscope.api.utilities import gen, JsonResponse
from labthings.core.utilities import ( from labthings.core.utilities import get_by_path, set_by_path, create_from_path
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 from labthings.server.view import View

View file

@ -7,8 +7,9 @@ import datetime
import logging import logging
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from collections import OrderedDict
from .capture import CaptureObject from .capture import CaptureObject, build_captures_from_exif
from openflexure_microscope.utilities import entry_by_uuid from openflexure_microscope.utilities import entry_by_uuid
from labthings.core.lock import StrictLock from labthings.core.lock import StrictLock
@ -91,20 +92,6 @@ class CameraEvent(object):
class BaseCamera(metaclass=ABCMeta): class BaseCamera(metaclass=ABCMeta):
""" """
Base implementation of StreamingCamera. Base implementation of StreamingCamera.
Attributes:
thread: Background thread reading frames from camera
camera: Camera object
lock (:py:class:`labthings.lock.StrictLock`): Strict lock controlling thread
access to stage hardware
frame (bytes): Current frame is stored here by background thread
last_access (time): Time of last client access to the camera
stream_timeout (int): Number of inactive seconds before timing out the stream
stream_timeout_enabled (bool): Enable or disable timing out the stream
status (dict): Dictionary for capture state
paths (dict): Dictionary of capture paths
images (list): List of image capture objects
videos (list): List of video capture objects
""" """
def __init__(self): def __init__(self):
@ -121,16 +108,33 @@ class BaseCamera(metaclass=ABCMeta):
self.stream_timeout = 20 self.stream_timeout = 20
self.stream_timeout_enabled = False self.stream_timeout_enabled = False
self.status = {"board": None} self.stream_active = False
self.record_active = False
self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH} self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH}
# Capture data # Capture data
self.images = [] self.images = OrderedDict()
self.videos = [] self.videos = OrderedDict()
@property
@abstractmethod
def configuration(self):
"""The current camera configuration."""
pass
@property
@abstractmethod
def state(self):
"""The current read-only camera state."""
pass
@property
def settings(self):
return self.read_settings()
@abstractmethod @abstractmethod
def apply_settings(self, config: dict): def update_settings(self, config: dict):
"""Update settings from a config dictionary""" """Update settings from a config dictionary"""
with self.lock: with self.lock:
# Apply valid config params to camera object # Apply valid config params to camera object
@ -143,10 +147,6 @@ class BaseCamera(metaclass=ABCMeta):
"""Return the current settings as a dictionary""" """Return the current settings as a dictionary"""
return {"paths": self.paths} return {"paths": self.paths}
def save_settings(self):
"""(Optional) Save any settings to disk that need to be stored"""
return
def __enter__(self): def __enter__(self):
"""Create camera on context enter.""" """Create camera on context enter."""
return self return self
@ -159,7 +159,7 @@ class BaseCamera(metaclass=ABCMeta):
"""Close the BaseCamera and all attached StreamObjects.""" """Close the BaseCamera and all attached StreamObjects."""
logging.info("Closing {}".format(self)) logging.info("Closing {}".format(self))
# Close all StreamObjects # Close all StreamObjects
for capture_list in [self.images, self.videos]: for capture_list in [self.images.values(), self.videos.values()]:
for stream_object in capture_list: for stream_object in capture_list:
stream_object.close() stream_object.close()
# Empty temp directory # Empty temp directory
@ -178,6 +178,9 @@ class BaseCamera(metaclass=ABCMeta):
shutil.rmtree(self.paths["temp"]) shutil.rmtree(self.paths["temp"])
logging.debug("Cleared {}.".format(self.paths["temp"])) logging.debug("Cleared {}.".format(self.paths["temp"]))
def rebuild_captures(self):
self.images = build_captures_from_exif(self.paths["default"])
# START AND STOP WORKER THREAD # START AND STOP WORKER THREAD
def start_worker(self, timeout: int = 5) -> bool: def start_worker(self, timeout: int = 5) -> bool:
@ -187,7 +190,7 @@ class BaseCamera(metaclass=ABCMeta):
self.last_access = time.time() self.last_access = time.time()
self.stop = False self.stop = False
if not self.status["stream_active"]: if not self.stream_active:
# start background frame thread # start background frame thread
self.thread = threading.Thread(target=self._thread) self.thread = threading.Thread(target=self._thread)
self.thread.daemon = True self.thread.daemon = True
@ -207,12 +210,12 @@ class BaseCamera(metaclass=ABCMeta):
logging.debug("Stopping worker thread") logging.debug("Stopping worker thread")
timeout_time = time.time() + timeout timeout_time = time.time() + timeout
if self.status["stream_active"]: if self.stream_active:
self.stop = True self.stop = True
self.thread.join() # Wait for stream thread to exit self.thread.join() # Wait for stream thread to exit
logging.debug("Waiting for stream thread to exit.") logging.debug("Waiting for stream thread to exit.")
while self.status["stream_active"]: while self.stream_active:
if time.time() > timeout_time: if time.time() > timeout_time:
logging.debug("Timeout waiting for worker thread close.") logging.debug("Timeout waiting for worker thread close.")
raise TimeoutError("Timeout waiting for worker thread close.") raise TimeoutError("Timeout waiting for worker thread close.")
@ -242,20 +245,22 @@ class BaseCamera(metaclass=ABCMeta):
@property @property
def image(self): def image(self):
"""Return the latest captured image.""" """Return the latest captured image."""
return last_entry(self.images) return last_entry(self.images.values())
@property @property
def video(self): def video(self):
"""Return the latest recorded video.""" """Return the latest recorded video."""
return last_entry(self.videos) return last_entry(self.videos.values())
def image_from_id(self, image_id): def image_from_id(self, image_id):
"""Return an image StreamObject with a matching ID.""" """Return an image StreamObject with a matching ID."""
return entry_by_uuid(image_id, self.images) logging.warning("image_from_id is deprecated. Access captures as a dictionary.")
return entry_by_uuid(image_id, self.images.values())
def video_from_id(self, video_id): def video_from_id(self, video_id):
"""Return a video StreamObject with a matching ID.""" """Return a video StreamObject with a matching ID."""
return entry_by_uuid(video_id, self.videos) logging.warning("video_from_id is deprecated. Access captures as a dictionary.")
return entry_by_uuid(video_id, self.videos.values())
# CREATING NEW CAPTURES # CREATING NEW CAPTURES
@ -280,7 +285,7 @@ class BaseCamera(metaclass=ABCMeta):
# Generate file name # Generate file name
if not filename: if not filename:
filename = generate_numbered_basename(self.images) filename = generate_numbered_basename(self.images.values())
logging.debug(filename) logging.debug(filename)
filename = "{}.{}".format(filename, fmt) filename = "{}.{}".format(filename, fmt)
@ -298,7 +303,9 @@ class BaseCamera(metaclass=ABCMeta):
output.put_tags(["temporary"]) output.put_tags(["temporary"])
# Update capture list # Update capture list
self.images.append(output) capture_key = str(output.id)
logging.debug(f"Adding image {output} with key {capture_key}")
self.images[capture_key] = output
return output return output
@ -320,10 +327,11 @@ class BaseCamera(metaclass=ABCMeta):
folder (str): Name of the folder in which to store the capture. folder (str): Name of the folder in which to store the capture.
fmt (str): Format of the capture. fmt (str): Format of the capture.
""" """
# TODO: Remove the redundancy here
# Generate file name # Generate file name
if not filename: if not filename:
filename = generate_numbered_basename(self.videos) filename = generate_numbered_basename(self.videos.values())
logging.debug(filename) logging.debug(filename)
filename = "{}.{}".format(filename, fmt) filename = "{}.{}".format(filename, fmt)
@ -341,7 +349,9 @@ class BaseCamera(metaclass=ABCMeta):
output.put_tags(["temporary"]) output.put_tags(["temporary"])
# Update capture list # Update capture list
self.videos.append(output) capture_key = str(output.id)
logging.debug(f"Adding video {output} with key {capture_key}")
self.videos[capture_key] = output
return output return output
@ -352,7 +362,7 @@ class BaseCamera(metaclass=ABCMeta):
self.frames_iterator = self.frames() self.frames_iterator = self.frames()
logging.debug("Entering worker thread.") logging.debug("Entering worker thread.")
self.status["stream_active"] = True self.stream_active = True
for frame in self.frames_iterator: for frame in self.frames_iterator:
self.frame = frame self.frame = frame
@ -365,9 +375,7 @@ class BaseCamera(metaclass=ABCMeta):
and ( # If using timeout and ( # If using timeout
time.time() - self.last_access > self.stream_timeout time.time() - self.last_access > self.stream_timeout
) )
and not self.status[ # And timeout time and not self.preview_active # And GPU preview is not active
"preview_active"
] # And GPU preview is not active
): ):
self.frames_iterator.close() self.frames_iterator.close()
break break
@ -383,4 +391,4 @@ class BaseCamera(metaclass=ABCMeta):
logging.debug("BaseCamera worker thread exiting...") logging.debug("BaseCamera worker thread exiting...")
# Set stream_activate state # Set stream_activate state
self.status["stream_active"] = False self.stream_active = False

View file

@ -4,12 +4,14 @@ import os
import shutil import shutil
import glob import glob
import datetime import datetime
import yaml
import json import json
import logging import logging
from PIL import Image from PIL import Image
import dateutil.parser
import atexit import atexit
from collections import OrderedDict
from openflexure_microscope.camera import piexif from openflexure_microscope.camera import piexif
from openflexure_microscope.camera.piexif._exceptions import InvalidImageDataError from openflexure_microscope.camera.piexif._exceptions import InvalidImageDataError
from openflexure_microscope.config import JSONEncoder from openflexure_microscope.config import JSONEncoder
@ -34,11 +36,9 @@ def pull_usercomment_dict(filepath):
try: try:
return json.loads(exif_dict["Exif"][37510].decode()) return json.loads(exif_dict["Exif"][37510].decode())
except json.decoder.JSONDecodeError: except json.decoder.JSONDecodeError:
# TODO: Remove YAML support in a later version logging.error(
logging.warning( f"Capture {filepath} has old, corrupt, or missing OpenFlexure metadata. Unable to reload to server."
f"Capture {filepath} has metadata stored in YAML format. This is now deprecated in favour of JSON."
) )
return yaml.load(exif_dict["Exif"][37510].decode())
else: else:
return None return None
@ -60,14 +60,15 @@ def build_captures_from_exif(capture_path):
logging.debug("Reloading captures from {}...".format(capture_path)) logging.debug("Reloading captures from {}...".format(capture_path))
files = make_file_list(capture_path, EXIF_FORMATS) files = make_file_list(capture_path, EXIF_FORMATS)
captures = [] captures = OrderedDict()
for f in files: for f in files:
logging.debug("Reloading capture {}...".format(f)) logging.debug("Reloading capture {}...".format(f))
exif = pull_usercomment_dict(f) exif = pull_usercomment_dict(f)
if exif: if exif:
capture = capture_from_exif(f, exif) capture = capture_from_exif(f, exif)
captures.append(capture) if capture:
captures[capture.id] = capture
else: else:
logging.error("Invalid data at {}. Skipping.".format(f)) logging.error("Invalid data at {}. Skipping.".format(f))
@ -93,18 +94,24 @@ def capture_from_exif(path, exif_dict):
# Build file path information # Build file path information
capture.split_file_path(capture.file) capture.split_file_path(capture.file)
# Populate capture parameters # Image metadata
capture.id = exif_dict["id"] try:
capture.timestring = exif_dict["time"] image_metadata = exif_dict.pop("image")
capture.format = exif_dict["format"] except KeyError as e:
logging.error(
f"Unable to obtain valid 2.0 OpenFlexure metadata from file {path}"
)
return None
capture.custom_metadata = ( # Populate capture parameters
exif_dict["custom"] if "custom" in exif_dict.keys() else {} capture.id = image_metadata.get("id")
) capture.datetime = dateutil.parser.isoparse(image_metadata.get("acquisitionDate"))
capture.system_metadata = ( capture.format = image_metadata.get("format")
exif_dict["system"] if "system" in exif_dict.keys() else {} capture.tags = image_metadata.get("tags")
) capture.annotations = image_metadata.get("annotations")
capture.tags = exif_dict["tags"]
# Since we popped the "image" key, we dump whatever is left in _metadata
capture._metadata = exif_dict
return capture return capture
@ -113,16 +120,6 @@ class CaptureObject(object):
""" """
StreamObject used to store and process on-disk capture data, and metadata. StreamObject used to store and process on-disk capture data, and metadata.
Serves to simplify modifying properties of on-disk capture data. Serves to simplify modifying properties of on-disk capture data.
Attributes:
timestring (str): Timestring of capture creation time
custom_metadata (dict): Dictionary of custom metadata to be included in metadata file
tags (list): List of tags. Essentially just as extra custom metadata field, but useful for quick organisation
filefolder (str): Folder in which the capture file will be stored
filename (str): Full name of the capture file
basename (str): Filename of the capture, without a file extension
format (str): Format of the capture data
""" """
def __init__(self, filepath) -> None: def __init__(self, filepath) -> None:
@ -131,17 +128,20 @@ class CaptureObject(object):
# Store a nice ID # Store a nice ID
self.id = uuid.uuid4() #: str: Unique capture ID self.id = uuid.uuid4() #: str: Unique capture ID
logging.debug("Created StreamObject {}".format(self.id)) logging.debug("Created StreamObject {}".format(self.id))
self.timestring = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") self.datetime = datetime.datetime.now()
# Create file name. Default to UUID # Create file name. Default to UUID
self.file = filepath self.file = filepath
self.split_file_path(self.file) self.split_file_path(self.file)
# Dictionary for storing custom metadata if not os.path.exists(self.filefolder):
self.custom_metadata = {} os.makedirs(self.filefolder)
# Dictionary for adding top-level metadata (cannmot be accessed through web API)
self.system_metadata = {}
# Dictionary for adding top-level metadata (cannmot be accessed through web API)
self._metadata = {}
# Dictionary for storing custom annotations
self.annotations = {}
# List for storing tags # List for storing tags
self.tags = [] self.tags = []
@ -158,15 +158,11 @@ class CaptureObject(object):
Args: Args:
filepath (str): String of the full file path, including file format extension filepath (str): String of the full file path, including file format extension
""" """
# Split the full file path into a folder and a filename # Split the full file path into a folder and a name
self.filefolder, self.filename = os.path.split(filepath) self.filefolder, self.name = os.path.split(filepath)
# Split the filename out from it's file extension # Split the name out from it's file extension
self.basename = os.path.splitext(self.filename)[0] self.basename = os.path.splitext(self.name)[0]
self.format = self.filename.split(".")[-1] self.format = self.name.split(".")[-1]
# Create folder and file
if not os.path.exists(self.filefolder):
os.makedirs(self.filefolder)
@property @property
def exists(self) -> bool: def exists(self) -> bool:
@ -204,17 +200,24 @@ class CaptureObject(object):
# HANDLE METADATA # HANDLE METADATA
def put_metadata(self, data: dict, system: bool = False) -> None: def put_annotations(self, data: dict) -> None:
""" """
Merge metadata from a passed dictionary into the capture metadata, and saves. Merge annotations from a passed dictionary into the capture metadata, and saves.
Args: Args:
data (dict): Dictionary of metadata to be added data (dict): Dictionary of metadata to be added
""" """
if system: self.annotations.update(data)
self.system_metadata.update(data) self.save_metadata()
else:
self.custom_metadata.update(data) def put_metadata(self, data: dict) -> None:
"""
Merge root metadata from a passed dictionary into the capture metadata, and saves.
Args:
data (dict): Dictionary of metadata to be added
"""
self._metadata.update(data)
self.save_metadata() self.save_metadata()
def save_metadata(self) -> None: def save_metadata(self) -> None:
@ -244,12 +247,15 @@ class CaptureObject(object):
and any added custom metadata and tags. and any added custom metadata and tags.
""" """
d = { d = {
"id": self.id, "image": {
"time": self.timestring, "id": self.id,
"format": self.format, "name": self.name,
"tags": self.tags, "acquisitionDate": self.datetime.isoformat(),
"custom": self.custom_metadata, "format": self.format,
"system": self.system_metadata, "tags": self.tags,
"annotations": self.annotations,
},
**self._metadata,
} }
# Add custom metadata to dictionary # Add custom metadata to dictionary
@ -262,7 +268,7 @@ class CaptureObject(object):
""" """
# Create basic state dictionary # Create basic state dictionary
d = {"path": self.file, "filename": self.filename, "metadata": self.metadata} d = {"path": self.file, "name": self.name, "metadata": self.metadata}
# Combined availability of data # Combined availability of data
if self.exists: if self.exists:

View file

@ -26,17 +26,13 @@ We override the logging settings in api.app by setting a level for PIL here.
pil_logger = logging.getLogger("PIL") pil_logger = logging.getLogger("PIL")
pil_logger.setLevel(logging.INFO) pil_logger.setLevel(logging.INFO)
# MAIN CLASS # MAIN CLASS
class MockStreamer(BaseCamera): class MissingCamera(BaseCamera):
def __init__(self): def __init__(self):
# Run BaseCamera init # Run BaseCamera init
BaseCamera.__init__(self) BaseCamera.__init__(self)
# Store state of PiCameraStreamer
self.status.update(
{"stream_active": False, "record_active": False, "board": None}
)
# Update config properties # Update config properties
self.image_resolution = (1312, 976) self.image_resolution = (1312, 976)
self.stream_resolution = (640, 480) self.stream_resolution = (640, 480)
@ -71,6 +67,16 @@ class MockStreamer(BaseCamera):
image.save(self.stream, format="JPEG") image.save(self.stream, format="JPEG")
@property
def configuration(self):
"""The current camera configuration."""
return {}
@property
def state(self):
"""The current read-only camera state."""
return {}
def initialisation(self): def initialisation(self):
"""Run any initialisation code when the frame iterator starts.""" """Run any initialisation code when the frame iterator starts."""
pass pass
@ -101,7 +107,7 @@ class MockStreamer(BaseCamera):
return conf_dict return conf_dict
def apply_settings(self, config: dict): def update_settings(self, config: dict):
""" """
Write a config dictionary to the PiCameraStreamer config. Write a config dictionary to the PiCameraStreamer config.
@ -117,7 +123,7 @@ class MockStreamer(BaseCamera):
with self.lock: with self.lock:
# Apply valid config params to camera object # Apply valid config params to camera object
if not self.status["record_active"]: # If not recording a video if not self.record_active: # If not recording a video
for key, value in config.items(): # For each provided setting for key, value in config.items(): # For each provided setting
if hasattr(self, key): if hasattr(self, key):
@ -192,7 +198,7 @@ class MockStreamer(BaseCamera):
if isinstance(output, CaptureObject): if isinstance(output, CaptureObject):
target = output.file target = output.file
else: else:
target = target target = output
with self.lock: with self.lock:
if isinstance(target, str): if isinstance(target, str):

View file

@ -47,6 +47,11 @@ from .base import BaseCamera, CaptureObject
from .set_picamera_gain import set_analog_gain, set_digital_gain from .set_picamera_gain import set_analog_gain, set_digital_gain
from openflexure_microscope.paths import settings_file_path from openflexure_microscope.paths import settings_file_path
from openflexure_microscope.utilities import (
serialise_array_b64,
ndarray_to_json,
json_to_ndarray,
)
# MAIN CLASS # MAIN CLASS
@ -84,15 +89,8 @@ class PiCameraStreamer(BaseCamera):
picamera.PiCamera() picamera.PiCamera()
) #: :py:class:`picamera.PiCamera`: Picamera object ) #: :py:class:`picamera.PiCamera`: Picamera object
# Store status of PiCameraStreamer # Store state of PiCameraStreamer
self.status.update( self.preview_active = False
{
"stream_active": False,
"record_active": False,
"preview_active": False,
"board": f"picamera_{self.camera.revision}",
}
)
# Reset variable states # Reset variable states
self.set_zoom(1.0) self.set_zoom(1.0)
@ -116,15 +114,22 @@ class PiCameraStreamer(BaseCamera):
"picamera_lst.npy" "picamera_lst.npy"
) #: str: Path of .npy lens shading table file ) #: str: Path of .npy lens shading table file
# Update board identifier
self.status.update({})
# Create an empty stream # Create an empty stream
self.stream = io.BytesIO() self.stream = io.BytesIO()
# Start streaming # Start streaming
self.start_worker() self.start_worker()
@property
def configuration(self):
"""The current camera configuration."""
return {"board": self.camera.revision}
@property
def state(self):
"""The current read-only camera state."""
return {}
def initialisation(self): def initialisation(self):
"""Run any initialisation code when the frame iterator starts.""" """Run any initialisation code when the frame iterator starts."""
pass pass
@ -153,10 +158,7 @@ class PiCameraStreamer(BaseCamera):
"image_resolution": self.image_resolution, "image_resolution": self.image_resolution,
"numpy_resolution": self.numpy_resolution, "numpy_resolution": self.numpy_resolution,
"jpeg_quality": self.jpeg_quality, "jpeg_quality": self.jpeg_quality,
"picamera_lst_path": self.picamera_lst_path "picamera": {},
if os.path.isfile(self.picamera_lst_path)
else None,
"picamera_settings": {},
} }
) )
@ -165,18 +167,22 @@ class PiCameraStreamer(BaseCamera):
try: try:
value = getattr(self.camera, key) value = getattr(self.camera, key)
logging.debug("Reading PiCamera().{}: {}".format(key, value)) logging.debug("Reading PiCamera().{}: {}".format(key, value))
conf_dict["picamera_settings"][key] = value conf_dict["picamera"][key] = value
except AttributeError: except AttributeError:
logging.debug("Unable to read PiCamera attribute {}".format(key)) logging.debug("Unable to read PiCamera attribute {}".format(key))
# Include a serialised lens shading table
if (
hasattr(self.camera, "lens_shading_table")
and getattr(self.camera, "lens_shading_table") is not None
):
conf_dict["picamera"]["lens_shading_table"] = ndarray_to_json(
getattr(self.camera, "lens_shading_table")
)
return conf_dict return conf_dict
def save_settings(self): def update_settings(self, config: dict):
"""Save lens-shading table to disk"""
logging.info("Saving picamera_lst to {}".format(self.picamera_lst_path))
self.save_lens_shading_table()
def apply_settings(self, config: dict):
""" """
Write a config dictionary to the PiCameraStreamer config. Write a config dictionary to the PiCameraStreamer config.
@ -194,36 +200,37 @@ class PiCameraStreamer(BaseCamera):
with self.lock: with self.lock:
# Apply valid config params to Picamera object # Apply valid config params to Picamera object
if not self.status["record_active"]: # If not recording a video if not self.record_active: # If not recording a video
# Pause stream while changing settings # Pause stream while changing settings
if self.status["stream_active"]: # If stream is active if self.stream_active: # If stream is active
logging.info("Pausing stream to update config.") logging.info("Pausing stream to update config.")
self.stop_stream_recording() # Pause stream self.stop_stream_recording() # Pause stream
paused_stream = True # Remember to unpause stream when done paused_stream = True # Remember to unpause stream when done
# PiCamera parameters # PiCamera parameters
if "picamera_settings" in config: # If new settings are given if "picamera" in config: # If new settings are given
self.apply_picamera_settings( self.apply_picamera_settings(
config["picamera_settings"], pause_for_effect=True config["picamera"], pause_for_effect=True
) )
# Handle lens shading if camera supports it
if (
hasattr(self.camera, "lens_shading_table")
and "lens_shading_table" in config["picamera"]
):
try:
self.camera.lens_shading_table = json_to_ndarray(
config["picamera"].get("lens_shading_table")
)
except KeyError as e:
logging.error(e)
# PiCameraStreamer parameters # PiCameraStreamer parameters
for key, value in config.items(): # For each provided setting for key, value in config.items(): # For each provided setting
if (key != "picamera_settings") and hasattr(self, key): if (key != "picamera") and hasattr(self, key):
setattr(self, key, value) setattr(self, key, value)
# Handle lens shading if camera supports it
if ("picamera_lst_path" in config) and hasattr(
self.camera, "lens_shading_table"
):
logging.debug(
"Applying lens_shading_table from file: {}".format(
config["picamera_lst_path"]
)
)
self.apply_lens_shading_table(config["picamera_lst_path"])
# If stream was paused to update config, unpause # If stream was paused to update config, unpause
if paused_stream: if paused_stream:
logging.info("Resuming stream.") logging.info("Resuming stream.")
@ -292,53 +299,18 @@ class PiCameraStreamer(BaseCamera):
if pause_for_effect: if pause_for_effect:
time.sleep(0.2) time.sleep(0.2)
def read_lens_shading_table(self):
"""
Read the current lens shading table as a numpy array, if it exists. Return None otherwise.
"""
if hasattr(self.camera, "lens_shading_table"):
return self.camera.lens_shading_table
else:
return None
def save_lens_shading_table(self):
"""
Save the current lens shading table to an .npy file, if it exists.
"""
logging.debug(self.read_lens_shading_table())
if self.read_lens_shading_table() is not None:
np.save(self.picamera_lst_path, self.read_lens_shading_table())
else:
logging.warning("Unable to save a nonexistant lens shading table")
def apply_lens_shading_table(self, lst_array_or_path):
"""
Apply a lens shading table from an .npy file, or numpy array.
Args:
lst_array_or_path: Numpy array, or path to .npy file, describing the lens-shading table
"""
if isinstance(lst_array_or_path, np.ndarray):
self.camera.lens_shading_table = lst_array_or_path
elif (type(lst_array_or_path) == str) and os.path.isfile(lst_array_or_path):
self.camera.lens_shading_table = np.load(lst_array_or_path)
else:
logging.error(
"Unsupported or missing data for camera lens_shading_table. Must be numpy ndarray, or .npy file path string. Skipping."
)
def set_zoom(self, zoom_value: float = 1.0) -> None: def set_zoom(self, zoom_value: float = 1.0) -> None:
""" """
Change the camera zoom, handling re-centering and scaling. Change the camera zoom, handling re-centering and scaling.
""" """
with self.lock: with self.lock:
self.status["zoom_value"] = float(zoom_value) self.zoom_value = float(zoom_value)
if self.status["zoom_value"] < 1: if self.zoom_value < 1:
self.status["zoom_value"] = 1 self.zoom_value = 1
# Richard's code for zooming ! # Richard's code for zooming !
fov = self.camera.zoom fov = self.camera.zoom
centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0]) centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0])
size = 1.0 / self.status["zoom_value"] size = 1.0 / self.zoom_value
# If the new zoom value would be invalid, move the centre to # If the new zoom value would be invalid, move the centre to
# keep it within the camera's sensor (this is only relevant # keep it within the camera's sensor (this is only relevant
# when zooming out, if the FoV is not centred on (0.5, 0.5) # when zooming out, if the FoV is not centred on (0.5, 0.5)
@ -365,7 +337,7 @@ class PiCameraStreamer(BaseCamera):
self.camera.preview.window = window self.camera.preview.window = window
if fullscreen: if fullscreen:
self.camera.preview.fullscreen = fullscreen self.camera.preview.fullscreen = fullscreen
self.status["preview_active"] = True self.preview_active = True
except picamera.exc.PiCameraMMALError as e: except picamera.exc.PiCameraMMALError as e:
logging.error( logging.error(
"Suppressed a MMALError in start_preview. Exception: {}".format(e) "Suppressed a MMALError in start_preview. Exception: {}".format(e)
@ -380,7 +352,7 @@ class PiCameraStreamer(BaseCamera):
def stop_preview(self): def stop_preview(self):
"""Stop the on board GPU camera preview.""" """Stop the on board GPU camera preview."""
self.camera.stop_preview() self.camera.stop_preview()
self.status["preview_active"] = False self.preview_active = False
def start_recording(self, output, fmt: str = "h264", quality: int = 15): def start_recording(self, output, fmt: str = "h264", quality: int = 15):
"""Start recording. """Start recording.
@ -398,7 +370,7 @@ class PiCameraStreamer(BaseCamera):
""" """
with self.lock: with self.lock:
# Start recording method only if a current recording is not running # Start recording method only if a current recording is not running
if not self.status["record_active"]: if not self.record_active:
# Start the camera video recording on port 2 # Start the camera video recording on port 2
logging.info("Recording to {}".format(output)) logging.info("Recording to {}".format(output))
@ -411,8 +383,8 @@ class PiCameraStreamer(BaseCamera):
quality=quality, quality=quality,
) )
# Update status dictionary # Update state
self.status["record_active"] = True self.record_active = True
return output return output
@ -431,8 +403,8 @@ class PiCameraStreamer(BaseCamera):
self.camera.stop_recording(splitter_port=2) self.camera.stop_recording(splitter_port=2)
logging.info("Recording stopped") logging.info("Recording stopped")
# Update status dictionary # Update state
self.status["record_active"] = False self.record_active = False
def stop_stream_recording( def stop_stream_recording(
self, splitter_port: int = 1, resolution: Tuple[int, int] = None self, splitter_port: int = 1, resolution: Tuple[int, int] = None
@ -500,7 +472,7 @@ class PiCameraStreamer(BaseCamera):
self.camera.resolution = resolution self.camera.resolution = resolution
# If the stream should be active # If the stream should be active
if self.status["stream_active"]: if self.stream_active:
try: try:
# Start recording on stream port # Start recording on stream port
self.camera.start_recording( self.camera.start_recording(
@ -551,7 +523,7 @@ class PiCameraStreamer(BaseCamera):
if isinstance(output, CaptureObject): if isinstance(output, CaptureObject):
target = output.file target = output.file
else: else:
target = target target = output
with self.lock: with self.lock:
logging.info("Capturing to {}".format(output)) logging.info("Capturing to {}".format(output))
@ -680,7 +652,6 @@ class PiCameraStreamer(BaseCamera):
# Start stream recording (and set resolution) # Start stream recording (and set resolution)
self.start_stream_recording() self.start_stream_recording()
# Update status
logging.debug("STREAM ACTIVE") logging.debug("STREAM ACTIVE")
# While the iterator is not closed # While the iterator is not closed

View file

@ -1,4 +1,5 @@
import json import json
import flask
import os import os
import errno import errno
import logging import logging
@ -7,7 +8,12 @@ from uuid import UUID
import numpy as np import numpy as np
from fractions import Fraction from fractions import Fraction
from .paths import OPENFLEXURE_ETC_PATH, CONFIG_FILE_PATH, DEFAULT_CONFIG_FILE_PATH from .paths import (
SETTINGS_FILE_PATH,
DEFAULT_SETTINGS_FILE_PATH,
CONFIGURATION_FILE_PATH,
DEFAULT_CONFIGURATION_FILE_PATH,
)
class OpenflexureSettingsFile: class OpenflexureSettingsFile:
@ -19,21 +25,21 @@ class OpenflexureSettingsFile:
expand (bool): Expand paths to valid auxillary config files. expand (bool): Expand paths to valid auxillary config files.
""" """
def __init__(self, config_path: str = None): def __init__(self, path: str, defaults: dict = {}):
global DEFAULT_CONFIG, CONFIG_FILE_PATH global DEFAULT_SETTINGS
# Set arguments # Set arguments
self.config_path = config_path or CONFIG_FILE_PATH self.path = path
# Initialise basic config file with defaults if it doesn't exist # Initialise basic config file with defaults if it doesn't exist
initialise_file(self.config_path, populate=DEFAULT_CONFIG) initialise_file(self.path, populate=defaults)
def load(self) -> dict: def load(self) -> dict:
""" """
Loads settings from a file on-disk. Loads settings from a file on-disk.
""" """
# Unexpanded config dictionary (used at load/save time) # Unexpanded config dictionary (used at load/save time)
loaded_config = load_json_file(self.config_path) loaded_config = load_json_file(self.path)
logging.debug("Reading settings from disk") logging.debug("Reading settings from disk")
return loaded_config return loaded_config
@ -50,11 +56,11 @@ class OpenflexureSettingsFile:
save_settings = config save_settings = config
if backup: if backup:
if os.path.isfile(self.config_path): if os.path.isfile(self.path):
shutil.copyfile(self.config_path, self.config_path + ".bk") shutil.copyfile(self.path, self.path + ".bk")
logging.debug("Saving settings dictionary to disk") logging.debug("Saving settings dictionary to disk")
save_json_file(self.config_path, save_settings) save_json_file(self.path, save_settings)
def merge(self, config: dict) -> dict: def merge(self, config: dict) -> dict:
""" """
@ -71,7 +77,7 @@ class OpenflexureSettingsFile:
return settings return settings
class JSONEncoder(json.JSONEncoder): class JSONEncoder(flask.json.JSONEncoder):
""" """
A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays
""" """
@ -88,14 +94,13 @@ class JSONEncoder(json.JSONEncoder):
# Numpy arrays # Numpy arrays
elif isinstance(o, np.ndarray): elif isinstance(o, np.ndarray):
return o.tolist() return o.tolist()
# UUIDs
elif isinstance(o, UUID):
return str(o)
else: else:
# call base class implementation which takes care of # call base class implementation which takes care of
# raising exceptions for unsupported types # raising exceptions for unsupported types
try: return flask.json.JSONEncoder.default(self, o)
return json.JSONEncoder.default(self, o)
# if it's some mystery object, just return a string representation
except TypeError:
return str(o)
# HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES # HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES
@ -178,10 +183,22 @@ def initialise_file(config_path, populate: str = "{}\n"):
outfile.write(populate) outfile.write(populate)
# Load the default config # Load the default settings
with open(DEFAULT_CONFIG_FILE_PATH, "r") as default_rc: with open(DEFAULT_SETTINGS_FILE_PATH, "r") as default_settings:
DEFAULT_CONFIG = default_rc.read() DEFAULT_SETTINGS = default_settings.read()
#: Default user settings object #: Default user settings object
user_settings = OpenflexureSettingsFile(config_path=CONFIG_FILE_PATH) user_settings = OpenflexureSettingsFile(
path=SETTINGS_FILE_PATH, defaults=DEFAULT_SETTINGS
)
# Load the default configuration
with open(DEFAULT_CONFIGURATION_FILE_PATH, "r") as default_configuration:
DEFAULT_CONFIGURATION = default_configuration.read()
#: Default user settings object
user_configuration = OpenflexureSettingsFile(
path=CONFIGURATION_FILE_PATH, defaults=DEFAULT_CONFIGURATION
)

View file

@ -6,15 +6,21 @@ import logging
import pkg_resources import pkg_resources
import uuid import uuid
from openflexure_microscope.stage.base import BaseStage from openflexure_microscope.stage.mock import MissingStage
from openflexure_microscope.stage.mock import MockStage from openflexure_microscope.camera.mock import MissingCamera
from openflexure_microscope.camera.base import BaseCamera from openflexure_microscope.stage.sanga import SangaStage
from openflexure_microscope.camera.mock import MockStreamer
try:
from openflexure_microscope.camera.pi import PiCameraStreamer
except ImportError:
logging.warning("Unable to import PiCameraStreamer")
from openflexure_microscope.camera.mock import MissingCamera
from openflexure_microscope.utilities import serialise_array_b64 from openflexure_microscope.utilities import serialise_array_b64
from openflexure_microscope.config import user_settings from openflexure_microscope.config import user_settings, user_configuration
from labthings.core.lock import CompositeLock from labthings.core.lock import CompositeLock
from labthings.core.utilities import rupdate
class Microscope: class Microscope:
@ -24,20 +30,26 @@ class Microscope:
The camera and stage objects may already be initialised, and can be passed as arguments. The camera and stage objects may already be initialised, and can be passed as arguments.
""" """
def __init__(self): def __init__(self, settings=user_settings, configuration=user_configuration):
# Initial attributes self.id = uuid.uuid4()
self.id = uuid.uuid4() #: Microscope UUID self.name = self.id
self.name = self.id #: Microscope name (modifiable)
self.fov = [0, 0] #: Microscope field-of-view in stage motor steps self.fov = [0, 0] #: Microscope field-of-view in stage motor steps
self.camera = None #: Currently connected camera object
self.stage = None #: Currently connected stage object # Store settings and configuration files
self.settings_file = settings
self.configuration_file = configuration
# Initialise with an empty composite lock # Initialise with an empty composite lock
#: :py:class:`labthings.lock.CompositeLock`: Composite lock for locking both camera and stage #: :py:class:`labthings.lock.CompositeLock`: Composite lock for locking both camera and stage
self.lock = CompositeLock([]) self.lock = CompositeLock([])
self.camera = None #: Currently connected camera object
self.stage = None #: Currently connected stage object
self.setup(self.configuration_file.load()) # Attach components
# Apply settings loaded from file # Apply settings loaded from file
self.apply_settings(user_settings.load()) self.update_settings(self.settings_file.load())
def __enter__(self): def __enter__(self):
"""Create microscope on context enter.""" """Create microscope on context enter."""
@ -56,58 +68,49 @@ class Microscope:
self.stage.close() self.stage.close()
logging.info("Closed {}".format(self)) logging.info("Closed {}".format(self))
def attach(self, camera: BaseCamera, stage: BaseStage): def setup(self, configuration):
""" """
Retroactively attaches a camera and stage to the microscope object. Attach microscope components based on initially passed configuration file
Allows the microscope to be created as a "dummy", with hardware communications
opened at a later time.
Args:
camera (:py:class:`openflexure_microscope.camera.base.BaseCamera`): camera object
stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): stage object
""" """
settings_full = self.read_settings() ### Detector
if configuration.get("camera"):
camera_type = configuration["camera"].get("type")
if camera_type in ("PiCamera", "PiCameraStreamer"):
try:
self.camera = PiCameraStreamer()
except Exception as e:
logging.error(e)
logging.warning("No compatible camera hardware found.")
logging.debug("Attaching camera...") ### Stage
self.camera = ( if configuration.get("stage"):
camera stage_type = configuration["stage"].get("type")
) #: :py:class:`openflexure_microscope.camera.base.BaseCamera`: Picamera object stage_port = configuration["stage"].get("port")
if stage_type in ("SangaBoard", "SangaStage"):
try:
self.stage = SangaStage(port=stage_port)
except Exception as e:
logging.error(e)
logging.warning("No compatible Sangaboard hardware found.")
### Fallbacks
if not self.camera: if not self.camera:
logging.info("No camera attached.") self.camera = MissingCamera()
else:
logging.info("Attached camera {}".format(camera))
if hasattr(self.camera, "lock"): # If camera has a lock
logging.info("Attaching {} to composite lock.".format(self.camera.lock))
# Add the lock to the microscope composite lock
self.lock.locks.append(self.camera.lock)
logging.debug("Attaching stage...")
self.stage = (
stage
) #: :py:class:`openflexure_microscope.stage.base.BaseStage`: OpenFlexure stage object
if not self.stage: if not self.stage:
logging.info("No stage attached.") self.stage = MissingStage()
else:
logging.info("Attached stage {}".format(stage))
if hasattr(self.stage, "lock"): # If stage object has a lock ### Locks
logging.info( if hasattr(self.camera, "lock"):
"Attaching lock {} to composite lock.".format(self.stage.lock) self.lock.locks.append(self.camera.lock)
) if hasattr(self.stage, "lock"):
# Add the lock to the microscope composite lock self.lock.locks.append(self.stage.lock)
self.lock.locks.append(self.stage.lock)
logging.info("Reapplying settings to newly attached devices")
self.apply_settings(settings_full)
def has_real_stage(self) -> bool: def has_real_stage(self) -> bool:
""" """
Check if a real (non-mock) stage is currently attached. Check if a real (non-mock) stage is currently attached.
""" """
if hasattr(self, "stage") and not isinstance(self.stage, MockStage): if hasattr(self, "stage") and not isinstance(self.stage, MissingStage):
return True return True
else: else:
return False return False
@ -116,49 +119,45 @@ class Microscope:
""" """
Check if a real (non-mock) camera is currently attached. Check if a real (non-mock) camera is currently attached.
""" """
if hasattr(self, "camera") and not isinstance(self.camera, MockStreamer): if hasattr(self, "camera") and not isinstance(self.camera, MissingCamera):
return True return True
else: else:
return False return False
# Create unified status # Create unified state
@property @property
def status(self): def state(self):
"""Dictionary of the basic microscope status. """Dictionary of the basic microscope state.
Return: Return:
dict: Dictionary containing complete microscope status dict: Dictionary containing complete microscope state
""" """
state = { state = {"camera": self.camera.state, "stage": self.stage.state}
"camera": self.camera.status,
"stage": self.stage.status,
"version": pkg_resources.get_distribution("openflexure_microscope").version,
}
return state return state
def apply_settings(self, config: dict): def update_settings(self, settings: dict):
""" """
Applies a settings dictionary to the microscope. Missing parameters will be left untouched. Applies a settings dictionary to the microscope. Missing parameters will be left untouched.
""" """
logging.debug("Microscope: Applying config: {}".format(config)) logging.debug("Microscope: Applying settings: {}".format(settings))
# If attached to a camera # If attached to a camera
if ("camera_settings" in config) and self.camera: if ("camera" in settings) and self.camera:
self.camera.apply_settings(config["camera_settings"]) self.camera.update_settings(settings["camera"])
# If attached to a stage # If attached to a stage
if ("stage_settings" in config) and self.stage: if ("stage" in settings) and self.stage:
self.stage.apply_settings(config["stage_settings"]) self.stage.update_settings(settings["stage"])
# Todo: tidy up with some loopy goodness # Microscope settings
if "id" in config: if "id" in settings:
self.id = config["id"] self.id = settings["id"]
if "name" in config: if "name" in settings:
self.name = config["name"] self.name = settings["name"]
if "fov" in config: if "fov" in settings:
self.fov = config["fov"] self.fov = settings["fov"]
def read_settings(self, json_safe=False): def read_settings(self, full: bool = True):
""" """
Get an updated settings dictionary. Get an updated settings dictionary.
@ -174,16 +173,34 @@ class Microscope:
# If attached to a camera # If attached to a camera
if self.camera: if self.camera:
settings_current_camera = self.camera.read_settings() settings_current_camera = self.camera.read_settings()
settings_current["camera_settings"] = 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()
settings_current["stage_settings"] = settings_current_stage settings_current["stage"] = settings_current_stage
settings_full = user_settings.merge(settings_current) settings_full = self.settings_file.merge(settings_current)
return settings_full if full:
return settings_full
else:
return settings_current
def save_settings(self): def save_settings(self):
""" """
@ -192,11 +209,31 @@ class Microscope:
# Read curent config # Read curent config
current_config = self.read_settings() current_config = self.read_settings()
# Save config to file # Save config to file
if self.camera: self.settings_file.save(current_config, backup=True)
self.camera.save_settings()
if self.stage: @property
self.stage.save_settings() def configuration(self):
user_settings.save(current_config, backup=True) initial_configuration = self.configuration_file.load()
current_configuration = {
"application": {
"name": "openflexure_microscope",
"version": pkg_resources.get_distribution(
"openflexure_microscope"
).version,
},
"stage": {
"type": self.stage.__class__.__name__,
**self.stage.configuration,
},
"camera": {
"type": self.camera.__class__.__name__,
**self.camera.configuration,
},
}
initial_configuration.update(current_configuration)
return initial_configuration
@property @property
def metadata(self): def metadata(self):
@ -204,23 +241,10 @@ class Microscope:
Microscope system metadata, to be applied to basically all captures Microscope system metadata, to be applied to basically all captures
""" """
system_metadata = { system_metadata = {
"microscope_settings": self.read_settings(), "id": self.id,
"microscope_state": self.status, "settings": self.read_settings(full=False),
"microscope_id": self.id, "state": self.state,
"microscope_name": self.name, "configuration": self.configuration,
} }
# Store an encoded copy of the PiCamera lens shading table, if it exists
if self.camera and hasattr(self.camera, "read_lens_shading_table"):
# Read LST. Returns None if no LST is active
lst_arr = self.camera.read_lens_shading_table()
b64_string, dtype, shape = serialise_array_b64(lst_arr)
system_metadata["lens_shading_table"] = {
"b64_string": b64_string,
"dtype": dtype,
"shape": shape,
}
return system_metadata return system_metadata

View file

@ -0,0 +1,9 @@
{
"camera": {
"type": "PiCamera"
},
"stage": {
"type": "SangaStage",
"port": null
}
}

View file

@ -3,5 +3,5 @@
4100, 4100,
3146 3146
], ],
"jpeg_quality": 75 "jpeg_quality": 100
} }

View file

@ -10,12 +10,34 @@ def check_rw(path):
def settings_file_path(filename: str): def settings_file_path(filename: str):
"""Generate a full file path for a filename to be stored in server settings folder""" """Generate a full file path for a filename to be stored in server settings folder"""
return os.path.join(OPENFLEXURE_ETC_PATH, filename) settings_dir = os.path.join(OPENFLEXURE_VAR_PATH, "settings")
if not os.path.exists(settings_dir):
os.makedirs(settings_dir)
return os.path.join(settings_dir, filename)
def data_file_path(filename: str): def data_file_path(filename: str):
"""Generate a full file path for a filename to be stored in server data folder""" """Generate a full file path for a filename to be stored in server data folder"""
return os.path.join(OPENFLEXURE_VAR_PATH, filename) data_dir = os.path.join(OPENFLEXURE_VAR_PATH, "data")
if not os.path.exists(data_dir):
os.makedirs(data_dir)
return os.path.join(data_dir, filename)
def extensions_file_path(filename: str):
"""Generate a full file path for a folder to be stored in server extensions"""
ext_dir = os.path.join(OPENFLEXURE_VAR_PATH, "extensions")
if not os.path.exists(ext_dir):
os.makedirs(ext_dir)
return os.path.join(ext_dir, filename)
def logs_file_path(filename: str):
"""Generate a full file path for a filename to be stored in server logs"""
logs_dir = os.path.join(OPENFLEXURE_VAR_PATH, "logs")
if not os.path.exists(logs_dir):
os.makedirs(logs_dir)
return os.path.join(logs_dir, filename)
# HANDLE DEFAULTS FILES STORED IN THIS APPLICATION # HANDLE DEFAULTS FILES STORED IN THIS APPLICATION
@ -23,10 +45,13 @@ def data_file_path(filename: str):
HERE = os.path.abspath(os.path.dirname(__file__)) HERE = os.path.abspath(os.path.dirname(__file__))
#: Path of default (first-run) microscope settings #: Path of default (first-run) microscope settings
DEFAULT_CONFIG_FILE_PATH = os.path.join(HERE, "microscope_settings.default.json") DEFAULT_SETTINGS_FILE_PATH = os.path.join(HERE, "microscope_settings.default.json")
#: Path of default (first-run) microscope configuration
DEFAULT_CONFIGURATION_FILE_PATH = os.path.join(
HERE, "microscope_configuration.default.json"
)
# BASE PATHS
# DATA BASE PATHS
if os.name == "nt": if os.name == "nt":
PREFERRED_VAR_PATH = os.getenv("PROGRAMDATA") or "C:\\ProgramData" PREFERRED_VAR_PATH = os.getenv("PROGRAMDATA") or "C:\\ProgramData"
@ -49,37 +74,11 @@ else:
OPENFLEXURE_VAR_PATH = FALLBACK_OPENFLEXURE_VAR_PATH OPENFLEXURE_VAR_PATH = FALLBACK_OPENFLEXURE_VAR_PATH
# SERVER BASE PATHS
if os.name == "nt":
PREFERRED_ETC_PATH = os.getenv("PROGRAMDATA") or "C:\\ProgramData"
FALLBACK_ETC_PATH = os.path.expanduser("~")
else:
PREFERRED_ETC_PATH = "/etc"
FALLBACK_ETC_PATH = os.path.join(os.path.expanduser("~"), ".config")
PREFERRED_OPENFLEXURE_ETC_PATH = os.path.join(PREFERRED_ETC_PATH, "openflexure")
FALLBACK_OPENFLEXURE_ETC_PATH = os.path.join(FALLBACK_ETC_PATH, "openflexure")
if not os.path.exists(PREFERRED_OPENFLEXURE_ETC_PATH) and check_rw(PREFERRED_ETC_PATH):
os.makedirs(PREFERRED_OPENFLEXURE_ETC_PATH)
if check_rw(PREFERRED_OPENFLEXURE_ETC_PATH):
OPENFLEXURE_ETC_PATH = PREFERRED_OPENFLEXURE_ETC_PATH
else:
if not os.path.exists(FALLBACK_OPENFLEXURE_ETC_PATH):
os.makedirs(FALLBACK_OPENFLEXURE_ETC_PATH)
OPENFLEXURE_ETC_PATH = FALLBACK_OPENFLEXURE_ETC_PATH
# SERVER PATHS # SERVER PATHS
#: Path of microscope settings directory #: Path of microscope settings file
CONFIG_FILE_PATH = os.path.join(OPENFLEXURE_ETC_PATH, "microscope_settings.json") SETTINGS_FILE_PATH = settings_file_path("microscope_settings.json")
#: Path of microscope configuration file
CONFIGURATION_FILE_PATH = settings_file_path("microscope_configuration.json")
#: Path of microscope extensions directory #: Path of microscope extensions directory
OPENFLEXURE_EXTENSIONS_PATH = os.path.join( OPENFLEXURE_EXTENSIONS_PATH = extensions_file_path("microscope_extensions")
OPENFLEXURE_ETC_PATH, "microscope_extensions"
)
# DATA PATHS

View file

@ -0,0 +1,77 @@
from openflexure_microscope.paths import (
FALLBACK_OPENFLEXURE_VAR_PATH,
PREFERRED_OPENFLEXURE_VAR_PATH,
)
import logging
import os
from . import check_settings, check_capture_reload
# Paths for suggestions
LOGS_PATHS = [
os.path.join(PREFERRED_OPENFLEXURE_VAR_PATH, "logs"),
os.path.join(FALLBACK_OPENFLEXURE_VAR_PATH, "logs"),
]
SETTINGS_PATHS = [
os.path.join(var_path, "settings", "microscope_settings.json")
for var_path in (PREFERRED_OPENFLEXURE_VAR_PATH, FALLBACK_OPENFLEXURE_VAR_PATH)
]
CONFIG_PATHS = [
os.path.join(var_path, "settings", "microscope_configuration.json")
for var_path in (PREFERRED_OPENFLEXURE_VAR_PATH, FALLBACK_OPENFLEXURE_VAR_PATH)
]
DATA_PATHS = [
os.path.join(PREFERRED_OPENFLEXURE_VAR_PATH, "data"),
os.path.join(FALLBACK_OPENFLEXURE_VAR_PATH, "data"),
]
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
logging.debug("Testing debug logger. One two one two.")
class bcolors:
HEADER = "\033[95m"
OKBLUE = "\033[94m"
OKGREEN = "\033[92m"
WARNING = "\033[93m"
FAIL = "\033[91m"
ENDC = "\033[0m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"
error_keys = {
"config_settings_import_error": "The configuration submodule could not be imported properly. This is usually because the default config or settings files are badly broken somehow. See further errors for details",
"default_config_empty": "The default configuration file is empty, and could not be automatically populated.",
"default_settings_empty": "The default settings file is empty, and could not be automatically populated.",
"default_config_error": f"The default configuration file could not be parsed. To fix, consider backing up and deleting your configuration files from {CONFIG_PATHS} to reset the configuration.",
"default_settings_error": f"The default settings file could not be parsed. To fix, consider backing up and deleting your configuration files from {SETTINGS_PATHS} to reset the configuration.",
"capture_rebuild_timeout": f"Capture database rebuilding took a long time. This may not cause catastrophic errors, but rather will cause the server to hang for a while. To fix, consider moving your captures from {DATA_PATHS} to another location.",
}
if __name__ == "__main__":
spoof = False
error_sources = []
if spoof:
error_sources = list(error_keys.keys())
error_sources.extend(check_settings.main())
error_sources.extend(check_capture_reload.main())
if not error_sources:
print()
print(bcolors.OKGREEN + "No errors found!" + bcolors.ENDC)
print(
"That's not to say everything is fine, only that our automatic diagnostics couldn't find much."
)
print(f"You can check through the server logs at {LOGS_PATHS}")
else:
for err_code in error_sources:
logging.error(err_code)
if error_keys.get(err_code):
print(bcolors.FAIL + error_keys.get(err_code) + bcolors.ENDC)

View file

@ -0,0 +1,34 @@
from openflexure_microscope.rescue.monitor_timeout import launch_timeout_test_process
from openflexure_microscope.camera.capture import build_captures_from_exif
from openflexure_microscope.camera.base import BASE_CAPTURE_PATH
from openflexure_microscope.config import user_settings
import logging
def check_capture_rebuild(timeout=10):
logging.info("Loading user settings...")
settings = user_settings.load()
cap_path = str(settings.get("camera", {}).get("paths", {}).get("default"))
logging.info(f"Capture path found: {cap_path}")
if not cap_path:
logging.error(
"No capture path defined in settings. This is unusual for anything other than a first-run. \nFalling back to default path."
)
cap_path = BASE_CAPTURE_PATH
logging.info("Starting capture reload with a 10 second timeout...")
passed_timeout_test = launch_timeout_test_process(
build_captures_from_exif, args=(cap_path,)
)
return passed_timeout_test
def main():
error_sources = []
passed_timeout = check_capture_rebuild()
if not passed_timeout:
error_sources.append("capture_rebuild_timeout")
return error_sources

View file

@ -0,0 +1,47 @@
import logging
import json
ERROR_SOURCES = []
def trace_config_exceptions():
error_sources = []
from openflexure_microscope.paths import (
DEFAULT_CONFIGURATION_FILE_PATH,
SETTINGS_FILE_PATH,
)
try:
default_config = json.load(DEFAULT_CONFIGURATION_FILE_PATH)
if not default_config:
error_sources.append("default_config_empty")
except Exception as e:
logging.error("Error parsing config:")
logging.error(e)
error_sources.append("default_config_error")
try:
default_settings = json.load(SETTINGS_FILE_PATH)
if not default_settings:
error_sources.append("default_settings_empty")
except Exception as e:
logging.error("Error parsing settings:")
logging.error(e)
error_sources.append("default_settings_error")
return error_sources
def main():
error_sources = []
logging.info("Attempting default settings and config import...")
try:
from openflexure_microscope import config
except Exception as e:
error_sources.append("config_settings_import_error")
logging.error("Error importing config:")
logging.error(e)
error_sources.extend(trace_config_exceptions())
return error_sources

View file

@ -0,0 +1,59 @@
#!/bin/python
#
# Copyright 2016 Flavio Garcia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Usage: monitor_process.py <service_name>
#
# Example(crontab, every 5 minutes):
# */5 * * * * /root/bin/monitor_service.py prosody > /dev/null 2>&1
#
import sys
import subprocess
class ServiceMonitor(object):
def __init__(self, service):
self.service = service
def is_active(self):
"""Return True if service is running"""
for line in self.status():
if "Active:" in line:
if "(running)" in line:
return True
return False
def status(self):
cmd = f"/bin/systemctl status {self.service}.service"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
stdout_list = proc.communicate()[0].decode().split("\n")
return stdout_list
def start(self):
cmd = f"/bin/systemctl start {self.service}.service"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
proc.communicate()
def stop(self):
cmd = f"/bin/systemctl stop {self.service}.service"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
proc.communicate()
def log(self, n_lines: int = 100):
cmd = f"/bin/journalctl -u {self.service}.service -n {n_lines} --no-pager"
proc = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
stdout_list = proc.communicate()[0].decode().split("\n")
return stdout_list

View file

@ -0,0 +1,39 @@
import multiprocessing
import logging
import time
# bar
def test_long_fn():
for _ in range(100):
print("Tick")
time.sleep(1)
def launch_timeout_test_process(target, args=(), kwargs=None, timeout=10):
if not kwargs:
kwargs = {}
# Start target as a process
p = multiprocessing.Process(target=target, args=args, kwargs=kwargs)
p.start()
# Wait for 10 seconds or until process finishes
p.join(timeout)
# If thread is still active
if p.is_alive():
logging.error(
f"Function {target} reached timeout after {timeout} seconds. Terminating."
)
# Terminate
p.terminate()
p.join()
return False
else:
return True
if __name__ == "__main__":
launch_timeout_test_process(test_long_fn, timeout=5)

View file

@ -14,7 +14,7 @@ class BaseStage(metaclass=ABCMeta):
self.lock = StrictLock(timeout=5) self.lock = StrictLock(timeout=5)
@abstractmethod @abstractmethod
def apply_settings(self, config: dict): def update_settings(self, config: dict):
"""Update settings from a config dictionary""" """Update settings from a config dictionary"""
pass pass
@ -23,18 +23,16 @@ class BaseStage(metaclass=ABCMeta):
"""Return the current settings as a dictionary""" """Return the current settings as a dictionary"""
pass pass
def save_settings(self): @property
"""(Optional) Save any settings to disk that need to be stored""" @abstractmethod
return def state(self):
"""The general state dictionary of the board."""
pass
@property @property
@abstractmethod @abstractmethod
def status(self): def configuration(self):
"""The general state dictionary of the board. """The general stage configuration."""
Should at least contain 'position', and 'board' keys.
Note: A None/Null value for 'board' will disable stage
movement in the OpenFlexure eV client software,
"""
pass pass
@property @property
@ -51,11 +49,7 @@ class BaseStage(metaclass=ABCMeta):
@property @property
def position_map(self): def position_map(self):
return { return {"x": self.position[0], "y": self.position[1], "z": self.position[2]}
"x": self.position[0],
"y": self.position[1],
"z": self.position[2],
}
@property @property
@abstractmethod @abstractmethod

View file

@ -7,7 +7,7 @@ import time
import logging import logging
class MockStage(BaseStage): class MissingStage(BaseStage):
def __init__(self, port=None, **kwargs): def __init__(self, port=None, **kwargs):
BaseStage.__init__(self) BaseStage.__init__(self)
self._position = [0, 0, 0] self._position = [0, 0, 0]
@ -17,19 +17,17 @@ class MockStage(BaseStage):
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
@property @property
def status(self): def state(self):
"""The general status dictionary of the board.""" """The general state dictionary of the board."""
status = { state = {"position": self.position_map}
"position": self.position_map, return state
"board": None,
"firmware": None,
"version": None
}
return status
def apply_settings(self, config: dict): @property
def configuration(self):
return {}
def update_settings(self, config: dict):
"""Update settings from a config dictionary""" """Update settings from a config dictionary"""
# Set backlash. Expects a dictionary with axis labels # Set backlash. Expects a dictionary with axis labels
if "backlash" in config: if "backlash" in config:
# Construct backlash array # Construct backlash array

View file

@ -23,6 +23,7 @@ class SangaStage(BaseStage):
"""Class managing serial communications with the motors for an Openflexure stage""" """Class managing serial communications with the motors for an Openflexure stage"""
BaseStage.__init__(self) BaseStage.__init__(self)
self.port = port
self.board = Sangaboard(port, **kwargs) self.board = Sangaboard(port, **kwargs)
self._backlash = ( self._backlash = (
@ -31,14 +32,17 @@ class SangaStage(BaseStage):
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
@property @property
def status(self): def state(self):
"""The general status dictionary of the board.""" """The general state dictionary of the board."""
status = { return {"position": self.position_map}
"position": self.position_map,
@property
def configuration(self):
return {
"port": self.port,
"board": self.board.board, "board": self.board.board,
"firmware": self.board.firmware, "firmware": self.board.firmware,
} }
return status
@property @property
def n_axes(self): def n_axes(self):
@ -81,7 +85,7 @@ class SangaStage(BaseStage):
else: else:
self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int) self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int)
def apply_settings(self, config: dict): def update_settings(self, config: dict):
"""Update settings from a config dictionary""" """Update settings from a config dictionary"""
# Set backlash. Expects a dictionary with axis labels # Set backlash. Expects a dictionary with axis labels

View file

@ -4,6 +4,7 @@ import operator
import base64 import base64
from uuid import UUID from uuid import UUID
import numpy as np import numpy as np
import logging
from collections import abc from collections import abc
from functools import reduce from functools import reduce
from contextlib import contextmanager from contextlib import contextmanager
@ -21,6 +22,31 @@ def serialise_array_b64(npy_arr):
return b64_string, dtype, shape return b64_string, dtype, shape
def ndarray_to_json(arr: np.ndarray):
if isinstance(arr, memoryview):
# We can transparently convert memoryview objects to arrays
# This comes in very handy for the lens shading table.
arr = np.array(arr)
b64_string, dtype, shape = serialise_array_b64(arr)
return {
"@type": "ndarray",
"dtype": dtype,
"shape": shape,
"base64": b64_string
}
def json_to_ndarray(json_dict: dict):
if not json_dict.get("@type") != "ndarray":
logging.warning("No valid @type attribute found. Conversion may fail.")
for required_param in ("dtype", "shape", "base64"):
if not json_dict.get(required_param):
raise KeyError(f"Missing required key {required_param}")
return deserialise_array_b64(json_dict.get("base64"), json_dict.get("dtype"), json_dict.get("shape"))
@contextmanager @contextmanager
def set_properties(obj, **kwargs): def set_properties(obj, **kwargs):
"""A context manager to set, then reset, certain properties of an object. """A context manager to set, then reset, certain properties of an object.

518
poetry.lock generated
View file

@ -12,12 +12,12 @@ description = "A pluggable API specification generator. Currently supports the O
name = "apispec" name = "apispec"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "3.2.0" version = "3.3.0"
[package.extras] [package.extras]
dev = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock", "flake8 (3.7.9)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.20,<2.0)", "tox"] dev = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)", "tox"]
docs = ["marshmallow (>=2.19.2)", "pyyaml (5.2)", "sphinx (2.3.0)", "sphinx-issues (1.2.0)", "sphinx-rtd-theme (0.4.3)"] docs = ["marshmallow (>=2.19.2)", "pyyaml (5.3)", "sphinx (2.4.1)", "sphinx-issues (1.2.0)", "sphinx-rtd-theme (0.4.3)"]
lint = ["flake8 (3.7.9)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.20,<2.0)"] lint = ["flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)"]
tests = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock"] tests = ["PyYAML (>=3.10)", "prance (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock"]
validation = ["prance (>=0.11)"] validation = ["prance (>=0.11)"]
yaml = ["PyYAML (>=3.10)"] yaml = ["PyYAML (>=3.10)"]
@ -97,6 +97,18 @@ optional = false
python-versions = "*" python-versions = "*"
version = "2019.11.28" version = "2019.11.28"
[[package]]
category = "main"
description = "Foreign Function Interface for Python calling C code."
marker = "sys_platform == \"win32\" and platform_python_implementation == \"CPython\""
name = "cffi"
optional = false
python-versions = "*"
version = "1.14.0"
[package.dependencies]
pycparser = "*"
[[package]] [[package]]
category = "dev" category = "dev"
description = "Universal encoding detector for Python 2 and 3" description = "Universal encoding detector for Python 2 and 3"
@ -110,8 +122,8 @@ category = "main"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
name = "click" name = "click"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "7.0" version = "7.1.1"
[[package]] [[package]]
category = "dev" category = "dev"
@ -161,13 +173,51 @@ version = "3.0.8"
Flask = ">=0.9" Flask = ">=0.9"
Six = "*" Six = "*"
[[package]]
category = "main"
description = "Coroutine-based network library"
name = "gevent"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.4.0"
[package.dependencies]
cffi = ">=1.11.5"
greenlet = ">=0.4.14"
[package.extras]
dnspython = ["dnspython", "idna"]
doc = ["repoze.sphinx.autointerface"]
events = ["zope.event", "zope.interface"]
test = ["zope.interface", "zope.event", "requests", "objgraph", "psutil", "futures", "mock", "coverage (>=5.0a3)", "coveralls (>=1.0)"]
[[package]]
category = "main"
description = "Websocket handler for the gevent pywsgi server, a Python network library"
name = "gevent-websocket"
optional = false
python-versions = "*"
version = "0.10.1"
[package.dependencies]
gevent = "*"
[[package]]
category = "main"
description = "Lightweight in-process concurrent programming"
marker = "platform_python_implementation == \"CPython\""
name = "greenlet"
optional = false
python-versions = "*"
version = "0.4.15"
[[package]] [[package]]
category = "dev" category = "dev"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
name = "idna" name = "idna"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.8" version = "2.9"
[[package]] [[package]]
category = "dev" category = "dev"
@ -204,8 +254,8 @@ category = "main"
description = "A very fast and expressive template engine." description = "A very fast and expressive template engine."
name = "jinja2" name = "jinja2"
optional = false optional = false
python-versions = "*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.10.3" version = "2.11.1"
[package.dependencies] [package.dependencies]
MarkupSafe = ">=0.23" MarkupSafe = ">=0.23"
@ -215,23 +265,21 @@ i18n = ["Babel (>=0.8)"]
[[package]] [[package]]
category = "main" category = "main"
description = "" description = "Python implementation of LabThings, based on the Flask microframework"
name = "labthings" name = "labthings"
optional = false optional = false
python-versions = "^3.6" python-versions = ">=3.6,<4.0"
version = "0.1.0" version = "0.2.0"
[package.dependencies] [package.dependencies]
Flask = "^1.1.1" Flask = ">=1.1.1,<2.0.0"
apispec = "^3.2.0" apispec = ">=3.2.0,<4.0.0"
flask-cors = "^3.0.8" flask-cors = ">=3.0.8,<4.0.0"
marshmallow = "^3.3.0" gevent = ">=1.4.0,<2.0.0"
webargs = "^5.5.2" gevent-websocket = ">=0.10.1,<0.11.0"
marshmallow = ">=3.4.0,<4.0.0"
webargs = ">=5.5.3,<6.0.0"
[package.source]
reference = "ea5142a9642e7b852a6e1175f3c4aa1a4bfac044"
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."
@ -254,12 +302,12 @@ description = "A lightweight library for converting complex datatypes to and fro
name = "marshmallow" name = "marshmallow"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "3.3.0" version = "3.5.1"
[package.extras] [package.extras]
dev = ["pytest", "pytz", "simplejson", "mypy (0.750)", "flake8 (3.7.9)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.20,<2.0)", "tox"] dev = ["pytest", "pytz", "simplejson", "mypy (0.761)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)", "tox"]
docs = ["sphinx (2.2.2)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)"] docs = ["sphinx (2.4.3)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)"]
lint = ["mypy (0.750)", "flake8 (3.7.9)", "flake8-bugbear (19.8.0)", "pre-commit (>=1.20,<2.0)"] lint = ["mypy (0.761)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)"]
tests = ["pytest", "pytz", "simplejson"] tests = ["pytest", "pytz", "simplejson"]
[[package]] [[package]]
@ -276,7 +324,7 @@ description = "NumPy is the fundamental package for array computing with Python.
name = "numpy" name = "numpy"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "1.18.1" version = "1.18.2"
[[package]] [[package]]
category = "dev" category = "dev"
@ -284,7 +332,7 @@ description = "Core utilities for Python packages"
name = "packaging" name = "packaging"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "20.0" version = "20.3"
[package.dependencies] [package.dependencies]
pyparsing = ">=2.0.2" pyparsing = ">=2.0.2"
@ -296,7 +344,7 @@ description = "A pure Python interface for the Raspberry Pi camera module."
name = "picamera" name = "picamera"
optional = true optional = true
python-versions = "*" python-versions = "*"
version = "1.13.1b0" version = "1.13.2b0"
[package.extras] [package.extras]
array = ["numpy"] array = ["numpy"]
@ -304,7 +352,7 @@ doc = ["sphinx"]
test = ["coverage", "pytest", "mock", "pillow", "numpy"] test = ["coverage", "pytest", "mock", "pillow", "numpy"]
[package.source] [package.source]
reference = "b39c2b6e42f5f7f57bb46eafcb5c9e2bbdb5d0cb" reference = "79177fa7f28d6d5c6eab5ba814b420b1785b6b24"
type = "git" type = "git"
url = "https://github.com/rwb27/picamera.git" url = "https://github.com/rwb27/picamera.git"
[[package]] [[package]]
@ -315,13 +363,33 @@ optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "5.4.1" version = "5.4.1"
[[package]]
category = "main"
description = "Cross-platform lib for process and system monitoring in Python."
name = "psutil"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "5.7.0"
[package.extras]
enum = ["enum34"]
[[package]]
category = "main"
description = "C parser in Python"
marker = "sys_platform == \"win32\" and platform_python_implementation == \"CPython\""
name = "pycparser"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "2.20"
[[package]] [[package]]
category = "dev" category = "dev"
description = "Pygments is a syntax highlighting package written in Python." description = "Pygments is a syntax highlighting package written in Python."
name = "pygments" name = "pygments"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=3.5"
version = "2.5.2" version = "2.6.1"
[[package]] [[package]]
category = "dev" category = "dev"
@ -353,6 +421,17 @@ optional = false
python-versions = "*" python-versions = "*"
version = "3.4" version = "3.4"
[[package]]
category = "main"
description = "Extensions to the standard Python datetime module"
name = "python-dateutil"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
version = "2.8.1"
[package.dependencies]
six = ">=1.5"
[[package]] [[package]]
category = "dev" category = "dev"
description = "World timezone definitions, modern and historical" description = "World timezone definitions, modern and historical"
@ -361,30 +440,22 @@ optional = false
python-versions = "*" python-versions = "*"
version = "2019.3" version = "2019.3"
[[package]]
category = "main"
description = "YAML parser and emitter for Python"
name = "pyyaml"
optional = false
python-versions = "*"
version = "3.13"
[[package]] [[package]]
category = "dev" category = "dev"
description = "Python HTTP for Humans." description = "Python HTTP for Humans."
name = "requests" name = "requests"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "2.22.0" version = "2.23.0"
[package.dependencies] [package.dependencies]
certifi = ">=2017.4.17" certifi = ">=2017.4.17"
chardet = ">=3.0.2,<3.1.0" chardet = ">=3.0.2,<4"
idna = ">=2.5,<2.9" idna = ">=2.5,<3"
urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
[package.extras] [package.extras]
security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)"] security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
[[package]] [[package]]
@ -414,6 +485,14 @@ version = "1.3.0"
[package.dependencies] [package.dependencies]
numpy = ">=1.13.3" numpy = ">=1.13.3"
[[package]]
category = "main"
description = "Simple, fast, extensible JSON encoder/decoder for Python"
name = "simplejson"
optional = false
python-versions = ">=2.5, !=3.0.*, !=3.1.*, !=3.2.*"
version = "3.17.0"
[[package]] [[package]]
category = "main" category = "main"
description = "Python 2 and 3 compatibility utilities" description = "Python 2 and 3 compatibility utilities"
@ -436,7 +515,7 @@ description = "Python documentation generator"
name = "sphinx" name = "sphinx"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "2.3.1" version = "2.4.4"
[package.dependencies] [package.dependencies]
Jinja2 = ">=2.3" Jinja2 = ">=2.3"
@ -459,40 +538,43 @@ sphinxcontrib-serializinghtml = "*"
[package.extras] [package.extras]
docs = ["sphinxcontrib-websupport"] docs = ["sphinxcontrib-websupport"]
test = ["pytest", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.761)", "docutils-stubs"] test = ["pytest (<5.3.3)", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.761)", "docutils-stubs"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "" description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
name = "sphinxcontrib-applehelp" name = "sphinxcontrib-applehelp"
optional = false optional = false
python-versions = "*" python-versions = ">=3.5"
version = "1.0.1"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]]
category = "dev"
description = ""
name = "sphinxcontrib-devhelp"
optional = false
python-versions = "*"
version = "1.0.1"
[package.extras]
test = ["pytest", "flake8", "mypy"]
[[package]]
category = "dev"
description = ""
name = "sphinxcontrib-htmlhelp"
optional = false
python-versions = "*"
version = "1.0.2" version = "1.0.2"
[package.extras] [package.extras]
test = ["pytest", "flake8", "mypy", "html5lib"] lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]]
category = "dev"
description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
name = "sphinxcontrib-devhelp"
optional = false
python-versions = ">=3.5"
version = "1.0.2"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]]
category = "dev"
description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
name = "sphinxcontrib-htmlhelp"
optional = false
python-versions = ">=3.5"
version = "1.0.3"
[package.extras]
lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest", "html5lib"]
[[package]] [[package]]
category = "dev" category = "dev"
@ -519,25 +601,27 @@ test = ["pytest", "flake8", "mypy"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
name = "sphinxcontrib-qthelp" name = "sphinxcontrib-qthelp"
optional = false optional = false
python-versions = "*" python-versions = ">=3.5"
version = "1.0.2" version = "1.0.3"
[package.extras] [package.extras]
test = ["pytest", "flake8", "mypy"] lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
name = "sphinxcontrib-serializinghtml" name = "sphinxcontrib-serializinghtml"
optional = false optional = false
python-versions = "*" python-versions = ">=3.5"
version = "1.1.3" version = "1.1.4"
[package.extras] [package.extras]
test = ["pytest", "flake8", "mypy"] lint = ["flake8", "mypy", "docutils-stubs"]
test = ["pytest"]
[[package]] [[package]]
category = "dev" category = "dev"
@ -561,8 +645,8 @@ category = "dev"
description = "HTTP library with thread-safe connection pooling, file post, and more." description = "HTTP library with thread-safe connection pooling, file post, and more."
name = "urllib3" name = "urllib3"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
version = "1.25.7" version = "1.25.8"
[package.extras] [package.extras]
brotli = ["brotlipy (>=0.6.0)"] brotli = ["brotlipy (>=0.6.0)"]
@ -575,10 +659,11 @@ description = "Declarative parsing and validation of HTTP request objects, with
name = "webargs" name = "webargs"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "5.5.2" version = "5.5.3"
[package.dependencies] [package.dependencies]
marshmallow = ">=2.15.2" marshmallow = ">=2.15.2"
simplejson = ">=2.1.0"
[package.extras] [package.extras]
dev = ["pytest", "mock", "webtest (2.0.33)", "Flask (>=0.12.2)", "Django (>=1.11.16)", "bottle (>=0.12.13)", "tornado (>=4.5.2)", "pyramid (>=1.9.1)", "webapp2 (>=3.0.0b1)", "falcon (>=1.4.0,<2.0)", "flake8 (3.7.8)", "pre-commit (>=1.17,<2.0)", "tox", "webtest-aiohttp (2.0.0)", "pytest-aiohttp (>=0.3.0)", "aiohttp (>=3.0.0)", "mypy (0.730)", "flake8-bugbear (19.8.0)"] dev = ["pytest", "mock", "webtest (2.0.33)", "Flask (>=0.12.2)", "Django (>=1.11.16)", "bottle (>=0.12.13)", "tornado (>=4.5.2)", "pyramid (>=1.9.1)", "webapp2 (>=3.0.0b1)", "falcon (>=1.4.0,<2.0)", "flake8 (3.7.8)", "pre-commit (>=1.17,<2.0)", "tox", "webtest-aiohttp (2.0.0)", "pytest-aiohttp (>=0.3.0)", "aiohttp (>=3.0.0)", "mypy (0.730)", "flake8-bugbear (19.8.0)"]
@ -592,12 +677,11 @@ category = "main"
description = "The comprehensive WSGI web application library." description = "The comprehensive WSGI web application library."
name = "werkzeug" name = "werkzeug"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "0.16.0" version = "1.0.0"
[package.extras] [package.extras]
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"] dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
termcolor = ["termcolor"]
watchdog = ["watchdog"] watchdog = ["watchdog"]
[[package]] [[package]]
@ -612,7 +696,7 @@ version = "1.11.2"
rpi = ["picamera", "RPi.GPIO"] rpi = ["picamera", "RPi.GPIO"]
[metadata] [metadata]
content-hash = "53c82daa835ef29f7aac8ac7d36048487a9d989662a58a2d05d9a70465fbc8c6" content-hash = "3a3a3bd4ffd7e0fe4c4c418a1b191c33afd375e6f06bb298189fd84ff75ee4cc"
python-versions = "^3.6" python-versions = "^3.6"
[metadata.files] [metadata.files]
@ -621,8 +705,8 @@ alabaster = [
{file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
] ]
apispec = [ apispec = [
{file = "apispec-3.2.0-py2.py3-none-any.whl", hash = "sha256:f2ecb3a6534cfb42cf69b5c1222d2c0e695aab5ec3d7edde31b7354670948f05"}, {file = "apispec-3.3.0-py2.py3-none-any.whl", hash = "sha256:9bf4e51d56c9067c60668b78210ae213894f060f85593dc2ad8805eb7d875a2a"},
{file = "apispec-3.2.0.tar.gz", hash = "sha256:c1625ae910f699c9adb21928693a3245b9faab6843f1b1c94ab2d505549cd78a"}, {file = "apispec-3.3.0.tar.gz", hash = "sha256:419d0564b899e182c2af50483ea074db8cb05fee60838be58bb4542095d5c08d"},
] ]
appdirs = [ appdirs = [
{file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"}, {file = "appdirs-1.4.3-py2.py3-none-any.whl", hash = "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"},
@ -648,13 +732,43 @@ certifi = [
{file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"}, {file = "certifi-2019.11.28-py2.py3-none-any.whl", hash = "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3"},
{file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"}, {file = "certifi-2019.11.28.tar.gz", hash = "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"},
] ]
cffi = [
{file = "cffi-1.14.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384"},
{file = "cffi-1.14.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30"},
{file = "cffi-1.14.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c"},
{file = "cffi-1.14.0-cp27-cp27m-win32.whl", hash = "sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78"},
{file = "cffi-1.14.0-cp27-cp27m-win_amd64.whl", hash = "sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793"},
{file = "cffi-1.14.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e"},
{file = "cffi-1.14.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a"},
{file = "cffi-1.14.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff"},
{file = "cffi-1.14.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f"},
{file = "cffi-1.14.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa"},
{file = "cffi-1.14.0-cp35-cp35m-win32.whl", hash = "sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5"},
{file = "cffi-1.14.0-cp35-cp35m-win_amd64.whl", hash = "sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4"},
{file = "cffi-1.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d"},
{file = "cffi-1.14.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc"},
{file = "cffi-1.14.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac"},
{file = "cffi-1.14.0-cp36-cp36m-win32.whl", hash = "sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f"},
{file = "cffi-1.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b"},
{file = "cffi-1.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3"},
{file = "cffi-1.14.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66"},
{file = "cffi-1.14.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0"},
{file = "cffi-1.14.0-cp37-cp37m-win32.whl", hash = "sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f"},
{file = "cffi-1.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26"},
{file = "cffi-1.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd"},
{file = "cffi-1.14.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55"},
{file = "cffi-1.14.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2"},
{file = "cffi-1.14.0-cp38-cp38-win32.whl", hash = "sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8"},
{file = "cffi-1.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b"},
{file = "cffi-1.14.0.tar.gz", hash = "sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6"},
]
chardet = [ chardet = [
{file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
{file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
] ]
click = [ click = [
{file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"},
{file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"},
] ]
colorama = [ colorama = [
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
@ -672,9 +786,62 @@ flask-cors = [
{file = "Flask-Cors-3.0.8.tar.gz", hash = "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16"}, {file = "Flask-Cors-3.0.8.tar.gz", hash = "sha256:72170423eb4612f0847318afff8c247b38bd516b7737adfc10d1c2cdbb382d16"},
{file = "Flask_Cors-3.0.8-py2.py3-none-any.whl", hash = "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"}, {file = "Flask_Cors-3.0.8-py2.py3-none-any.whl", hash = "sha256:f4d97201660e6bbcff2d89d082b5b6d31abee04b1b3003ee073a6fd25ad1d69a"},
] ]
gevent = [
{file = "gevent-1.4.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b7d3a285978b27b469c0ff5fb5a72bcd69f4306dbbf22d7997d83209a8ba917"},
{file = "gevent-1.4.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:44089ed06a962a3a70e96353c981d628b2d4a2f2a75ea5d90f916a62d22af2e8"},
{file = "gevent-1.4.0-cp27-cp27m-win32.whl", hash = "sha256:0e1e5b73a445fe82d40907322e1e0eec6a6745ca3cea19291c6f9f50117bb7ea"},
{file = "gevent-1.4.0-cp27-cp27m-win_amd64.whl", hash = "sha256:74b7528f901f39c39cdbb50cdf08f1a2351725d9aebaef212a29abfbb06895ee"},
{file = "gevent-1.4.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0ff2b70e8e338cf13bedf146b8c29d475e2a544b5d1fe14045aee827c073842c"},
{file = "gevent-1.4.0-cp34-cp34m-macosx_10_14_x86_64.whl", hash = "sha256:0774babec518a24d9a7231d4e689931f31b332c4517a771e532002614e270a64"},
{file = "gevent-1.4.0-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:d752bcf1b98174780e2317ada12013d612f05116456133a6acf3e17d43b71f05"},
{file = "gevent-1.4.0-cp34-cp34m-win32.whl", hash = "sha256:3249011d13d0c63bea72d91cec23a9cf18c25f91d1f115121e5c9113d753fa12"},
{file = "gevent-1.4.0-cp34-cp34m-win_amd64.whl", hash = "sha256:d1e6d1f156e999edab069d79d890859806b555ce4e4da5b6418616322f0a3df1"},
{file = "gevent-1.4.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7d0809e2991c9784eceeadef01c27ee6a33ca09ebba6154317a257353e3af922"},
{file = "gevent-1.4.0-cp35-cp35m-win32.whl", hash = "sha256:14b4d06d19d39a440e72253f77067d27209c67e7611e352f79fe69e0f618f76e"},
{file = "gevent-1.4.0-cp35-cp35m-win_amd64.whl", hash = "sha256:53b72385857e04e7faca13c613c07cab411480822ac658d97fd8a4ddbaf715c8"},
{file = "gevent-1.4.0-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:8d9ec51cc06580f8c21b41fd3f2b3465197ba5b23c00eb7d422b7ae0380510b0"},
{file = "gevent-1.4.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:2711e69788ddb34c059a30186e05c55a6b611cb9e34ac343e69cf3264d42fe1c"},
{file = "gevent-1.4.0-cp36-cp36m-win32.whl", hash = "sha256:e5bcc4270671936349249d26140c267397b7b4b1381f5ec8b13c53c5b53ab6e1"},
{file = "gevent-1.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:9f7a1e96fec45f70ad364e46de32ccacab4d80de238bd3c2edd036867ccd48ad"},
{file = "gevent-1.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:50024a1ee2cf04645535c5ebaeaa0a60c5ef32e262da981f4be0546b26791950"},
{file = "gevent-1.4.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:4bfa291e3c931ff3c99a349d8857605dca029de61d74c6bb82bd46373959c942"},
{file = "gevent-1.4.0-cp37-cp37m-win32.whl", hash = "sha256:ab4dc33ef0e26dc627559786a4fba0c2227f125db85d970abbf85b77506b3f51"},
{file = "gevent-1.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:896b2b80931d6b13b5d9feba3d4eebc67d5e6ec54f0cf3339d08487d55d93b0e"},
{file = "gevent-1.4.0-pp260-pypy_41-macosx_10_14_x86_64.whl", hash = "sha256:107f4232db2172f7e8429ed7779c10f2ed16616d75ffbe77e0e0c3fcdeb51a51"},
{file = "gevent-1.4.0-pp260-pypy_41-win32.whl", hash = "sha256:28a0c5417b464562ab9842dd1fb0cc1524e60494641d973206ec24d6ec5f6909"},
{file = "gevent-1.4.0.tar.gz", hash = "sha256:1eb7fa3b9bd9174dfe9c3b59b7a09b768ecd496debfc4976a9530a3e15c990d1"},
]
gevent-websocket = [
{file = "gevent-websocket-0.10.1.tar.gz", hash = "sha256:7eaef32968290c9121f7c35b973e2cc302ffb076d018c9068d2f5ca8b2d85fb0"},
{file = "gevent_websocket-0.10.1-py3-none-any.whl", hash = "sha256:17b67d91282f8f4c973eba0551183fc84f56f1c90c8f6b6b30256f31f66f5242"},
]
greenlet = [
{file = "greenlet-0.4.15-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163"},
{file = "greenlet-0.4.15-cp27-cp27m-win32.whl", hash = "sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87"},
{file = "greenlet-0.4.15-cp27-cp27m-win_amd64.whl", hash = "sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638"},
{file = "greenlet-0.4.15-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248"},
{file = "greenlet-0.4.15-cp33-cp33m-win32.whl", hash = "sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720"},
{file = "greenlet-0.4.15-cp33-cp33m-win_amd64.whl", hash = "sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28"},
{file = "greenlet-0.4.15-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939"},
{file = "greenlet-0.4.15-cp34-cp34m-win32.whl", hash = "sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625"},
{file = "greenlet-0.4.15-cp34-cp34m-win_amd64.whl", hash = "sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490"},
{file = "greenlet-0.4.15-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0"},
{file = "greenlet-0.4.15-cp35-cp35m-win32.whl", hash = "sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4"},
{file = "greenlet-0.4.15-cp35-cp35m-win_amd64.whl", hash = "sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6"},
{file = "greenlet-0.4.15-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8"},
{file = "greenlet-0.4.15-cp36-cp36m-win32.whl", hash = "sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0"},
{file = "greenlet-0.4.15-cp36-cp36m-win_amd64.whl", hash = "sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656"},
{file = "greenlet-0.4.15-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214"},
{file = "greenlet-0.4.15-cp37-cp37m-win32.whl", hash = "sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043"},
{file = "greenlet-0.4.15-cp37-cp37m-win_amd64.whl", hash = "sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304"},
{file = "greenlet-0.4.15-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e538b8dae561080b542b0f5af64d47ef859f22517f7eca617bb314e0e03fd7ef"},
{file = "greenlet-0.4.15-cp38-cp38-win32.whl", hash = "sha256:51155342eb4d6058a0ffcd98a798fe6ba21195517da97e15fca3db12ab201e6e"},
{file = "greenlet-0.4.15-cp38-cp38-win_amd64.whl", hash = "sha256:7457d685158522df483196b16ec648b28f8e847861adb01a55d41134e7734122"},
{file = "greenlet-0.4.15.tar.gz", hash = "sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc"},
]
idna = [ idna = [
{file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"}, {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
{file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"}, {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
] ]
imagesize = [ imagesize = [
{file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"}, {file = "imagesize-1.2.0-py2.py3-none-any.whl", hash = "sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1"},
@ -689,10 +856,13 @@ itsdangerous = [
{file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"},
] ]
jinja2 = [ jinja2 = [
{file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"}, {file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"},
{file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"}, {file = "Jinja2-2.11.1.tar.gz", hash = "sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250"},
]
labthings = [
{file = "labthings-0.2.0-py3-none-any.whl", hash = "sha256:e2235c0db45c7134403bd78b2aa55ae5aa1bfee677735c9674f52bfcb605e998"},
{file = "labthings-0.2.0.tar.gz", hash = "sha256:f77391bbb02676ac7c28de59c425c4dc8d6de63031f2a2db8bffd26bc691cb5e"},
] ]
labthings = []
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"},
@ -747,39 +917,39 @@ markupsafe = [
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
] ]
marshmallow = [ marshmallow = [
{file = "marshmallow-3.3.0-py2.py3-none-any.whl", hash = "sha256:3e53dd9e9358977a3929e45cdbe4a671f9eff53a7d6a23f33ed3eab8c1890d8f"}, {file = "marshmallow-3.5.1-py2.py3-none-any.whl", hash = "sha256:ac2e13b30165501b7d41fc0371b8df35944f5849769d136f20e2c5f6cdc6e665"},
{file = "marshmallow-3.3.0.tar.gz", hash = "sha256:0ba81b6da4ae69eb229b74b3c741ff13fe04fb899824377b1aff5aaa1a9fd46e"}, {file = "marshmallow-3.5.1.tar.gz", hash = "sha256:90854221bbb1498d003a0c3cc9d8390259137551917961c8b5258c64026b2f85"},
] ]
mccabe = [ mccabe = [
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
] ]
numpy = [ numpy = [
{file = "numpy-1.18.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:20b26aaa5b3da029942cdcce719b363dbe58696ad182aff0e5dcb1687ec946dc"}, {file = "numpy-1.18.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a1baa1dc8ecd88fb2d2a651671a84b9938461e8a8eed13e2f0a812a94084d1fa"},
{file = "numpy-1.18.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:70a840a26f4e61defa7bdf811d7498a284ced303dfbc35acb7be12a39b2aa121"}, {file = "numpy-1.18.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a244f7af80dacf21054386539699ce29bcc64796ed9850c99a34b41305630286"},
{file = "numpy-1.18.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:17aa7a81fe7599a10f2b7d95856dc5cf84a4eefa45bc96123cbbc3ebc568994e"}, {file = "numpy-1.18.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6fcc5a3990e269f86d388f165a089259893851437b904f422d301cdce4ff25c8"},
{file = "numpy-1.18.1-cp35-cp35m-win32.whl", hash = "sha256:f3d0a94ad151870978fb93538e95411c83899c9dc63e6fb65542f769568ecfa5"}, {file = "numpy-1.18.2-cp35-cp35m-win32.whl", hash = "sha256:b5ad0adb51b2dee7d0ee75a69e9871e2ddfb061c73ea8bc439376298141f77f5"},
{file = "numpy-1.18.1-cp35-cp35m-win_amd64.whl", hash = "sha256:1786a08236f2c92ae0e70423c45e1e62788ed33028f94ca99c4df03f5be6b3c6"}, {file = "numpy-1.18.2-cp35-cp35m-win_amd64.whl", hash = "sha256:87902e5c03355335fc5992a74ba0247a70d937f326d852fc613b7f53516c0963"},
{file = "numpy-1.18.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ae0975f42ab1f28364dcda3dde3cf6c1ddab3e1d4b2909da0cb0191fa9ca0480"}, {file = "numpy-1.18.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9ab21d1cb156a620d3999dd92f7d1c86824c622873841d6b080ca5495fa10fef"},
{file = "numpy-1.18.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:cf7eb6b1025d3e169989416b1adcd676624c2dbed9e3bcb7137f51bfc8cc2572"}, {file = "numpy-1.18.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:cdb3a70285e8220875e4d2bc394e49b4988bdb1298ffa4e0bd81b2f613be397c"},
{file = "numpy-1.18.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b765ed3930b92812aa698a455847141869ef755a87e099fddd4ccf9d81fffb57"}, {file = "numpy-1.18.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6d205249a0293e62bbb3898c4c2e1ff8a22f98375a34775a259a0523111a8f6c"},
{file = "numpy-1.18.1-cp36-cp36m-win32.whl", hash = "sha256:2d75908ab3ced4223ccba595b48e538afa5ecc37405923d1fea6906d7c3a50bc"}, {file = "numpy-1.18.2-cp36-cp36m-win32.whl", hash = "sha256:a35af656a7ba1d3decdd4fae5322b87277de8ac98b7d9da657d9e212ece76a61"},
{file = "numpy-1.18.1-cp36-cp36m-win_amd64.whl", hash = "sha256:9acdf933c1fd263c513a2df3dceecea6f3ff4419d80bf238510976bf9bcb26cd"}, {file = "numpy-1.18.2-cp36-cp36m-win_amd64.whl", hash = "sha256:1598a6de323508cfeed6b7cd6c4efb43324f4692e20d1f76e1feec7f59013448"},
{file = "numpy-1.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56bc8ded6fcd9adea90f65377438f9fea8c05fcf7c5ba766bef258d0da1554aa"}, {file = "numpy-1.18.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:deb529c40c3f1e38d53d5ae6cd077c21f1d49e13afc7936f7f868455e16b64a0"},
{file = "numpy-1.18.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e422c3152921cece8b6a2fb6b0b4d73b6579bd20ae075e7d15143e711f3ca2ca"}, {file = "numpy-1.18.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd77d58fb2acf57c1d1ee2835567cd70e6f1835e32090538f17f8a3a99e5e34b"},
{file = "numpy-1.18.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b3af02ecc999c8003e538e60c89a2b37646b39b688d4e44d7373e11c2debabec"}, {file = "numpy-1.18.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b1fe1a6f3a6f355f6c29789b5927f8bd4f134a4bd9a781099a7c4f66af8850f5"},
{file = "numpy-1.18.1-cp37-cp37m-win32.whl", hash = "sha256:d92350c22b150c1cae7ebb0ee8b5670cc84848f6359cf6b5d8f86617098a9b73"}, {file = "numpy-1.18.2-cp37-cp37m-win32.whl", hash = "sha256:2e40be731ad618cb4974d5ba60d373cdf4f1b8dcbf1dcf4d9dff5e212baf69c5"},
{file = "numpy-1.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:77c3bfe65d8560487052ad55c6998a04b654c2fbc36d546aef2b2e511e760971"}, {file = "numpy-1.18.2-cp37-cp37m-win_amd64.whl", hash = "sha256:4ba59db1fcc27ea31368af524dcf874d9277f21fd2e1f7f1e2e0c75ee61419ed"},
{file = "numpy-1.18.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c98c5ffd7d41611407a1103ae11c8b634ad6a43606eca3e2a5a269e5d6e8eb07"}, {file = "numpy-1.18.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:59ca9c6592da581a03d42cc4e270732552243dc45e87248aa8d636d53812f6a5"},
{file = "numpy-1.18.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9537eecf179f566fd1c160a2e912ca0b8e02d773af0a7a1120ad4f7507cd0d26"}, {file = "numpy-1.18.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1b0ece94018ae21163d1f651b527156e1f03943b986188dd81bc7e066eae9d1c"},
{file = "numpy-1.18.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e840f552a509e3380b0f0ec977e8124d0dc34dc0e68289ca28f4d7c1d0d79474"}, {file = "numpy-1.18.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:82847f2765835c8e5308f136bc34018d09b49037ec23ecc42b246424c767056b"},
{file = "numpy-1.18.1-cp38-cp38-win32.whl", hash = "sha256:590355aeade1a2eaba17617c19edccb7db8d78760175256e3cf94590a1a964f3"}, {file = "numpy-1.18.2-cp38-cp38-win32.whl", hash = "sha256:5e0feb76849ca3e83dd396254e47c7dba65b3fa9ed3df67c2556293ae3e16de3"},
{file = "numpy-1.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:39d2c685af15d3ce682c99ce5925cc66efc824652e10990d2462dfe9b8918c6a"}, {file = "numpy-1.18.2-cp38-cp38-win_amd64.whl", hash = "sha256:ba3c7a2814ec8a176bb71f91478293d633c08582119e713a0c5351c0f77698da"},
{file = "numpy-1.18.1.zip", hash = "sha256:b6ff59cee96b454516e47e7721098e6ceebef435e3e21ac2d6c3b8b02628eb77"}, {file = "numpy-1.18.2.zip", hash = "sha256:e7894793e6e8540dbeac77c87b489e331947813511108ae097f1715c018b8f3d"},
] ]
packaging = [ packaging = [
{file = "packaging-20.0-py2.py3-none-any.whl", hash = "sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb"}, {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"},
{file = "packaging-20.0.tar.gz", hash = "sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8"}, {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"},
] ]
picamera = [] picamera = []
pillow = [ pillow = [
@ -835,9 +1005,26 @@ pillow = [
{file = "Pillow-5.4.1.win32-py3.6.exe", hash = "sha256:4132c78200372045bb348fcad8d52518c8f5cfc077b1089949381ee4a61f1c6d"}, {file = "Pillow-5.4.1.win32-py3.6.exe", hash = "sha256:4132c78200372045bb348fcad8d52518c8f5cfc077b1089949381ee4a61f1c6d"},
{file = "Pillow-5.4.1.win32-py3.7.exe", hash = "sha256:75d1f20bd8072eff92c5f457c266a61619a02d03ece56544195c56d41a1a0522"}, {file = "Pillow-5.4.1.win32-py3.7.exe", hash = "sha256:75d1f20bd8072eff92c5f457c266a61619a02d03ece56544195c56d41a1a0522"},
] ]
psutil = [
{file = "psutil-5.7.0-cp27-none-win32.whl", hash = "sha256:298af2f14b635c3c7118fd9183843f4e73e681bb6f01e12284d4d70d48a60953"},
{file = "psutil-5.7.0-cp27-none-win_amd64.whl", hash = "sha256:75e22717d4dbc7ca529ec5063000b2b294fc9a367f9c9ede1f65846c7955fd38"},
{file = "psutil-5.7.0-cp35-cp35m-win32.whl", hash = "sha256:f344ca230dd8e8d5eee16827596f1c22ec0876127c28e800d7ae20ed44c4b310"},
{file = "psutil-5.7.0-cp35-cp35m-win_amd64.whl", hash = "sha256:e2d0c5b07c6fe5a87fa27b7855017edb0d52ee73b71e6ee368fae268605cc3f5"},
{file = "psutil-5.7.0-cp36-cp36m-win32.whl", hash = "sha256:a02f4ac50d4a23253b68233b07e7cdb567bd025b982d5cf0ee78296990c22d9e"},
{file = "psutil-5.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1413f4158eb50e110777c4f15d7c759521703bd6beb58926f1d562da40180058"},
{file = "psutil-5.7.0-cp37-cp37m-win32.whl", hash = "sha256:d008ddc00c6906ec80040d26dc2d3e3962109e40ad07fd8a12d0284ce5e0e4f8"},
{file = "psutil-5.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:73f35ab66c6c7a9ce82ba44b1e9b1050be2a80cd4dcc3352cc108656b115c74f"},
{file = "psutil-5.7.0-cp38-cp38-win32.whl", hash = "sha256:60b86f327c198561f101a92be1995f9ae0399736b6eced8f24af41ec64fb88d4"},
{file = "psutil-5.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:d84029b190c8a66a946e28b4d3934d2ca1528ec94764b180f7d6ea57b0e75e26"},
{file = "psutil-5.7.0.tar.gz", hash = "sha256:685ec16ca14d079455892f25bd124df26ff9137664af445563c1bd36629b5e0e"},
]
pycparser = [
{file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"},
{file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"},
]
pygments = [ pygments = [
{file = "Pygments-2.5.2-py2.py3-none-any.whl", hash = "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b"}, {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"},
{file = "Pygments-2.5.2.tar.gz", hash = "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"}, {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"},
] ]
pylint = [ pylint = [
{file = "pylint-2.4.4-py3-none-any.whl", hash = "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"}, {file = "pylint-2.4.4-py3-none-any.whl", hash = "sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4"},
@ -851,26 +1038,17 @@ pyserial = [
{file = "pyserial-3.4-py2.py3-none-any.whl", hash = "sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8"}, {file = "pyserial-3.4-py2.py3-none-any.whl", hash = "sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8"},
{file = "pyserial-3.4.tar.gz", hash = "sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627"}, {file = "pyserial-3.4.tar.gz", hash = "sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627"},
] ]
python-dateutil = [
{file = "python-dateutil-2.8.1.tar.gz", hash = "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c"},
{file = "python_dateutil-2.8.1-py2.py3-none-any.whl", hash = "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"},
]
pytz = [ pytz = [
{file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"}, {file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"},
{file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"}, {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"},
] ]
pyyaml = [
{file = "PyYAML-3.13-cp27-cp27m-win32.whl", hash = "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f"},
{file = "PyYAML-3.13-cp27-cp27m-win_amd64.whl", hash = "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537"},
{file = "PyYAML-3.13-cp34-cp34m-win32.whl", hash = "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3"},
{file = "PyYAML-3.13-cp34-cp34m-win_amd64.whl", hash = "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04"},
{file = "PyYAML-3.13-cp35-cp35m-win32.whl", hash = "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1"},
{file = "PyYAML-3.13-cp35-cp35m-win_amd64.whl", hash = "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613"},
{file = "PyYAML-3.13-cp36-cp36m-win32.whl", hash = "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a"},
{file = "PyYAML-3.13-cp36-cp36m-win_amd64.whl", hash = "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b"},
{file = "PyYAML-3.13-cp37-cp37m-win32.whl", hash = "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"},
{file = "PyYAML-3.13-cp37-cp37m-win_amd64.whl", hash = "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1"},
{file = "PyYAML-3.13.tar.gz", hash = "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf"},
]
requests = [ requests = [
{file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"}, {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
{file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"}, {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
] ]
rope = [ rope = [
{file = "rope-0.14.0-py2-none-any.whl", hash = "sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969"}, {file = "rope-0.14.0-py2-none-any.whl", hash = "sha256:6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969"},
@ -898,6 +1076,36 @@ scipy = [
{file = "scipy-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:10325f0ffac2400b1ec09537b7e403419dcd25d9fee602a44e8a32119af9079e"}, {file = "scipy-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:10325f0ffac2400b1ec09537b7e403419dcd25d9fee602a44e8a32119af9079e"},
{file = "scipy-1.3.0.tar.gz", hash = "sha256:c3bb4bd2aca82fb498247deeac12265921fe231502a6bc6edea3ee7fe6c40a7a"}, {file = "scipy-1.3.0.tar.gz", hash = "sha256:c3bb4bd2aca82fb498247deeac12265921fe231502a6bc6edea3ee7fe6c40a7a"},
] ]
simplejson = [
{file = "simplejson-3.17.0-cp27-cp27m-macosx_10_13_x86_64.whl", hash = "sha256:87d349517b572964350cc1adc5a31b493bbcee284505e81637d0174b2758ba17"},
{file = "simplejson-3.17.0-cp27-cp27m-win32.whl", hash = "sha256:1d1e929cdd15151f3c0b2efe953b3281b2fd5ad5f234f77aca725f28486466f6"},
{file = "simplejson-3.17.0-cp27-cp27m-win_amd64.whl", hash = "sha256:1ea59f570b9d4916ae5540a9181f9c978e16863383738b69a70363bc5e63c4cb"},
{file = "simplejson-3.17.0-cp33-cp33m-win32.whl", hash = "sha256:8027bd5f1e633eb61b8239994e6fc3aba0346e76294beac22a892eb8faa92ba1"},
{file = "simplejson-3.17.0-cp33-cp33m-win_amd64.whl", hash = "sha256:22a7acb81968a7c64eba7526af2cf566e7e2ded1cb5c83f0906b17ff1540f866"},
{file = "simplejson-3.17.0-cp34-cp34m-win32.whl", hash = "sha256:17163e643dbf125bb552de17c826b0161c68c970335d270e174363d19e7ea882"},
{file = "simplejson-3.17.0-cp34-cp34m-win_amd64.whl", hash = "sha256:0fe3994207485efb63d8f10a833ff31236ed27e3b23dadd0bf51c9900313f8f2"},
{file = "simplejson-3.17.0-cp35-cp35m-win32.whl", hash = "sha256:4cf91aab51b02b3327c9d51897960c554f00891f9b31abd8a2f50fd4a0071ce8"},
{file = "simplejson-3.17.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fc9051d249dd5512e541f20330a74592f7a65b2d62e18122ca89bf71f94db748"},
{file = "simplejson-3.17.0-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:86afc5b5cbd42d706efd33f280fec7bd7e2772ef54e3f34cf6b30777cd19a614"},
{file = "simplejson-3.17.0-cp36-cp36m-win32.whl", hash = "sha256:926bcbef9eb60e798eabda9cd0bbcb0fca70d2779aa0aa56845749d973eb7ad5"},
{file = "simplejson-3.17.0-cp36-cp36m-win_amd64.whl", hash = "sha256:daaf4d11db982791be74b23ff4729af2c7da79316de0bebf880fa2d60bcc8c5a"},
{file = "simplejson-3.17.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:9a126c3a91df5b1403e965ba63b304a50b53d8efc908a8c71545ed72535374a3"},
{file = "simplejson-3.17.0-cp37-cp37m-win32.whl", hash = "sha256:fc046afda0ed8f5295212068266c92991ab1f4a50c6a7144b69364bdee4a0159"},
{file = "simplejson-3.17.0-cp37-cp37m-win_amd64.whl", hash = "sha256:7cce4bac7e0d66f3a080b80212c2238e063211fe327f98d764c6acbc214497fc"},
{file = "simplejson-3.17.0.tar.gz", hash = "sha256:2b4b2b738b3b99819a17feaf118265d0753d5536049ea570b3c43b51c4701e81"},
{file = "simplejson-3.17.0.win-amd64-py2.7.exe", hash = "sha256:1d346c2c1d7dd79c118f0cc7ec5a1c4127e0c8ffc83e7b13fc5709ff78c9bb84"},
{file = "simplejson-3.17.0.win-amd64-py3.3.exe", hash = "sha256:5cfd495527f8b85ce21db806567de52d98f5078a8e9427b18e251c68bd573a26"},
{file = "simplejson-3.17.0.win-amd64-py3.4.exe", hash = "sha256:8de378d589eccbc75941e480b4d5b4db66f22e4232f87543b136b1f093fff342"},
{file = "simplejson-3.17.0.win-amd64-py3.5.exe", hash = "sha256:f4b64a1031acf33e281fd9052336d6dad4d35eee3404c95431c8c6bc7a9c0588"},
{file = "simplejson-3.17.0.win-amd64-py3.6.exe", hash = "sha256:ad8dd3454d0c65c0f92945ac86f7b9efb67fa2040ba1b0189540e984df904378"},
{file = "simplejson-3.17.0.win-amd64-py3.7.exe", hash = "sha256:229edb079d5dd81bf12da952d4d825bd68d1241381b37d3acf961b384c9934de"},
{file = "simplejson-3.17.0.win32-py2.7.exe", hash = "sha256:4fd5f79590694ebff8dc980708e1c182d41ce1fda599a12189f0ca96bf41ad70"},
{file = "simplejson-3.17.0.win32-py3.3.exe", hash = "sha256:d140e9376e7f73c1f9e0a8e3836caf5eec57bbafd99259d56979da05a6356388"},
{file = "simplejson-3.17.0.win32-py3.4.exe", hash = "sha256:da00675e5e483ead345429d4f1374ab8b949fba4429d60e71ee9d030ced64037"},
{file = "simplejson-3.17.0.win32-py3.5.exe", hash = "sha256:7739940d68b200877a15a5ff5149e1599737d6dd55e302625650629350466418"},
{file = "simplejson-3.17.0.win32-py3.6.exe", hash = "sha256:60aad424e47c5803276e332b2a861ed7a0d46560e8af53790c4c4fb3420c26c2"},
{file = "simplejson-3.17.0.win32-py3.7.exe", hash = "sha256:1fbba86098bbfc1f85c5b69dc9a6d009055104354e0d9880bb00b692e30e0078"},
]
six = [ six = [
{file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"}, {file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"},
{file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"}, {file = "six-1.14.0.tar.gz", hash = "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a"},
@ -907,20 +1115,20 @@ snowballstemmer = [
{file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"}, {file = "snowballstemmer-2.0.0.tar.gz", hash = "sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"},
] ]
sphinx = [ sphinx = [
{file = "Sphinx-2.3.1-py3-none-any.whl", hash = "sha256:298537cb3234578b2d954ff18c5608468229e116a9757af3b831c2b2b4819159"}, {file = "Sphinx-2.4.4-py3-none-any.whl", hash = "sha256:fc312670b56cb54920d6cc2ced455a22a547910de10b3142276495ced49231cb"},
{file = "Sphinx-2.3.1.tar.gz", hash = "sha256:e6e766b74f85f37a5f3e0773a1e1be8db3fcb799deb58ca6d18b70b0b44542a5"}, {file = "Sphinx-2.4.4.tar.gz", hash = "sha256:b4c750d546ab6d7e05bdff6ac24db8ae3e8b8253a3569b754e445110a0a12b66"},
] ]
sphinxcontrib-applehelp = [ sphinxcontrib-applehelp = [
{file = "sphinxcontrib-applehelp-1.0.1.tar.gz", hash = "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897"}, {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
{file = "sphinxcontrib_applehelp-1.0.1-py2.py3-none-any.whl", hash = "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"}, {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
] ]
sphinxcontrib-devhelp = [ sphinxcontrib-devhelp = [
{file = "sphinxcontrib-devhelp-1.0.1.tar.gz", hash = "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34"}, {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
{file = "sphinxcontrib_devhelp-1.0.1-py2.py3-none-any.whl", hash = "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"}, {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
] ]
sphinxcontrib-htmlhelp = [ sphinxcontrib-htmlhelp = [
{file = "sphinxcontrib-htmlhelp-1.0.2.tar.gz", hash = "sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422"}, {file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"},
{file = "sphinxcontrib_htmlhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"}, {file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"},
] ]
sphinxcontrib-httpdomain = [ sphinxcontrib-httpdomain = [
{file = "sphinxcontrib-httpdomain-1.7.0.tar.gz", hash = "sha256:ac40b4fba58c76b073b03931c7b8ead611066a6aebccafb34dc19694f4eb6335"}, {file = "sphinxcontrib-httpdomain-1.7.0.tar.gz", hash = "sha256:ac40b4fba58c76b073b03931c7b8ead611066a6aebccafb34dc19694f4eb6335"},
@ -931,12 +1139,12 @@ sphinxcontrib-jsmath = [
{file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"},
] ]
sphinxcontrib-qthelp = [ sphinxcontrib-qthelp = [
{file = "sphinxcontrib-qthelp-1.0.2.tar.gz", hash = "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"}, {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
{file = "sphinxcontrib_qthelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20"}, {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
] ]
sphinxcontrib-serializinghtml = [ sphinxcontrib-serializinghtml = [
{file = "sphinxcontrib-serializinghtml-1.1.3.tar.gz", hash = "sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227"}, {file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"},
{file = "sphinxcontrib_serializinghtml-1.1.3-py2.py3-none-any.whl", hash = "sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"}, {file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"},
] ]
toml = [ toml = [
{file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"}, {file = "toml-0.10.0-py2.7.egg", hash = "sha256:f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"},
@ -967,17 +1175,17 @@ typed-ast = [
{file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"},
] ]
urllib3 = [ urllib3 = [
{file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"}, {file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"},
{file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"}, {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"},
] ]
webargs = [ webargs = [
{file = "webargs-5.5.2-py2-none-any.whl", hash = "sha256:3f9dc15de183d356c9a0acc159c100ea0506c0c240c1e6f1d8b308c5fed4dbbd"}, {file = "webargs-5.5.3-py2-none-any.whl", hash = "sha256:fc81c9f9d391acfbce406a319217319fd8b2fd862f7fdb5319ad06944f36ed25"},
{file = "webargs-5.5.2-py3-none-any.whl", hash = "sha256:fa4ad3ad9b38bedd26c619264fdc50d7ae014b49186736bca851e5b5228f2a1b"}, {file = "webargs-5.5.3-py3-none-any.whl", hash = "sha256:4f04918864c7602886335d8099f9b8960ee698b6b914f022736ed50be6b71235"},
{file = "webargs-5.5.2.tar.gz", hash = "sha256:3beca296598067cec24a0b6f91c0afcc19b6e3c4d84ab026b931669628bb47b4"}, {file = "webargs-5.5.3.tar.gz", hash = "sha256:871642a2e0c62f21d5b78f357750ac7a87e6bc734c972f633aa5fb6204fbf29a"},
] ]
werkzeug = [ werkzeug = [
{file = "Werkzeug-0.16.0-py2.py3-none-any.whl", hash = "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"}, {file = "Werkzeug-1.0.0-py2.py3-none-any.whl", hash = "sha256:6dc65cf9091cf750012f56f2cad759fa9e879f511b5ff8685e456b4e3bf90d16"},
{file = "Werkzeug-0.16.0.tar.gz", hash = "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7"}, {file = "Werkzeug-1.0.0.tar.gz", hash = "sha256:169ba8a33788476292d04186ab33b01d6add475033dfc07215e6d219cc077096"},
] ]
wrapt = [ wrapt = [
{file = "wrapt-1.11.2.tar.gz", hash = "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"}, {file = "wrapt-1.11.2.tar.gz", hash = "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"},

View file

@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
[tool.poetry] [tool.poetry]
name = "openflexure_microscope" name = "openflexure_microscope"
version = "2.0.0-beta.2" version = "2.0.0"
description = "Python module, and Flask-based web API, to run the OpenFlexure Microscope." description = "Python module, and Flask-based web API, to run the OpenFlexure Microscope."
authors = [ authors = [
@ -25,16 +25,17 @@ license = "GPL-3.0"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.6" python = "^3.6"
Flask = "^1.0" Flask = "^1.0"
pyyaml = "^3.13"
numpy = "^1.17.0" numpy = "^1.17.0"
Pillow = "^5.4" Pillow = "^5.4"
scipy = "1.3.0" # Exact version until we can guarantee a wheel scipy = "1.3.0" # Exact version until we can guarantee a wheel
picamera = { git = "https://github.com/rwb27/picamera.git", branch = "lens-shading", optional = true } # Lens shading requires >1.14 or RWB fork, lens-shading branch. picamera = { git = "https://github.com/rwb27/picamera.git", branch = "master", optional = true } # Lens shading requires >1.14 or RWB fork, lens-shading branch.
"RPi.GPIO" = { version = "^0.6.5", optional = true } "RPi.GPIO" = { version = "^0.6.5", optional = true }
pyserial = "^3.4" # Used for sangaboard (basic_serial_instrument) until we move to sangaboard pip library pyserial = "^3.4" # Used for sangaboard (basic_serial_instrument) until we move to sangaboard pip library
labthings = { git = "https://github.com/labthings/python-labthings.git", branch = "master"} # TODO: Pin to a fixed version before 2.0.0 stable python-dateutil = "^2.8"
psutil = "^5.6.7" # Autostorage extension
labthings = "0.2.0"
[tool.poetry.extras] [tool.poetry.extras]
rpi = ["picamera", "RPi.GPIO"] rpi = ["picamera", "RPi.GPIO"]