Drafted actions routes
This commit is contained in:
parent
6581612312
commit
d79fd55301
8 changed files with 360 additions and 25 deletions
|
|
@ -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():
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue