#!/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, gen, get_bool from openflexure_microscope import Microscope from openflexure_microscope.camera.pi import StreamingCamera from openflexure_stage import OpenFlexureStage 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...") api_microscope.attach( StreamingCamera(), 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): """ Create a generic MethodView with a globally available microscope object passed as an argument. """ self.microscope = microscope MethodView.__init__(self) # State endpoints 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 { "position": { "x": 0, "y": 0, "z": 0 }, "preview_active": false, "record_active": false, "stream_active": true } :>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)) # Positioning endpoints 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 /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['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['position']) app.add_url_rule( uri('/position/'), view_func=PositionAPI.as_view('position', microscope=api_microscope)) # Capture endpoints class CaptureListAPI(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 """ return jsonify({"error": "not yet implemented"}) def post(self): """ Create a new image capture. .. :quickref: Capture collection; New capture **Example request**: .. sourcecode:: http POST /position/ 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('/capture//'), view_func=CaptureAPI.as_view('capture', microscope=api_microscope)) class CaptureDownloadAPI(MicroscopeView): def get(self, capture_id, filename): """ Return image data for a capture. If no (filename) is specified, the request will redirect to download the capture under it's currently set filename. If (filename) is specified, the capture data will be returned as an image file with the requested filename. .. :quickref: Capture; Download capture file **Example request**: .. sourcecode:: http GET /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 """ print(capture_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) # General download view download_view = CaptureDownloadAPI.as_view('capture_download', microscope=api_microscope) # Full route, including filename app.add_url_rule( uri('/capture//download/'), view_func=download_view) # Route for shortcut where no filename is specified app.add_url_rule( uri('/capture//download/'), defaults={'filename': None}, view_func=download_view) if __name__ == "__main__": app.run(host='0.0.0.0', port="5000", threaded=True, debug=True, use_reloader=False)