Merge branch 'api-v2' into 'master'
Api v2 See merge request openflexure/openflexure-microscope-server!35
This commit is contained in:
commit
7cc595d92f
47 changed files with 1903 additions and 223 deletions
|
|
@ -142,4 +142,4 @@ class TimelapseAPI(MicroscopeViewPlugin):
|
||||||
self.timelapse_task = taskify(self.plugin.timelapse)(n_images)
|
self.timelapse_task = taskify(self.plugin.timelapse)(n_images)
|
||||||
|
|
||||||
# Return the state of the task (will show ID, start time, and status before the task has finished)
|
# Return the state of the task (will show ID, start time, and status before the task has finished)
|
||||||
return jsonify(self.timelapse_task.state), 202
|
return jsonify(self.timelapse_task.state), 201
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ An example of a long running task may look like:
|
||||||
def post(self):
|
def post(self):
|
||||||
# Attach the long-running method as a microscope task
|
# Attach the long-running method as a microscope task
|
||||||
self.my_task = taskify(self.plugin.long_running_function)()
|
self.my_task = taskify(self.plugin.long_running_function)()
|
||||||
return jsonify(self.my_task.state), 202
|
return jsonify(self.my_task.state), 201
|
||||||
|
|
||||||
After some time, once the task has completed, it could be retreived using:
|
After some time, once the task has completed, it could be retreived using:
|
||||||
|
|
||||||
|
|
@ -132,7 +132,7 @@ For example, a timelapse plugin may look like:
|
||||||
self.timelapse_task = taskify(self.plugin.timelapse)(n_images)
|
self.timelapse_task = taskify(self.plugin.timelapse)(n_images)
|
||||||
|
|
||||||
# Return the state of the task (will show ID, start time, and status before the task has finished)
|
# Return the state of the task (will show ID, start time, and status before the task has finished)
|
||||||
return jsonify(self.timelapse_task.state), 202
|
return jsonify(self.timelapse_task.state), 201
|
||||||
|
|
||||||
|
|
||||||
Notice that even though we never use the stage here, we still acquire the lock. This means that during the timelapse,
|
Notice that even though we never use the stage here, we still acquire the lock. This means that during the timelapse,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ from openflexure_microscope import Microscope
|
||||||
from openflexure_microscope.camera.capture import build_captures_from_exif
|
from openflexure_microscope.camera.capture import build_captures_from_exif
|
||||||
from openflexure_microscope.config import settings_file_path, JSONEncoder
|
from openflexure_microscope.config import settings_file_path, JSONEncoder
|
||||||
from openflexure_microscope.api.v1 import blueprints
|
from openflexure_microscope.api.v1 import blueprints
|
||||||
|
from openflexure_microscope.api import v2
|
||||||
|
|
||||||
# Import device modules
|
# Import device modules
|
||||||
# NB this will eventually be handled by the RC file, so you can choose what device
|
# NB this will eventually be handled by the RC file, so you can choose what device
|
||||||
|
|
@ -43,7 +44,8 @@ DEFAULT_LOGFILE = settings_file_path("openflexure_microscope.log")
|
||||||
if (__name__ == "__main__") or (not is_gunicorn):
|
if (__name__ == "__main__") or (not is_gunicorn):
|
||||||
# If imported, but not by gunicorn
|
# If imported, but not by gunicorn
|
||||||
print("Letting sys handle logs")
|
print("Letting sys handle logs")
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
else:
|
else:
|
||||||
# Direct standard Python logging to file and console
|
# Direct standard Python logging to file and console
|
||||||
root = logging.getLogger()
|
root = logging.getLogger()
|
||||||
|
|
@ -141,6 +143,38 @@ app.register_blueprint(plugin_blueprint, url_prefix=uri("/plugin", "v1"))
|
||||||
task_blueprint = blueprints.task.construct_blueprint(api_microscope)
|
task_blueprint = blueprints.task.construct_blueprint(api_microscope)
|
||||||
app.register_blueprint(task_blueprint, url_prefix=uri("/task", "v1"))
|
app.register_blueprint(task_blueprint, url_prefix=uri("/task", "v1"))
|
||||||
|
|
||||||
|
### V2
|
||||||
|
# Root routes
|
||||||
|
v2_root_blueprint = v2.blueprints.root.construct_blueprint(api_microscope)
|
||||||
|
app.register_blueprint(v2_root_blueprint, url_prefix=uri("/", "v2"))
|
||||||
|
|
||||||
|
v2_streams_blueprint = v2.blueprints.streams.construct_blueprint(api_microscope)
|
||||||
|
app.register_blueprint(v2_streams_blueprint, url_prefix=uri("/streams", "v2"))
|
||||||
|
|
||||||
|
# Captures routes
|
||||||
|
v2_captures_blueprint = v2.blueprints.captures.construct_blueprint(api_microscope)
|
||||||
|
app.register_blueprint(v2_captures_blueprint, url_prefix=uri("/captures", "v2"))
|
||||||
|
|
||||||
|
# Settings routes
|
||||||
|
v2_settings_blueprint = v2.blueprints.settings.construct_blueprint(api_microscope)
|
||||||
|
app.register_blueprint(v2_settings_blueprint, url_prefix=uri("/settings", "v2"))
|
||||||
|
|
||||||
|
# Status routes
|
||||||
|
v2_status_blueprint = v2.blueprints.status.construct_blueprint(api_microscope)
|
||||||
|
app.register_blueprint(v2_status_blueprint, url_prefix=uri("/status", "v2"))
|
||||||
|
|
||||||
|
# Plugins routes
|
||||||
|
v2_plugin_blueprint = v2.blueprints.plugins.construct_blueprint(api_microscope)
|
||||||
|
app.register_blueprint(v2_plugin_blueprint, url_prefix=uri("/plugins", "v2"))
|
||||||
|
|
||||||
|
# Tasks routes
|
||||||
|
v2_tasks_blueprint = v2.blueprints.tasks.construct_blueprint(api_microscope)
|
||||||
|
app.register_blueprint(v2_tasks_blueprint, url_prefix=uri("/tasks", "v2"))
|
||||||
|
|
||||||
|
# Actions routes
|
||||||
|
v2_actions_blueprint = v2.blueprints.actions.construct_blueprint(api_microscope)
|
||||||
|
app.register_blueprint(v2_actions_blueprint, url_prefix=uri("/actions", "v2"))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/routes")
|
@app.route("/routes")
|
||||||
def routes():
|
def routes():
|
||||||
|
|
@ -184,7 +218,7 @@ def cleanup():
|
||||||
|
|
||||||
# Save config
|
# Save config
|
||||||
logging.debug("Saving config for teardown...")
|
logging.debug("Saving config for teardown...")
|
||||||
api_microscope.save_config(backup=True)
|
api_microscope.save_settings()
|
||||||
|
|
||||||
logging.debug("Settling...")
|
logging.debug("Settling...")
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,18 @@
|
||||||
import logging
|
import logging
|
||||||
from werkzeug.exceptions import BadRequest
|
from werkzeug.exceptions import BadRequest
|
||||||
from flask import url_for
|
from flask import url_for, Blueprint
|
||||||
|
|
||||||
|
|
||||||
|
def blueprint_for_module(module_name, api_ver=2, suffix=""):
|
||||||
|
return Blueprint(
|
||||||
|
blueprint_name_for_module(module_name, api_ver=api_ver, suffix=suffix),
|
||||||
|
module_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def blueprint_name_for_module(module_name, api_ver=2, suffix=""):
|
||||||
|
bp_name = module_name.split(".")[-1]
|
||||||
|
return f"v{api_ver}_{bp_name}_blueprint{suffix}"
|
||||||
|
|
||||||
|
|
||||||
class JsonResponse:
|
class JsonResponse:
|
||||||
|
|
|
||||||
|
|
@ -152,7 +152,7 @@ class ConfigAPI(MicroscopeView):
|
||||||
:>header Content-Type: application/json
|
:>header Content-Type: application/json
|
||||||
:status 200: state available
|
:status 200: state available
|
||||||
"""
|
"""
|
||||||
return jsonify(self.microscope.read_config(json_safe=True))
|
return jsonify(self.microscope.read_settings(json_safe=True))
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -223,10 +223,10 @@ class ConfigAPI(MicroscopeView):
|
||||||
logging.debug("Updating settings from POST request:")
|
logging.debug("Updating settings from POST request:")
|
||||||
logging.debug(payload.json)
|
logging.debug(payload.json)
|
||||||
|
|
||||||
self.microscope.apply_config(payload.json)
|
self.microscope.apply_settings(payload.json)
|
||||||
self.microscope.save_config()
|
self.microscope.save_settings()
|
||||||
|
|
||||||
return jsonify(self.microscope.read_config(json_safe=True))
|
return jsonify(self.microscope.read_settings(json_safe=True))
|
||||||
|
|
||||||
|
|
||||||
def construct_blueprint(microscope_obj):
|
def construct_blueprint(microscope_obj):
|
||||||
|
|
|
||||||
|
|
@ -130,16 +130,19 @@ class ListAPI(MicroscopeView):
|
||||||
output.file, use_video_port=use_video_port, resize=resize, bayer=bayer
|
output.file, use_video_port=use_video_port, resize=resize, bayer=bayer
|
||||||
)
|
)
|
||||||
|
|
||||||
metadata.update(
|
# Inject system metadata
|
||||||
{
|
system_metadata = {
|
||||||
"microscope_settings": self.microscope.read_config(),
|
"microscope_settings": self.microscope.read_settings(),
|
||||||
"microscope_state": self.microscope.state,
|
"microscope_state": self.microscope.state,
|
||||||
"microscope_id": self.microscope.id,
|
"microscope_id": self.microscope.id,
|
||||||
"microscope_name": self.microscope.name,
|
"microscope_name": self.microscope.name,
|
||||||
}
|
}
|
||||||
)
|
output.system_metadata.update(system_metadata)
|
||||||
|
|
||||||
|
# Insert custom metadata
|
||||||
output.put_metadata(metadata)
|
output.put_metadata(metadata)
|
||||||
|
|
||||||
|
# Insert custom tags
|
||||||
output.put_tags(tags)
|
output.put_tags(tags)
|
||||||
|
|
||||||
return jsonify(output.state)
|
return jsonify(output.state)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class ZoomAPI(MicroscopeView):
|
||||||
:<header Content-Type: application/json
|
:<header Content-Type: application/json
|
||||||
:status 200: preview started/stopped
|
:status 200: preview started/stopped
|
||||||
"""
|
"""
|
||||||
zoom_value = self.microscope.camera.state["zoom_value"]
|
zoom_value = self.microscope.camera.status["zoom_value"]
|
||||||
|
|
||||||
return jsonify({"zoom_value": zoom_value})
|
return jsonify({"zoom_value": zoom_value})
|
||||||
|
|
||||||
|
|
@ -47,7 +47,7 @@ class ZoomAPI(MicroscopeView):
|
||||||
|
|
||||||
self.microscope.camera.set_zoom(zoom_value)
|
self.microscope.camera.set_zoom(zoom_value)
|
||||||
|
|
||||||
return jsonify(self.microscope.camera.state)
|
return jsonify(self.microscope.camera.status)
|
||||||
|
|
||||||
|
|
||||||
class OverlayAPI(MicroscopeView):
|
class OverlayAPI(MicroscopeView):
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from openflexure_microscope.api.views import MicroscopeViewPlugin
|
||||||
from flask import Blueprint, jsonify
|
from flask import Blueprint, jsonify
|
||||||
from openflexure_microscope.api.views import MicroscopeView
|
from openflexure_microscope.api.views import MicroscopeView
|
||||||
|
|
||||||
|
import copy
|
||||||
import logging
|
import logging
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
|
@ -21,7 +22,7 @@ class PluginFormAPI(MicroscopeView):
|
||||||
A complete list of enabled plugins can be found in the microscope state.
|
A complete list of enabled plugins can be found in the microscope state.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
out = self.microscope.plugin.forms
|
out = self.microscope.plugins.forms
|
||||||
return jsonify(out)
|
return jsonify(out)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -38,7 +39,7 @@ def construct_blueprint(microscope_obj):
|
||||||
all_routes = []
|
all_routes = []
|
||||||
|
|
||||||
# For each plugin attached to the microscope object
|
# For each plugin attached to the microscope object
|
||||||
for plugin_name, plugin_obj in microscope_obj.plugin.plugins:
|
for plugin_name, plugin_obj in microscope_obj.plugins._legacy_plugins.items():
|
||||||
|
|
||||||
# If plugin contains valid endpoints
|
# If plugin contains valid endpoints
|
||||||
if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict):
|
if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict):
|
||||||
|
|
@ -94,7 +95,7 @@ def construct_blueprint(microscope_obj):
|
||||||
if hasattr(plugin_obj, "api_form") and isinstance(
|
if hasattr(plugin_obj, "api_form") and isinstance(
|
||||||
plugin_obj.api_form, dict
|
plugin_obj.api_form, dict
|
||||||
):
|
):
|
||||||
api_form_info = plugin_obj.api_form
|
api_form_info = copy.deepcopy(plugin_obj.api_form)
|
||||||
api_form_info["id"] = plugin_name
|
api_form_info["id"] = plugin_name
|
||||||
if "forms" in api_form_info and isinstance(
|
if "forms" in api_form_info and isinstance(
|
||||||
api_form_info["forms"], list
|
api_form_info["forms"], list
|
||||||
|
|
@ -110,8 +111,7 @@ def construct_blueprint(microscope_obj):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store the complete form in Microscope().plugin.form
|
# Store the complete form in Microscope().plugin.form
|
||||||
microscope_obj.plugin.forms.append(api_form_info)
|
microscope_obj.plugins.forms.append(api_form_info)
|
||||||
print(microscope_obj.plugin.forms)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import blueprints
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
from . import root, captures, settings, status, tasks, streams, plugins, actions
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
"""
|
||||||
|
Top-level representation of enabled actions
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import Blueprint, url_for, jsonify
|
||||||
|
from sys import platform
|
||||||
|
|
||||||
|
from openflexure_microscope.api.utilities import blueprint_for_module
|
||||||
|
from openflexure_microscope.utilities import get_docstring, description_from_view
|
||||||
|
from openflexure_microscope.api.views import MicroscopeView
|
||||||
|
|
||||||
|
from . import camera, stage, system
|
||||||
|
|
||||||
|
|
||||||
|
class ActionsAPI(MicroscopeView):
|
||||||
|
def get(self):
|
||||||
|
return jsonify(actions_representation())
|
||||||
|
|
||||||
|
|
||||||
|
_actions = {
|
||||||
|
"capture": {
|
||||||
|
"rule": "/camera/capture/",
|
||||||
|
"view_class": camera.CaptureAPI,
|
||||||
|
"conditions": True,
|
||||||
|
},
|
||||||
|
"previewStart": {
|
||||||
|
"rule": "/camera/preview/start",
|
||||||
|
"view_class": camera.GPUPreviewStartAPI,
|
||||||
|
"conditions": True,
|
||||||
|
},
|
||||||
|
"previewStop": {
|
||||||
|
"rule": "/camera/preview/stop",
|
||||||
|
"view_class": camera.GPUPreviewStopAPI,
|
||||||
|
"conditions": True,
|
||||||
|
},
|
||||||
|
"move": {
|
||||||
|
"rule": "/stage/move/",
|
||||||
|
"view_class": stage.MoveStageAPI,
|
||||||
|
"conditions": True,
|
||||||
|
},
|
||||||
|
"shutdown": {
|
||||||
|
"rule": "/system/shutdown/",
|
||||||
|
"view_class": system.ShutdownAPI,
|
||||||
|
"conditions": (platform == "linux"),
|
||||||
|
},
|
||||||
|
"reboot": {
|
||||||
|
"rule": "/system/reboot/",
|
||||||
|
"view_class": system.RebootAPI,
|
||||||
|
"conditions": (platform == "linux"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def enabled_actions():
|
||||||
|
global _actions
|
||||||
|
return {k: v for k, v in _actions.items() if v["conditions"]}
|
||||||
|
|
||||||
|
|
||||||
|
def actions_representation():
|
||||||
|
global _actions
|
||||||
|
|
||||||
|
actions = {}
|
||||||
|
for name, action in enabled_actions().items():
|
||||||
|
d = {
|
||||||
|
"links": {"self": url_for(f".{name}")},
|
||||||
|
"rule": action["rule"],
|
||||||
|
"view_class": str(action["view_class"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
d.update(description_from_view(action["view_class"]))
|
||||||
|
|
||||||
|
actions[name] = d
|
||||||
|
|
||||||
|
return actions
|
||||||
|
|
||||||
|
|
||||||
|
def construct_blueprint(microscope_obj):
|
||||||
|
global _actions
|
||||||
|
|
||||||
|
blueprint = blueprint_for_module(__name__)
|
||||||
|
|
||||||
|
# For each enabled action route defined in our dictionary above
|
||||||
|
for name, action in enabled_actions().items():
|
||||||
|
# Add the action to our blueprint
|
||||||
|
blueprint.add_url_rule(
|
||||||
|
action["rule"],
|
||||||
|
view_func=action["view_class"].as_view(name, microscope=microscope_obj),
|
||||||
|
)
|
||||||
|
|
||||||
|
blueprint.add_url_rule(
|
||||||
|
"/", view_func=ActionsAPI.as_view("actions", microscope=microscope_obj)
|
||||||
|
)
|
||||||
|
|
||||||
|
return blueprint
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
from openflexure_microscope.api.utilities import get_bool, JsonResponse
|
||||||
|
from openflexure_microscope.api.views import MicroscopeView
|
||||||
|
from openflexure_microscope.utilities import filter_dict
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from flask import jsonify, request, abort, url_for, redirect, send_file
|
||||||
|
|
||||||
|
|
||||||
|
class CaptureAPI(MicroscopeView):
|
||||||
|
"""
|
||||||
|
Create a new image capture.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
"""
|
||||||
|
Create a new image capture.
|
||||||
|
|
||||||
|
.. :quickref: Actions; New capture
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /actions/camera/capture HTTP/1.1
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"filename": "myfirstcapture",
|
||||||
|
"temporary": false,
|
||||||
|
"use_video_port": true,
|
||||||
|
"bayer": true,
|
||||||
|
"size": {
|
||||||
|
"width": 640,
|
||||||
|
"height": 480
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:>header Accept: application/json
|
||||||
|
|
||||||
|
:<json string filename: filename of stored capture
|
||||||
|
:<json boolean temporary: delete the capture file on microscope after closing
|
||||||
|
:<json boolean use_video_port: capture still image from the video port
|
||||||
|
:<json boolean bayer: keep raw capture data in the image file
|
||||||
|
:<json json size: - **x** *(int)*: x-axis resize
|
||||||
|
- **y** *(int)*: y-axis resize
|
||||||
|
|
||||||
|
:>json boolean available: availability of capture data
|
||||||
|
:>json string filename: filename of capture
|
||||||
|
:>json string id: unique id of the capture object
|
||||||
|
:>json boolean temporary: delete the capture file on microscope after closing
|
||||||
|
:>json boolean locked: file locked for modifications (mostly used for video recording)
|
||||||
|
:>json string path: path on pi storage to the capture file, if available
|
||||||
|
:>json boolean stream: capture stored in-memory as a BytesIO stream
|
||||||
|
:>json json uri: - **download** *(string)*: api uri to the capture file download
|
||||||
|
- **state** *(string)*: api uri to the capture json representation
|
||||||
|
|
||||||
|
:<header Content-Type: application/json
|
||||||
|
:status 200: capture created
|
||||||
|
"""
|
||||||
|
payload = JsonResponse(request)
|
||||||
|
|
||||||
|
filename = payload.param("filename")
|
||||||
|
temporary = payload.param("temporary", default=False, convert=bool)
|
||||||
|
use_video_port = payload.param("use_video_port", default=False, convert=bool)
|
||||||
|
bayer = payload.param("bayer", default=True, convert=bool)
|
||||||
|
metadata = payload.param("metadata", default={}, convert=dict)
|
||||||
|
tags = payload.param("tags", default=[], convert=list)
|
||||||
|
|
||||||
|
resize = payload.param("size", default=None)
|
||||||
|
if resize:
|
||||||
|
if ("width" in resize) and ("height" in resize):
|
||||||
|
resize = (
|
||||||
|
int(resize["width"]),
|
||||||
|
int(resize["height"]),
|
||||||
|
) # Convert dict to tuple
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# Explicitally acquire lock (prevents empty files being created if lock is unavailable)
|
||||||
|
with self.microscope.camera.lock:
|
||||||
|
output = self.microscope.camera.new_image(
|
||||||
|
temporary=temporary, filename=filename
|
||||||
|
)
|
||||||
|
|
||||||
|
self.microscope.camera.capture(
|
||||||
|
output.file, use_video_port=use_video_port, resize=resize, bayer=bayer
|
||||||
|
)
|
||||||
|
|
||||||
|
# Inject system metadata
|
||||||
|
system_metadata = {
|
||||||
|
"microscope_settings": self.microscope.read_settings(),
|
||||||
|
"microscope_state": self.microscope.state,
|
||||||
|
"microscope_id": self.microscope.id,
|
||||||
|
"microscope_name": self.microscope.name,
|
||||||
|
}
|
||||||
|
output.system_metadata.update(system_metadata)
|
||||||
|
|
||||||
|
# Insert custom metadata
|
||||||
|
output.put_metadata(metadata)
|
||||||
|
|
||||||
|
# Insert custom tags
|
||||||
|
output.put_tags(tags)
|
||||||
|
|
||||||
|
return jsonify(output.state)
|
||||||
|
|
||||||
|
|
||||||
|
class GPUPreviewStartAPI(MicroscopeView):
|
||||||
|
"""
|
||||||
|
Start the onboard GPU preview.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post(self, operation):
|
||||||
|
"""
|
||||||
|
Start the onboard GPU preview.
|
||||||
|
Optional "window" parameter can be passed to control the position and size of the preview window,
|
||||||
|
in the format ``[x, y, width, height]``.
|
||||||
|
|
||||||
|
.. :quickref: Actions; Start on-board preview
|
||||||
|
|
||||||
|
**Example requests**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /actions/camera/preview/start HTTP/1.1
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"window": [0, 0, 480, 320],
|
||||||
|
}
|
||||||
|
|
||||||
|
:>header Accept: application/json
|
||||||
|
|
||||||
|
:<header Content-Type: application/json
|
||||||
|
:status 200: preview started/stopped
|
||||||
|
"""
|
||||||
|
payload = JsonResponse(request)
|
||||||
|
|
||||||
|
window = payload.param("window", default=[])
|
||||||
|
logging.debug(window)
|
||||||
|
|
||||||
|
if len(window) != 4:
|
||||||
|
fullscreen = True
|
||||||
|
window = None
|
||||||
|
else:
|
||||||
|
fullscreen = False
|
||||||
|
window = [int(w) for w in window]
|
||||||
|
|
||||||
|
self.microscope.camera.start_preview(fullscreen=fullscreen, window=window)
|
||||||
|
|
||||||
|
return jsonify(self.microscope.state)
|
||||||
|
|
||||||
|
|
||||||
|
class GPUPreviewStopAPI(MicroscopeView):
|
||||||
|
"""
|
||||||
|
Stop the onboard GPU preview.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post(self, operation):
|
||||||
|
"""
|
||||||
|
Stop the onboard GPU preview.
|
||||||
|
|
||||||
|
.. :quickref: Actions; Stop on-board preview
|
||||||
|
|
||||||
|
**Example requests**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /actions/camera/preview/stop HTTP/1.1
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
:>header Accept: application/json
|
||||||
|
|
||||||
|
:<header Content-Type: application/json
|
||||||
|
:status 200: preview started/stopped
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.microscope.camera.stop_preview()
|
||||||
|
return jsonify(self.microscope.state)
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
from openflexure_microscope.api.utilities import JsonResponse
|
||||||
|
from openflexure_microscope.api.views import MicroscopeView
|
||||||
|
from openflexure_microscope.utilities import axes_to_array, filter_dict
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, request
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class MoveStageAPI(MicroscopeView):
|
||||||
|
"""
|
||||||
|
Handle stage movements.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
"""
|
||||||
|
Set x, y and z positions of the stage.
|
||||||
|
|
||||||
|
.. :quickref: Position; Update current position
|
||||||
|
|
||||||
|
:reqheader Accept: application/json
|
||||||
|
:<json boolean absolute: (true) move to absolute position, (false) move by relative amount
|
||||||
|
:<json boolean force: allow moving by more than programmed limit
|
||||||
|
:<json int x: x steps
|
||||||
|
:<json int y: y steps
|
||||||
|
:<json int z: z steps
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Create response object
|
||||||
|
payload = JsonResponse(request)
|
||||||
|
logging.debug(payload.json)
|
||||||
|
|
||||||
|
# Handle absolute positioning (calculate a relative move from current position and target)
|
||||||
|
if (payload.param("absolute") is True) and (
|
||||||
|
self.microscope.stage
|
||||||
|
): # Only if stage exists
|
||||||
|
target_position = axes_to_array(payload.json, ["x", "y", "z"])
|
||||||
|
logging.debug("TARGET: {}".format(target_position))
|
||||||
|
position = [
|
||||||
|
target_position[i] - self.microscope.stage.position[i] for i in range(3)
|
||||||
|
]
|
||||||
|
logging.debug("DELTA: {}".format(position))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Get coordinates from payload
|
||||||
|
position = axes_to_array(payload.json, ["x", "y", "z"], [0, 0, 0])
|
||||||
|
|
||||||
|
logging.debug(position)
|
||||||
|
|
||||||
|
# Move if stage exists
|
||||||
|
if self.microscope.stage:
|
||||||
|
# Explicitally acquire lock
|
||||||
|
with self.microscope.stage.lock:
|
||||||
|
self.microscope.stage.move_rel(position)
|
||||||
|
|
||||||
|
out = filter_dict(self.microscope.state, ["stage", "position"])
|
||||||
|
|
||||||
|
return jsonify(out)
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
from openflexure_microscope.api.views import MicroscopeView
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
class ShutdownAPI(MicroscopeView):
|
||||||
|
"""
|
||||||
|
Attempt to shutdown the device
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
"""
|
||||||
|
Attempt to shutdown the device
|
||||||
|
|
||||||
|
.. :quickref: Actions; Shutdown
|
||||||
|
|
||||||
|
"""
|
||||||
|
subprocess.Popen(["shutdown", "-h", "now"])
|
||||||
|
|
||||||
|
return "{}", 201
|
||||||
|
|
||||||
|
|
||||||
|
class RebootAPI(MicroscopeView):
|
||||||
|
"""
|
||||||
|
Attempt to reboot the device
|
||||||
|
"""
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
"""
|
||||||
|
Attempt to shutdown the device
|
||||||
|
|
||||||
|
.. :quickref: Actions; Shutdown
|
||||||
|
|
||||||
|
"""
|
||||||
|
subprocess.Popen(["sudo", "shutdown", "-r", "now"])
|
||||||
|
|
||||||
|
return "{}", 201
|
||||||
416
openflexure_microscope/api/v2/blueprints/captures.py
Normal file
416
openflexure_microscope/api/v2/blueprints/captures.py
Normal file
|
|
@ -0,0 +1,416 @@
|
||||||
|
"""
|
||||||
|
Top-level representation of all acquired captures
|
||||||
|
"""
|
||||||
|
|
||||||
|
from openflexure_microscope.api.utilities import get_bool, JsonResponse
|
||||||
|
from openflexure_microscope.api.views import MicroscopeView
|
||||||
|
from openflexure_microscope.utilities import filter_dict
|
||||||
|
from openflexure_microscope.api.utilities import blueprint_for_module
|
||||||
|
|
||||||
|
from flask import jsonify, request, abort, url_for, redirect, send_file, Blueprint
|
||||||
|
|
||||||
|
|
||||||
|
def captures_representation(capture_list: list, include_unavailable: bool = False):
|
||||||
|
"""
|
||||||
|
Generate a dictionary representation of all captures, including Flask route URLs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
capture_list (list): List of capture objects
|
||||||
|
include_unavailable (bool): Include unavailable captures in response?
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary representation of all captures
|
||||||
|
|
||||||
|
"""
|
||||||
|
if include_unavailable:
|
||||||
|
captures = {image.id: image.state for image in capture_list}
|
||||||
|
else:
|
||||||
|
captures = {
|
||||||
|
image.id: image.state for image in capture_list if image.state["available"]
|
||||||
|
}
|
||||||
|
|
||||||
|
for capture_key, capture_repr in captures.items():
|
||||||
|
# Add API routes to returned representations
|
||||||
|
extra_state = {
|
||||||
|
"links": {
|
||||||
|
"self": "{}".format(url_for(".capture", capture_id=capture_key)),
|
||||||
|
"download": "{}".format(
|
||||||
|
url_for(
|
||||||
|
".capture_download",
|
||||||
|
capture_id=capture_key,
|
||||||
|
filename=capture_repr["filename"],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"tags": "{}".format(url_for(".capture_tags", capture_id=capture_key)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
captures[capture_key].update(extra_state)
|
||||||
|
|
||||||
|
return captures
|
||||||
|
|
||||||
|
|
||||||
|
class ListAPI(MicroscopeView):
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
Get list of image captures.
|
||||||
|
|
||||||
|
.. :quickref: Captures; Get collection of captures
|
||||||
|
|
||||||
|
:>header Accept: application/json
|
||||||
|
:query include_unavailable: return json representations of captures that have been completely deleted
|
||||||
|
|
||||||
|
:>jsonarr boolean available: availability of capture data
|
||||||
|
:>jsonarr string filename: filename of capture
|
||||||
|
:>jsonarr string id: unique id of the capture object
|
||||||
|
:>jsonarr boolean keep_on_disk: keep the capture file on microscope after closing
|
||||||
|
:>jsonarr boolean locked: file locked for modifications (mostly used for video recording)
|
||||||
|
:>jsonarr string path: path on pi storage to the capture file, if available
|
||||||
|
:>jsonarr boolean stream: capture stored in-memory as a BytesIO stream
|
||||||
|
:>jsonarr json uri: - **download** *(string)*: api uri to the capture file download
|
||||||
|
- **state** *(string)*: api uri to the capture json representation
|
||||||
|
|
||||||
|
:>header Content-Type: application/json
|
||||||
|
:status 200: capture found
|
||||||
|
:status 404: no capture found with that id
|
||||||
|
"""
|
||||||
|
representation = captures_representation(self.microscope.camera.images)
|
||||||
|
|
||||||
|
return jsonify(representation)
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
"""
|
||||||
|
Delete all captures
|
||||||
|
|
||||||
|
.. :quickref: Captures; Delete all captures
|
||||||
|
"""
|
||||||
|
for image in self.microscope.camera.images:
|
||||||
|
image.delete()
|
||||||
|
|
||||||
|
captures = [image.state for image in self.microscope.camera.images]
|
||||||
|
|
||||||
|
return jsonify(captures)
|
||||||
|
|
||||||
|
|
||||||
|
class CaptureAPI(MicroscopeView):
|
||||||
|
def get(self, capture_id):
|
||||||
|
"""
|
||||||
|
Get JSON representation of a capture
|
||||||
|
|
||||||
|
.. :quickref: Capture; Get capture
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/ HTTP/1.1
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"available": true,
|
||||||
|
"bytestream": false,
|
||||||
|
"keep_on_disk": false,
|
||||||
|
"locked": false,
|
||||||
|
"metadata": {
|
||||||
|
"filename": "2018-11-16_10-21-53.jpeg",
|
||||||
|
"id": "d0b2067abbb946f19351e075c5e7cd5b",
|
||||||
|
"path": "capture/2018-11-16_10-21-53.jpeg",
|
||||||
|
"time": "2018-11-16_10-21-53"
|
||||||
|
}
|
||||||
|
"uri": {
|
||||||
|
"download": "/api/v1/capture/d0b2067abbb946f19351e075c5e7cd5b/download",
|
||||||
|
"state": "/api/v1/capture/d0b2067abbb946f19351e075c5e7cd5b/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:>json boolean available: availability of capture data
|
||||||
|
:>json string filename: filename of capture
|
||||||
|
:>json string id: unique id of the capture object
|
||||||
|
:>json boolean keep_on_disk: keep the capture file on microscope after closing
|
||||||
|
:>json boolean locked: file locked for modifications (mostly used for video recording)
|
||||||
|
:>json string path: path on pi storage to the capture file, if available
|
||||||
|
:>json boolean stream: capture stored in-memory as a BytesIO stream
|
||||||
|
:>json json uri: - **download** *(string)*: api uri to the capture file download
|
||||||
|
- **state** *(string)*: api uri to the capture json representation
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
representation = captures_representation(self.microscope.camera.images)[
|
||||||
|
capture_id
|
||||||
|
]
|
||||||
|
except KeyError:
|
||||||
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
|
return jsonify(representation)
|
||||||
|
|
||||||
|
def delete(self, capture_id):
|
||||||
|
"""
|
||||||
|
Delete all capture data from the Pi, even if `keep_on_disk=true;`.
|
||||||
|
|
||||||
|
.. :quickref: Capture; Delete capture.
|
||||||
|
"""
|
||||||
|
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||||
|
|
||||||
|
if not capture_obj:
|
||||||
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
|
capture_obj.delete()
|
||||||
|
|
||||||
|
return jsonify({"return": capture_id})
|
||||||
|
|
||||||
|
def put(self, capture_id):
|
||||||
|
"""
|
||||||
|
Add arbitrary metadata to the capture
|
||||||
|
|
||||||
|
.. :quickref: Capture; Update capture metadata
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PUT /camera/capture/d0b2067abbb946f19351e075c5e7cd5b HTTP/1.1
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"user_id": "ofm_1234",
|
||||||
|
"patient_number": 1452,
|
||||||
|
}
|
||||||
|
|
||||||
|
:>header Accept: application/json
|
||||||
|
|
||||||
|
:<header Content-Type: application/json
|
||||||
|
:status 200: metadata updated
|
||||||
|
"""
|
||||||
|
|
||||||
|
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||||
|
|
||||||
|
if not capture_obj:
|
||||||
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
|
data_dict = JsonResponse(request).json
|
||||||
|
|
||||||
|
capture_obj.put_metadata(data_dict)
|
||||||
|
|
||||||
|
capture_metadata = capture_obj.state
|
||||||
|
|
||||||
|
return jsonify(capture_metadata)
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadRedirectAPI(MicroscopeView):
|
||||||
|
def get(self, capture_id):
|
||||||
|
"""
|
||||||
|
Return image data for a capture.
|
||||||
|
|
||||||
|
Return capture data as an image file with the requested filename.
|
||||||
|
I.e., `/(capture_id)/download/foo.jpeg` will download the image as
|
||||||
|
`foo.jpeg`, regardless of the capture's initially set filename.
|
||||||
|
|
||||||
|
Route automatically redirects to download the capture under it's currently set filename.
|
||||||
|
I.e., `/(capture_id)/download` will
|
||||||
|
redirect to `/(capture_id)/download/(filename)`.
|
||||||
|
|
||||||
|
.. :quickref: Capture; Download capture image
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/download/2018-11-20_16-04-17.jpeg HTTP/1.1
|
||||||
|
Accept: image/jpeg
|
||||||
|
|
||||||
|
:>header Accept: image/jpeg
|
||||||
|
:query thumbnail: return an image thumbnail e.g. ?thumbnail=true
|
||||||
|
|
||||||
|
:>header Content-Type: image/jpeg
|
||||||
|
:status 200: capture data found
|
||||||
|
:status 404: no capture found with that id
|
||||||
|
|
||||||
|
"""
|
||||||
|
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||||
|
|
||||||
|
if not capture_obj or not capture_obj.state["available"]:
|
||||||
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
|
thumbnail = get_bool(request.args.get("thumbnail"))
|
||||||
|
|
||||||
|
return redirect(
|
||||||
|
url_for(
|
||||||
|
".capture_download",
|
||||||
|
capture_id=capture_id,
|
||||||
|
filename=capture_obj.filename,
|
||||||
|
thumbnail=thumbnail,
|
||||||
|
),
|
||||||
|
code=307,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadAPI(MicroscopeView):
|
||||||
|
def get(self, capture_id, filename):
|
||||||
|
|
||||||
|
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||||
|
|
||||||
|
if not capture_obj or not capture_obj.state["available"]:
|
||||||
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
|
thumbnail = get_bool(request.args.get("thumbnail"))
|
||||||
|
|
||||||
|
# If no filename is specified, redirect to the capture's currently set filename
|
||||||
|
if not filename:
|
||||||
|
return redirect(
|
||||||
|
url_for(
|
||||||
|
"capture_download",
|
||||||
|
capture_id=capture_id,
|
||||||
|
filename=capture_obj.filename,
|
||||||
|
thumbnail=thumbnail,
|
||||||
|
),
|
||||||
|
code=307,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Download the image data using the requested filename
|
||||||
|
if thumbnail:
|
||||||
|
img = capture_obj.thumbnail
|
||||||
|
else:
|
||||||
|
img = capture_obj.data
|
||||||
|
|
||||||
|
return send_file(img, mimetype="image/jpeg")
|
||||||
|
|
||||||
|
|
||||||
|
class TagsAPI(MicroscopeView):
|
||||||
|
def get(self, capture_id):
|
||||||
|
"""
|
||||||
|
Return tag list for a capture.
|
||||||
|
|
||||||
|
.. :quickref: Capture; Show capture tags
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1
|
||||||
|
Accept: text/json
|
||||||
|
|
||||||
|
:>header Accept: text/json
|
||||||
|
|
||||||
|
:>header Content-Type: text/json
|
||||||
|
:status 200: capture data found
|
||||||
|
:status 404: no capture found with that id
|
||||||
|
"""
|
||||||
|
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||||
|
|
||||||
|
if not capture_obj or not capture_obj.state["available"]:
|
||||||
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
|
return jsonify(capture_obj.tags)
|
||||||
|
|
||||||
|
def put(self, capture_id):
|
||||||
|
"""
|
||||||
|
Add tags to the capture
|
||||||
|
|
||||||
|
.. :quickref: Capture; Update capture tags
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PUT /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
["tests", "mytag", "someothertag"]
|
||||||
|
|
||||||
|
:>header Accept: application/json
|
||||||
|
|
||||||
|
:<header Content-Type: application/json
|
||||||
|
:status 200: metadata updated
|
||||||
|
"""
|
||||||
|
|
||||||
|
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||||
|
|
||||||
|
if not capture_obj or not capture_obj.state["available"]:
|
||||||
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
|
data_dict = JsonResponse(request).json
|
||||||
|
|
||||||
|
if type(data_dict) != list:
|
||||||
|
return abort(400)
|
||||||
|
|
||||||
|
capture_obj.put_tags(data_dict)
|
||||||
|
|
||||||
|
return jsonify(capture_obj.tags)
|
||||||
|
|
||||||
|
def delete(self, capture_id):
|
||||||
|
"""
|
||||||
|
Delete tags from the capture
|
||||||
|
|
||||||
|
.. :quickref: Capture; Delete tags.
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
DELETE /camera/capture/d0b2067abbb946f19351e075c5e7cd5b/tags HTTP/1.1
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
["tests", "mytag"]
|
||||||
|
|
||||||
|
:>header Accept: application/json
|
||||||
|
|
||||||
|
:<header Content-Type: application/json
|
||||||
|
:status 200: metadata updated
|
||||||
|
"""
|
||||||
|
capture_obj = self.microscope.camera.image_from_id(capture_id)
|
||||||
|
|
||||||
|
if not capture_obj:
|
||||||
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
|
data_dict = JsonResponse(request).json
|
||||||
|
|
||||||
|
if type(data_dict) != list:
|
||||||
|
return abort(400)
|
||||||
|
|
||||||
|
for tag in data_dict:
|
||||||
|
capture_obj.delete_tag(str(tag))
|
||||||
|
|
||||||
|
return jsonify(capture_obj.tags)
|
||||||
|
|
||||||
|
|
||||||
|
def construct_blueprint(microscope_obj):
|
||||||
|
|
||||||
|
blueprint = blueprint_for_module(__name__)
|
||||||
|
|
||||||
|
# Tag routes
|
||||||
|
blueprint.add_url_rule(
|
||||||
|
"/<capture_id>/tags",
|
||||||
|
view_func=TagsAPI.as_view("capture_tags", microscope=microscope_obj),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Capture routes
|
||||||
|
blueprint.add_url_rule(
|
||||||
|
"/<capture_id>/download/<filename>",
|
||||||
|
view_func=DownloadAPI.as_view("capture_download", microscope=microscope_obj),
|
||||||
|
)
|
||||||
|
|
||||||
|
blueprint.add_url_rule(
|
||||||
|
"/<capture_id>/download",
|
||||||
|
view_func=DownloadRedirectAPI.as_view(
|
||||||
|
"capture_download_redirect", microscope=microscope_obj
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
blueprint.add_url_rule(
|
||||||
|
"/<capture_id>/",
|
||||||
|
view_func=CaptureAPI.as_view("capture", microscope=microscope_obj),
|
||||||
|
)
|
||||||
|
|
||||||
|
blueprint.add_url_rule(
|
||||||
|
"/", view_func=ListAPI.as_view("captures", microscope=microscope_obj)
|
||||||
|
)
|
||||||
|
return blueprint
|
||||||
93
openflexure_microscope/api/v2/blueprints/plugins.py
Normal file
93
openflexure_microscope/api/v2/blueprints/plugins.py
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
"""
|
||||||
|
Top-level representation of attached and enabled plugins
|
||||||
|
"""
|
||||||
|
|
||||||
|
from openflexure_microscope.plugins import PluginLoader, MicroscopePlugin
|
||||||
|
from openflexure_microscope.api.views import MicroscopeViewPlugin
|
||||||
|
from openflexure_microscope.utilities import get_docstring, description_from_view
|
||||||
|
from openflexure_microscope.api.utilities import blueprint_for_module
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, url_for
|
||||||
|
from openflexure_microscope.api.views import MicroscopeView
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import logging
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
|
def plugins_representation(plugin_loader_object: PluginLoader):
|
||||||
|
"""
|
||||||
|
Generate a dictionary representation of all plugins, including Flask route URLs
|
||||||
|
|
||||||
|
Args:
|
||||||
|
plugin_loader_object (:py:class:`openflexure_microscope.plugins.PluginLoader`): Microscope plugin loader
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary representation of all plugins
|
||||||
|
"""
|
||||||
|
plugins = {}
|
||||||
|
|
||||||
|
for plugin in plugin_loader_object.active:
|
||||||
|
logging.debug(f"Representing plugin {plugin._name}")
|
||||||
|
d = {
|
||||||
|
"python_name": plugin._name_python_safe,
|
||||||
|
"plugin": str(plugin),
|
||||||
|
"views": {},
|
||||||
|
"gui": plugin.gui,
|
||||||
|
"description": get_docstring(plugin),
|
||||||
|
}
|
||||||
|
|
||||||
|
for view_id, view_data in plugin.views.items():
|
||||||
|
logging.debug(f"Representing view {view_id}")
|
||||||
|
uri = url_for(f"v2_plugins_blueprint.{view_id}")
|
||||||
|
# Make links dictionary if it doesn't yet exist
|
||||||
|
view_d = {"links": {"self": uri}}
|
||||||
|
|
||||||
|
view_d.update(description_from_view(view_data["view"]))
|
||||||
|
|
||||||
|
d["views"][view_id] = view_d
|
||||||
|
|
||||||
|
plugins[plugin._name] = d
|
||||||
|
|
||||||
|
return plugins
|
||||||
|
|
||||||
|
|
||||||
|
class PluginFormAPI(MicroscopeView):
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
Return the current plugin forms
|
||||||
|
|
||||||
|
.. :quickref: Plugin; Get forms
|
||||||
|
|
||||||
|
Returns an array of present plugin forms (describing plugin user interfaces.)
|
||||||
|
Please note, this is *not* a list of all enabled plugins, only those with associated
|
||||||
|
user interface forms.
|
||||||
|
|
||||||
|
A complete list of enabled plugins can be found in the microscope state.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return jsonify(plugins_representation(self.microscope.plugins))
|
||||||
|
|
||||||
|
|
||||||
|
def construct_blueprint(microscope_obj):
|
||||||
|
|
||||||
|
blueprint = blueprint_for_module(__name__)
|
||||||
|
|
||||||
|
# Create a base route to return plugin API forms, if any exist
|
||||||
|
blueprint.add_url_rule(
|
||||||
|
"/", view_func=PluginFormAPI.as_view("plugins", microscope=microscope_obj)
|
||||||
|
)
|
||||||
|
|
||||||
|
# For each plugin attached to the microscope object
|
||||||
|
for plugin in microscope_obj.plugins.active:
|
||||||
|
|
||||||
|
for plugin_view_id, plugin_view in plugin.views.items():
|
||||||
|
# Add route to the plugins blueprint
|
||||||
|
blueprint.add_url_rule(
|
||||||
|
plugin_view["rule"],
|
||||||
|
view_func=plugin_view["view"].as_view(
|
||||||
|
plugin_view_id, microscope=microscope_obj, plugin=plugin
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return blueprint
|
||||||
52
openflexure_microscope/api/v2/blueprints/root.py
Normal file
52
openflexure_microscope/api/v2/blueprints/root.py
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
from flask import Blueprint, jsonify, url_for
|
||||||
|
from openflexure_microscope import Microscope
|
||||||
|
from openflexure_microscope.api.views import MicroscopeView
|
||||||
|
|
||||||
|
from openflexure_microscope.utilities import get_docstring, bottom_level_name
|
||||||
|
from openflexure_microscope.api.utilities import blueprint_name_for_module
|
||||||
|
from openflexure_microscope.api.v2.blueprints import (
|
||||||
|
settings,
|
||||||
|
status,
|
||||||
|
plugins,
|
||||||
|
captures,
|
||||||
|
actions,
|
||||||
|
streams,
|
||||||
|
)
|
||||||
|
|
||||||
|
# List of submodules containing create_blueprint methods using standard blueprint_for_module naming
|
||||||
|
_root_blueprint_modules = [settings, status, plugins, captures, actions, streams]
|
||||||
|
|
||||||
|
|
||||||
|
def root_representation():
|
||||||
|
"""
|
||||||
|
Generate a dictionar representation of all top-level blueprint rules
|
||||||
|
"""
|
||||||
|
global _root_blueprint_modules
|
||||||
|
d = {}
|
||||||
|
|
||||||
|
for blueprint_module in _root_blueprint_modules:
|
||||||
|
module_short_name = bottom_level_name(blueprint_module)
|
||||||
|
blueprint_name = blueprint_name_for_module(blueprint_module.__name__)
|
||||||
|
|
||||||
|
d[module_short_name] = {
|
||||||
|
"name": blueprint_module.__name__,
|
||||||
|
"description": get_docstring(blueprint_module),
|
||||||
|
"links": {"self": url_for(f"{blueprint_name}.{module_short_name}")},
|
||||||
|
}
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
class RootAPI(MicroscopeView):
|
||||||
|
def get(self):
|
||||||
|
|
||||||
|
return jsonify(root_representation())
|
||||||
|
|
||||||
|
|
||||||
|
def construct_blueprint(microscope_obj):
|
||||||
|
blueprint = Blueprint("root_blueprint", __name__)
|
||||||
|
|
||||||
|
blueprint.add_url_rule(
|
||||||
|
"/", view_func=RootAPI.as_view("root_repr", microscope=microscope_obj)
|
||||||
|
)
|
||||||
|
return blueprint
|
||||||
158
openflexure_microscope/api/v2/blueprints/settings.py
Normal file
158
openflexure_microscope/api/v2/blueprints/settings.py
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
"""
|
||||||
|
Writeable settings for the microscope, and attached hardware
|
||||||
|
"""
|
||||||
|
|
||||||
|
from openflexure_microscope.api.utilities import gen, JsonResponse
|
||||||
|
from openflexure_microscope.api.views import MicroscopeView
|
||||||
|
from openflexure_microscope.api.utilities import blueprint_for_module
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify, request
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsAPI(MicroscopeView):
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
JSON representation of the microscope settings.
|
||||||
|
|
||||||
|
.. :quickref: Settings; Get microscope settings
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /settings/ HTTP/1.1
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "0e2c6fac5421429aac67c7903107bdd8",
|
||||||
|
"name": "docuscope-2000",
|
||||||
|
"fov": [4100, 3146],
|
||||||
|
"camera_settings": {
|
||||||
|
"image_resolution": [2592, 1944],
|
||||||
|
"numpy_resolution": [1312, 976],
|
||||||
|
"video_resolution": [832, 624],
|
||||||
|
"jpeg_quality": 75,
|
||||||
|
"picamera_settings": {
|
||||||
|
"analog_gain": 1.0,
|
||||||
|
"digital_gain": 1.0,
|
||||||
|
"awb_gains": [0.92578125, 2.94921875],
|
||||||
|
"awb_mode": "off",
|
||||||
|
"exposure_mode": "off",
|
||||||
|
"framerate": 24.0,
|
||||||
|
"saturation": 0,
|
||||||
|
"shutter_speed": 5378
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"stage_settings": {
|
||||||
|
"backlash": {
|
||||||
|
"x": 256,
|
||||||
|
"y": 256,
|
||||||
|
"z": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"plugins": [
|
||||||
|
"openflexure_microscope.plugins.default.autofocus:AutofocusPlugin",
|
||||||
|
"openflexure_microscope.plugins.default.scan:ScanPlugin",
|
||||||
|
"openflexure_microscope.plugins.default.camera_calibration:Plugin"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:>header Accept: application/json
|
||||||
|
:>header Content-Type: application/json
|
||||||
|
:status 200: state available
|
||||||
|
"""
|
||||||
|
return jsonify(self.microscope.read_settings())
|
||||||
|
|
||||||
|
def put(self):
|
||||||
|
"""
|
||||||
|
Modify microscope configuration
|
||||||
|
|
||||||
|
.. :quickref: Config; Set microscope config
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
PUT /config HTTP/1.1
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "0e2c6fac5421429aac67c7903107bdd8",
|
||||||
|
"name": "docuscope-2000",
|
||||||
|
"fov": [4100, 3146],
|
||||||
|
"camera_settings": {
|
||||||
|
"image_resolution": [2592, 1944],
|
||||||
|
"numpy_resolution": [1312, 976],
|
||||||
|
"video_resolution": [832, 624],
|
||||||
|
"jpeg_quality": 75,
|
||||||
|
"picamera_settings": {
|
||||||
|
"analog_gain": 1.0,
|
||||||
|
"digital_gain": 1.0,
|
||||||
|
"awb_gains": [0.92578125, 2.94921875],
|
||||||
|
"awb_mode": "off",
|
||||||
|
"exposure_mode": "off",
|
||||||
|
"framerate": 24.0,
|
||||||
|
"saturation": 0,
|
||||||
|
"shutter_speed": 5378
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"stage_settings": {
|
||||||
|
"backlash": {
|
||||||
|
"x": 256,
|
||||||
|
"y": 256,
|
||||||
|
"z": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"plugins": [
|
||||||
|
"openflexure_microscope.plugins.default.autofocus:AutofocusPlugin",
|
||||||
|
"openflexure_microscope.plugins.default.scan:ScanPlugin",
|
||||||
|
"openflexure_microscope.plugins.default.camera_calibration:Plugin"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
:>header Accept: application/json
|
||||||
|
|
||||||
|
:<json string id: Unique string identifier of the microscope.
|
||||||
|
:<json string name: Friendly name for the microscope
|
||||||
|
:<json array fov: Field of view (motor steps per full width and height of frame)
|
||||||
|
:<json json camera_settings: - **image_resolution** *(array)*: Resolution of full image captures
|
||||||
|
- **numpy_resolution** *(array)*: Resolution of full numpy array captures
|
||||||
|
- **video_resolution** *(array)*: Resolution of video recordings, low res image captures, and the preview stream
|
||||||
|
- **jpeg_quality** *(int)*: Quality in which to store JPEG capture data
|
||||||
|
- **picamera_settings** *(json)*: Key-value pairs to apply directly to any attached PiCamera object
|
||||||
|
:<json json stage_settings: - **backlash** *(json)*: x, y, and z backlash compensation, in motor steps
|
||||||
|
:<json array plugins: Array of plugin paths to load. Requires reloading the microscope object after applying
|
||||||
|
|
||||||
|
:<header Content-Type: application/json
|
||||||
|
:status 200: capture created
|
||||||
|
|
||||||
|
"""
|
||||||
|
payload = JsonResponse(request)
|
||||||
|
|
||||||
|
logging.debug("Updating settings from PUT request:")
|
||||||
|
logging.debug(payload.json)
|
||||||
|
|
||||||
|
self.microscope.apply_settings(payload.json)
|
||||||
|
self.microscope.save_settings()
|
||||||
|
|
||||||
|
return jsonify(self.microscope.read_settings(json_safe=True))
|
||||||
|
|
||||||
|
|
||||||
|
def construct_blueprint(microscope_obj):
|
||||||
|
|
||||||
|
blueprint = blueprint_for_module(__name__)
|
||||||
|
|
||||||
|
blueprint.add_url_rule(
|
||||||
|
"/", view_func=SettingsAPI.as_view("settings", microscope=microscope_obj)
|
||||||
|
)
|
||||||
|
|
||||||
|
return blueprint
|
||||||
69
openflexure_microscope/api/v2/blueprints/status.py
Normal file
69
openflexure_microscope/api/v2/blueprints/status.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
"""
|
||||||
|
Read-only status of the microscope, and attached hardware info
|
||||||
|
"""
|
||||||
|
|
||||||
|
from openflexure_microscope.api.views import MicroscopeView
|
||||||
|
from openflexure_microscope.api.utilities import blueprint_for_module
|
||||||
|
|
||||||
|
from flask import Blueprint, jsonify
|
||||||
|
|
||||||
|
|
||||||
|
class StatusAPI(MicroscopeView):
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
JSON representation of the microscope status (read-only properties, modifiable with actions)
|
||||||
|
|
||||||
|
.. :quickref: Status; Microscope status
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /status/ HTTP/1.1
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"camera": {
|
||||||
|
"preview_active": false,
|
||||||
|
"record_active": false,
|
||||||
|
"stream_active": true
|
||||||
|
},
|
||||||
|
"plugin": {},
|
||||||
|
"stage": {
|
||||||
|
"backlash": {
|
||||||
|
"x": 128,
|
||||||
|
"y": 128,
|
||||||
|
"z": 128
|
||||||
|
},
|
||||||
|
"position": {
|
||||||
|
"x": -8080,
|
||||||
|
"y": 5665,
|
||||||
|
"z": -12600
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:>header Accept: application/json
|
||||||
|
:>header Content-Type: application/json
|
||||||
|
:status 200: state available
|
||||||
|
"""
|
||||||
|
return jsonify(self.microscope.status)
|
||||||
|
|
||||||
|
|
||||||
|
def construct_blueprint(microscope_obj):
|
||||||
|
|
||||||
|
blueprint = blueprint_for_module(__name__)
|
||||||
|
|
||||||
|
blueprint.add_url_rule(
|
||||||
|
"/", view_func=StatusAPI.as_view("status", microscope=microscope_obj)
|
||||||
|
)
|
||||||
|
|
||||||
|
return blueprint
|
||||||
107
openflexure_microscope/api/v2/blueprints/streams.py
Normal file
107
openflexure_microscope/api/v2/blueprints/streams.py
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
"""
|
||||||
|
Top-level description of routes related to live camera stream data
|
||||||
|
"""
|
||||||
|
|
||||||
|
from openflexure_microscope.api.utilities import gen, JsonResponse
|
||||||
|
from openflexure_microscope.api.views import MicroscopeView
|
||||||
|
from openflexure_microscope.api.utilities import (
|
||||||
|
blueprint_for_module,
|
||||||
|
blueprint_name_for_module,
|
||||||
|
)
|
||||||
|
from openflexure_microscope.utilities import description_from_view
|
||||||
|
|
||||||
|
from flask import Response, Blueprint, jsonify, request, url_for
|
||||||
|
|
||||||
|
|
||||||
|
class StreamAPI(MicroscopeView):
|
||||||
|
def get(self):
|
||||||
|
return jsonify(streams_representation())
|
||||||
|
|
||||||
|
|
||||||
|
class MjpegAPI(MicroscopeView):
|
||||||
|
"""
|
||||||
|
Real-time MJPEG stream from the microscope camera
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
Real-time MJPEG stream from the microscope camera
|
||||||
|
|
||||||
|
.. :quickref: Stream; Camera MJPEG stream
|
||||||
|
|
||||||
|
:>header Accept: image/jpeg
|
||||||
|
:>header Content-Type: image/jpeg
|
||||||
|
:status 200: stream active
|
||||||
|
"""
|
||||||
|
# Restart stream worker thread
|
||||||
|
self.microscope.camera.start_worker()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
gen(self.microscope.camera),
|
||||||
|
mimetype="multipart/x-mixed-replace; boundary=frame",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SnapshotAPI(MicroscopeView):
|
||||||
|
"""
|
||||||
|
Single JPEG snapshot from the camera stream
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
Single snapshot from the camera stream
|
||||||
|
|
||||||
|
.. :quickref: Stream; Camera snapshot
|
||||||
|
|
||||||
|
:>header Accept: image/jpeg
|
||||||
|
:>header Content-Type: image/jpeg
|
||||||
|
:status 200: stream active
|
||||||
|
"""
|
||||||
|
# Restart stream worker thread
|
||||||
|
self.microscope.camera.start_worker()
|
||||||
|
|
||||||
|
return Response(self.microscope.camera.get_frame(), mimetype="image/jpeg")
|
||||||
|
|
||||||
|
|
||||||
|
_streams = {
|
||||||
|
"mjpeg": {"rule": "/mjpeg", "view_class": MjpegAPI, "conditions": True},
|
||||||
|
"snapshot": {"rule": "/snapshot", "view_class": SnapshotAPI, "conditions": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def enabled_streams():
|
||||||
|
global _streams
|
||||||
|
return {k: v for k, v in _streams.items() if v["conditions"]}
|
||||||
|
|
||||||
|
|
||||||
|
def streams_representation():
|
||||||
|
global _streams
|
||||||
|
|
||||||
|
streams = {}
|
||||||
|
for name, stream in enabled_streams().items():
|
||||||
|
d = {"links": {"self": url_for(f".{name}")}}
|
||||||
|
|
||||||
|
d.update(description_from_view(stream["view_class"]))
|
||||||
|
|
||||||
|
streams[name] = d
|
||||||
|
|
||||||
|
return streams
|
||||||
|
|
||||||
|
|
||||||
|
def construct_blueprint(microscope_obj):
|
||||||
|
|
||||||
|
blueprint = blueprint_for_module(__name__)
|
||||||
|
|
||||||
|
# For each enabled stream route defined in our dictionary above
|
||||||
|
for name, stream in enabled_streams().items():
|
||||||
|
# Add the action to our blueprint
|
||||||
|
blueprint.add_url_rule(
|
||||||
|
stream["rule"],
|
||||||
|
view_func=stream["view_class"].as_view(name, microscope=microscope_obj),
|
||||||
|
)
|
||||||
|
|
||||||
|
blueprint.add_url_rule(
|
||||||
|
"/", view_func=StreamAPI.as_view("streams", microscope=microscope_obj)
|
||||||
|
)
|
||||||
|
|
||||||
|
return blueprint
|
||||||
158
openflexure_microscope/api/v2/blueprints/tasks.py
Normal file
158
openflexure_microscope/api/v2/blueprints/tasks.py
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
from openflexure_microscope.api.views import MethodView
|
||||||
|
from flask import jsonify, abort, Blueprint, url_for
|
||||||
|
|
||||||
|
from openflexure_microscope.common import tasks
|
||||||
|
|
||||||
|
|
||||||
|
def tasks_representation():
|
||||||
|
"""
|
||||||
|
Generate a dictionary representation of all tasks, including Flask route URLs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionary representation of all tasks
|
||||||
|
"""
|
||||||
|
tasks_dict = tasks.states()
|
||||||
|
|
||||||
|
for task_key, task_repr in tasks_dict.items():
|
||||||
|
# Add API routes to returned representations
|
||||||
|
extra_state = {
|
||||||
|
"links": {"self": "{}".format(url_for(".task", task_id=task_key))}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks_dict[task_key].update(extra_state)
|
||||||
|
|
||||||
|
return tasks_dict
|
||||||
|
|
||||||
|
|
||||||
|
class TaskListAPI(MethodView):
|
||||||
|
def get(self):
|
||||||
|
"""
|
||||||
|
Get list of long-running tasks.
|
||||||
|
|
||||||
|
.. :quickref: Tasks; Get collection of tasks
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"end_time": "2019-01-23 16-33-33",
|
||||||
|
"id": "db13a66787e1419bb06b1504e4d80b0c",
|
||||||
|
"return": [
|
||||||
|
0.848622546386467,
|
||||||
|
0.6106785018091292,
|
||||||
|
],
|
||||||
|
"start_time": "2019-01-23 16-33-13",
|
||||||
|
"status": "success"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"end_time": null,
|
||||||
|
"id": "df46558cc8844924821bd0181881871e",
|
||||||
|
"return": null,
|
||||||
|
"start_time": "2019-01-23 16-34-54",
|
||||||
|
"status": "running"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
:>header Accept: application/json
|
||||||
|
:query include_unavailable: return json representations of captures that have been completely deleted
|
||||||
|
|
||||||
|
:>header Content-Type: application/json
|
||||||
|
"""
|
||||||
|
|
||||||
|
return jsonify(tasks_representation())
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
"""
|
||||||
|
Clean list of long-running tasks (running tasks persist).
|
||||||
|
|
||||||
|
.. :quickref: Tasks; Clean collection of tasks
|
||||||
|
|
||||||
|
:>header Accept: application/json
|
||||||
|
|
||||||
|
:>header Content-Type: application/json
|
||||||
|
"""
|
||||||
|
|
||||||
|
tasks.cleanup_tasks()
|
||||||
|
|
||||||
|
return jsonify(tasks_representation())
|
||||||
|
|
||||||
|
|
||||||
|
class TaskAPI(MethodView):
|
||||||
|
def get(self, task_id):
|
||||||
|
"""
|
||||||
|
Get JSON representation of a task
|
||||||
|
|
||||||
|
.. :quickref: Tasks; Get task
|
||||||
|
|
||||||
|
**Example request**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /task/db13a66787e1419bb06b1504e4d80b0c/ HTTP/1.1
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
**Example response**:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Vary: Accept
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"end_time": "2019-01-23 16-33-33",
|
||||||
|
"id": "db13a66787e1419bb06b1504e4d80b0c",
|
||||||
|
"return": [
|
||||||
|
0.848622546386467,
|
||||||
|
0.6106785018091292,
|
||||||
|
],
|
||||||
|
"start_time": "2019-01-23 16-33-13",
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
task = tasks_representation()[task_id]
|
||||||
|
except KeyError:
|
||||||
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
|
# Return task state
|
||||||
|
return jsonify(task.state)
|
||||||
|
|
||||||
|
def delete(self, task_id):
|
||||||
|
"""
|
||||||
|
Terminate a particular task.
|
||||||
|
|
||||||
|
.. :quickref: Tasks; Terminate a task
|
||||||
|
|
||||||
|
:>header Accept: application/json
|
||||||
|
|
||||||
|
:>header Content-Type: application/json
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
task = tasks_representation()[task_id]
|
||||||
|
except KeyError:
|
||||||
|
return abort(404) # 404 Not Found
|
||||||
|
|
||||||
|
task.terminate()
|
||||||
|
|
||||||
|
return jsonify(task.state)
|
||||||
|
|
||||||
|
|
||||||
|
def construct_blueprint(microscope_obj):
|
||||||
|
|
||||||
|
blueprint = Blueprint("v2_tasks_blueprint", __name__)
|
||||||
|
|
||||||
|
blueprint.add_url_rule("/", view_func=TaskListAPI.as_view("task_list"))
|
||||||
|
|
||||||
|
blueprint.add_url_rule("/<task_id>/", view_func=TaskAPI.as_view("task"))
|
||||||
|
|
||||||
|
return blueprint
|
||||||
|
|
@ -100,7 +100,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
last_access (time): Time of last client access to the camera
|
last_access (time): Time of last client access to the camera
|
||||||
stream_timeout (int): Number of inactive seconds before timing out the stream
|
stream_timeout (int): Number of inactive seconds before timing out the stream
|
||||||
stream_timeout_enabled (bool): Enable or disable timing out the stream
|
stream_timeout_enabled (bool): Enable or disable timing out the stream
|
||||||
state (dict): Dictionary for capture state
|
status (dict): Dictionary for capture state
|
||||||
paths (dict): Dictionary of capture paths
|
paths (dict): Dictionary of capture paths
|
||||||
images (list): List of image capture objects
|
images (list): List of image capture objects
|
||||||
videos (list): List of video capture objects
|
videos (list): List of video capture objects
|
||||||
|
|
@ -120,7 +120,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
self.stream_timeout = 20
|
self.stream_timeout = 20
|
||||||
self.stream_timeout_enabled = False
|
self.stream_timeout_enabled = False
|
||||||
|
|
||||||
self.state = {"board": None}
|
self.status = {"board": None}
|
||||||
|
|
||||||
# TODO: Load/save these to config
|
# TODO: Load/save these to config
|
||||||
self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH}
|
self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH}
|
||||||
|
|
@ -130,7 +130,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
self.videos = []
|
self.videos = []
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def apply_config(self, config: dict):
|
def apply_settings(self, config: dict):
|
||||||
"""Update settings from a config dictionary"""
|
"""Update settings from a config dictionary"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# Apply valid config params to camera object
|
# Apply valid config params to camera object
|
||||||
|
|
@ -139,11 +139,11 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
setattr(self, key, value) # Set to the target value
|
setattr(self, key, value) # Set to the target value
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def read_config(self) -> dict:
|
def read_settings(self) -> dict:
|
||||||
"""Return the current settings as a dictionary"""
|
"""Return the current settings as a dictionary"""
|
||||||
return {"paths": self.paths}
|
return {"paths": self.paths}
|
||||||
|
|
||||||
def save_config(self):
|
def save_settings(self):
|
||||||
"""(Optional) Save any settings to disk that need to be stored"""
|
"""(Optional) Save any settings to disk that need to be stored"""
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -187,7 +187,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
self.last_access = time.time()
|
self.last_access = time.time()
|
||||||
self.stop = False
|
self.stop = False
|
||||||
|
|
||||||
if not self.state["stream_active"]:
|
if not self.status["stream_active"]:
|
||||||
# start background frame thread
|
# start background frame thread
|
||||||
self.thread = threading.Thread(target=self._thread)
|
self.thread = threading.Thread(target=self._thread)
|
||||||
self.thread.daemon = True
|
self.thread.daemon = True
|
||||||
|
|
@ -207,12 +207,12 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
logging.debug("Stopping worker thread")
|
logging.debug("Stopping worker thread")
|
||||||
timeout_time = time.time() + timeout
|
timeout_time = time.time() + timeout
|
||||||
|
|
||||||
if self.state["stream_active"]:
|
if self.status["stream_active"]:
|
||||||
self.stop = True
|
self.stop = True
|
||||||
self.thread.join() # Wait for stream thread to exit
|
self.thread.join() # Wait for stream thread to exit
|
||||||
logging.debug("Waiting for stream thread to exit.")
|
logging.debug("Waiting for stream thread to exit.")
|
||||||
|
|
||||||
while self.state["stream_active"]:
|
while self.status["stream_active"]:
|
||||||
if time.time() > timeout_time:
|
if time.time() > timeout_time:
|
||||||
logging.debug("Timeout waiting for worker thread close.")
|
logging.debug("Timeout waiting for worker thread close.")
|
||||||
raise TimeoutError("Timeout waiting for worker thread close.")
|
raise TimeoutError("Timeout waiting for worker thread close.")
|
||||||
|
|
@ -352,7 +352,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
self.frames_iterator = self.frames()
|
self.frames_iterator = self.frames()
|
||||||
logging.debug("Entering worker thread.")
|
logging.debug("Entering worker thread.")
|
||||||
|
|
||||||
self.state["stream_active"] = True
|
self.status["stream_active"] = True
|
||||||
|
|
||||||
for frame in self.frames_iterator:
|
for frame in self.frames_iterator:
|
||||||
self.frame = frame
|
self.frame = frame
|
||||||
|
|
@ -365,7 +365,7 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
and ( # If using timeout
|
and ( # If using timeout
|
||||||
time.time() - self.last_access > self.stream_timeout
|
time.time() - self.last_access > self.stream_timeout
|
||||||
)
|
)
|
||||||
and not self.state[ # And timeout time
|
and not self.status[ # And timeout time
|
||||||
"preview_active"
|
"preview_active"
|
||||||
] # And GPU preview is not active
|
] # And GPU preview is not active
|
||||||
):
|
):
|
||||||
|
|
@ -383,4 +383,4 @@ class BaseCamera(metaclass=ABCMeta):
|
||||||
|
|
||||||
logging.debug("BaseCamera worker thread exiting...")
|
logging.debug("BaseCamera worker thread exiting...")
|
||||||
# Set stream_activate state
|
# Set stream_activate state
|
||||||
self.state["stream_active"] = False
|
self.status["stream_active"] = False
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,9 @@ def pull_usercomment_dict(filepath):
|
||||||
return json.loads(exif_dict["Exif"][37510].decode())
|
return json.loads(exif_dict["Exif"][37510].decode())
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
# TODO: Remove YAML support in a later version
|
# TODO: Remove YAML support in a later version
|
||||||
logging.warning(f"Capture {filepath} has metadata stored in YAML format. This is now deprecated in favour of JSON.")
|
logging.warning(
|
||||||
|
f"Capture {filepath} has metadata stored in YAML format. This is now deprecated in favour of JSON."
|
||||||
|
)
|
||||||
return yaml.load(exif_dict["Exif"][37510].decode())
|
return yaml.load(exif_dict["Exif"][37510].decode())
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
@ -97,7 +99,7 @@ def capture_from_exif(path, exif_dict):
|
||||||
capture.timestring = exif_dict["time"]
|
capture.timestring = exif_dict["time"]
|
||||||
capture.format = exif_dict["format"]
|
capture.format = exif_dict["format"]
|
||||||
|
|
||||||
capture._metadata = exif_dict["custom"]
|
capture.custom_metadata = exif_dict["custom"]
|
||||||
capture.tags = exif_dict["tags"]
|
capture.tags = exif_dict["tags"]
|
||||||
|
|
||||||
return capture
|
return capture
|
||||||
|
|
@ -110,7 +112,7 @@ class CaptureObject(object):
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
timestring (str): Timestring of capture creation time
|
timestring (str): Timestring of capture creation time
|
||||||
_metadata (dict): Dictionary of custom metadata to be included in metadata file
|
custom_metadata (dict): Dictionary of custom metadata to be included in metadata file
|
||||||
tags (list): List of tags. Essentially just as extra custom metadata field, but useful for quick organisation
|
tags (list): List of tags. Essentially just as extra custom metadata field, but useful for quick organisation
|
||||||
filefolder (str): Folder in which the capture file will be stored
|
filefolder (str): Folder in which the capture file will be stored
|
||||||
filename (str): Full name of the capture file
|
filename (str): Full name of the capture file
|
||||||
|
|
@ -132,7 +134,9 @@ class CaptureObject(object):
|
||||||
self.split_file_path(self.file)
|
self.split_file_path(self.file)
|
||||||
|
|
||||||
# Dictionary for storing custom metadata
|
# Dictionary for storing custom metadata
|
||||||
self._metadata = {}
|
self.custom_metadata = {}
|
||||||
|
# Dictionary for adding top-level metadata (cannmot be accessed through web API)
|
||||||
|
self.system_metadata = {}
|
||||||
|
|
||||||
# List for storing tags
|
# List for storing tags
|
||||||
self.tags = []
|
self.tags = []
|
||||||
|
|
@ -203,7 +207,7 @@ class CaptureObject(object):
|
||||||
Args:
|
Args:
|
||||||
data (dict): Dictionary of metadata to be added
|
data (dict): Dictionary of metadata to be added
|
||||||
"""
|
"""
|
||||||
self._metadata.update(data)
|
self.custom_metadata.update(data)
|
||||||
self.save_metadata()
|
self.save_metadata()
|
||||||
|
|
||||||
def save_metadata(self) -> None:
|
def save_metadata(self) -> None:
|
||||||
|
|
@ -233,13 +237,14 @@ class CaptureObject(object):
|
||||||
"""
|
"""
|
||||||
d = {
|
d = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"filename": self.filename,
|
|
||||||
"time": self.timestring,
|
"time": self.timestring,
|
||||||
"format": self.format,
|
"format": self.format,
|
||||||
"tags": self.tags,
|
"tags": self.tags,
|
||||||
"custom": self._metadata,
|
"custom": self.custom_metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
d.update(self.system_metadata)
|
||||||
|
|
||||||
# Add custom metadata to dictionary
|
# Add custom metadata to dictionary
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
@ -250,7 +255,7 @@ class CaptureObject(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Create basic state dictionary
|
# Create basic state dictionary
|
||||||
d = {"path": self.file, "metadata": self.metadata}
|
d = {"path": self.file, "filename": self.filename, "metadata": self.metadata}
|
||||||
|
|
||||||
# Combined availability of data
|
# Combined availability of data
|
||||||
if self.exists:
|
if self.exists:
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,13 @@ from typing import Tuple
|
||||||
from openflexure_microscope.camera.base import BaseCamera
|
from openflexure_microscope.camera.base import BaseCamera
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
PIL spams the logger with debug-level information. This is a pain when debugging api.app.
|
||||||
|
We override the logging settings in api.app by setting a level for PIL here.
|
||||||
|
"""
|
||||||
|
pil_logger = logging.getLogger("PIL")
|
||||||
|
pil_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
# MAIN CLASS
|
# MAIN CLASS
|
||||||
class MockStreamer(BaseCamera):
|
class MockStreamer(BaseCamera):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -26,7 +33,7 @@ class MockStreamer(BaseCamera):
|
||||||
BaseCamera.__init__(self)
|
BaseCamera.__init__(self)
|
||||||
|
|
||||||
# Store state of PiCameraStreamer
|
# Store state of PiCameraStreamer
|
||||||
self.state.update(
|
self.status.update(
|
||||||
{"stream_active": False, "record_active": False, "board": None}
|
{"stream_active": False, "record_active": False, "board": None}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -74,13 +81,13 @@ class MockStreamer(BaseCamera):
|
||||||
BaseCamera.close(self)
|
BaseCamera.close(self)
|
||||||
|
|
||||||
# HANDLE SETTINGS
|
# HANDLE SETTINGS
|
||||||
def read_config(self) -> dict:
|
def read_settings(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Return config dictionary of the PiCameraStreamer.
|
Return config dictionary of the PiCameraStreamer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Get config items from the base class
|
# Get config items from the base class
|
||||||
conf_dict = BaseCamera.read_config(self)
|
conf_dict = BaseCamera.read_settings(self)
|
||||||
|
|
||||||
# Include device-specific config items
|
# Include device-specific config items
|
||||||
conf_dict.update(
|
conf_dict.update(
|
||||||
|
|
@ -94,7 +101,7 @@ class MockStreamer(BaseCamera):
|
||||||
|
|
||||||
return conf_dict
|
return conf_dict
|
||||||
|
|
||||||
def apply_config(self, config: dict):
|
def apply_settings(self, config: dict):
|
||||||
"""
|
"""
|
||||||
Write a config dictionary to the PiCameraStreamer config.
|
Write a config dictionary to the PiCameraStreamer config.
|
||||||
|
|
||||||
|
|
@ -110,7 +117,7 @@ class MockStreamer(BaseCamera):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
|
||||||
# Apply valid config params to camera object
|
# Apply valid config params to camera object
|
||||||
if not self.state["record_active"]: # If not recording a video
|
if not self.status["record_active"]: # If not recording a video
|
||||||
|
|
||||||
for key, value in config.items(): # For each provided setting
|
for key, value in config.items(): # For each provided setting
|
||||||
if hasattr(self, key):
|
if hasattr(self, key):
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ from .set_picamera_gain import set_analog_gain, set_digital_gain
|
||||||
|
|
||||||
from openflexure_microscope.config import settings_file_path
|
from openflexure_microscope.config import settings_file_path
|
||||||
|
|
||||||
|
|
||||||
# MAIN CLASS
|
# MAIN CLASS
|
||||||
class PiCameraStreamer(BaseCamera):
|
class PiCameraStreamer(BaseCamera):
|
||||||
"""Raspberry Pi camera implementation of PiCameraStreamer."""
|
"""Raspberry Pi camera implementation of PiCameraStreamer."""
|
||||||
|
|
@ -64,7 +65,10 @@ class PiCameraStreamer(BaseCamera):
|
||||||
"exposure_compensation",
|
"exposure_compensation",
|
||||||
"image_effect",
|
"image_effect",
|
||||||
"meter_mode",
|
"meter_mode",
|
||||||
"sharpness"
|
"sharpness",
|
||||||
|
"annotate_text",
|
||||||
|
"annotate_text_size",
|
||||||
|
"zoom",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -75,8 +79,8 @@ class PiCameraStreamer(BaseCamera):
|
||||||
picamera.PiCamera()
|
picamera.PiCamera()
|
||||||
) #: :py:class:`picamera.PiCamera`: Picamera object
|
) #: :py:class:`picamera.PiCamera`: Picamera object
|
||||||
|
|
||||||
# Store state of PiCameraStreamer
|
# Store status of PiCameraStreamer
|
||||||
self.state.update(
|
self.status.update(
|
||||||
{
|
{
|
||||||
"stream_active": False,
|
"stream_active": False,
|
||||||
"record_active": False,
|
"record_active": False,
|
||||||
|
|
@ -89,15 +93,26 @@ class PiCameraStreamer(BaseCamera):
|
||||||
self.set_zoom(1.0)
|
self.set_zoom(1.0)
|
||||||
|
|
||||||
# Set default settings
|
# Set default settings
|
||||||
self.image_resolution = tuple(self.camera.MAX_RESOLUTION) #: tuple: Resolution for image captures
|
self.image_resolution = tuple(
|
||||||
self.stream_resolution = (832, 624) #: tuple: Resolution for stream and video captures
|
self.camera.MAX_RESOLUTION
|
||||||
self.numpy_resolution = (1312, 976) #: tuple: Resolution for numpy array captures
|
) #: tuple: Resolution for image captures
|
||||||
|
self.stream_resolution = (
|
||||||
|
832,
|
||||||
|
624,
|
||||||
|
) #: tuple: Resolution for stream and video captures
|
||||||
|
self.numpy_resolution = (
|
||||||
|
1312,
|
||||||
|
976,
|
||||||
|
) #: tuple: Resolution for numpy array captures
|
||||||
self.jpeg_quality = 75 #: int: JPEG quality
|
self.jpeg_quality = 75 #: int: JPEG quality
|
||||||
|
|
||||||
# Set default lens shading table path
|
# Set default lens shading table path
|
||||||
self.picamera_lst_path = settings_file_path("picamera_lst.npy") #: str: Path of .npy lens shading table file
|
self.picamera_lst_path = settings_file_path(
|
||||||
|
"picamera_lst.npy"
|
||||||
|
) #: str: Path of .npy lens shading table file
|
||||||
|
|
||||||
# Update board identifier
|
# Update board identifier
|
||||||
self.state.update({})
|
self.status.update({})
|
||||||
|
|
||||||
# Create an empty stream
|
# Create an empty stream
|
||||||
self.stream = io.BytesIO()
|
self.stream = io.BytesIO()
|
||||||
|
|
@ -118,13 +133,13 @@ class PiCameraStreamer(BaseCamera):
|
||||||
self.camera.close()
|
self.camera.close()
|
||||||
|
|
||||||
# HANDLE SETTINGS
|
# HANDLE SETTINGS
|
||||||
def read_config(self) -> dict:
|
def read_settings(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Return config dictionary of the PiCameraStreamer.
|
Return config dictionary of the PiCameraStreamer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Get config items from the base class
|
# Get config items from the base class
|
||||||
conf_dict = BaseCamera.read_config(self)
|
conf_dict = BaseCamera.read_settings(self)
|
||||||
|
|
||||||
# Include device-specific config items
|
# Include device-specific config items
|
||||||
conf_dict.update(
|
conf_dict.update(
|
||||||
|
|
@ -133,7 +148,9 @@ class PiCameraStreamer(BaseCamera):
|
||||||
"image_resolution": self.image_resolution,
|
"image_resolution": self.image_resolution,
|
||||||
"numpy_resolution": self.numpy_resolution,
|
"numpy_resolution": self.numpy_resolution,
|
||||||
"jpeg_quality": self.jpeg_quality,
|
"jpeg_quality": self.jpeg_quality,
|
||||||
"picamera_lst_path": self.picamera_lst_path if os.path.isfile(self.picamera_lst_path) else None,
|
"picamera_lst_path": self.picamera_lst_path
|
||||||
|
if os.path.isfile(self.picamera_lst_path)
|
||||||
|
else None,
|
||||||
"picamera_settings": {},
|
"picamera_settings": {},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -149,12 +166,12 @@ class PiCameraStreamer(BaseCamera):
|
||||||
|
|
||||||
return conf_dict
|
return conf_dict
|
||||||
|
|
||||||
def save_config(self):
|
def save_settings(self):
|
||||||
"""Save lens-shading table to disk"""
|
"""Save lens-shading table to disk"""
|
||||||
logging.info("Saving picamera_lst to {}".format(self.picamera_lst_path))
|
logging.info("Saving picamera_lst to {}".format(self.picamera_lst_path))
|
||||||
self.save_lens_shading_table()
|
self.save_lens_shading_table()
|
||||||
|
|
||||||
def apply_config(self, config: dict):
|
def apply_settings(self, config: dict):
|
||||||
"""
|
"""
|
||||||
Write a config dictionary to the PiCameraStreamer config.
|
Write a config dictionary to the PiCameraStreamer config.
|
||||||
|
|
||||||
|
|
@ -172,10 +189,10 @@ class PiCameraStreamer(BaseCamera):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
|
|
||||||
# Apply valid config params to Picamera object
|
# Apply valid config params to Picamera object
|
||||||
if not self.state["record_active"]: # If not recording a video
|
if not self.status["record_active"]: # If not recording a video
|
||||||
|
|
||||||
# Pause stream while changing settings
|
# Pause stream while changing settings
|
||||||
if self.state["stream_active"]: # If stream is active
|
if self.status["stream_active"]: # If stream is active
|
||||||
logging.info("Pausing stream to update config.")
|
logging.info("Pausing stream to update config.")
|
||||||
self.stop_stream_recording() # Pause stream
|
self.stop_stream_recording() # Pause stream
|
||||||
paused_stream = True # Remember to unpause stream when done
|
paused_stream = True # Remember to unpause stream when done
|
||||||
|
|
@ -268,7 +285,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
"""
|
"""
|
||||||
Read the current lens shading table as a numpy array, if it exists. Return None otherwise.
|
Read the current lens shading table as a numpy array, if it exists. Return None otherwise.
|
||||||
"""
|
"""
|
||||||
if hasattr(self.camera, 'lens_shading_table'):
|
if hasattr(self.camera, "lens_shading_table"):
|
||||||
return self.camera.lens_shading_table
|
return self.camera.lens_shading_table
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
@ -292,20 +309,25 @@ class PiCameraStreamer(BaseCamera):
|
||||||
elif (type(lst_array_or_path) == str) and os.path.isfile(lst_array_or_path):
|
elif (type(lst_array_or_path) == str) and os.path.isfile(lst_array_or_path):
|
||||||
self.camera.lens_shading_table = np.load(lst_array_or_path)
|
self.camera.lens_shading_table = np.load(lst_array_or_path)
|
||||||
else:
|
else:
|
||||||
logging.error("Unsupported or missing data for camera lens_shading_table. Must be numpy ndarray, or .npy file path string. Skipping.")
|
logging.error(
|
||||||
|
"Unsupported or missing data for camera lens_shading_table. Must be numpy ndarray, or .npy file path string. Skipping."
|
||||||
|
)
|
||||||
|
|
||||||
def set_zoom(self, zoom_value: float = 1.0) -> None:
|
def set_zoom(self, zoom_value: float = 1.0) -> None:
|
||||||
"""
|
"""
|
||||||
Change the camera zoom, handling re-centering and scaling.
|
Change the camera zoom, handling re-centering and scaling.
|
||||||
"""
|
"""
|
||||||
|
logging.warning(
|
||||||
|
"set_zoom is deprecated. Please use the 'zoom' property in picamera_settings."
|
||||||
|
)
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.state["zoom_value"] = float(zoom_value)
|
self.status["zoom_value"] = float(zoom_value)
|
||||||
if self.state["zoom_value"] < 1:
|
if self.status["zoom_value"] < 1:
|
||||||
self.state["zoom_value"] = 1
|
self.status["zoom_value"] = 1
|
||||||
# Richard's code for zooming !
|
# Richard's code for zooming !
|
||||||
fov = self.camera.zoom
|
fov = self.camera.zoom
|
||||||
centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0])
|
centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0])
|
||||||
size = 1.0 / self.state["zoom_value"]
|
size = 1.0 / self.status["zoom_value"]
|
||||||
# If the new zoom value would be invalid, move the centre to
|
# If the new zoom value would be invalid, move the centre to
|
||||||
# keep it within the camera's sensor (this is only relevant
|
# keep it within the camera's sensor (this is only relevant
|
||||||
# when zooming out, if the FoV is not centred on (0.5, 0.5)
|
# when zooming out, if the FoV is not centred on (0.5, 0.5)
|
||||||
|
|
@ -332,7 +354,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
self.camera.preview.window = window
|
self.camera.preview.window = window
|
||||||
if fullscreen:
|
if fullscreen:
|
||||||
self.camera.preview.fullscreen = fullscreen
|
self.camera.preview.fullscreen = fullscreen
|
||||||
self.state["preview_active"] = True
|
self.status["preview_active"] = True
|
||||||
except picamera.exc.PiCameraMMALError as e:
|
except picamera.exc.PiCameraMMALError as e:
|
||||||
logging.error(
|
logging.error(
|
||||||
"Suppressed a MMALError in start_preview. Exception: {}".format(e)
|
"Suppressed a MMALError in start_preview. Exception: {}".format(e)
|
||||||
|
|
@ -347,7 +369,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
def stop_preview(self):
|
def stop_preview(self):
|
||||||
"""Stop the on board GPU camera preview."""
|
"""Stop the on board GPU camera preview."""
|
||||||
self.camera.stop_preview()
|
self.camera.stop_preview()
|
||||||
self.state["preview_active"] = False
|
self.status["preview_active"] = False
|
||||||
|
|
||||||
def start_recording(self, output, fmt: str = "h264", quality: int = 15):
|
def start_recording(self, output, fmt: str = "h264", quality: int = 15):
|
||||||
"""Start recording.
|
"""Start recording.
|
||||||
|
|
@ -365,7 +387,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
"""
|
"""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
# Start recording method only if a current recording is not running
|
# Start recording method only if a current recording is not running
|
||||||
if not self.state["record_active"]:
|
if not self.status["record_active"]:
|
||||||
|
|
||||||
# Start the camera video recording on port 2
|
# Start the camera video recording on port 2
|
||||||
logging.info("Recording to {}".format(output))
|
logging.info("Recording to {}".format(output))
|
||||||
|
|
@ -378,8 +400,8 @@ class PiCameraStreamer(BaseCamera):
|
||||||
quality=quality,
|
quality=quality,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update state dictionary
|
# Update status dictionary
|
||||||
self.state["record_active"] = True
|
self.status["record_active"] = True
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
@ -398,8 +420,8 @@ class PiCameraStreamer(BaseCamera):
|
||||||
self.camera.stop_recording(splitter_port=2)
|
self.camera.stop_recording(splitter_port=2)
|
||||||
logging.info("Recording stopped")
|
logging.info("Recording stopped")
|
||||||
|
|
||||||
# Update state dictionary
|
# Update status dictionary
|
||||||
self.state["record_active"] = False
|
self.status["record_active"] = False
|
||||||
|
|
||||||
def stop_stream_recording(
|
def stop_stream_recording(
|
||||||
self, splitter_port: int = 1, resolution: Tuple[int, int] = None
|
self, splitter_port: int = 1, resolution: Tuple[int, int] = None
|
||||||
|
|
@ -467,7 +489,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
self.camera.resolution = resolution
|
self.camera.resolution = resolution
|
||||||
|
|
||||||
# If the stream should be active
|
# If the stream should be active
|
||||||
if self.state["stream_active"]:
|
if self.status["stream_active"]:
|
||||||
try:
|
try:
|
||||||
# Start recording on stream port
|
# Start recording on stream port
|
||||||
self.camera.start_recording(
|
self.camera.start_recording(
|
||||||
|
|
@ -633,7 +655,7 @@ class PiCameraStreamer(BaseCamera):
|
||||||
# Start stream recording (and set resolution)
|
# Start stream recording (and set resolution)
|
||||||
self.start_stream_recording()
|
self.start_stream_recording()
|
||||||
|
|
||||||
# Update state
|
# Update status
|
||||||
logging.debug("STREAM ACTIVE")
|
logging.debug("STREAM ACTIVE")
|
||||||
|
|
||||||
# While the iterator is not closed
|
# While the iterator is not closed
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ class JSONEncoder(json.JSONEncoder):
|
||||||
"""
|
"""
|
||||||
A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays
|
A custom JSON encoder, with type conversions for PiCamera fractions, Numpy integers, and Numpy arrays
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def default(self, o, markers=None):
|
def default(self, o, markers=None):
|
||||||
# PiCamera fractions
|
# PiCamera fractions
|
||||||
if isinstance(o, Fraction):
|
if isinstance(o, Fraction):
|
||||||
|
|
@ -39,6 +40,7 @@ class JSONEncoder(json.JSONEncoder):
|
||||||
|
|
||||||
# MAIN CONFIG CLASS
|
# MAIN CONFIG CLASS
|
||||||
|
|
||||||
|
|
||||||
class OpenflexureSettingsFile:
|
class OpenflexureSettingsFile:
|
||||||
"""
|
"""
|
||||||
An object to handle expansion, conversion, and saving of the microscope configuration.
|
An object to handle expansion, conversion, and saving of the microscope configuration.
|
||||||
|
|
@ -85,16 +87,16 @@ class OpenflexureSettingsFile:
|
||||||
# If the loaded config was in contracted format
|
# If the loaded config was in contracted format
|
||||||
if self.expand:
|
if self.expand:
|
||||||
# Contract self._config into self.raw_config
|
# Contract self._config into self.raw_config
|
||||||
save_config = self.contract_config(config)
|
save_settings = self.contract_config(config)
|
||||||
else:
|
else:
|
||||||
save_config = config
|
save_settings = config
|
||||||
|
|
||||||
if backup:
|
if backup:
|
||||||
if os.path.isfile(self.config_path):
|
if os.path.isfile(self.config_path):
|
||||||
shutil.copyfile(self.config_path, self.config_path + ".bk")
|
shutil.copyfile(self.config_path, self.config_path + ".bk")
|
||||||
|
|
||||||
logging.debug("Saving settings dictionary to disk")
|
logging.debug("Saving settings dictionary to disk")
|
||||||
save_json_file(self.config_path, save_config)
|
save_json_file(self.config_path, save_settings)
|
||||||
|
|
||||||
def merge(self, config: dict, backup: bool = True):
|
def merge(self, config: dict, backup: bool = True):
|
||||||
logging.debug("Merging settings with file on disk")
|
logging.debug("Merging settings with file on disk")
|
||||||
|
|
@ -164,6 +166,7 @@ class OpenflexureSettingsFile:
|
||||||
|
|
||||||
# HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES
|
# HANDLE BASIC LOADING AND SAVING OF SETTINGS FILES
|
||||||
|
|
||||||
|
|
||||||
def load_json_file(config_path) -> dict:
|
def load_json_file(config_path) -> dict:
|
||||||
"""
|
"""
|
||||||
Open a .json config file
|
Open a .json config file
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ from openflexure_microscope.stage.mock import MockStage
|
||||||
from openflexure_microscope.camera.base import BaseCamera
|
from openflexure_microscope.camera.base import BaseCamera
|
||||||
from openflexure_microscope.camera.mock import MockStreamer
|
from openflexure_microscope.camera.mock import MockStreamer
|
||||||
|
|
||||||
from openflexure_microscope.plugins import PluginMount
|
from openflexure_microscope.plugins import PluginLoader
|
||||||
from openflexure_microscope.task import TaskOrchestrator
|
from openflexure_microscope.task import TaskOrchestrator
|
||||||
from openflexure_microscope.common.lock import CompositeLock
|
from openflexure_microscope.common.lock import CompositeLock
|
||||||
from openflexure_microscope.config import user_settings
|
from openflexure_microscope.config import user_settings
|
||||||
|
|
@ -30,7 +30,7 @@ class Microscope:
|
||||||
stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): Stage object
|
stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): Stage object
|
||||||
task: (:py:class:`openflexure_microscope.task.TaskOrchestrator`): Threaded ask orchestrator for managing
|
task: (:py:class:`openflexure_microscope.task.TaskOrchestrator`): Threaded ask orchestrator for managing
|
||||||
background tasks using microscope hardware
|
background tasks using microscope hardware
|
||||||
plugin (:py:class:`openflexure_microscope.plugins.PluginMount`): Mounting point for all microscope plugins
|
plugins (:py:class:`openflexure_microscope.plugins.PluginLoader`): Mounting point for all microscope plugins
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -49,10 +49,10 @@ class Microscope:
|
||||||
self.task = TaskOrchestrator()
|
self.task = TaskOrchestrator()
|
||||||
|
|
||||||
# Apply settings loaded from file
|
# Apply settings loaded from file
|
||||||
self.apply_config(user_settings.load())
|
self.apply_settings(user_settings.load())
|
||||||
|
|
||||||
# Create plugin mount-point and attach plugins from maps
|
# Create plugin mount-point and attach plugins from maps
|
||||||
self.plugin = PluginMount(self)
|
self.plugins = PluginLoader(self)
|
||||||
self.attach_plugins(self.plugin_maps)
|
self.attach_plugins(self.plugin_maps)
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
|
|
@ -84,7 +84,7 @@ class Microscope:
|
||||||
stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): stage object
|
stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): stage object
|
||||||
"""
|
"""
|
||||||
|
|
||||||
settings_full = self.read_config()
|
settings_full = self.read_settings()
|
||||||
|
|
||||||
logging.debug("Attaching camera...")
|
logging.debug("Attaching camera...")
|
||||||
self.camera = (
|
self.camera = (
|
||||||
|
|
@ -117,7 +117,7 @@ class Microscope:
|
||||||
self.lock.locks.append(self.stage.lock)
|
self.lock.locks.append(self.stage.lock)
|
||||||
|
|
||||||
logging.info("Reapplying settings to newly attached devices")
|
logging.info("Reapplying settings to newly attached devices")
|
||||||
self.apply_config(settings_full)
|
self.apply_settings(settings_full)
|
||||||
|
|
||||||
def attach_plugins(self, plugin_maps: list):
|
def attach_plugins(self, plugin_maps: list):
|
||||||
"""
|
"""
|
||||||
|
|
@ -125,7 +125,7 @@ class Microscope:
|
||||||
"""
|
"""
|
||||||
if plugin_maps:
|
if plugin_maps:
|
||||||
for plugin_map in plugin_maps:
|
for plugin_map in plugin_maps:
|
||||||
self.plugin.attach(plugin_map)
|
self.plugins.attach(plugin_map)
|
||||||
else:
|
else:
|
||||||
logging.warning("No plugins specified. Skipping.")
|
logging.warning("No plugins specified. Skipping.")
|
||||||
|
|
||||||
|
|
@ -134,7 +134,7 @@ class Microscope:
|
||||||
Empty the plugin mount and re-attach from config.
|
Empty the plugin mount and re-attach from config.
|
||||||
"""
|
"""
|
||||||
logging.info("Tearing down existing PluginMount...")
|
logging.info("Tearing down existing PluginMount...")
|
||||||
self.plugin = PluginMount(self)
|
self.plugins = PluginLoader(self)
|
||||||
logging.info("Repopulating PluginMount...")
|
logging.info("Repopulating PluginMount...")
|
||||||
self.attach_plugins(self.plugin_maps)
|
self.attach_plugins(self.plugin_maps)
|
||||||
|
|
||||||
|
|
@ -157,17 +157,36 @@ class Microscope:
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
dict: Dictionary containing position data,
|
dict: Dictionary containing position data,
|
||||||
and :py:attr:`openflexure_microscope.camera.base.BaseCamera.state`
|
and :py:attr:`openflexure_microscope.camera.base.BaseCamera.status`
|
||||||
"""
|
"""
|
||||||
|
# DEPRECATED
|
||||||
|
logging.warning(
|
||||||
|
"Microscope.state is deprecated. Use Microscope.status instead. State will be removed in a future version."
|
||||||
|
)
|
||||||
state = {
|
state = {
|
||||||
"camera": self.camera.state,
|
"camera": self.camera.status,
|
||||||
"stage": self.stage.state,
|
"stage": self.stage.status,
|
||||||
"plugin": self.plugin.state,
|
"plugin": self.plugins.state,
|
||||||
"version": pkg_resources.get_distribution("openflexure_microscope").version,
|
"version": pkg_resources.get_distribution("openflexure_microscope").version,
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
|
|
||||||
def apply_config(self, config: dict):
|
# Create unified status
|
||||||
|
@property
|
||||||
|
def status(self):
|
||||||
|
"""Dictionary of the basic microscope status.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
dict: Dictionary containing complete microscope status
|
||||||
|
"""
|
||||||
|
state = {
|
||||||
|
"camera": self.camera.status,
|
||||||
|
"stage": self.stage.status,
|
||||||
|
"version": pkg_resources.get_distribution("openflexure_microscope").version,
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
|
||||||
|
def apply_settings(self, config: dict):
|
||||||
"""
|
"""
|
||||||
Applies a config dictionary. Missing parameters will be left untouched.
|
Applies a config dictionary. Missing parameters will be left untouched.
|
||||||
"""
|
"""
|
||||||
|
|
@ -175,11 +194,11 @@ class Microscope:
|
||||||
|
|
||||||
# If attached to a camera
|
# If attached to a camera
|
||||||
if ("camera_settings" in config) and self.camera:
|
if ("camera_settings" in config) and self.camera:
|
||||||
self.camera.apply_config(config["camera_settings"])
|
self.camera.apply_settings(config["camera_settings"])
|
||||||
|
|
||||||
# If attached to a stage
|
# If attached to a stage
|
||||||
if ("stage_settings" in config) and self.stage:
|
if ("stage_settings" in config) and self.stage:
|
||||||
self.stage.apply_config(config["stage_settings"])
|
self.stage.apply_settings(config["stage_settings"])
|
||||||
|
|
||||||
# Todo: tidy up with some loopy goodness
|
# Todo: tidy up with some loopy goodness
|
||||||
if "id" in config:
|
if "id" in config:
|
||||||
|
|
@ -191,7 +210,7 @@ class Microscope:
|
||||||
if "plugins" in config:
|
if "plugins" in config:
|
||||||
self.plugin_maps = config["plugins"]
|
self.plugin_maps = config["plugins"]
|
||||||
|
|
||||||
def read_config(self, json_safe=False):
|
def read_settings(self, json_safe=False):
|
||||||
"""
|
"""
|
||||||
Get an updated settings dictionary.
|
Get an updated settings dictionary.
|
||||||
|
|
||||||
|
|
@ -211,47 +230,43 @@ class Microscope:
|
||||||
|
|
||||||
# If attached to a camera
|
# If attached to a camera
|
||||||
if self.camera:
|
if self.camera:
|
||||||
settings_current_camera = self.camera.read_config()
|
settings_current_camera = self.camera.read_settings()
|
||||||
settings_current["camera_settings"] = settings_current_camera
|
settings_current["camera_settings"] = settings_current_camera
|
||||||
|
|
||||||
# If attached to a stage
|
# If attached to a stage
|
||||||
if self.stage:
|
if self.stage:
|
||||||
settings_current_stage = self.stage.read_config()
|
settings_current_stage = self.stage.read_settings()
|
||||||
settings_current["stage_settings"] = settings_current_stage
|
settings_current["stage_settings"] = settings_current_stage
|
||||||
|
|
||||||
settings_full = user_settings.merge(settings_current)
|
settings_full = user_settings.merge(settings_current)
|
||||||
|
|
||||||
return settings_full
|
return settings_full
|
||||||
|
|
||||||
def save_config(self):
|
def save_settings(self):
|
||||||
"""
|
"""
|
||||||
Merges the current settings back to disk
|
Merges the current settings back to disk
|
||||||
"""
|
"""
|
||||||
# Read curent config
|
# Read curent config
|
||||||
current_config = self.read_config()
|
current_config = self.read_settings()
|
||||||
# Merge in server version responsible for saving the config file
|
|
||||||
current_config["server_version"] = pkg_resources.get_distribution(
|
|
||||||
"openflexure_microscope"
|
|
||||||
).version
|
|
||||||
# Save config to file
|
# Save config to file
|
||||||
if self.camera:
|
if self.camera:
|
||||||
self.camera.save_config()
|
self.camera.save_settings()
|
||||||
if self.stage:
|
if self.stage:
|
||||||
self.stage.save_config()
|
self.stage.save_settings()
|
||||||
user_settings.save(current_config, backup=True)
|
user_settings.save(current_config, backup=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self) -> dict:
|
def config(self) -> dict:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
"Reading microscope through config property is deprecated.\
|
"Reading microscope through config property is deprecated.\
|
||||||
Please use read_config method instead."
|
Please use read_settings method instead."
|
||||||
)
|
)
|
||||||
return self.read_config()
|
return self.read_settings()
|
||||||
|
|
||||||
@config.setter
|
@config.setter
|
||||||
def config(self, config: dict) -> None:
|
def config(self, config: dict) -> None:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
"Setting microscope through config property is deprecated.\
|
"Setting microscope through config property is deprecated.\
|
||||||
Please use apply_config method instead."
|
Please use apply_settings method instead."
|
||||||
)
|
)
|
||||||
self.apply_config(config)
|
self.apply_settings(config)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,6 @@
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"openflexure_microscope.plugins.default.autofocus:AutofocusPlugin",
|
"openflexure_microscope.plugins.default.autofocus:AutofocusPlugin",
|
||||||
"openflexure_microscope.plugins.default.scan:ScanPlugin",
|
"openflexure_microscope.plugins.default.scan:ScanPlugin",
|
||||||
"openflexure_microscope.plugins.default.camera_calibration:Plugin"
|
"openflexure_microscope.plugins.default.camera_calibration:AutocalibrationPlugin"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ __all__ = [
|
||||||
"load_plugin_class",
|
"load_plugin_class",
|
||||||
"load_plugin_module",
|
"load_plugin_module",
|
||||||
"class_from_map",
|
"class_from_map",
|
||||||
"PluginMount",
|
"PluginLoader",
|
||||||
"MicroscopePlugin",
|
"MicroscopePlugin",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -12,6 +12,6 @@ from .loader import (
|
||||||
load_plugin_class,
|
load_plugin_class,
|
||||||
load_plugin_module,
|
load_plugin_module,
|
||||||
class_from_map,
|
class_from_map,
|
||||||
PluginMount,
|
PluginLoader,
|
||||||
MicroscopePlugin,
|
MicroscopePlugin,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ class MeasureSharpnessAPI(MicroscopeViewPlugin):
|
||||||
|
|
||||||
|
|
||||||
class AutofocusAPI(MicroscopeViewPlugin):
|
class AutofocusAPI(MicroscopeViewPlugin):
|
||||||
|
"""
|
||||||
|
Run a standard autofocus
|
||||||
|
"""
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
payload = JsonResponse(request)
|
payload = JsonResponse(request)
|
||||||
|
|
||||||
|
|
@ -29,12 +33,17 @@ class AutofocusAPI(MicroscopeViewPlugin):
|
||||||
task = taskify(self.plugin.autofocus)(dz)
|
task = taskify(self.plugin.autofocus)(dz)
|
||||||
|
|
||||||
# return a handle on the autofocus task
|
# return a handle on the autofocus task
|
||||||
return jsonify(task.state), 202
|
return jsonify(task.state), 201
|
||||||
|
|
||||||
else:
|
else:
|
||||||
abort(503, 'No stage connected. Unable to autofocus.')
|
abort(503, "No stage connected. Unable to autofocus.")
|
||||||
|
|
||||||
|
|
||||||
class FastAutofocusAPI(MicroscopeViewPlugin):
|
class FastAutofocusAPI(MicroscopeViewPlugin):
|
||||||
|
"""
|
||||||
|
Run a fast autofocus
|
||||||
|
"""
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
payload = JsonResponse(request)
|
payload = JsonResponse(request)
|
||||||
|
|
||||||
|
|
@ -49,7 +58,7 @@ class FastAutofocusAPI(MicroscopeViewPlugin):
|
||||||
task = taskify(self.plugin.fast_autofocus)(dz, backlash=backlash)
|
task = taskify(self.plugin.fast_autofocus)(dz, backlash=backlash)
|
||||||
|
|
||||||
# return a handle on the autofocus task
|
# return a handle on the autofocus task
|
||||||
return jsonify(task.state), 202
|
return jsonify(task.state), 201
|
||||||
|
|
||||||
else:
|
else:
|
||||||
abort(503, 'No stage connected. Unable to autofocus.')
|
abort(503, "No stage connected. Unable to autofocus.")
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,2 @@
|
||||||
__all__ = ["Plugin"]
|
__all__ = ["Plugin"]
|
||||||
from .plugin import Plugin
|
from .plugin import AutocalibrationPlugin
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,12 @@ class RecalibrateAPIView(MicroscopeViewPlugin):
|
||||||
task = taskify(self.plugin.recalibrate)()
|
task = taskify(self.plugin.recalibrate)()
|
||||||
|
|
||||||
# Return a handle on the autofocus task
|
# Return a handle on the autofocus task
|
||||||
return jsonify(task.state), 202
|
return jsonify(task.state), 201
|
||||||
|
|
||||||
|
|
||||||
class Plugin(MicroscopePlugin):
|
class AutocalibrationPlugin(MicroscopePlugin):
|
||||||
"""
|
"""
|
||||||
A set of default plugins
|
Auto-calibration plugin
|
||||||
"""
|
"""
|
||||||
|
|
||||||
api_views = {"/recalibrate": RecalibrateAPIView}
|
api_views = {"/recalibrate": RecalibrateAPIView}
|
||||||
|
|
@ -37,10 +37,10 @@ class Plugin(MicroscopePlugin):
|
||||||
"""
|
"""
|
||||||
scamera = self.microscope.camera
|
scamera = self.microscope.camera
|
||||||
with scamera.lock:
|
with scamera.lock:
|
||||||
assert not scamera.state[
|
assert not scamera.status[
|
||||||
"record_active"
|
"record_active"
|
||||||
], "Can't recalibrate while recording!"
|
], "Can't recalibrate while recording!"
|
||||||
streaming = scamera.state["stream_active"]
|
streaming = scamera.status["stream_active"]
|
||||||
if streaming:
|
if streaming:
|
||||||
logging.info("Stopping stream before recalibration")
|
logging.info("Stopping stream before recalibration")
|
||||||
scamera.stop_stream_recording(resolution=(640, 480))
|
scamera.stop_stream_recording(resolution=(640, 480))
|
||||||
|
|
@ -51,7 +51,7 @@ class Plugin(MicroscopePlugin):
|
||||||
recalibrate_camera(scamera.camera)
|
recalibrate_camera(scamera.camera)
|
||||||
finally:
|
finally:
|
||||||
scamera.camera.resolution = old_resolution
|
scamera.camera.resolution = old_resolution
|
||||||
self.microscope.save_config()
|
self.microscope.save_settings()
|
||||||
if streaming:
|
if streaming:
|
||||||
logging.info("Restarting stream after recalibration")
|
logging.info("Restarting stream after recalibration")
|
||||||
scamera.start_stream_recording()
|
scamera.start_stream_recording()
|
||||||
|
|
|
||||||
|
|
@ -60,4 +60,4 @@ class TileScanAPI(MicroscopeViewPlugin):
|
||||||
)
|
)
|
||||||
|
|
||||||
# return a handle on the scan task
|
# return a handle on the scan task
|
||||||
return jsonify(task.state), 202
|
return jsonify(task.state), 201
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@ class ScanPlugin(MicroscopePlugin):
|
||||||
api_views = {"/tile": TileScanAPI}
|
api_views = {"/tile": TileScanAPI}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
MicroscopePlugin.__init__(self)
|
||||||
|
|
||||||
self.images_to_be_captured: int = 1
|
self.images_to_be_captured: int = 1
|
||||||
update_task_data({"images_to_be_captured": self.images_to_be_captured})
|
update_task_data({"images_to_be_captured": self.images_to_be_captured})
|
||||||
|
|
||||||
|
|
@ -97,18 +99,19 @@ class ScanPlugin(MicroscopePlugin):
|
||||||
if "scan" not in tags:
|
if "scan" not in tags:
|
||||||
tags.append("scan")
|
tags.append("scan")
|
||||||
|
|
||||||
metadata.update(
|
# Inject system metadata
|
||||||
{
|
system_metadata = {
|
||||||
"scan_id": scan_id,
|
"microscope_settings": self.microscope.read_settings(),
|
||||||
"basename": basename,
|
"microscope_state": self.microscope.state,
|
||||||
"microscope_settings": self.microscope.read_config(),
|
"microscope_id": self.microscope.id,
|
||||||
"microscope_state": self.microscope.state,
|
"microscope_name": self.microscope.name,
|
||||||
"microscope_id": self.microscope.id,
|
}
|
||||||
"microscope_name": self.microscope.name,
|
output.system_metadata.update(system_metadata)
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# Insert custom metadata
|
||||||
output.put_metadata(metadata)
|
output.put_metadata(metadata)
|
||||||
|
|
||||||
|
# Insert custom tags
|
||||||
output.put_tags(tags)
|
output.put_tags(tags)
|
||||||
|
|
||||||
def tile(
|
def tile(
|
||||||
|
|
@ -146,14 +149,18 @@ class ScanPlugin(MicroscopePlugin):
|
||||||
if "time" not in metadata:
|
if "time" not in metadata:
|
||||||
metadata["time"] = generate_basename()
|
metadata["time"] = generate_basename()
|
||||||
|
|
||||||
metadata.update({
|
metadata.update(
|
||||||
"scan_parameters": {
|
{
|
||||||
"step_size": step_size,
|
"scan_id": scan_id,
|
||||||
"grid": grid,
|
"basename": basename,
|
||||||
"style": style,
|
"scan_parameters": {
|
||||||
"autofocus_dz": autofocus_dz
|
"step_size": step_size,
|
||||||
|
"grid": grid,
|
||||||
|
"style": style,
|
||||||
|
"autofocus_dz": autofocus_dz,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
# Check if autofocus is enabled
|
# Check if autofocus is enabled
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ class LongRunningAPI(MicroscopeViewPlugin):
|
||||||
# Attach the long-running method as a microscope task
|
# Attach the long-running method as a microscope task
|
||||||
try:
|
try:
|
||||||
task = self.microscope.task.start(self.plugin.long_running, time_to_run)
|
task = self.microscope.task.start(self.plugin.long_running, time_to_run)
|
||||||
return jsonify(task.state), 202
|
return jsonify(task.state), 201
|
||||||
|
|
||||||
except TaskDeniedException:
|
except TaskDeniedException:
|
||||||
return abort(409)
|
return abort(409)
|
||||||
|
|
@ -94,7 +94,7 @@ class SomeExceptionAPI(MicroscopeViewPlugin):
|
||||||
# Attach the long-running method as a microscope task
|
# Attach the long-running method as a microscope task
|
||||||
try:
|
try:
|
||||||
task = self.microscope.task.start(self.plugin.some_exception)
|
task = self.microscope.task.start(self.plugin.some_exception)
|
||||||
return jsonify(task.state), 202
|
return jsonify(task.state), 201
|
||||||
|
|
||||||
except TaskDeniedException:
|
except TaskDeniedException:
|
||||||
return abort(409)
|
return abort(409)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
import importlib
|
import importlib
|
||||||
|
import copy
|
||||||
import os
|
import os
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from openflexure_microscope.utilities import camel_to_snake, camel_to_spine
|
||||||
|
from openflexure_microscope.api.views import MicroscopeViewPlugin
|
||||||
|
|
||||||
|
|
||||||
class ConColors:
|
class ConColors:
|
||||||
HEADER = "\033[95m"
|
HEADER = "\033[95m"
|
||||||
|
|
@ -80,7 +84,6 @@ def name_from_module(plugin_path):
|
||||||
|
|
||||||
|
|
||||||
def load_plugin_module(plugin_path):
|
def load_plugin_module(plugin_path):
|
||||||
|
|
||||||
# If the loader was found (i.e. plugin probably exists)
|
# If the loader was found (i.e. plugin probably exists)
|
||||||
if check_module(plugin_path):
|
if check_module(plugin_path):
|
||||||
try:
|
try:
|
||||||
|
|
@ -137,7 +140,7 @@ def class_from_map(plugin_map):
|
||||||
return load_plugin_class(*plugin_arr)
|
return load_plugin_class(*plugin_arr)
|
||||||
|
|
||||||
|
|
||||||
class PluginMount(object):
|
class PluginLoader(object):
|
||||||
"""
|
"""
|
||||||
A mount-point for all loaded plugins. Attaches to a Microscope object.
|
A mount-point for all loaded plugins. Attaches to a Microscope object.
|
||||||
|
|
||||||
|
|
@ -147,30 +150,22 @@ class PluginMount(object):
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.plugins = [] # List of plugin objects
|
self._plugins = [] # List of plugin objects
|
||||||
|
self._legacy_plugins = {} # DEPRECATED: Dictionary of plugins with old names
|
||||||
self.forms = [] # List of plugin forms
|
self.forms = [] # List of plugin forms
|
||||||
logging.info("Creating plugin mount")
|
logging.info("Creating plugin mount")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
return [m[0] for m in self.members]
|
# DEPRECATED
|
||||||
|
logging.warning(
|
||||||
|
"PluginMount.state is deprecated. Use PluginMount.active instead. State will be removed in a future version."
|
||||||
|
)
|
||||||
|
return list(self._legacy_plugins.keys())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def members(self):
|
def active(self):
|
||||||
ignores = ["state", "members", "attach"]
|
return self._plugins
|
||||||
plugin_array = []
|
|
||||||
for obj_name in dir(self):
|
|
||||||
if not obj_name in ignores and not obj_name[:2] == "__":
|
|
||||||
obj = getattr(self, obj_name)
|
|
||||||
if isinstance(obj, MicroscopePlugin):
|
|
||||||
plugin_members = [
|
|
||||||
member
|
|
||||||
for member in inspect.getmembers(obj)
|
|
||||||
if not member[0][:2] == "__"
|
|
||||||
]
|
|
||||||
plugin_info = (obj_name, plugin_members)
|
|
||||||
plugin_array.append(plugin_info)
|
|
||||||
return plugin_array
|
|
||||||
|
|
||||||
def attach(self, plugin_map):
|
def attach(self, plugin_map):
|
||||||
"""
|
"""
|
||||||
|
|
@ -183,7 +178,9 @@ class PluginMount(object):
|
||||||
|
|
||||||
if plugin_class is not None:
|
if plugin_class is not None:
|
||||||
|
|
||||||
pythonsafe_plugin_name = plugin_name.replace("/", "_")
|
logging.debug(f"Loading plugin class {plugin_class}, {plugin_name}")
|
||||||
|
|
||||||
|
plugin_name_python_safe = plugin_name.replace("/", "_")
|
||||||
|
|
||||||
if plugin_class and plugin_name:
|
if plugin_class and plugin_name:
|
||||||
plugin_object = plugin_class()
|
plugin_object = plugin_class()
|
||||||
|
|
@ -200,18 +197,24 @@ class PluginMount(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
elif isinstance(
|
elif isinstance(
|
||||||
plugin_object, MicroscopePlugin
|
plugin_object, BasePlugin
|
||||||
): # If plugin_object is an instance of MicroscopePlugin
|
): # If plugin_object is an instance of MicroscopePlugin
|
||||||
# Attach plugin_object to the plugin mount
|
# Attach plugin_object to the plugin mount
|
||||||
setattr(self, pythonsafe_plugin_name, plugin_object)
|
setattr(self, plugin_name_python_safe, plugin_object)
|
||||||
self.plugins.append((plugin_name, plugin_object))
|
# Store the plugin object, and it's properties
|
||||||
|
self._plugins.append(plugin_object)
|
||||||
|
# DEPRECATED: Store the plugin with it's old name
|
||||||
|
self._legacy_plugins[plugin_name] = plugin_object
|
||||||
|
|
||||||
# Grant plugin access to the hardware
|
# Grant plugin access to the hardware
|
||||||
plugin_object.microscope = self.parent
|
if isinstance(plugin_object, MicroscopePlugin):
|
||||||
|
plugin_object.microscope = self.parent
|
||||||
|
|
||||||
logging.info(
|
logging.info(
|
||||||
ConColors.OKGREEN
|
ConColors.OKGREEN
|
||||||
+ "Plugin {} loaded as {}.".format(plugin_map, plugin_name)
|
+ "Plugin {} loaded as {}.".format(
|
||||||
|
plugin_map, plugin_object._name
|
||||||
|
)
|
||||||
+ ConColors.ENDC
|
+ ConColors.ENDC
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -219,7 +222,102 @@ class PluginMount(object):
|
||||||
logging.warning(f"Error loading plugin {plugin_map}. Moving on.")
|
logging.warning(f"Error loading plugin {plugin_map}. Moving on.")
|
||||||
|
|
||||||
|
|
||||||
class MicroscopePlugin:
|
class BasePlugin:
|
||||||
|
"""
|
||||||
|
Parent class for all plugins.
|
||||||
|
|
||||||
|
Handles binding route views and forms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._views = (
|
||||||
|
{}
|
||||||
|
) # Key: Full, Python-safe ID. Val: Original rule, and view class
|
||||||
|
self._rules = {} # Key: Original rule. Val: View class
|
||||||
|
self._gui = None
|
||||||
|
|
||||||
|
# If old api_views dictionary is found
|
||||||
|
if hasattr(self, "api_views"):
|
||||||
|
# Convert to new format
|
||||||
|
self._convert_old_api_views()
|
||||||
|
# If old api_form dictionary is found
|
||||||
|
if hasattr(self, "api_form"):
|
||||||
|
# Convert to new format
|
||||||
|
self._convert_old_api_form()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def views(self):
|
||||||
|
return self._views
|
||||||
|
|
||||||
|
def add_view(self, rule, view_class):
|
||||||
|
# Remove all leading slashes from view route
|
||||||
|
cleaned_rule = rule
|
||||||
|
while cleaned_rule[0] == "/":
|
||||||
|
cleaned_rule = cleaned_rule[1:]
|
||||||
|
|
||||||
|
# Expand the rule to include plugin name
|
||||||
|
full_rule = "/{}/{}".format(self._name_uri_safe, cleaned_rule)
|
||||||
|
|
||||||
|
# Create a Python-safe route ID
|
||||||
|
view_id = cleaned_rule.replace("/", "_")
|
||||||
|
|
||||||
|
# Store route information in a dictionary
|
||||||
|
d = {"rule": full_rule, "view": view_class}
|
||||||
|
|
||||||
|
# Add view to private views dictionary
|
||||||
|
self._views[view_id] = d
|
||||||
|
# Store the rule expansion information
|
||||||
|
self._rules[rule] = self._views[view_id]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gui(self):
|
||||||
|
if not self._gui:
|
||||||
|
return None
|
||||||
|
|
||||||
|
api_gui = copy.deepcopy(self._gui)
|
||||||
|
api_gui["id"] = self._name
|
||||||
|
|
||||||
|
if "forms" in api_gui and isinstance(api_gui["forms"], list):
|
||||||
|
for form in api_gui["forms"]:
|
||||||
|
if "route" in form and form["route"] in self._rules.keys():
|
||||||
|
form["route"] = self._rules[form["route"]]["rule"]
|
||||||
|
else:
|
||||||
|
logging.warn(
|
||||||
|
"No valid expandable route found for {}".format(form["route"])
|
||||||
|
)
|
||||||
|
return api_gui
|
||||||
|
|
||||||
|
def set_gui(self, form_dictionary: dict):
|
||||||
|
self._gui = form_dictionary
|
||||||
|
|
||||||
|
def _convert_old_api_views(self):
|
||||||
|
for view_route, view_class in self.api_views.items():
|
||||||
|
self.add_view(view_route, view_class)
|
||||||
|
|
||||||
|
def _convert_old_api_form(self):
|
||||||
|
self.set_gui(self.api_form)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _name(self):
|
||||||
|
return self.__class__.__name__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _name_python_safe(self):
|
||||||
|
return camel_to_snake(self._name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _name_uri_safe(self):
|
||||||
|
return camel_to_spine(self._name)
|
||||||
|
|
||||||
|
def _full_name(self):
|
||||||
|
module = self.__class__.__module__
|
||||||
|
if module is None or module == str.__class__.__module__:
|
||||||
|
return self.__class__.__name__ # Avoid reporting __builtin__
|
||||||
|
else:
|
||||||
|
return module + "." + self.__class__.__name__
|
||||||
|
|
||||||
|
|
||||||
|
class MicroscopePlugin(BasePlugin):
|
||||||
"""
|
"""
|
||||||
Parent class for all microscope plugins.
|
Parent class for all microscope plugins.
|
||||||
|
|
||||||
|
|
@ -230,6 +328,7 @@ class MicroscopePlugin:
|
||||||
api_views = {} # Initially empty dictionary of API views associated with the plugin
|
api_views = {} # Initially empty dictionary of API views associated with the plugin
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
BasePlugin.__init__(self)
|
||||||
self.microscope = (
|
self.microscope = (
|
||||||
None
|
None
|
||||||
) #: :py:class:`openflexure_microscope.microscope.Microscope`: Microscope object
|
) #: :py:class:`openflexure_microscope.microscope.Microscope`: Microscope object
|
||||||
|
|
|
||||||
|
|
@ -57,4 +57,4 @@ class TaskAPI(MicroscopeViewPlugin):
|
||||||
task = taskify(self.plugin.generate_random_numbers_for_a_while)(val_int)
|
task = taskify(self.plugin.generate_random_numbers_for_a_while)(val_int)
|
||||||
|
|
||||||
# return a handle on the autofocus task
|
# return a handle on the autofocus task
|
||||||
return jsonify(task.state), 202
|
return jsonify(task.state), 201
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "test-plugin",
|
"id": "test-plugin",
|
||||||
"icon": "pets",
|
"icon": "pets",
|
||||||
"forms": [
|
"forms": [{
|
||||||
{
|
|
||||||
"name": "Simple request",
|
"name": "Simple request",
|
||||||
"isCollapsible": false,
|
"isCollapsible": false,
|
||||||
"isTask": false,
|
"isTask": false,
|
||||||
|
|
@ -10,10 +9,8 @@
|
||||||
"route": "/do",
|
"route": "/do",
|
||||||
"submitLabel": "Do things",
|
"submitLabel": "Do things",
|
||||||
"schema": [
|
"schema": [
|
||||||
[
|
[{
|
||||||
{
|
|
||||||
"fieldType": "numberInput",
|
"fieldType": "numberInput",
|
||||||
"placeholder": "Some integer",
|
|
||||||
"name": "val_int",
|
"name": "val_int",
|
||||||
"label": "Number value",
|
"label": "Number value",
|
||||||
"minValue": 0
|
"minValue": 0
|
||||||
|
|
@ -25,8 +22,7 @@
|
||||||
"name": "val_str"
|
"name": "val_str"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[{
|
||||||
{
|
|
||||||
"fieldType": "radioList",
|
"fieldType": "radioList",
|
||||||
"name": "val_radio",
|
"name": "val_radio",
|
||||||
"label": "Radio value",
|
"label": "Radio value",
|
||||||
|
|
@ -54,8 +50,8 @@
|
||||||
|
|
||||||
{
|
{
|
||||||
"fieldType": "textInput",
|
"fieldType": "textInput",
|
||||||
"placeholder": "Some string",
|
|
||||||
"label": "Non-persistent string",
|
"label": "Non-persistent string",
|
||||||
|
"default": "A default value",
|
||||||
"name": "val_disposable"
|
"name": "val_disposable"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -66,15 +62,12 @@
|
||||||
"selfUpdate": true,
|
"selfUpdate": true,
|
||||||
"route": "/task",
|
"route": "/task",
|
||||||
"submitLabel": "Start task",
|
"submitLabel": "Start task",
|
||||||
"schema": [
|
"schema": [{
|
||||||
{
|
"fieldType": "numberInput",
|
||||||
"fieldType": "numberInput",
|
"name": "run_time",
|
||||||
"placeholder": "",
|
"label": "Run time (seconds)",
|
||||||
"name": "run_time",
|
"minValue": 1
|
||||||
"label": "Run time (seconds)",
|
}]
|
||||||
"minValue": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -23,6 +23,8 @@ class ExamplePlugin(MicroscopePlugin):
|
||||||
api_views = {"/do": DoAPI, "/task": TaskAPI}
|
api_views = {"/do": DoAPI, "/task": TaskAPI}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
MicroscopePlugin.__init__(self)
|
||||||
|
|
||||||
self.val_int = 10
|
self.val_int = 10
|
||||||
self.val_str = "Hello"
|
self.val_str = "Hello"
|
||||||
self.val_radio = "First"
|
self.val_radio = "First"
|
||||||
|
|
|
||||||
|
|
@ -14,22 +14,22 @@ class BaseStage(metaclass=ABCMeta):
|
||||||
self.lock = StrictLock(timeout=5)
|
self.lock = StrictLock(timeout=5)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def apply_config(self, config: dict):
|
def apply_settings(self, config: dict):
|
||||||
"""Update settings from a config dictionary"""
|
"""Update settings from a config dictionary"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def read_config(self):
|
def read_settings(self):
|
||||||
"""Return the current settings as a dictionary"""
|
"""Return the current settings as a dictionary"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def save_config(self):
|
def save_settings(self):
|
||||||
"""(Optional) Save any settings to disk that need to be stored"""
|
"""(Optional) Save any settings to disk that need to be stored"""
|
||||||
return
|
return
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def state(self):
|
def status(self):
|
||||||
"""The general state dictionary of the board.
|
"""The general state dictionary of the board.
|
||||||
Should at least contain 'position', and 'board' keys.
|
Should at least contain 'position', and 'board' keys.
|
||||||
Note: A None/Null value for 'board' will disable stage
|
Note: A None/Null value for 'board' will disable stage
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,9 @@ class MockStage(BaseStage):
|
||||||
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def status(self):
|
||||||
"""The general state dictionary of the board."""
|
"""The general status dictionary of the board."""
|
||||||
state = {
|
status = {
|
||||||
"position": {
|
"position": {
|
||||||
"x": self.position[0],
|
"x": self.position[0],
|
||||||
"y": self.position[1],
|
"y": self.position[1],
|
||||||
|
|
@ -27,9 +27,9 @@ class MockStage(BaseStage):
|
||||||
"board": None,
|
"board": None,
|
||||||
"version": "0",
|
"version": "0",
|
||||||
}
|
}
|
||||||
return state
|
return status
|
||||||
|
|
||||||
def apply_config(self, config: dict):
|
def apply_settings(self, config: dict):
|
||||||
"""Update settings from a config dictionary"""
|
"""Update settings from a config dictionary"""
|
||||||
|
|
||||||
# Set backlash. Expects a dictionary with axis labels
|
# Set backlash. Expects a dictionary with axis labels
|
||||||
|
|
@ -38,7 +38,7 @@ class MockStage(BaseStage):
|
||||||
backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0])
|
backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0])
|
||||||
self.backlash = backlash
|
self.backlash = backlash
|
||||||
|
|
||||||
def read_config(self) -> dict:
|
def read_settings(self) -> dict:
|
||||||
"""Return the current settings as a dictionary"""
|
"""Return the current settings as a dictionary"""
|
||||||
blsh = self.backlash.tolist()
|
blsh = self.backlash.tolist()
|
||||||
config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}}
|
config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}}
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,9 @@ class SangaStage(BaseStage):
|
||||||
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def status(self):
|
||||||
"""The general state dictionary of the board."""
|
"""The general status dictionary of the board."""
|
||||||
state = {
|
status = {
|
||||||
"position": {
|
"position": {
|
||||||
"x": self.position[0],
|
"x": self.position[0],
|
||||||
"y": self.position[1],
|
"y": self.position[1],
|
||||||
|
|
@ -42,7 +42,7 @@ class SangaStage(BaseStage):
|
||||||
"board": self.board.board,
|
"board": self.board.board,
|
||||||
"firmware": self.board.firmware,
|
"firmware": self.board.firmware,
|
||||||
}
|
}
|
||||||
return state
|
return status
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def n_axes(self):
|
def n_axes(self):
|
||||||
|
|
@ -85,7 +85,7 @@ class SangaStage(BaseStage):
|
||||||
else:
|
else:
|
||||||
self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int)
|
self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int)
|
||||||
|
|
||||||
def apply_config(self, config: dict):
|
def apply_settings(self, config: dict):
|
||||||
"""Update settings from a config dictionary"""
|
"""Update settings from a config dictionary"""
|
||||||
|
|
||||||
# Set backlash. Expects a dictionary with axis labels
|
# Set backlash. Expects a dictionary with axis labels
|
||||||
|
|
@ -94,7 +94,7 @@ class SangaStage(BaseStage):
|
||||||
backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0])
|
backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0])
|
||||||
self.backlash = backlash
|
self.backlash = backlash
|
||||||
|
|
||||||
def read_config(self) -> dict:
|
def read_settings(self) -> dict:
|
||||||
"""Return the current settings as a dictionary"""
|
"""Return the current settings as a dictionary"""
|
||||||
blsh = self.backlash.tolist()
|
blsh = self.backlash.tolist()
|
||||||
config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}}
|
config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ class ExtensibleSerialInstrument(object):
|
||||||
Set up the serial port and so on.
|
Set up the serial port and so on.
|
||||||
"""
|
"""
|
||||||
logging.info("Updating ESI port settings")
|
logging.info("Updating ESI port settings")
|
||||||
|
logging.debug(kwargs)
|
||||||
self.port_settings.update(kwargs)
|
self.port_settings.update(kwargs)
|
||||||
logging.info("Opening ESI connection to port {}".format(port))
|
logging.info("Opening ESI connection to port {}".format(port))
|
||||||
self.open(port, False) # Eventually this shouldn't rely on init...
|
self.open(port, False) # Eventually this shouldn't rely on init...
|
||||||
|
|
@ -86,7 +87,9 @@ class ExtensibleSerialInstrument(object):
|
||||||
assert (
|
assert (
|
||||||
port is not None
|
port is not None
|
||||||
), "We don't have a serial port to open, meaning you didn't specify a valid port. Are you sure the instrument is connected?"
|
), "We don't have a serial port to open, meaning you didn't specify a valid port. Are you sure the instrument is connected?"
|
||||||
|
logging.info("Creating serial.Serial instance...")
|
||||||
self._ser = serial.Serial(port, **self.port_settings)
|
self._ser = serial.Serial(port, **self.port_settings)
|
||||||
|
logging.info(f"Created {self._ser}")
|
||||||
# the block above wraps the serial IO layer with a text IO layer
|
# the block above wraps the serial IO layer with a text IO layer
|
||||||
# this allows us to read/write in neat lines. NB the buffer size must
|
# this allows us to read/write in neat lines. NB the buffer size must
|
||||||
# be set to 1 byte for maximum responsiveness.
|
# be set to 1 byte for maximum responsiveness.
|
||||||
|
|
@ -130,11 +133,6 @@ class ExtensibleSerialInstrument(object):
|
||||||
assert (
|
assert (
|
||||||
self._ser.isOpen()
|
self._ser.isOpen()
|
||||||
), "Attempted to write to the serial port before it was opened. Perhaps you need to call the 'open' method first?"
|
), "Attempted to write to the serial port before it was opened. Perhaps you need to call the 'open' method first?"
|
||||||
# TODO: Check if this code is needed and if not kill it
|
|
||||||
# try:
|
|
||||||
# if self._ser.outWaiting()>0: self._ser.flushOutput() #ensure there's nothing waiting
|
|
||||||
# except AttributeError:
|
|
||||||
# if self._ser.out_waiting>0: self._ser.flushOutput() #ensure there's nothing waiting
|
|
||||||
data = query_string + self.termination_character
|
data = query_string + self.termination_character
|
||||||
data = data.encode()
|
data = data.encode()
|
||||||
self._ser.write(data)
|
self._ser.write(data)
|
||||||
|
|
@ -193,10 +191,15 @@ class ExtensibleSerialInstrument(object):
|
||||||
will keep reading until a termination phrase is reached.
|
will keep reading until a termination phrase is reached.
|
||||||
"""
|
"""
|
||||||
with self.communications_lock:
|
with self.communications_lock:
|
||||||
|
logging.debug("Flushing input buffer...")
|
||||||
self.flush_input_buffer()
|
self.flush_input_buffer()
|
||||||
|
logging.debug(f"Writing query: {queryString}")
|
||||||
self.write(queryString)
|
self.write(queryString)
|
||||||
|
logging.debug("Query written")
|
||||||
if self.ignore_echo == True: # Needs Implementing for a multiline read!
|
if self.ignore_echo == True: # Needs Implementing for a multiline read!
|
||||||
|
logging.debug("Reading first line...")
|
||||||
first_line = self.readline(timeout).strip()
|
first_line = self.readline(timeout).strip()
|
||||||
|
logging.debug(f"Read finished. Got {first_line}")
|
||||||
if first_line == queryString:
|
if first_line == queryString:
|
||||||
return self.readline(timeout).strip()
|
return self.readline(timeout).strip()
|
||||||
else:
|
else:
|
||||||
|
|
@ -206,11 +209,15 @@ class ExtensibleSerialInstrument(object):
|
||||||
if termination_line is not None:
|
if termination_line is not None:
|
||||||
multiline = True
|
multiline = True
|
||||||
if multiline:
|
if multiline:
|
||||||
|
logging.debug("Reading multiline...")
|
||||||
return self.read_multiline(termination_line)
|
return self.read_multiline(termination_line)
|
||||||
else:
|
else:
|
||||||
return self.readline(
|
logging.debug("Reading response...")
|
||||||
timeout
|
line = (
|
||||||
).strip() # question: should we strip the final newline?
|
self.readline(timeout).strip()
|
||||||
|
) # question: should we strip the final newline?
|
||||||
|
logging.debug(f"Read finished. Got {line}")
|
||||||
|
return line
|
||||||
|
|
||||||
def parsed_query(
|
def parsed_query(
|
||||||
self,
|
self,
|
||||||
|
|
@ -218,7 +225,7 @@ class ExtensibleSerialInstrument(object):
|
||||||
response_string=r"%d",
|
response_string=r"%d",
|
||||||
re_flags=0,
|
re_flags=0,
|
||||||
parse_function=None,
|
parse_function=None,
|
||||||
**kwargs
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Perform a query, returning a parsed form of the response.
|
Perform a query, returning a parsed form of the response.
|
||||||
|
|
@ -345,6 +352,7 @@ class ExtensibleSerialInstrument(object):
|
||||||
def find_port(self):
|
def find_port(self):
|
||||||
"""Iterate through the available serial ports and query them to see
|
"""Iterate through the available serial ports and query them to see
|
||||||
if our instrument is there."""
|
if our instrument is there."""
|
||||||
|
print("Auto-scanning ports")
|
||||||
with self.communications_lock:
|
with self.communications_lock:
|
||||||
success = False
|
success = False
|
||||||
for (
|
for (
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
# Once initialised, `firmware` is a string that identifies the firmware version
|
# Once initialised, `firmware` is a string that identifies the firmware version
|
||||||
firmware = None
|
firmware = None
|
||||||
|
|
||||||
def __init__(self, port=None, **kwargs):
|
def __init__(self, port=None, timeout: int = 2, **kwargs):
|
||||||
"""Create a sangaboard object.
|
"""Create a sangaboard object.
|
||||||
|
|
||||||
Arguments are passed to the constructor of
|
Arguments are passed to the constructor of
|
||||||
|
|
@ -101,13 +101,16 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Initialise basic serial instrument with specified
|
# Initialise basic serial instrument with specified
|
||||||
|
logging.info(f"Initialising ExtensibleSerialInstrument on port {port}")
|
||||||
ExtensibleSerialInstrument.__init__(self, port, **kwargs)
|
ExtensibleSerialInstrument.__init__(self, port, **kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Make absolutely sure that whatever port we're on is valid
|
# Make absolutely sure that whatever port we're on is valid
|
||||||
|
logging.info("Checking valid firmware...")
|
||||||
self.check_valid_firmware()
|
self.check_valid_firmware()
|
||||||
|
|
||||||
# Bit messy: Defining all valid modules as not available, then overwriting with available information if available.
|
# Bit messy: Defining all valid modules as not available, then overwriting with available information if available.
|
||||||
|
logging.info("Loading modules...")
|
||||||
self.light_sensor = LightSensor(False)
|
self.light_sensor = LightSensor(False)
|
||||||
|
|
||||||
for module in self.list_modules():
|
for module in self.list_modules():
|
||||||
|
|
@ -145,10 +148,11 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
"""
|
"""
|
||||||
Overrides superclass, used in self.open(), and port scanning
|
Overrides superclass, used in self.open(), and port scanning
|
||||||
"""
|
"""
|
||||||
|
logging.info("Testing communication to SangaBoard")
|
||||||
return self.check_valid_firmware()
|
return self.check_valid_firmware()
|
||||||
|
|
||||||
def check_valid_firmware(self):
|
def check_valid_firmware(self):
|
||||||
logging.debug("Running firmware checks")
|
logging.info("Running firmware checks...")
|
||||||
|
|
||||||
# Request firmware version from the board
|
# Request firmware version from the board
|
||||||
self.firmware = self.query("version", timeout=2).rstrip()
|
self.firmware = self.query("version", timeout=2).rstrip()
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ from openflexure_microscope.common.tasks import (
|
||||||
remove_task,
|
remove_task,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# DEPRECATED
|
||||||
class TaskOrchestrator:
|
class TaskOrchestrator:
|
||||||
"""
|
"""
|
||||||
DEPRECATED: Class responsible for spawning threaded tasks, and storing their returns.
|
DEPRECATED: Class responsible for spawning threaded tasks, and storing their returns.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import re
|
||||||
import copy
|
import copy
|
||||||
import operator
|
import operator
|
||||||
from collections import abc
|
from collections import abc
|
||||||
|
|
@ -5,6 +6,40 @@ from functools import reduce
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
|
||||||
|
def bottom_level_name(obj):
|
||||||
|
return obj.__name__.split(".")[-1]
|
||||||
|
|
||||||
|
|
||||||
|
def description_from_view(view_class):
|
||||||
|
methods = []
|
||||||
|
for method_key in ["get", "post", "put", "delete"]:
|
||||||
|
if hasattr(view_class, method_key):
|
||||||
|
methods.append(method_key.upper())
|
||||||
|
brief_description = get_docstring(view_class).partition("\n")[0].strip()
|
||||||
|
|
||||||
|
d = {"methods": methods, "description": brief_description}
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def get_docstring(obj):
|
||||||
|
ds = obj.__doc__
|
||||||
|
if ds:
|
||||||
|
return ds.strip()
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def camel_to_snake(name):
|
||||||
|
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
|
||||||
|
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
|
||||||
|
|
||||||
|
|
||||||
|
def camel_to_spine(name):
|
||||||
|
s1 = re.sub("(.)([A-Z][a-z]+)", r"\1-\2", name)
|
||||||
|
return re.sub("([a-z0-9])([A-Z])", r"\1-\2", s1).lower()
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def set_properties(obj, **kwargs):
|
def set_properties(obj, **kwargs):
|
||||||
"""A context manager to set, then reset, certain properties of an object.
|
"""A context manager to set, then reset, certain properties of an object.
|
||||||
|
|
|
||||||
|
|
@ -14,18 +14,18 @@ logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
|
||||||
|
|
||||||
class TestPluginMethods(unittest.TestCase):
|
class TestPluginMethods(unittest.TestCase):
|
||||||
def test_plugin_load(self):
|
def test_plugin_load(self):
|
||||||
plugin_arr = microscope.plugin.plugins
|
plugin_arr = microscope.plugins.plugins
|
||||||
|
|
||||||
plugin_names = [plugin[0] for plugin in plugin_arr]
|
plugin_names = [plugin[0] for plugin in plugin_arr]
|
||||||
|
|
||||||
self.assertTrue("testing" in plugin_names)
|
self.assertTrue("testing" in plugin_names)
|
||||||
|
|
||||||
def test_camera_access(self):
|
def test_camera_access(self):
|
||||||
identify = microscope.plugin.testing.identify()
|
identify = microscope.plugins.testing.identify()
|
||||||
self.assertTrue(identify[0] is microscope.camera)
|
self.assertTrue(identify[0] is microscope.camera)
|
||||||
|
|
||||||
def test_stage_access(self):
|
def test_stage_access(self):
|
||||||
identify = microscope.plugin.testing.identify()
|
identify = microscope.plugins.testing.identify()
|
||||||
self.assertTrue(identify[1] is microscope.stage)
|
self.assertTrue(identify[1] is microscope.stage)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
microscope.attach(PiCameraStreamer(), OpenFlexureStage())
|
microscope.attach(PiCameraStreamer(), OpenFlexureStage())
|
||||||
|
|
||||||
microscope.plugin.attach("openflexure_microscope.plugins.testing:Plugin")
|
microscope.plugins.attach("openflexure_microscope.plugins.testing:Plugin")
|
||||||
|
|
||||||
suites = [unittest.TestLoader().loadTestsFromTestCase(TestPluginMethods)]
|
suites = [unittest.TestLoader().loadTestsFromTestCase(TestPluginMethods)]
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue