Drafted actions routes

This commit is contained in:
jtc42 2019-11-15 16:40:25 +00:00
parent 6581612312
commit d79fd55301
8 changed files with 360 additions and 25 deletions

View file

@ -143,7 +143,7 @@ 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 ### V2
# Tasks routes # Root routes
v2_stream_blueprint = v2.blueprints.stream.construct_blueprint(api_microscope) v2_stream_blueprint = v2.blueprints.stream.construct_blueprint(api_microscope)
app.register_blueprint(v2_stream_blueprint, url_prefix=uri("/", "v2")) app.register_blueprint(v2_stream_blueprint, url_prefix=uri("/", "v2"))
@ -167,6 +167,10 @@ app.register_blueprint(v2_plugin_blueprint, url_prefix=uri("/plugins", "v2"))
v2_tasks_blueprint = v2.blueprints.tasks.construct_blueprint(api_microscope) v2_tasks_blueprint = v2.blueprints.tasks.construct_blueprint(api_microscope)
app.register_blueprint(v2_tasks_blueprint, url_prefix=uri("/tasks", "v2")) 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():

View file

@ -0,0 +1,87 @@
from flask import Blueprint, url_for, jsonify
from sys import platform
from . import camera, stage, system
_actions = {
"capture": {
"rule": "/camera/capture/",
"view_class": camera.CaptureAPI,
"description": "Take a single still capture",
"conditions": True
},
"preview_start": {
"rule": "/camera/preview/start",
"view_class": camera.GPUPreviewStartAPI,
"description": "Start the on-board GPU preview",
"conditions": True
},
"preview_stop": {
"rule": "/camera/preview/stop",
"view_class": camera.GPUPreviewStopAPI,
"description": "Stop the on-board GPU preview",
"conditions": True
},
"move": {
"rule": "/stage/move/",
"view_class": stage.MoveStageAPI,
"description": "Move the microscope stage",
"conditions": True
},
"shutdown": {
"rule": "/system/shutdown/",
"view_class": system.ShutdownAPI,
"description": "Shutdown the device",
"conditions": (platform == "linux")
},
"reboot": {
"rule": "/system/reboot/",
"view_class": system.RebootAPI,
"description": "Reboot the device",
"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}")},
"description": action["description"],
"rule": action["rule"],
"view_class": str(action["view_class"])
}
actions[name] = d
return actions
def construct_blueprint(microscope_obj):
global _actions
blueprint = Blueprint("actions_blueprint", __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.route("/")
def representation():
return jsonify(actions_representation())
return blueprint

View file

@ -0,0 +1,166 @@
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):
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):
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):
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)

View file

@ -0,0 +1,54 @@
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):
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)

View file

@ -0,0 +1,33 @@
from openflexure_microscope.api.views import MicroscopeView
from flask import jsonify
import subprocess
import logging
# TODO: Make robust against different host OS
class ShutdownAPI(MicroscopeView):
def post(self):
"""
Attempt to shutdown the device
.. :quickref: Actions; Shutdown
"""
subprocess.Popen(['shutdown', '-h', 'now'])
return '', 202
class RebootAPI(MicroscopeView):
def post(self):
"""
Attempt to shutdown the device
.. :quickref: Actions; Shutdown
"""
subprocess.Popen(['sudo', 'shutdown', '-r', 'now'])
return '', 202

View file

@ -26,7 +26,7 @@ def captures_representation(capture_list: list, include_unavailable: bool = Fals
# Add API routes to returned representations # Add API routes to returned representations
extra_state = { extra_state = {
"links": { "links": {
"properties": "{}".format(url_for(".capture", capture_id=capture_key)), "self": "{}".format(url_for(".capture", capture_id=capture_key)),
"download": "{}".format(url_for(".capture_download", capture_id=capture_key, filename=capture_repr["filename"])), "download": "{}".format(url_for(".capture_download", capture_id=capture_key, filename=capture_repr["filename"])),
"tags": "{}".format(url_for(".capture_tags", capture_id=capture_key)), "tags": "{}".format(url_for(".capture_tags", capture_id=capture_key)),
} }
@ -131,30 +131,15 @@ class CaptureAPI(MicroscopeView):
- **state** *(string)*: api uri to the capture json representation - **state** *(string)*: api uri to the capture json representation
""" """
capture_obj = self.microscope.camera.image_from_id(capture_id)
if not capture_obj: all_captures = captures_representation(self.microscope.camera.images, include_unavailable=True)
if capture_id in all_captures:
representation = all_captures[capture_id]
else:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
# Get capture state return jsonify(representation)
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): def delete(self, capture_id):
""" """

View file

@ -17,7 +17,7 @@ def tasks_representation():
# Add API routes to returned representations # Add API routes to returned representations
extra_state = { extra_state = {
"links": { "links": {
"properties": "{}".format(url_for(".task", task_id=task_key)), "self": "{}".format(url_for(".task", task_id=task_key)),
} }
} }

View file

@ -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):
@ -93,6 +97,7 @@ class PiCameraStreamer(BaseCamera):
self.stream_resolution = (832, 624) #: tuple: Resolution for stream and video 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.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
@ -298,6 +303,7 @@ class PiCameraStreamer(BaseCamera):
""" """
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.status["zoom_value"] = float(zoom_value) self.status["zoom_value"] = float(zoom_value)
if self.status["zoom_value"] < 1: if self.status["zoom_value"] < 1: