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,70 +201,73 @@ class StreamingCamera(BaseCamera):
paused_stream = False paused_stream = False
# Apply valid config params to Picamera object with self.lock:
if not self.state['record_active']: # If not recording a video
logging.debug("Applying config:") # Apply valid config params to Picamera object
logging.debug(config) if not self.state['record_active']: # If not recording a video
# Pause stream while changing settings logging.debug("Applying config:")
if self.state['stream_active']: # If stream is active logging.debug(config)
logging.info("Pausing stream to update config.")
self.stop_stream_recording() # Pause stream
paused_stream = True # Remember to unpause stream when done
# PiCamera parameters (applied directly to PiCamera object) # Pause stream while changing settings
if 'picamera_params' in config: if self.state['stream_active']: # If stream is active
for key in PICAMERA_KEYS: logging.info("Pausing stream to update config.")
if key in config['picamera_params']: self.stop_stream_recording() # Pause stream
logging.debug("Setting parameter {}: {}".format(key, config['picamera_params'][key])) paused_stream = True # Remember to unpause stream when done
setattr(self.camera, key, config['picamera_params'][key])
# StreamingCamera parameters (applied via StreamingCamera config) # PiCamera parameters (applied directly to PiCamera object)
for key in CONFIG_KEYS: if 'picamera_params' in config:
if key in config: for key in PICAMERA_KEYS:
logging.debug("Setting parameter {}: {}".format(key, config[key])) if key in config['picamera_params']:
self._config[key] = config[key] logging.debug("Setting parameter {}: {}".format(key, config['picamera_params'][key]))
setattr(self.camera, key, config['picamera_params'][key])
# Richard's library to set analog and digital gains # StreamingCamera parameters (applied via StreamingCamera config)
if 'analog_gain' in config: for key in CONFIG_KEYS:
set_analog_gain(self.camera, config['analog_gain']) if key in config:
if 'digital_gain' in config: logging.debug("Setting parameter {}: {}".format(key, config[key]))
set_digital_gain(self.camera, config['digital_gain']) self._config[key] = config[key]
# Handle lens shading, if a new one is given # Richard's library to set analog and digital gains
if 'shading_table_path' in config: if 'analog_gain' in config:
self.apply_shading_table() set_analog_gain(self.camera, config['analog_gain'])
if 'digital_gain' in config:
set_digital_gain(self.camera, config['digital_gain'])
# If stream was paused to update config, unpause # Handle lens shading, if a new one is given
if paused_stream: if 'shading_table_path' in config:
logging.info("Resuming stream.") self.apply_shading_table()
self.start_stream_recording()
else: # If stream was paused to update config, unpause
raise Exception( if paused_stream:
"Cannot update camera config while recording is active.") logging.info("Resuming stream.")
self.start_stream_recording()
else:
raise Exception(
"Cannot update camera config while recording is active.")
def set_zoom(self, zoom_value: float=1.) -> None: def set_zoom(self, zoom_value: float=1.) -> None:
""" """
Change the camera zoom, handling recentering and scaling. Change the camera zoom, handling recentering and scaling.
""" """
self.state['zoom_value'] = float(zoom_value) with self.lock:
if self.state['zoom_value'] < 1: self.state['zoom_value'] = float(zoom_value)
self.state['zoom_value'] = 1 if self.state['zoom_value'] < 1:
# Richard's code for zooming ! self.state['zoom_value'] = 1
fov = self.camera.zoom # Richard's code for zooming !
centre = np.array([fov[0] + fov[2]/2.0, fov[1] + fov[3]/2.0]) fov = self.camera.zoom
size = 1.0/self.state['zoom_value'] centre = np.array([fov[0] + fov[2]/2.0, fov[1] + fov[3]/2.0])
# If the new zoom value would be invalid, move the centre to size = 1.0/self.state['zoom_value']
# keep it within the camera's sensor (this is only relevant # If the new zoom value would be invalid, move the centre to
# when zooming out, if the FoV is not centred on (0.5, 0.5) # keep it within the camera's sensor (this is only relevant
for i in range(2): # when zooming out, if the FoV is not centred on (0.5, 0.5)
if np.abs(centre[i] - 0.5) + size/2 > 0.5: for i in range(2):
centre[i] = 0.5 + (1.0 - size)/2 * np.sign(centre[i]-0.5) if np.abs(centre[i] - 0.5) + size/2 > 0.5:
logging.info("setting zoom, centre {}, size {}".format(centre, size)) centre[i] = 0.5 + (1.0 - size)/2 * np.sign(centre[i]-0.5)
new_fov = (centre[0] - size/2, centre[1] - size/2, size, size) logging.info("setting zoom, centre {}, size {}".format(centre, size))
self.camera.zoom = new_fov new_fov = (centre[0] - size/2, centre[1] - size/2, size, size)
self.camera.zoom = new_fov
# LAUNCH ACTIONS # LAUNCH ACTIONS
@ -299,48 +302,50 @@ class StreamingCamera(BaseCamera):
output_object (str/BytesIO): Target object. output_object (str/BytesIO): Target object.
""" """
# Start recording method only if a current recording is not running with self.lock:
if not self.state['record_active']: # Start recording method only if a current recording is not running
if not self.state['record_active']:
# If output is a StreamObject
if isinstance(output, CaptureObject):
# Lock the StreamObject while recording
output.lock()
# Set target to capture stream
output_stream = output.stream
else:
output_stream = output
# Start the camera video recording on port 2
logging.info("Recording to {}".format(output))
self.camera.start_recording(
output_stream,
format=fmt,
splitter_port=2,
resize=self.config['video_resolution'],
quality=quality)
# Update state dictionary
self.state['record_active'] = True
return output
# If output is a StreamObject
if isinstance(output, CaptureObject):
# Lock the StreamObject while recording
output.lock()
# Set target to capture stream
output_stream = output.stream
else: else:
output_stream = output logging.error(
"Cannot start a new recording\
# Start the camera video recording on port 2 until the current recording has stopped.")
logging.info("Recording to {}".format(output)) return None
self.camera.start_recording(
output_stream,
format=fmt,
splitter_port=2,
resize=self.config['video_resolution'],
quality=quality)
# Update state dictionary
self.state['record_active'] = True
return output
else:
logging.error(
"Cannot start a new recording\
until the current recording has stopped.")
return None
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."""
# Stop the camera video recording on port 2 with self.lock:
logging.info("Stopping recording") # Stop the camera video recording on port 2
self.camera.stop_recording(splitter_port=2) logging.info("Stopping recording")
logging.info("Recording stopped") self.camera.stop_recording(splitter_port=2)
logging.info("Recording stopped")
# Update state dictionary # Update state dictionary
self.state['record_active'] = False self.state['record_active'] = False
def stop_stream_recording( def stop_stream_recording(
self, self,
@ -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,32 +425,33 @@ class StreamingCamera(BaseCamera):
resize ((int, int)): Resize the captured image. resize ((int, int)): Resize the captured image.
""" """
# If output is a StreamObject with self.lock:
if isinstance(output, CaptureObject): # If output is a StreamObject
# Set target to capture stream if isinstance(output, CaptureObject):
output_stream = output.stream # Set target to capture stream
else: output_stream = output.stream
output_stream = output else:
output_stream = output
logging.info("Capturing to {}".format(output)) logging.info("Capturing to {}".format(output))
# Set resolution and stop stream recording if necesarry # Set resolution and stop stream recording if necesarry
if not use_video_port: if not use_video_port:
self.stop_stream_recording() self.stop_stream_recording()
self.camera.capture( self.camera.capture(
output_stream, output_stream,
format=fmt, format=fmt,
quality=100, quality=100,
resize=resize, resize=resize,
bayer=(not use_video_port) and bayer, bayer=(not use_video_port) and bayer,
use_video_port=use_video_port) use_video_port=use_video_port)
# Set resolution and start stream recording if necesarry # Set resolution and start stream recording if necesarry
if not use_video_port: if not use_video_port:
self.start_stream_recording() self.start_stream_recording()
return output return output
def yuv( def yuv(
self, self,
@ -459,38 +464,39 @@ 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.
""" """
if use_video_port: with self.lock:
resolution = self.config['video_resolution'] if use_video_port:
else: resolution = self.config['video_resolution']
resolution = self.config['numpy_resolution'] else:
resolution = self.config['numpy_resolution']
if resize: if resize:
size = resize size = resize
else: else:
size = resolution size = resolution
if not use_video_port:
self.stop_stream_recording(resolution=resolution)
logging.debug("Creating PiYUVArray")
with picamera.array.PiYUVArray(self.camera, size=size) as output:
logging.info("Capturing to {}".format(output))
self.camera.capture(
output,
resize=size,
format='yuv',
use_video_port=use_video_port)
if not use_video_port: if not use_video_port:
self.start_stream_recording() self.stop_stream_recording(resolution=resolution)
if rgb: logging.debug("Creating PiYUVArray")
logging.debug("Converting to RGB") with picamera.array.PiYUVArray(self.camera, size=size) as output:
return output.rgb_array
else: logging.info("Capturing to {}".format(output))
return output.array
self.camera.capture(
output,
resize=size,
format='yuv',
use_video_port=use_video_port)
if not use_video_port:
self.start_stream_recording()
if rgb:
logging.debug("Converting to RGB")
return output.rgb_array
else:
return output.array
def array( def array(
self, self,
@ -502,35 +508,35 @@ 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:
resolution = self.config['video_resolution']
else:
resolution = self.config['numpy_resolution']
if use_video_port: if resize:
resolution = self.config['video_resolution'] size = resize
else: else:
resolution = self.config['numpy_resolution'] size = resolution
if resize: # Always pause stream, to prevent resizer memory issues
size = resize self.stop_stream_recording(resolution=resolution)
else:
size = resolution
# Always pause stream, to prevent resizer memory issues logging.debug("Creating PiRGBArray")
self.stop_stream_recording(resolution=resolution) with picamera.array.PiRGBArray(self.camera, size=size) as output:
logging.debug("Creating PiRGBArray") logging.info("Capturing to {}".format(output))
with picamera.array.PiRGBArray(self.camera, size=size) as output:
logging.info("Capturing to {}".format(output)) self.camera.capture(
output,
resize=size,
format='rgb',
use_video_port=use_video_port)
self.camera.capture( # Resume stream
output, self.start_stream_recording()
resize=size,
format='rgb',
use_video_port=use_video_port)
# Resume stream return output.array
self.start_stream_recording()
return output.array
# HANDLE STREAM FRAMES # HANDLE STREAM FRAMES

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 = []
for _ in range(t_run): print("Acquiring camera lock...")
n_array.append(random.random()) with self.microscope.camera.lock:
time.sleep(1) 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 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: