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"
from .microscope import Microscope

View file

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

View file

@ -1,9 +1,8 @@
#!/usr/bin/env python
from flask import (
Flask, render_template, jsonify, send_file)
Flask, jsonify, send_file)
from flask.logging import default_handler
from serial import SerialException
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.api.v1 import blueprints
import atexit
import logging
import sys, os
import sys
import os
# Handle logging
is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
@ -63,6 +65,7 @@ api_microscope = Microscope(None, None)
# 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:
@ -91,7 +94,7 @@ def attach_microscope():
logging.debug("Creating camera object...")
api_camera = StreamingCamera(config=api_microscope.rc.read())
logging.debug("Creating stage object...")
try:
api_stage = SangaStage()
@ -113,10 +116,9 @@ def attach_microscope():
logging.debug("Microscope successfully attached!")
##### WEBAPP ROUTES ######
# WEBAPP ROUTES
##### API ROUTES ######
from openflexure_microscope.api.v1 import blueprints
# API ROUTES
# Base routes
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)
app.register_blueprint(task_blueprint, url_prefix=uri('/task', 'v1'))
@app.route('/routes')
def routes():
"""
@ -145,13 +148,19 @@ def 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))
return send_file(
DEFAULT_LOGFILE,
as_attachment=True,
attachment_filename='openflexure_microscope_{}.log'.format(timestamp)
)
# Automatically clean up microscope at exit
def cleanup():
@ -166,6 +175,7 @@ def cleanup():
api_microscope.close()
logging.debug("App teardown complete.")
atexit.register(cleanup)
if __name__ == "__main__":

View file

@ -1,8 +1,8 @@
import pprint
import logging
from werkzeug.exceptions import BadRequest
from flask import url_for
class JsonPayload:
def __init__(self, request):
"""
@ -32,7 +32,8 @@ class JsonPayload:
Args:
key (str): JSON key to look for
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 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.utilities import filter_dict
@ -201,7 +201,10 @@ class CaptureAPI(MicroscopeView):
# If available, also add download link
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)
@ -297,7 +300,12 @@ class DownloadRedirectAPI(MicroscopeView):
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):
@ -312,7 +320,12 @@ class DownloadAPI(MicroscopeView):
# 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, 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
if thumbnail:
@ -360,7 +373,11 @@ class MetadataRedirectAPI(MicroscopeView):
if not capture_obj or not capture_obj.state['available']:
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):
@ -373,7 +390,11 @@ class MetadataAPI(MicroscopeView):
# 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.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
data = capture_obj.yaml

View file

