From f106c2ecba5223c8e5e2cb475ea33fcd547da048 Mon Sep 17 00:00:00 2001 From: Joel Collins Date: Thu, 15 Nov 2018 17:18:04 +0000 Subject: [PATCH] Started testing pluggable views --- openflexure_microscope/api/app.py | 187 ++++++++++++++++++ openflexure_microscope/api/doc/Makefile | 20 ++ openflexure_microscope/api/doc/make.bat | 36 ++++ .../api/doc/requirements.txt | 3 + openflexure_microscope/api/doc/source/conf.py | 172 ++++++++++++++++ .../api/doc/source/index.rst | 36 ++++ .../api/templates/index.html | 9 + openflexure_microscope/api/v1.py | 48 ++++- openflexure_microscope/microscope.py | 2 + 9 files changed, 503 insertions(+), 10 deletions(-) create mode 100644 openflexure_microscope/api/app.py create mode 100644 openflexure_microscope/api/doc/Makefile create mode 100644 openflexure_microscope/api/doc/make.bat create mode 100644 openflexure_microscope/api/doc/requirements.txt create mode 100644 openflexure_microscope/api/doc/source/conf.py create mode 100644 openflexure_microscope/api/doc/source/index.rst create mode 100644 openflexure_microscope/api/templates/index.html diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py new file mode 100644 index 00000000..4a109823 --- /dev/null +++ b/openflexure_microscope/api/app.py @@ -0,0 +1,187 @@ +#!/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.camera.pi import StreamingCamera + +app = Flask(__name__) +cam = StreamingCamera() + + +def parse_payload(request): + """Convert request to JSON. Will eventually handle error-checking.""" + # TODO: Handle invalid JSON payloads + state = request.get_json() + return state + + +def gen(camera): + """Video streaming generator function.""" + while True: + # the obtained frame is a jpeg + frame = camera.get_frame() + + yield (b'--frame\r\n' + b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n') + + +@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. + """ + 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/doc/Makefile b/openflexure_microscope/api/doc/Makefile new file mode 100644 index 00000000..8663bf75 --- /dev/null +++ b/openflexure_microscope/api/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = openflexure_microscope_api +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/openflexure_microscope/api/doc/make.bat b/openflexure_microscope/api/doc/make.bat new file mode 100644 index 00000000..5c929a79 --- /dev/null +++ b/openflexure_microscope/api/doc/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build +set SPHINXPROJ=openflexure_microscope_api + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/openflexure_microscope/api/doc/requirements.txt b/openflexure_microscope/api/doc/requirements.txt new file mode 100644 index 00000000..45ac0217 --- /dev/null +++ b/openflexure_microscope/api/doc/requirements.txt @@ -0,0 +1,3 @@ +Sphinx +sphinxcontrib-httpdomain +sphinx_rtd_theme \ No newline at end of file diff --git a/openflexure_microscope/api/doc/source/conf.py b/openflexure_microscope/api/doc/source/conf.py new file mode 100644 index 00000000..2d3c8f87 --- /dev/null +++ b/openflexure_microscope/api/doc/source/conf.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'openflexure_microscope_api' +copyright = '2018, Joel Collins' +author = 'Joel Collins' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '0.0.1' + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.ifconfig', + 'sphinxcontrib.httpdomain', + 'sphinxcontrib.autohttp.flask', + 'sphinxcontrib.autohttp.flaskqref', +] + +# Override ordering +autodoc_member_order = 'bysource' + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'openflexure_microscope_apidoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'openflexure_microscope_api.tex', 'openflexure\\_microscope\\_api Documentation', + 'Joel Collins', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'openflexure_microscope_api', 'openflexure_microscope_api Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'openflexure_microscope_api', 'openflexure_microscope_api Documentation', + author, 'openflexure_microscope_api', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Extension configuration ------------------------------------------------- + +# -- Options for intersphinx extension --------------------------------------- + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'https://docs.python.org/': None} \ No newline at end of file diff --git a/openflexure_microscope/api/doc/source/index.rst b/openflexure_microscope/api/doc/source/index.rst new file mode 100644 index 00000000..2fd5d45a --- /dev/null +++ b/openflexure_microscope/api/doc/source/index.rst @@ -0,0 +1,36 @@ +.. openflexure_microscope_api documentation master file, created by + sphinx-quickstart on Thu Nov 15 11:08:18 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +OpenFlexure Microscope API +====================================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + + + +API Documentation +================= + +Summary +------- +.. qrefflask:: openflexure_microscope.api.v1:app + :undoc-static: + +API Details +----------- + +.. autoflask:: openflexure_microscope.api.v1:app + :undoc-static: \ No newline at end of file diff --git a/openflexure_microscope/api/templates/index.html b/openflexure_microscope/api/templates/index.html new file mode 100644 index 00000000..d1f67f86 --- /dev/null +++ b/openflexure_microscope/api/templates/index.html @@ -0,0 +1,9 @@ + + + Video Streaming Demonstration + + +

Video Streaming Demonstration

+ + + diff --git a/openflexure_microscope/api/v1.py b/openflexure_microscope/api/v1.py index 53b9da62..f0e21cc2 100644 --- a/openflexure_microscope/api/v1.py +++ b/openflexure_microscope/api/v1.py @@ -17,6 +17,7 @@ from flask import ( redirect, request, jsonify, send_file, abort, make_response) +from flask.views import MethodView import numpy as np @@ -29,21 +30,22 @@ from openflexure_stage import OpenFlexureStage import logging, sys logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) +logging.info("Creating StreamingCamera") +cam = StreamingCamera() +logging.info("Creating stage") +stage = OpenFlexureStage("/dev/ttyUSB0") # Create the microscope object globally (common to all spawned server threads) +logging.info("Creating microscope") microscope = Microscope( - StreamingCamera(), - OpenFlexureStage("/dev/ttyUSB0") + cam, + stage ) +logging.info("Creating app") # Create flask app app = Flask(__name__) -# Make errors more API friendly -@app.errorhandler(404) -def not_found(error): - return make_response(jsonify({'error': 'Not found'}), 404) - # Some useful functions def uri(suffix, base='/api/v1'): return base + suffix @@ -57,6 +59,30 @@ def index(): 'index_v1.html' ) + +##### TESTING CLASS STUFFS ###### +class MicroscopeView(MethodView): + + def __init__(self, microscope_obj): + self.microscope_obj = microscope_obj + + +class TestAPI(MicroscopeView): + + def get(self): + """ + Get method for my cool new MethodView + """ + return jsonify(self.microscope_obj.state) + + def post(self): + """ + Post method for my cool new MethodView + """ + return jsonify(request.get_json()) + +app.add_url_rule('/test', view_func=TestAPI.as_view('test', microscope_obj=microscope)) + # Define API routes # Basic routes @@ -84,7 +110,9 @@ def state(): @app.route(uri('/position'), methods=['GET', 'POST', 'PUT']) def position(): - """Set and get the microscope stage position""" + """ + Set and get the microscope stage position + """ global microscope if request.method == 'POST' or request.method == 'PUT': @@ -249,5 +277,5 @@ def thumb_capture(capture_uuid): capture_obj.thumbnail, mimetype='image/jpeg') -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/microscope.py b/openflexure_microscope/microscope.py index 355f23ea..672f1670 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -8,7 +8,9 @@ from .camera.pi import StreamingCamera class Microscope(object): def __init__(self, camera: StreamingCamera, stage: OpenFlexureStage): """Create the microscope object. The camera and stage should already be initialised.""" + print("Assigning camera") self.camera = camera + print("Assigning stage") self.stage = stage self.stage.backlash = np.zeros(3, dtype=np.int)