Implemented root API v2 routes

This commit is contained in:
jtc42 2019-11-14 16:33:00 +00:00
parent 5aa783c269
commit 6581612312
27 changed files with 1205 additions and 148 deletions

View file

@ -21,6 +21,7 @@ from openflexure_microscope import Microscope
from openflexure_microscope.camera.capture import build_captures_from_exif
from openflexure_microscope.config import settings_file_path, JSONEncoder
from openflexure_microscope.api.v1 import blueprints
from openflexure_microscope.api import v2
# Import device modules
# NB this will eventually be handled by the RC file, so you can choose what device
@ -141,6 +142,31 @@ app.register_blueprint(plugin_blueprint, url_prefix=uri("/plugin", "v1"))
task_blueprint = blueprints.task.construct_blueprint(api_microscope)
app.register_blueprint(task_blueprint, url_prefix=uri("/task", "v1"))
### V2
# Tasks routes
v2_stream_blueprint = v2.blueprints.stream.construct_blueprint(api_microscope)
app.register_blueprint(v2_stream_blueprint, url_prefix=uri("/", "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"))
@app.route("/routes")
def routes():
@ -184,7 +210,7 @@ def cleanup():
# Save config
logging.debug("Saving config for teardown...")
api_microscope.save_config(backup=True)
api_microscope.save_settings()
logging.debug("Settling...")
time.sleep(0.5)

View file

@ -152,7 +152,7 @@ class ConfigAPI(MicroscopeView):
:>header Content-Type: application/json
: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):
"""
@ -223,10 +223,10 @@ class ConfigAPI(MicroscopeView):
logging.debug("Updating settings from POST request:")
logging.debug(payload.json)
self.microscope.apply_config(payload.json)
self.microscope.save_config()
self.microscope.apply_settings(payload.json)
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):

View file

@ -130,16 +130,19 @@ class ListAPI(MicroscopeView):
output.file, use_video_port=use_video_port, resize=resize, bayer=bayer
)
metadata.update(
{
"microscope_settings": self.microscope.read_config(),
# 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)

View file

@ -16,7 +16,7 @@ class ZoomAPI(MicroscopeView):
:<header Content-Type: application/json
: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})
@ -47,7 +47,7 @@ class ZoomAPI(MicroscopeView):
self.microscope.camera.set_zoom(zoom_value)
return jsonify(self.microscope.camera.state)
return jsonify(self.microscope.camera.status)
class OverlayAPI(MicroscopeView):

View file

@ -3,6 +3,7 @@ from openflexure_microscope.api.views import MicroscopeViewPlugin
from flask import Blueprint, jsonify
from openflexure_microscope.api.views import MicroscopeView
import copy
import logging
import warnings
@ -21,7 +22,7 @@ class PluginFormAPI(MicroscopeView):
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)
@ -38,7 +39,10 @@ def construct_blueprint(microscope_obj):
all_routes = []
# For each plugin attached to the microscope object
for plugin_name, plugin_obj in microscope_obj.plugin.plugins:
for plugin_representation in microscope_obj.plugins.active:
plugin_obj = plugin_representation["plugin"]
plugin_name = plugin_representation["name"]
# If plugin contains valid endpoints
if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict):
@ -94,7 +98,7 @@ def construct_blueprint(microscope_obj):
if hasattr(plugin_obj, "api_form") and isinstance(
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
if "forms" in api_form_info and isinstance(
api_form_info["forms"], list
@ -110,8 +114,8 @@ def construct_blueprint(microscope_obj):
)
# Store the complete form in Microscope().plugin.form
microscope_obj.plugin.forms.append(api_form_info)
print(microscope_obj.plugin.forms)
microscope_obj.plugins.forms.append(api_form_info)
print(microscope_obj.plugins.forms)
else:
warnings.warn(

View file

@ -0,0 +1 @@
from . import blueprints

View file

@ -0,0 +1 @@
from . import captures, settings, status, tasks, stream, plugins, actions

View file

@ -0,0 +1,422 @@
from openflexure_microscope.api.utilities import get_bool, JsonResponse
from openflexure_microscope.api.views import MicroscopeView
from openflexure_microscope.utilities import filter_dict
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": {
"properties": "{}".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
"""
include_unavailable = get_bool(request.args.get("include_unavailable"))
representation = captures_representation(self.microscope.camera.images, include_unavailable=include_unavailable)
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
"""
capture_obj = self.microscope.camera.image_from_id(capture_id)
if not capture_obj:
return abort(404) # 404 Not Found
# Get capture state
capture_metadata = capture_obj.state
# Add API routes to returned state
uri_dict = {
"uri": {
"state": "{}".format(url_for(".capture", capture_id=capture_obj.id))
}
}
# If available, also add download link
if capture_metadata["available"]:
uri_dict["uri"]["download"] = "{}download/{}".format(
url_for(".capture", capture_id=capture_obj.id), capture_obj.filename
)
capture_metadata.update(uri_dict)
return jsonify(capture_metadata)
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("captures_blueprint", __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("capture_list", microscope=microscope_obj),
)
return blueprint

View file

@ -0,0 +1,151 @@
from openflexure_microscope.plugins import PluginLoader, MicroscopePlugin
from openflexure_microscope.api.views import MicroscopeViewPlugin
from flask import Blueprint, jsonify
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:
d = {
"name": plugin["name"],
"plugin": str(plugin["plugin"]),
"routes": plugin["routes"],
"form": plugin["form"]
}
plugins.append(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("plugins_blueprint", __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),
)
all_routes = []
# For each plugin attached to the microscope object
for plugin_representation in microscope_obj.plugins.active:
plugin_obj = plugin_representation["plugin"]
plugin_name = plugin_representation["name"]
# If plugin contains valid endpoints
if hasattr(plugin_obj, "api_views") and isinstance(plugin_obj.api_views, dict):
# We'll keep a record of how each route was expanded
expanded_routes = {}
# For each defined endpoint
for view_route, view_class in plugin_obj.api_views.items():
# Remove all leading slashes from view route
cleaned_route = view_route
while cleaned_route[0] == "/":
cleaned_route = cleaned_route[1:]
# Construct a full view route from the plugin name
full_view_route = "/{}/{}".format(plugin_name, cleaned_route)
logging.debug(full_view_route)
# Record how the view_route got expanded
expanded_routes[view_route] = full_view_route
# Check if endpoint name clashes
if full_view_route not in all_routes and issubclass(
view_class, MicroscopeViewPlugin
):
# Add route to main route dictionary
all_routes.append(full_view_route)
# Create a Python-safe name for the route
plugin_route_id = "plugin{}".format(full_view_route).replace(
"/", "_"
)
# Add route to the plugins blueprint
blueprint.add_url_rule(
full_view_route,
view_func=view_class.as_view(
plugin_route_id,
microscope=microscope_obj,
plugin=plugin_obj,
),
)
# Add route to the plugin representation dictionary
plugin_representation["routes"].append(full_view_route)
else:
warnings.warn(
"An endpoint /{} has already been loaded. Skipping {}.".format(
full_view_route, view_class
)
)
# If plugin includes an API form
if hasattr(plugin_obj, "api_form") and isinstance(
plugin_obj.api_form, dict
):
# TODO: We deep copy this to avoid clashing between API versions. Can be removed when v1 is removed.
api_form_info = copy.deepcopy(plugin_obj.api_form)
api_form_info["id"] = plugin_name
if "forms" in api_form_info and isinstance(
api_form_info["forms"], list
):
for form in api_form_info["forms"]:
if "route" in form and form["route"] in expanded_routes.keys():
form["route"] = expanded_routes[form["route"]]
else:
logging.warn(
"No valid expandable route found for {}".format(
form["route"]
)
)
# Store the complete form in Microscope().plugin.form
plugin_representation["form"] = api_form_info
print(microscope_obj.plugins.forms)
else:
warnings.warn(
"No valid 'api_views' dictionary found in {}".format(plugin_obj)
)
return blueprint

View file

@ -0,0 +1,153 @@
from openflexure_microscope.api.utilities import gen, JsonResponse
from openflexure_microscope.api.views import MicroscopeView
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("v2_settings_blueprint", __name__)
blueprint.add_url_rule(
"/", view_func=SettingsAPI.as_view("settings", microscope=microscope_obj)
)
return blueprint

View file

@ -0,0 +1,64 @@
from openflexure_microscope.api.views import MicroscopeView
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("v2_status_blueprint", __name__)
blueprint.add_url_rule(
"/", view_func=StatusAPI.as_view("status", microscope=microscope_obj)
)
return blueprint

View file

@ -0,0 +1,57 @@
from openflexure_microscope.api.utilities import gen, JsonResponse
from openflexure_microscope.api.views import MicroscopeView
from flask import Response, Blueprint, jsonify, request
class StreamAPI(MicroscopeView):
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):
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")
def construct_blueprint(microscope_obj):
blueprint = Blueprint("v2_stream_blueprint", __name__)
blueprint.add_url_rule(
"/stream", view_func=StreamAPI.as_view("stream", microscope=microscope_obj)
)
blueprint.add_url_rule(
"/snapshot",
view_func=SnapshotAPI.as_view("snapshot", microscope=microscope_obj),
)
return blueprint

View file

@ -0,0 +1,160 @@
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": {
"properties": "{}".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.tasks()[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.tasks()[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

View file

@ -100,7 +100,7 @@ class BaseCamera(metaclass=ABCMeta):
last_access (time): Time of last client access to the camera
stream_timeout (int): Number of inactive seconds before timing out the stream
stream_timeout_enabled (bool): Enable or disable timing out the stream
state (dict): Dictionary for capture state
status (dict): Dictionary for capture state
paths (dict): Dictionary of capture paths
images (list): List of image capture objects
videos (list): List of video capture objects
@ -120,7 +120,7 @@ class BaseCamera(metaclass=ABCMeta):
self.stream_timeout = 20
self.stream_timeout_enabled = False
self.state = {"board": None}
self.status = {"board": None}
# TODO: Load/save these to config
self.paths = {"default": BASE_CAPTURE_PATH, "temp": TEMP_CAPTURE_PATH}
@ -130,7 +130,7 @@ class BaseCamera(metaclass=ABCMeta):
self.videos = []
@abstractmethod
def apply_config(self, config: dict):
def apply_settings(self, config: dict):
"""Update settings from a config dictionary"""
with self.lock:
# Apply valid config params to camera object
@ -139,11 +139,11 @@ class BaseCamera(metaclass=ABCMeta):
setattr(self, key, value) # Set to the target value
@abstractmethod
def read_config(self) -> dict:
def read_settings(self) -> dict:
"""Return the current settings as a dictionary"""
return {"paths": self.paths}
def save_config(self):
def save_settings(self):
"""(Optional) Save any settings to disk that need to be stored"""
return
@ -187,7 +187,7 @@ class BaseCamera(metaclass=ABCMeta):
self.last_access = time.time()
self.stop = False
if not self.state["stream_active"]:
if not self.status["stream_active"]:
# start background frame thread
self.thread = threading.Thread(target=self._thread)
self.thread.daemon = True
@ -207,12 +207,12 @@ class BaseCamera(metaclass=ABCMeta):
logging.debug("Stopping worker thread")
timeout_time = time.time() + timeout
if self.state["stream_active"]:
if self.status["stream_active"]:
self.stop = True
self.thread.join() # Wait 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:
logging.debug("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()
logging.debug("Entering worker thread.")
self.state["stream_active"] = True
self.status["stream_active"] = True
for frame in self.frames_iterator:
self.frame = frame
@ -365,7 +365,7 @@ class BaseCamera(metaclass=ABCMeta):
and ( # If using 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"
] # And GPU preview is not active
):
@ -383,4 +383,4 @@ class BaseCamera(metaclass=ABCMeta):
logging.debug("BaseCamera worker thread exiting...")
# Set stream_activate state
self.state["stream_active"] = False
self.status["stream_active"] = False

View file

@ -97,7 +97,7 @@ def capture_from_exif(path, exif_dict):
capture.timestring = exif_dict["time"]
capture.format = exif_dict["format"]
capture._metadata = exif_dict["custom"]
capture.custom_metadata = exif_dict["custom"]
capture.tags = exif_dict["tags"]
return capture
@ -110,7 +110,7 @@ class CaptureObject(object):
Attributes:
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
filefolder (str): Folder in which the capture file will be stored
filename (str): Full name of the capture file
@ -132,7 +132,9 @@ class CaptureObject(object):
self.split_file_path(self.file)
# 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
self.tags = []
@ -203,7 +205,7 @@ class CaptureObject(object):
Args:
data (dict): Dictionary of metadata to be added
"""
self._metadata.update(data)
self.custom_metadata.update(data)
self.save_metadata()
def save_metadata(self) -> None:
@ -225,6 +227,7 @@ class CaptureObject(object):
# Insert exif into file
piexif.insert(exif_bytes, self.file)
@property
def metadata(self) -> dict:
"""
@ -233,13 +236,14 @@ class CaptureObject(object):
"""
d = {
"id": self.id,
"filename": self.filename,
"time": self.timestring,
"format": self.format,
"tags": self.tags,
"custom": self._metadata,
"custom": self.custom_metadata,
}
d.update(self.system_metadata)
# Add custom metadata to dictionary
return d
@ -250,7 +254,7 @@ class CaptureObject(object):
"""
# 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
if self.exists:

View file

@ -26,7 +26,7 @@ class MockStreamer(BaseCamera):
BaseCamera.__init__(self)
# Store state of PiCameraStreamer
self.state.update(
self.status.update(
{"stream_active": False, "record_active": False, "board": None}
)
@ -74,13 +74,13 @@ class MockStreamer(BaseCamera):
BaseCamera.close(self)
# HANDLE SETTINGS
def read_config(self) -> dict:
def read_settings(self) -> dict:
"""
Return config dictionary of the PiCameraStreamer.
"""
# Get config items from the base class
conf_dict = BaseCamera.read_config(self)
conf_dict = BaseCamera.read_settings(self)
# Include device-specific config items
conf_dict.update(
@ -94,7 +94,7 @@ class MockStreamer(BaseCamera):
return conf_dict
def apply_config(self, config: dict):
def apply_settings(self, config: dict):
"""
Write a config dictionary to the PiCameraStreamer config.
@ -110,7 +110,7 @@ class MockStreamer(BaseCamera):
with self.lock:
# 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
if hasattr(self, key):

View file

@ -75,8 +75,8 @@ class PiCameraStreamer(BaseCamera):
picamera.PiCamera()
) #: :py:class:`picamera.PiCamera`: Picamera object
# Store state of PiCameraStreamer
self.state.update(
# Store status of PiCameraStreamer
self.status.update(
{
"stream_active": False,
"record_active": False,
@ -97,7 +97,7 @@ class PiCameraStreamer(BaseCamera):
self.picamera_lst_path = settings_file_path("picamera_lst.npy") #: str: Path of .npy lens shading table file
# Update board identifier
self.state.update({})
self.status.update({})
# Create an empty stream
self.stream = io.BytesIO()
@ -118,13 +118,13 @@ class PiCameraStreamer(BaseCamera):
self.camera.close()
# HANDLE SETTINGS
def read_config(self) -> dict:
def read_settings(self) -> dict:
"""
Return config dictionary of the PiCameraStreamer.
"""
# Get config items from the base class
conf_dict = BaseCamera.read_config(self)
conf_dict = BaseCamera.read_settings(self)
# Include device-specific config items
conf_dict.update(
@ -149,12 +149,12 @@ class PiCameraStreamer(BaseCamera):
return conf_dict
def save_config(self):
def save_settings(self):
"""Save lens-shading table to disk"""
logging.info("Saving picamera_lst to {}".format(self.picamera_lst_path))
self.save_lens_shading_table()
def apply_config(self, config: dict):
def apply_settings(self, config: dict):
"""
Write a config dictionary to the PiCameraStreamer config.
@ -172,10 +172,10 @@ class PiCameraStreamer(BaseCamera):
with self.lock:
# 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
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.")
self.stop_stream_recording() # Pause stream
paused_stream = True # Remember to unpause stream when done
@ -299,13 +299,13 @@ class PiCameraStreamer(BaseCamera):
Change the camera zoom, handling re-centering and scaling.
"""
with self.lock:
self.state["zoom_value"] = float(zoom_value)
if self.state["zoom_value"] < 1:
self.state["zoom_value"] = 1
self.status["zoom_value"] = float(zoom_value)
if self.status["zoom_value"] < 1:
self.status["zoom_value"] = 1
# Richard's code for zooming !
fov = self.camera.zoom
centre = np.array([fov[0] + fov[2] / 2.0, fov[1] + fov[3] / 2.0])
size = 1.0 / self.state["zoom_value"]
size = 1.0 / self.status["zoom_value"]
# If the new zoom value would be invalid, move the centre to
# keep it within the camera's sensor (this is only relevant
# when zooming out, if the FoV is not centred on (0.5, 0.5)
@ -332,7 +332,7 @@ class PiCameraStreamer(BaseCamera):
self.camera.preview.window = window
if fullscreen:
self.camera.preview.fullscreen = fullscreen
self.state["preview_active"] = True
self.status["preview_active"] = True
except picamera.exc.PiCameraMMALError as e:
logging.error(
"Suppressed a MMALError in start_preview. Exception: {}".format(e)
@ -347,7 +347,7 @@ class PiCameraStreamer(BaseCamera):
def stop_preview(self):
"""Stop the on board GPU camera 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):
"""Start recording.
@ -365,7 +365,7 @@ class PiCameraStreamer(BaseCamera):
"""
with self.lock:
# 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
logging.info("Recording to {}".format(output))
@ -378,8 +378,8 @@ class PiCameraStreamer(BaseCamera):
quality=quality,
)
# Update state dictionary
self.state["record_active"] = True
# Update status dictionary
self.status["record_active"] = True
return output
@ -398,8 +398,8 @@ class PiCameraStreamer(BaseCamera):
self.camera.stop_recording(splitter_port=2)
logging.info("Recording stopped")
# Update state dictionary
self.state["record_active"] = False
# Update status dictionary
self.status["record_active"] = False
def stop_stream_recording(
self, splitter_port: int = 1, resolution: Tuple[int, int] = None
@ -467,7 +467,7 @@ class PiCameraStreamer(BaseCamera):
self.camera.resolution = resolution
# If the stream should be active
if self.state["stream_active"]:
if self.status["stream_active"]:
try:
# Start recording on stream port
self.camera.start_recording(
@ -633,7 +633,7 @@ class PiCameraStreamer(BaseCamera):
# Start stream recording (and set resolution)
self.start_stream_recording()
# Update state
# Update status
logging.debug("STREAM ACTIVE")
# While the iterator is not closed

View file

@ -85,16 +85,16 @@ class OpenflexureSettingsFile:
# If the loaded config was in contracted format
if self.expand:
# Contract self._config into self.raw_config
save_config = self.contract_config(config)
save_settings = self.contract_config(config)
else:
save_config = config
save_settings = config
if backup:
if os.path.isfile(self.config_path):
shutil.copyfile(self.config_path, self.config_path + ".bk")
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):
logging.debug("Merging settings with file on disk")

View file

@ -11,7 +11,7 @@ from openflexure_microscope.stage.mock import MockStage
from openflexure_microscope.camera.base import BaseCamera
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.common.lock import CompositeLock
from openflexure_microscope.config import user_settings
@ -30,7 +30,7 @@ class Microscope:
stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): Stage object
task: (:py:class:`openflexure_microscope.task.TaskOrchestrator`): Threaded ask orchestrator for managing
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):
@ -49,10 +49,10 @@ class Microscope:
self.task = TaskOrchestrator()
# 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
self.plugin = PluginMount(self)
self.plugins = PluginLoader(self)
self.attach_plugins(self.plugin_maps)
def __enter__(self):
@ -84,7 +84,7 @@ class Microscope:
stage (:py:class:`openflexure_microscope.stage.base.BaseStage`): stage object
"""
settings_full = self.read_config()
settings_full = self.read_settings()
logging.debug("Attaching camera...")
self.camera = (
@ -117,7 +117,7 @@ class Microscope:
self.lock.locks.append(self.stage.lock)
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):
"""
@ -125,7 +125,7 @@ class Microscope:
"""
if plugin_maps:
for plugin_map in plugin_maps:
self.plugin.attach(plugin_map)
self.plugins.attach(plugin_map)
else:
logging.warning("No plugins specified. Skipping.")
@ -134,7 +134,7 @@ class Microscope:
Empty the plugin mount and re-attach from config.
"""
logging.info("Tearing down existing PluginMount...")
self.plugin = PluginMount(self)
self.plugins = PluginLoader(self)
logging.info("Repopulating PluginMount...")
self.attach_plugins(self.plugin_maps)
@ -157,17 +157,34 @@ class Microscope:
Return:
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 = {
"camera": self.camera.state,
"stage": self.stage.state,
"plugin": self.plugin.state,
"camera": self.camera.status,
"stage": self.stage.status,
"plugin": self.plugins.state,
"version": pkg_resources.get_distribution("openflexure_microscope").version,
}
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.
"""
@ -175,11 +192,11 @@ class Microscope:
# If attached to a 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 ("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
if "id" in config:
@ -191,7 +208,7 @@ class Microscope:
if "plugins" in config:
self.plugin_maps = config["plugins"]
def read_config(self, json_safe=False):
def read_settings(self, json_safe=False):
"""
Get an updated settings dictionary.
@ -211,47 +228,43 @@ class Microscope:
# If attached to a 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
# If attached to a 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_full = user_settings.merge(settings_current)
return settings_full
def save_config(self):
def save_settings(self):
"""
Merges the current settings back to disk
"""
# Read curent config
current_config = self.read_config()
# Merge in server version responsible for saving the config file
current_config["server_version"] = pkg_resources.get_distribution(
"openflexure_microscope"
).version
current_config = self.read_settings()
# Save config to file
if self.camera:
self.camera.save_config()
self.camera.save_settings()
if self.stage:
self.stage.save_config()
self.stage.save_settings()
user_settings.save(current_config, backup=True)
@property
def config(self) -> dict:
logging.warning(
"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
def config(self, config: dict) -> None:
logging.warning(
"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)

View file

@ -3,7 +3,7 @@ __all__ = [
"load_plugin_class",
"load_plugin_module",
"class_from_map",
"PluginMount",
"PluginLoader",
"MicroscopePlugin",
]
@ -12,6 +12,6 @@ from .loader import (
load_plugin_class,
load_plugin_module,
class_from_map,
PluginMount,
PluginLoader,
MicroscopePlugin,
)

View file

@ -37,10 +37,10 @@ class Plugin(MicroscopePlugin):
"""
scamera = self.microscope.camera
with scamera.lock:
assert not scamera.state[
assert not scamera.status[
"record_active"
], "Can't recalibrate while recording!"
streaming = scamera.state["stream_active"]
streaming = scamera.status["stream_active"]
if streaming:
logging.info("Stopping stream before recalibration")
scamera.stop_stream_recording(resolution=(640, 480))
@ -51,7 +51,7 @@ class Plugin(MicroscopePlugin):
recalibrate_camera(scamera.camera)
finally:
scamera.camera.resolution = old_resolution
self.microscope.save_config()
self.microscope.save_settings()
if streaming:
logging.info("Restarting stream after recalibration")
scamera.start_stream_recording()

View file

@ -97,18 +97,19 @@ class ScanPlugin(MicroscopePlugin):
if "scan" not in tags:
tags.append("scan")
metadata.update(
{
"scan_id": scan_id,
"basename": basename,
"microscope_settings": self.microscope.read_config(),
"microscope_state": self.microscope.state,
"microscope_id": self.microscope.id,
"microscope_name": self.microscope.name,
}
)
# 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)
def tile(

View file

@ -137,7 +137,7 @@ def class_from_map(plugin_map):
return load_plugin_class(*plugin_arr)
class PluginMount(object):
class PluginLoader(object):
"""
A mount-point for all loaded plugins. Attaches to a Microscope object.
@ -147,30 +147,19 @@ class PluginMount(object):
def __init__(self, parent):
self.parent = parent
self.plugins = [] # List of plugin objects
self._plugins = [] # List of plugin objects
self.forms = [] # List of plugin forms
logging.info("Creating plugin mount")
@property
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 [m["python_name"] for m in self._plugins]
@property
def members(self):
ignores = ["state", "members", "attach"]
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 active(self):
return self._plugins
def attach(self, plugin_map):
"""
@ -204,7 +193,15 @@ class PluginMount(object):
): # If plugin_object is an instance of MicroscopePlugin
# Attach plugin_object to the plugin mount
setattr(self, pythonsafe_plugin_name, plugin_object)
self.plugins.append((plugin_name, plugin_object))
# Store the plugin object, and it's properties
self._plugins.append({
"name": plugin_name,
"python_name": pythonsafe_plugin_name,
"plugin": plugin_object,
"routes": [],
"form": None
})
# Grant plugin access to the hardware
plugin_object.microscope = self.parent

View file

@ -14,22 +14,22 @@ class BaseStage(metaclass=ABCMeta):
self.lock = StrictLock(timeout=5)
@abstractmethod
def apply_config(self, config: dict):
def apply_settings(self, config: dict):
"""Update settings from a config dictionary"""
pass
@abstractmethod
def read_config(self):
def read_settings(self):
"""Return the current settings as a dictionary"""
pass
def save_config(self):
def save_settings(self):
"""(Optional) Save any settings to disk that need to be stored"""
return
@property
@abstractmethod
def state(self):
def status(self):
"""The general state dictionary of the board.
Should at least contain 'position', and 'board' keys.
Note: A None/Null value for 'board' will disable stage

View file

@ -16,9 +16,9 @@ class MockStage(BaseStage):
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
@property
def state(self):
"""The general state dictionary of the board."""
state = {
def status(self):
"""The general status dictionary of the board."""
status = {
"position": {
"x": self.position[0],
"y": self.position[1],
@ -27,9 +27,9 @@ class MockStage(BaseStage):
"board": None,
"version": "0",
}
return state
return status
def apply_config(self, config: dict):
def apply_settings(self, config: dict):
"""Update settings from a config dictionary"""
# 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])
self.backlash = backlash
def read_config(self) -> dict:
def read_settings(self) -> dict:
"""Return the current settings as a dictionary"""
blsh = self.backlash.tolist()
config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}}

View file

@ -31,9 +31,9 @@ class SangaStage(BaseStage):
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
@property
def state(self):
"""The general state dictionary of the board."""
state = {
def status(self):
"""The general status dictionary of the board."""
status = {
"position": {
"x": self.position[0],
"y": self.position[1],
@ -42,7 +42,7 @@ class SangaStage(BaseStage):
"board": self.board.board,
"firmware": self.board.firmware,
}
return state
return status
@property
def n_axes(self):
@ -85,7 +85,7 @@ class SangaStage(BaseStage):
else:
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"""
# 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])
self.backlash = backlash
def read_config(self) -> dict:
def read_settings(self) -> dict:
"""Return the current settings as a dictionary"""
blsh = self.backlash.tolist()
config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}}

View file

@ -14,18 +14,18 @@ logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
class TestPluginMethods(unittest.TestCase):
def test_plugin_load(self):
plugin_arr = microscope.plugin.plugins
plugin_arr = microscope.plugins.plugins
plugin_names = [plugin[0] for plugin in plugin_arr]
self.assertTrue("testing" in plugin_names)
def test_camera_access(self):
identify = microscope.plugin.testing.identify()
identify = microscope.plugins.testing.identify()
self.assertTrue(identify[0] is microscope.camera)
def test_stage_access(self):
identify = microscope.plugin.testing.identify()
identify = microscope.plugins.testing.identify()
self.assertTrue(identify[1] is microscope.stage)
@ -35,7 +35,7 @@ if __name__ == "__main__":
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)]