Improved flake8 results

This commit is contained in:
Joel Collins 2019-05-24 16:32:20 +01:00
parent fb606b189d
commit 3b9ef670fe
24 changed files with 256 additions and 126 deletions

View file

@ -1,4 +1,4 @@
__all__ = ['Microscope', 'config', 'task', 'lock'] __all__ = ['Microscope', 'config', 'task', 'lock', 'utilities']
__version__ = "0.1.0" __version__ = "0.1.0"
from .microscope import Microscope from .microscope import Microscope

View file

@ -1 +1,3 @@
__all__ = ['utilities']
from . import utilities from . import utilities

View file

@ -1,9 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
from flask import ( from flask import (
Flask, render_template, jsonify, send_file) Flask, jsonify, send_file)
from flask.logging import default_handler
from serial import SerialException from serial import SerialException
from datetime import datetime from datetime import datetime
@ -21,9 +20,12 @@ from openflexure_microscope.camera.capture import build_captures_from_exif
from openflexure_microscope.config import USER_CONFIG_DIR from openflexure_microscope.config import USER_CONFIG_DIR
from openflexure_microscope.api.v1 import blueprints
import atexit import atexit
import logging import logging
import sys, os import sys
import os
# Handle logging # Handle logging
is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "") is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
@ -63,6 +65,7 @@ api_microscope = Microscope(None, None)
# TODO: Offload to a thread? # TODO: Offload to a thread?
stored_image_list = build_captures_from_exif() stored_image_list = build_captures_from_exif()
# Generate API URI based on version from filename # Generate API URI based on version from filename
def uri(suffix, api_version, base=None): def uri(suffix, api_version, base=None):
if not base: if not base:
@ -91,7 +94,7 @@ def attach_microscope():
logging.debug("Creating camera object...") logging.debug("Creating camera object...")
api_camera = StreamingCamera(config=api_microscope.rc.read()) api_camera = StreamingCamera(config=api_microscope.rc.read())
logging.debug("Creating stage object...") logging.debug("Creating stage object...")
try: try:
api_stage = SangaStage() api_stage = SangaStage()
@ -113,10 +116,9 @@ def attach_microscope():
logging.debug("Microscope successfully attached!") logging.debug("Microscope successfully attached!")
##### WEBAPP ROUTES ###### # WEBAPP ROUTES
##### API ROUTES ###### # API ROUTES
from openflexure_microscope.api.v1 import blueprints
# Base routes # Base routes
base_blueprint = blueprints.base.construct_blueprint(api_microscope) base_blueprint = blueprints.base.construct_blueprint(api_microscope)
@ -138,6 +140,7 @@ app.register_blueprint(plugin_blueprint, url_prefix=uri('/plugin', 'v1'))
task_blueprint = blueprints.task.construct_blueprint(api_microscope) 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'))
@app.route('/routes') @app.route('/routes')
def routes(): def routes():
""" """
@ -145,13 +148,19 @@ def routes():
""" """
return jsonify(list_routes(app)) return jsonify(list_routes(app))
@app.route('/log') @app.route('/log')
def err_log(): def err_log():
""" """
Most recent 1mb of log output Most recent 1mb of log output
""" """
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") 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)) return send_file(
DEFAULT_LOGFILE,
as_attachment=True,
attachment_filename='openflexure_microscope_{}.log'.format(timestamp)
)
# Automatically clean up microscope at exit # Automatically clean up microscope at exit
def cleanup(): def cleanup():
@ -166,6 +175,7 @@ def cleanup():
api_microscope.close() api_microscope.close()
logging.debug("App teardown complete.") logging.debug("App teardown complete.")
atexit.register(cleanup) atexit.register(cleanup)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,8 +1,8 @@
import pprint
import logging import logging
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from flask import url_for from flask import url_for
class JsonPayload: class JsonPayload:
def __init__(self, request): def __init__(self, request):
""" """
@ -32,7 +32,8 @@ class JsonPayload:
Args: Args:
key (str): JSON key to look for key (str): JSON key to look for
default: Value to return if no matching key-value pair is found. default: Value to return if no matching key-value pair is found.
convert: Converter function. By passing a type, the value will be converted to that type. **Use with caution!** convert: Converter function. By passing a type, the value will be converted to that type.
**Use with caution!**
""" """
# If no JSON payload exists, make an empty dictionary # If no JSON payload exists, make an empty dictionary
if not self.json: if not self.json:

View file

@ -1,4 +1,4 @@
from openflexure_microscope.api.utilities import gen, get_bool, JsonPayload from openflexure_microscope.api.utilities import get_bool, JsonPayload
from openflexure_microscope.api.v1.views import MicroscopeView from openflexure_microscope.api.v1.views import MicroscopeView
from openflexure_microscope.utilities import filter_dict from openflexure_microscope.utilities import filter_dict
@ -201,7 +201,10 @@ class CaptureAPI(MicroscopeView):
# If available, also add download link # If available, also add download link
if capture_metadata['available']: if capture_metadata['available']:
uri_dict['uri']['download'] = '{}download/{}'.format(url_for('.capture', capture_id=capture_obj.id), capture_obj.filename) uri_dict['uri']['download'] = '{}download/{}'.format(
url_for('.capture', capture_id=capture_obj.id),
capture_obj.filename
)
capture_metadata.update(uri_dict) capture_metadata.update(uri_dict)
@ -297,7 +300,12 @@ class DownloadRedirectAPI(MicroscopeView):
thumbnail = get_bool(request.args.get('thumbnail')) thumbnail = get_bool(request.args.get('thumbnail'))
return redirect(url_for('.capture_download', capture_id=capture_id, filename=capture_obj.filename, thumbnail=thumbnail), code=307) return redirect(url_for(
'.capture_download',
capture_id=capture_id,
filename=capture_obj.filename,
thumbnail=thumbnail
), code=307)
class DownloadAPI(MicroscopeView): class DownloadAPI(MicroscopeView):
@ -312,7 +320,12 @@ class DownloadAPI(MicroscopeView):
# If no filename is specified, redirect to the capture's currently set filename # If no filename is specified, redirect to the capture's currently set filename
if not filename: if not filename:
return redirect(url_for('capture_download', capture_id=capture_id, filename=capture_obj.filename, thumbnail=thumbnail), code=307) return redirect(url_for(
'capture_download',
capture_id=capture_id,
filename=capture_obj.filename,
thumbnail=thumbnail
), code=307)
# Download the image data using the requested filename # Download the image data using the requested filename
if thumbnail: if thumbnail:
@ -360,7 +373,11 @@ class MetadataRedirectAPI(MicroscopeView):
if not capture_obj or not capture_obj.state['available']: if not capture_obj or not capture_obj.state['available']:
return abort(404) # 404 Not Found return abort(404) # 404 Not Found
return redirect(url_for('.metadata_download', capture_id=capture_id, filename=capture_obj.metadataname), code=307) return redirect(url_for(
'.metadata_download',
capture_id=capture_id,
filename=capture_obj.metadataname
), code=307)
class MetadataAPI(MicroscopeView): class MetadataAPI(MicroscopeView):
@ -373,7 +390,11 @@ class MetadataAPI(MicroscopeView):
# If no filename is specified, redirect to the capture's currently set filename # If no filename is specified, redirect to the capture's currently set filename
if not filename: if not filename:
return redirect(url_for('capture_download', capture_id=capture_id, filename=capture_obj.metadataname), code=307) return redirect(url_for(
'capture_download',
capture_id=capture_id,
filename=capture_obj.metadataname
), code=307)
# Download the metadata using the requested filename # Download the metadata using the requested filename
data = capture_obj.yaml data = capture_obj.yaml

View file

@ -35,7 +35,7 @@ class ZoomAPI(MicroscopeView):
Accept: application/json Accept: application/json
{ {
"zoom_value": 2.5, "zoom_value": 2.5,
} }
:>header Accept: application/json :>header Accept: application/json
@ -83,8 +83,8 @@ class OverlayAPI(MicroscopeView):
Accept: application/json Accept: application/json
{ {
"text": "2019/01/15 14:48", "text": "2019/01/15 14:48",
"size": 50 "size": 50
} }
:>header Accept: application/json :>header Accept: application/json
@ -92,7 +92,7 @@ class OverlayAPI(MicroscopeView):
:<header Content-Type: application/json :<header Content-Type: application/json
:status 200: preview started/stopped :status 200: preview started/stopped
""" """
payload = JsonPayload(request) payload = JsonPayload(request)
text = payload.param('text', default="", convert=str) text = payload.param('text', default="", convert=str)
size = payload.param('size', default=50, convert=int) size = payload.param('size', default=50, convert=int)

View file

@ -4,6 +4,7 @@ from openflexure_microscope.api.v1.views import MicroscopeView
from flask import jsonify, request from flask import jsonify, request
import logging import logging
class GPUPreviewAPI(MicroscopeView): class GPUPreviewAPI(MicroscopeView):
def post(self, operation): def post(self, operation):
@ -45,8 +46,8 @@ class GPUPreviewAPI(MicroscopeView):
fullscreen = True fullscreen = True
window = None window = None
else: else:
fullscreen = False fullscreen = False
window = [int(w) for w in window] window = [int(w) for w in window]
self.microscope.camera.start_preview(fullscreen=fullscreen, window=window) self.microscope.camera.start_preview(fullscreen=fullscreen, window=window)
elif operation == "stop": elif operation == "stop":

View file

@ -1,4 +1,4 @@
from openflexure_microscope.api.utilities import gen, JsonPayload from openflexure_microscope.api.utilities import JsonPayload
from openflexure_microscope.api.v1.views import MicroscopeView from openflexure_microscope.api.v1.views import MicroscopeView
from openflexure_microscope.utilities import axes_to_array, filter_dict from openflexure_microscope.utilities import axes_to_array, filter_dict

View file

