diff --git a/openflexure_microscope/__init__.py b/openflexure_microscope/__init__.py index f372f2fb..8cd70322 100644 --- a/openflexure_microscope/__init__.py +++ b/openflexure_microscope/__init__.py @@ -1,4 +1,4 @@ -__all__ = ['Microscope', 'config', 'task', 'lock'] +__all__ = ['Microscope', 'config', 'task', 'lock', 'utilities'] __version__ = "0.1.0" from .microscope import Microscope diff --git a/openflexure_microscope/api/__init__.py b/openflexure_microscope/api/__init__.py index 08fef38a..daa8c0a6 100644 --- a/openflexure_microscope/api/__init__.py +++ b/openflexure_microscope/api/__init__.py @@ -1 +1,3 @@ +__all__ = ['utilities'] + from . import utilities diff --git a/openflexure_microscope/api/app.py b/openflexure_microscope/api/app.py index 665faf80..6f7133f2 100644 --- a/openflexure_microscope/api/app.py +++ b/openflexure_microscope/api/app.py @@ -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__": diff --git a/openflexure_microscope/api/utilities.py b/openflexure_microscope/api/utilities.py index 27dbeb7b..b3f919e0 100644 --- a/openflexure_microscope/api/utilities.py +++ b/openflexure_microscope/api/utilities.py @@ -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: diff --git a/openflexure_microscope/api/v1/blueprints/camera/capture.py b/openflexure_microscope/api/v1/blueprints/camera/capture.py index ffa41bdd..029b8c34 100644 --- a/openflexure_microscope/api/v1/blueprints/camera/capture.py +++ b/openflexure_microscope/api/v1/blueprints/camera/capture.py @@ -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 diff --git a/openflexure_microscope/api/v1/blueprints/camera/function.py b/openflexure_microscope/api/v1/blueprints/camera/function.py index aee019d1..acaefc5f 100644 --- a/openflexure_microscope/api/v1/blueprints/camera/function.py +++ b/openflexure_microscope/api/v1/blueprints/camera/function.py @@ -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): :
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") diff --git a/openflexure_microscope/camera/capture.py b/openflexure_microscope/camera/capture.py index 0d4d9166..1fddedfe 100644 --- a/openflexure_microscope/camera/capture.py +++ b/openflexure_microscope/camera/capture.py @@ -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 diff --git a/openflexure_microscope/camera/pi.py b/openflexure_microscope/camera/pi.py index dc7745dd..9d7a491a 100644 --- a/openflexure_microscope/camera/pi.py +++ b/openflexure_microscope/camera/pi.py @@ -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) diff --git a/openflexure_microscope/camera/set_picamera_gain.py b/openflexure_microscope/camera/set_picamera_gain.py index 8ef33df3..26191902 100644 --- a/openflexure_microscope/camera/set_picamera_gain.py +++ b/openflexure_microscope/camera/set_picamera_gain.py @@ -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...") - - diff --git a/openflexure_microscope/config.py b/openflexure_microscope/config.py index 928882c1..1d0645ff 100644 --- a/openflexure_microscope/config.py +++ b/openflexure_microscope/config.py @@ -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") diff --git a/openflexure_microscope/exceptions.py b/openflexure_microscope/exceptions.py index a1136933..b03bc88d 100644 --- a/openflexure_microscope/exceptions.py +++ b/openflexure_microscope/exceptions.py @@ -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 diff --git a/openflexure_microscope/lock.py b/openflexure_microscope/lock.py index 94e25f5d..59a37a32 100644 --- a/openflexure_microscope/lock.py +++ b/openflexure_microscope/lock.py @@ -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 diff --git a/openflexure_microscope/microscope.py b/openflexure_microscope/microscope.py index 5b684b7c..03503800 100644 --- a/openflexure_microscope/microscope.py +++ b/openflexure_microscope/microscope.py @@ -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, diff --git a/openflexure_microscope/stage/__init__.py b/openflexure_microscope/stage/__init__.py index 9ef9d9f3..46cd36a8 100644 --- a/openflexure_microscope/stage/__init__.py +++ b/openflexure_microscope/stage/__init__.py @@ -1 +1,3 @@ -from . import base, mock, sanga \ No newline at end of file +__all__ = ['base', 'mock', 'sanga'] + +from . import base, mock, sanga diff --git a/openflexure_microscope/stage/base.py b/openflexure_microscope/stage/base.py index 4236e7ee..297f0f45 100644 --- a/openflexure_microscope/stage/base.py +++ b/openflexure_microscope/stage/base.py @@ -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 diff --git a/openflexure_microscope/stage/mock.py b/openflexure_microscope/stage/mock.py index f706e1b9..345d822f 100644 --- a/openflexure_microscope/stage/mock.py +++ b/openflexure_microscope/stage/mock.py @@ -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 \ No newline at end of file + pass diff --git a/openflexure_microscope/stage/sanga.py b/openflexure_microscope/stage/sanga.py index b14eff1b..aa1ccfa2 100644 --- a/openflexure_microscope/stage/sanga.py +++ b/openflexure_microscope/stage/sanga.py @@ -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 \ No newline at end of file + raise value # Propagate the exception diff --git a/openflexure_microscope/task.py b/openflexure_microscope/task.py index eb9656af..e2a9a085 100644 --- a/openflexure_microscope/task.py +++ b/openflexure_microscope/task.py @@ -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): """ diff --git a/openflexure_microscope/utilities.py b/openflexure_microscope/utilities.py index 2e8f6a6b..de2503a0 100644 --- a/openflexure_microscope/utilities.py +++ b/openflexure_microscope/utilities.py @@ -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. """