Merge branch 'master' into 'rwb27-plugin-comments'
# Conflicts: # docs/source/extensions/structure.rst
This commit is contained in:
commit
7c59245753
65 changed files with 1829 additions and 844 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
openflexure_microscope/api/static/** filter=lfs diff=lfs merge=lfs -text
|
||||
|
|
@ -226,7 +226,7 @@ intersphinx_mapping = {
|
|||
"openflexure_stage": ("https://openflexure-stage.readthedocs.io/en/latest/", None),
|
||||
"picamera": ("https://picamera.readthedocs.io/en/release-1.13/", 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 ----------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -14,7 +14,7 @@ from labthings.server import fields
|
|||
class MicroscopeIdentifySchema(Schema):
|
||||
name = fields.String() # Microscopes name
|
||||
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)
|
||||
stage = fields.String() # Stage object (represented as a string)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ from labthings.server import fields
|
|||
class MicroscopeIdentifySchema(Schema):
|
||||
name = fields.String() # Microscopes name
|
||||
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)
|
||||
stage = fields.String() # Stage object (represented as a string)
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import io # Used in our capture action
|
|||
class MicroscopeIdentifySchema(Schema):
|
||||
name = fields.String() # Microscopes name
|
||||
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)
|
||||
stage = fields.String() # Stage object (represented as a string)
|
||||
|
||||
|
|
|
|||
53
docs/source/extensions/lifecycle_hooks.rst
Normal file
53
docs/source/extensions/lifecycle_hooks.rst
Normal 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
|
||||
|
|
@ -107,7 +107,7 @@ We start by creating a schema to describe how to serialise a :py:class:`openflex
|
|||
class MicroscopeIdentifySchema(Schema):
|
||||
name = fields.String() # Microscopes name
|
||||
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)
|
||||
stage = fields.String() # Stage object (represented as a string)
|
||||
|
||||
|
|
|
|||
|
|
@ -24,3 +24,13 @@ Once this extension is loaded, any other extensions will have access to your met
|
|||
# Call a function from your extension
|
||||
if my_found_extension:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -11,4 +11,5 @@ Developing API Extensions
|
|||
./extensions/properties.rst
|
||||
./extensions/actions.rst
|
||||
./extensions/tasks_locks.rst
|
||||
./extensions/ev_gui.rst
|
||||
./extensions/ev_gui.rst
|
||||
./extensions/lifecycle_hooks.rst
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
__all__ = ["Microscope", "config", "utilities"]
|
||||
__version__ = "0.1.0"
|
||||
|
||||
from .microscope import Microscope
|
||||
from . import config
|
||||
from . import utilities
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import time
|
||||
import atexit
|
||||
import logging
|
||||
import logging, logging.handlers
|
||||
import os
|
||||
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.paths import (
|
||||
OPENFLEXURE_ETC_PATH,
|
||||
OPENFLEXURE_VAR_PATH,
|
||||
OPENFLEXURE_EXTENSIONS_PATH,
|
||||
settings_file_path,
|
||||
logs_file_path,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
# Handle logging
|
||||
is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
|
||||
|
||||
DEFAULT_LOGFILE = settings_file_path("openflexure_microscope.log")
|
||||
DEFAULT_LOGFILE = logs_file_path("openflexure_microscope.log")
|
||||
|
||||
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(
|
||||
DEFAULT_LOGFILE, maxBytes=1_000_000, backupCount=7
|
||||
)
|
||||
error_formatter = logging.Formatter(
|
||||
"[%(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:
|
||||
handler.setFormatter(error_formatter)
|
||||
logger.addHandler(handler)
|
||||
error_handlers = [rotating_logfile, logging.StreamHandler()]
|
||||
|
||||
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
|
||||
logging.info(f"Running with data path {OPENFLEXURE_VAR_PATH}")
|
||||
logging.info(f"Running with server path {OPENFLEXURE_ETC_PATH}")
|
||||
|
||||
# Create flask app
|
||||
app, labthing = create_app(
|
||||
|
|
@ -69,6 +61,7 @@ app, labthing = create_app(
|
|||
title=f"OpenFlexure Microscope {api_microscope.name}",
|
||||
description="Test LabThing-based API for OpenFlexure Microscope",
|
||||
version=pkg_resources.get_distribution("openflexure_microscope").version,
|
||||
flask_kwargs={"static_url_path": ""},
|
||||
)
|
||||
|
||||
# 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.CaptureDownload, f"/captures/<id>/download/<filename>")
|
||||
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
|
||||
labthing.add_view(views.MjpegStream, f"/streams/mjpeg")
|
||||
|
|
@ -109,12 +108,18 @@ labthing.add_view(views.SnapshotStream, f"/streams/snapshot")
|
|||
|
||||
# Attach microscope action resources
|
||||
labthing.add_view(views.actions.ActionsView, "/actions")
|
||||
labthing.add_root_link(views.actions.ActionsView, "actions")
|
||||
for name, action in views.enabled_root_actions().items():
|
||||
view_class = action["view_class"]
|
||||
rule = action["rule"]
|
||||
labthing.add_view(view_class, f"/actions{rule}")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def openflexure_ev():
|
||||
return app.send_static_file("index.html")
|
||||
|
||||
|
||||
@app.route("/routes")
|
||||
@cross_origin()
|
||||
def routes():
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import traceback
|
|||
from .autofocus import autofocus_extension_v2
|
||||
from .scan import scan_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)
|
||||
try:
|
||||
|
|
@ -11,4 +12,4 @@ try:
|
|||
except Exception as e:
|
||||
logging.error(
|
||||
f"Exception loading builtin extension picamera_autocalibrate: \n{traceback.format_exc()}"
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
from labthings.server.find import find_component
|
||||
from labthings.server.extensions import BaseExtension
|
||||
from labthings.server.view import View
|
||||
from labthings.server.decorators import (
|
||||
ThingAction,
|
||||
ThingProperty,
|
||||
)
|
||||
from labthings.server.decorators import ThingAction, ThingProperty, marshal_task
|
||||
|
||||
from openflexure_microscope.devel import JsonResponse, request, jsonify, taskify, abort
|
||||
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):
|
||||
"""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)
|
||||
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(
|
||||
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.
|
||||
|
||||
|
|
@ -242,7 +239,7 @@ def fast_up_down_up_autofocus(
|
|||
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.
|
||||
|
||||
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
|
||||
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
|
||||
|
|
@ -308,6 +305,7 @@ class AutofocusAPI(View):
|
|||
Run a standard autofocus
|
||||
"""
|
||||
|
||||
@marshal_task
|
||||
def post(self):
|
||||
payload = JsonResponse(request)
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
|
@ -323,7 +321,7 @@ class AutofocusAPI(View):
|
|||
task = taskify(autofocus)(microscope, dz)
|
||||
|
||||
# return a handle on the autofocus task
|
||||
return jsonify(task.state), 201
|
||||
return task
|
||||
|
||||
else:
|
||||
abort(503, "No stage connected. Unable to autofocus.")
|
||||
|
|
@ -335,6 +333,7 @@ class FastAutofocusAPI(View):
|
|||
Run a fast autofocus
|
||||
"""
|
||||
|
||||
@marshal_task
|
||||
def post(self):
|
||||
payload = JsonResponse(request)
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
|
|
@ -344,24 +343,27 @@ class FastAutofocusAPI(View):
|
|||
|
||||
# Figure out the parameters to use
|
||||
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:
|
||||
backlash = 0
|
||||
|
||||
if microscope.has_real_stage():
|
||||
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 jsonify(task.state), 201
|
||||
return task
|
||||
|
||||
else:
|
||||
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_up_down_up_autofocus, "fast_up_down_up_autofocus")
|
||||
autofocus_extension_v2.add_method(autofocus, "autofocus")
|
||||
|
||||
autofocus_extension_v2.add_view(MeasureSharpnessAPI, "/measure_sharpness")
|
||||
|
|
|
|||
271
openflexure_microscope/api/default_extensions/autostorage.py
Normal file
271
openflexure_microscope/api/default_extensions/autostorage.py
Normal 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)
|
||||
)
|
||||
|
|
@ -1,10 +1,7 @@
|
|||
from labthings.server.view import View
|
||||
from labthings.server.find import find_component
|
||||
from labthings.server.extensions import BaseExtension
|
||||
from labthings.server.decorators import (
|
||||
marshal_task,
|
||||
ThingAction,
|
||||
)
|
||||
from labthings.server.decorators import marshal_task, ThingAction
|
||||
|
||||
from labthings.core.tasks import taskify
|
||||
|
||||
|
|
@ -24,8 +21,8 @@ def recalibrate(microscope):
|
|||
"""
|
||||
scamera = microscope.camera
|
||||
with scamera.lock:
|
||||
assert not scamera.status["record_active"], "Can't recalibrate while recording!"
|
||||
streaming = scamera.status["stream_active"]
|
||||
assert not scamera.record_active, "Can't recalibrate while recording!"
|
||||
streaming = scamera.stream_active
|
||||
if streaming:
|
||||
logging.info("Stopping stream before recalibration")
|
||||
scamera.stop_stream_recording(resolution=(640, 480))
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
import itertools
|
||||
import logging
|
||||
import uuid
|
||||
import datetime
|
||||
from typing import Tuple
|
||||
from functools import reduce
|
||||
|
||||
from openflexure_microscope.camera.base import generate_basename
|
||||
from labthings.server.find import (
|
||||
find_component,
|
||||
find_extension,
|
||||
)
|
||||
from labthings.server.find import find_component, find_extension
|
||||
from labthings.server.extensions import BaseExtension
|
||||
from labthings.server.decorators import (
|
||||
marshal_task,
|
||||
use_args,
|
||||
ThingAction,
|
||||
)
|
||||
from labthings.server.decorators import marshal_task, use_args, ThingAction
|
||||
from labthings.server import fields
|
||||
|
||||
from openflexure_microscope.devel import taskify, abort, update_task_progress
|
||||
|
|
@ -78,12 +72,12 @@ def progress():
|
|||
def capture(
|
||||
microscope,
|
||||
basename,
|
||||
scan_id,
|
||||
temporary: bool = False,
|
||||
use_video_port: bool = False,
|
||||
resize: Tuple[int, int] = None,
|
||||
bayer: bool = False,
|
||||
metadata: dict = {},
|
||||
annotations: dict = {},
|
||||
tags: list = [],
|
||||
):
|
||||
|
||||
|
|
@ -101,16 +95,14 @@ def capture(
|
|||
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
|
||||
output.put_metadata(microscope.metadata, system=True)
|
||||
output.put_metadata({"instrument": microscope.metadata})
|
||||
|
||||
# Insert custom metadata
|
||||
output.put_metadata(metadata)
|
||||
|
||||
# Insert custom metadata
|
||||
output.put_annotations(annotations)
|
||||
# Insert custom tags
|
||||
output.put_tags(tags)
|
||||
|
||||
|
|
@ -122,7 +114,7 @@ def tile(
|
|||
microscope,
|
||||
basename: str = None,
|
||||
temporary: bool = False,
|
||||
step_size: int = [2000, 1500, 100],
|
||||
stride_size: int = [2000, 1500, 100],
|
||||
grid: list = [3, 3, 5],
|
||||
style="raster",
|
||||
autofocus_dz: int = 50,
|
||||
|
|
@ -131,13 +123,13 @@ def tile(
|
|||
bayer: bool = False,
|
||||
fast_autofocus=False,
|
||||
metadata: dict = {},
|
||||
annotations: dict = {},
|
||||
tags: list = [],
|
||||
):
|
||||
global _images_to_be_captured
|
||||
global _images_captured_so_far
|
||||
|
||||
# Keep task progress
|
||||
# TODO: Make this line not nasty
|
||||
_images_to_be_captured = reduce((lambda x, y: x * y), grid)
|
||||
_images_captured_so_far = 0
|
||||
|
||||
|
|
@ -145,28 +137,22 @@ def tile(
|
|||
if not basename:
|
||||
basename = generate_basename()
|
||||
|
||||
# Generate a stack ID
|
||||
scan_id = uuid.uuid4()
|
||||
|
||||
# Store initial position
|
||||
initial_position = microscope.stage.position
|
||||
|
||||
# Add scan metadata
|
||||
if "time" not in metadata:
|
||||
metadata["time"] = generate_basename()
|
||||
|
||||
metadata.update(
|
||||
{
|
||||
"scan_id": scan_id,
|
||||
"basename": basename,
|
||||
"scan_parameters": {
|
||||
"step_size": step_size,
|
||||
"grid": grid,
|
||||
"style": style,
|
||||
"autofocus_dz": autofocus_dz,
|
||||
},
|
||||
# Add dataset metadata
|
||||
dataset_d = {
|
||||
"dataset": {
|
||||
"id": uuid.uuid4(),
|
||||
"type": "xyzScan",
|
||||
"name": basename,
|
||||
"acquisitionDate": datetime.datetime.now().isoformat(),
|
||||
"strideSize": stride_size,
|
||||
"grid": grid,
|
||||
"style": style,
|
||||
"autofocusDz": autofocus_dz,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# Check if autofocus is enabled
|
||||
autofocus_extension = find_extension("org.openflexure.autofocus")
|
||||
|
|
@ -180,25 +166,19 @@ def tile(
|
|||
else:
|
||||
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)
|
||||
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
|
||||
next_z = initial_position[2]
|
||||
if fast_autofocus: # If fast autofocus is enabled, make
|
||||
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
|
||||
initial_z = initial_position[2]
|
||||
next_z = initial_z # Save this value for use in raster scans
|
||||
|
||||
# Now step through each point in the x-y coordinate array
|
||||
for line in x_y_grid:
|
||||
# If rastering, rather than snake (or eventually spiral)
|
||||
# Return focus to initial position
|
||||
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")
|
||||
microscope.stage.move_abs(
|
||||
[line[0][0], line[0][1], next_z]
|
||||
|
|
@ -211,31 +191,29 @@ def tile(
|
|||
# Refocus
|
||||
if autofocus_enabled:
|
||||
if fast_autofocus:
|
||||
autofocus_extension.fast_autofocus(
|
||||
dz=autofocus_dz,
|
||||
target_z=-z_stack_dz / 2.0, # Finish below the focus
|
||||
initial_move_up=False, # We're already at the top of the scan
|
||||
# Run fast autofocus. Client should provide dz ~ 2000
|
||||
autofocus_extension.fast_up_down_up_autofocus(
|
||||
microscope, dz=autofocus_dz
|
||||
)
|
||||
# TODO: save the focus data for future reference? Use it for diagnostics?
|
||||
else:
|
||||
logging.debug("Running autofocus")
|
||||
# Run slow autofocus. Client should provide dz ~ 50
|
||||
autofocus_extension.autofocus(
|
||||
range(-3 * autofocus_dz, 4 * autofocus_dz, autofocus_dz)
|
||||
)
|
||||
logging.debug("Finished autofocus")
|
||||
time.sleep(1) # TODO: Remove
|
||||
time.sleep(1)
|
||||
|
||||
# If we're not doing a z-stack, just capture
|
||||
if grid[2] <= 1:
|
||||
capture(
|
||||
microscope,
|
||||
basename,
|
||||
scan_id,
|
||||
temporary=temporary,
|
||||
use_video_port=use_video_port,
|
||||
resize=resize,
|
||||
bayer=bayer,
|
||||
metadata=metadata,
|
||||
metadata=dataset_d,
|
||||
annotations=annotations,
|
||||
tags=tags,
|
||||
)
|
||||
# Update task progress
|
||||
|
|
@ -247,27 +225,17 @@ def tile(
|
|||
microscope=microscope,
|
||||
basename=basename,
|
||||
temporary=temporary,
|
||||
scan_id=scan_id,
|
||||
step_size=step_size[2],
|
||||
step_size=stride_size[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,
|
||||
resize=resize,
|
||||
bayer=bayer,
|
||||
metadata=metadata,
|
||||
metadata=dataset_d,
|
||||
annotations=annotations,
|
||||
tags=tags,
|
||||
)
|
||||
# Make sure we use our current best estimate of focus (i.e. the current position) next point
|
||||
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))
|
||||
microscope.stage.move_abs(initial_position)
|
||||
|
|
@ -277,52 +245,40 @@ def stack(
|
|||
microscope,
|
||||
basename: str = None,
|
||||
temporary: bool = False,
|
||||
scan_id: str = None,
|
||||
step_size: int = 100,
|
||||
steps: int = 5,
|
||||
center: bool = True,
|
||||
return_to_start: bool = True,
|
||||
use_video_port: bool = False,
|
||||
resize: Tuple[int, int] = None,
|
||||
bayer: bool = False,
|
||||
metadata: dict = {},
|
||||
annotations: dict = {},
|
||||
tags: list = [],
|
||||
):
|
||||
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
|
||||
initial_position = microscope.stage.position
|
||||
logging.debug(f"Starting z-stack from position {microscope.stage.position}")
|
||||
|
||||
with microscope.lock:
|
||||
# Move to center scan
|
||||
if center:
|
||||
logging.debug("Moving to starting position")
|
||||
microscope.stage.move_rel([0, 0, int((-step_size * steps) / 2)])
|
||||
logging.debug("Moving to z-stack starting position")
|
||||
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):
|
||||
time.sleep(0.1)
|
||||
logging.debug("Capturing...")
|
||||
logging.debug(f"Capturing from position {microscope.stage.position}")
|
||||
capture(
|
||||
microscope,
|
||||
basename,
|
||||
scan_id,
|
||||
temporary=temporary,
|
||||
use_video_port=use_video_port,
|
||||
resize=resize,
|
||||
bayer=bayer,
|
||||
metadata=metadata,
|
||||
annotations=annotations,
|
||||
tags=tags,
|
||||
)
|
||||
# Update task progress
|
||||
|
|
@ -344,16 +300,18 @@ def stack(
|
|||
class TileScanAPI(View):
|
||||
@use_args(
|
||||
{
|
||||
"filename": fields.String(),
|
||||
"filename": fields.String(missing=None, example=None),
|
||||
"temporary": fields.Boolean(missing=False),
|
||||
"step_size": fields.List(fields.Integer, missing=[2000, 1500, 100]),
|
||||
"grid": fields.List(fields.Integer, missing=[3, 3, 5]),
|
||||
"stride_size": fields.List(
|
||||
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"),
|
||||
"autofocus_dz": fields.Integer(missing=50),
|
||||
"fast_autofocus": fields.Boolean(missing=False),
|
||||
"use_video_port": 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=[]),
|
||||
"resize": fields.Dict(missing=None), # TODO: Validate keys
|
||||
}
|
||||
|
|
@ -380,7 +338,7 @@ class TileScanAPI(View):
|
|||
microscope,
|
||||
basename=args.get("filename"),
|
||||
temporary=args.get("temporary"),
|
||||
step_size=args.get("step_size"),
|
||||
stride_size=args.get("stride_size"),
|
||||
grid=args.get("grid"),
|
||||
style=args.get("style"),
|
||||
autofocus_dz=args.get("autofocus_dz"),
|
||||
|
|
@ -388,7 +346,7 @@ class TileScanAPI(View):
|
|||
resize=resize,
|
||||
bayer=args.get("bayer"),
|
||||
fast_autofocus=args.get("fast_autofocus"),
|
||||
metadata=args.get("metadata"),
|
||||
annotations=args.get("annotations"),
|
||||
tags=args.get("tags"),
|
||||
)
|
||||
|
||||
|
|
@ -396,6 +354,6 @@ class TileScanAPI(View):
|
|||
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")
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from openflexure_microscope.devel import (
|
|||
update_task_progress,
|
||||
)
|
||||
|
||||
from flask import send_file, abort
|
||||
from flask import send_file, abort, url_for
|
||||
|
||||
import uuid
|
||||
import os
|
||||
|
|
@ -16,13 +16,56 @@ import logging
|
|||
|
||||
from labthings.server.find import find_component
|
||||
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.utilities import description_from_view
|
||||
from labthings.server.decorators import (
|
||||
ThingAction,
|
||||
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:
|
||||
"""
|
||||
ZIP-builder manager
|
||||
|
|
@ -38,8 +81,7 @@ class ZipManager:
|
|||
|
||||
# Get array of captures from IDs
|
||||
capture_list = [
|
||||
microscope.camera.image_from_id(capture_id)
|
||||
for capture_id in capture_id_list
|
||||
microscope.camera.images.get(capture_id) for capture_id in capture_id_list
|
||||
]
|
||||
# Remove Nones from list (missing/invalid captures)
|
||||
capture_list = [capture for capture in capture_list if capture]
|
||||
|
|
@ -76,20 +118,23 @@ class ZipManager:
|
|||
# Update task progress
|
||||
update_task_progress(int((index / n_files) * 100))
|
||||
|
||||
session_id = uuid.uuid4()
|
||||
session_key = str(session_id)
|
||||
# self.session_zips[session_id] = fp
|
||||
self.session_zips[session_key] = {
|
||||
"id": session_id,
|
||||
"fp": fp,
|
||||
"data_size": data_size_megabytes,
|
||||
"zip_size": os.path.getsize(fp.name) * 1e-6,
|
||||
}
|
||||
session_id = str(uuid.uuid4())
|
||||
session_description = ZipObjectDescription(
|
||||
session_id, fp, data_size=data_size_megabytes
|
||||
)
|
||||
self.session_zips[session_id] = session_description
|
||||
|
||||
return self.session_zips[session_key]
|
||||
return self.session_zips[session_id]
|
||||
|
||||
def zip_from_id(self, session_id):
|
||||
return self.session_zips[session_id]["fp"]
|
||||
def marshaled_build_zip_from_capture_ids(self, *args, **kwargs):
|
||||
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
|
||||
|
|
@ -98,56 +143,68 @@ default_zip_manager = ZipManager()
|
|||
|
||||
@ThingAction
|
||||
class ZipBuilderAPIView(View):
|
||||
@marshal_task
|
||||
def post(self):
|
||||
|
||||
ids = list(JsonResponse(request).json)
|
||||
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 jsonify(task.state), 201
|
||||
return task
|
||||
|
||||
|
||||
@ThingProperty
|
||||
class ZipListAPIView(View):
|
||||
@marshal_with(ZipObjectSchema(many=True))
|
||||
def get(self):
|
||||
return jsonify(default_zip_manager.session_zips)
|
||||
return default_zip_manager.session_zips.values()
|
||||
|
||||
|
||||
class ZipGetterAPIView(View):
|
||||
"""
|
||||
Download or delete a particular capture collection ZIP file
|
||||
"""
|
||||
|
||||
def get(self, session_id):
|
||||
"""
|
||||
Download a particular capture collection ZIP file
|
||||
"""
|
||||
if not session_id in default_zip_manager.session_zips:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
logging.info(f"Session ID: {session_id}")
|
||||
|
||||
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",
|
||||
as_attachment=True,
|
||||
attachment_filename=f"{session_id}.zip",
|
||||
)
|
||||
|
||||
def delete(self, session_id):
|
||||
"""
|
||||
Close and delete a particular capture collection ZIP file
|
||||
"""
|
||||
if not session_id in default_zip_manager.session_zips:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
logging.info(f"Session ID: {session_id}")
|
||||
|
||||
fp = default_zip_manager.zip_from_id(session_id)
|
||||
logging.debug(fp.name)
|
||||
fp.close()
|
||||
os.unlink(fp.name)
|
||||
|
||||
assert not os.path.exists(fp.name)
|
||||
|
||||
# Close the file
|
||||
default_zip_manager.session_zips[session_id].close()
|
||||
# Delete the file reference
|
||||
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(ZipListAPIView, "/get")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
from labthings.server.view import View
|
||||
from labthings.server.extensions import BaseExtension
|
||||
from labthings.server.decorators import (
|
||||
ThingAction,
|
||||
ThingProperty,
|
||||
)
|
||||
from labthings.server.decorators import ThingAction, ThingProperty
|
||||
|
||||
from openflexure_microscope.api.utilities.gui import build_gui
|
||||
|
||||
|
|
@ -35,7 +32,7 @@ def dynamic_form():
|
|||
"name": "val_int",
|
||||
"label": "Number value",
|
||||
"minValue": 0,
|
||||
"value": val_int
|
||||
"value": val_int,
|
||||
},
|
||||
{
|
||||
"fieldType": "htmlBlock",
|
||||
|
|
@ -66,7 +63,7 @@ static_form = {
|
|||
"name": "val_int",
|
||||
"label": "Number value",
|
||||
"minValue": 0,
|
||||
"default": 1
|
||||
"default": 1,
|
||||
},
|
||||
{
|
||||
"fieldType": "htmlBlock",
|
||||
|
|
|
|||
|
|
@ -1,44 +1,12 @@
|
|||
from openflexure_microscope import Microscope
|
||||
from openflexure_microscope.camera.capture import build_captures_from_exif
|
||||
from openflexure_microscope.microscope import Microscope
|
||||
|
||||
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()
|
||||
|
||||
# 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
|
||||
logging.debug("Restoring captures...")
|
||||
default_microscope.camera.images = build_captures_from_exif(
|
||||
default_microscope.camera.paths["default"]
|
||||
)
|
||||
default_microscope.camera.rebuild_captures()
|
||||
|
||||
logging.debug("Microscope successfully attached!")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8605f70737729f74f48d3183b30b4a2f50706f4f750cd208aeb8c136253f7d01
|
||||
size 772
|
||||
3
openflexure_microscope/api/static/css/main.24f9722e.css
Normal file
3
openflexure_microscope/api/static/css/main.24f9722e.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e68506e72389393d35cc47e5184459fff9a23bedea8c9ee96491f66c88e71954
|
||||
size 1375875
|
||||
3
openflexure_microscope/api/static/favicon-16x16.png
Normal file
3
openflexure_microscope/api/static/favicon-16x16.png
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ee944cc13be0a52ce48a81a09c179dac70729bd7580a516ccc8ac10b4cfa3f98
|
||||
size 1310
|
||||
3
openflexure_microscope/api/static/favicon-32x32.png
Normal file
3
openflexure_microscope/api/static/favicon-32x32.png
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:710327f8b3d83c83019ed2c4b27de8caf8e4e923c9148f93c1c0a2662f683f26
|
||||
size 1659
|
||||
3
openflexure_microscope/api/static/favicon.ico
Normal file
3
openflexure_microscope/api/static/favicon.ico
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0405ef60ab42df1c2e8721bf1f500c8c7d9e1afae130307101b8f77925ffdac1
|
||||
size 1150
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c4a1baec300d09e03a8380b85918267ee80faae8e00c6c56b48e2e74b1d9b38d
|
||||
size 57620
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a87d66c91b2e7dc5530aef76c03bd6a3d25ea5826110bf4803b561b811cc8726
|
||||
size 44300
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b7f4a3ab562048f28dd1fa691601bc43363a61d0f876d16d8316c52e4f32d696
|
||||
size 128180
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8c998b4a9c0acbb9fe5dd572c206a5a33fdd5ca2b58db87fc3b893beac85068d
|
||||
size 143258
|
||||
3
openflexure_microscope/api/static/index.html
Normal file
3
openflexure_microscope/api/static/index.html
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:463bfdbd635ddff1d00df38b1149ffd3947b492d71cfb546549dffebd311b090
|
||||
size 843
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a536bfa88fd1bf69284804285c5cf1da33cf05f164f175eb4085f7d057ff4f17
|
||||
size 378133
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:996b19391845b7cf1810f1181d935aad524897763da4c36847c7715f762b63fa
|
||||
size 1597950
|
||||
3
openflexure_microscope/api/static/js/main.eaa8787b.js
Normal file
3
openflexure_microscope/api/static/js/main.eaa8787b.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0a77d7d2e5e4220883bc1491a627435cc80d0b2574d51972bcd6b12add7a4a94
|
||||
size 116850
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1404c938e8f60b1694dd48357d486d919f5b1e32c9f226ca26fc0ee399019542
|
||||
size 395811
|
||||
3
openflexure_microscope/api/static/titleicon.svg
Normal file
3
openflexure_microscope/api/static/titleicon.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:187e61d8f6171eebf611502412556c707fab1d837f8763b2b60ce3915303f0ad
|
||||
size 1734
|
||||
|
|
@ -3,6 +3,12 @@ import logging
|
|||
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):
|
||||
# Make a working copy of 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
|
||||
if "forms" in gui_description and isinstance(api_gui["forms"], list):
|
||||
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():
|
||||
form["route"] = extension_object._rules[form["route"]]["rule"]
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from .actions import enabled_root_actions
|
||||
from .captures import *
|
||||
from .state import *
|
||||
from .instrument import *
|
||||
from .streams import *
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
"""
|
||||
Top-level representation of enabled actions
|
||||
"""
|
||||
|
||||
from flask import url_for
|
||||
from . import camera, stage, system
|
||||
|
||||
from labthings.server.view import View
|
||||
|
|
@ -74,9 +74,9 @@ class ActionsView(View):
|
|||
d = {
|
||||
"links": {
|
||||
"self": {
|
||||
"href": current_labthing().url_for(action["view_class"]),
|
||||
"href": url_for(action["view_class"].endpoint, _external=True),
|
||||
"mimetype": "application/json",
|
||||
**description_from_view(action["view_class"])
|
||||
**description_from_view(action["view_class"]),
|
||||
}
|
||||
},
|
||||
"rule": action["rule"],
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class CaptureAPI(View):
|
|||
"bayer": fields.Boolean(
|
||||
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"]),
|
||||
"resize": fields.Dict(
|
||||
missing=None, example={"width": 640, "height": 480}
|
||||
|
|
@ -75,11 +75,10 @@ class CaptureAPI(View):
|
|||
)
|
||||
|
||||
# Inject system metadata
|
||||
output.put_metadata(microscope.metadata, system=True)
|
||||
output.put_metadata({"instrument": microscope.metadata})
|
||||
|
||||
# Insert custom metadata
|
||||
output.put_metadata(args.get("metadata"))
|
||||
|
||||
output.put_annotations(args.get("annotations"))
|
||||
# Insert custom tags
|
||||
output.put_tags(args.get("tags"))
|
||||
|
||||
|
|
@ -132,10 +131,7 @@ class RAMCaptureAPI(View):
|
|||
|
||||
stream.seek(0)
|
||||
|
||||
return send_file(
|
||||
io.BytesIO(stream.getbuffer()),
|
||||
mimetype="image/jpeg"
|
||||
)
|
||||
return send_file(io.BytesIO(stream.getbuffer()), mimetype="image/jpeg")
|
||||
|
||||
|
||||
@ThingAction
|
||||
|
|
@ -167,8 +163,8 @@ class GPUPreviewStartAPI(View):
|
|||
|
||||
microscope.camera.start_preview(fullscreen=fullscreen, window=window)
|
||||
|
||||
# TODO: Make schema for microscope status
|
||||
return jsonify(microscope.status)
|
||||
# TODO: Make schema for microscope state
|
||||
return jsonify(microscope.state)
|
||||
|
||||
|
||||
@ThingAction
|
||||
|
|
@ -179,5 +175,5 @@ class GPUPreviewStopAPI(View):
|
|||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
microscope.camera.stop_preview()
|
||||
# TODO: Make schema for microscope status
|
||||
return jsonify(microscope.status)
|
||||
# TODO: Make schema for microscope state
|
||||
return jsonify(microscope.state)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
from openflexure_microscope.api.utilities import JsonResponse
|
||||
from labthings.server.view import View
|
||||
from labthings.server.find import find_component
|
||||
from labthings.server.decorators import (
|
||||
use_args,
|
||||
marshal_with,
|
||||
doc,
|
||||
ThingAction,
|
||||
)
|
||||
from labthings.server.decorators import use_args, marshal_with, doc, ThingAction
|
||||
from labthings.server import fields
|
||||
|
||||
from openflexure_microscope.utilities import axes_to_array, filter_dict
|
||||
|
|
@ -57,8 +52,8 @@ class MoveStageAPI(View):
|
|||
else:
|
||||
logging.warning("Unable to move. No stage found.")
|
||||
|
||||
# TODO: Make schema for microscope status
|
||||
return jsonify(microscope.status["stage"]["position"])
|
||||
# TODO: Make schema for microscope state
|
||||
return jsonify(microscope.state["stage"]["position"])
|
||||
|
||||
|
||||
@ThingAction
|
||||
|
|
@ -71,5 +66,5 @@ class ZeroStageAPI(View):
|
|||
microscope = find_component("org.openflexure.microscope")
|
||||
microscope.stage.zero_position()
|
||||
|
||||
# TODO: Make schema for microscope status
|
||||
return jsonify(microscope.status["stage"])
|
||||
# TODO: Make schema for microscope state
|
||||
return jsonify(microscope.state["stage"])
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@ import subprocess
|
|||
import os
|
||||
from sys import platform
|
||||
|
||||
from labthings.server.decorators import (
|
||||
ThingAction,
|
||||
doc_response,
|
||||
)
|
||||
from labthings.server.decorators import ThingAction, doc_response
|
||||
|
||||
|
||||
def is_raspberrypi(raise_on_errors=False):
|
||||
|
|
@ -28,9 +25,14 @@ class ShutdownAPI(View):
|
|||
"""
|
||||
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
|
||||
|
|
@ -44,6 +46,11 @@ class RebootAPI(View):
|
|||
"""
|
||||
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
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ from openflexure_microscope.api.utilities import get_bool, JsonResponse
|
|||
from labthings.server.schema import Schema
|
||||
from labthings.server import fields
|
||||
from labthings.server.view import View
|
||||
from labthings.server.utilities import (
|
||||
description_from_view,
|
||||
)
|
||||
from labthings.server.utilities import description_from_view
|
||||
from labthings.server.decorators import marshal_with, doc_response, Tag, ThingProperty
|
||||
|
||||
from labthings.server.find import find_component
|
||||
|
|
@ -16,14 +14,38 @@ from labthings.server.find import find_component
|
|||
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):
|
||||
id = fields.String()
|
||||
file = fields.String(
|
||||
data_key="path", description="Path of file on microscope device"
|
||||
)
|
||||
exists = fields.Bool(data_key="available")
|
||||
filename = fields.String()
|
||||
metadata = fields.Dict()
|
||||
name = fields.String()
|
||||
metadata = fields.Nested(CaptureMetadataSchema())
|
||||
|
||||
links = fields.Dict()
|
||||
|
||||
|
|
@ -41,16 +63,18 @@ class CaptureSchema(Schema):
|
|||
"mimetype": "application/json",
|
||||
**description_from_view(CaptureTags),
|
||||
},
|
||||
"metadata": {
|
||||
"href": url_for(CaptureMetadata.endpoint, id=data.id, _external=True),
|
||||
"annotations": {
|
||||
"href": url_for(
|
||||
CaptureAnnotations.endpoint, id=data.id, _external=True
|
||||
),
|
||||
"mimetype": "application/json",
|
||||
**description_from_view(CaptureMetadata),
|
||||
**description_from_view(CaptureAnnotations),
|
||||
},
|
||||
"download": {
|
||||
"href": url_for(
|
||||
CaptureDownload.endpoint,
|
||||
id=data.id,
|
||||
filename=data.filename,
|
||||
filename=data.name,
|
||||
_external=True,
|
||||
),
|
||||
"mimetype": "image/jpeg",
|
||||
|
|
@ -64,6 +88,9 @@ capture_schema = CaptureSchema()
|
|||
capture_list_schema = CaptureSchema(many=True)
|
||||
|
||||
|
||||
from pprint import pprint
|
||||
|
||||
|
||||
@ThingProperty
|
||||
@Tag("captures")
|
||||
class CaptureList(View):
|
||||
|
|
@ -73,7 +100,7 @@ class CaptureList(View):
|
|||
List all image captures
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
image_list = microscope.camera.images
|
||||
image_list = microscope.camera.images.values()
|
||||
return image_list
|
||||
|
||||
|
||||
|
|
@ -85,7 +112,7 @@ class CaptureView(View):
|
|||
Description of a single image capture
|
||||
"""
|
||||
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:
|
||||
return abort(404) # 404 Not Found
|
||||
|
|
@ -97,12 +124,15 @@ class CaptureView(View):
|
|||
Delete a single image capture
|
||||
"""
|
||||
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:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
# Delete the capture file
|
||||
capture_obj.delete()
|
||||
# Delete from capture list
|
||||
del microscope.camera.images[id]
|
||||
|
||||
return "", 204
|
||||
|
||||
|
|
@ -115,7 +145,7 @@ class CaptureDownload(View):
|
|||
Image data for a single image capture
|
||||
"""
|
||||
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:
|
||||
return abort(404) # 404 Not Found
|
||||
|
|
@ -150,7 +180,7 @@ class CaptureTags(View):
|
|||
Get tags associated with a single image capture
|
||||
"""
|
||||
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:
|
||||
return abort(404) # 404 Not Found
|
||||
|
|
@ -162,7 +192,7 @@ class CaptureTags(View):
|
|||
Add tags to a single image capture
|
||||
"""
|
||||
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:
|
||||
return abort(404) # 404 Not Found
|
||||
|
|
@ -177,12 +207,12 @@ class CaptureTags(View):
|
|||
|
||||
return jsonify(capture_obj.tags)
|
||||
|
||||
def delete(self, capture_id):
|
||||
def delete(self, id):
|
||||
"""
|
||||
Delete tags from a single image capture
|
||||
"""
|
||||
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:
|
||||
return abort(404) # 404 Not Found
|
||||
|
|
@ -199,25 +229,25 @@ class CaptureTags(View):
|
|||
|
||||
|
||||
@Tag("captures")
|
||||
class CaptureMetadata(View):
|
||||
class CaptureAnnotations(View):
|
||||
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")
|
||||
capture_obj = microscope.camera.image_from_id(id)
|
||||
capture_obj = microscope.camera.images.get(id)
|
||||
|
||||
if not capture_obj:
|
||||
return abort(404) # 404 Not Found
|
||||
|
||||
return jsonify(capture_obj.metadata)
|
||||
return jsonify(capture_obj.annotations)
|
||||
|
||||
def put(self, id):
|
||||
"""
|
||||
Update metadata for a single image capture
|
||||
"""
|
||||
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:
|
||||
return abort(404) # 404 Not Found
|
||||
|
|
@ -228,7 +258,6 @@ class CaptureMetadata(View):
|
|||
if type(data_dict) != dict:
|
||||
return abort(400)
|
||||
|
||||
# TODO: Allow putting system metadata maybe?
|
||||
capture_obj.put_metadata(data_dict)
|
||||
capture_obj.put_annotations(data_dict)
|
||||
|
||||
return jsonify(capture_obj.metadata)
|
||||
return jsonify(capture_obj.annotations)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
from openflexure_microscope.api.utilities import JsonResponse
|
||||
|
||||
from labthings.core.utilities import (
|
||||
get_by_path,
|
||||
set_by_path,
|
||||
create_from_path,
|
||||
)
|
||||
from labthings.core.utilities import get_by_path, set_by_path, create_from_path
|
||||
|
||||
from labthings.server.find import find_component
|
||||
from labthings.server.view import View
|
||||
|
|
@ -34,7 +30,7 @@ class SettingsProperty(View):
|
|||
logging.debug("Updating settings from PUT request:")
|
||||
logging.debug(payload.json)
|
||||
|
||||
microscope.apply_settings(payload.json)
|
||||
microscope.update_settings(payload.json)
|
||||
microscope.save_settings()
|
||||
|
||||
return self.get()
|
||||
|
|
@ -69,24 +65,24 @@ class NestedSettingsProperty(View):
|
|||
dictionary = create_from_path(keys)
|
||||
set_by_path(dictionary, keys, payload.json)
|
||||
|
||||
microscope.apply_settings(dictionary)
|
||||
microscope.update_settings(dictionary)
|
||||
microscope.save_settings()
|
||||
|
||||
return self.get(route)
|
||||
|
||||
|
||||
@ThingProperty
|
||||
class StatusProperty(View):
|
||||
class StateProperty(View):
|
||||
def get(self):
|
||||
"""
|
||||
Show current read-only state of the microscope
|
||||
"""
|
||||
microscope = find_component("org.openflexure.microscope")
|
||||
return jsonify(microscope.status)
|
||||
return jsonify(microscope.state)
|
||||
|
||||
|
||||
@Tag("properties")
|
||||
class NestedStatusProperty(View):
|
||||
class NestedStateProperty(View):
|
||||
@doc_response(404, description="Status key cannot be found")
|
||||
def get(self, route):
|
||||
"""
|
||||
|
|
@ -96,7 +92,35 @@ class NestedStatusProperty(View):
|
|||
keys = route.split("/")
|
||||
|
||||
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:
|
||||
return abort(404)
|
||||
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
from openflexure_microscope.api.utilities import gen, JsonResponse
|
||||
|
||||
from labthings.core.utilities import (
|
||||
get_by_path,
|
||||
set_by_path,
|
||||
create_from_path,
|
||||
)
|
||||
from labthings.core.utilities import get_by_path, set_by_path, create_from_path
|
||||
|
||||
from labthings.server.find import find_component
|
||||
from labthings.server.view import View
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ import datetime
|
|||
import logging
|
||||
|
||||
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 labthings.core.lock import StrictLock
|
||||
|
||||
|
|
@ -91,20 +92,6 @@ class CameraEvent(object):
|
|||
class BaseCamera(metaclass=ABCMeta):
|
||||
"""
|
||||
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):
|
||||
|
|
@ -121,16 +108,33 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
self.stream_timeout = 20
|
||||
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}
|
||||
|
||||
# Capture data
|
||||
self.images = []
|
||||
self.videos = []
|
||||
self.images = OrderedDict()
|
||||
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
|
||||
def apply_settings(self, config: dict):
|
||||
def update_settings(self, config: dict):
|
||||
"""Update settings from a config dictionary"""
|
||||
with self.lock:
|
||||
# Apply valid config params to camera object
|
||||
|
|
@ -143,10 +147,6 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
"""Return the current settings as a dictionary"""
|
||||
return {"paths": self.paths}
|
||||
|
||||
def save_settings(self):
|
||||
"""(Optional) Save any settings to disk that need to be stored"""
|
||||
return
|
||||
|
||||
def __enter__(self):
|
||||
"""Create camera on context enter."""
|
||||
return self
|
||||
|
|
@ -159,7 +159,7 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
"""Close the BaseCamera and all attached StreamObjects."""
|
||||
logging.info("Closing {}".format(self))
|
||||
# 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:
|
||||
stream_object.close()
|
||||
# Empty temp directory
|
||||
|
|
@ -178,6 +178,9 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
shutil.rmtree(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
|
||||
|
||||
def start_worker(self, timeout: int = 5) -> bool:
|
||||
|
|
@ -187,7 +190,7 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
self.last_access = time.time()
|
||||
self.stop = False
|
||||
|
||||
if not self.status["stream_active"]:
|
||||
if not self.stream_active:
|
||||
# start background frame thread
|
||||
self.thread = threading.Thread(target=self._thread)
|
||||
self.thread.daemon = True
|
||||
|
|
@ -207,12 +210,12 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
logging.debug("Stopping worker thread")
|
||||
timeout_time = time.time() + timeout
|
||||
|
||||
if self.status["stream_active"]:
|
||||
if self.stream_active:
|
||||
self.stop = True
|
||||
self.thread.join() # Wait 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:
|
||||
logging.debug("Timeout waiting for worker thread close.")
|
||||
raise TimeoutError("Timeout waiting for worker thread close.")
|
||||
|
|
@ -242,20 +245,22 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
@property
|
||||
def image(self):
|
||||
"""Return the latest captured image."""
|
||||
return last_entry(self.images)
|
||||
return last_entry(self.images.values())
|
||||
|
||||
@property
|
||||
def video(self):
|
||||
"""Return the latest recorded video."""
|
||||
return last_entry(self.videos)
|
||||
return last_entry(self.videos.values())
|
||||
|
||||
def image_from_id(self, image_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):
|
||||
"""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
|
||||
|
||||
|
|
@ -280,7 +285,7 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
|
||||
# Generate file name
|
||||
if not filename:
|
||||
filename = generate_numbered_basename(self.images)
|
||||
filename = generate_numbered_basename(self.images.values())
|
||||
logging.debug(filename)
|
||||
filename = "{}.{}".format(filename, fmt)
|
||||
|
||||
|
|
@ -298,7 +303,9 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
output.put_tags(["temporary"])
|
||||
|
||||
# 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
|
||||
|
||||
|
|
@ -320,10 +327,11 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
folder (str): Name of the folder in which to store the capture.
|
||||
fmt (str): Format of the capture.
|
||||
"""
|
||||
# TODO: Remove the redundancy here
|
||||
|
||||
# Generate file name
|
||||
if not filename:
|
||||
filename = generate_numbered_basename(self.videos)
|
||||
filename = generate_numbered_basename(self.videos.values())
|
||||
logging.debug(filename)
|
||||
filename = "{}.{}".format(filename, fmt)
|
||||
|
||||
|
|
@ -341,7 +349,9 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
output.put_tags(["temporary"])
|
||||
|
||||
# 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
|
||||
|
||||
|
|
@ -352,7 +362,7 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
self.frames_iterator = self.frames()
|
||||
logging.debug("Entering worker thread.")
|
||||
|
||||
self.status["stream_active"] = True
|
||||
self.stream_active = True
|
||||
|
||||
for frame in self.frames_iterator:
|
||||
self.frame = frame
|
||||
|
|
@ -365,9 +375,7 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
and ( # If using timeout
|
||||
time.time() - self.last_access > self.stream_timeout
|
||||
)
|
||||
and not self.status[ # And timeout time
|
||||
"preview_active"
|
||||
] # And GPU preview is not active
|
||||
and not self.preview_active # And GPU preview is not active
|
||||
):
|
||||
self.frames_iterator.close()
|
||||
break
|
||||
|
|
@ -383,4 +391,4 @@ class BaseCamera(metaclass=ABCMeta):
|
|||
|
||||
logging.debug("BaseCamera worker thread exiting...")
|
||||
# Set stream_activate state
|
||||
self.status["stream_active"] = False
|
||||
self.stream_active = False
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@ import os
|
|||
import shutil
|
||||
import glob
|
||||
import datetime
|
||||
import yaml
|
||||
import json
|
||||
import logging
|
||||
from PIL import Image
|
||||
import dateutil.parser
|
||||
import atexit
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from openflexure_microscope.camera import piexif
|
||||
from openflexure_microscope.camera.piexif._exceptions import InvalidImageDataError
|
||||
from openflexure_microscope.config import JSONEncoder
|
||||
|
|
@ -34,11 +36,9 @@ def pull_usercomment_dict(filepath):
|
|||
try:
|
||||
return json.loads(exif_dict["Exif"][37510].decode())
|
||||
except json.decoder.JSONDecodeError:
|
||||
# TODO: Remove YAML support in a later version
|
||||
logging.warning(
|
||||
f"Capture {filepath} has metadata stored in YAML format. This is now deprecated in favour of JSON."
|
||||
logging.error(
|
||||
f"Capture {filepath} has old, corrupt, or missing OpenFlexure metadata. Unable to reload to server."
|
||||
)
|
||||
return yaml.load(exif_dict["Exif"][37510].decode())
|
||||
else:
|
||||
return None
|
||||
|
||||
|
|
@ -60,14 +60,15 @@ def build_captures_from_exif(capture_path):
|
|||
|
||||
logging.debug("Reloading captures from {}...".format(capture_path))
|
||||
files = make_file_list(capture_path, EXIF_FORMATS)
|
||||
captures = []
|
||||
captures = OrderedDict()
|
||||
|
||||
for f in files:
|
||||
logging.debug("Reloading capture {}...".format(f))
|
||||
exif = pull_usercomment_dict(f)
|
||||
if exif:
|
||||
capture = capture_from_exif(f, exif)
|
||||
captures.append(capture)
|
||||
if capture:
|
||||
captures[capture.id] = capture
|
||||
else:
|
||||
logging.error("Invalid data at {}. Skipping.".format(f))
|
||||
|
||||
|
|
@ -93,18 +94,24 @@ def capture_from_exif(path, exif_dict):
|
|||
# Build file path information
|
||||
capture.split_file_path(capture.file)
|
||||
|
||||
# Populate capture parameters
|
||||
capture.id = exif_dict["id"]
|
||||
capture.timestring = exif_dict["time"]
|
||||
capture.format = exif_dict["format"]
|
||||
# Image metadata
|
||||
try:
|
||||
image_metadata = exif_dict.pop("image")
|
||||
except KeyError as e:
|
||||
logging.error(
|
||||
f"Unable to obtain valid 2.0 OpenFlexure metadata from file {path}"
|
||||
)
|
||||
return None
|
||||
|
||||
capture.custom_metadata = (
|
||||
exif_dict["custom"] if "custom" in exif_dict.keys() else {}
|
||||
)
|
||||
capture.system_metadata = (
|
||||
exif_dict["system"] if "system" in exif_dict.keys() else {}
|
||||
)
|
||||
capture.tags = exif_dict["tags"]
|
||||
# Populate capture parameters
|
||||
capture.id = image_metadata.get("id")
|
||||
capture.datetime = dateutil.parser.isoparse(image_metadata.get("acquisitionDate"))
|
||||
capture.format = image_metadata.get("format")
|
||||
capture.tags = image_metadata.get("tags")
|
||||
capture.annotations = image_metadata.get("annotations")
|
||||
|
||||
# Since we popped the "image" key, we dump whatever is left in _metadata
|
||||
capture._metadata = exif_dict
|
||||
|
||||
return capture
|
||||
|
||||
|
|
@ -113,16 +120,6 @@ class CaptureObject(object):
|
|||
"""
|
||||
StreamObject used to store and process on-disk capture data, and metadata.
|
||||
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:
|
||||
|
|
@ -131,17 +128,20 @@ class CaptureObject(object):
|
|||
# Store a nice ID
|
||||
self.id = uuid.uuid4() #: str: Unique capture 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
|
||||
self.file = filepath
|
||||
self.split_file_path(self.file)
|
||||
|
||||
# Dictionary for storing custom metadata
|
||||
self.custom_metadata = {}
|
||||
# Dictionary for adding top-level metadata (cannmot be accessed through web API)
|
||||
self.system_metadata = {}
|
||||
if not os.path.exists(self.filefolder):
|
||||
os.makedirs(self.filefolder)
|
||||
|
||||
# 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
|
||||
self.tags = []
|
||||
|
||||
|
|
@ -158,15 +158,11 @@ class CaptureObject(object):
|
|||
Args:
|
||||
filepath (str): String of the full file path, including file format extension
|
||||
"""
|
||||
# Split the full file path into a folder and a filename
|
||||
self.filefolder, self.filename = os.path.split(filepath)
|
||||
# Split the filename out from it's file extension
|
||||
self.basename = os.path.splitext(self.filename)[0]
|
||||
self.format = self.filename.split(".")[-1]
|
||||
|
||||
# Create folder and file
|
||||
if not os.path.exists(self.filefolder):
|
||||
os.makedirs(self.filefolder)
|
||||
# Split the full file path into a folder and a name
|
||||
self.filefolder, self.name = os.path.split(filepath)
|
||||
# Split the name out from it's file extension
|
||||
self.basename = os.path.splitext(self.name)[0]
|
||||
self.format = self.name.split(".")[-1]
|
||||
|
||||
@property
|
||||
def exists(self) -> bool:
|
||||
|
|
@ -204,17 +200,24 @@ class CaptureObject(object):
|
|||
|
||||
# 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:
|
||||
data (dict): Dictionary of metadata to be added
|
||||
"""
|
||||
if system:
|
||||
self.system_metadata.update(data)
|
||||
else:
|
||||
self.custom_metadata.update(data)
|
||||
self.annotations.update(data)
|
||||
self.save_metadata()
|
||||
|
||||
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()
|
||||
|
||||
def save_metadata(self) -> None:
|
||||
|
|
@ -244,12 +247,15 @@ class CaptureObject(object):
|
|||
and any added custom metadata and tags.
|
||||
"""
|
||||
d = {
|
||||
"id": self.id,
|
||||
"time": self.timestring,
|
||||
"format": self.format,
|
||||
"tags": self.tags,
|
||||
"custom": self.custom_metadata,
|
||||
"system": self.system_metadata,
|
||||
"image": {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"acquisitionDate": self.datetime.isoformat(),
|
||||
"format": self.format,
|
||||
"tags": self.tags,
|
||||
"annotations": self.annotations,
|
||||
},
|
||||
**self._metadata,
|
||||
}
|
||||
|
||||
# Add custom metadata to dictionary
|
||||
|
|
@ -262,7 +268,7 @@ class CaptureObject(object):
|
|||
"""
|
||||
|
||||
# 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
|
||||
if self.exists:
|
||||
|
|
|
|||
|
|
@ -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.setLevel(logging.INFO)
|
||||
|
||||
|
||||
# MAIN CLASS
|
||||
class MockStreamer(BaseCamera):
|
||||
class MissingCamera(BaseCamera):
|
||||
def __init__(self):
|
||||
# Run BaseCamera init
|
||||
BaseCamera.__init__(self)
|
||||
|
||||
# Store state of PiCameraStreamer
|
||||
self.status.update(
|
||||
{"stream_active": False, "record_active": False, "board": None}
|
||||
)
|
||||
|
||||
# Update config properties
|
||||
self.image_resolution = (1312, 976)
|
||||
self.stream_resolution = (640, 480)
|
||||
|
|
@ -71,6 +67,16 @@ class MockStreamer(BaseCamera):
|
|||
|
||||
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):
|
||||
"""Run any initialisation code when the frame iterator starts."""
|
||||
pass
|
||||
|
|
@ -101,7 +107,7 @@ class MockStreamer(BaseCamera):
|
|||
|
||||
return conf_dict
|
||||
|
||||
def apply_settings(self, config: dict):
|
||||
def update_settings(self, config: dict):
|
||||
"""
|
||||
Write a config dictionary to the PiCameraStreamer config.
|
||||
|
||||
|
|
@ -117,7 +123,7 @@ class MockStreamer(BaseCamera):
|
|||
with self.lock:
|
||||
|
||||
# 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
|
||||
if hasattr(self, key):
|
||||
|
|
@ -192,7 +198,7 @@ class MockStreamer(BaseCamera):
|
|||
if isinstance(output, CaptureObject):
|
||||
target = output.file
|
||||
else:
|
||||
target = target
|
||||
target = output
|
||||
|
||||
with self.lock:
|
||||
if isinstance(target, str):
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ from .base import BaseCamera, CaptureObject
|
|||
from .set_picamera_gain import set_analog_gain, set_digital_gain
|
||||
|
||||
from openflexure_microscope.paths import settings_file_path
|
||||
from openflexure_microscope.utilities import (
|
||||
serialise_array_b64,
|
||||
ndarray_to_json,
|
||||
json_to_ndarray,
|
||||
)
|
||||
|
||||
|
||||
# MAIN CLASS
|
||||
|
|
@ -84,15 +89,8 @@ class PiCameraStreamer(BaseCamera):
|
|||
picamera.PiCamera()
|
||||
) #: :py:class:`picamera.PiCamera`: Picamera object
|
||||
|
||||
# Store status of PiCameraStreamer
|
||||
self.status.update(
|
||||
{
|
||||
"stream_active": False,
|
||||
"record_active": False,
|
||||
"preview_active": False,
|
||||
"board": f"picamera_{self.camera.revision}",
|
||||
}
|
||||
)
|
||||
# Store state of PiCameraStreamer
|
||||
self.preview_active = False
|
||||
|
||||
# Reset variable states
|
||||
self.set_zoom(1.0)
|
||||
|
|
@ -116,15 +114,22 @@ class PiCameraStreamer(BaseCamera):
|
|||
"picamera_lst.npy"
|
||||
) #: str: Path of .npy lens shading table file
|
||||
|
||||
# Update board identifier
|
||||
self.status.update({})
|
||||
|
||||
# Create an empty stream
|
||||
self.stream = io.BytesIO()
|
||||
|
||||
# Start streaming
|
||||
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):
|
||||
"""Run any initialisation code when the frame iterator starts."""
|
||||
pass
|
||||
|
|
@ -153,10 +158,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
"image_resolution": self.image_resolution,
|
||||
"numpy_resolution": self.numpy_resolution,
|
||||
"jpeg_quality": self.jpeg_quality,
|
||||
"picamera_lst_path": self.picamera_lst_path
|
||||
if os.path.isfile(self.picamera_lst_path)
|
||||
else None,
|
||||
"picamera_settings": {},
|
||||
"picamera": {},
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -165,18 +167,22 @@ class PiCameraStreamer(BaseCamera):
|
|||
try:
|
||||
value = getattr(self.camera, key)
|
||||
logging.debug("Reading PiCamera().{}: {}".format(key, value))
|
||||
conf_dict["picamera_settings"][key] = value
|
||||
conf_dict["picamera"][key] = value
|
||||
except AttributeError:
|
||||
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
|
||||
|
||||
def save_settings(self):
|
||||
"""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):
|
||||
def update_settings(self, config: dict):
|
||||
"""
|
||||
Write a config dictionary to the PiCameraStreamer config.
|
||||
|
||||
|
|
@ -194,36 +200,37 @@ class PiCameraStreamer(BaseCamera):
|
|||
with self.lock:
|
||||
|
||||
# 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
|
||||
if self.status["stream_active"]: # If stream is active
|
||||
if self.stream_active: # If stream is active
|
||||
logging.info("Pausing stream to update config.")
|
||||
self.stop_stream_recording() # Pause stream
|
||||
paused_stream = True # Remember to unpause stream when done
|
||||
|
||||
# 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(
|
||||
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
|
||||
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)
|
||||
|
||||
# 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 paused_stream:
|
||||
logging.info("Resuming stream.")
|
||||
|
|
@ -292,53 +299,18 @@ class PiCameraStreamer(BaseCamera):
|
|||
if pause_for_effect:
|
||||
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:
|
||||
"""
|
||||
Change the camera zoom, handling re-centering and scaling.
|
||||
"""
|
||||
with self.lock:
|
||||
self.status["zoom_value"] = float(zoom_value)
|
||||
if self.status["zoom_value"] < 1:
|
||||
self.status["zoom_value"] = 1
|
||||
self.zoom_value = float(zoom_value)
|
||||
if self.zoom_value < 1:
|
||||
self.zoom_value = 1
|
||||
# Richard's code for zooming !
|
||||
fov = self.camera.zoom
|
||||
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
|
||||
# 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)
|
||||
|
|
@ -365,7 +337,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
self.camera.preview.window = window
|
||||
if fullscreen:
|
||||
self.camera.preview.fullscreen = fullscreen
|
||||
self.status["preview_active"] = True
|
||||
self.preview_active = True
|
||||
except picamera.exc.PiCameraMMALError as e:
|
||||
logging.error(
|
||||
"Suppressed a MMALError in start_preview. Exception: {}".format(e)
|
||||
|
|
@ -380,7 +352,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
def stop_preview(self):
|
||||
"""Stop the on board GPU camera 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):
|
||||
"""Start recording.
|
||||
|
|
@ -398,7 +370,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
"""
|
||||
with self.lock:
|
||||
# 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
|
||||
logging.info("Recording to {}".format(output))
|
||||
|
|
@ -411,8 +383,8 @@ class PiCameraStreamer(BaseCamera):
|
|||
quality=quality,
|
||||
)
|
||||
|
||||
# Update status dictionary
|
||||
self.status["record_active"] = True
|
||||
# Update state
|
||||
self.record_active = True
|
||||
|
||||
return output
|
||||
|
||||
|
|
@ -431,8 +403,8 @@ class PiCameraStreamer(BaseCamera):
|
|||
self.camera.stop_recording(splitter_port=2)
|
||||
logging.info("Recording stopped")
|
||||
|
||||
# Update status dictionary
|
||||
self.status["record_active"] = False
|
||||
# Update state
|
||||
self.record_active = False
|
||||
|
||||
def stop_stream_recording(
|
||||
self, splitter_port: int = 1, resolution: Tuple[int, int] = None
|
||||
|
|
@ -500,7 +472,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
self.camera.resolution = resolution
|
||||
|
||||
# If the stream should be active
|
||||
if self.status["stream_active"]:
|
||||
if self.stream_active:
|
||||
try:
|
||||
# Start recording on stream port
|
||||
self.camera.start_recording(
|
||||
|
|
@ -551,7 +523,7 @@ class PiCameraStreamer(BaseCamera):
|
|||
if isinstance(output, CaptureObject):
|
||||
target = output.file
|
||||
else:
|
||||
target = target
|
||||
target = output
|
||||
|
||||
with self.lock:
|
||||
logging.info("Capturing to {}".format(output))
|
||||
|
|
@ -680,7 +652,6 @@ class PiCameraStreamer(BaseCamera):
|
|||
# Start stream recording (and set resolution)
|
||||
self.start_stream_recording()
|
||||
|
||||
# Update status
|
||||
logging.debug("STREAM ACTIVE")
|
||||
|
||||
# While the iterator is not closed
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import json
|
||||
import flask
|
||||
import os
|
||||
import errno
|
||||
import logging
|
||||
|
|
@ -7,7 +8,12 @@ from uuid import UUID
|
|||
import numpy as np
|
||||
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:
|
||||
|
|
@ -19,21 +25,21 @@ class OpenflexureSettingsFile:
|
|||
expand (bool): Expand paths to valid auxillary config files.
|
||||
"""
|
||||
|
||||
def __init__(self, config_path: str = None):
|
||||
global DEFAULT_CONFIG, CONFIG_FILE_PATH
|
||||
def __init__(self, path: str, defaults: dict = {}):
|
||||
global DEFAULT_SETTINGS
|
||||
|
||||
# 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_file(self.config_path, populate=DEFAULT_CONFIG)
|
||||
initialise_file(self.path, populate=defaults)
|
||||
|
||||
def load(self) -> dict:
|
||||
"""
|
||||
Loads settings from a file on-disk.
|
||||
"""
|
||||
# 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")
|
||||
return loaded_config
|
||||
|
|
@ -50,11 +56,11 @@ class OpenflexureSettingsFile:
|
|||
save_settings = config
|
||||
|
||||
if backup:
|
||||
if os.path.isfile(self.config_path):
|
||||
shutil.copyfile(self.config_path, self.config_path + ".bk")
|
||||
if os.path.isfile(self.path):
|
||||
shutil.copyfile(self.path, self.path + ".bk")
|
||||
|
||||
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:
|
||||
"""
|
||||
|
|
@ -71,7 +77,7 @@ class OpenflexureSettingsFile:
|
|||
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
|
||||
"""
|
||||
|
|
@ -88,14 +94,13 @@ class JSONEncoder(json.JSONEncoder):
|
|||
# Numpy arrays
|
||||
elif isinstance(o, np.ndarray):
|
||||
return o.tolist()
|
||||
# UUIDs
|
||||
elif isinstance(o, UUID):
|
||||
return str(o)
|
||||
else:
|
||||
# call base class implementation which takes care of
|
||||
# raising exceptions for unsupported types
|
||||
try:
|
||||
return json.JSONEncoder.default(self, o)
|
||||
# if it's some mystery object, just return a string representation
|
||||
except TypeError:
|
||||
return str(o)
|
||||
return flask.json.JSONEncoder.default(self, o)
|
||||
|
||||
|
||||
# HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES
|
||||
|
|
@ -178,10 +183,22 @@ def initialise_file(config_path, populate: str = "{}\n"):
|
|||
outfile.write(populate)
|
||||
|
||||
|
||||
# Load the default config
|
||||
with open(DEFAULT_CONFIG_FILE_PATH, "r") as default_rc:
|
||||
DEFAULT_CONFIG = default_rc.read()
|
||||
|
||||
# Load the default settings
|
||||
with open(DEFAULT_SETTINGS_FILE_PATH, "r") as default_settings:
|
||||
DEFAULT_SETTINGS = default_settings.read()
|
||||
|
||||
#: 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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,15 +6,21 @@ import logging
|
|||
import pkg_resources
|
||||
import uuid
|
||||
|
||||
from openflexure_microscope.stage.base import BaseStage
|
||||
from openflexure_microscope.stage.mock import MockStage
|
||||
from openflexure_microscope.camera.base import BaseCamera
|
||||
from openflexure_microscope.camera.mock import MockStreamer
|
||||
from openflexure_microscope.stage.mock import MissingStage
|
||||
from openflexure_microscope.camera.mock import MissingCamera
|
||||
from openflexure_microscope.stage.sanga import SangaStage
|
||||
|
||||
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.config import user_settings
|
||||
from openflexure_microscope.config import user_settings, user_configuration
|
||||
|
||||
from labthings.core.lock import CompositeLock
|
||||
from labthings.core.utilities import rupdate
|
||||
|
||||
|
||||
class Microscope:
|
||||
|
|
@ -24,20 +30,26 @@ class Microscope:
|
|||
The camera and stage objects may already be initialised, and can be passed as arguments.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Initial attributes
|
||||
self.id = uuid.uuid4() #: Microscope UUID
|
||||
self.name = self.id #: Microscope name (modifiable)
|
||||
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
|
||||
def __init__(self, settings=user_settings, configuration=user_configuration):
|
||||
self.id = uuid.uuid4()
|
||||
self.name = self.id
|
||||
|
||||
self.fov = [0, 0] #: Microscope field-of-view in stage motor steps
|
||||
|
||||
# Store settings and configuration files
|
||||
self.settings_file = settings
|
||||
self.configuration_file = configuration
|
||||
|
||||
# Initialise with an empty composite lock
|
||||
#: :py:class:`labthings.lock.CompositeLock`: Composite lock for locking both camera and stage
|
||||
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
|
||||
self.apply_settings(user_settings.load())
|
||||
self.update_settings(self.settings_file.load())
|
||||
|
||||
def __enter__(self):
|
||||
"""Create microscope on context enter."""
|
||||
|
|
@ -56,58 +68,49 @@ class Microscope:
|
|||
self.stage.close()
|
||||
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.
|
||||
|
||||
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
|
||||
Attach microscope components based on initially passed configuration file
|
||||
"""
|
||||
|
||||
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...")
|
||||
self.camera = (
|
||||
camera
|
||||
) #: :py:class:`openflexure_microscope.camera.base.BaseCamera`: Picamera object
|
||||
### Stage
|
||||
if configuration.get("stage"):
|
||||
stage_type = configuration["stage"].get("type")
|
||||
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:
|
||||
logging.info("No camera attached.")
|
||||
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
|
||||
self.camera = MissingCamera()
|
||||
if not self.stage:
|
||||
logging.info("No stage attached.")
|
||||
else:
|
||||
logging.info("Attached stage {}".format(stage))
|
||||
self.stage = MissingStage()
|
||||
|
||||
if hasattr(self.stage, "lock"): # If stage object has a lock
|
||||
logging.info(
|
||||
"Attaching lock {} to composite lock.".format(self.stage.lock)
|
||||
)
|
||||
# Add the lock to the microscope composite lock
|
||||
self.lock.locks.append(self.stage.lock)
|
||||
|
||||
logging.info("Reapplying settings to newly attached devices")
|
||||
self.apply_settings(settings_full)
|
||||
### Locks
|
||||
if hasattr(self.camera, "lock"):
|
||||
self.lock.locks.append(self.camera.lock)
|
||||
if hasattr(self.stage, "lock"):
|
||||
self.lock.locks.append(self.stage.lock)
|
||||
|
||||
def has_real_stage(self) -> bool:
|
||||
"""
|
||||
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
|
||||
else:
|
||||
return False
|
||||
|
|
@ -116,49 +119,45 @@ class Microscope:
|
|||
"""
|
||||
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
|
||||
else:
|
||||
return False
|
||||
|
||||
# Create unified status
|
||||
# Create unified state
|
||||
@property
|
||||
def status(self):
|
||||
"""Dictionary of the basic microscope status.
|
||||
def state(self):
|
||||
"""Dictionary of the basic microscope state.
|
||||
|
||||
Return:
|
||||
dict: Dictionary containing complete microscope status
|
||||
dict: Dictionary containing complete microscope state
|
||||
"""
|
||||
state = {
|
||||
"camera": self.camera.status,
|
||||
"stage": self.stage.status,
|
||||
"version": pkg_resources.get_distribution("openflexure_microscope").version,
|
||||
}
|
||||
state = {"camera": self.camera.state, "stage": self.stage.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.
|
||||
"""
|
||||
logging.debug("Microscope: Applying config: {}".format(config))
|
||||
logging.debug("Microscope: Applying settings: {}".format(settings))
|
||||
|
||||
# If attached to a camera
|
||||
if ("camera_settings" in config) and self.camera:
|
||||
self.camera.apply_settings(config["camera_settings"])
|
||||
if ("camera" in settings) and self.camera:
|
||||
self.camera.update_settings(settings["camera"])
|
||||
|
||||
# If attached to a stage
|
||||
if ("stage_settings" in config) and self.stage:
|
||||
self.stage.apply_settings(config["stage_settings"])
|
||||
if ("stage" in settings) and self.stage:
|
||||
self.stage.update_settings(settings["stage"])
|
||||
|
||||
# Todo: tidy up with some loopy goodness
|
||||
if "id" in config:
|
||||
self.id = config["id"]
|
||||
if "name" in config:
|
||||
self.name = config["name"]
|
||||
if "fov" in config:
|
||||
self.fov = config["fov"]
|
||||
# Microscope settings
|
||||
if "id" in settings:
|
||||
self.id = settings["id"]
|
||||
if "name" in settings:
|
||||
self.name = settings["name"]
|
||||
if "fov" in settings:
|
||||
self.fov = settings["fov"]
|
||||
|
||||
def read_settings(self, json_safe=False):
|
||||
def read_settings(self, full: bool = True):
|
||||
"""
|
||||
Get an updated settings dictionary.
|
||||
|
||||
|
|
@ -174,16 +173,34 @@ class Microscope:
|
|||
# If attached to a camera
|
||||
if self.camera:
|
||||
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 self.stage:
|
||||
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):
|
||||
"""
|
||||
|
|
@ -192,11 +209,31 @@ class Microscope:
|
|||
# Read curent config
|
||||
current_config = self.read_settings()
|
||||
# Save config to file
|
||||
if self.camera:
|
||||
self.camera.save_settings()
|
||||
if self.stage:
|
||||
self.stage.save_settings()
|
||||
user_settings.save(current_config, backup=True)
|
||||
self.settings_file.save(current_config, backup=True)
|
||||
|
||||
@property
|
||||
def configuration(self):
|
||||
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
|
||||
def metadata(self):
|
||||
|
|
@ -204,23 +241,10 @@ class Microscope:
|
|||
Microscope system metadata, to be applied to basically all captures
|
||||
"""
|
||||
system_metadata = {
|
||||
"microscope_settings": self.read_settings(),
|
||||
"microscope_state": self.status,
|
||||
"microscope_id": self.id,
|
||||
"microscope_name": self.name,
|
||||
"id": self.id,
|
||||
"settings": self.read_settings(full=False),
|
||||
"state": self.state,
|
||||
"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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"camera": {
|
||||
"type": "PiCamera"
|
||||
},
|
||||
"stage": {
|
||||
"type": "SangaStage",
|
||||
"port": null
|
||||
}
|
||||
}
|
||||
|
|
@ -3,5 +3,5 @@
|
|||
4100,
|
||||
3146
|
||||
],
|
||||
"jpeg_quality": 75
|
||||
"jpeg_quality": 100
|
||||
}
|
||||
|
|
@ -10,12 +10,34 @@ def check_rw(path):
|
|||
|
||||
def settings_file_path(filename: str):
|
||||
"""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):
|
||||
"""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
|
||||
|
|
@ -23,10 +45,13 @@ def data_file_path(filename: str):
|
|||
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
#: 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"
|
||||
)
|
||||
|
||||
|
||||
# DATA BASE PATHS
|
||||
# BASE PATHS
|
||||
|
||||
if os.name == "nt":
|
||||
PREFERRED_VAR_PATH = os.getenv("PROGRAMDATA") or "C:\\ProgramData"
|
||||
|
|
@ -49,37 +74,11 @@ else:
|
|||
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
|
||||
|
||||
#: Path of microscope settings directory
|
||||
CONFIG_FILE_PATH = os.path.join(OPENFLEXURE_ETC_PATH, "microscope_settings.json")
|
||||
#: Path of microscope settings file
|
||||
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
|
||||
OPENFLEXURE_EXTENSIONS_PATH = os.path.join(
|
||||
OPENFLEXURE_ETC_PATH, "microscope_extensions"
|
||||
)
|
||||
|
||||
|
||||
# DATA PATHS
|
||||
OPENFLEXURE_EXTENSIONS_PATH = extensions_file_path("microscope_extensions")
|
||||
|
|
|
|||
0
openflexure_microscope/rescue/__init__.py
Normal file
0
openflexure_microscope/rescue/__init__.py
Normal file
77
openflexure_microscope/rescue/auto.py
Normal file
77
openflexure_microscope/rescue/auto.py
Normal 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)
|
||||
34
openflexure_microscope/rescue/check_capture_reload.py
Normal file
34
openflexure_microscope/rescue/check_capture_reload.py
Normal 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
|
||||
47
openflexure_microscope/rescue/check_settings.py
Normal file
47
openflexure_microscope/rescue/check_settings.py
Normal 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
|
||||
59
openflexure_microscope/rescue/monitor_service.py
Normal file
59
openflexure_microscope/rescue/monitor_service.py
Normal 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
|
||||
39
openflexure_microscope/rescue/monitor_timeout.py
Normal file
39
openflexure_microscope/rescue/monitor_timeout.py
Normal 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)
|
||||
|
|
@ -14,7 +14,7 @@ class BaseStage(metaclass=ABCMeta):
|
|||
self.lock = StrictLock(timeout=5)
|
||||
|
||||
@abstractmethod
|
||||
def apply_settings(self, config: dict):
|
||||
def update_settings(self, config: dict):
|
||||
"""Update settings from a config dictionary"""
|
||||
pass
|
||||
|
||||
|
|
@ -23,18 +23,16 @@ class BaseStage(metaclass=ABCMeta):
|
|||
"""Return the current settings as a dictionary"""
|
||||
pass
|
||||
|
||||
def save_settings(self):
|
||||
"""(Optional) Save any settings to disk that need to be stored"""
|
||||
return
|
||||
@property
|
||||
@abstractmethod
|
||||
def state(self):
|
||||
"""The general state dictionary of the board."""
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def status(self):
|
||||
"""The general state dictionary of the board.
|
||||
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,
|
||||
"""
|
||||
def configuration(self):
|
||||
"""The general stage configuration."""
|
||||
pass
|
||||
|
||||
@property
|
||||
|
|
@ -51,11 +49,7 @@ class BaseStage(metaclass=ABCMeta):
|
|||
|
||||
@property
|
||||
def position_map(self):
|
||||
return {
|
||||
"x": self.position[0],
|
||||
"y": self.position[1],
|
||||
"z": self.position[2],
|
||||
}
|
||||
return {"x": self.position[0], "y": self.position[1], "z": self.position[2]}
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import time
|
|||
import logging
|
||||
|
||||
|
||||
class MockStage(BaseStage):
|
||||
class MissingStage(BaseStage):
|
||||
def __init__(self, port=None, **kwargs):
|
||||
BaseStage.__init__(self)
|
||||
self._position = [0, 0, 0]
|
||||
|
|
@ -17,19 +17,17 @@ class MockStage(BaseStage):
|
|||
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""The general status dictionary of the board."""
|
||||
status = {
|
||||
"position": self.position_map,
|
||||
"board": None,
|
||||
"firmware": None,
|
||||
"version": None
|
||||
}
|
||||
return status
|
||||
def state(self):
|
||||
"""The general state dictionary of the board."""
|
||||
state = {"position": self.position_map}
|
||||
return state
|
||||
|
||||
def apply_settings(self, config: dict):
|
||||
@property
|
||||
def configuration(self):
|
||||
return {}
|
||||
|
||||
def update_settings(self, config: dict):
|
||||
"""Update settings from a config dictionary"""
|
||||
|
||||
# Set backlash. Expects a dictionary with axis labels
|
||||
if "backlash" in config:
|
||||
# Construct backlash array
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ class SangaStage(BaseStage):
|
|||
"""Class managing serial communications with the motors for an Openflexure stage"""
|
||||
BaseStage.__init__(self)
|
||||
|
||||
self.port = port
|
||||
self.board = Sangaboard(port, **kwargs)
|
||||
|
||||
self._backlash = (
|
||||
|
|
@ -31,14 +32,17 @@ class SangaStage(BaseStage):
|
|||
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""The general status dictionary of the board."""
|
||||
status = {
|
||||
"position": self.position_map,
|
||||
def state(self):
|
||||
"""The general state dictionary of the board."""
|
||||
return {"position": self.position_map}
|
||||
|
||||
@property
|
||||
def configuration(self):
|
||||
return {
|
||||
"port": self.port,
|
||||
"board": self.board.board,
|
||||
"firmware": self.board.firmware,
|
||||
}
|
||||
return status
|
||||
|
||||
@property
|
||||
def n_axes(self):
|
||||
|
|
@ -81,7 +85,7 @@ class SangaStage(BaseStage):
|
|||
else:
|
||||
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"""
|
||||
|
||||
# Set backlash. Expects a dictionary with axis labels
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import operator
|
|||
import base64
|
||||
from uuid import UUID
|
||||
import numpy as np
|
||||
import logging
|
||||
from collections import abc
|
||||
from functools import reduce
|
||||
from contextlib import contextmanager
|
||||
|
|
@ -21,6 +22,31 @@ def serialise_array_b64(npy_arr):
|
|||
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
|
||||
def set_properties(obj, **kwargs):
|
||||
"""A context manager to set, then reset, certain properties of an object.
|
||||
|
|
|
|||
518
poetry.lock
generated
518
poetry.lock
generated
|
|
@ -12,12 +12,12 @@ description = "A pluggable API specification generator. Currently supports the O
|
|||
name = "apispec"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "3.2.0"
|
||||
version = "3.3.0"
|
||||
|
||||
[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"]
|
||||
docs = ["marshmallow (>=2.19.2)", "pyyaml (5.2)", "sphinx (2.3.0)", "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)"]
|
||||
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.3)", "sphinx (2.4.1)", "sphinx-issues (1.2.0)", "sphinx-rtd-theme (0.4.3)"]
|
||||
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"]
|
||||
validation = ["prance (>=0.11)"]
|
||||
yaml = ["PyYAML (>=3.10)"]
|
||||
|
|
@ -97,6 +97,18 @@ optional = false
|
|||
python-versions = "*"
|
||||
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]]
|
||||
category = "dev"
|
||||
description = "Universal encoding detector for Python 2 and 3"
|
||||
|
|
@ -110,8 +122,8 @@ category = "main"
|
|||
description = "Composable command line interface toolkit"
|
||||
name = "click"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "7.0"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "7.1.1"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
|
@ -161,13 +173,51 @@ version = "3.0.8"
|
|||
Flask = ">=0.9"
|
||||
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]]
|
||||
category = "dev"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
name = "idna"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "2.8"
|
||||
version = "2.9"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
|
@ -204,8 +254,8 @@ category = "main"
|
|||
description = "A very fast and expressive template engine."
|
||||
name = "jinja2"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "2.10.3"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "2.11.1"
|
||||
|
||||
[package.dependencies]
|
||||
MarkupSafe = ">=0.23"
|
||||
|
|
@ -215,23 +265,21 @@ i18n = ["Babel (>=0.8)"]
|
|||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = ""
|
||||
description = "Python implementation of LabThings, based on the Flask microframework"
|
||||
name = "labthings"
|
||||
optional = false
|
||||
python-versions = "^3.6"
|
||||
version = "0.1.0"
|
||||
python-versions = ">=3.6,<4.0"
|
||||
version = "0.2.0"
|
||||
|
||||
[package.dependencies]
|
||||
Flask = "^1.1.1"
|
||||
apispec = "^3.2.0"
|
||||
flask-cors = "^3.0.8"
|
||||
marshmallow = "^3.3.0"
|
||||
webargs = "^5.5.2"
|
||||
Flask = ">=1.1.1,<2.0.0"
|
||||
apispec = ">=3.2.0,<4.0.0"
|
||||
flask-cors = ">=3.0.8,<4.0.0"
|
||||
gevent = ">=1.4.0,<2.0.0"
|
||||
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]]
|
||||
category = "dev"
|
||||
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"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "3.3.0"
|
||||
version = "3.5.1"
|
||||
|
||||
[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"]
|
||||
docs = ["sphinx (2.2.2)", "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)"]
|
||||
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.4.3)", "sphinx-issues (1.2.0)", "alabaster (0.7.12)", "sphinx-version-warning (1.1.2)"]
|
||||
lint = ["mypy (0.761)", "flake8 (3.7.9)", "flake8-bugbear (20.1.4)", "pre-commit (>=1.20,<3.0)"]
|
||||
tests = ["pytest", "pytz", "simplejson"]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -276,7 +324,7 @@ description = "NumPy is the fundamental package for array computing with Python.
|
|||
name = "numpy"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "1.18.1"
|
||||
version = "1.18.2"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
|
@ -284,7 +332,7 @@ description = "Core utilities for Python packages"
|
|||
name = "packaging"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "20.0"
|
||||
version = "20.3"
|
||||
|
||||
[package.dependencies]
|
||||
pyparsing = ">=2.0.2"
|
||||
|
|
@ -296,7 +344,7 @@ description = "A pure Python interface for the Raspberry Pi camera module."
|
|||
name = "picamera"
|
||||
optional = true
|
||||
python-versions = "*"
|
||||
version = "1.13.1b0"
|
||||
version = "1.13.2b0"
|
||||
|
||||
[package.extras]
|
||||
array = ["numpy"]
|
||||
|
|
@ -304,7 +352,7 @@ doc = ["sphinx"]
|
|||
test = ["coverage", "pytest", "mock", "pillow", "numpy"]
|
||||
|
||||
[package.source]
|
||||
reference = "b39c2b6e42f5f7f57bb46eafcb5c9e2bbdb5d0cb"
|
||||
reference = "79177fa7f28d6d5c6eab5ba814b420b1785b6b24"
|
||||
type = "git"
|
||||
url = "https://github.com/rwb27/picamera.git"
|
||||
[[package]]
|
||||
|
|
@ -315,13 +363,33 @@ optional = false
|
|||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
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]]
|
||||
category = "dev"
|
||||
description = "Pygments is a syntax highlighting package written in Python."
|
||||
name = "pygments"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "2.5.2"
|
||||
python-versions = ">=3.5"
|
||||
version = "2.6.1"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
|
@ -353,6 +421,17 @@ optional = false
|
|||
python-versions = "*"
|
||||
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]]
|
||||
category = "dev"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
|
|
@ -361,30 +440,22 @@ optional = false
|
|||
python-versions = "*"
|
||||
version = "2019.3"
|
||||
|
||||
[[package]]
|
||||
category = "main"
|
||||
description = "YAML parser and emitter for Python"
|
||||
name = "pyyaml"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "3.13"
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = "Python HTTP for Humans."
|
||||
name = "requests"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "2.22.0"
|
||||
version = "2.23.0"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
chardet = ">=3.0.2,<3.1.0"
|
||||
idna = ">=2.5,<2.9"
|
||||
chardet = ">=3.0.2,<4"
|
||||
idna = ">=2.5,<3"
|
||||
urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
|
||||
|
||||
[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"]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -414,6 +485,14 @@ version = "1.3.0"
|
|||
[package.dependencies]
|
||||
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]]
|
||||
category = "main"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
|
|
@ -436,7 +515,7 @@ description = "Python documentation generator"
|
|||
name = "sphinx"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
version = "2.3.1"
|
||||
version = "2.4.4"
|
||||
|
||||
[package.dependencies]
|
||||
Jinja2 = ">=2.3"
|
||||
|
|
@ -459,40 +538,43 @@ sphinxcontrib-serializinghtml = "*"
|
|||
|
||||
[package.extras]
|
||||
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]]
|
||||
category = "dev"
|
||||
description = ""
|
||||
description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
|
||||
name = "sphinxcontrib-applehelp"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
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 = "*"
|
||||
python-versions = ">=3.5"
|
||||
version = "1.0.2"
|
||||
|
||||
[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]]
|
||||
category = "dev"
|
||||
|
|
@ -519,25 +601,27 @@ test = ["pytest", "flake8", "mypy"]
|
|||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = ""
|
||||
description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
|
||||
name = "sphinxcontrib-qthelp"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.0.2"
|
||||
python-versions = ">=3.5"
|
||||
version = "1.0.3"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest", "flake8", "mypy"]
|
||||
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||
test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
description = ""
|
||||
description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
|
||||
name = "sphinxcontrib-serializinghtml"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "1.1.3"
|
||||
python-versions = ">=3.5"
|
||||
version = "1.1.4"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest", "flake8", "mypy"]
|
||||
lint = ["flake8", "mypy", "docutils-stubs"]
|
||||
test = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
category = "dev"
|
||||
|
|
@ -561,8 +645,8 @@ category = "dev"
|
|||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
name = "urllib3"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
|
||||
version = "1.25.7"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
|
||||
version = "1.25.8"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotlipy (>=0.6.0)"]
|
||||
|
|
@ -575,10 +659,11 @@ description = "Declarative parsing and validation of HTTP request objects, with
|
|||
name = "webargs"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
version = "5.5.2"
|
||||
version = "5.5.3"
|
||||
|
||||
[package.dependencies]
|
||||
marshmallow = ">=2.15.2"
|
||||
simplejson = ">=2.1.0"
|
||||
|
||||
[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)"]
|
||||
|
|
@ -592,12 +677,11 @@ category = "main"
|
|||
description = "The comprehensive WSGI web application library."
|
||||
name = "werkzeug"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
version = "0.16.0"
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
version = "1.0.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pytest", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinx-issues"]
|
||||
termcolor = ["termcolor"]
|
||||
watchdog = ["watchdog"]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -612,7 +696,7 @@ version = "1.11.2"
|
|||
rpi = ["picamera", "RPi.GPIO"]
|
||||
|
||||
[metadata]
|
||||
content-hash = "53c82daa835ef29f7aac8ac7d36048487a9d989662a58a2d05d9a70465fbc8c6"
|
||||
content-hash = "3a3a3bd4ffd7e0fe4c4c418a1b191c33afd375e6f06bb298189fd84ff75ee4cc"
|
||||
python-versions = "^3.6"
|
||||
|
||||
[metadata.files]
|
||||
|
|
@ -621,8 +705,8 @@ alabaster = [
|
|||
{file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
|
||||
]
|
||||
apispec = [
|
||||
{file = "apispec-3.2.0-py2.py3-none-any.whl", hash = "sha256:f2ecb3a6534cfb42cf69b5c1222d2c0e695aab5ec3d7edde31b7354670948f05"},
|
||||
{file = "apispec-3.2.0.tar.gz", hash = "sha256:c1625ae910f699c9adb21928693a3245b9faab6843f1b1c94ab2d505549cd78a"},
|
||||
{file = "apispec-3.3.0-py2.py3-none-any.whl", hash = "sha256:9bf4e51d56c9067c60668b78210ae213894f060f85593dc2ad8805eb7d875a2a"},
|
||||
{file = "apispec-3.3.0.tar.gz", hash = "sha256:419d0564b899e182c2af50483ea074db8cb05fee60838be58bb4542095d5c08d"},
|
||||
]
|
||||
appdirs = [
|
||||
{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.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 = [
|
||||
{file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"},
|
||||
{file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
|
||||
]
|
||||
click = [
|
||||
{file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"},
|
||||
{file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"},
|
||||
{file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"},
|
||||
{file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"},
|
||||
]
|
||||
colorama = [
|
||||
{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-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 = [
|
||||
{file = "idna-2.8-py2.py3-none-any.whl", hash = "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"},
|
||||
{file = "idna-2.8.tar.gz", hash = "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407"},
|
||||
{file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
|
||||
{file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
|
||||
]
|
||||
imagesize = [
|
||||
{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"},
|
||||
]
|
||||
jinja2 = [
|
||||
{file = "Jinja2-2.10.3-py2.py3-none-any.whl", hash = "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f"},
|
||||
{file = "Jinja2-2.10.3.tar.gz", hash = "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"},
|
||||
{file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"},
|
||||
{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 = [
|
||||
{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"},
|
||||
|
|
@ -747,39 +917,39 @@ markupsafe = [
|
|||
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
|
||||
]
|
||||
marshmallow = [
|
||||
{file = "marshmallow-3.3.0-py2.py3-none-any.whl", hash = "sha256:3e53dd9e9358977a3929e45cdbe4a671f9eff53a7d6a23f33ed3eab8c1890d8f"},
|
||||
{file = "marshmallow-3.3.0.tar.gz", hash = "sha256:0ba81b6da4ae69eb229b74b3c741ff13fe04fb899824377b1aff5aaa1a9fd46e"},
|
||||
{file = "marshmallow-3.5.1-py2.py3-none-any.whl", hash = "sha256:ac2e13b30165501b7d41fc0371b8df35944f5849769d136f20e2c5f6cdc6e665"},
|
||||
{file = "marshmallow-3.5.1.tar.gz", hash = "sha256:90854221bbb1498d003a0c3cc9d8390259137551917961c8b5258c64026b2f85"},
|
||||
]
|
||||
mccabe = [
|
||||
{file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"},
|
||||
{file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"},
|
||||
]
|
||||
numpy = [
|
||||
{file = "numpy-1.18.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:20b26aaa5b3da029942cdcce719b363dbe58696ad182aff0e5dcb1687ec946dc"},
|
||||
{file = "numpy-1.18.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:70a840a26f4e61defa7bdf811d7498a284ced303dfbc35acb7be12a39b2aa121"},
|
||||
{file = "numpy-1.18.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:17aa7a81fe7599a10f2b7d95856dc5cf84a4eefa45bc96123cbbc3ebc568994e"},
|
||||
{file = "numpy-1.18.1-cp35-cp35m-win32.whl", hash = "sha256:f3d0a94ad151870978fb93538e95411c83899c9dc63e6fb65542f769568ecfa5"},
|
||||
{file = "numpy-1.18.1-cp35-cp35m-win_amd64.whl", hash = "sha256:1786a08236f2c92ae0e70423c45e1e62788ed33028f94ca99c4df03f5be6b3c6"},
|
||||
{file = "numpy-1.18.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ae0975f42ab1f28364dcda3dde3cf6c1ddab3e1d4b2909da0cb0191fa9ca0480"},
|
||||
{file = "numpy-1.18.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:cf7eb6b1025d3e169989416b1adcd676624c2dbed9e3bcb7137f51bfc8cc2572"},
|
||||
{file = "numpy-1.18.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:b765ed3930b92812aa698a455847141869ef755a87e099fddd4ccf9d81fffb57"},
|
||||
{file = "numpy-1.18.1-cp36-cp36m-win32.whl", hash = "sha256:2d75908ab3ced4223ccba595b48e538afa5ecc37405923d1fea6906d7c3a50bc"},
|
||||
{file = "numpy-1.18.1-cp36-cp36m-win_amd64.whl", hash = "sha256:9acdf933c1fd263c513a2df3dceecea6f3ff4419d80bf238510976bf9bcb26cd"},
|
||||
{file = "numpy-1.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56bc8ded6fcd9adea90f65377438f9fea8c05fcf7c5ba766bef258d0da1554aa"},
|
||||
{file = "numpy-1.18.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e422c3152921cece8b6a2fb6b0b4d73b6579bd20ae075e7d15143e711f3ca2ca"},
|
||||
{file = "numpy-1.18.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b3af02ecc999c8003e538e60c89a2b37646b39b688d4e44d7373e11c2debabec"},
|
||||
{file = "numpy-1.18.1-cp37-cp37m-win32.whl", hash = "sha256:d92350c22b150c1cae7ebb0ee8b5670cc84848f6359cf6b5d8f86617098a9b73"},
|
||||
{file = "numpy-1.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:77c3bfe65d8560487052ad55c6998a04b654c2fbc36d546aef2b2e511e760971"},
|
||||
{file = "numpy-1.18.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c98c5ffd7d41611407a1103ae11c8b634ad6a43606eca3e2a5a269e5d6e8eb07"},
|
||||
{file = "numpy-1.18.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9537eecf179f566fd1c160a2e912ca0b8e02d773af0a7a1120ad4f7507cd0d26"},
|
||||
{file = "numpy-1.18.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e840f552a509e3380b0f0ec977e8124d0dc34dc0e68289ca28f4d7c1d0d79474"},
|
||||
{file = "numpy-1.18.1-cp38-cp38-win32.whl", hash = "sha256:590355aeade1a2eaba17617c19edccb7db8d78760175256e3cf94590a1a964f3"},
|
||||
{file = "numpy-1.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:39d2c685af15d3ce682c99ce5925cc66efc824652e10990d2462dfe9b8918c6a"},
|
||||
{file = "numpy-1.18.1.zip", hash = "sha256:b6ff59cee96b454516e47e7721098e6ceebef435e3e21ac2d6c3b8b02628eb77"},
|
||||
{file = "numpy-1.18.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a1baa1dc8ecd88fb2d2a651671a84b9938461e8a8eed13e2f0a812a94084d1fa"},
|
||||
{file = "numpy-1.18.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:a244f7af80dacf21054386539699ce29bcc64796ed9850c99a34b41305630286"},
|
||||
{file = "numpy-1.18.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6fcc5a3990e269f86d388f165a089259893851437b904f422d301cdce4ff25c8"},
|
||||
{file = "numpy-1.18.2-cp35-cp35m-win32.whl", hash = "sha256:b5ad0adb51b2dee7d0ee75a69e9871e2ddfb061c73ea8bc439376298141f77f5"},
|
||||
{file = "numpy-1.18.2-cp35-cp35m-win_amd64.whl", hash = "sha256:87902e5c03355335fc5992a74ba0247a70d937f326d852fc613b7f53516c0963"},
|
||||
{file = "numpy-1.18.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9ab21d1cb156a620d3999dd92f7d1c86824c622873841d6b080ca5495fa10fef"},
|
||||
{file = "numpy-1.18.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:cdb3a70285e8220875e4d2bc394e49b4988bdb1298ffa4e0bd81b2f613be397c"},
|
||||
{file = "numpy-1.18.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:6d205249a0293e62bbb3898c4c2e1ff8a22f98375a34775a259a0523111a8f6c"},
|
||||
{file = "numpy-1.18.2-cp36-cp36m-win32.whl", hash = "sha256:a35af656a7ba1d3decdd4fae5322b87277de8ac98b7d9da657d9e212ece76a61"},
|
||||
{file = "numpy-1.18.2-cp36-cp36m-win_amd64.whl", hash = "sha256:1598a6de323508cfeed6b7cd6c4efb43324f4692e20d1f76e1feec7f59013448"},
|
||||
{file = "numpy-1.18.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:deb529c40c3f1e38d53d5ae6cd077c21f1d49e13afc7936f7f868455e16b64a0"},
|
||||
{file = "numpy-1.18.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:cd77d58fb2acf57c1d1ee2835567cd70e6f1835e32090538f17f8a3a99e5e34b"},
|
||||
{file = "numpy-1.18.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:b1fe1a6f3a6f355f6c29789b5927f8bd4f134a4bd9a781099a7c4f66af8850f5"},
|
||||
{file = "numpy-1.18.2-cp37-cp37m-win32.whl", hash = "sha256:2e40be731ad618cb4974d5ba60d373cdf4f1b8dcbf1dcf4d9dff5e212baf69c5"},
|
||||
{file = "numpy-1.18.2-cp37-cp37m-win_amd64.whl", hash = "sha256:4ba59db1fcc27ea31368af524dcf874d9277f21fd2e1f7f1e2e0c75ee61419ed"},
|
||||
{file = "numpy-1.18.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:59ca9c6592da581a03d42cc4e270732552243dc45e87248aa8d636d53812f6a5"},
|
||||
{file = "numpy-1.18.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1b0ece94018ae21163d1f651b527156e1f03943b986188dd81bc7e066eae9d1c"},
|
||||
{file = "numpy-1.18.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:82847f2765835c8e5308f136bc34018d09b49037ec23ecc42b246424c767056b"},
|
||||
{file = "numpy-1.18.2-cp38-cp38-win32.whl", hash = "sha256:5e0feb76849ca3e83dd396254e47c7dba65b3fa9ed3df67c2556293ae3e16de3"},
|
||||
{file = "numpy-1.18.2-cp38-cp38-win_amd64.whl", hash = "sha256:ba3c7a2814ec8a176bb71f91478293d633c08582119e713a0c5351c0f77698da"},
|
||||
{file = "numpy-1.18.2.zip", hash = "sha256:e7894793e6e8540dbeac77c87b489e331947813511108ae097f1715c018b8f3d"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-20.0-py2.py3-none-any.whl", hash = "sha256:aec3fdbb8bc9e4bb65f0634b9f551ced63983a529d6a8931817d52fdd0816ddb"},
|
||||
{file = "packaging-20.0.tar.gz", hash = "sha256:fe1d8331dfa7cc0a883b49d75fc76380b2ab2734b220fbb87d774e4fd4b851f8"},
|
||||
{file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"},
|
||||
{file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"},
|
||||
]
|
||||
picamera = []
|
||||
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.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 = [
|
||||
{file = "Pygments-2.5.2-py2.py3-none-any.whl", hash = "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b"},
|
||||
{file = "Pygments-2.5.2.tar.gz", hash = "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"},
|
||||
{file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"},
|
||||
{file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"},
|
||||
]
|
||||
pylint = [
|
||||
{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.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 = [
|
||||
{file = "pytz-2019.3-py2.py3-none-any.whl", hash = "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d"},
|
||||
{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 = [
|
||||
{file = "requests-2.22.0-py2.py3-none-any.whl", hash = "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"},
|
||||
{file = "requests-2.22.0.tar.gz", hash = "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4"},
|
||||
{file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
|
||||
{file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},
|
||||
]
|
||||
rope = [
|
||||
{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.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 = [
|
||||
{file = "six-1.14.0-py2.py3-none-any.whl", hash = "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"},
|
||||
{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"},
|
||||
]
|
||||
sphinx = [
|
||||
{file = "Sphinx-2.3.1-py3-none-any.whl", hash = "sha256:298537cb3234578b2d954ff18c5608468229e116a9757af3b831c2b2b4819159"},
|
||||
{file = "Sphinx-2.3.1.tar.gz", hash = "sha256:e6e766b74f85f37a5f3e0773a1e1be8db3fcb799deb58ca6d18b70b0b44542a5"},
|
||||
{file = "Sphinx-2.4.4-py3-none-any.whl", hash = "sha256:fc312670b56cb54920d6cc2ced455a22a547910de10b3142276495ced49231cb"},
|
||||
{file = "Sphinx-2.4.4.tar.gz", hash = "sha256:b4c750d546ab6d7e05bdff6ac24db8ae3e8b8253a3569b754e445110a0a12b66"},
|
||||
]
|
||||
sphinxcontrib-applehelp = [
|
||||
{file = "sphinxcontrib-applehelp-1.0.1.tar.gz", hash = "sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897"},
|
||||
{file = "sphinxcontrib_applehelp-1.0.1-py2.py3-none-any.whl", hash = "sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"},
|
||||
{file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"},
|
||||
{file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"},
|
||||
]
|
||||
sphinxcontrib-devhelp = [
|
||||
{file = "sphinxcontrib-devhelp-1.0.1.tar.gz", hash = "sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34"},
|
||||
{file = "sphinxcontrib_devhelp-1.0.1-py2.py3-none-any.whl", hash = "sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"},
|
||||
{file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"},
|
||||
{file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"},
|
||||
]
|
||||
sphinxcontrib-htmlhelp = [
|
||||
{file = "sphinxcontrib-htmlhelp-1.0.2.tar.gz", hash = "sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422"},
|
||||
{file = "sphinxcontrib_htmlhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"},
|
||||
{file = "sphinxcontrib-htmlhelp-1.0.3.tar.gz", hash = "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"},
|
||||
{file = "sphinxcontrib_htmlhelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f"},
|
||||
]
|
||||
sphinxcontrib-httpdomain = [
|
||||
{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"},
|
||||
]
|
||||
sphinxcontrib-qthelp = [
|
||||
{file = "sphinxcontrib-qthelp-1.0.2.tar.gz", hash = "sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"},
|
||||
{file = "sphinxcontrib_qthelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20"},
|
||||
{file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"},
|
||||
{file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"},
|
||||
]
|
||||
sphinxcontrib-serializinghtml = [
|
||||
{file = "sphinxcontrib-serializinghtml-1.1.3.tar.gz", hash = "sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227"},
|
||||
{file = "sphinxcontrib_serializinghtml-1.1.3-py2.py3-none-any.whl", hash = "sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"},
|
||||
{file = "sphinxcontrib-serializinghtml-1.1.4.tar.gz", hash = "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc"},
|
||||
{file = "sphinxcontrib_serializinghtml-1.1.4-py2.py3-none-any.whl", hash = "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"},
|
||||
]
|
||||
toml = [
|
||||
{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"},
|
||||
]
|
||||
urllib3 = [
|
||||
{file = "urllib3-1.25.7-py2.py3-none-any.whl", hash = "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293"},
|
||||
{file = "urllib3-1.25.7.tar.gz", hash = "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"},
|
||||
{file = "urllib3-1.25.8-py2.py3-none-any.whl", hash = "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc"},
|
||||
{file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"},
|
||||
]
|
||||
webargs = [
|
||||
{file = "webargs-5.5.2-py2-none-any.whl", hash = "sha256:3f9dc15de183d356c9a0acc159c100ea0506c0c240c1e6f1d8b308c5fed4dbbd"},
|
||||
{file = "webargs-5.5.2-py3-none-any.whl", hash = "sha256:fa4ad3ad9b38bedd26c619264fdc50d7ae014b49186736bca851e5b5228f2a1b"},
|
||||
{file = "webargs-5.5.2.tar.gz", hash = "sha256:3beca296598067cec24a0b6f91c0afcc19b6e3c4d84ab026b931669628bb47b4"},
|
||||
{file = "webargs-5.5.3-py2-none-any.whl", hash = "sha256:fc81c9f9d391acfbce406a319217319fd8b2fd862f7fdb5319ad06944f36ed25"},
|
||||
{file = "webargs-5.5.3-py3-none-any.whl", hash = "sha256:4f04918864c7602886335d8099f9b8960ee698b6b914f022736ed50be6b71235"},
|
||||
{file = "webargs-5.5.3.tar.gz", hash = "sha256:871642a2e0c62f21d5b78f357750ac7a87e6bc734c972f633aa5fb6204fbf29a"},
|
||||
]
|
||||
werkzeug = [
|
||||
{file = "Werkzeug-0.16.0-py2.py3-none-any.whl", hash = "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4"},
|
||||
{file = "Werkzeug-0.16.0.tar.gz", hash = "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7"},
|
||||
{file = "Werkzeug-1.0.0-py2.py3-none-any.whl", hash = "sha256:6dc65cf9091cf750012f56f2cad759fa9e879f511b5ff8685e456b4e3bf90d16"},
|
||||
{file = "Werkzeug-1.0.0.tar.gz", hash = "sha256:169ba8a33788476292d04186ab33b01d6add475033dfc07215e6d219cc077096"},
|
||||
]
|
||||
wrapt = [
|
||||
{file = "wrapt-1.11.2.tar.gz", hash = "sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1"},
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
|
|||
|
||||
[tool.poetry]
|
||||
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."
|
||||
|
||||
authors = [
|
||||
|
|
@ -25,16 +25,17 @@ license = "GPL-3.0"
|
|||
[tool.poetry.dependencies]
|
||||
python = "^3.6"
|
||||
Flask = "^1.0"
|
||||
pyyaml = "^3.13"
|
||||
numpy = "^1.17.0"
|
||||
Pillow = "^5.4"
|
||||
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 }
|
||||
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]
|
||||
rpi = ["picamera", "RPi.GPIO"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue