#!/usr/bin/env python from flask import ( Flask, jsonify, send_file) from serial import SerialException from datetime import datetime from flask_cors import CORS from openflexure_microscope.api.exceptions import JSONExceptionHandler from openflexure_microscope.api.utilities import list_routes from openflexure_microscope import Microscope from openflexure_microscope.camera.pi import StreamingCamera from openflexure_microscope.stage.sanga import SangaStage from openflexure_microscope.stage.mock import MockStage from openflexure_microscope.camera.capture import build_captures_from_exif from openflexure_microscope.config import USER_CONFIG_DIR from openflexure_microscope.api.v1 import blueprints import atexit import logging import sys import os # Handle logging is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "") DEFAULT_LOGFILE = os.path.join(USER_CONFIG_DIR, 'openflexure_microscope.log') if (__name__ == "__main__") or (not is_gunicorn): # If imported, but not by gunicorn print("Letting sys handle logs") logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) else: # Direct standard Python logging to file and console root = logging.getLogger() error_formatter = logging.Formatter("[%(asctime)s] [%(threadName)s] [%(levelname)s] %(message)s") rotating_logfile = logging.handlers.RotatingFileHandler( DEFAULT_LOGFILE, maxBytes=1000000, backupCount=7 ) error_handlers = [ rotating_logfile, logging.StreamHandler() ] for handler in error_handlers: handler.setFormatter(error_formatter) root.addHandler(handler) root.setLevel(logging.getLogger("gunicorn.error").level) # Create a dummy microscope object, with no hardware attachments api_microscope = Microscope() # Rebuild the capture list # TODO: Offload to a thread? stored_image_list = build_captures_from_exif() # Generate API URI based on version from filename def uri(suffix, api_version, base=None): if not base: base = "/api/{}".format(api_version) return_uri = base + suffix logging.debug("Created app route: {}".format(return_uri)) return return_uri # Create flask app app = Flask(__name__) app.url_map.strict_slashes = False CORS(app, resources=r'/api/*') # Make errors more API friendly handler = JSONExceptionHandler(app) # 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, stored_image_list logging.debug("First request made. Populating microscope with hardware...") logging.debug("Creating camera object...") # TODO: Try except finally, like with stage api_camera = StreamingCamera() logging.debug("Creating stage object...") # TODO: Tidy this up. api_stage may be referenced before assignment. Use some form of Maybe monad? try: api_stage = SangaStage() except (SerialException, OSError) as e: logging.error(e) logging.warning("No valid stage hardware found. Falling back to mock stage!") api_stage = MockStage() finally: logging.debug("Attaching devices to microscope...") api_microscope.attach( api_camera, api_stage ) logging.debug("Restoring captures...") if stored_image_list: api_microscope.camera.images = stored_image_list logging.debug("Microscope successfully attached!") # WEBAPP ROUTES # API ROUTES # Base routes base_blueprint = blueprints.base.construct_blueprint(api_microscope) app.register_blueprint(base_blueprint, url_prefix=uri('', 'v1')) # Stage routes stage_blueprint = blueprints.stage.construct_blueprint(api_microscope) app.register_blueprint(stage_blueprint, url_prefix=uri('/stage', 'v1')) # Camera routes camera_blueprint = blueprints.camera.construct_blueprint(api_microscope) app.register_blueprint(camera_blueprint, url_prefix=uri('/camera', 'v1')) # Plugin routes plugin_blueprint = blueprints.plugins.construct_blueprint(api_microscope) app.register_blueprint(plugin_blueprint, url_prefix=uri('/plugin', 'v1')) # Task routes task_blueprint = blueprints.task.construct_blueprint(api_microscope) app.register_blueprint(task_blueprint, url_prefix=uri('/task', 'v1')) @app.route('/routes') def routes(): """ List of all connected API routes """ return jsonify(list_routes(app)) @app.route('/log') def err_log(): """ Most recent 1mb of log output """ timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") return send_file( DEFAULT_LOGFILE, as_attachment=True, attachment_filename='openflexure_microscope_{}.log'.format(timestamp) ) # Automatically clean up microscope at exit def cleanup(): global api_microscope logging.debug("App teardown started...") # Save config if api_microscope.rc: api_microscope.rc.save(backup=True) # Close down the microscope api_microscope.close() logging.debug("App teardown complete.") atexit.register(cleanup) if __name__ == "__main__": app.run(host='0.0.0.0', port="5000", threaded=True, debug=True, use_reloader=False)