@ -121,7 +121,7 @@ class TaskAPI(MicroscopeView):
""" """
success = self.microscope.task.delete(task_id) success = self.microscope.task.delete(task_id)
if success: if success:
data = { data = {
'status': 'success', 'status': 'success',

View file

@ -1 +1,3 @@
__all__ = ['pi', 'base']
from . import pi, base from . import pi, base

View file

@ -3,7 +3,6 @@ import time
import os import os
import threading import threading
import datetime import datetime
import yaml
import logging import logging
try: try:
@ -15,7 +14,6 @@ except ImportError:
from _thread import get_ident from _thread import get_ident
from .capture import CaptureObject, BASE_CAPTURE_PATH, TEMP_CAPTURE_PATH from .capture import CaptureObject, BASE_CAPTURE_PATH, TEMP_CAPTURE_PATH
from openflexure_microscope.config import USER_CONFIG_DIR
from openflexure_microscope.utilities import entry_by_id from openflexure_microscope.utilities import entry_by_id
from openflexure_microscope.lock import StrictLock from openflexure_microscope.lock import StrictLock
@ -54,7 +52,7 @@ class CameraEvent(object):
def __init__(self): def __init__(self):
self.events = {} self.events = {}
def wait(self, timeout: int=5): def wait(self, timeout: int = 5):
"""Wait for the next frame (invoked from each client's thread).""" """Wait for the next frame (invoked from each client's thread)."""
ident = get_ident() ident = get_ident()
if ident not in self.events: if ident not in self.events:
@ -92,32 +90,42 @@ class CameraEvent(object):
class BaseCamera(object): class BaseCamera(object):
""" """
Base implementation of StreamingCamera. Base implementation of StreamingCamera.
Attributes:
thread: Background thread reading frames from camera
camera: Camera object
lock (:py:class:`openflexure_microscope.lock.StrictLock`): Strict lock controlling thread
access to stage hardware
frame (bytes): Current frame is stored here by background thread
last_access (time): Time of last client access to the camera
state (dict): Dictionary for capture state
config (dict): Dictionary of base camera config
paths (dict): Dictionary of capture paths
images (list): List of image capture objects
videos (list): List of video capture objects
""" """
def __init__(self): def __init__(self):
self.thread = None #: Background thread reading frames from camera self.thread = None
self.camera = None #: Camera object self.camera = None
self.lock = StrictLock(timeout=1) #: :py:class:`openflexure_microscope.lock.StrictLock`: Strict lock controlling thread access to camera hardware self.lock = StrictLock(timeout=1)
self.frame = None #: bytes: Current frame is stored here by background thread self.frame = None
self.last_access = 0 #: time: Time of last client access to the camera self.last_access = 0
self.event = CameraEvent() self.event = CameraEvent()
self.stream_timeout = 20 #: int: Number of inactive seconds before timing out the stream self.state = {}
self.stream_timeout_enabled = False #: bool: Enable or disable timing out the stream self.config = {}
self.state = {} #: dict: Dictionary for capture state
self.config = {} #: dict: Dictionary of base camera config
self.paths = { self.paths = {
'image': BASE_CAPTURE_PATH, 'image': BASE_CAPTURE_PATH,
'video': BASE_CAPTURE_PATH, 'video': BASE_CAPTURE_PATH,
'image_tmp': TEMP_CAPTURE_PATH, 'image_tmp': TEMP_CAPTURE_PATH,
'video_tpm': TEMP_CAPTURE_PATH 'video_tpm': TEMP_CAPTURE_PATH
} #: dict: Dictionary of capture paths }
# Capture data # Capture data
self.images = [] #: list: List of image capture objects self.images = []
self.videos = [] #: list: List of video recording objects self.videos = []
def apply_config(self, config): def apply_config(self, config):
"""Update settings from a config dictionary""" """Update settings from a config dictionary"""
@ -164,7 +172,6 @@ class BaseCamera(object):
self.last_access = time.time() self.last_access = time.time()
self.stop = False self.stop = False
#if self.thread is None:
if not self.state['stream_active']: if not self.state['stream_active']:
# start background frame thread # start background frame thread
self.thread = threading.Thread(target=self._thread) self.thread = threading.Thread(target=self._thread)
@ -185,13 +192,11 @@ class BaseCamera(object):
logging.debug("Stopping worker thread") logging.debug("Stopping worker thread")
timeout_time = time.time() + timeout timeout_time = time.time() + timeout
#if self.thread:
if self.state['stream_active']: if self.state['stream_active']:
self.stop = True self.stop = True
self.thread.join() # Wait for stream thread to exit self.thread.join() # Wait for stream thread to exit
logging.debug("Waiting for stream thread to exit.") logging.debug("Waiting for stream thread to exit.")
#while self.thread:
while self.state['stream_active']: while self.state['stream_active']:
if time.time() > timeout_time: if time.time() > timeout_time:
logging.debug("Timeout waiting for worker thread close.") logging.debug("Timeout waiting for worker thread close.")
@ -255,7 +260,8 @@ class BaseCamera(object):
Args: Args:
write_to_file (bool): Should the StreamObject write to a file, or an in-memory byte stream. write_to_file (bool): Should the StreamObject write to a file, or an in-memory byte stream.
temporary (bool): Should the data be deleted after session ends. Creating the capture with a content manager sets this to true. temporary (bool): Should the data be deleted after session ends.
Creating the capture with a content manager sets this to true.
filename (str): Name of the stored file. Defaults to timestamp. filename (str): Name of the stored file. Defaults to timestamp.
fmt (str): Format of the capture. fmt (str): Format of the capture.
""" """
@ -298,7 +304,8 @@ class BaseCamera(object):
Args: Args:
write_to_file (bool): Should the StreamObject write to a file, or an in-memory byte stream. write_to_file (bool): Should the StreamObject write to a file, or an in-memory byte stream.
temporary (bool): Should the data be deleted after session ends. Creating the capture with a content manager sets this to true. temporary (bool): Should the data be deleted after session ends.
Creating the capture with a content manager sets this to true.
filename (str): Name of the stored file. Defaults to timestamp. filename (str): Name of the stored file. Defaults to timestamp.
fmt (str): Format of the capture. fmt (str): Format of the capture.
""" """
@ -363,6 +370,3 @@ class BaseCamera(object):
logging.debug("BaseCamera worker thread exiting...") logging.debug("BaseCamera worker thread exiting...")
# Set stream_activate state # Set stream_activate state
self.state['stream_active'] = False self.state['stream_active'] = False
# Reset thread
#self.thread = None
logging.debug("Stream thread is None")

View file

@ -11,12 +11,18 @@ import atexit
from openflexure_microscope.camera import piexif from openflexure_microscope.camera import piexif
"""
Attributes:
BASE_CAPTURE_PATH (str): Base path to store all captures
TEMP_CAPTURE_PATH (str): Base path to store all temporary captures (automatically emptied)
"""
PIL_FORMATS = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF'] PIL_FORMATS = ['JPG', 'JPEG', 'PNG', 'TIF', 'TIFF']
EXIF_FORMATS = ['JPG', 'JPEG', 'TIF', 'TIFF'] EXIF_FORMATS = ['JPG', 'JPEG', 'TIF', 'TIFF']
THUMBNAIL_SIZE = (200, 150) THUMBNAIL_SIZE = (200, 150)
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs') #: str: Base path to store all captures BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs')
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp') #: str: Base path to store all temporary captures (automatically emptied) TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp')
# TODO: Move these methods to a camera utilities module? # TODO: Move these methods to a camera utilities module?
@ -107,7 +113,20 @@ class CaptureObject(object):
""" """
StreamObject used to store and process capture data, and metadata. StreamObject used to store and process capture data, and metadata.
Note: Captures cannot be stored in a lower-level directory than BASE_CAPTURE_PATH. Attributes:
timestring (str): Timestring of capture creation time
temporary (bool): Mark the capture as temporary, to be deleted as the server closes,
or as resources are required
_metadata (dict): Dictionary of custom metadata to be included in metadata file
tags (list): List of tags. Essentially just as extra custom metadata field, but useful for quick organisation
bytestream (:py:class:`io.BytesIO`): Byte bytestream that data will be written to
filefolder (str): Folder in which the capture file will be stored
filename (str): Full name of the capture file
basename (str): Filename of the capture, without a file extension
format (str): Format of the capture data
Notes:
Captures cannot be stored in a lower-level directory than BASE_CAPTURE_PATH.
""" """
def __init__( def __init__(
@ -120,10 +139,10 @@ class CaptureObject(object):
# Store a nice ID # Store a nice ID
self.id = uuid.uuid4().hex #: str: Unique capture ID self.id = uuid.uuid4().hex #: str: Unique capture ID
logging.debug("Created StreamObject {}".format(self.id)) logging.debug("Created StreamObject {}".format(self.id))
self.timestring = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") #: str: Timestring of capture creation time self.timestring = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
# Keep on disk after close by default # Keep on disk after close by default
self.temporary = temporary #: bool: Mark the capture as temporary, to be deleted as the server closes, or as resources are required self.temporary = temporary
# Create file name. Default to UUID # Create file name. Default to UUID
if not filepath: if not filepath:
@ -133,13 +152,13 @@ class CaptureObject(object):
self.split_file_path(self.file) self.split_file_path(self.file)
# Dictionary for storing custom metadata # Dictionary for storing custom metadata
self._metadata = {} #: dict: Dictionary of custom metadata to be included in metadata file self._metadata = {}
# List for storing tags # List for storing tags
self.tags = [] #: list: List of tags. Essentially just as extra custom metadata field, but useful for quick organisation self.tags = []
# Byte bytestream properties # Byte bytestream properties
self.bytestream = io.BytesIO() # Byte bytestream that data will be written to self.bytestream = io.BytesIO()
# Set default write target # Set default write target
if not write_to_file: if not write_to_file:
@ -182,12 +201,14 @@ class CaptureObject(object):
def split_file_path(self, filepath): def split_file_path(self, filepath):
""" """
Take a full file path, and split it into separated class properties. Take a full file path, and split it into separated class properties.
Args: Args:
filepath (str): String of the full file path, including file format extension filepath (str): String of the full file path, including file format extension
""" """
self.filefolder, self.filename = os.path.split(filepath) # Split the full file path into a folder and a filename # Split the full file path into a folder and a filename
self.basename = os.path.splitext(self.filename)[0] # Split the filename out from it's file extension self.filefolder, self.filename = os.path.split(filepath)
# Split the filename out from it's file extension
self.basename = os.path.splitext(self.filename)[0]
self.format = self.filename.split('.')[-1] self.format = self.filename.split('.')[-1]
# Create folder and file # Create folder and file

View file

@ -242,7 +242,7 @@ class StreamingCamera(BaseCamera):
logging.info("Starting the GPU preview") logging.info("Starting the GPU preview")
# TODO: Commented out as I honestly can't remember why this was here. May need to put back? # TODO: Commented out as I honestly can't remember why this was here. May need to put back?
#self.start_stream_recording() # self.start_stream_recording()
try: try:
if not self.camera.preview: if not self.camera.preview:
@ -365,7 +365,8 @@ class StreamingCamera(BaseCamera):
Args: Args:
splitter_port (int): Splitter port to start recording on splitter_port (int): Splitter port to start recording on
resolution ((int, int)): Resolution to set the camera to, before starting recording. Defaults to `self.config['stream_resolution']`. resolution ((int, int)): Resolution to set the camera to, before starting recording.
Defaults to `self.config['stream_resolution']`.
""" """
with self.lock: with self.lock:
# If stream object was destroyed # If stream object was destroyed
@ -392,7 +393,7 @@ class StreamingCamera(BaseCamera):
self.stream, self.stream,
format='mjpeg', format='mjpeg',
quality=self.config['jpeg_quality'], quality=self.config['jpeg_quality'],
bitrate=-1, # RWB: disable bitrate control bitrate=-1, # RWB: disable bitrate control
# (bitrate control makes JPEG size less good as a focus # (bitrate control makes JPEG size less good as a focus
# metric) # metric)
splitter_port=splitter_port) splitter_port=splitter_port)

View file

@ -19,11 +19,16 @@ def set_gain(camera, gain, value):
""" """
if gain not in [MMAL_PARAMETER_ANALOG_GAIN, MMAL_PARAMETER_DIGITAL_GAIN]: if gain not in [MMAL_PARAMETER_ANALOG_GAIN, MMAL_PARAMETER_DIGITAL_GAIN]:
raise ValueError("The gain parameter was not valid") raise ValueError("The gain parameter was not valid")
ret = mmal.mmal_port_parameter_set_rational(camera._camera.control._port, ret = mmal.mmal_port_parameter_set_rational(
gain, camera._camera.control._port,
to_rational(value)) gain,
to_rational(value)
)
if ret == 4: if ret == 4:
raise exc.PiCameraMMALError(ret, "Are you running the latest version of the userland libraries? Gain setting was introduced in late 2017.") raise exc.PiCameraMMALError(
ret,
"Are you running the latest version of the userland libraries? Gain setting was introduced in late 2017."
)
elif ret != 0: elif ret != 0:
raise exc.PiCameraMMALError(ret) raise exc.PiCameraMMALError(ret)
@ -40,7 +45,7 @@ def set_digital_gain(camera, value):
if __name__ == "__main__": if __name__ == "__main__":
with picamera.PiCamera() as cam: with picamera.PiCamera() as cam:
cam.start_preview(fullscreen=False, window=(0,50,640,480)) cam.start_preview(fullscreen=False, window=(0, 50, 640, 480))
time.sleep(2) time.sleep(2)
# fix the auto white balance gains at their current values # fix the auto white balance gains at their current values
@ -58,10 +63,10 @@ if __name__ == "__main__":
logging.info("Attempting to set digital gain to 1") logging.info("Attempting to set digital gain to 1")
set_digital_gain(cam, 1) set_digital_gain(cam, 1)
# The old code is left in here in case it is a useful example... # The old code is left in here in case it is a useful example...
#ret = mmal.mmal_port_parameter_set_rational(cam._camera.control._port, # ret = mmal.mmal_port_parameter_set_rational(cam._camera.control._port,
# MMAL_PARAMETER_DIGITAL_GAIN, # MMAL_PARAMETER_DIGITAL_GAIN,
# to_rational(1)) # to_rational(1))
#print("Return code: {}".format(ret)) # print("Return code: {}".format(ret))
try: try:
while True: while True:
@ -69,5 +74,3 @@ if __name__ == "__main__":
time.sleep(1) time.sleep(1)
except KeyboardInterrupt: except KeyboardInterrupt:
logging.info("Stopping...") logging.info("Stopping...")

View file

@ -7,13 +7,21 @@ import copy
from fractions import Fraction from fractions import Fraction
from collections import abc from collections import abc
"""
Attributes:
USER_CONFIG_DIR (str): Default path of the user-config directory, containing runtime-config and
calibration files. Obtained from ``os.path.join(os.path.expanduser("~"), ".openflexure")``.
USER_CONFIG_FILE (str): Default path of the user microscope_settings.yaml runtime-config file.
Obtained from ``os.path.join(USER_CONFIG_DIR, "microscope_settings.yaml")``
"""
# HANDLE THE DEFAULT CONFIGURATION FILE # HANDLE THE DEFAULT CONFIGURATION FILE
HERE = os.path.abspath(os.path.dirname(__file__)) HERE = os.path.abspath(os.path.dirname(__file__))
DEFAULT_CONFIG_PATH = os.path.join(HERE, 'microscope_settings.default.yaml') DEFAULT_CONFIG_PATH = os.path.join(HERE, 'microscope_settings.default.yaml')
USER_CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".openflexure") #: str: Default path of the user-config directory, containing runtime-config and calibration files. Obtained from ``os.path.join(os.path.expanduser("~"), ".openflexure")``. USER_CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".openflexure")
USER_CONFIG_FILE = os.path.join(USER_CONFIG_DIR, "microscope_settings.yaml") #: str: Default path of the user microscope_settings.yaml runtime-config file. Obtained from ``os.path.join(USER_CONFIG_DIR, "microscope_settings.yaml")`` USER_CONFIG_FILE = os.path.join(USER_CONFIG_DIR, "microscope_settings.yaml")
USER_CONFIG_FILE_OLD = os.path.join(USER_CONFIG_DIR, "microscoperc.yaml") USER_CONFIG_FILE_OLD = os.path.join(USER_CONFIG_DIR, "microscoperc.yaml")
@ -44,8 +52,10 @@ def construct_yaml_bool(self, node):
value = self.construct_scalar(node) value = self.construct_scalar(node)
return override_bool_values[value.lower()] return override_bool_values[value.lower()]
yaml.Loader.add_constructor(u'tag:yaml.org,2002:bool', construct_yaml_bool) yaml.Loader.add_constructor(u'tag:yaml.org,2002:bool', construct_yaml_bool)
# HANDLE CONVERTING ARBITRARY CONFIG TO JSON-SAFE JSON # HANDLE CONVERTING ARBITRARY CONFIG TO JSON-SAFE JSON
def json_convert(v): def json_convert(v):
@ -99,6 +109,7 @@ def json_map(data, clean_keys=True):
return to_map(d, json_convert) return to_map(d, json_convert)
# HANDLE BASIC LOADING AND SAVING OF YAML FILES # HANDLE BASIC LOADING AND SAVING OF YAML FILES
def load_yaml_file(config_path) -> dict: def load_yaml_file(config_path) -> dict:
@ -213,7 +224,8 @@ class OpenflexureConfig:
Read the current full config. Read the current full config.
Args: Args:
json_safe (bool): Converts invalid data types to JSON types, and removes any excessively large values (e.g. lens-shading tables). json_safe (bool): Converts invalid data types to JSON types, and removes any
excessively large values (e.g. lens-shading tables).
""" """
if json_safe: if json_safe:
logging.info("Reading config as JSON-safe dictionary") logging.info("Reading config as JSON-safe dictionary")

View file

@ -17,11 +17,11 @@ class LockError(ThreadError):
self.message = LockError.ERROR_CODES[code] self.message = LockError.ERROR_CODES[code]
else: else:
self.message = "Unknown error." self.message = "Unknown error."
self.string = "{}: {}".format(self.code, self.message) self.string = "{}: {}".format(self.code, self.message)
print(self.string) print(self.string)
ThreadError.__init__(self) ThreadError.__init__(self)
def __str__(self): def __str__(self):
return self.string return self.string

View file

@ -6,6 +6,13 @@ from openflexure_microscope.exceptions import LockError
class StrictLock(object): class StrictLock(object):
""" """
Class that behaves like a Python RLock, but with stricter timeout conditions and custom exceptions. Class that behaves like a Python RLock, but with stricter timeout conditions and custom exceptions.
Args:
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
Attributes:
_lock (:py:class:`threading.RLock`): Parent RLock object
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
""" """
def __init__(self, timeout=1): def __init__(self, timeout=1):
self._lock = RLock() self._lock = RLock()
@ -35,6 +42,14 @@ class CompositeLock(object):
""" """
Class that behaves like a :py:class:`openflexure_microscope.lock.StrictLock`, Class that behaves like a :py:class:`openflexure_microscope.lock.StrictLock`,
but allows multiple locks to be acquired and released. but allows multiple locks to be acquired and released.
Args:
locks (list): List of parent RLock objects
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
Attributes:
locks (list): List of parent RLock objects
timeout (int): Time, in seconds, lock acquisition will wait before raising an exception
""" """
def __init__(self, locks, timeout=1): def __init__(self, locks, timeout=1):
self.locks = locks self.locks = locks

