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"
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 import Microscope, config
from openflexure_microscope.lock import LockError
from openflexure_microscope.camera.pi import StreamingCamera
from openflexure_stage import OpenFlexureStage
from openflexure_microscope.stage.openflexure import Stage
import atexit
import logging, sys
@ -66,6 +67,16 @@ def _handle_http_exception(e):
for code in default_exceptions:
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
@app.before_first_request
@ -77,7 +88,7 @@ def attach_microscope():
logging.debug("Creating camera object...")
api_camera = StreamingCamera(config=openflexurerc)
logging.debug("Creating stage object...")
api_stage = OpenFlexureStage("/dev/ttyUSB0")
api_stage = Stage("/dev/ttyUSB0")
logging.debug("Attaching devices to microscope...")
api_microscope.attach(

View file

@ -19,6 +19,7 @@ except ImportError:
from .capture import CaptureObject, capture_from_dict, BASE_CAPTURE_PATH
from openflexure_microscope.config import USER_CONFIG_DIR
from openflexure_microscope.utilities import entry_by_id
from openflexure_microscope.lock import StrictLock
def last_entry(object_list: list):
@ -29,7 +30,6 @@ def last_entry(object_list: list):
return None
def generate_basename():
"""Return a default filename based on the capture datetime"""
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.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.last_access = 0 #: time: Time of last client access to the camera
self.event = CameraEvent()

View file

@ -201,6 +201,8 @@ class StreamingCamera(BaseCamera):
paused_stream = False
with self.lock:
# Apply valid config params to Picamera object
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.
"""
with self.lock:
self.state['zoom_value'] = float(zoom_value)
if self.state['zoom_value'] < 1:
self.state['zoom_value'] = 1
@ -299,6 +302,7 @@ class StreamingCamera(BaseCamera):
output_object (str/BytesIO): Target object.
"""
with self.lock:
# Start recording method only if a current recording is not running
if not self.state['record_active']:
@ -334,6 +338,7 @@ class StreamingCamera(BaseCamera):
def stop_recording(self) -> bool:
"""Stop the last started video recording on splitter port 2."""
with self.lock:
# Stop the camera video recording on port 2
logging.info("Stopping recording")
self.camera.stop_recording(splitter_port=2)
@ -378,7 +383,6 @@ class StreamingCamera(BaseCamera):
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']`.
"""
# If no stream object exists
if not hasattr(self, 'stream'):
self.stream = io.BytesIO() # Create a stream object
@ -421,6 +425,7 @@ class StreamingCamera(BaseCamera):
resize ((int, int)): Resize the captured image.
"""
with self.lock:
# If output is a StreamObject
if isinstance(output, CaptureObject):
# 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.
resize ((int, int)): Resize the captured image.
"""
with self.lock:
if use_video_port:
resolution = self.config['video_resolution']
else:
@ -502,7 +508,7 @@ class StreamingCamera(BaseCamera):
use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster.
resize ((int, int)): Resize the captured image.
"""
with self.lock:
if use_video_port:
resolution = self.config['video_resolution']
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.
time_to_run = payload.param('time', default=10, convert=int)
# Attach the long-running method as a microscope task
try:
task = self.microscope.task.start(self.plugin.long_running, time_to_run)
return jsonify(task.state), 202

View file

@ -39,10 +39,12 @@ class Plugin(MicroscopePlugin):
print("Starting a long-running task...")
n_array = []
print("Acquiring camera lock...")
with self.microscope.camera.lock:
for _ in range(t_run):
n_array.append(random.random())
time.sleep(1)
print("Long-running task finished!")
print("Long-running task finished! Releasing lock.")
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):
"""
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 just allows a single task at a time.
Currently always allows tasks to start, as locks determine access to hardware.
"""
return not any([task._running for task in self.tasks])
#return not any([task._running for task in self.tasks])
return True
class Task: