Merge branch 'full-mock-hardware' into 'master'

Full mock hardware support

See merge request openflexure/openflexure-microscope-server!26
This commit is contained in:
Joel Collins 2019-06-28 20:52:04 +00:00
commit 2276cc683f
12 changed files with 396 additions and 118 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 = (640, 480)
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

@ -186,7 +186,8 @@ class SangaStage(BaseStage):
def close(self):
"""Cleanly close communication with the stage"""
self.board.close()
if hasattr(self, 'board'):
self.board.close()
# Methods specific to Sangaboard
def release_motors(self):

View file

@ -16,18 +16,18 @@ that is read and/or set with a single serial query (i.e. a read followed by a wr
"""
from __future__ import print_function, division
from __future__ import division
import re
from functools import partial
import threading
import serial
import serial.tools.list_ports
from serial import FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS
from serial import PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE
from serial import STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO
import io
import time
import warnings
import logging
class ExtensibleSerialInstrument(object):
"""
@ -54,14 +54,17 @@ class ExtensibleSerialInstrument(object):
ignore_echo = False
port_settings = {}
def __init__(self, port=None, **kwargs):
def __init__(self, port, **kwargs):
"""
Set up the serial port and so on.
"""
logging.info("Updating ESI port settings")
self.port_settings.update(kwargs)
logging.info("Opening ESI connection to port {}".format(port))
self.open(port, False) # Eventually this shouldn't rely on init...
logging.info("Opened ESI connection to port {}".format(port))
def open(self, port=None, quiet=True):
def open(self, port, quiet=True):
"""Open communications with the serial port.
If no port is specified, it will attempt to autodetect. If quiet=True
@ -69,10 +72,9 @@ class ExtensibleSerialInstrument(object):
"""
with self.communications_lock:
if hasattr(self,'_ser') and self._ser.isOpen():
if not quiet: print("Warning: attempted to open an already-open port!")
if not quiet: logging.warning("Attempted to open an already-open port!")
return
if port is None: port=self.find_port()
assert port is not None, "We don't have a serial port to open, meaning you didn't specify a valid port and autodetection failed. Are you sure the instrument is connected?"
assert port is not None, "We don't have a serial port to open, meaning you didn't specify a valid port. Are you sure the instrument is connected?"
self._ser = serial.Serial(port,**self.port_settings)
#the block above wraps the serial IO layer with a text IO layer
#this allows us to read/write in neat lines. NB the buffer size must
@ -81,11 +83,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.warning("The serial port didn't close cleanly:", e)
logging.debug("Connection closed")
def __del__(self):
"""Close the port when the object is deleted
@ -164,7 +168,7 @@ class ExtensibleSerialInstrument(object):
if first_line == queryString:
return self.readline(timeout).strip()
else:
print('This command did not echo!!!')
logging.info('This command did not echo!!!')
return first_line
if termination_line is not None:
@ -238,9 +242,9 @@ class ExtensibleSerialInstrument(object):
else:
return parsed_result
except ValueError:
print("Parsing Error")
print("Matched Groups:", res.groups())
print("Parsing Functions:", parse_function)
logging.error("Parsing Error")
logging.info("Matched Groups:", res.groups())
logging.info("Parsing Functions:", parse_function)
raise ValueError("Stage response to %s ('%s') couldn't be parsed by the supplied function" % (query_string, reply))
def int_query(self, query_string, **kwargs):
"""Perform a query and return the result(s) as integer(s) (see parsedQuery)"""
@ -257,31 +261,6 @@ class ExtensibleSerialInstrument(object):
Usually this function sends a command and checks for a known reply."""
with self.communications_lock:
return True
def find_port(self):
"""Iterate through the available serial ports and query them to see
if our instrument is there."""
with self.communications_lock:
success = False
for port_name, _, _ in serial.tools.list_ports.comports(): #loop through serial ports, apparently 256 is the limit?!
try:
print("Trying port",port_name)
self.open(port_name)
success = True
print("Success!")
except:
pass
finally:
try:
self.close()
except:
pass #we don't care if there's an error closing the port...
if success:
break #again, make sure this happens *after* closing the port
if success:
return port_name
else:
return None
class OptionalModule(object):
"""This allows a `ExtensibleSerialInstrument` to have optional features.
@ -363,7 +342,6 @@ class QueriedProperty(object):
# TODO: standardise the return (single value only vs parsed result), consider bool
def __get__(self, obj, objtype=None):
#print 'get', obj, objtype
if issubclass(type(obj),OptionalModule):
obj.confirm_available()
obj=obj._parent
@ -386,7 +364,6 @@ class QueriedProperty(object):
return value
def __set__(self, obj, value):
#print 'set', obj, value
if issubclass(type(obj),OptionalModule):
obj.confirm_available()
obj=obj._parent

View file

@ -10,8 +10,10 @@ It is (c) Richard Bowman & Julian Stirling 2019 and released under GNU GPL v3
from __future__ import print_function, division
import time
from .extensible_serial_instrument import ExtensibleSerialInstrument, OptionalModule, QueriedProperty, EIGHTBITS, PARITY_NONE, STOPBITS_ONE
import serial, serial.tools.list_ports
import re
import warnings
import logging
class Sangaboard(ExtensibleSerialInstrument):
"""Class managing serial communications with a Sangaboard
@ -28,17 +30,27 @@ class Sangaboard(ExtensibleSerialInstrument):
This class can be used as a context manager, i.e. it's encouraged to use it as::
with Sangaboard() as sb:
sb.move_rel([1000,0,0])
with Sangaboard() as sb:
sb.move_rel([1000,0,0])
In that case, the serial port will automatically be closed at the end of the block,
even if an error occurs. Otherwise, be sure to call the
:meth:`~.ExtensibleSerialInstrument.close()` method to release the serial port.
"""
# List of valid and deprecated firmwares, for communication checks
valid_firmwares = [
(0, 5),
(0, 4)
]
deprecated_firmwares = [
(0, 4)
]
# These are the settings for the sangaboards serial port, and can usually be left as default.
port_settings = {'baudrate':115200, 'bytesize':EIGHTBITS, 'parity':PARITY_NONE, 'stopbits':STOPBITS_ONE}
"""These are the settings for the sangaboards serial port, and can usually be left as default."""
# position, step time and ramp time are get/set using simple serial
# commands.
# position, step time and ramp time are get/set using simple serial commands.
position = QueriedProperty(get_cmd="p?", response_string=r"%d %d %d",
doc="Get the position of the axes as a tuple of 3 integers.")
step_time = QueriedProperty(get_cmd="dt?", set_cmd="dt %d", response_string="minimum step delay %d",
@ -52,10 +64,12 @@ class Sangaboard(ExtensibleSerialInstrument):
"control. Small moves may last less than `2*ramp_time`, in which case the acceleration "
"will be the same, but the motor will never reach full speed. It is saved to EEPROM on "
"the sangaboard, so it will be persistent even if the motor controller is turned off.")
# The names of the sangaboard's axes. NB this also defines the number of axes
axis_names = ('x', 'y', 'z')
"""The names of the sangaboard's axes. NB this also defines the number of axes."""
# Once initialised, `firmware` is a string that identifies the firmware version
firmware = None
"""Once initialised, `firmware` is a string that identifies the firmware version."""
def __init__(self, port=None, **kwargs):
"""Create a sangaboard object.
@ -66,30 +80,18 @@ class Sangaboard(ExtensibleSerialInstrument):
you will use to communicate with the motor controller. That's the first argument so
it doesn't need to be named.
"""
super(Sangaboard, self).__init__(port, **kwargs)
try:
# Request firmware version from the board
self.firmware = self.query("version",timeout=2).rstrip()
# The slightly complicated regexp below will match the version string,
# and store the version number in the "groups" of the regexp. The version
# number should be in the format 1.2 and the groups will be "1.2", "1", "2"
# (for any number of elements).
try:
match = re.match(r"Sangaboard Firmware v(([\d]+)(?:\.([\d]+))+)", self.firmware)
assert match, "Version string \"{}\" not recognised.".format(self.firmware)
except AssertionError:
match = re.match(r"OpenFlexure Motor Board v(([\d]+)(?:\.([\d]+))+)", self.firmware)
assert match, "Version string \"{}\" not recognised.".format(self.firmware)
version = [int(g) for g in match.groups()[1:]]
self.firmware_version = match.group(1)
support_str = "This version of the Python module requires firmware v0.5 (with legacy support for v0.4)"
assert version[0] == 0, support_str
if version[1] == 4:
warnings.warn("Warning this Sangaboard is using v0.4 of the firmware, this will soon be depreciated!\n"+
"Please consider updating the Sangaboard firmware",Warning)
else:
assert version[1] == 5, support_str
# If no port is specified
if not port:
# Scan all available ports, and check for valid firmware
scanned_port = self.scan_ports()
# Initialise basic serial instrument with specified
ExtensibleSerialInstrument.__init__(self, scanned_port, **kwargs)
try:
# Make absolutely sure that whatever port we're on is valid
self.check_valid_firmware()
#Bit messy: Defining all valid modules as not available, then overwriting with available information if available.
self.light_sensor = LightSensor(False)
@ -101,11 +103,11 @@ class Sangaboard(ExtensibleSerialInstrument):
if module_model in self.supported_light_sensors:
self.light_sensor = LightSensor(True,parent=self,model=module_model)
else:
warnings.warn("Light sensor model \"%s\" not recognised."%(module_model),RuntimeWarning)
logging.warning("Light sensor model \"%s\" not recognised."%(module_model))
elif module_type.startswith('Endstops'):
self.endstops = Endstops(True, parent=self, model=module_model)
else:
warnings.warn("Module type \"{}\" not recognised.".format(module_type),RuntimeWarning)
logging.warning("Module type \"{}\" not recognised.".format(module_type))
self.board = self.query("board",timeout=2).rstrip()
@ -113,9 +115,79 @@ class Sangaboard(ExtensibleSerialInstrument):
# If an error occurred while setting up (e.g. because the board isn't connected or something)
# make sure we close the serial port cleanly (otherwise it hangs open).
self.close()
warnings.warn("You may need to update the firmware running on the Sangaboard.")
logging.error(e)
logging.error("You may need to update the firmware running on the Sangaboard.")
raise e
def scan_ports(self):
"""Iterate through the available serial ports and query them to see
if our instrument is there."""
logging.debug("Running Sangaboard port scanner")
with self.communications_lock:
success = False
for port_name, _, _ in serial.tools.list_ports.comports(): #loop through serial ports, apparently 256 is the limit?!
try:
logging.info("Trying port {}".format(port_name))
self.open(port_name)
success = True
logging.info("Checking firmware")
fw = self.check_valid_firmware()
if not fw:
success = False
except Exception as e:
logging.warning("Error on port {}".format(port_name))
logging.warning(e)
pass
finally:
try:
self.close()
except:
pass #we don't care if there's an error closing the port...
if success:
break #again, make sure this happens *after* closing the port
if success:
return port_name
else:
return None
def check_valid_firmware(self):
logging.debug("Running firmware checks")
# Request firmware version from the board
self.firmware = self.query("version",timeout=2).rstrip()
# Check for valid firmware string
if self.firmware:
match = re.match(r"Sangaboard Firmware v(([\d]+)(?:\.([\d]+))+)", self.firmware)
if not match:
# Try old firmware format
match = re.match(r"OpenFlexure Motor Board v(([\d]+)(?:\.([\d]+))+)", self.firmware)
if not match:
logging.error("Version string \"{}\" not recognised.".format(self.firmware))
return False
else:
logging.error("No firmware version string was returned.")
return False
# Check for matching/valid version number
version = [int(g) for g in match.groups()[1:]]
version_tuple = (version[0], version[1])
self.firmware_version = match.group(1)
if version_tuple not in Sangaboard.valid_firmwares:
logging.error("This version of the Python module requires firmware v0.5 (with legacy support for v0.4)")
return False
if version_tuple in Sangaboard.deprecated_firmwares:
logging.warning(
"Warning this Sangaboard is using v0.4 of the firmware, this will soon be depreciated! "
"Please consider updating the Sangaboard firmware"
)
return True
def move_rel(self, displacement, axis=None):
"""Make a relative move.
@ -170,10 +242,12 @@ class LightSensor(OptionalModule):
"""
valid_gains = None
_valid_gains_int = None
integration_time = QueriedProperty(get_cmd="light_sensor_integration_time?",
set_cmd="light_sensor_integration_time %d",
response_string="light sensor integration time %d ms",
doc="Get or set the integration time of the light sensor in milliseconds.")
integration_time = QueriedProperty(
get_cmd="light_sensor_integration_time?",
set_cmd="light_sensor_integration_time %d",
response_string="light sensor integration time %d ms",
doc="Get or set the integration time of the light sensor in milliseconds."
)
intensity = QueriedProperty(get_cmd="light_sensor_intensity?", response_string="%d",
doc="Read the current intensity measured by the light sensor (arbitrary units).")

16
poetry.lock generated
View file

@ -255,11 +255,11 @@ category = "main"
description = "SciPy: Scientific Library for Python"
name = "scipy"
optional = false
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
version = "1.2.1"
python-versions = ">=3.5"
version = "1.3.0"
[package.dependencies]
numpy = ">=1.8.2"
numpy = ">=1.13.3"
[[package]]
category = "main"
@ -271,11 +271,11 @@ version = "1.12.0"
[[package]]
category = "dev"
description = "This package provides 16 stemmer algorithms (15 + Poerter English stemmer) generated from Snowball algorithms."
description = "This package provides 23 stemmers for 22 languages generated from Snowball algorithms."
name = "snowballstemmer"
optional = false
python-versions = "*"
version = "1.2.1"
version = "1.9.0"
[[package]]
category = "dev"
@ -381,7 +381,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.15.4"
[metadata]
content-hash = "14a52bee3012c7880e4c87e940639606eb88ad7320e262d5228566b6df1ef601"
content-hash = "ec0d5aa30422b8f44fb92e8e818120d4f0e1b084773594ce2531e31db5af26f4"
python-versions = "^3.5"
[metadata.hashes]
@ -412,9 +412,9 @@ pytz = ["303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", "d74
pyyaml = ["3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", "3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", "40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", "558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", "a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", "aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", "bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", "d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", "d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", "e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", "e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"]
requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"]
"rpi.gpio" = ["a4210ad63bfe844e43995286de0d3950dfacfa0f3799bb9392770ac54a7d2e47"]
scipy = ["014cb900c003b5ac81a53f2403294e8ecf37aedc315b59a6b9370dce0aa7627a", "281a34da34a5e0de42d26aed692ab710141cad9d5d218b20643a9cb538ace976", "588f9cc4bfab04c45fbd19c1354b5ade377a8124d6151d511c83730a9b6b2338", "5a10661accd36b6e2e8855addcf3d675d6222006a15795420a39c040362def66", "628f60be272512ca1123524969649a8cb5ae8b31cca349f7c6f8903daf9034d7", "6dcc43a88e25b815c2dea1c6fac7339779fc988f5df8396e1de01610604a7c38", "70e37cec0ac0fe95c85b74ca4e0620169590fd5d3f44765f3c3a532cedb0e5fd", "7274735fb6fb5d67d3789ddec2cd53ed6362539b41aa6cc0d33a06c003aaa390", "78e12972e144da47326958ac40c2bd1c1cca908edc8b01c26a36f9ffd3dce466", "790cbd3c8d09f3a6d9c47c4558841e25bac34eb7a0864a9def8f26be0b8706af", "79792c8fe8e9d06ebc50fe23266522c8c89f20aa94ac8e80472917ecdce1e5ba", "865afedf35aaef6df6344bee0de391ee5e99d6e802950a237f9fb9b13e441f91", "870fd401ec7b64a895cff8e206ee16569158db00254b2f7157b4c9a5db72c722", "963815c226b29b0176d5e3d37fc9de46e2778ce4636a5a7af11a48122ef2577c", "9726791484f08e394af0b59eb80489ad94d0a53bbb58ab1837dcad4d58489863", "9de84a71bb7979aa8c089c4fb0ea0e2ed3917df3fb2a287a41aaea54bbad7f5d", "b2c324ddc5d6dbd3f13680ad16a29425841876a84a1de23a984236d1afff4fa6", "b86ae13c597fca087cb8c193870507c8916cefb21e52e1897da320b5a35075e5", "ba0488d4dbba2af5bf9596b849873102d612e49a118c512d9d302ceafa36e01a", "d78702af4102a3a4e23bb7372cec283e78f32f5573d92091aa6aaba870370fe1", "def0e5d681dd3eb562b059d355ae8bebe27f5cc455ab7c2b6655586b63d3a8ea", "e085d1babcb419bbe58e2e805ac61924dac4ca45a07c9fa081144739e500aa3c", "e2cfcbab37c082a5087aba5ff00209999053260441caadd4f0e8f4c2d6b72088", "e742f1f5dcaf222e8471c37ee3d1fd561568a16bb52e031c25674ff1cf9702d5", "f06819b028b8ef9010281e74c59cb35483933583043091ed6b261bb1540f11cc", "f15f2d60a11c306de7700ee9f65df7e9e463848dbea9c8051e293b704038da60", "f31338ee269d201abe76083a990905473987371ff6f3fdb76a3f9073a361cf37", "f6b88c8d302c3dac8dff7766955e38d670c82e0d79edfc7eae47d6bb2c186594"]
scipy = ["03b1e0775edbe6a4c64effb05fff2ce1429b76d29d754aa5ee2d848b60033351", "09d008237baabf52a5d4f5a6fcf9b3c03408f3f61a69c404472a16861a73917e", "10325f0ffac2400b1ec09537b7e403419dcd25d9fee602a44e8a32119af9079e", "1db9f964ed9c52dc5bd6127f0dd90ac89791daa690a5665cc01eae185912e1ba", "409846be9d6bdcbd78b9e5afe2f64b2da5a923dd7c1cd0615ce589489533fdbb", "4907040f62b91c2e170359c3d36c000af783f0fa1516a83d6c1517cde0af5340", "6c0543f2fdd38dee631fb023c0f31c284a532d205590b393d72009c14847f5b1", "826b9f5fbb7f908a13aa1efd4b7321e36992f5868d5d8311c7b40cf9b11ca0e7", "a7695a378c2ce402405ea37b12c7a338a8755e081869bd6b95858893ceb617ae", "a84c31e8409b420c3ca57fd30c7589378d6fdc8d155d866a7f8e6e80dec6fd06", "adadeeae5500de0da2b9e8dd478520d0a9945b577b2198f2462555e68f58e7ef", "b283a76a83fe463c9587a2c88003f800e08c3929dfbeba833b78260f9c209785", "c19a7389ab3cd712058a8c3c9ffd8d27a57f3d84b9c91a931f542682bb3d269d", "c3bb4bd2aca82fb498247deeac12265921fe231502a6bc6edea3ee7fe6c40a7a", "c5ea60ece0c0c1c849025bfc541b60a6751b491b6f11dd9ef37ab5b8c9041921", "db61a640ca20f237317d27bc658c1fc54c7581ff7f6502d112922dc285bdabee"]
six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"]
snowballstemmer = ["919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", "9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"]
snowballstemmer = ["9f3b9ffe0809d174f7047e121431acf99c89a7040f0ca84f94ba53a498e6d0c9"]
sphinx = ["22538e1bbe62b407cf5a8aabe1bb15848aa66bb79559f42f5202bbce6b757a69", "f9a79e746b87921cabc3baa375199c6076d1270cee53915dbd24fdbeaaacc427"]
sphinxcontrib-applehelp = ["edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", "fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"]
sphinxcontrib-devhelp = ["6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", "9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"]

View file

@ -28,12 +28,12 @@ Flask = "^1.0"
pyyaml = "^3.13"
numpy = "1.16.4" # Exact version until we can guarantee a wheel
Pillow = "^5.4"
"RPi.GPIO" = "^0.6.5"
openflexure-stage = "^0.2.1"
flask-cors = "^3.0"
scipy = "1.3.0" # Exact version until we can guarantee a wheel
picamera = { git = "https://github.com/rwb27/picamera.git", branch = "lens-shading" } # Lens shading requires >1.14 or RWB fork, lens-shading branch.
"RPi.GPIO" = { version = "^0.6.5", optional = true }
[tool.poetry.dev-dependencies]
sphinxcontrib-httpdomain = "^1.7"