Merge branch 'full-mock-hardware' into 'master'
Full mock hardware support See merge request openflexure/openflexure-microscope-server!26
This commit is contained in:
commit
2276cc683f
12 changed files with 396 additions and 118 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 = (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")
|
||||
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
16
poetry.lock
generated
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue