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"
|
||||
|
||||
from .microscope import Microscope
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
__all__ = ['utilities']
|
||||
|
||||
from . import utilities
|
||||
|
|
|
|||
|
|
@ -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__":
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ class TaskAPI(MicroscopeView):
|
|||
"""
|
||||
|
||||
success = self.microscope.task.delete(task_id)
|
||||
|
||||
|
||||
if success:
|
||||
data = {
|
||||
'status': 'success',
|
||||
|
|
|
|||
|
|
@ -1 +1,3 @@
|
|||
__all__ = ['pi', 'base']
|
||||
|
||||
from . import pi, base
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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...")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue