Implement hardware locks

This commit is contained in:
Joel Collins 2019-01-24 12:11:01 +00:00
parent a40cb9b9f6
commit 8292ebf7c7
10 changed files with 268 additions and 170 deletions

View file

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

View file

@ -23,8 +23,9 @@ from flask_cors import CORS
from openflexure_microscope.api.utilities import list_routes from openflexure_microscope.api.utilities import list_routes
from openflexure_microscope import Microscope, config from openflexure_microscope import Microscope, config
from openflexure_microscope.lock import LockError
from openflexure_microscope.camera.pi import StreamingCamera from openflexure_microscope.camera.pi import StreamingCamera
from openflexure_stage import OpenFlexureStage from openflexure_microscope.stage.openflexure import Stage
import atexit import atexit
import logging, sys import logging, sys
@ -66,6 +67,16 @@ def _handle_http_exception(e):
for code in default_exceptions: for code in default_exceptions:
app.errorhandler(code)(_handle_http_exception) app.errorhandler(code)(_handle_http_exception)
def _handle_lock_exception(e):
return make_response(
jsonify({
'status_code': 423,
'error': e.code,
'details': e.message
}),
423)
app.errorhandler(LockError)(_handle_lock_exception)
# After app starts, but before first request, attach hardware to global microscope # After app starts, but before first request, attach hardware to global microscope
@app.before_first_request @app.before_first_request
@ -77,7 +88,7 @@ def attach_microscope():
logging.debug("Creating camera object...") logging.debug("Creating camera object...")
api_camera = StreamingCamera(config=openflexurerc) api_camera = StreamingCamera(config=openflexurerc)
logging.debug("Creating stage object...") logging.debug("Creating stage object...")
api_stage = OpenFlexureStage("/dev/ttyUSB0") api_stage = Stage("/dev/ttyUSB0")
logging.debug("Attaching devices to microscope...") logging.debug("Attaching devices to microscope...")
api_microscope.attach( api_microscope.attach(

View file

@ -19,6 +19,7 @@ except ImportError:
from .capture import CaptureObject, capture_from_dict, BASE_CAPTURE_PATH from .capture import CaptureObject, capture_from_dict, BASE_CAPTURE_PATH
from openflexure_microscope.config import USER_CONFIG_DIR 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
def last_entry(object_list: list): def last_entry(object_list: list):
@ -29,7 +30,6 @@ def last_entry(object_list: list):
return None return None
def generate_basename(): def generate_basename():
"""Return a default filename based on the capture datetime""" """Return a default filename based on the capture datetime"""
return datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S") return datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
@ -87,6 +87,8 @@ class BaseCamera(object):
self.thread = None #: Background thread reading frames from camera self.thread = None #: Background thread reading frames from camera
self.camera = None #: Camera object self.camera = None #: Camera object
self.lock = StrictLock(timeout=1) #: Strict lock controlling thread access to camera hardware
self.frame = None #: bytes: Current frame is stored here by background thread 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.last_access = 0 #: time: Time of last client access to the camera
self.event = CameraEvent() self.event = CameraEvent()

View file

@ -201,6 +201,8 @@ class StreamingCamera(BaseCamera):
paused_stream = False paused_stream = False
with self.lock:
# Apply valid config params to Picamera object # Apply valid config params to Picamera object
if not self.state['record_active']: # If not recording a video if not self.state['record_active']: # If not recording a video
@ -249,6 +251,7 @@ class StreamingCamera(BaseCamera):
""" """
Change the camera zoom, handling recentering and scaling. Change the camera zoom, handling recentering and scaling.
""" """
with self.lock:
self.state['zoom_value'] = float(zoom_value) self.state['zoom_value'] = float(zoom_value)
if self.state['zoom_value'] < 1: if self.state['zoom_value'] < 1:
self.state['zoom_value'] = 1 self.state['zoom_value'] = 1
@ -299,6 +302,7 @@ class StreamingCamera(BaseCamera):
output_object (str/BytesIO): Target object. output_object (str/BytesIO): Target object.
""" """
with self.lock:
# Start recording method only if a current recording is not running # Start recording method only if a current recording is not running
if not self.state['record_active']: if not self.state['record_active']:
@ -334,6 +338,7 @@ class StreamingCamera(BaseCamera):
def stop_recording(self) -> bool: def stop_recording(self) -> bool:
"""Stop the last started video recording on splitter port 2.""" """Stop the last started video recording on splitter port 2."""
with self.lock:
# Stop the camera video recording on port 2 # Stop the camera video recording on port 2
logging.info("Stopping recording") logging.info("Stopping recording")
self.camera.stop_recording(splitter_port=2) self.camera.stop_recording(splitter_port=2)
@ -378,7 +383,6 @@ class StreamingCamera(BaseCamera):
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['video_resolution']`. resolution ((int, int)): Resolution to set the camera to, before starting recording. Defaults to `self.config['video_resolution']`.
""" """
# If no stream object exists # If no stream object exists
if not hasattr(self, 'stream'): if not hasattr(self, 'stream'):
self.stream = io.BytesIO() # Create a stream object self.stream = io.BytesIO() # Create a stream object
@ -421,6 +425,7 @@ class StreamingCamera(BaseCamera):
resize ((int, int)): Resize the captured image. resize ((int, int)): Resize the captured image.
""" """
with self.lock:
# If output is a StreamObject # If output is a StreamObject
if isinstance(output, CaptureObject): if isinstance(output, CaptureObject):
# Set target to capture stream # Set target to capture stream
@ -459,6 +464,7 @@ class StreamingCamera(BaseCamera):
use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster. use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster.
resize ((int, int)): Resize the captured image. resize ((int, int)): Resize the captured image.
""" """
with self.lock:
if use_video_port: if use_video_port:
resolution = self.config['video_resolution'] resolution = self.config['video_resolution']
else: else:
@ -502,7 +508,7 @@ class StreamingCamera(BaseCamera):
use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster. use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster.
resize ((int, int)): Resize the captured image. resize ((int, int)): Resize the captured image.
""" """
with self.lock:
if use_video_port: if use_video_port:
resolution = self.config['video_resolution'] resolution = self.config['video_resolution']
else: else:

View file

@ -0,0 +1,65 @@
from threading import RLock, ThreadError
class LockError(ThreadError):
ERROR_CODES = {
'ACQUIRE_ERROR': "Unable to acquire. Lock in use by another thread.",
'IN_USE_ERROR': "Lock in use by another thread."
}
def __init__(self, code, lock):
self.code = code
if code in LockError.ERROR_CODES:
self.message = LockError.ERROR_CODES[code]
else:
self.message = "Unknown error."
print("{}: {}".format(self.code, self.message))
ThreadError.__init__(self)
class StrictLock(object):
def __init__(self, timeout=1):
self._lock = RLock()
self.timeout = timeout
def acquire(self, blocking=True):
return self._lock.acquire(blocking, timeout=self.timeout)
def __enter__(self):
result = self._lock.acquire(blocking=True, timeout=self.timeout)
if result:
return result
else:
raise LockError('ACQUIRE_ERROR', self)
def __exit__(self, *args):
self._lock.release()
def release(self):
self._lock.release()
class CompositeLock(object):
def __init__(self, locks, timeout=1):
self.locks = locks
self.timeout = timeout
def acquire(self, blocking=True):
return (lock.acquire(blocking=blocking, timeout=self.timeout) for lock in self.locks)
def __enter__(self):
result = (lock.acquire(blocking=True, timeout=self.timeout) for lock in self.locks)
if all(result):
return result
else:
msg = "Unable to acquire all locks {}.".format(self.locks)
logging.error(msg)
raise ThreadError(msg)
def __exit__(self, *args):
for lock in self.locks:
lock.release()
def release(self):
for lock in self.locks:
lock.release()

View file

@ -67,6 +67,7 @@ class LongRunningAPI(MicroscopeViewPlugin):
# Extract a values from the JSON payload. # Extract a values from the JSON payload.
time_to_run = payload.param('time', default=10, convert=int) time_to_run = payload.param('time', default=10, convert=int)
# Attach the long-running method as a microscope task
try: try:
task = self.microscope.task.start(self.plugin.long_running, time_to_run) task = self.microscope.task.start(self.plugin.long_running, time_to_run)
return jsonify(task.state), 202 return jsonify(task.state), 202

View file

@ -39,10 +39,12 @@ class Plugin(MicroscopePlugin):
print("Starting a long-running task...") print("Starting a long-running task...")
n_array = [] n_array = []
print("Acquiring camera lock...")
with self.microscope.camera.lock:
for _ in range(t_run): for _ in range(t_run):
n_array.append(random.random()) n_array.append(random.random())
time.sleep(1) time.sleep(1)
print("Long-running task finished!") print("Long-running task finished! Releasing lock.")
return n_array return n_array

View file

@ -0,0 +1 @@
from . import openflexure

View file

@ -0,0 +1,10 @@
from openflexure_stage import OpenFlexureStage
from openflexure_microscope.lock import StrictLock
# TODO: Implement lock on movement
class Stage(OpenFlexureStage):
def __init__(self, *args, **kwargs):
self.lock = StrictLock(timeout=2) #: Strict lock controlling thread access to camera hardware
OpenFlexureStage.__init__(self, *args, **kwargs)

View file

@ -57,10 +57,10 @@ class TaskOrchestrator:
def start_condition(self, task_obj): def start_condition(self, task_obj):
""" """
Method returning bool describing if a particular task is allowed to run. Method returning bool describing if a particular task is allowed to run.
In the future, this will depend on the state of hardware locks? Currently always allows tasks to start, as locks determine access to hardware.
Currently just allows a single task at a time.
""" """
return not any([task._running for task in self.tasks]) #return not any([task._running for task in self.tasks])
return True
class Task: class Task: