Merge branch '2.9-dev' into 'master'

v2.9.0

See merge request openflexure/openflexure-microscope-server!101
This commit is contained in:
Joel Collins 2020-12-07 14:01:02 +00:00
commit 9cf0a9badf
45 changed files with 1558 additions and 1113 deletions

View file

@ -10,9 +10,15 @@ This includes installing the server in a mode better suited for active developme
# Developer guidelines
The Raspberry Pi image we use currently ships with Python 3.7.3. For local development, please use PyEnv or similar to make sure you're running on this version. For example, Windows users can use [Scoop](https://scoop.sh/) to install specific Python versions.
## Installation
* `git clone https://gitlab.com/openflexure/openflexure-microscope-server.git`
* `cd openflexure-microscope-server`
* (Optional) Set local Python version
* `pyenv init`
* `pyenv install 3.7.3`
* `pyenv local 3.7.3`
* `poetry install`
* Building the static interface will require a valid Node.js installation
* To build on a Raspberry Pi:

View file

@ -2,35 +2,35 @@ from labthings import find_component
from labthings.extensions import BaseExtension
def identify():
"""
Demonstrate access to Microscope.camera, and Microscope.stage
"""
microscope = find_component("org.openflexure.microscope")
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
response = (
f"My name is {microscope.name}. "
f"My parent camera is {microscope.camera}, "
f"and my parent stage is {microscope.stage}."
)
def identify(self):
"""
Demonstrate access to Microscope.camera, and Microscope.stage
"""
microscope = find_component("org.openflexure.microscope")
return response
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()
def rename(new_name):
"""
Rename the microscope
"""
microscope = find_component("org.openflexure.microscope")
microscope.name = new_name
microscope.save_settings()
# Create your extension object
my_extension = BaseExtension("com.myname.myextension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(identify, "identify")
my_extension.add_method(rename, "rename")
LABTHINGS_EXTENSIONS = (MyExtension,)

View file

@ -1,41 +0,0 @@
from labthings import find_component
from labthings.extensions import BaseExtension
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Create some instance variable
self.state_variable = "An example of a persistant instance variable"
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
def identify(self):
"""
Demonstrate access to Microscope.camera, and Microscope.stage
"""
microscope = find_component("org.openflexure.microscope")
response = (
f"My name is {microscope.name}. "
f"My parent camera is {microscope.camera}, "
f"and my parent stage is {microscope.stage}."
)
return response
def rename(self, new_name):
"""
Rename the microscope
"""
microscope = find_component("org.openflexure.microscope")
microscope.name = new_name
microscope.save_settings()
# Create your extension object
my_extension = MyExtension()

View file

@ -2,42 +2,45 @@ from labthings import fields, find_component
from labthings.extensions import BaseExtension
from labthings.views import View
## Extension methods
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
def identify(microscope):
"""
Demonstrate access to Microscope.camera, and Microscope.stage
"""
# Add our API Views (defined below MyExtension)
self.add_view(ExampleIdentifyView, "/identify")
self.add_view(ExampleRenameView, "/rename")
response = (
f"My name is {microscope.name}. "
f"My parent camera is {microscope.camera}, "
f"and my parent stage is {microscope.stage}."
)
def identify(self, microscope):
"""
Demonstrate access to Microscope.camera, and Microscope.stage
"""
response = (
f"My name is {microscope.name}. "
f"My parent camera is {microscope.camera}, "
f"and my parent stage is {microscope.stage}."
)
return response
return response
def rename(microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
def rename(self, microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
## Extension views
class ExampleIdentifyView(View):
def get(self):
# Find our microscope component
microscope = find_component("org.openflexure.microscope")
# Return our identify function's output
return identify(microscope)
return self.extension.identify(microscope)
class ExampleRenameView(View):
@ -53,21 +56,10 @@ class ExampleRenameView(View):
microscope = find_component("org.openflexure.microscope")
# Pass microscope and new name to our rename function
rename(microscope, new_name)
self.extension.rename(microscope, new_name)
# Return our identify function's output
return identify(microscope)
return self.extension.identify(microscope)
## Create extension
# Create your extension object
my_extension = BaseExtension("com.myname.myextension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(identify, "identify")
my_extension.add_method(rename, "rename")
# Add API views to your extension
my_extension.add_view(ExampleIdentifyView, "/identify")
my_extension.add_view(ExampleRenameView, "/rename")
LABTHINGS_EXTENSIONS = (MyExtension,)

View file

@ -2,7 +2,23 @@ from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension
from labthings.views import View
## Extension methods
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
# Add our API Views (defined below MyExtension)
self.add_view(ExampleIdentifyView, "/identify")
self.add_view(ExampleRenameView, "/rename")
def rename(self, microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
# Define which properties of a Microscope object we care about,
@ -15,18 +31,7 @@ class MicroscopeIdentifySchema(Schema):
stage = fields.String() # Stage object (represented as a string)
def rename(microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
## Extension views
class ExampleIdentifyView(View):
# Format our returned object using MicroscopeIdentifySchema
schema = MicroscopeIdentifySchema()
@ -54,21 +59,11 @@ class ExampleRenameView(View):
microscope = find_component("org.openflexure.microscope")
# Pass microscope and new name to our rename function
rename(microscope, new_name)
self.extension.rename(microscope, new_name)
# Return our microscope object,
# let schema handle formatting the output
return microscope
## Create extension
# Create your extension object
my_extension = BaseExtension("com.myname.myextension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(rename, "rename")
# Add API views to your extension
my_extension.add_view(ExampleIdentifyView, "/identify")
my_extension.add_view(ExampleRenameView, "/rename")
LABTHINGS_EXTENSIONS = (MyExtension,)

View file

@ -2,7 +2,23 @@ from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension
from labthings.views import PropertyView
## Extension methods
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
# Add our API Views (defined below MyExtension)
self.add_view(ExampleIdentifyView, "/identify")
self.add_view(ExampleRenameView, "/rename")
def rename(self, microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
# Define which properties of a Microscope object we care about,
@ -15,16 +31,7 @@ class MicroscopeIdentifySchema(Schema):
stage = fields.String() # Stage object (represented as a string)
def rename(microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
## Extension views
## Extension viewss
# Since we only have a GET method here, it'll register as a read-only property
class ExampleIdentifyView(PropertyView):
@ -68,21 +75,11 @@ class ExampleRenameView(PropertyView):
microscope = find_component("org.openflexure.microscope")
# Pass microscope and new name to our rename function
rename(microscope, new_name)
self.extension.rename(microscope, new_name)
# Return our microscope object,
# let schema handle formatting the output
return microscope
## Create extension
# Create your extension object
my_extension = BaseExtension("com.myname.myextension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(rename, "rename")
# Add API views to your extension
my_extension.add_view(ExampleIdentifyView, "/identify")
my_extension.add_view(ExampleRenameView, "/rename")
LABTHINGS_EXTENSIONS = (MyExtension,)

View file

@ -5,7 +5,23 @@ from labthings import Schema, fields, find_component
from labthings.extensions import BaseExtension
from labthings.views import ActionView, PropertyView
## Extension methods
# Create the extension class
class MyExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("com.myname.myextension", version="0.0.0")
# Add our API Views (defined below MyExtension)
self.add_view(ExampleIdentifyView, "/identify")
self.add_view(ExampleRenameView, "/rename")
def rename(self, microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
# Define which properties of a Microscope object we care about,
@ -18,15 +34,6 @@ class MicroscopeIdentifySchema(Schema):
stage = fields.String() # Stage object (represented as a string)
def rename(microscope, new_name):
"""
Rename the microscope
"""
microscope.name = new_name
microscope.save_settings()
## Extension views
# Since we only have a GET method here, it'll register as a read-only property
@ -71,7 +78,7 @@ class ExampleRenameView(PropertyView):
microscope = find_component("org.openflexure.microscope")
# Pass microscope and new name to our rename function
rename(microscope, new_name)
self.extension.rename(microscope, new_name)
# Return our microscope object,
# let schema handle formatting the output
@ -109,15 +116,4 @@ class QuickCaptureAPI(ActionView):
return send_file(io.BytesIO(stream.read()), mimetype="image/jpeg")
## Create extension
# Create your extension object
my_extension = BaseExtension("com.myname.myextension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(rename, "rename")
# Add API views to your extension
my_extension.add_view(ExampleIdentifyView, "/identify")
my_extension.add_view(ExampleRenameView, "/rename")
my_extension.add_view(QuickCaptureAPI, "/quick-capture")
LABTHINGS_EXTENSIONS = (MyExtension,)

View file

@ -7,52 +7,59 @@ from labthings.views import ActionView
# Used in our timelapse function
from openflexure_microscope.captures.capture_manager import generate_basename
## Extension methods
# Create the extension class
class TimelapseExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("org.openflexure.timelapse-extension", version="0.0.0")
def timelapse(microscope, n_images, t_between):
"""
Save a set of images in a timelapse
# Add our API views
self.add_view(TimelapseAPIView, "/timelapse")
Args:
microscope: Microscope object
n_images (int): Number of images to take
t_between (int/float): Time, in seconds, between sequential captures
"""
base_file_name = generate_basename()
folder = "TIMELAPSE_{}".format(base_file_name)
def timelapse(self, microscope, n_images, t_between):
"""
Save a set of images in a timelapse
# Take exclusive control over both the camera and stage
with microscope.camera.lock, microscope.stage.lock:
for n in range(n_images):
# Elegantly handle action cancellation
if current_action() and current_action().stopped:
return
# Generate a filename
filename = f"{base_file_name}_image{n}"
# Create a file to save the image to
output = microscope.camera.new_image(
filename=filename, folder=folder, temporary=False
)
Args:
microscope: Microscope object
n_images (int): Number of images to take
t_between (int/float): Time, in seconds, between sequential captures
"""
base_file_name = generate_basename()
folder = "TIMELAPSE_{}".format(base_file_name)
# Capture
microscope.camera.capture(output)
# Take exclusive control over both the camera and stage
with microscope.camera.lock, microscope.stage.lock:
for n in range(n_images):
# Elegantly handle action cancellation
if current_action() and current_action().stopped:
return
# Generate a filename
filename = f"{base_file_name}_image{n}"
# Create a file to save the image to
output = microscope.camera.new_image(
filename=filename, folder=folder, temporary=False
)
# Add system metadata
output.put_metadata(microscope.metadata, system=True)
# Capture
microscope.camera.capture(output)
# Update task progress (only does anyting if the function is running in a LabThings task)
progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent
update_action_progress(progress_pct)
# Add system metadata
output.put_metadata(microscope.metadata, system=True)
# Wait for the specified time
time.sleep(t_between)
# Update task progress (only does anyting if the function is running in a LabThings task)
progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent
update_action_progress(progress_pct)
# Wait for the specified time
time.sleep(t_between)
## Extension views
class TimelapseAPI(ActionView):
class TimelapseAPIView(ActionView):
"""
Take a series of images in a timelapse
"""
@ -71,16 +78,9 @@ class TimelapseAPI(ActionView):
microscope = find_component("org.openflexure.microscope")
# Start "timelapse"
return timelapse(microscope, args.get("n_images"), args.get("t_between"))
return self.extension.timelapse(
microscope, args.get("n_images"), args.get("t_between")
)
## Create extension
# Create your extension object
my_extension = BaseExtension("com.myname.timelapse-extension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(timelapse, "timelapse")
# Add API views to your extension
my_extension.add_view(TimelapseAPI, "/timelapse")
LABTHINGS_EXTENSIONS = (TimelapseExtension,)

View file

@ -10,54 +10,91 @@ from openflexure_microscope.api.utilities.gui import build_gui
# Used in our timelapse function
from openflexure_microscope.captures.capture_manager import generate_basename
## Extension methods
# Create the extension class
class TimelapseExtension(BaseExtension):
def __init__(self):
# Superclass init function
super().__init__("org.openflexure.timelapse-extension", version="0.0.0")
def timelapse(microscope, n_images, t_between):
"""
Save a set of images in a timelapse
# Add our API views
self.add_view(TimelapseAPIView, "/timelapse")
Args:
microscope: Microscope object
n_images (int): Number of images to take
t_between (int/float): Time, in seconds, between sequential captures
"""
base_file_name = generate_basename()
folder = "TIMELAPSE_{}".format(base_file_name)
# Add our GUI description
gui_description = {
"icon": "timelapse", # Name of an icon from https://material.io/resources/icons/
"forms": [ # List of forms. Each form is a collapsible accordion panel
{
"name": "Start a timelapse", # Form title
"route": "/timelapse", # The URL rule (as given by "add_view") of your submission view
"isTask": True, # This forms submission starts a background task
"isCollapsible": False, # This form cannot be collapsed into an accordion
"submitLabel": "Start", # Label for the form submit button
"schema": [ # List of dictionaries. Each element is a form component.
{
"fieldType": "numberInput",
"name": "n_images", # Name of the view arg this value corresponds to
"label": "Number of images",
"min": 1, # HTML number input attribute
"default": 5, # HTML number input attribute
},
{
"fieldType": "numberInput",
"name": "t_between",
"label": "Time (seconds) between images",
"min": 0.1, # HTML number input attribute
"step": 0.1, # HTML number input attribute
"default": 1, # HTML number input attribute
},
],
}
],
}
self.add_meta("gui", build_gui(gui_description, self))
# Take exclusive control over both the camera and stage
with microscope.camera.lock, microscope.stage.lock:
for n in range(n_images):
# Elegantly handle action cancellation
if current_action() and current_action().stopped:
return
# Generate a filename
filename = f"{base_file_name}_image{n}"
# Create a file to save the image to
output = microscope.camera.new_image(
filename=filename, folder=folder, temporary=False
)
def timelapse(self, microscope, n_images, t_between):
"""
Save a set of images in a timelapse
# Capture
microscope.camera.capture(output)
Args:
microscope: Microscope object
n_images (int): Number of images to take
t_between (int/float): Time, in seconds, between sequential captures
"""
base_file_name = generate_basename()
folder = "TIMELAPSE_{}".format(base_file_name)
# Add system metadata
output.put_metadata(microscope.metadata, system=True)
# Take exclusive control over both the camera and stage
with microscope.camera.lock, microscope.stage.lock:
for n in range(n_images):
# Elegantly handle action cancellation
if current_action() and current_action().stopped:
return
# Generate a filename
filename = f"{base_file_name}_image{n}"
# Create a file to save the image to
output = microscope.camera.new_image(
filename=filename, folder=folder, temporary=False
)
# Update task progress (only does anyting if the function is running in a LabThings task)
progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent
update_action_progress(progress_pct)
# Capture
microscope.camera.capture(output)
# Wait for the specified time
time.sleep(t_between)
# Add system metadata
output.put_metadata(microscope.metadata, system=True)
# Update task progress (only does anyting if the function is running in a LabThings task)
progress_pct = ((n + 1) / n_images) * 100 # Progress, in percent
update_action_progress(progress_pct)
# Wait for the specified time
time.sleep(t_between)
## Extension views
class TimelapseAPI(ActionView):
class TimelapseAPIView(ActionView):
"""
Take a series of images in a timelapse, running as a background task
Take a series of images in a timelapse
"""
args = {
@ -73,53 +110,10 @@ class TimelapseAPI(ActionView):
# Find our microscope component
microscope = find_component("org.openflexure.microscope")
# Create and start "timelapse", running in a background task
return timelapse(microscope, args.get("n_images"), args.get("t_between"))
# Start "timelapse"
return self.extension.timelapse(
microscope, args.get("n_images"), args.get("t_between")
)
## Extension GUI (OpenFlexure eV)
# Alternate form without any dynamic parts
extension_gui = {
"icon": "timelapse", # Name of an icon from https://material.io/resources/icons/
"forms": [ # List of forms. Each form is a collapsible accordion panel
{
"name": "Start a timelapse", # Form title
"route": "/timelapse", # The URL rule (as given by "add_view") of your submission view
"isTask": True, # This forms submission starts a background task
"isCollapsible": False, # This form cannot be collapsed into an accordion
"submitLabel": "Start", # Label for the form submit button
"schema": [ # List of dictionaries. Each element is a form component.
{
"fieldType": "numberInput",
"name": "n_images", # Name of the view arg this value corresponds to
"label": "Number of images",
"min": 1, # HTML number input attribute
"default": 5, # HTML number input attribute
},
{
"fieldType": "numberInput",
"name": "t_between",
"label": "Time (seconds) between images",
"min": 0.1, # HTML number input attribute
"step": 0.1, # HTML number input attribute
"default": 1, # HTML number input attribute
},
],
}
],
}
## Create extension
# Create your extension object
my_extension = BaseExtension("com.myname.timelapse-extension", version="0.0.0")
# Add methods to your extension
my_extension.add_method(timelapse, "timelapse")
# Add API views to your extension
my_extension.add_view(TimelapseAPI, "/timelapse")
# Add OpenFlexure eV GUI to your extension
my_extension.add_meta("gui", build_gui(extension_gui, my_extension))
LABTHINGS_EXTENSIONS = (TimelapseExtension,)

View file

@ -16,6 +16,15 @@ Package extensions
------------------
Generally, for adding anything other than very simple functionality, extensions should be written as `package distributions <https://packaging.python.org/tutorials/packaging-projects/>`_. This has the advantage of allowing relative imports, so functionality can be easily split over several files. For example, class definitions associated with API routes can be separated from class definitions associated with the microscope extension.
Your module must be a folder within the extensions folder (by default ``/var/openflexure/extensions/microscope_extensions``), and include a top-level ``__init__.py`` file which includes (or imports) all of your extension objects.
Your module must be a folder within the extensions folder (by default ``/var/openflexure/extensions/microscope_extensions``), and include a top-level ``__init__.py`` file which includes (or imports) all of your extension classes, and includes them in a global constant `LABTHINGS_EXTENSIONS` list.
For example, if your extension classes are defined in a file ``my_extension.py``, your adjascent ``__init__.py`` file may look like:
.. code-block:: python
from .my_extension import MyExtensionClass, MyOtherExtensionClass
LABTHINGS_EXTENSIONS = (MyExtensionClass, MyOtherExtensionClass)
In order to enable a globally installed, packaged extension, create a file in the applications extensions directory (by default ``/var/openflexure/extensions/microscope_extensions``) which imports your extension object(s) from your module.

View file

@ -1,9 +1,11 @@
Basic extension structure
=========================
An extension starts as a simple instance of :py:class:`labthings.extensions.BaseExtension`.
An extension starts as a subclass of :py:class:`labthings.extensions.BaseExtension`.
Each extension is described by a single ``BaseExtension`` instance, containing any number of methods, API views, and additional hardware components.
You will build your extension by subclassing :py:class:`labthings.extensions.BaseExtension`, and adding the class to a top-level `LABTHINGS_EXTENSIONS` list.
In order to access the currently running microscope object, use the :py:func:`labthings.find_component` function, with the argument ``"org.openflexure.microscope"``. Likewise, any new components attached by other extensions can be found using their full name, as above.
A simple extension file, with no API views but application-available methods may look like:
@ -24,13 +26,3 @@ 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.extensions.BaseExtension` is recommended.
The same simple extension as seen above can be written using subclassing:
.. literalinclude:: ./example_extension/01b_basic_structure_subclass.py

View file

@ -1,6 +1,14 @@
Adding web API views
====================
Key terminology
---------------
API View (or View)
++++++++++++++++++
*"A view function is the code you write to respond to requests to your application [...] For RESTful APIs its especially helpful to execute a different function for each HTTP method. With the [View class] you can easily do that. Each HTTP method maps to a function with the same name (just in lowercase)"* - `Flask documentation <https://flask.palletsprojects.com/en/1.1.x/views/>`_
Introduction
------------
Extensions can create views to expose extension functionality via the web API. Creating API views for your extension is strongly recommended, as this is the primary way we encourage interaction with the microscope device.
@ -13,6 +21,8 @@ Continuing our example on the previous page, and discussed below, adding API vie
Note that we are now passing our microscope object as an argument to our API methods. Finding the microscope component is performed by the API view at request-time, and passed onto the functions.
Your extension functions can be accessed from within an API View by using ``self.extension``. Once your view has been added to your extension, this will point to the extension object, allowing your API views to use your extension functionality.
In this case, our extension will have two new API views at `/identify` and `/rename`. The `/identify` view only accepts GET requests, and the `/rename` view only accepts POST requests.
Request arguments

View file

@ -1,6 +1,11 @@
import logging
import traceback
from contextlib import contextmanager
from typing import List, Type
from labthings.extensions import BaseExtension
LABTHINGS_EXTENSIONS: List[Type[BaseExtension]] = []
@contextmanager
@ -17,14 +22,28 @@ def handle_extension_error(extension_name):
with handle_extension_error("autofocus"):
from .autofocus import autofocus_extension_v2
from .autofocus import AutofocusExtension
LABTHINGS_EXTENSIONS.append(AutofocusExtension)
with handle_extension_error("scan"):
from .scan import scan_extension_v2
from .scan import ScanExtension
LABTHINGS_EXTENSIONS.append(ScanExtension)
with handle_extension_error("zip builder"):
from .zip_builder import zip_extension_v2
from .zip_builder import ZipBuilderExtension
LABTHINGS_EXTENSIONS.append(ZipBuilderExtension)
with handle_extension_error("autostorage"):
from .autostorage import autostorage_extension_v2
with handle_extension_error("camera stage mapping"):
from camera_stage_mapping.ofm_extension import csm_extension
from .autostorage import AutostorageExtension
LABTHINGS_EXTENSIONS.append(AutostorageExtension)
with handle_extension_error("lens shading calibration"):
from .picamera_autocalibrate import lst_extension_v2
from .picamera_autocalibrate import LSTExtension
LABTHINGS_EXTENSIONS.append(LSTExtension)
with handle_extension_error("camera stage mapping"):
from .camera_stage_mapping import CSMExtension
LABTHINGS_EXTENSIONS.append(CSMExtension)

View file

@ -1,7 +1,7 @@
import logging
import time
from contextlib import contextmanager
from typing import Callable, List, Optional, Tuple
from typing import Callable, Dict, List, Optional, Tuple
import numpy as np
from labthings import current_action, fields, find_component
@ -39,7 +39,7 @@ class JPEGSharpnessMonitor:
self.camera.stream.stop_tracking()
self.camera.stream.reset_tracking()
def focus_rel(self, dz: int, backlash: bool = False, **kwargs):
def focus_rel(self, dz: int, backlash: bool = False, **kwargs) -> Tuple[int, int]:
# Store the start time and position
self.camera.stream.start_tracking()
self.stage_times.append(time.time())
@ -62,22 +62,28 @@ class JPEGSharpnessMonitor:
self.camera.stream.reset_tracking()
# Index of the data for this movement
data_index = len(self.stage_positions) - 2
data_index: int = len(self.stage_positions) - 2
# Final z position after move
final_z_position = self.stage_positions[-1][2]
final_z_position: int = self.stage_positions[-1][2]
return data_index, final_z_position
def move_data(self, istart: int, istop: Optional[int] = None):
def move_data(
self, istart: int, istop: Optional[int] = None
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
"""Extract sharpness as a function of (interpolated) z"""
if istop is None:
istop = istart + 2
jpeg_times = np.array(self.jpeg_times)
jpeg_sizes = np.array(self.jpeg_sizes)
stage_times = np.array(self.stage_times)[istart:istop]
stage_zs = np.array(self.stage_positions)[istart:istop, 2]
jpeg_times: np.ndarray = np.array(self.jpeg_times) # np.ndarray[float]
jpeg_sizes: np.ndarray = np.array(self.jpeg_sizes) # np.ndarray[int]
stage_times: np.ndarray = np.array(self.stage_times)[
istart:istop
] # np.ndarray[float]
stage_zs: np.ndarray = np.array(self.stage_positions)[
istart:istop, 2
] # np.ndarray[int]
try:
start = np.argmax(jpeg_times > stage_times[0])
stop = np.argmax(jpeg_times > stage_times[1])
start: int = int(np.argmax(jpeg_times > stage_times[0]))
stop: int = int(np.argmax(jpeg_times > stage_times[1]))
except ValueError as e:
if np.sum(jpeg_times > stage_times[0]) == 0:
raise ValueError(
@ -89,10 +95,12 @@ class JPEGSharpnessMonitor:
stop = len(jpeg_times)
logging.debug("changing stop to %s", (stop))
jpeg_times = jpeg_times[start:stop]
jpeg_zs = np.interp(jpeg_times, stage_times, stage_zs)
jpeg_zs: np.ndarray = np.interp(
jpeg_times, stage_times, stage_zs
) # np.ndarray[float]
return jpeg_times, jpeg_zs, jpeg_sizes[start:stop]
def sharpest_z_on_move(self, index: int):
def sharpest_z_on_move(self, index: int) -> int:
"""Return the z position of the sharpest image on a given move"""
_, jz, js = self.move_data(index)
if len(js) == 0:
@ -101,7 +109,7 @@ class JPEGSharpnessMonitor:
)
return jz[np.argmax(js)]
def data_dict(self):
def data_dict(self) -> Dict[str, np.ndarray]:
"""Return the gathered data as a single convenient dictionary"""
data = {}
for k in ["jpeg_times", "jpeg_sizes", "stage_times", "stage_positions"]:
@ -109,18 +117,28 @@ class JPEGSharpnessMonitor:
return data
def sharpness_sum_lap2(rgb_image: np.ndarray):
@contextmanager
def monitor_sharpness(microscope: Microscope):
m: JPEGSharpnessMonitor = JPEGSharpnessMonitor(microscope)
m.start()
try:
yield m
finally:
m.stop()
def sharpness_sum_lap2(rgb_image: np.ndarray) -> np.float:
"""Return an image sharpness metric: sum(laplacian(image)**")"""
image_bw = np.mean(rgb_image, 2)
image_lap = ndimage.filters.laplace(image_bw)
image_bw: np.float = np.mean(rgb_image, 2)
image_lap: np.float = ndimage.filters.laplace(image_bw)
return np.mean(image_lap.astype(np.float) ** 4)
def sharpness_edge(image: np.ndarray):
def sharpness_edge(image: np.ndarray) -> np.float:
"""Return a sharpness metric optimised for vertical lines"""
gray = np.mean(image.astype(float), 2)
n = 20
edge = np.array([[-1] * n + [1] * n])
gray: np.float = np.mean(image.astype(float), 2)
n: int = 20
edge: np.ndarray = np.array([[-1] * n + [1] * n])
return np.sum(
[np.sum(ndimage.filters.convolve(gray, W) ** 2) for W in [edge, edge.T]]
)
@ -129,184 +147,203 @@ def sharpness_edge(image: np.ndarray):
### Autofocus extension
def measure_sharpness(microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2):
"""Measure the sharpness of the camera's current view."""
class AutofocusExtension(BaseExtension):
def __init__(self):
super().__init__(
"org.openflexure.autofocus",
version="2.0.0",
description="Actions to move the microscope in Z and pick the point with the sharpest image.",
)
self.add_view(
MeasureSharpnessAPI, "/measure_sharpness", endpoint="measure_sharpness"
)
self.add_view(AutofocusAPI, "/autofocus", endpoint="autofocus")
self.add_view(FastAutofocusAPI, "/fast_autofocus", endpoint="fast_autofocus")
self.add_view(
UpDownUpAutofocusAPI, "/updownup_autofocus", endpoint="updownup_autofocus"
)
if hasattr(microscope.camera, "array") and callable(
getattr(microscope.camera, "array")
):
return metric_fn(getattr(microscope.camera, "array")(use_video_port=True))
else:
raise RuntimeError(f"Object {microscope.camera} has no method `array`")
self.add_view(
MoveAndMeasureAPI, "/move_and_measure", endpoint="move_and_measure"
)
def measure_sharpness(
self, microscope: Microscope, metric_fn: Callable = sharpness_sum_lap2
) -> np.float:
"""Measure the sharpness of the camera's current view."""
def autofocus(
microscope: Microscope,
dz: List[int],
settle: float = 0.5,
metric_fn: Callable = sharpness_sum_lap2,
):
"""Perform a simple autofocus routine.
The stage is moved to z positions (relative to current position) in dz,
and at each position an image is captured and the sharpness function
evaulated. We then move back to the position where the sharpness was
highest. No interpolation is performed.
dz is assumed to be in ascending order (starting at -ve values)
"""
camera = microscope.camera
stage = microscope.stage
if hasattr(microscope.camera, "array") and callable(
getattr(microscope.camera, "array")
):
return metric_fn(getattr(microscope.camera, "array")(use_video_port=True))
else:
raise RuntimeError(f"Object {microscope.camera} has no method `array`")
with set_properties(stage, backlash=256), stage.lock, camera.lock:
sharpnesses = []
positions = []
def autofocus(
self,
microscope: Microscope,
dz: List[int],
settle: float = 0.5,
metric_fn: Callable = sharpness_sum_lap2,
) -> Tuple[List[int], List[np.float]]:
"""Perform a simple autofocus routine.
The stage is moved to z positions (relative to current position) in dz,
and at each position an image is captured and the sharpness function
evaulated. We then move back to the position where the sharpness was
highest. No interpolation is performed.
dz is assumed to be in ascending order (starting at -ve values)
"""
camera: BaseCamera = microscope.camera
stage: BaseStage = microscope.stage
# Some cameras may not have annotate_text. Reset if it does
if getattr(camera, "annotate_text"):
setattr(camera, "annotate_text", "")
with set_properties(stage, backlash=256), stage.lock, camera.lock:
sharpnesses: List[np.float] = []
positions: List[int] = []
for _ in stage.scan_z(dz, return_to_start=False):
if current_action() and current_action().stopped:
return
positions.append(stage.position[2])
time.sleep(settle)
sharpnesses.append(measure_sharpness(microscope, metric_fn))
# Some cameras may not have annotate_text. Reset if it does
if getattr(camera, "annotate_text", None):
setattr(camera, "annotate_text", "")
newposition = positions[np.argmax(sharpnesses)]
stage.move_rel((0, 0, newposition - stage.position[2]))
for _ in stage.scan_z(dz, return_to_start=False):
if current_action() and current_action().stopped:
return [], []
positions.append(stage.position[2])
time.sleep(settle)
sharpnesses.append(self.measure_sharpness(microscope, metric_fn))
return positions, sharpnesses
newposition: int = positions[int(np.argmax(sharpnesses))]
stage.move_rel((0, 0, newposition - stage.position[2]))
return positions, sharpnesses
@contextmanager
def monitor_sharpness(microscope: Microscope):
m = JPEGSharpnessMonitor(microscope)
m.start()
try:
yield m
finally:
m.stop()
def move_and_find_focus(microscope: Microscope, dz: int):
"""Make a relative Z move and return the peak sharpness position"""
with monitor_sharpness(microscope) as m:
m.focus_rel(dz)
return m.sharpest_z_on_move(0)
def move_and_measure(microscope: Microscope, dz: int):
"""Make a relative Z move and return the sharpness data"""
with monitor_sharpness(microscope) as m:
m.focus_rel(dz)
return m.data_dict()
def fast_autofocus(microscope: Microscope, dz: int = 2000):
"""Perform a down-up-down-up autofocus"""
with microscope.camera.lock, microscope.stage.lock:
def move_and_find_focus(self, microscope: Microscope, dz: int) -> int:
"""Make a relative Z move and return the peak sharpness position"""
with monitor_sharpness(microscope) as m:
# Move to (-dz / 2)
m.focus_rel(-dz / 2)
# Move to dz while monitoring sharpness
# i: Sharpness monitor index for this move
# z: Final z position after move
i, z = m.focus_rel(dz)
# Get the z position with highest sharpness from the previous move (index i)
fz = m.sharpest_z_on_move(i)
# Move all the way to the start so it's consistent
# Store final absolute z position from this return move
i, z = m.focus_rel(-dz)
# Move to the target position fz
# Can't do absolute move here yet so move by (fz - z)
m.focus_rel(fz - z)
# Return all focus data
m.focus_rel(dz)
return m.sharpest_z_on_move(0)
def move_and_measure(
self, microscope: Microscope, dz: int
) -> Dict[str, np.ndarray]:
"""Make a relative Z move and return the sharpness data"""
with monitor_sharpness(microscope) as m:
m.focus_rel(dz)
return m.data_dict()
def fast_autofocus(
self, microscope: Microscope, dz: int = 2000
) -> Dict[str, np.ndarray]:
"""Perform a down-up-down-up autofocus"""
with microscope.camera.lock, microscope.stage.lock:
with monitor_sharpness(microscope) as m:
# Move to (-dz / 2)
m.focus_rel(-dz / 2)
# Move to dz while monitoring sharpness
# i: Sharpness monitor index for this move
# z: Final z position after move
i, z = m.focus_rel(dz)
# Get the z position with highest sharpness from the previous move (index i)
fz: int = m.sharpest_z_on_move(i)
# Move all the way to the start so it's consistent
# Store final absolute z position from this return move
i, z = m.focus_rel(-dz)
# Move to the target position fz
# Can't do absolute move here yet so move by (fz - z)
m.focus_rel(fz - z)
# Return all focus data
return m.data_dict()
def fast_up_down_up_autofocus(
microscope: Microscope,
dz: int = 2000,
target_z: int = 0,
initial_move_up: bool = True,
mini_backlash: int = 25,
):
"""Autofocus by measuring on the way down, and moving back up with feedback.
def fast_up_down_up_autofocus(
self,
microscope: Microscope,
dz: int = 2000,
target_z: int = 0,
initial_move_up: bool = True,
mini_backlash: int = 25,
) -> Dict[str, np.ndarray]:
"""Autofocus by measuring on the way down, and moving back up with feedback.
This autofocus method is very efficient, as it only passes the peak once.
The sequence of moves it performs is:
This autofocus method is very efficient, as it only passes the peak once.
The sequence of moves it performs is:
1. Move to the top of the range `dz/2` (can be disabled)
1. Move to the top of the range `dz/2` (can be disabled)
2. Move down by `dz` while monitoring JPEG size to find the focus.
2. Move down by `dz` while monitoring JPEG size to find the focus.
3. Move back up to the `target_z` position, relative to the sharpest image.
3. Move back up to the `target_z` position, relative to the sharpest image.
4. Measure the sharpness, and compare against the curve recorded in (2) to \\
estimate how much further we need to go. Make this move, to reach our \\
target position.
4. Measure the sharpness, and compare against the curve recorded in (2) to \\
estimate how much further we need to go. Make this move, to reach our \\
target position.
Moving back to the target position in two steps allows us to correct for
backlash, by using the sharpness-vs-z curve as a rough encoder for Z.
Moving back to the target position in two steps allows us to correct for
backlash, by using the sharpness-vs-z curve as a rough encoder for Z.
Parameters:
dz: number of steps over which to scan (optional, default 2000)
Parameters:
dz: number of steps over which to scan (optional, default 2000)
target_z: we aim to finish at this position, relative to focus. This may
be useful if, for example, you want to acquire a stack of images in Z.
It is optional, and the default value of 0 will finish at the focus.
target_z: we aim to finish at this position, relative to focus. This may
be useful if, for example, you want to acquire a stack of images in Z.
It is optional, and the default value of 0 will finish at the focus.
initial_move_up: (optional, default True) set this to `False` to move down
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.
initial_move_up: (optional, default True) set this to `False` to move down
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 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
may cause you to overshoot, which is a problem.
"""
with microscope.camera.lock, microscope.stage.lock:
with monitor_sharpness(microscope) as m:
# Ensure the MJPEG stream has started
microscope.camera.start_stream()
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
may cause you to overshoot, which is a problem.
"""
with microscope.camera.lock, microscope.stage.lock:
with monitor_sharpness(microscope) as m:
# Ensure the MJPEG stream has started
microscope.camera.start_stream()
logging.debug("Initial move")
if initial_move_up:
m.focus_rel(dz / 2)
# move down
logging.debug("Move down")
i, z = m.focus_rel(-dz)
# now inspect where the sharpest point is, and estimate the sharpness
# (JPEG size) that we should find at the start of the Z stack
_, jz, js = m.move_data(i)
best_z = jz[np.argmax(js)]
logging.debug("Initial move")
if initial_move_up:
m.focus_rel(dz / 2)
# move down
logging.debug("Move down")
i: int
z: int
i, z = m.focus_rel(-dz)
# now inspect where the sharpest point is, and estimate the sharpness
# (JPEG size) that we should find at the start of the Z stack
jz: np.ndarray # np.ndarray[float]
js: np.ndarray # np.ndarray[float]
_, jz, js = m.move_data(i)
best_z: int = jz[np.argmax(js)]
# now move to the start of the z stack
logging.debug("Move to the start of the z stack")
i, z = m.focus_rel(
best_z + target_z - z + mini_backlash
) # takes us to the start of the stack
# now move to the start of the z stack
logging.debug("Move to the start of the z stack")
i, z = m.focus_rel(
best_z + target_z - z + mini_backlash
) # takes us to the start of the stack
# We've deliberately undershot - figure out how much further we should move based on the curve
logging.debug("Calculate remining movement")
current_js = m.camera.stream.last.size
imax = np.argmax(js) # we want to crop out just the bit below the peak
js = js[imax:] # NB z is in DECREASING order
jz = jz[imax:]
inow = np.argmax(
js < current_js
) # use the curve we recorded to estimate our position
# We've deliberately undershot - figure out how much further we should move based on the curve
logging.debug("Calculate remining movement")
current_js = m.camera.stream.last.size
imax: int = int(
np.argmax(js)
) # we want to crop out just the bit below the peak
js = js[imax:] # NB z is in DECREASING order
jz = jz[imax:]
inow: int = int(
np.argmax(js < current_js)
) # use the curve we recorded to estimate our position
# So, the Z position corresponding to our current sharpness value is zs[inow]
# That means we should move forwards, by best_z - zs[inow]
logging.debug("Correction move")
correction_move = best_z + target_z - jz[inow]
logging.debug(
"Fast autofocus scan: correcting backlash by moving %s steps",
(correction_move),
)
m.focus_rel(correction_move)
return m.data_dict()
# So, the Z position corresponding to our current sharpness value is zs[inow]
# That means we should move forwards, by best_z - zs[inow]
logging.debug("Correction move")
correction_move: int = best_z + target_z - jz[inow]
logging.debug(
"Fast autofocus scan: correcting backlash by moving %s steps",
(correction_move),
)
m.focus_rel(correction_move)
return m.data_dict()
class MeasureSharpnessAPI(View):
@ -316,7 +353,7 @@ class MeasureSharpnessAPI(View):
if not microscope:
abort(503, "No microscope connected. Unable to measure sharpness.")
return {"sharpness": measure_sharpness(microscope)}
return {"sharpness": self.extension.measure_sharpness(microscope)}
class MoveAndMeasureAPI(ActionView):
@ -335,7 +372,7 @@ class MoveAndMeasureAPI(ActionView):
if microscope.has_real_stage():
# Acquire microscope lock with 1s timeout
with microscope.camera.lock, microscope.stage.lock:
return move_and_measure(microscope, dz=args.get("dz"))
return self.extension.move_and_measure(microscope, dz=args.get("dz"))
else:
abort(503, "No stage connected. Unable to autofocus.")
@ -361,7 +398,7 @@ class AutofocusAPI(ActionView):
logging.debug("Running autofocus...")
# return a handle on the autofocus task
return autofocus(microscope, dz)
return self.extension.autofocus(microscope, dz)
else:
abort(503, "No stage connected. Unable to autofocus.")
@ -385,7 +422,7 @@ class FastAutofocusAPI(ActionView):
# Acquire microscope lock with 1s timeout
with microscope.lock(timeout=1):
# Run fast_autofocus
return fast_autofocus(microscope, dz=args.get("dz"))
return self.extension.fast_autofocus(microscope, dz=args.get("dz"))
else:
abort(503, "No stage connected. Unable to autofocus.")
@ -417,38 +454,9 @@ class UpDownUpAutofocusAPI(ActionView):
# Acquire microscope lock with 1s timeout
with microscope.lock(timeout=1):
# Run fast_up_down_up_autofocus
return fast_up_down_up_autofocus(
return self.extension.fast_up_down_up_autofocus(
microscope, dz=dz, mini_backlash=backlash
)
else:
abort(503, "No stage connected. Unable to autofocus.")
autofocus_extension_v2 = BaseExtension(
"org.openflexure.autofocus",
version="2.0.0",
description="Actions to move the microscope in Z and pick the point with the sharpest image.",
)
autofocus_extension_v2.add_method(fast_autofocus, "fast_autofocus")
autofocus_extension_v2.add_method(
fast_up_down_up_autofocus, "fast_up_down_up_autofocus"
)
autofocus_extension_v2.add_method(autofocus, "autofocus")
autofocus_extension_v2.add_method(move_and_measure, "move_and_measure")
autofocus_extension_v2.add_view(
MeasureSharpnessAPI, "/measure_sharpness", endpoint="measure_sharpness"
)
autofocus_extension_v2.add_view(AutofocusAPI, "/autofocus", endpoint="autofocus")
autofocus_extension_v2.add_view(
FastAutofocusAPI, "/fast_autofocus", endpoint="fast_autofocus"
)
autofocus_extension_v2.add_view(
UpDownUpAutofocusAPI, "/updownup_autofocus", endpoint="updownup_autofocus"
)
autofocus_extension_v2.add_view(
MoveAndMeasureAPI, "/move_and_measure", endpoint="move_and_measure"
)

View file

@ -93,12 +93,43 @@ class AutostorageExtension(BaseExtension):
# We'll store a reference to a CaptureManager object, who's capture paths will be modified
self.capture_manager: Optional[CaptureManager] = None
self.initial_location: str = get_default_location()
# Register the on_microscope function to run when the microscope is attached
self.on_component("org.openflexure.microscope", self.on_microscope)
self.add_view(GetLocationsView, "/list-locations")
self.add_view(PreferredLocationView, "/location")
self.add_view(PreferredLocationGUIView, "/location-from-title")
self.add_meta("gui", build_gui(self.dynamic_form, self))
def dynamic_form(self):
self.check_location()
return {
"icon": "sd_storage",
"title": "Storage",
"viewPanel": "gallery",
"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": self.get_titles(),
"value": self.get_preferred_title(),
}
],
}
],
}
def on_microscope(self, microscope_obj: Microscope):
"""Function to automatically call when the parent LabThing has a microscope attached."""
logging.debug("Autostorage extension found microscope %s", microscope_obj)
@ -204,21 +235,18 @@ class AutostorageExtension(BaseExtension):
return self.key_to_title(self.get_preferred_key())
autostorage_extension_v2 = AutostorageExtension()
class GetLocationsView(PropertyView):
def get(self):
autostorage_extension_v2.check_location()
return autostorage_extension_v2.get_locations()
self.extension.check_location()
return self.extension.get_locations()
class PreferredLocationView(PropertyView):
schema = fields.String(required=True, example="Default")
def get(self):
autostorage_extension_v2.check_location()
return autostorage_extension_v2.get_preferred_key()
self.extension.check_location()
return self.extension.get_preferred_key()
def post(self, new_path_key):
microscope = find_component("org.openflexure.microscope")
@ -226,8 +254,8 @@ class PreferredLocationView(PropertyView):
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)
self.extension.check_location()
self.extension.set_preferred_key(new_path_key)
microscope.save_settings()
@ -237,46 +265,10 @@ class PreferredLocationGUIView(View):
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)
new_path_key = self.extension.title_to_key(new_path_title)
logging.debug(new_path_key)
autostorage_extension_v2.check_location()
autostorage_extension_v2.set_preferred_key(new_path_key)
self.extension.check_location()
self.extension.set_preferred_key(new_path_key)
return new_path_title
def dynamic_form():
autostorage_extension_v2.check_location()
return {
"icon": "sd_storage",
"title": "Storage",
"viewPanel": "gallery",
"forms": [
{
"name": "Autostorage",
"isCollapsible": False,
"isTask": False,
"route": "/location-from-title",
"emitOnResponse": "globalUpdateCaptures",
"submitLabel": "Set path",
"schema": [
{
"fieldType": "selectList",
"name": "new_path_title",
"label": "Capture storage path",
"options": autostorage_extension_v2.get_titles(),
"value": autostorage_extension_v2.get_preferred_title(),
}
],
}
],
}
autostorage_extension_v2.add_view(GetLocationsView, "/list-locations")
autostorage_extension_v2.add_view(PreferredLocationView, "/location")
autostorage_extension_v2.add_view(PreferredLocationGUIView, "/location-from-title")
autostorage_extension_v2.add_meta(
"gui", build_gui(dynamic_form, autostorage_extension_v2)
)

View file

@ -0,0 +1,351 @@
"""
OpenFlexure Microscope API extension for stage calibration
This file contains the HTTP API for camera/stage calibration. It
includes calibration functions that measure the relationship between
stage coordinates and camera coordinates, as well as functions that
move by a specified displacement in pixels, perform closed-loop moves,
and return the calibration data.
This module is only intended to be called from the OpenFlexure Microscope
server, and depends on that server and its underlying LabThings library.
"""
import io
import json
import logging
import os
import time
from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple
import numpy as np
import PIL
from camera_stage_mapping.camera_stage_calibration_1d import (
calibrate_backlash_1d,
image_to_stage_displacement_from_1d,
)
from camera_stage_mapping.camera_stage_tracker import Tracker
from camera_stage_mapping.closed_loop_move import closed_loop_move, closed_loop_scan
from camera_stage_mapping.scan_coords_times import ordered_spiral
from labthings import fields
from labthings.extensions import BaseExtension
from labthings.find import find_component
from labthings.utilities import create_from_path, get_by_path, set_by_path
from labthings.views import ActionView, PropertyView
from openflexure_microscope.config import JSONEncoder
from openflexure_microscope.microscope import Microscope
from openflexure_microscope.paths import data_file_path
CSM_DATAFILE_NAME = "csm_calibration.json"
CSM_DATAFILE_PATH = data_file_path(CSM_DATAFILE_NAME)
CoordinateType = Tuple[float, float, float]
XYCoordinateType = Tuple[float, float]
class MoveHistory(NamedTuple):
times: List[float]
stage_positions: List[CoordinateType]
class LoggingMoveWrapper:
"""Wrap a move function, and maintain a log position/time.
This class is callable, so it doesn't change the signature
of the function it wraps - it just makes it possible to get
a list of all the moves we've made, and how long they took.
Said list is intended to be useful for calibrating the stage
so we can estimate how long moves will take.
"""
def __init__(self, move_function: Callable):
self._move_function: Callable = move_function
self._current_position: Optional[CoordinateType] = None
self.clear_history()
def __call__(self, new_position: CoordinateType, *args, **kwargs):
"""Move to a new position, and record it"""
self._history.append((time.time(), self._current_position))
self._move_function(new_position, *args, **kwargs)
self._current_position = new_position
self._history.append((time.time(), self._current_position))
@property
def history(self) -> MoveHistory:
"""The history, as a numpy array of times and another of positions"""
times: List[float] = [t for t, p in self._history if p is not None]
positions: List[CoordinateType] = [p for t, p in self._history if p is not None]
return MoveHistory(times, positions)
def clear_history(self):
"""Reset our history to be an empty list"""
self._history: List[Tuple[float, Optional[CoordinateType]]] = []
class CSMExtension(BaseExtension):
"""
Use the camera as an encoder, so we can relate camera and stage coordinates
"""
def __init__(self):
BaseExtension.__init__(
self, "org.openflexure.camera_stage_mapping", version="0.0.1"
)
self.add_view(Calibrate1DView, "/calibrate_1d", endpoint="calibrate_1d")
self.add_view(CalibrateXYView, "/calibrate_xy", endpoint="calibrate_xy")
self.add_view(
MoveInImageCoordinatesView,
"/move_in_image_coordinates",
endpoint="move_in_image_coordinates",
)
self.add_view(
ClosedLoopMoveInImageCoordinatesView,
"/closed_loop_move_in_image_coordinates",
)
self.add_view(
TestClosedLoopSpiralScanView,
"/test_closed_loop_spiral_scan",
endpoint="test_closed_loop_spiral_scan",
)
self.add_view(
GetCalibrationFile, "/get_calibration", endpoint="get_calibration"
)
_microscope: Optional[Microscope] = None
@property
def microscope(self):
# TODO: does caching the microscope actually help?
if self._microscope is None:
self._microscope = find_component("org.openflexure.microscope")
return self._microscope
def update_settings(self, settings):
"""Update the stored extension settings dictionary"""
keys: List[str] = ["extensions", self.name]
dictionary: dict = create_from_path(keys)
set_by_path(dictionary, keys, settings)
logging.info("Updating settings with %s", dictionary)
self.microscope.update_settings(dictionary)
self.microscope.save_settings()
def get_settings(self) -> Dict[str, Any]:
"""Retrieve the settings for this extension"""
keys: List[str] = ["extensions", self.name]
return get_by_path(self.microscope.read_settings(), keys)
def camera_stage_functions(self) -> Tuple[Callable, Callable, Callable, Callable]:
"""Return functions that allow us to interface with the microscope"""
def grab_image():
jpeg: bytes = self.microscope.camera.get_frame()
return np.array(PIL.Image.open(io.BytesIO(jpeg)))
def get_position() -> CoordinateType:
return self.microscope.stage.position
move: Callable = self.microscope.stage.move_abs
def wait():
time.sleep(0.2)
return grab_image, get_position, move, wait
def calibrate_1d(self, direction: Tuple[float, float, float]) -> dict:
"""Move a microscope's stage in 1D, and figure out the relationship with the camera"""
grab_image: Callable
get_position: Callable
move: Callable
wait: Callable
grab_image, get_position, move, wait = self.camera_stage_functions()
move = LoggingMoveWrapper(move) # log positions and times for stage calibration
tracker = Tracker(grab_image, get_position, settle=wait)
direction_array: np.ndarray = np.array(direction)
result: dict = calibrate_backlash_1d(tracker, move, direction_array)
result["move_history"] = move.history
return result
def calibrate_xy(self) -> Dict[str, dict]:
"""Move the microscope's stage in X and Y, to calibrate its relationship to the camera"""
logging.info("Calibrating X axis:")
cal_x: dict = self.calibrate_1d((1, 0, 0))
logging.info("Calibrating Y axis:")
cal_y: dict = self.calibrate_1d((0, 1, 0))
# Combine X and Y calibrations to make a 2D calibration
cal_xy: dict = image_to_stage_displacement_from_1d([cal_x, cal_y])
self.update_settings(cal_xy)
data: Dict[str, dict] = {
"camera_stage_mapping_calibration": cal_xy,
"linear_calibration_x": cal_x,
"linear_calibration_y": cal_y,
}
with open(CSM_DATAFILE_PATH, "w") as f:
json.dump(data, f, cls=JSONEncoder)
return data
@property
def image_to_stage_displacement_matrix(self) -> np.ndarray: # 2x2 integer array
"""A 2x2 matrix that converts displacement in image coordinates to stage coordinates."""
displacement_matrix = self.get_settings().get("image_to_stage_displacement")
if not displacement_matrix:
raise ValueError("The microscope has not yet been calibrated.")
return np.array(displacement_matrix)
def move_in_image_coordinates(self, displacement_in_pixels: XYCoordinateType):
"""Move by a given number of pixels on the camera"""
relative_move: np.ndarray = np.dot(
np.array(displacement_in_pixels), self.image_to_stage_displacement_matrix
)
self.microscope.stage.move_rel([relative_move[0], relative_move[1], 0])
def closed_loop_move_in_image_coordinates(
self, displacement_in_pixels: XYCoordinateType, **kwargs
):
"""Move by a given number of pixels on the camera, using the camera as an encoder."""
grab_image, get_position, _, wait = self.camera_stage_functions()
tracker = Tracker(grab_image, get_position, settle=wait)
tracker.acquire_template()
closed_loop_move(
tracker,
self.move_in_image_coordinates,
np.array(displacement_in_pixels),
**kwargs
)
def closed_loop_scan(
self, scan_path: List[XYCoordinateType], **kwargs
) -> List[CoordinateType]:
"""Perform closed-loop moves to each point defined in scan_path.
This returns a generator, which will move the stage to each point in
``scan_path``, then yield ``i, pos`` where ``i``
is the index of the scan point, and ``pos`` is the estimated position
in pixels relative to the starting point. To use it properly, you
should iterate over it, for example::
for i, pos in self.extension.closed_loop_scan(scan_path):
capture_image(f"image_{i}.jpg")
``scan_path`` should be an Nx2 array defining
the points to visit in pixels relative to the current position.
If an exception occurs during the scan, we automatically return to the
starting point. Keyword arguments are passed to
``closed_loop_move.closed_loop_scan``.
"""
grab_image, get_position, move, wait = self.camera_stage_functions()
tracker = Tracker(grab_image, get_position, settle=wait)
tracker.acquire_template()
return closed_loop_scan(
tracker, self.move_in_image_coordinates, move, np.array(scan_path), **kwargs
)
def test_closed_loop_spiral_scan(
self, step_size: Tuple[int, int], N: int, **kwargs
):
"""Move the microscope in a spiral scan, and return the positions."""
scan_path: List[XYCoordinateType] = ordered_spiral(0, 0, N, *step_size)
for _ in self.closed_loop_scan(scan_path, **kwargs):
pass
class Calibrate1DView(ActionView):
args = {"direction": fields.List(fields.Float(), required=True, example=[1, 0, 0])}
def post(self, args):
"""Calibrate one axis of the microscope stage against the camera."""
direction: Tuple[float, float, float] = args.get("direction")
return self.extension.calibrate_1d(direction)
class CalibrateXYView(ActionView):
def post(self):
"""Calibrate both axes of the microscope stage against the camera."""
return self.extension.calibrate_xy()
class MoveInImageCoordinatesView(ActionView):
args = {
"x": fields.Float(
description="The number of pixels to move in X", required=True, example=100
),
"y": fields.Float(
description="The number of pixels to move in Y", required=True, example=100
),
}
def post(self, args):
"""Move the microscope stage, such that we move by a given number of pixels on the camera"""
logging.debug("moving in pixels")
self.extension.move_in_image_coordinates((args.get("x"), args.get("y")))
return self.extension.microscope.state["stage"]["position"]
class ClosedLoopMoveInImageCoordinatesView(ActionView):
args = {
"x": fields.Float(
description="The number of pixels to move in X", required=True, example=100
),
"y": fields.Float(
description="The number of pixels to move in Y", required=True, example=100
),
}
def post(self, args):
"""Move the microscope stage, such that we move by a given number of pixels on the camera"""
logging.debug("moving in pixels")
self.extension.closed_loop_move_in_image_coordinates(
(args.get("x"), args.get("y"))
)
return self.extension.microscope.state["stage"]["position"]
class TestClosedLoopSpiralScanView(ActionView):
args = {
"x_step": fields.Float(
description="The number of pixels to move in X", required=True, example=100
),
"y_step": fields.Float(
description="The number of pixels to move in Y", required=True, example=100
),
"N": fields.Int(
description="The number of rings in the spiral scan",
required=True,
example=3,
),
}
def post(self, args):
"""Move the microscope stage, such that we move by a given number of pixels on the camera"""
logging.debug("moving in pixels")
return self.extension.test_closed_loop_spiral_scan(
(args.get("x"), args.get("y")), args.get("N")
)
class GetCalibrationFile(PropertyView):
def get(self):
"""Get the calibration data in JSON format."""
datafile_path = CSM_DATAFILE_PATH
if os.path.isfile(datafile_path):
with open(datafile_path, "rb") as f:
return json.load(f)
else:
return {}

View file

@ -1 +1 @@
from .extension import lst_extension_v2
from .extension import LSTExtension

View file

@ -42,23 +42,43 @@ def pause_stream(scamera: BaseCamera):
scamera.start_stream()
def recalibrate(microscope: Microscope):
"""Reset the camera's settings.
class LSTExtension(BaseExtension):
def __init__(self) -> None:
super().__init__(
"org.openflexure.calibration.picamera",
version="2.0.0-beta.1",
description="Routines to perform flat-field correction on the camera.",
)
This generates new gains, exposure time, and lens shading
table such that the background is as uniform as possible
with a gray level of 230. It takes a little while to run.
"""
with pause_stream(microscope.camera) as scamera:
if hasattr(scamera, "picamera"):
picamera_obj: picamerax.PiCamera = getattr(scamera, "picamera")
auto_expose_and_freeze_settings(picamera_obj)
recalibrate_camera(picamera_obj)
microscope.save_settings()
else:
raise RuntimeError(
"Recalibrate can only be used with a Raspberry Pi camera"
)
self.add_view(RecalibrateView, "/recalibrate", endpoint="recalibrate")
self.add_view(
FlattenLSTView,
"/flatten_lens_shading_table",
endpoint="flatten_lens_shading_table",
)
self.add_view(
DeleteLSTView,
"/delete_lens_shading_table",
endpoint="delete_lens_shading_table",
)
def recalibrate(self, microscope: Microscope):
"""Reset the camera's settings.
This generates new gains, exposure time, and lens shading
table such that the background is as uniform as possible
with a gray level of 230. It takes a little while to run.
"""
with pause_stream(microscope.camera) as scamera:
if hasattr(scamera, "picamera"):
picamera_obj: picamerax.PiCamera = getattr(scamera, "picamera")
auto_expose_and_freeze_settings(picamera_obj)
recalibrate_camera(picamera_obj)
microscope.save_settings()
else:
raise RuntimeError(
"Recalibrate can only be used with a Raspberry Pi camera"
)
class RecalibrateView(ActionView):
@ -70,7 +90,7 @@ class RecalibrateView(ActionView):
logging.info("Starting microscope recalibration...")
return recalibrate(microscope)
return self.extension.recalibrate(microscope)
class FlattenLSTView(ActionView):
@ -102,22 +122,3 @@ class DeleteLSTView(ActionView):
with pause_stream(microscope.camera) as scamera:
scamera.camera.lens_shading_table = None
microscope.save_settings()
lst_extension_v2 = BaseExtension(
"org.openflexure.calibration.picamera",
version="2.0.0-beta.1",
description="Routines to perform flat-field correction on the camera.",
)
lst_extension_v2.add_method(
recalibrate, "org.openflexure.calibration.picamera.recalibrate"
)
lst_extension_v2.add_view(RecalibrateView, "/recalibrate", endpoint="recalibrate")
lst_extension_v2.add_view(
FlattenLSTView, "/flatten_lens_shading_table", endpoint="flatten_lens_shading_table"
)
lst_extension_v2.add_view(
DeleteLSTView, "/delete_lens_shading_table", endpoint="delete_lens_shading_table"
)

View file

@ -8,14 +8,16 @@ from picamerax import PiCamera
from picamerax.array import PiBayerArray, PiRGBArray
def rgb_image(camera: PiCamera, resize: Optional[Tuple[int, int]] = None, **kwargs):
def rgb_image(
camera: PiCamera, resize: Optional[Tuple[int, int]] = None, **kwargs
) -> PiRGBArray:
"""Capture an image and return an RGB numpy array"""
with PiRGBArray(camera, size=resize) as output:
camera.capture(output, format="rgb", resize=resize, **kwargs)
return output.array
def flat_lens_shading_table(camera: PiCamera):
def flat_lens_shading_table(camera: PiCamera) -> np.ndarray:
"""Return a flat (i.e. unity gain) lens shading table.
This is mostly useful because it makes it easy to get the size
@ -78,10 +80,12 @@ def auto_expose_and_freeze_settings(camera: PiCamera):
def channels_from_bayer_array(bayer_array: np.ndarray) -> np.ndarray:
"""Given the 'array' from a PiBayerArray, return the 4 channels."""
bayer_pattern: List[Tuple[int, int]] = [(0, 0), (0, 1), (1, 0), (1, 1)]
channels: np.ndarray = np.zeros(
(4, bayer_array.shape[0] // 2, bayer_array.shape[1] // 2),
dtype=bayer_array.dtype,
channels_shape: Tuple[int, ...] = (
4,
bayer_array.shape[0] // 2,
bayer_array.shape[1] // 2,
)
channels: np.ndarray = np.zeros(channels_shape, dtype=bayer_array.dtype)
for i, offset in enumerate(bayer_pattern):
# We simplify life by dealing with only one channel at a time.
channels[i, :, :] = np.sum(
@ -105,9 +109,13 @@ def lst_from_channels(channels: np.ndarray) -> np.ndarray:
[channels.shape[0]] + lst_resolution, dtype=np.float
)
for i in range(lens_shading.shape[0]):
image_channel = channels[i, :, :]
image_channel: np.ndarray = channels[i, :, :]
iw: int
ih: int
iw, ih = image_channel.shape
ls_channel = lens_shading[i, :, :]
ls_channel: np.ndarray = lens_shading[i, :, :]
lw: int
lh: int
lw, lh = ls_channel.shape
# The lens shading table is rounded **up** in size to 1/64th of the size of
# the image. Rather than handle edge images separately, I'm just going to
@ -116,7 +124,7 @@ def lst_from_channels(channels: np.ndarray) -> np.ndarray:
# half the size of the full image - remember the Bayer pattern... This
# should give results very close to 6by9's solution, albeit considerably
# less computationally efficient!
padded_image_channel = np.pad(
padded_image_channel: np.ndarray = np.pad(
image_channel, [(0, lw * 32 - iw), (0, lh * 32 - ih)], mode="edge"
) # Pad image to the right and bottom
logging.info(
@ -129,7 +137,7 @@ def lst_from_channels(channels: np.ndarray) -> np.ndarray:
)
# Next, fill the shading table (except edge pixels). Please excuse the
# for loop - I know it's not fast but this code needn't be!
box = 3 # We average together a square of this side length for each pixel.
box: int = 3 # We average together a square of this side length for each pixel.
# NB this isn't quite what 6by9's program does - it averages 3 pixels
# horizontally, but not vertically.
for dx in np.arange(box) - box // 2:
@ -150,10 +158,10 @@ def lst_from_channels(channels: np.ndarray) -> np.ndarray:
# What we actually want to calculate is the gains needed to compensate for the
# lens shading - that's 1/lens_shading_table_float as we currently have it.
gains = 32.0 / lens_shading # 32 is unity gain
gains: np.ndarray = 32.0 / lens_shading # 32 is unity gain
gains[gains > 255] = 255 # clip at 255, maximum gain is 255/32
gains[gains < 32] = 32 # clip at 32, minimum gain is 1 (is this necessary?)
lens_shading_table = gains.astype(np.uint8)
lens_shading_table: np.ndarray = gains.astype(np.uint8)
return lens_shading_table[::-1, :, :].copy()

View file

@ -99,9 +99,12 @@ def construct_grid(
class ScanExtension(BaseExtension):
def __init__(self):
BaseExtension.__init__(self, "org.openflexure.scan", version="2.0.0")
self._images_to_be_captured: int = 1
self._images_captured_so_far: int = 0
BaseExtension.__init__(self, "org.openflexure.scan", version="2.0.0")
self.add_view(TileScanAPI, "/tile", endpoint="tile")
def capture(
self,
@ -356,9 +359,6 @@ class ScanExtension(BaseExtension):
microscope.stage.move_abs(initial_position)
scan_extension_v2 = ScanExtension()
class TileScanArgs(FullCaptureArgs):
namemode = fields.String(missing="coordinates", example="coordinates")
grid = fields.List(fields.Integer, missing=[3, 3, 3], example=[3, 3, 3])
@ -398,7 +398,7 @@ class TileScanAPI(ActionView):
# Acquire microscope lock with 1s timeout
with microscope.lock(timeout=1):
# Run scan_extension_v2
return scan_extension_v2.tile(
return self.extension.tile(
microscope,
basename=args.get("filename"),
namemode=args.get("namemode"),
@ -414,6 +414,3 @@ class TileScanAPI(ActionView):
annotations=args.get("annotations"),
tags=args.get("tags"),
)
scan_extension_v2.add_view(TileScanAPI, "/tile", endpoint="tile")

View file

@ -131,8 +131,18 @@ class ZipManager:
zd.close()
# Create a global ZIP manager
default_zip_manager = ZipManager()
class ZipBuilderExtension(BaseExtension):
def __init__(self):
super().__init__(
"org.openflexure.zipbuilder",
version="2.0.0",
description="Build and download capture collections as ZIP files",
)
self.manager = ZipManager()
self.add_view(ZipGetterAPIView, "/get/<string:session_id>", endpoint="get_id")
self.add_view(ZipListAPIView, "/get", endpoint="get")
self.add_view(ZipBuilderAPIView, "/build", endpoint="build")
class ZipBuilderAPIView(ActionView):
@ -142,7 +152,7 @@ class ZipBuilderAPIView(ActionView):
microscope = find_component("org.openflexure.microscope")
# Return a handle on the autofocus task
return default_zip_manager.marshaled_build_zip_from_capture_ids(
return self.extension.manager.marshaled_build_zip_from_capture_ids(
microscope, args
)
@ -151,7 +161,7 @@ class ZipListAPIView(PropertyView):
schema = ZipObjectSchema(many=True)
def get(self):
return default_zip_manager.session_zips.values()
return self.extension.manager.session_zips.values()
class ZipGetterAPIView(View):
@ -163,13 +173,13 @@ class ZipGetterAPIView(View):
"""
Download a particular capture collection ZIP file
"""
if not session_id in default_zip_manager.session_zips:
if not session_id in self.extension.manager.session_zips:
return abort(404) # 404 Not Found
logging.info("Session ID: %s", session_id)
return send_file(
default_zip_manager.zip_fp_from_id(session_id).name,
self.extension.manager.zip_fp_from_id(session_id).name,
mimetype="application/zip",
as_attachment=True,
attachment_filename=f"{session_id}.zip",
@ -179,26 +189,12 @@ class ZipGetterAPIView(View):
"""
Close and delete a particular capture collection ZIP file
"""
if not session_id in default_zip_manager.session_zips:
if not session_id in self.extension.manager.session_zips:
return abort(404) # 404 Not Found
# Close the file
default_zip_manager.session_zips[session_id].close()
self.extension.manager.session_zips[session_id].close()
# Delete the file reference
del default_zip_manager.session_zips[session_id]
del self.extension.manager.session_zips[session_id]
return {"return": session_id}
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>", endpoint="get_id"
)
zip_extension_v2.add_view(ZipListAPIView, "/get", endpoint="get")
zip_extension_v2.add_view(ZipBuilderAPIView, "/build", endpoint="build")

View file

@ -1 +1,3 @@
from .tools import devtools_extension_v2
from .tools import DevToolsExtension
LABTHINGS_EXTENSIONS = [DevToolsExtension]

View file

@ -6,6 +6,17 @@ from labthings.extensions import BaseExtension
from labthings.views import ActionView
class DevToolsExtension(BaseExtension):
def __init__(self) -> None:
super().__init__(
"org.openflexure.dev.tools",
version="0.1.0",
description="Actions to cause various traumatic events in the microscope, used for testing.",
)
self.add_view(RaiseException, "/raise")
self.add_view(SleepFor, "/sleep")
class RaiseException(ActionView):
def post(self):
raise Exception("The developer raised an exception")
@ -23,13 +34,3 @@ class SleepFor(ActionView):
end = time.time()
logging.info("Waking up!")
return {"TimeAsleep": (end - start)}
devtools_extension_v2 = BaseExtension(
"org.openflexure.dev.tools",
version="0.1.0",
description="Actions to cause various traumatic events in the microscope, used for testing.",
)
devtools_extension_v2.add_view(RaiseException, "/raise")
devtools_extension_v2.add_view(SleepFor, "/sleep")

View file

@ -2308,9 +2308,9 @@
}
},
"@types/express-serve-static-core": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.13.tgz",
"integrity": "sha512-RgDi5a4nuzam073lRGKTUIaL3eF2+H7LJvJ8eUnCI0wA6SNjXc44DCmWNiTLs/AZ7QlsFWZiw/gTG3nSQGL0fA==",
"version": "4.17.14",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.14.tgz",
"integrity": "sha512-uFTLwu94TfUFMToXNgRZikwPuZdOtDgs3syBtAIr/OXorL1kJqUJT9qCLnRZ5KBOWfZQikQ2xKgR2tnDj1OgDA==",
"dev": true,
"requires": {
"@types/node": "*",
@ -2403,9 +2403,9 @@
"dev": true
},
"@types/serve-static": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.7.tgz",
"integrity": "sha512-3diZWucbR+xTmbDlU+FRRxBf+31OhFew7cJXML/zh9NmvSPTNoFecAwHB66BUqFgENJtqMiyl7JAwUE/siqdLw==",
"version": "1.13.8",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz",
"integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==",
"dev": true,
"requires": {
"@types/mime": "*",
@ -2442,9 +2442,9 @@
}
},
"@types/webpack": {
"version": "4.41.24",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.24.tgz",
"integrity": "sha512-1A0MXPwZiMOD3DPMuOKUKcpkdPo8Lq33UGggZ7xio6wJ/jV1dAu5cXDrOfGDnldUroPIRLsr/DT43/GqOA4RFQ==",
"version": "4.41.25",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.25.tgz",
"integrity": "sha512-cr6kZ+4m9lp86ytQc1jPOJXgINQyz3kLLunZ57jznW+WIAL0JqZbGubQk4GlD42MuQL5JGOABrxdpqqWeovlVQ==",
"dev": true,
"requires": {
"@types/anymatch": "*",
@ -2477,9 +2477,9 @@
}
},
"@types/webpack-sources": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.0.0.tgz",
"integrity": "sha512-a5kPx98CNFRKQ+wqawroFunvFqv7GHm/3KOI52NY9xWADgc8smu4R6prt4EU/M4QfVjvgBkMqU4fBhw3QfMVkg==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-2.1.0.tgz",
"integrity": "sha512-LXn/oYIpBeucgP1EIJbKQ2/4ZmpvRl+dlrFdX7+94SKRUV3Evy3FsfMZY318vGhkWUS5MPhtOM3w1/hCOAOXcg==",
"dev": true,
"requires": {
"@types/node": "*",
@ -2594,9 +2594,9 @@
}
},
"@vue/cli-overlay": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/@vue/cli-overlay/-/cli-overlay-4.5.8.tgz",
"integrity": "sha512-M6q4QPKQfErHk54ycxBASgBadgdsK45f6v2NWUTAiFXdTtqv1Z7gR8JAbQhiXbni/m9571bLJnZFv5c8uhAHnw==",
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/@vue/cli-overlay/-/cli-overlay-4.5.9.tgz",
"integrity": "sha512-E2PWv6tCdUz+eEDj2Th2oxiKmzMe02qi0PcxiNaO7oaqggmEOrp1rLgop7DWpiLDBiqUZk2x0vjK/q2Tz8z/eg==",
"dev": true
},
"@vue/cli-plugin-babel": {
@ -2708,18 +2708,18 @@
}
},
"@vue/cli-plugin-router": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-router/-/cli-plugin-router-4.5.8.tgz",
"integrity": "sha512-tJwVLdX4jj0Ia/1hsBejRbo4gs0hb2z1z5ert+4Ve5RNdpOKUG69OaPQgXPqvuYSQh9MW7bqG0iJmYtVD+KBNw==",
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-router/-/cli-plugin-router-4.5.9.tgz",
"integrity": "sha512-eBBfbZpQ1sJrdlx8i7iReFxSnuzwmrv+s2OCT3kjBd6uWRqGnD4VihpS4srC7vZLzDQrDplumSn0a93L9Qf3wQ==",
"dev": true,
"requires": {
"@vue/cli-shared-utils": "^4.5.8"
"@vue/cli-shared-utils": "^4.5.9"
},
"dependencies": {
"@vue/cli-shared-utils": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.8.tgz",
"integrity": "sha512-pa6oenhBO/5HeDLRSokiwVN01gROACEDy3ESXWuPmragOREGNmmFKtkPHlqeYavGEX6LFp7f0VK3uMX6UYS5mQ==",
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.9.tgz",
"integrity": "sha512-anvsrv+rkQC+hgxaT2nQQxnSWSsIzyysZ36LO7qPjXvDRBvcvKLAAviFlUkYbZ+ntbV8puzJ3zw+gUhQw4SEVA==",
"dev": true,
"requires": {
"@hapi/joi": "^15.0.1",
@ -2798,15 +2798,15 @@
}
},
"@vue/cli-plugin-vuex": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.8.tgz",
"integrity": "sha512-wU9WPhay1zBxtdd/HLRYhFRLBbG9lw3YTBJep9sTnYwUeXiEgum4O88Q2j6cwBBPPHMgeMrKMPHS85Jf4hMc0g==",
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/@vue/cli-plugin-vuex/-/cli-plugin-vuex-4.5.9.tgz",
"integrity": "sha512-mFNIJhYiJjzCgytkDHX00ROy5Yzl7prkZpUbeDE0biwcLteMf2s3qZVbESOQl6GcviqcfEt2f3tHQQtLNa+OLg==",
"dev": true
},
"@vue/cli-service": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/@vue/cli-service/-/cli-service-4.5.8.tgz",
"integrity": "sha512-YcVEJvA5vQOcfRzhCQDfHxRW9WHvKqlQeiuSvNddfV2uQqKD3ifhsNYiZZuFvbev9qgRUmtmxSafpwYzj/LLBw==",
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/@vue/cli-service/-/cli-service-4.5.9.tgz",
"integrity": "sha512-E3XlfM0q+UnnjbC9rwLIWNo2umZCRwnlMJY0KOhY1hFvqisGIYzFmQQ4o01KGyTx2BZNMuQg7Kw+BZ5gyM1Wig==",
"dev": true,
"requires": {
"@intervolga/optimize-cssnano-plugin": "^1.0.5",
@ -2815,10 +2815,10 @@
"@types/minimist": "^1.2.0",
"@types/webpack": "^4.0.0",
"@types/webpack-dev-server": "^3.11.0",
"@vue/cli-overlay": "^4.5.8",
"@vue/cli-plugin-router": "^4.5.8",
"@vue/cli-plugin-vuex": "^4.5.8",
"@vue/cli-shared-utils": "^4.5.8",
"@vue/cli-overlay": "^4.5.9",
"@vue/cli-plugin-router": "^4.5.9",
"@vue/cli-plugin-vuex": "^4.5.9",
"@vue/cli-shared-utils": "^4.5.9",
"@vue/component-compiler-utils": "^3.1.2",
"@vue/preload-webpack-plugin": "^1.1.0",
"@vue/web-component-wrapper": "^1.2.0",
@ -2868,9 +2868,9 @@
},
"dependencies": {
"@vue/cli-shared-utils": {
"version": "4.5.8",
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.8.tgz",
"integrity": "sha512-pa6oenhBO/5HeDLRSokiwVN01gROACEDy3ESXWuPmragOREGNmmFKtkPHlqeYavGEX6LFp7f0VK3uMX6UYS5mQ==",
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/@vue/cli-shared-utils/-/cli-shared-utils-4.5.9.tgz",
"integrity": "sha512-anvsrv+rkQC+hgxaT2nQQxnSWSsIzyysZ36LO7qPjXvDRBvcvKLAAviFlUkYbZ+ntbV8puzJ3zw+gUhQw4SEVA==",
"dev": true,
"requires": {
"@hapi/joi": "^15.0.1",
@ -2928,16 +2928,16 @@
}
},
"browserslist": {
"version": "4.14.7",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz",
"integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==",
"version": "4.15.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.15.0.tgz",
"integrity": "sha512-IJ1iysdMkGmjjYeRlDU8PQejVwxvVO5QOfXH7ylW31GO6LwNRSmm/SgRXtNsEXqMLl2e+2H5eEJ7sfynF8TCaQ==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001157",
"caniuse-lite": "^1.0.30001164",
"colorette": "^1.2.1",
"electron-to-chromium": "^1.3.591",
"electron-to-chromium": "^1.3.612",
"escalade": "^3.1.1",
"node-releases": "^1.1.66"
"node-releases": "^1.1.67"
}
},
"cacache": {
@ -2967,9 +2967,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001157",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001157.tgz",
"integrity": "sha512-gOerH9Wz2IRZ2ZPdMfBvyOi3cjaz4O4dgNwPGzx8EhqAs4+2IL/O+fJsbt+znSigujoZG8bVcIAUM/I/E5K3MA==",
"version": "1.0.30001165",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001165.tgz",
"integrity": "sha512-8cEsSMwXfx7lWSUMA2s08z9dIgsnR5NAqjXP23stdsU3AUWkCr/rr4s4OFtHXn5XXr6+7kam3QFVoYyXNPdJPA==",
"dev": true
},
"color-convert": {
@ -2990,9 +2990,9 @@
"optional": true
},
"electron-to-chromium": {
"version": "1.3.592",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.592.tgz",
"integrity": "sha512-kGNowksvqQiPb1pUSQKpd8JFoGPLxYOwduNRCqCxGh/2Q1qE2JdmwouCW41lUzDxOb/2RIV4lR0tVIfboWlO9A==",
"version": "1.3.615",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.615.tgz",
"integrity": "sha512-fNYTQXoUhNc6RmHDlGN4dgcLURSBIqQCN7ls6MuQ741+NJyLNRz8DxAC+pZpOKfRs6cfY0lv2kWdy8Oxf9j4+A==",
"dev": true
},
"emojis-list": {
@ -3083,9 +3083,9 @@
}
},
"node-releases": {
"version": "1.1.66",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz",
"integrity": "sha512-JHEQ1iWPGK+38VLB2H9ef2otU4l8s3yAMt9Xf934r6+ojCYDMHPMqvCc9TnzfeFSP1QEOeU6YZEd3+De0LTCgg==",
"version": "1.1.67",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz",
"integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==",
"dev": true
},
"p-limit": {
@ -3235,9 +3235,9 @@
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.1.0",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.0.tgz",
"integrity": "sha512-fTtCdI7VeyNK0HP4q4y9Z9ts8TUeaF+2/FjKx8CJ/7/Oem1rCX7zIJe+d+jLrVnVNQjENd3gqmANraLcdRWwnQ==",
"version": "npm:vue-loader@16.1.1",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.1.1.tgz",
"integrity": "sha512-wz/+HFg/3SBayHWAlZXARcnDTl3VOChrfW9YnxvAweiuyKX/7IGx1ad/4yJHmwhgWlOVYMAbTiI7GV8G33PfGQ==",
"dev": true,
"optional": true,
"requires": {
@ -3872,34 +3872,34 @@
},
"dependencies": {
"browserslist": {
"version": "4.14.7",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz",
"integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==",
"version": "4.15.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.15.0.tgz",
"integrity": "sha512-IJ1iysdMkGmjjYeRlDU8PQejVwxvVO5QOfXH7ylW31GO6LwNRSmm/SgRXtNsEXqMLl2e+2H5eEJ7sfynF8TCaQ==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001157",
"caniuse-lite": "^1.0.30001164",
"colorette": "^1.2.1",
"electron-to-chromium": "^1.3.591",
"electron-to-chromium": "^1.3.612",
"escalade": "^3.1.1",
"node-releases": "^1.1.66"
"node-releases": "^1.1.67"
}
},
"caniuse-lite": {
"version": "1.0.30001157",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001157.tgz",
"integrity": "sha512-gOerH9Wz2IRZ2ZPdMfBvyOi3cjaz4O4dgNwPGzx8EhqAs4+2IL/O+fJsbt+znSigujoZG8bVcIAUM/I/E5K3MA==",
"version": "1.0.30001165",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001165.tgz",
"integrity": "sha512-8cEsSMwXfx7lWSUMA2s08z9dIgsnR5NAqjXP23stdsU3AUWkCr/rr4s4OFtHXn5XXr6+7kam3QFVoYyXNPdJPA==",
"dev": true
},
"electron-to-chromium": {
"version": "1.3.592",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.592.tgz",
"integrity": "sha512-kGNowksvqQiPb1pUSQKpd8JFoGPLxYOwduNRCqCxGh/2Q1qE2JdmwouCW41lUzDxOb/2RIV4lR0tVIfboWlO9A==",
"version": "1.3.615",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.615.tgz",
"integrity": "sha512-fNYTQXoUhNc6RmHDlGN4dgcLURSBIqQCN7ls6MuQ741+NJyLNRz8DxAC+pZpOKfRs6cfY0lv2kWdy8Oxf9j4+A==",
"dev": true
},
"node-releases": {
"version": "1.1.66",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.66.tgz",
"integrity": "sha512-JHEQ1iWPGK+38VLB2H9ef2otU4l8s3yAMt9Xf934r6+ojCYDMHPMqvCc9TnzfeFSP1QEOeU6YZEd3+De0LTCgg==",
"version": "1.1.67",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz",
"integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==",
"dev": true
}
}
@ -4864,16 +4864,16 @@
}
},
"cli-highlight": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.4.tgz",
"integrity": "sha512-s7Zofobm20qriqDoU9sXptQx0t2R9PEgac92mENNm7xaEe1hn71IIMsXMK+6encA6WRCWWxIGQbipr3q998tlQ==",
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.9.tgz",
"integrity": "sha512-t8RNIZgiI24i/mslZ8XT8o660RUj5ZbUJpEZrZa/BNekTzdC2LfMRAnt0Y7sgzNM4FGW5tmWg/YnbTH8o1eIOQ==",
"dev": true,
"requires": {
"chalk": "^3.0.0",
"highlight.js": "^9.6.0",
"chalk": "^4.0.0",
"highlight.js": "^10.0.0",
"mz": "^2.4.0",
"parse5": "^5.1.1",
"parse5-htmlparser2-tree-adapter": "^5.1.1",
"parse5-htmlparser2-tree-adapter": "^6.0.0",
"yargs": "^15.0.0"
},
"dependencies": {
@ -4887,9 +4887,9 @@
}
},
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
@ -5628,28 +5628,28 @@
"dev": true
},
"csso": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/csso/-/csso-4.1.0.tgz",
"integrity": "sha512-h+6w/W1WqXaJA4tb1dk7r5tVbOm97MsKxzwnvOR04UQ6GILroryjMWu3pmCCtL2mLaEStQ0fZgeGiy99mo7iyg==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz",
"integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==",
"dev": true,
"requires": {
"css-tree": "^1.0.0"
"css-tree": "^1.1.2"
},
"dependencies": {
"css-tree": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0.tgz",
"integrity": "sha512-CdVYz/Yuqw0VdKhXPBIgi8DO3NicJVYZNWeX9XcIuSp9ZoFT5IcleVRW07O5rMjdcx1mb+MEJPknTTEW7DdsYw==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.2.tgz",
"integrity": "sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==",
"dev": true,
"requires": {
"mdn-data": "2.0.12",
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
}
},
"mdn-data": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.12.tgz",
"integrity": "sha512-ULbAlgzVb8IqZ0Hsxm6hHSlQl3Jckst2YEQS7fODu9ilNWy2LvcoSY7TRFIktABP2mdppBioc66va90T+NUs8Q==",
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"dev": true
},
"source-map": {
@ -6089,9 +6089,9 @@
},
"dependencies": {
"domelementtype": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.2.tgz",
"integrity": "sha512-wFwTwCVebUrMgGeAwRL/NhZtHAUyT9n9yg4IMDwf10+6iCMxSkVq9MGCVEH+QZWo1nNidy8kNvwmv4zWHDTqvA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz",
"integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==",
"dev": true
}
}
@ -6620,9 +6620,9 @@
}
},
"eslint-plugin-prettier": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz",
"integrity": "sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.2.0.tgz",
"integrity": "sha512-kOUSJnFjAUFKwVxuzy6sA5yyMx6+o9ino4gCdShzBNx4eyFRudWRYKCFolKjoM40PEiuU6Cn7wBLfq3WsGg7qg==",
"dev": true,
"requires": {
"prettier-linter-helpers": "^1.0.0"
@ -8216,9 +8216,9 @@
"dev": true
},
"highlight.js": {
"version": "9.18.3",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.3.tgz",
"integrity": "sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ==",
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.4.1.tgz",
"integrity": "sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg==",
"dev": true
},
"hmac-drbg": {
@ -9521,9 +9521,9 @@
}
},
"loglevel": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.0.tgz",
"integrity": "sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==",
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz",
"integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==",
"dev": true
},
"loose-envify": {
@ -10206,11 +10206,49 @@
"dev": true
},
"object-is": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz",
"integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.4.tgz",
"integrity": "sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3"
}
},
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true
},
"object-visit": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
"integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
"dev": true,
"requires": {
"isobject": "^3.0.0"
}
},
"object.assign": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
"integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
"dev": true,
"requires": {
"define-properties": "^1.1.2",
"function-bind": "^1.1.1",
"has-symbols": "^1.0.0",
"object-keys": "^1.0.11"
}
},
"object.getownpropertydescriptors": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz",
"integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.1"
},
@ -10251,9 +10289,9 @@
}
},
"object-inspect": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
"integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
"integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==",
"dev": true
},
"object.assign": {
@ -10270,43 +10308,6 @@
}
}
},
"object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true
},
"object-visit": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
"integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=",
"dev": true,
"requires": {
"isobject": "^3.0.0"
}
},
"object.assign": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
"integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
"dev": true,
"requires": {
"define-properties": "^1.1.2",
"function-bind": "^1.1.1",
"has-symbols": "^1.0.0",
"object-keys": "^1.0.11"
}
},
"object.getownpropertydescriptors": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz",
"integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.0-next.1"
}
},
"object.pick": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
@ -10317,15 +10318,70 @@
}
},
"object.values": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz",
"integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.2.tgz",
"integrity": "sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"es-abstract": "^1.17.0-next.1",
"function-bind": "^1.1.1",
"es-abstract": "^1.18.0-next.1",
"has": "^1.0.3"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
"dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.0",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
},
"is-callable": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
"integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==",
"dev": true
},
"is-regex": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
"integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
"dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
},
"object-inspect": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
"integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==",
"dev": true
},
"object.assign": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"has-symbols": "^1.0.1",
"object-keys": "^1.1.1"
}
}
}
},
"obuf": {
@ -10561,12 +10617,20 @@
"dev": true
},
"parse5-htmlparser2-tree-adapter": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-5.1.1.tgz",
"integrity": "sha512-CF+TKjXqoqyDwHqBhFQ+3l5t83xYi6fVT1tQNg+Ye0JRLnTxWvIroCjEp1A0k4lneHNBGnICUf0cfYVYGEazqw==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
"integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
"dev": true,
"requires": {
"parse5": "^5.1.1"
"parse5": "^6.0.1"
},
"dependencies": {
"parse5": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==",
"dev": true
}
}
},
"parseurl": {
@ -10783,9 +10847,9 @@
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
@ -12452,9 +12516,9 @@
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"requires": {
"ms": "^2.1.1"
@ -12770,68 +12834,13 @@
}
},
"string.prototype.trimend": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.2.tgz",
"integrity": "sha512-8oAG/hi14Z4nOVP0z6mdiVZ/wqjDtWSLygMigTzAb+7aPEDTleeFf+WrF+alzecxIRkckkJVn+dTlwzJXORATw==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz",
"integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.1"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
"dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.0",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
},
"is-callable": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
"integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==",
"dev": true
},
"is-regex": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
"integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
"dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
},
"object-inspect": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
"integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==",
"dev": true
},
"object.assign": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"has-symbols": "^1.0.1",
"object-keys": "^1.1.1"
}
}
"call-bind": "^1.0.0",
"define-properties": "^1.1.3"
}
},
"string.prototype.trimleft": {
@ -12855,68 +12864,13 @@
}
},
"string.prototype.trimstart": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.2.tgz",
"integrity": "sha512-7F6CdBTl5zyu30BJFdzSTlSlLPwODC23Od+iLoVH8X6+3fvDPPuBVVj9iaB1GOsSTSIgVfsfm27R2FGrAPznWg==",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz",
"integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==",
"dev": true,
"requires": {
"define-properties": "^1.1.3",
"es-abstract": "^1.18.0-next.1"
},
"dependencies": {
"es-abstract": {
"version": "1.18.0-next.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
"dev": true,
"requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"is-callable": "^1.2.2",
"is-negative-zero": "^2.0.0",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
}
},
"is-callable": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz",
"integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==",
"dev": true
},
"is-regex": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz",
"integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==",
"dev": true,
"requires": {
"has-symbols": "^1.0.1"
}
},
"object-inspect": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
"integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==",
"dev": true
},
"object.assign": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz",
"integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==",
"dev": true,
"requires": {
"call-bind": "^1.0.0",
"define-properties": "^1.1.3",
"has-symbols": "^1.0.1",
"object-keys": "^1.1.1"
}
}
"call-bind": "^1.0.0",
"define-properties": "^1.1.3"
}
},
"string_decoder": {
@ -13387,9 +13341,9 @@
}
},
"uikit": {
"version": "3.5.9",
"resolved": "https://registry.npmjs.org/uikit/-/uikit-3.5.9.tgz",
"integrity": "sha512-0197WceeBMlCLscsmds6zqL8K3TOtdUCvoHPJEuYqYY2gKa7JttlA6MiyzU0yBPzDlcawXF2TdOmQ/DWy6ZDEw==",
"version": "3.5.10",
"resolved": "https://registry.npmjs.org/uikit/-/uikit-3.5.10.tgz",
"integrity": "sha512-X+OUD02atmks1yYG9C3FSo1EcQ0xfV0+NCHspTShd2uMKYcC5LiMq6o/MyxBxx2bQSeKXh6Kh8TqQ/v4LWI4Tg==",
"dev": true
},
"unicode-canonical-property-names-ecmascript": {
@ -13699,9 +13653,9 @@
}
},
"object-inspect": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz",
"integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
"integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==",
"dev": true
},
"object.assign": {
@ -13915,9 +13869,9 @@
"dev": true
},
"vuex": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.5.1.tgz",
"integrity": "sha512-w7oJzmHQs0FM9LXodfskhw9wgKBiaB+totOdb8sNzbTB2KDCEEwEs29NzBZFh/lmEK1t5tDmM1vtsO7ubG1DFw==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.6.0.tgz",
"integrity": "sha512-W74OO2vCJPs9/YjNjW8lLbj+jzT24waTo2KShI8jLvJW8OaIkgb3wuAMA7D+ZiUxDOx3ubwSZTaJBip9G8a3aQ==",
"dev": true
},
"watchpack": {

View file

@ -18,25 +18,25 @@
"devDependencies": {
"@vue/cli-plugin-babel": "^3.12.1",
"@vue/cli-plugin-eslint": "^4.5.9",
"@vue/cli-service": "^4.5.8",
"@vue/cli-service": "^4.5.9",
"@vue/eslint-config-prettier": "^6.0.0",
"axios": "^0.19.2",
"babel-eslint": "^10.1.0",
"css-loader": "^3.6.0",
"eslint": "^6.8.0",
"eslint-plugin-prettier": "^3.1.4",
"eslint-plugin-prettier": "^3.2.0",
"eslint-plugin-vue": "^6.2.2",
"less": "^3.12.2",
"less-loader": "^5.0.0",
"mdns-js": "^1.0.3",
"prettier": "^1.19.1",
"uikit": "^3.5.9",
"uikit": "^3.5.10",
"vue": "^2.6.12",
"vue-friendly-iframe": "^0.17.0",
"vue-template-compiler": "^2.6.12",
"vue-tour": "^1.5.0",
"vuejs-paginate": "^2.1.0",
"vuex": "^3.5.1"
"vuex": "^3.6.0"
},
"postcss": {
"plugins": {

View file

@ -98,7 +98,9 @@ export default {
this.$emit("input", newSelected);
// Move focus back to key textbox
this.$refs.textboxKey.focus();
if (this.$refs.textboxKey) {
this.$refs.textboxKey.focus();
}
},
delMetadataKey: function(key) {

View file

@ -198,7 +198,6 @@ export default {
if (!error) {
error = Error("Unknown error");
}
this.$emit("error", error);
this.$emit("finished");
})

View file

@ -248,9 +248,8 @@
:submit-data="scanPayload"
:submit-label="'Start Scan'"
:button-primary="true"
@submit="onScanSubmit"
@response="onScanResponse"
@error="onScanError"
@error="modalError"
>
</taskSubmitter>
</div>
@ -433,14 +432,10 @@ export default {
});
},
onScanSubmit: function() {},
onScanResponse: function() {
this.modalNotify("Finished scan.");
},
onScanError: function(error) {
this.modalError(error);
// Emit signal to update capture list
this.$root.$emit("globalUpdateCaptures");
}
}
};

View file

@ -119,7 +119,6 @@ export default {
deleteAll: function() {
axios.all(this.allURLs.map(l => axios.delete(l))).then(() => {
console.log("Delete finished")
// Emit signal to update capture list
this.$root.$emit("globalUpdateCaptures");
});

View file

@ -18,7 +18,7 @@
:submit-label="'Create ZIP'"
:submit-data="captureIds"
@response="onResponse"
@error="onError"
@error="modalError"
>
</taskSubmitter>
</div>
@ -163,10 +163,6 @@ export default {
this.lastSessionId = response.output.id;
this.downloadUrl = `${this.zipGetterUri}/${this.lastSessionId}`;
this.downloadReady = true;
},
onError: function(error) {
this.modalError(error); // Let mixin handle error
}
}
};

View file

@ -197,7 +197,7 @@ export default {
var captures = [];
for (var capture of this.captures) {
// Add to capture list if matched
if (!capture.dataset) {
if (!this.isDatasetPopulated(capture.dataset)) {
captures.push(capture);
}
}
@ -212,7 +212,7 @@ export default {
for (var capture of this.captures) {
var dataset = capture.dataset;
if (dataset) {
if (this.isDatasetPopulated(dataset)) {
var id = dataset["id"];
// If this scan ID hasn't been seen before
@ -361,6 +361,18 @@ export default {
}
},
isDatasetPopulated: function(dataset) {
if (
!dataset || // If no dataset key
(dataset.constructor === Object && // Or dataset is an object...
Object.keys(dataset).length === 0) // ...but it's empty
) {
return false;
} else {
return true;
}
},
filterCaptures: function(list, filterTags) {
// Filter a list of captures by an array of tags
var result = [];

View file

@ -11,7 +11,11 @@
<!-- Left side controls -->
<div
class="uk-navbar-left uk-padding-remove-top uk-padding-remove-bottom"
></div>
>
<select v-model="filterLevel" class="uk-select">
<option v-for="level in allLevels" :key="level">{{ level }}</option>
</select>
</div>
<!-- Right side buttons -->
<div class="uk-navbar-right">
@ -40,15 +44,14 @@
v-for="item in pagedItems"
:key="item.timestamp"
uk-alert
class="logging-entry"
:class="{
'uk-alert-warning uk-alert': item.data.levelname == 'WARNING',
'uk-alert-danger uk-alert': item.data.levelname == 'ERROR'
}"
>
<p>
<b>{{ formatDateTime(item.data.created) }}</b>
</p>
{{ item.data.levelname }}: {{ item.data.message }}
<b>{{ formatDateTime(item.data.created) }}</b>
<div class="logging-message">{{ formatMessage(item) }}</div>
</div>
<Paginate
@ -84,11 +87,28 @@ export default {
return {
maxitems: 20,
page: 1,
logs: []
logs: [],
allLevels: ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
filterLevel: "WARNING"
};
},
computed: {
filteredLevels: function() {
let cutoffIndex = this.allLevels.indexOf(this.filterLevel);
return this.allLevels.slice(cutoffIndex, -1);
},
filteredItems: function() {
var items = [];
for (var item of this.logs) {
// Add to capture list if matched
if (this.filteredLevels.includes(item.data.levelname)) {
items.push(item);
}
}
return items;
},
loggingUri: function() {
return `${this.$store.getters.baseUri}/api/v2/events/logging`;
},
@ -97,10 +117,10 @@ export default {
},
pagedItems: function() {
let startIndex = (this.page - 1) * this.maxitems;
return this.logs.slice(startIndex, startIndex + this.maxitems);
return this.filteredItems.slice(startIndex, startIndex + this.maxitems);
},
numberOfPages: function() {
return Math.floor(this.logs.length / this.maxitems);
return Math.floor(this.filteredItems.length / this.maxitems);
}
},
@ -134,6 +154,9 @@ export default {
formatDateTime: function(isoDateTimeString) {
let date = new Date(isoDateTimeString);
return date.toLocaleDateString() + " " + date.toLocaleTimeString();
},
formatMessage: function(item) {
return item.data.levelname + ": " + item.data.message;
}
}
};
@ -147,4 +170,10 @@ export default {
margin-bottom: 30px;
height: 80px;
}
.logging-entry {
white-space: break-spaces;
}
.logging-message {
font-family: monospace;
}
</style>

View file

@ -148,6 +148,7 @@
:submit-on-event="'globalFastAutofocusEvent'"
@taskStarted="isAutofocusing = 1"
@finished="isAutofocusing = 0"
@error="modalError"
></taskSubmitter>
</div>
@ -160,6 +161,7 @@
:button-primary="false"
@taskStarted="isAutofocusing = 2"
@finished="isAutofocusing = 0"
@error="modalError"
></taskSubmitter>
</div>
@ -172,6 +174,7 @@
:button-primary="false"
@taskStarted="isAutofocusing = 3"
@finished="isAutofocusing = 0"
@error="modalError"
></taskSubmitter>
</div>
</div>

View file

@ -12,7 +12,7 @@
:submit-url="recalibrationLinks.calibrate_xy.href"
:submit-label="'Auto-Calibrate using camera'"
@response="onRecalibrateResponse"
@error="onRecalibrateError"
@error="modalError"
>
</taskSubmitter>
</div>
@ -149,10 +149,6 @@ export default {
this.modalNotify("Finished stage-to-camera calibration.");
// Update local settings
this.updateSettings();
},
onRecalibrateError: function(error) {
this.modalError(error); // Let mixin handle error
}
}
};

View file

@ -11,7 +11,7 @@
:submit-url="recalibrationLinks.recalibrate.href"
:submit-label="'Auto-Calibrate'"
@response="onRecalibrateResponse"
@error="onRecalibrateError"
@error="modalError"
>
</taskSubmitter>
</div>
@ -128,10 +128,6 @@ export default {
this.modalNotify("Finished recalibration.");
},
onRecalibrateError: function(error) {
this.modalError(error); // Let mixin handle error
},
flattenLensShadingTableRequest: function() {
axios.post(this.recalibrationLinks.flatten_lens_shading_table.href);
},

View file

@ -95,7 +95,7 @@
submit-label="Start scan"
@submit="scanRunning = true"
@response="scanRunning = false"
@error="scanRunning = false"
@error="onScanError"
></taskSubmitter>
<br />
@ -233,6 +233,11 @@ export default {
});
},
onScanError: function(error) {
this.scanRunning = false;
this.modalError(error);
},
decrement: function() {
if (this.stepValue > 0) {
this.stepValue = this.stepValue - 1;

View file

@ -79,19 +79,25 @@ class CaptureSchema(ImageSchema):
"href": url_for(CaptureView.endpoint, id_=capture_id, _external=True),
"mimetype": "application/json",
**description_from_view(CaptureView),
},
}
if CaptureView.endpoint
else {},
"tags": {
"href": url_for(CaptureTags.endpoint, id_=capture_id, _external=True),
"mimetype": "application/json",
**description_from_view(CaptureTags),
},
}
if CaptureTags.endpoint
else {},
"annotations": {
"href": url_for(
CaptureAnnotations.endpoint, id_=capture_id, _external=True
),
"mimetype": "application/json",
**description_from_view(CaptureAnnotations),
},
}
if CaptureAnnotations.endpoint
else {},
"download": {
"href": url_for(
CaptureDownload.endpoint,
@ -101,7 +107,9 @@ class CaptureSchema(ImageSchema):
),
"mimetype": "image/jpeg",
**description_from_view(CaptureDownload),
},
}
if CaptureDownload.endpoint
else {},
}
if isinstance(data, dict):

View file

@ -3,14 +3,16 @@ import io
import logging
import time
from abc import ABCMeta, abstractmethod
from collections import namedtuple
from types import TracebackType
from typing import BinaryIO, List, Optional, Tuple, Type, Union
from typing import BinaryIO, List, NamedTuple, Optional, Tuple, Type, Union
from labthings import ClientEvent, StrictLock
# Class to store a frames metadata
TrackerFrame = namedtuple("TrackerFrame", ["size", "time"])
class TrackerFrame(NamedTuple):
size: int
time: float
class FrameStream(io.BytesIO):
@ -170,6 +172,21 @@ class BaseCamera(metaclass=ABCMeta):
thumbnail: Dimensions and quality (x, y, quality) of a thumbnail to generate, if supported
"""
def start_worker(self, **_) -> bool:
"""Start the background camera thread if it isn't running yet."""
logging.warning(
"`start_worker` method has been deprecated and is no longer required. Please avoid calling this method."
)
return True
def get_frame(self) -> bytes:
"""
Return the current camera frame.
Just an alias of self.stream.getframe()
"""
return self.stream.getframe()
def __enter__(self):
"""Create camera on context enter."""
return self

View file

@ -178,7 +178,7 @@ class MissingCamera(BaseCamera):
"""
Change the camera zoom, handling re-centering and scaling.
"""
logging.warning("Zoom not implemented in mock camera")
logging.info("Zoom not implemented in mock camera")
def start_stream(self):
pass
@ -188,11 +188,11 @@ class MissingCamera(BaseCamera):
def start_preview(self, *_, **__):
"""Start the on board GPU camera preview."""
logging.warning("GPU preview not implemented in mock camera")
logging.info("GPU preview not implemented in mock camera")
def stop_preview(self):
"""Stop the on board GPU camera preview."""
logging.warning("GPU preview not implemented in mock camera")
logging.info("GPU preview not implemented in mock camera")
def start_recording(self, *_, **__):
"""Start recording.

View file

@ -22,7 +22,7 @@ try:
except Exception as e: # pylint: disable=W0703
logging.error(e)
logging.warning("Unable to import PiCameraStreamer")
from labthings import CompositeLock, StrictLock
from labthings import CompositeLock
from openflexure_microscope.config import user_configuration, user_settings
@ -48,7 +48,7 @@ class Microscope:
# Initialise with an empty composite lock
#: :py:class:`labthings.CompositeLock`: Composite lock for locking both camera and stage
self.lock: Union[CompositeLock, StrictLock] = CompositeLock([])
self.lock: CompositeLock = CompositeLock([])
self.camera: BaseCamera = None #: Currently connected camera object
self.stage: BaseStage = None #: Currently connected stage object

View file

@ -5,6 +5,8 @@ import numpy as np
from labthings import StrictLock
from typing_extensions import Literal
CoordinateType = Tuple[int, int, int]
class BaseStage(metaclass=ABCMeta):
"""
@ -41,7 +43,7 @@ class BaseStage(metaclass=ABCMeta):
@property
@abstractmethod
def position(self) -> Tuple[int, int, int]:
def position(self) -> CoordinateType:
"""The current position, as a list"""
@property
@ -64,7 +66,7 @@ class BaseStage(metaclass=ABCMeta):
@abstractmethod
def move_rel(
self,
displacement: Union[int, Tuple[int, int, int]],
displacement: Union[int, CoordinateType],
axis: Optional[Literal["x", "y", "z"]] = None,
backlash: bool = True,
):
@ -74,7 +76,7 @@ class BaseStage(metaclass=ABCMeta):
"""
@abstractmethod
def move_abs(self, final: Tuple[int, int, int], **kwargs):
def move_abs(self, final: CoordinateType, **kwargs):
"""Make an absolute move to a position"""
@abstractmethod
@ -87,7 +89,7 @@ class BaseStage(metaclass=ABCMeta):
def scan_linear(
self,
rel_positions: List[Tuple[int, int, int]],
rel_positions: List[CoordinateType],
backlash: bool = True,
return_to_start: bool = True,
):

View file

@ -1,6 +1,7 @@
import logging
import time
from collections.abc import Iterable
from types import GeneratorType
from typing import Optional, Tuple, Union
import numpy as np
@ -11,6 +12,19 @@ from openflexure_microscope.stage.base import BaseStage
from openflexure_microscope.utilities import axes_to_array
def _displacement_to_array(
displacement: int, axis: Literal["x", "y", "z"]
) -> np.ndarray:
# Create the displacement array
return np.array(
[
displacement if axis == "x" else 0,
displacement if axis == "y" else 0,
displacement if axis == "z" else 0,
]
)
class SangaStage(BaseStage):
"""
Sangaboard v0.2 and v0.3 powered Stage object
@ -30,11 +44,9 @@ class SangaStage(BaseStage):
self.port = port
self.board = Sangaboard(port, **kwargs)
self._backlash = (
None # Initialise backlash storage, used by property setter/getter
)
# Initialise backlash storage, used by property setter/getter
self._backlash = None
self.settle_time = 0.2 # Default move settle time
self._position_on_enter = None
@property
@ -56,11 +68,11 @@ class SangaStage(BaseStage):
return 3
@property
def position(self):
def position(self) -> Tuple[int, int, int]:
return self.board.position
@property
def backlash(self):
def backlash(self) -> np.ndarray:
"""The distance used for backlash compensation.
Software backlash compensation is enabled by setting this property to a value
other than `None`. The value can either be an array-like object (list, tuple,
@ -75,8 +87,12 @@ class SangaStage(BaseStage):
back by ``backlash[i]``. This is computed per-axis, so if some axes are moving
in the same direction as ``backlash``, they won't do two moves.
"""
if self._backlash is not None:
if isinstance(self._backlash, np.ndarray):
return self._backlash
elif isinstance(self._backlash, list):
return np.array(self._backlash)
elif isinstance(self._backlash, int):
return np.array([self._backlash] * self.n_axes)
else:
return np.array([0] * self.n_axes)
@ -98,13 +114,16 @@ class SangaStage(BaseStage):
if "backlash" in config:
# Construct backlash array
backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0])
self.backlash = backlash
self.backlash = np.array(backlash)
if "settle_time" in config:
self.settle_time = config.get("settle_time")
def read_settings(self) -> dict:
"""Return the current settings as a dictionary"""
blsh = self.backlash.tolist()
if self.backlash is not None:
blsh = self.backlash.tolist()
else:
blsh = None
config = {
"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]},
"settle_time": self.settle_time,
@ -114,7 +133,7 @@ class SangaStage(BaseStage):
def move_rel(
self,
displacement: Union[int, Tuple[int, int, int]],
displacement: Union[int, Tuple[int, int, int], np.ndarray],
axis: Optional[Literal["x", "y", "z"]] = None,
backlash: bool = True,
):
@ -122,11 +141,19 @@ class SangaStage(BaseStage):
displacement: integer or array/list of 3 integers
axis: None (for 3-axis moves) or one of 'x','y','z'
backlash: (default: True) whether to correct for backlash.
Backlash Correction:
This backlash correction strategy ensures we're always approaching the
end point from the same direction, while minimising the amount of extra
motion. It's a good option if you're scanning in a line, for example,
as it will kick in when moving to the start of the line, but not for each
point on the line.
For each axis where we're moving in the *opposite*
direction to self.backlash, we deliberately overshoot:
"""
with self.lock:
logging.debug("Moving sangaboard by %s", displacement)
if not backlash or self.backlash is None:
return self.board.move_rel(displacement, axis=axis)
# If we specify an axis name and a displacement int, convert to a displacement tuple
if axis:
# Displacement MUST be an integer if axis name is specified
@ -137,37 +164,42 @@ class SangaStage(BaseStage):
# Axis name MUST be x, y, or z
if axis not in ("x", "y", "z"):
raise ValueError("axis must be one of x, y, or z")
move = (
displacement if axis == "x" else 0,
displacement if axis == "y" else 0,
displacement if axis == "z" else 0,
# Calculate displacement array
displacement_array: np.ndarray = _displacement_to_array(
displacement, axis
)
displacement = move
elif isinstance(displacement, np.ndarray):
displacement_array = displacement
elif isinstance(displacement, (list, tuple, GeneratorType)):
# Convert our displacement tuple/generator into a numpy array
displacement_array = np.array(list(displacement))
else:
raise TypeError(f"Unsupported displacement type {type(displacement)}")
initial_move = np.array(displacement, dtype=np.int)
# Backlash Correction
# This backlash correction strategy ensures we're always approaching the
# end point from the same direction, while minimising the amount of extra
# motion. It's a good option if you're scanning in a line, for example,
# as it will kick in when moving to the start of the line, but not for each
# point on the line.
# For each axis where we're moving in the *opposite*
# direction to self.backlash, we deliberately overshoot:
# Handle simple case, no backlash
if not backlash or self.backlash is None:
return self.board.move_rel(displacement_array)
# Handle move with backlash correction
# Calculate main movement
initial_move: np.ndarray = displacement_array
initial_move -= np.where(
self.backlash * displacement < 0,
self.backlash * displacement_array < 0,
self.backlash,
np.zeros(self.n_axes, dtype=self.backlash.dtype),
)
# Make the main movement
self.board.move_rel(initial_move)
if np.any(displacement - initial_move != 0):
# Handle backlash if required
if np.any(displacement_array - initial_move != 0):
# If backlash correction has kicked in and made us overshoot, move
# to the correct end position (i.e. the move we were asked to make)
self.board.move_rel(displacement - initial_move)
self.board.move_rel(displacement_array - initial_move)
# Settle outside of the stage lock so that another move request
# can just take over before settling
time.sleep(self.settle_time)
def move_abs(self, final: Tuple[int, int, int], **kwargs):
def move_abs(self, final: Union[Tuple[int, int, int], np.ndarray], **kwargs):
"""Make an absolute move to a position
"""
with self.lock:
@ -220,15 +252,21 @@ class SangaStage(BaseStage):
class SangaDeltaStage(SangaStage):
def __init__(
self, port=None, flex_h=80, flex_a=50, flex_b=50, camera_angle=0, **kwargs
self,
port: Optional[str] = None,
flex_h: int = 80,
flex_a: int = 50,
flex_b: int = 50,
camera_angle: float = 0,
**kwargs,
):
self.flex_h = flex_h
self.flex_a = flex_a
self.flex_b = flex_b
self.flex_h: int = flex_h
self.flex_a: int = flex_a
self.flex_b: int = flex_b
# Set up camera rotation relative to stage
camera_theta = (camera_angle / 180) * np.pi
self.R_camera = np.array(
camera_theta: float = (camera_angle / 180) * np.pi
self.R_camera: np.ndarray = np.array(
[
[np.cos(camera_theta), -np.sin(camera_theta), 0],
[np.sin(camera_theta), np.cos(camera_theta), 0],
@ -239,13 +277,15 @@ class SangaDeltaStage(SangaStage):
logging.debug(self.R_camera)
# Transformation matrix converting delta into cartesian
x_fac = -1 * np.multiply(
x_fac: np.float = -1 * np.multiply(
np.divide(2, np.sqrt(3)), np.divide(self.flex_b, self.flex_h)
)
y_fac = -1 * np.divide(self.flex_b, self.flex_h)
z_fac = np.multiply(np.divide(1, 3), np.divide(self.flex_b, self.flex_a))
y_fac: np.float = -1 * np.divide(self.flex_b, self.flex_h)
z_fac: np.float = np.multiply(
np.divide(1, 3), np.divide(self.flex_b, self.flex_a)
)
self.Tvd = np.array(
self.Tvd: np.ndarray = np.array(
[
[-x_fac, x_fac, 0],
[0.5 * y_fac, 0.5 * y_fac, -y_fac],
@ -254,40 +294,76 @@ class SangaDeltaStage(SangaStage):
)
logging.debug(self.Tvd)
self.Tdv = np.linalg.inv(self.Tvd)
self.Tdv: np.ndarray = np.linalg.inv(self.Tvd)
logging.debug(self.Tdv)
SangaStage.__init__(self, port=port, **kwargs)
@property
def raw_position(self) -> Tuple[int, int, int]:
return self.board.position
@property
def position(self):
# TODO: Account for camera rotation
position = np.dot(self.Tvd, self.board.position)
position: np.ndarray = np.dot(self.Tvd, self.raw_position)
position = np.dot(np.linalg.inv(self.R_camera), position)
position: np.ndarray = np.dot(np.linalg.inv(self.R_camera), position)
return [int(p) for p in position]
def move_rel(self, displacement, axis=None, backlash=True):
def move_rel(
self,
displacement: Union[int, Tuple[int, int, int], np.ndarray],
axis: Optional[Literal["x", "y", "z"]] = None,
backlash: bool = True,
):
# If we specify an axis name and a displacement int, convert to a displacement tuple
if axis:
# Displacement MUST be an integer if axis name is specified
if not isinstance(displacement, int):
raise TypeError(
"Displacement must be an integer when axis is specified"
)
# Axis name MUST be x, y, or z
if axis not in ("x", "y", "z"):
raise ValueError("axis must be one of x, y, or z")
# Calculate displacement array
cartesian_displacement_array: np.ndarray = _displacement_to_array(
displacement, axis
)
elif isinstance(displacement, np.ndarray):
cartesian_displacement_array = displacement
elif isinstance(displacement, (list, tuple, GeneratorType)):
# Convert our displacement tuple/generator into a numpy array
cartesian_displacement_array = np.array(list(displacement))
else:
raise TypeError(f"Unsupported displacement type {type(displacement)}")
# Transform into camera coordinates
displacement = np.dot(self.R_camera, displacement)
camera_displacement_array: np.ndarray = np.dot(
self.R_camera, cartesian_displacement_array
)
# Transform into delta coordinates
displacement = np.dot(self.Tdv, displacement)
delta_displacement_array: np.ndarray = np.dot(
self.Tdv, camera_displacement_array
)
logging.debug("Delta displacement: %s", (displacement))
logging.debug("Delta displacement: %s", (delta_displacement_array))
# Do the move
SangaStage.move_rel(self, displacement, axis=None, backlash=backlash)
SangaStage.move_rel(
self, delta_displacement_array, axis=None, backlash=backlash
)
def move_abs(self, final, **kwargs):
def move_abs(self, final: Union[Tuple[int, int, int], np.ndarray], **kwargs):
# Transform into camera coordinates
final = np.dot(self.R_camera, final)
camera_final_array: np.ndarray = np.dot(self.R_camera, final)
# Transform into delta coordinates
final = np.dot(self.Tdv, final)
delta_final_array: np.ndarray = np.dot(self.Tdv, camera_final_array)
logging.debug("Delta final: %s", (final))
# Do the move
SangaStage.move_abs(self, final, **kwargs)
SangaStage.move_abs(self, delta_final_array, **kwargs)

View file

@ -1,18 +1,25 @@
import base64
import copy
import logging
import sys
import time
from contextlib import contextmanager
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Tuple, Type, Union
import numpy as np
# TypedDict was added to typing in 3.8. Use typing_extensions for <3.8
if sys.version_info >= (3, 8):
from typing import TypedDict # pylint: disable=no-name-in-module
else:
from typing_extensions import TypedDict
class Timer(object):
def __init__(self, name):
self.name = name
self.start = None
self.end = None
def __init__(self, name: str):
self.name: str = name
self.start: Optional[float] = None
self.end: Optional[float] = None
def __enter__(self):
self.start = time.time()
@ -22,19 +29,27 @@ class Timer(object):
logging.debug("%s time: %s", self.name, self.end - self.start)
def deserialise_array_b64(b64_string: str, dtype: str, shape: List[int]):
flat_arr = np.frombuffer(base64.b64decode(b64_string), dtype)
JSONArrayType = TypedDict(
"JSONArrayType",
{"@type": str, "base64": str, "dtype": str, "shape": Tuple[int, ...]},
)
def deserialise_array_b64(
b64_string: str, dtype: Union[Type[np.dtype], str], shape: Tuple[int, ...]
):
flat_arr: np.ndarray = np.frombuffer(base64.b64decode(b64_string), dtype)
return flat_arr.reshape(shape)
def serialise_array_b64(npy_arr: np.ndarray):
b64_string = base64.b64encode(npy_arr).decode("ascii")
dtype = str(npy_arr.dtype)
shape = npy_arr.shape
def serialise_array_b64(npy_arr: np.ndarray) -> Tuple[str, str, Tuple[int, ...]]:
b64_string: str = base64.b64encode(npy_arr.tobytes()).decode("ascii")
dtype: str = str(npy_arr.dtype)
shape: Tuple[int, ...] = npy_arr.shape
return b64_string, dtype, shape
def ndarray_to_json(arr: np.ndarray):
def ndarray_to_json(arr: np.ndarray) -> JSONArrayType:
if isinstance(arr, memoryview):
# We can transparently convert memoryview objects to arrays
# This comes in very handy for the lens shading table.
@ -43,7 +58,7 @@ def ndarray_to_json(arr: np.ndarray):
return {"@type": "ndarray", "dtype": dtype, "shape": shape, "base64": b64_string}
def json_to_ndarray(json_dict: dict):
def json_to_ndarray(json_dict: JSONArrayType):
if not json_dict.get("@type") != "ndarray":
logging.warning("No valid @type attribute found. Conversion may fail.")
for required_param in ("dtype", "shape", "base64"):
@ -52,7 +67,7 @@ def json_to_ndarray(json_dict: dict):
b64_string: Optional[str] = json_dict.get("base64")
dtype: Optional[str] = json_dict.get("dtype")
shape: Optional[List[int]] = json_dict.get("shape")
shape: Optional[Tuple[int, ...]] = json_dict.get("shape")
if b64_string and dtype and shape:
return deserialise_array_b64(b64_string, dtype, shape)

81
poetry.lock generated
View file

@ -8,20 +8,20 @@ python-versions = "*"
[[package]]
name = "apispec"
version = "3.3.2"
version = "4.0.0"
description = "A pluggable API specification generator. Currently supports the OpenAPI Specification (f.k.a. the Swagger specification)."
category = "main"
optional = false
python-versions = ">=3.5"
python-versions = ">=3.6"
[package.dependencies]
PyYAML = {version = ">=3.10", optional = true, markers = "extra == \"yaml\""}
[package.extras]
dev = ["PyYAML (>=3.10)", "prance[osv] (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock", "flake8 (==3.8.3)", "flake8-bugbear (==20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"]
docs = ["marshmallow (>=2.19.2)", "pyyaml (==5.3.1)", "sphinx (==3.2.1)", "sphinx-issues (==1.2.0)", "sphinx-rtd-theme (==0.5.0)"]
dev = ["PyYAML (>=3.10)", "prance[osv] (>=0.11)", "marshmallow (>=3.0.0)", "pytest", "mock", "flake8 (==3.8.3)", "flake8-bugbear (==20.1.4)", "pre-commit (>=2.4,<3.0)", "tox"]
docs = ["marshmallow (>=3.0.0)", "pyyaml (==5.3.1)", "sphinx (==3.2.1)", "sphinx-issues (==1.2.0)", "sphinx-rtd-theme (==0.5.0)"]
lint = ["flake8 (==3.8.3)", "flake8-bugbear (==20.1.4)", "pre-commit (>=2.4,<3.0)"]
tests = ["PyYAML (>=3.10)", "prance[osv] (>=0.11)", "marshmallow (>=2.19.2)", "pytest", "mock"]
tests = ["PyYAML (>=3.10)", "prance[osv] (>=0.11)", "marshmallow (>=3.0.0)", "pytest", "mock"]
validation = ["prance[osv] (>=0.11)"]
yaml = ["PyYAML (>=3.10)"]
@ -278,7 +278,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "importlib-metadata"
version = "3.1.0"
version = "3.1.1"
description = "Read metadata from Python packages"
category = "main"
optional = false
@ -288,8 +288,8 @@ python-versions = ">=3.6"
zipp = ">=0.5"
[package.extras]
docs = ["sphinx", "rst.linker"]
testing = ["packaging", "pep517", "unittest2", "importlib-resources (>=1.3)"]
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
[[package]]
name = "iniconfig"
@ -336,28 +336,21 @@ i18n = ["Babel (>=0.8)"]
[[package]]
name = "labthings"
version = "1.1.5"
version = "1.2.2"
description = "Python implementation of LabThings, based on the Flask microframework"
category = "main"
optional = false
python-versions = "^3.6"
develop = true
python-versions = ">=3.6,<4.0"
[package.dependencies]
apispec = "^3.2.0"
apispec_webframeworks = "^0.5.2"
Flask = "^1.1.1"
flask-cors = "^3.0.8"
marshmallow = "^3.4.0"
webargs = "^6.0.0"
apispec = ">=3.2,<5.0"
apispec_webframeworks = ">=0.5.2,<0.6.0"
Flask = ">=1.1.1,<2.0.0"
flask-cors = ">=3.0.8,<4.0.0"
marshmallow = ">=3.4.0,<4.0.0"
webargs = ">=6.0.0,<7.0.0"
zeroconf = ">=0.24.5,<0.29.0"
[package.source]
type = "git"
url = "https://github.com/labthings/python-labthings"
reference = "master"
resolved_reference = "e9ed2444c08e3854ebefdadaa753700401392c97"
[[package]]
name = "lazy-object-proxy"
version = "1.4.3"
@ -428,6 +421,25 @@ category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "numpy-stubs"
version = "0.0.1"
description = ""
category = "main"
optional = false
python-versions = "*"
develop = false
[package.dependencies]
numpy = ">=1.16.0"
typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""}
[package.source]
type = "git"
url = "https://github.com/numpy/numpy-stubs.git"
reference = "master"
resolved_reference = "c49d2d6875971a669a166ea93ef998911af283a1"
[[package]]
name = "opencv-python-headless"
version = "4.4.0.44"
@ -441,7 +453,7 @@ numpy = ">=1.17.3"
[[package]]
name = "packaging"
version = "20.4"
version = "20.7"
description = "Core utilities for Python packages"
category = "main"
optional = false
@ -449,7 +461,6 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[package.dependencies]
pyparsing = ">=2.0.2"
six = "*"
[[package]]
name = "pastel"
@ -956,7 +967,7 @@ rpi = ["RPi.GPIO"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7.3"
content-hash = "e2f1011751b49fc5837f475f5304822216724a89f877ee5daf9c281cd8830673"
content-hash = "2c0faa244099ec3433edfc2cb9321f5de8bd5c0c32e135cae23f91e717cb07d7"
[metadata.files]
alabaster = [
@ -964,8 +975,8 @@ alabaster = [
{file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"},
]
apispec = [
{file = "apispec-3.3.2-py2.py3-none-any.whl", hash = "sha256:a1df9ec6b2cd0edf45039ef025abd7f0660808fa2edf737d3ba1cf5ef1a4625b"},
{file = "apispec-3.3.2.tar.gz", hash = "sha256:d23ebd5b71e541e031b02a19db10b5e6d5ef8452c552833e3e1afc836b40b1ad"},
{file = "apispec-4.0.0-py2.py3-none-any.whl", hash = "sha256:20d271f7c8d130719be223fdb122af391ff8d59fb24958c793f632305b87f8ed"},
{file = "apispec-4.0.0.tar.gz", hash = "sha256:360e28e5e84a4d7023b16de2b897327fe3da63ddc8e01f9165b9113b7fe1c48a"},
]
apispec-webframeworks = [
{file = "apispec-webframeworks-0.5.2.tar.gz", hash = "sha256:0db35b267914b3f8c562aca0261957dbcb4176f255eacc22520277010818dcf3"},
@ -1090,8 +1101,8 @@ imagesize = [
{file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"},
]
importlib-metadata = [
{file = "importlib_metadata-3.1.0-py2.py3-none-any.whl", hash = "sha256:590690d61efdd716ff82c39ca9a9d4209252adfe288a4b5721181050acbd4175"},
{file = "importlib_metadata-3.1.0.tar.gz", hash = "sha256:d9b8a46a0885337627a6430db287176970fff18ad421becec1d64cfc763c2099"},
{file = "importlib_metadata-3.1.1-py3-none-any.whl", hash = "sha256:6112e21359ef8f344e7178aa5b72dc6e62b38b0d008e6d3cb212c5b84df72013"},
{file = "importlib_metadata-3.1.1.tar.gz", hash = "sha256:b0c2d3b226157ae4517d9625decf63591461c66b3a808c2666d538946519d170"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
@ -1109,7 +1120,10 @@ jinja2 = [
{file = "Jinja2-2.11.2-py2.py3-none-any.whl", hash = "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"},
{file = "Jinja2-2.11.2.tar.gz", hash = "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0"},
]
labthings = []
labthings = [
{file = "labthings-1.2.2-py3-none-any.whl", hash = "sha256:0f4fb76feed5ce9213d471f97700c4b52d9563f940954e1efb644889d3381d2d"},
{file = "labthings-1.2.2.tar.gz", hash = "sha256:62a8c5bceea25883a7f329a8e19bd95d211d4fe8ac7eb39ee2a2bd51befa4bd5"},
]
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"},
@ -1224,6 +1238,7 @@ numpy = [
{file = "numpy-1.19.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:0bfd85053d1e9f60234f28f63d4a5147ada7f432943c113a11afcf3e65d9d4c8"},
{file = "numpy-1.19.2.zip", hash = "sha256:0d310730e1e793527065ad7dde736197b705d0e4c9999775f212b03c44a8484c"},
]
numpy-stubs = []
opencv-python-headless = [
{file = "opencv-python-headless-4.4.0.44.tar.gz", hash = "sha256:6eefcacfb9b2da305277e1a93c7bf074dcd10b7aa154a0c963ded08fc0ffc02e"},
{file = "opencv_python_headless-4.4.0.44-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:0896413b35b4b64acae42b84f740a458e0520d51f806946adf185e710a2ab300"},
@ -1243,8 +1258,8 @@ opencv-python-headless = [
{file = "opencv_python_headless-4.4.0.44-cp38-cp38-win_amd64.whl", hash = "sha256:93f251c28739d8e8ade8898ddcbd75dcec60f779d9534644c568e7e65e2b76de"},
]
packaging = [
{file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"},
{file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"},
{file = "packaging-20.7-py2.py3-none-any.whl", hash = "sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376"},
{file = "packaging-20.7.tar.gz", hash = "sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236"},
]
pastel = [
{file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"},

View file

@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
[tool.poetry]
name = "openflexure-microscope-server"
version = "2.8.0"
version = "2.9.0"
description = "Python module, and Flask-based web API, to run the OpenFlexure Microscope."
authors = [
@ -43,10 +43,11 @@ expiringdict = "^1.2.1"
camera-stage-mapping = "0.1.4"
picamerax = ">=20.9.1"
pyyaml = "^5.3.1"
labthings = {git = "https://github.com/labthings/python-labthings", develop = true}
typing-extensions = "^3.7.4" # Needed for some type-hints in Python < 3.8 (e.g. Literal)
pytest-cov = "^2.10.1"
piexif = "^1.1.3"
labthings = "1.2.2"
typing-extensions = "^3.7.4" # Needed for some type-hints in Python < 3.8 (e.g. Literal)
numpy-stubs = {git = "https://github.com/numpy/numpy-stubs.git"} # Needed for Numpy < 1.20
[tool.poetry.extras]
rpi = ["RPi.GPIO"]
@ -66,7 +67,7 @@ poethepoet = "^0.9.0"
freezegun = "^1.0.0"
[tool.black]
exclude = '(\.eggs|\.git|\.venv|node_modules/)'
exclude = '(\.eggs|\.git|\.venv|\venv|node_modules/)'
[tool.isort]
multi_line_output = 3