@ -35,7 +35,7 @@ class ZoomAPI(MicroscopeView):
Accept: application/json
{
"zoom_value": 2.5,
"zoom_value": 2.5,
}
:>header Accept: application/json
@ -83,8 +83,8 @@ class OverlayAPI(MicroscopeView):
Accept: application/json
{
"text": "2019/01/15 14:48",
"size": 50
"text": "2019/01/15 14:48",
"size": 50
}
:>header Accept: application/json
@ -92,7 +92,7 @@ class OverlayAPI(MicroscopeView):
:<header Content-Type: application/json
:status 200: preview started/stopped
"""
payload = JsonPayload(request)
text = payload.param('text', default="", convert=str)
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
import logging
class GPUPreviewAPI(MicroscopeView):
def post(self, operation):
@ -45,8 +46,8 @@ class GPUPreviewAPI(MicroscopeView):
fullscreen = True
window = None
else:
fullscreen = False
window = [int(w) for w in window]
fullscreen = False
window = [int(w) for w in window]
self.microscope.camera.start_preview(fullscreen=fullscreen, window=window)
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.utilities import axes_to_array, filter_dict

View file

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

View file

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

View file

@ -3,7 +3,6 @@ import time
import os
import threading
import datetime
import yaml
import logging
try:
@ -15,7 +14,6 @@ except ImportError:
from _thread import get_ident
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.lock import StrictLock
@ -54,7 +52,7 @@ class CameraEvent(object):
def __init__(self):
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)."""
ident = get_ident()
if ident not in self.events:
@ -92,32 +90,42 @@ class CameraEvent(object):
class BaseCamera(object):
"""
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):
self.thread = None #: Background thread reading frames from camera
self.camera = None #: Camera object
self.thread = None
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.last_access = 0 #: time: Time of last client access to the camera
self.frame = None
self.last_access = 0
self.event = CameraEvent()
self.stream_timeout = 20 #: int: Number of inactive seconds before timing out the stream
self.stream_timeout_enabled = False #: bool: Enable or disable timing out the stream
self.state = {} #: dict: Dictionary for capture state
self.config = {} #: dict: Dictionary of base camera config
self.state = {}
self.config = {}
self.paths = {
'image': BASE_CAPTURE_PATH,
'video': BASE_CAPTURE_PATH,
'image_tmp': TEMP_CAPTURE_PATH,
'video_tpm': TEMP_CAPTURE_PATH
} #: dict: Dictionary of capture paths
}
# Capture data
self.images = [] #: list: List of image capture objects
self.videos = [] #: list: List of video recording objects
self.images = []
self.videos = []
def apply_config(self, config):
"""Update settings from a config dictionary"""
@ -164,7 +172,6 @@ class BaseCamera(object):
self.last_access = time.time()
self.stop = False
#if self.thread is None:
if not self.state['stream_active']:
# start background frame thread
self.thread = threading.Thread(target=self._thread)
@ -185,13 +192,11 @@ class BaseCamera(object):
logging.debug("Stopping worker thread")
timeout_time = time.time() + timeout
#if self.thread:
if self.state['stream_active']:
self.stop = True
self.thread.join() # Wait for stream thread to exit
logging.debug("Waiting for stream thread to exit.")
#while self.thread:
while self.state['stream_active']:
if time.time() > timeout_time:
logging.debug("Timeout waiting for worker thread close.")
@ -255,7 +260,8 @@ class BaseCamera(object):
Args:
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.
fmt (str): Format of the capture.
"""
@ -298,7 +304,8 @@ class BaseCamera(object):
Args:
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.
fmt (str): Format of the capture.
"""
@ -363,6 +370,3 @@ class BaseCamera(object):
logging.debug("BaseCamera worker thread exiting...")
# Set stream_activate state
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
"""
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']
EXIF_FORMATS = ['JPG', 'JPEG', 'TIF', 'TIFF']
THUMBNAIL_SIZE = (200, 150)
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs') #: str: Base path to store all captures
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp') #: str: Base path to store all temporary captures (automatically emptied)
BASE_CAPTURE_PATH = os.path.join(os.path.expanduser('~'), 'micrographs')
TEMP_CAPTURE_PATH = os.path.join(BASE_CAPTURE_PATH, 'tmp')
# 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.
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__(
@ -120,10 +139,10 @@ class CaptureObject(object):
# Store a nice ID
self.id = uuid.uuid4().hex #: str: Unique capture 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
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
if not filepath:
@ -133,13 +152,13 @@ class CaptureObject(object):
self.split_file_path(self.file)
# Dictionary for storing custom metadata
self._metadata = {} #: dict: Dictionary of custom metadata to be included in metadata file
self._metadata = {}
# 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
self.bytestream = io.BytesIO() # Byte bytestream that data will be written to
self.bytestream = io.BytesIO()
# Set default write target
if not write_to_file:
@ -182,12 +201,14 @@ class CaptureObject(object):
def split_file_path(self, filepath):
"""
Take a full file path, and split it into separated class properties.
Args:
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
self.basename = os.path.splitext(self.filename)[0] # Split the filename out from it's file extension
# Split the full file path into a folder and a filename
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]
# Create folder and file

View file

@ -242,7 +242,7 @@ class StreamingCamera(BaseCamera):
logging.info("Starting the GPU preview")
# 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:
if not self.camera.preview:
@ -365,7 +365,8 @@ class StreamingCamera(BaseCamera):
Args:
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:
# If stream object was destroyed
@ -392,7 +393,7 @@ class StreamingCamera(BaseCamera):
self.stream,
format='mjpeg',
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
# metric)
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]:
raise ValueError("The gain parameter was not valid")
ret = mmal.mmal_port_parameter_set_rational(camera._camera.control._port,
gain,
to_rational(value))
ret = mmal.mmal_port_parameter_set_rational(
camera._camera.control._port,
gain,
to_rational(value)
)
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:
raise exc.PiCameraMMALError(ret)
@ -40,7 +45,7 @@ def set_digital_gain(camera, value):
if __name__ == "__main__":
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)
# 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")
set_digital_gain(cam, 1)
# 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,
# to_rational(1))
#print("Return code: {}".format(ret))
# print("Return code: {}".format(ret))
try:
while True:
@ -69,5 +74,3 @@ if __name__ == "__main__":
time.sleep(1)
except KeyboardInterrupt:
logging.info("Stopping...")

View file

@ -7,13 +7,21 @@ import copy
from fractions import Fraction
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
HERE = os.path.abspath(os.path.dirname(__file__))
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_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_DIR = os.path.join(os.path.expanduser("~"), ".openflexure")
USER_CONFIG_FILE = os.path.join(USER_CONFIG_DIR, "microscope_settings.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)
return override_bool_values[value.lower()]
yaml.Loader.add_constructor(u'tag:yaml.org,2002:bool', construct_yaml_bool)
# HANDLE CONVERTING ARBITRARY CONFIG TO JSON-SAFE JSON
def json_convert(v):
@ -99,6 +109,7 @@ def json_map(data, clean_keys=True):
return to_map(d, json_convert)
# HANDLE BASIC LOADING AND SAVING OF YAML FILES
def load_yaml_file(config_path) -> dict:
@ -213,7 +224,8 @@ class OpenflexureConfig:
Read the current full config.
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:
logging.info("Reading config as JSON-safe dictionary")

View file

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

View file

@ -6,6 +6,13 @@ from openflexure_microscope.exceptions import LockError
class StrictLock(object):
"""
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):
self._lock = RLock()
@ -35,6 +42,14 @@ class CompositeLock(object):
"""
Class that behaves like a :py:class:`openflexure_microscope.lock.StrictLock`,
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):
self.locks = locks

View file

@ -25,24 +25,36 @@ class Microscope(object):
Args:
camera (:py:class:`openflexure_microscope.camera.pi.StreamingCamera`): camera 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.
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__(
self,
camera: StreamingCamera,
stage: OpenFlexureStage,
config = None,
config=None,
attach_plugins: bool = True):
# Create RC object
if config:
self.rc = config
else:
self.rc = OpenflexureConfig(expand=True) # Load config from default path
self.rc = OpenflexureConfig(expand=True)
# 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)
self.camera = None
@ -50,23 +62,22 @@ class Microscope(object):
self.attach(camera, stage)
# Create a task orchestrator
self.task = TaskOrchestrator() #: :py:class:`openflexure_microscope.task.TaskOrchestrator`: Threaded task orchestrator
self.task = TaskOrchestrator()
# 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
if attach_plugins:
self.attach_plugins()
# Set default parameters
if 'name' not in self.rc.config:
self.rc.write({'name': uuid.uuid4().hex})
if 'fov' not in self.rc.config:
self.rc.write({'fov': [0, 0]})
def __enter__(self):
"""Create microscope on context enter."""
return self
@ -160,7 +171,8 @@ class Microscope(object):
"""Dictionary of the basic microscope state.
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 = {
'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 openflexure_microscope.lock import StrictLock
class BaseStage(metaclass=ABCMeta):
"""
Attributes:
lock (:py:class:`openflexure_microscope.lock.StrictLock`): Strict lock controlling thread
access to camera hardware
"""
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
@abstractmethod
def state(self):
"""The general state dictionary of the board.
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,
"""
pass

View file

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

View file

@ -1,15 +1,24 @@
import numpy as np
import time
from .sangaboard import Sangaboard
from openflexure_microscope.stage.base import BaseStage
from openflexure_microscope.lock import StrictLock
import logging
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):
"""Class managing serial communications with the motors for an Openflexure stage"""
BaseStage.__init__(self)
self.board = Sangaboard(port, **kwargs)
self._backlash = None
@ -58,11 +67,8 @@ class SangaStage(BaseStage):
def backlash(self, blsh):
if blsh is None:
self._backlash = None
try:
assert len(blsh) == self.n_axes
self._backlash = np.array(blsh, dtype=np.int)
except:
self._backlash = np.array([int(blsh)]*self.n_axes, dtype=np.int)
assert len(blsh) == self.n_axes
self._backlash = np.array([int(blsh)]*self.n_axes, dtype=np.int)
def move_rel(self, displacement, axis=None, backlash=True):
"""Make a relative move, optionally correcting for backlash.
@ -91,9 +97,11 @@ class SangaStage(BaseStage):
# point on the line.
# For each axis where we're moving in the *opposite*
# direction to self.backlash, we deliberately overshoot:
initial_move -= np.where(self.backlash*displacement < 0,
self.backlash,
np.zeros(self.n_axes, dtype=self.backlash.dtype))
initial_move -= np.where(
self.backlash*displacement < 0,
self.backlash,
np.zeros(self.n_axes, dtype=self.backlash.dtype)
)
self.board.move_rel(initial_move)
if np.any(displacement - initial_move != 0):
# If backlash correction has kicked in and made us overshoot, move
@ -141,4 +149,4 @@ class SangaStage(BaseStage):
except Exception as e:
print("A further exception occurred when resetting position: {}".format(e))
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.
A microscope should contain exactly one instance of `TaskOrchestrator`.
Attributes:
tasks (list): List of `Task` objects
"""
def __init__(self):
self.tasks = [] #: list: List of `Task` objects
self.tasks = []
@property
def state(self):
@ -39,14 +43,14 @@ class TaskOrchestrator:
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
or eventual return of the task.
Args:
function (function): The target function to run in the background
args, kwargs: Arguments that will be passed to `function` at runtime.
"""
# Create a task object
task = Task(function, *args, **kwargs)
# If the task isn't allowed to run
if not self.start_condition(task):
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.
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):
# The task long-running method
self.task = function #: function: Function to be called in the task thread.
self.args = args #: Positional arguments to be passed to the task function.
self.kwargs = kwargs #: Keyword arguments to be passed to the task function.
self.task = function
self.args = args
self.kwargs = kwargs
self._running = False #: bool: If task is currently running
self.id = uuid.uuid4().hex #: str: Unique ID for the task
self._running = False
self.id = uuid.uuid4().hex
self.state = {
'id': self.id,
@ -105,7 +122,7 @@ class Task:
'return': None,
'start_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):
"""

View file

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