diff --git a/docs/source/api.rst b/docs/source/api.rst
index cbc93554..4d409ccd 100644
--- a/docs/source/api.rst
+++ b/docs/source/api.rst
@@ -3,14 +3,14 @@ REST API
Summary
-------
-.. qrefflask:: openflexure_microscope.api.v1:app
+.. qrefflask:: openflexure_microscope.api.app:app
:undoc-endpoints: index
:undoc-static:
:endpoints:
Details
-----------
-.. autoflask:: openflexure_microscope.api.v1:app
+.. autoflask:: openflexure_microscope.api.app:app
:undoc-endpoints: index
:undoc-static:
:endpoints:
diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py
index 4a109823..413d98cb 100644
--- a/openflexure_microscope/api/app.py
+++ b/openflexure_microscope/api/app.py
@@ -1,187 +1,130 @@
#!/usr/bin/env python
+"""
+TODO: Implement API route to cleanly shut down server
+TODO: Implement plugin API routes somehow
+"""
-from pprint import pprint
+import numpy as np
from importlib import import_module
import time
import datetime
+import os
from flask import (
- Flask, render_template, Response,
- redirect, request, jsonify, send_file)
+ Flask, render_template, Response, url_for,
+ redirect, request, jsonify, send_file, abort,
+ make_response)
-import numpy as np
+from flask.views import MethodView
+from werkzeug.exceptions import default_exceptions
+from openflexure_microscope.api.utilities import parse_payload, get_from_payload, gen, get_bool, list_routes
+
+from openflexure_microscope import Microscope, config
from openflexure_microscope.camera.pi import StreamingCamera
+from openflexure_stage import OpenFlexureStage
+import atexit
+import logging, sys
+
+logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
+
+# Create a dummy microscope object, with no hardware attachments
+openflexurerc = config.load_config() # Load default user config
+api_microscope = Microscope(None, None, config=openflexurerc)
+logging.debug("Created an empty microscope in global.")
+
+
+# Generate API URI based on version from filename
+def uri(suffix, api_version, base=None):
+ if not base:
+ base = "/api/{}".format(api_version)
+ uri = base + suffix
+ logging.debug("Created app route: {}".format(uri))
+ return uri
+
+# Create flask app
app = Flask(__name__)
-cam = StreamingCamera()
+app.url_map.strict_slashes = False
-def parse_payload(request):
- """Convert request to JSON. Will eventually handle error-checking."""
- # TODO: Handle invalid JSON payloads
- state = request.get_json()
- return state
+# Make errors more API friendly
+
+def _handle_http_exception(e):
+ return make_response(
+ jsonify({
+ 'status_code': e.code,
+ 'error': e.name,
+ 'details': e.description
+ }),
+ e.code)
+
+for code in default_exceptions:
+ app.errorhandler(code)(_handle_http_exception)
-def gen(camera):
- """Video streaming generator function."""
- while True:
- # the obtained frame is a jpeg
- frame = camera.get_frame()
+# After app starts, but before first request, attach hardware to global microscope
+@app.before_first_request
+def attach_microscope():
+ # Create the microscope object globally (common to all spawned server threads)
+ global api_microscope, openflexurerc
+ logging.debug("First request made. Populating microscope with hardware...")
- yield (b'--frame\r\n'
- b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')
+ logging.debug("Creating camera object...")
+ api_camera = StreamingCamera(config=openflexurerc)
+ logging.debug("Creating stage object...")
+ api_stage = OpenFlexureStage("/dev/ttyUSB0")
+ logging.debug("Attaching devices to microscope...")
+ api_microscope.attach(
+ api_camera,
+ api_stage
+ )
+
+ logging.debug("Microscope successfully attached!")
+
+
+##### WEBAPP ROUTES ######
@app.route('/')
def index():
- """Video streaming home page."""
- cam.start_worker() # Start the stream
- return render_template('index.html')
-
-
-@app.route('/stream')
-def stream():
- """Video streaming route. Put this in the src attribute of an img tag."""
- return Response(
- gen(cam),
- mimetype='multipart/x-mixed-replace; boundary=frame')
-
-
-# TODO: Be able to change the image resolution with an option
-@app.route('/capture/', methods=['GET', 'POST', 'PUT'])
-def capture():
"""
- POST/PUT: Capture a single image.
- GET: Return capture status, or download latest capture.
+ API demo app
"""
- if request.method == 'POST' or request.method == 'PUT':
- state = parse_payload(request)
- print(state)
+ return render_template(
+ 'index_v1.html'
+ )
- # Handle filename argument
- if 'filename' in state:
- filename = state['filename']
- else:
- filename = None
+##### API ROUTES ######
+from openflexure_microscope.api.v1 import blueprints
- if 'write_to_file' in state:
- write_to_file = bool(state['write_to_file'])
- else:
- write_to_file = False
+logging.debug("Registering blueprints...")
+# Base routes
+base_blueprint = blueprints.base.construct_blueprint(api_microscope)
+app.register_blueprint(base_blueprint, url_prefix=uri('', 'v1'))
- if 'use_video_port' in state:
- use_video_port = bool(state['use_video_port'])
- else:
- use_video_port = False
+# Stage routes
+stage_blueprint = blueprints.stage.construct_blueprint(api_microscope)
+app.register_blueprint(stage_blueprint, url_prefix=uri('/stage', 'v1'))
- if 'resize' in state:
- resize_h = int(state['resize'])
- resize_w = int(resize_h*(4/3))
- resize = (resize_w, resize_h)
- else:
- resize = None
+# Camera routes
+camera_blueprint = blueprints.camera.construct_blueprint(api_microscope)
+app.register_blueprint(camera_blueprint, url_prefix=uri('/camera', 'v1'))
- response = cam.capture(
- write_to_file=write_to_file,
- use_video_port=use_video_port,
- filename=filename,
- resize=resize)
+# Pluginroutes
+plugin_blueprint = blueprints.plugins.construct_blueprint(api_microscope)
+app.register_blueprint(plugin_blueprint, url_prefix=uri('/plugin', 'v1'))
- return str(response) + "\n"
-
- else: # If GET request
-
- download = request.args.get('download')
-
- state = cam.state
-
- if ((
- download == 'true' or
- download == 'True' or
- download == '1') and
- cam.image is not None):
-
- return send_file(cam.image.data, mimetype='image/jpeg')
- else:
- return jsonify(state)
+# List all routes
+list_routes(app)
-@app.route('/record/', methods=['GET', 'POST', 'PUT'])
-def record():
- """
- POST/PUT: Start or stop a video recording.
- GET: Return recording status, or download latest recording
- """
- if request.method == 'POST' or request.method == 'PUT':
- state = request.get_json()
- print(state)
+# Automatically clean up microscope at exit
+def cleanup():
+ global api_microscope
+ api_microscope.close()
- # Handle filename argument
- if 'filename' in state:
- filename = state['filename']
- else:
- filename = None
+atexit.register(cleanup)
- # Handle status argument
- if 'status' in state:
- status = bool(state['status'])
-
- # synchronise the arduino_time
- if status is True:
- response = cam.start_recording(filename=filename)
- return str(response)
-
- elif status is False:
- response = cam.stop_recording()
- return str(response)
-
- else:
- download = request.args.get('download')
-
- state = cam.state
-
- if ((
- download == 'true' or
- download == 'True' or
- download == '1') and
- cam.state['recent_video'] is not None):
-
- return send_file(
- cam.state['recent_video'],
- mimetype='video/H264',
- as_attachment=True)
- else:
- return jsonify(state)
-
-
-@app.route('/preview/', methods=['GET', 'POST', 'PUT'])
-def preview():
- """
- POST/PUT: Start or stop the direct-to-GPU camera preview on the Pi.
-
- GET: Return preview status
- """
- if request.method == 'POST' or request.method == 'PUT':
- state = request.get_json()
- print(state)
-
- if 'status' in state:
- status = bool(state['status'])
-
- if status is False:
- response = cam.stop_preview()
- else:
- response = cam.start_preview()
-
- return str(response)
-
- else:
- # Get args passed via URL (not useful here. Just for reference.)
- state = cam.state
- return jsonify(state)
-
-
-if __name__ == '__main__':
- app.run(host='0.0.0.0', threaded=True, debug=True, use_reloader=False)
+if __name__ == "__main__":
+ app.run(host='0.0.0.0', port="5000", threaded=True, debug=True, use_reloader=False)
diff --git a/openflexure_microscope/api/templates/index.html b/openflexure_microscope/api/templates/index.html
deleted file mode 100644
index d1f67f86..00000000
--- a/openflexure_microscope/api/templates/index.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
- Video Streaming Demonstration
-
-
- Video Streaming Demonstration
-
-
-
diff --git a/openflexure_microscope/api/templates/index_v0.html b/openflexure_microscope/api/templates/index_v0.html
deleted file mode 100644
index d1f67f86..00000000
--- a/openflexure_microscope/api/templates/index_v0.html
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
- Video Streaming Demonstration
-
-
- Video Streaming Demonstration
-
-
-
diff --git a/openflexure_microscope/api/utilities.py b/openflexure_microscope/api/utilities.py
index f82a6f42..f5b8de98 100644
--- a/openflexure_microscope/api/utilities.py
+++ b/openflexure_microscope/api/utilities.py
@@ -1,3 +1,5 @@
+import pprint
+
def parse_payload(request):
"""Convert request to JSON. Will eventually handle error-checking."""
# TODO: Handle invalid JSON payloads
@@ -31,4 +33,9 @@ def get_bool(get_arg):
get_arg == '1'):
return True
else:
- return False
\ No newline at end of file
+ return False
+
+
+def list_routes(app):
+ """Print available functions."""
+ pprint.pprint(list(map(lambda x: repr(x), app.url_map.iter_rules())))
\ No newline at end of file
diff --git a/openflexure_microscope/api/v0.py b/openflexure_microscope/api/v0.py
deleted file mode 100644
index 5987a9c0..00000000
--- a/openflexure_microscope/api/v0.py
+++ /dev/null
@@ -1,172 +0,0 @@
-#!/usr/bin/env python
-
-from pprint import pprint
-from importlib import import_module
-import time
-import datetime
-
-from flask import (
- Flask, render_template, Response,
- redirect, request, jsonify, send_file)
-
-import numpy as np
-
-from openflexure_microscope.api.utilities import parse_payload, gen
-from openflexure_microscope.camera.pi import StreamingCamera
-
-app = Flask(__name__)
-cam = StreamingCamera()
-
-
-
-@app.route('/')
-def index():
- """Video streaming home page."""
- cam.start_worker() # Start the stream
- return render_template('index_v0.html')
-
-
-@app.route('/stream')
-def stream():
- """Video streaming route. Put this in the src attribute of an img tag."""
- return Response(
- gen(cam),
- mimetype='multipart/x-mixed-replace; boundary=frame')
-
-
-# TODO: Be able to change the image resolution with an option
-@app.route('/capture/', methods=['GET', 'POST', 'PUT'])
-def capture():
- """
- POST/PUT: Capture a single image.
- GET: Return capture status, or download latest capture.
- """
- if request.method == 'POST' or request.method == 'PUT':
- state = parse_payload(request)
- print(state)
-
- # Handle filename argument
- if 'filename' in state:
- filename = state['filename']
- else:
- filename = None
-
- if 'write_to_file' in state:
- write_to_file = bool(state['write_to_file'])
- else:
- write_to_file = False
-
- if 'use_video_port' in state:
- use_video_port = bool(state['use_video_port'])
- else:
- use_video_port = False
-
- if 'resize' in state:
- resize_h = int(state['resize'])
- resize_w = int(resize_h*(4/3))
- resize = (resize_w, resize_h)
- else:
- resize = None
-
- response = cam.capture(
- write_to_file=write_to_file,
- use_video_port=use_video_port,
- filename=filename,
- resize=resize)
-
- return str(response) + "\n"
-
- else: # If GET request
-
- download = request.args.get('download')
-
- state = cam.state
-
- if ((
- download == 'true' or
- download == 'True' or
- download == '1') and
- cam.image is not None):
-
- return send_file(cam.image.data, mimetype='image/jpeg')
- else:
- return jsonify(state)
-
-
-@app.route('/record/', methods=['GET', 'POST', 'PUT'])
-def record():
- """
- POST/PUT: Start or stop a video recording.
- GET: Return recording status, or download latest recording
- """
- if request.method == 'POST' or request.method == 'PUT':
- state = request.get_json()
- print(state)
-
- # Handle filename argument
- if 'filename' in state:
- filename = state['filename']
- else:
- filename = None
-
- # Handle status argument
- if 'status' in state:
- status = bool(state['status'])
-
- # synchronise the arduino_time
- if status is True:
- response = cam.start_recording(filename=filename)
- return str(response)
-
- elif status is False:
- response = cam.stop_recording()
- return str(response)
-
- else:
- download = request.args.get('download')
-
- state = cam.state
-
- if ((
- download == 'true' or
- download == 'True' or
- download == '1') and
- cam.state['recent_video'] is not None):
-
- return send_file(
- cam.state['recent_video'],
- mimetype='video/H264',
- as_attachment=True)
- else:
- return jsonify(state)
-
-
-@app.route('/preview/', methods=['GET', 'POST', 'PUT'])
-def preview():
- """
- POST/PUT: Start or stop the direct-to-GPU camera preview on the Pi.
-
- GET: Return preview status
- """
- if request.method == 'POST' or request.method == 'PUT':
- state = request.get_json()
- print(state)
-
- if 'status' in state:
- status = bool(state['status'])
-
- if status is False:
- response = cam.stop_preview()
- else:
- response = cam.start_preview()
-
- return str(response)
-
- else:
- # Get args passed via URL (not useful here. Just for reference.)
- state = cam.state
- return jsonify(state)
-
-
-if __name__ == '__main__':
- app.run(host='0.0.0.0', threaded=True, debug=True, use_reloader=False)
diff --git a/openflexure_microscope/api/v1.py b/openflexure_microscope/api/v1.py
deleted file mode 100644
index c403894a..00000000
--- a/openflexure_microscope/api/v1.py
+++ /dev/null
@@ -1,695 +0,0 @@
-#!/usr/bin/env python
-"""
-TODO: Add proper docstrings
-TODO: Implement API route to cleanly shut down server
-TODO: Implement microscope function API routes (autofocus etc)
-"""
-
-import numpy as np
-from importlib import import_module
-import time
-import datetime
-import os
-
-from flask import (
- Flask, render_template, Response, url_for,
- redirect, request, jsonify, send_file, abort,
- make_response)
-
-from flask.views import MethodView
-from werkzeug.exceptions import default_exceptions
-
-from openflexure_microscope.api.utilities import parse_payload, get_from_payload, gen, get_bool
-
-from openflexure_microscope import Microscope, config
-from openflexure_microscope.camera.pi import StreamingCamera
-from openflexure_stage import OpenFlexureStage
-
-import atexit
-import logging, sys
-
-logging.basicConfig(stream=sys.stderr, level=logging.DEBUG)
-
-# Create a dummy microscope object, with no hardware attachments
-api_microscope = Microscope(None, None)
-logging.debug("Created an empty microscope in global.")
-
-# Generate API URI based on version from filename
-def uri(suffix, base=None):
- if not base:
- api_ver = os.path.splitext(os.path.basename(__file__))[0]
- base = "/api/{}".format(api_ver)
- uri = base + suffix
- logging.debug("Created app route: {}".format(uri))
- return uri
-
-# Create flask app
-app = Flask(__name__)
-app.url_map.strict_slashes = False
-
-
-# Make errors more API friendly
-
-def _handle_http_exception(e):
- return make_response(
- jsonify({
- 'status_code': e.code,
- 'error': e.name,
- 'details': e.description
- }),
- e.code)
-
-for code in default_exceptions:
- app.errorhandler(code)(_handle_http_exception)
-
-
-# After app starts, but before first request, attach hardware to global microscope
-@app.before_first_request
-def attach_microscope():
- # Create the microscope object globally (common to all spawned server threads)
- global api_microscope
- logging.debug("First request made. Populating microscope with hardware...")
- openflexurerc = config.load_config() # Load default user config
-
- api_microscope.attach(
- StreamingCamera(config=openflexurerc),
- OpenFlexureStage("/dev/ttyUSB0")
- )
-
- logging.debug("Microscope successfully attached!")
-
-
-##### WEBAPP ROUTES ######
-
-@app.route('/')
-def index():
- """
- API demo app
- """
- return render_template(
- 'index_v1.html'
- )
-
-##### API ROUTES ######
-
-
-# Basic microscope view
-
-class MicroscopeView(MethodView):
-
- def __init__(self, microscope, **kwargs):
- """
- Create a generic MethodView with a globally available
- microscope object passed as an argument.
- """
- self.microscope = microscope
-
- MethodView.__init__(self, **kwargs)
-
-
-class StreamAPI(MicroscopeView):
-
- def get(self):
- """
- Real-time MJPEG stream from the microscope camera
-
- .. :quickref: State; Camera 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')
-
-app.add_url_rule(
- uri('/stream/'),
- view_func=StreamAPI.as_view('stream', microscope=api_microscope))
-
-
-class StateAPI(MicroscopeView):
-
- def get(self):
- """
- JSON representation of the microscope object.
-
- .. :quickref: State; Microscope state
-
- **Example request**:
-
- .. sourcecode:: http
-
- GET /state/ 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.state)
-
-app.add_url_rule(
- uri('/state/'),
- view_func=StateAPI.as_view('state', microscope=api_microscope))
-
-
-class PositionAPI(MicroscopeView):
-
- def get(self):
- """
- Return current x, y and z positions of the stage.
-
- .. :quickref: Position; Get current position
-
- **Example request**:
-
- .. sourcecode:: http
-
- GET /stage/position/ HTTP/1.1
- Accept: application/json
-
- **Example response**:
-
- .. sourcecode:: http
-
- HTTP/1.1 200 OK
- Vary: Accept
- Content-Type: application/json
-
- {
- "x": 0,
- "y": 0,
- "z": 0
- }
-
- :>json int x: x steps
- :>json int y: y steps
- :>json int z: z steps
- """
- return jsonify(self.microscope.state['stage']['position'])
-
- def post(self):
- """
- Set x, y and z positions of the stage.
-
- .. :quickref: Position; Update current position
-
- :reqheader Accept: application/json
- : travel_limit:
- # Respond with 400 Bad Request
- response = {'error': 'Cannot move to absolute position beyond the safeguard limit.'}
- return jsonify(response), 400
-
- self.microscope.stage.move_rel(position)
-
- return jsonify(self.microscope.state['stage']['position'])
-
-app.add_url_rule(
- uri('/stage/position/'),
- view_func=PositionAPI.as_view('position', microscope=api_microscope))
-
-
-class StageParamsAPI(MicroscopeView):
-
- def get(self):
- """
- Return current parameters of the stage.
-
- .. :quickref: Stage params; Get current stage parameters
-
- **Example request**:
-
- .. sourcecode:: http
-
- GET /stage/params HTTP/1.1
- Accept: application/json
-
- **Example response**:
-
- .. sourcecode:: http
-
- HTTP/1.1 200 OK
- Vary: Accept
- Content-Type: application/json
-
- {
- "backlash": {
- "x": 0,
- "y": 0,
- "z": 128
- },
- }
-
- """
-
- return jsonify(self.microscope.state['stage'])
-
- def post(self):
- """
- Set parameters of the stage.
-
- .. :quickref: Stage params; Set current stage parameters
-
- :reqheader Accept: application/json
- :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
- - **metadata** *(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'))
-
- if include_unavailable:
- captures = [image.metadata for image in self.microscope.camera.images]
- else:
- captures = [image.metadata for image in self.microscope.camera.images if image.metadata['available']]
-
- return jsonify(captures)
-
- def delete(self):
- """
- Delete all captures (not yet implemented)
-
- .. :quickref: Capture collection; Delete all captures
- """
- for image in self.microscope.camera.images:
- image.delete()
-
- captures = [image.metadata for image in self.microscope.camera.images]
-
- return jsonify(captures)
-
- def post(self):
- """
- Create a new image capture.
-
- .. :quickref: Capture collection; New capture
-
- **Example request**:
-
- .. sourcecode:: http
-
- POST /camera/capture HTTP/1.1
- Accept: application/json
-
- {
- "filename": "myfirstcapture",
- "keep_on_disk": true,
- "use_video_port": true,
- "size": {
- "x": 640,
- "y": 480
- }
- }
-
- :>header Accept: application/json
-
- :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
- - **metadata** *(string)*: api uri to the capture json representation
-
- :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
- - **metadata** *(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 metadata
- capture_metadata = capture_obj.metadata
-
- # TODO: Tidy up adding URI to metadata
- # Add API routes to returned metadata
- uri_dict = {
- 'uri': {'metadata': uri('/capture/{}/'.format(capture_id))}
- }
-
- # If available, also add download link
- if capture_metadata['available']:
- uri_dict['uri']['download'] = uri('/capture/{}/download/{}'.format(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):
- """
- Modify the metadata of a capture (not yet implemented)
-
- .. :quickref: Capture; Update capture metadata
- """
- return jsonify({"return": capture_id})
-
-app.add_url_rule(
- uri('/camera/capture//'),
- view_func=CaptureAPI.as_view('capture', microscope=api_microscope))
-
-
-class CaptureDownloadRedirectAPI(MicroscopeView):
- def get(self, capture_id):
- """
- Redirect to download the capture under it's currently set filename.
- I.e., `/(capture_id)/download` will
- redirect to `/(capture_id)/download/(filename)`.
-
- Note: This route may be deprecated in the future. Where at all
- possible, please include a filename to download as.
-
- .. :quickref: Capture; Redirect to download
- """
- capture_obj = self.microscope.camera.image_from_id(capture_id)
-
- if not capture_obj or not capture_obj.metadata['available']:
- return abort(404) # 404 Not Found
-
- as_attachment = get_bool(request.args.get('as_attachment'))
- thumbnail = get_bool(request.args.get('thumbnail'))
-
- return redirect(url_for('capture_download', capture_id=capture_id, filename=capture_obj.filename, as_attachment=as_attachment, thumbnail=thumbnail), code=307)
-
-# Route for shortcut where no filename is specified
-app.add_url_rule(
- uri('/camera/capture//download'),
- view_func=CaptureDownloadRedirectAPI.as_view('capture_download_redirect', microscope=api_microscope))
-
-
-class CaptureDownloadAPI(MicroscopeView):
- def get(self, capture_id, filename):
- """
- 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.
-
- .. :quickref: Capture; Download capture file
-
- **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
- :query as_attachment: return the image as an attachment download e.g. ?as_attachment=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.metadata['available']:
- return abort(404) # 404 Not Found
-
- as_attachment = get_bool(request.args.get('as_attachment'))
- 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, as_attachment=as_attachment, 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',
- as_attachment=as_attachment,
- attachment_filename=filename)
-
-app.add_url_rule(
- uri('/camera/capture//download/'),
- view_func=CaptureDownloadAPI.as_view('capture_download', microscope=api_microscope))
-
-
-# Preview
-class GPUPreviewAPI(MicroscopeView):
-
- def post(self, operation):
- """
- Create a new image capture.
-
- .. :quickref: GPU Preview; Start/stop preview
-
- **Example requests**:
-
- .. sourcecode:: http
-
- POST /camera/preview/start HTTP/1.1
- Accept: application/json
-
- .. sourcecode:: http
-
- POST /camera/preview/stop HTTP/1.1
- Accept: application/json
-
- :>header Accept: application/json
-
- :'),
- view_func=GPUPreviewAPI.as_view('gpu_preview', microscope=api_microscope))
-
-# Automatically clean up microscope at exit
-def cleanup():
- global api_microscope
- api_microscope.close()
-
-atexit.register(cleanup)
-
-if __name__ == "__main__":
- app.run(host='0.0.0.0', port="5000", threaded=True, debug=True, use_reloader=False)
diff --git a/openflexure_microscope/api/v1/__init__.py b/openflexure_microscope/api/v1/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/openflexure_microscope/api/v1/blueprints/__init__.py b/openflexure_microscope/api/v1/blueprints/__init__.py
new file mode 100644
index 00000000..f987ef03
--- /dev/null
+++ b/openflexure_microscope/api/v1/blueprints/__init__.py
@@ -0,0 +1 @@
+from . import camera, stage, base, plugins
diff --git a/openflexure_microscope/api/v1/blueprints/base.py b/openflexure_microscope/api/v1/blueprints/base.py
new file mode 100644
index 00000000..da4205c7
--- /dev/null
+++ b/openflexure_microscope/api/v1/blueprints/base.py
@@ -0,0 +1,92 @@
+from openflexure_microscope.api.utilities import parse_payload, get_from_payload, gen, get_bool
+from openflexure_microscope.api.v1.views import MicroscopeView
+
+from flask import Response, Blueprint, jsonify
+
+
+class StreamAPI(MicroscopeView):
+
+ def get(self):
+ """
+ Real-time MJPEG stream from the microscope camera
+
+ .. :quickref: State; Camera 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 StateAPI(MicroscopeView):
+
+ def get(self):
+ """
+ JSON representation of the microscope object.
+
+ .. :quickref: State; Microscope state
+
+ **Example request**:
+
+ .. sourcecode:: http
+
+ GET /state/ 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.state)
+
+
+def construct_blueprint(microscope_obj):
+
+ blueprint = Blueprint('base_blueprint', __name__)
+
+ blueprint.add_url_rule(
+ '/stream',
+ view_func=StreamAPI.as_view('stream', microscope=microscope_obj)
+ )
+
+ blueprint.add_url_rule(
+ '/state',
+ view_func=StateAPI.as_view('state', microscope=microscope_obj)
+ )
+
+ return(blueprint)
diff --git a/openflexure_microscope/api/v1/blueprints/camera/__init__.py b/openflexure_microscope/api/v1/blueprints/camera/__init__.py
new file mode 100644
index 00000000..c367c97c
--- /dev/null
+++ b/openflexure_microscope/api/v1/blueprints/camera/__init__.py
@@ -0,0 +1,34 @@
+from openflexure_microscope.api.v1.views import MicroscopeView
+
+from flask import Blueprint
+
+from . import capture, record, preview
+
+
+def construct_blueprint(microscope_obj):
+
+ blueprint = Blueprint('camera_blueprint', __name__)
+
+ # Capture routes
+ blueprint.add_url_rule(
+ '/capture//download/',
+ view_func=capture.DownloadAPI.as_view('capture_download', microscope=microscope_obj))
+
+ blueprint.add_url_rule(
+ '/capture//download',
+ view_func=capture.DownloadRedirectAPI.as_view('capture_download_redirect', microscope=microscope_obj))
+
+ blueprint.add_url_rule(
+ '/capture//',
+ view_func=capture.CaptureAPI.as_view('capture', microscope=microscope_obj))
+
+ blueprint.add_url_rule(
+ '/capture/',
+ view_func=capture.ListAPI.as_view('capture_list', microscope=microscope_obj))
+
+ # Preview routes
+ blueprint.add_url_rule(
+ '/preview/',
+ view_func=preview.GPUPreviewAPI.as_view('gpu_preview', microscope=microscope_obj))
+
+ return(blueprint)
\ No newline at end of file
diff --git a/openflexure_microscope/api/v1/blueprints/camera/capture.py b/openflexure_microscope/api/v1/blueprints/camera/capture.py
new file mode 100644
index 00000000..5fe959cc
--- /dev/null
+++ b/openflexure_microscope/api/v1/blueprints/camera/capture.py
@@ -0,0 +1,289 @@
+from openflexure_microscope.api.utilities import parse_payload, get_from_payload, gen, get_bool
+from openflexure_microscope.api.v1.views import MicroscopeView
+
+from flask import Response, Blueprint, jsonify, request, abort, url_for, redirect, send_file
+
+import logging
+
+
+class ListAPI(MicroscopeView):
+
+ def get(self):
+ """
+ Get list of image captures.
+
+ .. :quickref: Capture collection; 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
+ - **metadata** *(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'))
+
+ if include_unavailable:
+ captures = [image.metadata for image in self.microscope.camera.images]
+ else:
+ captures = [image.metadata for image in self.microscope.camera.images if image.metadata['available']]
+
+ return jsonify(captures)
+
+ def delete(self):
+ """
+ Delete all captures (not yet implemented)
+
+ .. :quickref: Capture collection; Delete all captures
+ """
+ for image in self.microscope.camera.images:
+ image.delete()
+
+ captures = [image.metadata for image in self.microscope.camera.images]
+
+ return jsonify(captures)
+
+ def post(self):
+ """
+ Create a new image capture.
+
+ .. :quickref: Capture collection; New capture
+
+ **Example request**:
+
+ .. sourcecode:: http
+
+ POST /camera/capture HTTP/1.1
+ Accept: application/json
+
+ {
+ "filename": "myfirstcapture",
+ "keep_on_disk": true,
+ "use_video_port": true,
+ "size": {
+ "x": 640,
+ "y": 480
+ }
+ }
+
+ :>header Accept: application/json
+
+ :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
+ - **metadata** *(string)*: api uri to the capture json representation
+
+ :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
+ - **metadata** *(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 metadata
+ capture_metadata = capture_obj.metadata
+
+ # TODO: Tidy up adding URI to metadata
+ # Add API routes to returned metadata
+ uri_dict = {
+ 'uri': {'metadata': '{}/{}'.format(url_for(self), capture_id)}
+ }
+
+ # If available, also add download link
+ if capture_metadata['available']:
+ uri_dict['uri']['download'] = '{}/{}/download/{}'.format(url_for(self), 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):
+ """
+ Modify the metadata of a capture (not yet implemented)
+
+ .. :quickref: Capture; Update capture metadata
+ """
+ return jsonify({"return": capture_id})
+
+
+class DownloadRedirectAPI(MicroscopeView):
+ def get(self, capture_id):
+ """
+ Redirect to download the capture under it's currently set filename.
+ I.e., `/(capture_id)/download` will
+ redirect to `/(capture_id)/download/(filename)`.
+
+ Note: This route may be deprecated in the future. Where at all
+ possible, please include a filename to download as.
+
+ .. :quickref: Capture; Redirect to download
+ """
+ capture_obj = self.microscope.camera.image_from_id(capture_id)
+
+ if not capture_obj or not capture_obj.metadata['available']:
+ return abort(404) # 404 Not Found
+
+ as_attachment = get_bool(request.args.get('as_attachment'))
+ thumbnail = get_bool(request.args.get('thumbnail'))
+
+ return redirect(url_for('.capture_download', capture_id=capture_id, filename=capture_obj.filename, as_attachment=as_attachment, thumbnail=thumbnail), code=307)
+
+
+class DownloadAPI(MicroscopeView):
+ def get(self, capture_id, filename):
+ """
+ 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.
+
+ .. :quickref: Capture; Download capture file
+
+ **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
+ :query as_attachment: return the image as an attachment download e.g. ?as_attachment=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.metadata['available']:
+ return abort(404) # 404 Not Found
+
+ as_attachment = get_bool(request.args.get('as_attachment'))
+ 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, as_attachment=as_attachment, 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',
+ as_attachment=as_attachment,
+ attachment_filename=filename)
\ No newline at end of file
diff --git a/openflexure_microscope/api/v1/blueprints/camera/preview.py b/openflexure_microscope/api/v1/blueprints/camera/preview.py
new file mode 100644
index 00000000..12d59529
--- /dev/null
+++ b/openflexure_microscope/api/v1/blueprints/camera/preview.py
@@ -0,0 +1,38 @@
+from openflexure_microscope.api.utilities import parse_payload, get_from_payload, gen, get_bool
+from openflexure_microscope.api.v1.views import MicroscopeView
+
+from flask import Response, Blueprint, jsonify, request, abort, url_for, redirect, send_file
+
+import logging
+
+
+class GPUPreviewAPI(MicroscopeView):
+
+ def post(self, operation):
+ """
+ Create a new image capture.
+
+ .. :quickref: GPU Preview; Start/stop preview
+
+ **Example requests**:
+
+ .. sourcecode:: http
+
+ POST /camera/preview/start HTTP/1.1
+ Accept: application/json
+
+ .. sourcecode:: http
+
+ POST /camera/preview/stop HTTP/1.1
+ Accept: application/json
+
+ :>header Accept: application/json
+
+ :json int x: x steps
+ :>json int y: y steps
+ :>json int z: z steps
+ """
+ return jsonify(self.microscope.state['stage']['position'])
+
+ def post(self):
+ """
+ Set x, y and z positions of the stage.
+
+ .. :quickref: Position; Update current position
+
+ :reqheader Accept: application/json
+ : travel_limit:
+ # Respond with 400 Bad Request
+ response = {'error': 'Cannot move to absolute position beyond the safeguard limit.'}
+ return jsonify(response), 400
+
+ self.microscope.stage.move_rel(position)
+
+ return jsonify(self.microscope.state['stage']['position'])
+
+
+class StageParamsAPI(MicroscopeView):
+
+ def get(self):
+ """
+ Return current parameters of the stage.
+
+ .. :quickref: Stage params; Get current stage parameters
+
+ **Example request**:
+
+ .. sourcecode:: http
+
+ GET /stage/params HTTP/1.1
+ Accept: application/json
+
+ **Example response**:
+
+ .. sourcecode:: http
+
+ HTTP/1.1 200 OK
+ Vary: Accept
+ Content-Type: application/json
+
+ {
+ "backlash": {
+ "x": 0,
+ "y": 0,
+ "z": 128
+ },
+ }
+
+ """
+
+ return jsonify(self.microscope.state['stage'])
+
+ def post(self):
+ """
+ Set parameters of the stage.
+
+ .. :quickref: Stage params; Set current stage parameters
+
+ :reqheader Accept: application/json
+ :