View file

@ -25,24 +25,36 @@ class Microscope(object):
Args: Args:
camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera object camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera object
stage (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object stage (:py:class:`openflexure_stage.stage.OpenFlexureStage`): stage object
config (:py:class:`openflexure_microscope.config.OpenflexureConfig`): Runtime-config object, automatically created if None. config (:py:class:`openflexure_microscope.config.OpenflexureConfig`): Runtime-config object,
automatically created if None.
attach_plugins (bool): Should the microscope attach plugins listen in 'config' immediately. attach_plugins (bool): Should the microscope attach plugins listen in 'config' immediately.
Attributes:
rc (:py:class:`openflexure_microscope.config.OpenflexureConfig`): Runtime-config object,
automatically created if None.
lock (:py:class:`openflexure_microscope.lock.CompositeLock`): Composite lock controlling thread access
to multiple pieces of hardware.
camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): Camera object
stage (:py:class:`openflexure_stage.stage.OpenFlexureStage`): Stage object
task: (:py:class:`openflexure_microscope.task.TaskOrchestrator`): Threaded ask orchestrator for managing
background tasks using microscope hardware
plugin (:py:class:`openflexure_microscope.plugins.PluginMount`): Mounting point for all microscope plugins
""" """
def __init__( def __init__(
self, self,
camera: StreamingCamera, camera: StreamingCamera,
stage: OpenFlexureStage, stage: OpenFlexureStage,
config = None, config=None,
attach_plugins: bool = True): attach_plugins: bool = True):
# Create RC object # Create RC object
if config: if config:
self.rc = config self.rc = config
else: else:
self.rc = OpenflexureConfig(expand=True) # Load config from default path self.rc = OpenflexureConfig(expand=True)
# Initialise with an empty composite lock # Initialise with an empty composite lock
self.lock = CompositeLock([]) #: :py:class:`openflexure_microscope.lock.CompositeLock`: Composite lock controlling thread access to multiple pieces of hardware self.lock = CompositeLock([])
# Attach initial hardware (may be NoneTypes) # Attach initial hardware (may be NoneTypes)
self.camera = None self.camera = None
@ -50,23 +62,22 @@ class Microscope(object):
self.attach(camera, stage) self.attach(camera, stage)
# Create a task orchestrator # Create a task orchestrator
self.task = TaskOrchestrator() #: :py:class:`openflexure_microscope.task.TaskOrchestrator`: Threaded task orchestrator self.task = TaskOrchestrator()
# Create plugin mount-point # Create plugin mount-point
self.plugin = PluginMount(self) #: :py:class:`openflexure_microscope.plugins.PluginMount`: Mounting point for all microscope plugins self.plugin = PluginMount(self)
# Attach plugins # Attach plugins
if attach_plugins: if attach_plugins:
self.attach_plugins() self.attach_plugins()
# Set default parameters # Set default parameters
if 'name' not in self.rc.config: if 'name' not in self.rc.config:
self.rc.write({'name': uuid.uuid4().hex}) self.rc.write({'name': uuid.uuid4().hex})
if 'fov' not in self.rc.config: if 'fov' not in self.rc.config:
self.rc.write({'fov': [0, 0]}) self.rc.write({'fov': [0, 0]})
def __enter__(self): def __enter__(self):
"""Create microscope on context enter.""" """Create microscope on context enter."""
return self return self
@ -160,7 +171,8 @@ class Microscope(object):
"""Dictionary of the basic microscope state. """Dictionary of the basic microscope state.
Return: Return:
dict: Dictionary containing position data, and :py:attr:`openflexure_microscope.camera.base.BaseCamera.state` dict: Dictionary containing position data,
and :py:attr:`openflexure_microscope.camera.base.BaseCamera.state`
""" """
state = { state = {
'camera': self.camera.state, 'camera': self.camera.state,

View file

@ -1 +1,3 @@
from . import base, mock, sanga __all__ = ['base', 'mock', 'sanga']
from . import base, mock, sanga

View file

@ -1,16 +1,22 @@
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from openflexure_microscope.lock import StrictLock from openflexure_microscope.lock import StrictLock
class BaseStage(metaclass=ABCMeta): class BaseStage(metaclass=ABCMeta):
"""
Attributes:
lock (:py:class:`openflexure_microscope.lock.StrictLock`): Strict lock controlling thread
access to camera hardware
"""
def __init__(self): def __init__(self):
self.lock = StrictLock(timeout=5) #: :py:class:`openflexure_microscope.lock.StrictLock`: Strict lock controlling thread access to camera hardware self.lock = StrictLock(timeout=5)
@property @property
@abstractmethod @abstractmethod
def state(self): def state(self):
"""The general state dictionary of the board. """The general state dictionary of the board.
Should at least contain 'position', and 'board' keys. Should at least contain 'position', and 'board' keys.
Note: A None/Null value for 'board' will disable stage Note: A None/Null value for 'board' will disable stage
movement in the OpenFlexure eV client software, movement in the OpenFlexure eV client software,
""" """
pass pass

View file

@ -1,8 +1,5 @@
from .sangaboard import Sangaboard
from openflexure_microscope.stage.base import BaseStage from openflexure_microscope.stage.base import BaseStage
from openflexure_microscope.lock import StrictLock
import logging
class MockStage(BaseStage): class MockStage(BaseStage):
def __init__(self, port=None, **kwargs): def __init__(self, port=None, **kwargs):
@ -25,16 +22,14 @@ class MockStage(BaseStage):
} }
return state return state
@property @property
def n_axes(self): def n_axes(self):
return self._n_axis return self._n_axis
@property @property
def position(self): def position(self):
return self._position return self._position
@property @property
def backlash(self): def backlash(self):
return self._backlash if self._backlash else [0]*self.n_axes return self._backlash if self._backlash else [0]*self.n_axes
@ -43,11 +38,8 @@ class MockStage(BaseStage):
def backlash(self, blsh): def backlash(self, blsh):
if blsh is None: if blsh is None:
self._backlash = None self._backlash = None
try: assert len(blsh) == self.n_axes
assert len(blsh) == self.n_axes self._backlash = [int(blsh)]*self.n_axes
self._backlash = blsh
except:
self._backlash = [int(blsh)]*self.n_axes
def move_rel(self, displacement, axis=None, backlash=True): def move_rel(self, displacement, axis=None, backlash=True):
pass pass
@ -56,4 +48,4 @@ class MockStage(BaseStage):
pass pass
def close(self): def close(self):
pass pass

View file

@ -1,15 +1,24 @@
import numpy as np import numpy as np
import time
from .sangaboard import Sangaboard from .sangaboard import Sangaboard
from openflexure_microscope.stage.base import BaseStage from openflexure_microscope.stage.base import BaseStage
from openflexure_microscope.lock import StrictLock
import logging
class SangaStage(BaseStage): class SangaStage(BaseStage):
"""
Sangaboard v0.2 and v0.3 powered Stage object
Args:
port (str): Serial port on which to open communication
Attributes:
board (:py:class:`openflexure_microscope.stage.sangaboard.Sangaboard`): Parent Sangaboard object.
_backlash (list): 3-element (element-per-axis) list of backlash compensation in steps.
"""
def __init__(self, port=None, **kwargs): def __init__(self, port=None, **kwargs):
"""Class managing serial communications with the motors for an Openflexure stage""" """Class managing serial communications with the motors for an Openflexure stage"""
BaseStage.__init__(self) BaseStage.__init__(self)
self.board = Sangaboard(port, **kwargs) self.board = Sangaboard(port, **kwargs)
self._backlash = None self._backlash = None
@ -58,11 +67,8 @@ class SangaStage(BaseStage):
def backlash(self, blsh): def backlash(self, blsh):
if blsh is None: if blsh is None:
self._backlash = None self._backlash = None
try: assert len(blsh) == self.n_axes
assert len(blsh) == self.n_axes self._backlash = np.array([int(blsh)]*self.n_axes, dtype=np.int)
self._backlash = np.array(blsh, dtype=np.int)
except:
self._backlash = np.array([int(blsh)]*self.n_axes, dtype=np.int)
def move_rel(self, displacement, axis=None, backlash=True): def move_rel(self, displacement, axis=None, backlash=True):
"""Make a relative move, optionally correcting for backlash. """Make a relative move, optionally correcting for backlash.
@ -91,9 +97,11 @@ class SangaStage(BaseStage):
# point on the line. # point on the line.
# For each axis where we're moving in the *opposite* # For each axis where we're moving in the *opposite*
# direction to self.backlash, we deliberately overshoot: # direction to self.backlash, we deliberately overshoot:
initial_move -= np.where(self.backlash*displacement < 0, initial_move -= np.where(
self.backlash, self.backlash*displacement < 0,
np.zeros(self.n_axes, dtype=self.backlash.dtype)) self.backlash,
np.zeros(self.n_axes, dtype=self.backlash.dtype)
)
self.board.move_rel(initial_move) self.board.move_rel(initial_move)
if np.any(displacement - initial_move != 0): if np.any(displacement - initial_move != 0):
# If backlash correction has kicked in and made us overshoot, move # If backlash correction has kicked in and made us overshoot, move
@ -141,4 +149,4 @@ class SangaStage(BaseStage):
except Exception as e: except Exception as e:
print("A further exception occurred when resetting position: {}".format(e)) print("A further exception occurred when resetting position: {}".format(e))
print("Move completed, raising exception...") print("Move completed, raising exception...")
raise value #propagate the exception raise value # Propagate the exception

View file

@ -12,9 +12,13 @@ class TaskOrchestrator:
""" """
Class responsible for spawning threaded tasks, and storing their returns. Class responsible for spawning threaded tasks, and storing their returns.
A microscope should contain exactly one instance of `TaskOrchestrator`. A microscope should contain exactly one instance of `TaskOrchestrator`.
Attributes:
tasks (list): List of `Task` objects
""" """
def __init__(self): def __init__(self):
self.tasks = [] #: list: List of `Task` objects self.tasks = []
@property @property
def state(self): def state(self):
@ -39,14 +43,14 @@ class TaskOrchestrator:
Attach and start a new long-running task, if allowed by `start_condition()`. Attach and start a new long-running task, if allowed by `start_condition()`.
Returns an instance of :py:class:`openflexure_microscope.task.Task`, which can be used to check the state Returns an instance of :py:class:`openflexure_microscope.task.Task`, which can be used to check the state
or eventual return of the task. or eventual return of the task.
Args: Args:
function (function): The target function to run in the background function (function): The target function to run in the background
args, kwargs: Arguments that will be passed to `function` at runtime. args, kwargs: Arguments that will be passed to `function` at runtime.
""" """
# Create a task object # Create a task object
task = Task(function, *args, **kwargs) task = Task(function, *args, **kwargs)
# If the task isn't allowed to run # If the task isn't allowed to run
if not self.start_condition(task): if not self.start_condition(task):
raise TaskDeniedException("Unable to start this task. Aborting.") raise TaskDeniedException("Unable to start this task. Aborting.")
@ -88,16 +92,29 @@ class Task:
""" """
Class responsible for running a task function in a thread, and handling return or errors. Class responsible for running a task function in a thread, and handling return or errors.
Tasks should be created by an instance of :py:class:`openflexure_microscope.task.TaskOrchestrator`. Tasks should be created by an instance of :py:class:`openflexure_microscope.task.TaskOrchestrator`.
Args:
function (function): Function to be called in the task thread.
Attributes:
task (function): Function to be called in the task thread.
args: Positional arguments to be passed to the task function
kwargs: Keyword arguments to be passed to the task function.
_running (bool): If task is currently running
id (str): Unique ID for the task
state (dict): Dictionary describing the full state of the task.
`status` will be one of 'idle'', 'running', 'error', or 'success'.
`return` eventually holds the return value of the task function.
""" """
def __init__(self, function, *args, **kwargs): def __init__(self, function, *args, **kwargs):
# The task long-running method # The task long-running method
self.task = function #: function: Function to be called in the task thread. self.task = function
self.args = args #: Positional arguments to be passed to the task function. self.args = args
self.kwargs = kwargs #: Keyword arguments to be passed to the task function. self.kwargs = kwargs
self._running = False #: bool: If task is currently running self._running = False
self.id = uuid.uuid4().hex #: str: Unique ID for the task self.id = uuid.uuid4().hex
self.state = { self.state = {
'id': self.id, 'id': self.id,
@ -105,7 +122,7 @@ class Task:
'return': None, 'return': None,
'start_time': None, 'start_time': None,
'end_time': None, 'end_time': None,
} #: dict: Dictionary describing the full state of the task. `status` will be one of 'idle'', 'running', 'error', or 'success'. `return` eventually holds the return value of the task function. }
def process(self, f): def process(self, f):
""" """

View file

@ -7,7 +7,7 @@ from contextlib import contextmanager
@contextmanager @contextmanager
def set_properties(obj, **kwargs): def set_properties(obj, **kwargs):
"""A context manager to set, then reset, certain properties of an object. """A context manager to set, then reset, certain properties of an object.
The first argument is the object, subsequent keyword arguments are properties The first argument is the object, subsequent keyword arguments are properties
of said object, which are set initially, then reset to their previous values. of said object, which are set initially, then reset to their previous values.
""" """