Added support for a fully mocked device
This commit is contained in:
parent
eaa37f70e0
commit
4733b250ed
12 changed files with 283 additions and 41 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
__all__ = ['pi', 'base']
|
||||
|
||||
from . import pi, base
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
217
openflexure_microscope/camera/mock.py
Normal file
217
openflexure_microscope/camera/mock.py
Normal 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")
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue