Improved flake8 results
This commit is contained in:
parent
fb606b189d
commit
3b9ef670fe
24 changed files with 256 additions and 126 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
|
__all__ = ['utilities']
|
||||||
|
|
||||||
from . import utilities
|
from . import utilities
|
||||||
|
|
|
||||||
|
|
@ -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__":
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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":
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
|
__all__ = ['pi', 'base']
|
||||||
|
|
||||||
from . import pi, base
|
from . import pi, base
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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...")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -1 +1,3 @@
|
||||||
from . import base, mock, sanga
|
__all__ = ['base', 'mock', 'sanga']
|
||||||
|
|
||||||
|
from . import base, mock, sanga
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue