Added support for a fully mocked device

This commit is contained in:
jtc42 2019-06-28 12:52:55 +01:00
parent eaa37f70e0
commit 4733b250ed
12 changed files with 283 additions and 41 deletions

View file

@ -12,14 +12,17 @@ from openflexure_microscope.api.exceptions import JSONExceptionHandler
from openflexure_microscope.api.utilities import list_routes
from openflexure_microscope import Microscope
from openflexure_microscope.camera.pi import StreamingCamera
try:
from openflexure_microscope.camera.pi import StreamingCamera
except ImportError:
from openflexure_microscope.camera.mock import StreamingCamera
from openflexure_microscope.stage.sanga import SangaStage
from openflexure_microscope.stage.mock import MockStage
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 time
@ -101,7 +104,7 @@ def attach_microscope():
# TODO: Tidy this up. api_stage may be referenced before assignment. Use some form of Maybe monad?
try:
api_stage = SangaStage()
except (SerialException, OSError) as e:
except Exception as e:
logging.error(e)
logging.warning("No valid stage hardware found. Falling back to mock stage!")
api_stage = MockStage()

View file

@ -1,3 +0,0 @@
__all__ = ['pi', 'base']
from . import pi, base

View file

@ -167,15 +167,6 @@ class BaseCamera(metaclass=ABCMeta):
self.stop_worker()
logging.info("Closed {}".format(self))
def wait_for_camera(self, timeout=5):
"""Wait for camera object, with 5 second timeout."""
timeout_time = time.time() + timeout
while not self.camera:
if time.time() > timeout_time:
raise TimeoutError("Timeout waiting for camera")
else:
pass
# START AND STOP WORKER THREAD
def start_worker(self, timeout: int = 5) -> bool:

View file

@ -0,0 +1,217 @@
# -*- coding: utf-8 -*-
"""
"""
from __future__ import division
import io
import time
import numpy as np
from PIL import Image
import logging
# Type hinting
from typing import Tuple
from .base import BaseCamera, CaptureObject
# MAIN CLASS
class StreamingCamera(BaseCamera):
def __init__(self):
# Run BaseCamera init
BaseCamera.__init__(self)
# Store state of StreamingCamera
self.state.update({
'stream_active': False,
'record_active': False
})
# Update config properties
self.image_resolution = (1312, 976)
self.stream_resolution = (832, 624)
self.numpy_resolution = (1312, 976)
self.jpeg_quality = 75
self.framerate = 10
# Create an empty stream
self.stream = io.BytesIO()
# Start streaming
self.start_worker()
def initialisation(self):
"""Run any initialisation code when the frame iterator starts."""
pass
def close(self):
"""Close the Raspberry Pi StreamingCamera."""
# Run BaseCamera close method
BaseCamera.close(self)
# HANDLE SETTINGS
def read_config(self) -> dict:
"""
Return config dictionary of the StreamingCamera.
"""
conf_dict = {
'stream_resolution': self.stream_resolution,
'image_resolution': self.image_resolution,
'numpy_resolution': self.numpy_resolution,
'jpeg_quality': self.jpeg_quality
}
return conf_dict
def apply_config(self, config: dict):
"""
Write a config dictionary to the StreamingCamera config.
The passed dictionary may contain other parameters not relevant to
camera config. Eg. Passing a general config file will work fine.
Args:
config (dict): Dictionary of config parameters.
"""
# TODO: Include timing and batching logic when applying PiCamera settings
paused_stream = False
logging.debug("StreamingCamera: Applying config:")
logging.debug(config)
with self.lock:
# Apply valid config params to Picamera object
if not self.state['record_active']: # If not recording a video
# StreamingCamera parameters
for key, value in config.items(): # For each provided setting
if hasattr(self, key):
setattr(self, key, value)
# If stream was paused to update config, unpause
if paused_stream:
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:
"""
Change the camera zoom, handling re-centering and scaling.
"""
logging.warning("Zoom not implemented in mock camera")
# LAUNCH ACTIONS
def start_preview(self, fullscreen=True, window=None):
"""Start the on board GPU camera preview."""
logging.warning("GPU preview not implemented in mock camera")
def stop_preview(self):
"""Stop the on board GPU camera preview."""
logging.warning("GPU preview not implemented in mock camera")
def start_recording(
self,
output,
fmt: str = 'h264',
quality: int = 15):
"""Start recording.
Start a new video recording, writing to a output object.
Args:
output (CaptureObject/str): Output object to write data bytes to.
fmt (str): Format of the capture.
quality (int): Video recording quality.
Returns:
output_object (str/BytesIO): Target object.
"""
with self.lock:
# Start recording method only if a current recording is not running
logging.warning("Recording not implemented in mock camera")
def stop_recording(self):
"""Stop the last started video recording on splitter port 2."""
with self.lock:
logging.warning("Recording not implemented in mock camera")
def capture(
self,
output,
fmt: str = 'jpeg',
use_video_port: bool = False,
resize: Tuple[int, int] = None,
bayer: bool = True):
"""
Capture a still image to a StreamObject.
Defaults to JPEG format.
Target object can be overridden for development purposes.
Args:
output (CaptureObject/str): Output object to write data bytes to.
use_video_port (bool): Capture from the video port used for streaming. Lower resolution, faster.
fmt (str): Format of the capture.
resize ((int, int)): Resize the captured image.
bayer (bool): Store raw bayer data in capture
"""
with self.lock:
logging.warning("Capture not implemented in mock camera")
def gen_img(self):
imarray = np.random.rand(self.stream_resolution[1], self.stream_resolution[0], 3) * 255
im = Image.fromarray(imarray.astype('uint8')).convert('L')
im.save(self.stream, format="JPEG")
# HANDLE STREAM FRAMES
def frames(self):
"""
Create generator that returns frames from the camera.
Records video from port 1 to a byte stream,
and iterates sequential frames.
"""
# Run this initialisation method
self.initialisation()
# Update state
logging.debug("STREAM ACTIVE")
# While the iterator is not closed
try:
while True:
# reset stream for next frame
self.stream.seek(0)
self.stream.truncate()
# to stream, read the new frame
time.sleep(1 / self.framerate * 0.1)
# yield the result to be read
self.gen_img()
frame = self.stream.getvalue()
# ensure the size of package is right
if len(frame) == 0:
pass
else:
yield frame
# When GeneratorExit or StopIteration raised, run cleanup code
finally:
logging.debug("FRAME ITERATOR END")

View file

@ -529,6 +529,15 @@ class StreamingCamera(BaseCamera):
# HANDLE STREAM FRAMES
def wait_for_camera(self, timeout=5):
"""Wait for camera object, with 5 second timeout."""
timeout_time = time.time() + timeout
while not self.camera:
if time.time() > timeout_time:
raise TimeoutError("Timeout waiting for camera")
else:
pass
def frames(self):
"""
Create generator that returns frames from the camera.

View file

@ -1,8 +1,8 @@
import numpy as np
from picamera import PiCamera
from picamera.array import PiRGBArray, PiBayerArray
import time
from picamera import PiCamera
from picamera.array import PiRGBArray, PiBayerArray
def rgb_image(camera, resize=None, **kwargs):
"""Capture an image and return an RGB numpy array"""

View file

@ -78,8 +78,13 @@ def load_plugin_module(plugin_path):
# If the loader was found (i.e. plugin probably exists)
if check_module(plugin_path):
plugin_module = importlib.import_module(plugin_path)
plugin_name = name_from_module(plugin_path)
try:
plugin_module = importlib.import_module(plugin_path)
plugin_name = name_from_module(plugin_path)
except Exception as e:
logging.error("Error loading plugin.")
logging.error(e)
return None, None
# If no loader was found, try finding a file from path
else:
@ -156,24 +161,28 @@ class PluginMount(object):
"""
plugin_class, plugin_name = class_from_map(plugin_map)
pythonsafe_plugin_name = plugin_name.replace("/", "_")
if plugin_class is not None:
if plugin_class and plugin_name:
plugin_object = plugin_class()
pythonsafe_plugin_name = plugin_name.replace("/", "_")
if hasattr(self, plugin_name): # If a plugin with the same name is already attached.
logging.warning(ConColors.WARNING + "A plugin named {} has already been loaded. Skipping {}.".format(plugin_name, plugin_map) + ConColors.ENDC)
if plugin_class and plugin_name:
plugin_object = plugin_class()
elif isinstance(plugin_object, MicroscopePlugin): # If plugin_object is an instance of MicroscopePlugin
# Attach plugin_object to the plugin mount
setattr(self, pythonsafe_plugin_name, plugin_object)
self.plugins.append((plugin_name, plugin_object))
if hasattr(self, plugin_name): # If a plugin with the same name is already attached.
logging.warning(ConColors.WARNING + "A plugin named {} has already been loaded. Skipping {}.".format(plugin_name, plugin_map) + ConColors.ENDC)
# Grant plugin access to the hardware
plugin_object.microscope = self.parent
elif isinstance(plugin_object, MicroscopePlugin): # If plugin_object is an instance of MicroscopePlugin
# Attach plugin_object to the plugin mount
setattr(self, pythonsafe_plugin_name, plugin_object)
self.plugins.append((plugin_name, plugin_object))
logging.info(ConColors.OKGREEN + "Plugin {} loaded as {}.".format(plugin_map, plugin_name) + ConColors.ENDC)
# Grant plugin access to the hardware
plugin_object.microscope = self.parent
logging.info(ConColors.OKGREEN + "Plugin {} loaded as {}.".format(plugin_map, plugin_name) + ConColors.ENDC)
else:
logging.error("Error loading plugin. Moving on.")
class MicroscopePlugin:
"""

View file

@ -28,6 +28,7 @@ from serial import STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO
import io
import time
import warnings
import logging
class ExtensibleSerialInstrument(object):
"""
@ -58,8 +59,11 @@ class ExtensibleSerialInstrument(object):
"""
Set up the serial port and so on.
"""
logging.info("Updating BSI port settings")
self.port_settings.update(kwargs)
logging.info("Opening BSI connection to port {}".format(port))
self.open(port, False) # Eventually this shouldn't rely on init...
logging.info("Opened BSI connection to port {}".format(port))
def open(self, port=None, quiet=True):
"""Open communications with the serial port.
@ -81,11 +85,13 @@ class ExtensibleSerialInstrument(object):
def close(self):
"""Release the serial port"""
logging.debug("Closing serial connection")
with self.communications_lock:
try:
self._ser.close()
except Exception as e:
print("The serial port didn't close cleanly:", e)
logging.debug("Connection closed")
def __del__(self):
"""Close the port when the object is deleted

View file

@ -12,6 +12,7 @@ import time
from .extensible_serial_instrument import ExtensibleSerialInstrument, OptionalModule, QueriedProperty, EIGHTBITS, PARITY_NONE, STOPBITS_ONE
import re
import warnings
import logging
class Sangaboard(ExtensibleSerialInstrument):
"""Class managing serial communications with a Sangaboard
@ -68,6 +69,7 @@ class Sangaboard(ExtensibleSerialInstrument):
"""
super(Sangaboard, self).__init__(port, **kwargs)
try:
logging.debug("Running firmware checks")
# Request firmware version from the board
self.firmware = self.query("version",timeout=2).rstrip()
# The slightly complicated regexp below will match the version string,