This commit is contained in:
Joel Collins 2019-07-16 17:02:57 +01:00
commit 57832f94a8
10 changed files with 130 additions and 112 deletions

View file

@ -1,5 +1,11 @@
#!/usr/bin/env python
import time
import atexit
import logging
import sys
import os
from flask import (
Flask, jsonify, send_file)
@ -13,23 +19,22 @@ from openflexure_microscope.api.utilities import list_routes
from openflexure_microscope import Microscope
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
import atexit
import logging
import sys
import os
# Import device modules
# NB this will eventually be handled by the RC file, so you can choose what device
# class should be attached.
try:
from openflexure_microscope.camera.pi import PiCameraStreamer
except ImportError:
logging.warning("Unable to import PiCameraStreamer")
from openflexure_microscope.camera.mock import MockStreamer
from openflexure_microscope.stage.sanga import SangaStage
from openflexure_microscope.stage.mock import MockStage
# Handle logging
is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
@ -96,30 +101,37 @@ def attach_microscope():
global api_microscope, stored_image_list
logging.debug("First request made. Populating microscope with hardware...")
# Initialise camera
logging.debug("Creating camera object...")
# TODO: Try except finally, like with stage
api_camera = StreamingCamera()
try:
api_camera = PiCameraStreamer()
except Exception as e:
logging.error(e)
logging.warning("No valid camera hardware found. Falling back to mock camera!")
api_camera = MockStreamer()
# Initialise stage
logging.debug("Creating stage object...")
# TODO: Tidy this up. api_stage may be referenced before assignment. Use some form of Maybe monad?
try:
api_stage = SangaStage()
except Exception as e:
logging.error(e)
logging.warning("No valid stage hardware found. Falling back to mock stage!")
api_stage = MockStage()
finally:
logging.debug("Attaching devices to microscope...")
api_microscope.attach(
api_camera,
api_stage
)
logging.debug("Restoring captures...")
if stored_image_list:
api_microscope.camera.images = stored_image_list
# Attach devices to microscope
logging.debug("Attaching devices to microscope...")
api_microscope.attach(
api_camera,
api_stage
)
logging.debug("Microscope successfully attached!")
# Restore loaded capture array to camera object
logging.debug("Restoring captures...")
if stored_image_list:
api_microscope.camera.images = stored_image_list
logging.debug("Microscope successfully attached!")
# WEBAPP ROUTES

View file

@ -19,13 +19,13 @@ from .base import BaseCamera, CaptureObject
# MAIN CLASS
class StreamingCamera(BaseCamera):
class MockStreamer(BaseCamera):
def __init__(self):
# Run BaseCamera init
BaseCamera.__init__(self)
# Store state of StreamingCamera
# Store state of PiCameraStreamer
self.state.update({
'stream_active': False,
'record_active': False
@ -49,14 +49,14 @@ class StreamingCamera(BaseCamera):
pass
def close(self):
"""Close the Raspberry Pi StreamingCamera."""
"""Close the Raspberry Pi PiCameraStreamer."""
# Run BaseCamera close method
BaseCamera.close(self)
# HANDLE SETTINGS
def read_config(self) -> dict:
"""
Return config dictionary of the StreamingCamera.
Return config dictionary of the PiCameraStreamer.
"""
conf_dict = {
@ -70,7 +70,7 @@ class StreamingCamera(BaseCamera):
def apply_config(self, config: dict):
"""
Write a config dictionary to the StreamingCamera config.
Write a config dictionary to the PiCameraStreamer config.
The passed dictionary may contain other parameters not relevant to
camera config. Eg. Passing a general config file will work fine.
@ -81,7 +81,7 @@ class StreamingCamera(BaseCamera):
# TODO: Include timing and batching logic when applying PiCamera settings
paused_stream = False
logging.debug("StreamingCamera: Applying config:")
logging.debug("PiCameraStreamer: Applying config:")
logging.debug(config)
with self.lock:
@ -89,7 +89,7 @@ class StreamingCamera(BaseCamera):
# Apply valid config params to Picamera object
if not self.state['record_active']: # If not recording a video
# StreamingCamera parameters
# PiCameraStreamer parameters
for key, value in config.items(): # For each provided setting
if hasattr(self, key):
setattr(self, key, value)

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
"""
Raspberry Pi camera implementation of the StreamingCamera class.
Raspberry Pi camera implementation of the PiCameraStreamer class.
NOTES:
Still port used for image capture
@ -12,7 +12,7 @@ Video port:
Splitter port 2: Video capture
Splitter port 3: [Currently unused]
StreamingCamera streams at video_resolution
PiCameraStreamer streams at video_resolution
Camera capture resolution set to stream_resolution in frames()
Video port uses that resolution for everything. If a different resolution
is specified for video capture, this is handled by the resizer.
@ -41,8 +41,8 @@ from .set_picamera_gain import set_analog_gain, set_digital_gain
# MAIN CLASS
class StreamingCamera(BaseCamera):
"""Raspberry Pi camera implementation of StreamingCamera."""
class PiCameraStreamer(BaseCamera):
"""Raspberry Pi camera implementation of PiCameraStreamer."""
picamera_settings_keys = [
'exposure_mode',
'analog_gain',
@ -61,7 +61,7 @@ class StreamingCamera(BaseCamera):
# Attach to Pi camera
self.camera = picamera.PiCamera() #: :py:class:`picamera.PiCamera`: Picamera object
# Store state of StreamingCamera
# Store state of PiCameraStreamer
self.state.update({
'stream_active': False,
'record_active': False,
@ -87,7 +87,7 @@ class StreamingCamera(BaseCamera):
pass
def close(self):
"""Close the Raspberry Pi StreamingCamera."""
"""Close the Raspberry Pi PiCameraStreamer."""
# Run BaseCamera close method
BaseCamera.close(self)
# Detach Pi camera
@ -97,7 +97,7 @@ class StreamingCamera(BaseCamera):
# HANDLE SETTINGS
def read_config(self) -> dict:
"""
Return config dictionary of the StreamingCamera.
Return config dictionary of the PiCameraStreamer.
"""
conf_dict = {
@ -109,7 +109,7 @@ class StreamingCamera(BaseCamera):
}
# PiCamera parameters
for key in StreamingCamera.picamera_settings_keys:
for key in PiCameraStreamer.picamera_settings_keys:
try:
value = getattr(self.camera, key)
logging.debug("Reading PiCamera().{}: {}".format(key, value))
@ -121,7 +121,7 @@ class StreamingCamera(BaseCamera):
def apply_config(self, config: dict):
"""
Write a config dictionary to the StreamingCamera config.
Write a config dictionary to the PiCameraStreamer config.
The passed dictionary may contain other parameters not relevant to
camera config. Eg. Passing a general config file will work fine.
@ -132,7 +132,7 @@ class StreamingCamera(BaseCamera):
# TODO: Include timing and batching logic when applying PiCamera settings
paused_stream = False
logging.debug("StreamingCamera: Applying config:")
logging.debug("PiCameraStreamer: Applying config:")
logging.debug(config)
with self.lock:
@ -150,7 +150,7 @@ class StreamingCamera(BaseCamera):
if 'picamera_settings' in config: # If new settings are given
self.apply_picamera_settings(config['picamera_settings'], pause_for_effect=True)
# StreamingCamera parameters
# PiCameraStreamer parameters
for key, value in config.items(): # For each provided setting
if (key != 'picamera_settings') and hasattr(self, key):
setattr(self, key, value)

View file

@ -194,13 +194,6 @@ class SangaStage(BaseStage):
"""De-energise the stepper motor coils"""
self.board.release_motors()
def __del__(self):
"""Close the port when the object is deleted
NB if the object is created in a with statement, this will cause
the port to be closed at the end of the with block."""
self.close()
def __enter__(self):
"""When we use this in a with statement, remember where we started."""
self._position_on_enter = self.position

View file

@ -26,8 +26,8 @@ from serial import PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPA
from serial import STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO
import io
import time
import warnings
import logging
import warnings
class ExtensibleSerialInstrument(object):
"""
@ -54,7 +54,7 @@ class ExtensibleSerialInstrument(object):
ignore_echo = False
port_settings = {}
def __init__(self, port, **kwargs):
def __init__(self, port=None, **kwargs):
"""
Set up the serial port and so on.
"""
@ -64,7 +64,7 @@ class ExtensibleSerialInstrument(object):
self.open(port, False) # Eventually this shouldn't rely on init...
logging.info("Opened ESI connection to port {}".format(port))
def open(self, port, quiet=True):
def open(self, port=None, quiet=True):
"""Open communications with the serial port.
If no port is specified, it will attempt to autodetect. If quiet=True
@ -74,6 +74,8 @@ class ExtensibleSerialInstrument(object):
if hasattr(self,'_ser') and self._ser.isOpen():
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. 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
@ -82,28 +84,33 @@ class ExtensibleSerialInstrument(object):
assert self.test_communications(), "The instrument doesn't seem to be responding. Did you specify the right port?"
def close(self):
"""Release the serial port"""
"""Cleanly close the device. Includes proper logging statements."""
logging.debug("Closing serial connection")
with self.communications_lock:
try:
self._ser.close()
except Exception as e:
logging.warning("The serial port didn't close cleanly:", e)
logging.debug("Connection closed")
logging.warning("The serial port didn't close cleanly: {}".format(e))
logging.debug("Connection closed")
def __del__(self):
"""Close the port when the object is deleted
NB if the object is created in a with statement, this will cause
the port to be closed at the end of the with block."""
self.close()
"""Emergency close the device. Try to avoid having to use this."""
if hasattr(self, '_ser') and self._ser.isOpen():
with self.communications_lock:
print(
"Closing an open serial communication has been triggered by garbage collection!\n"\
"Please close this device more sensibly in future.")
try:
self._ser.close()
except Exception as e:
print("The serial port didn't close cleanly: {}".format(e))
def __enter__(self):
return self
def __exit__(self, type, value, traceback):
"""Close down the instrument. This happens in __del__ though."""
pass
"""Cleanly close down the instrument at end of a with block."""
self.close()
def write(self,query_string):
"""Write a string to the serial port"""
@ -232,7 +239,7 @@ class ExtensibleSerialInstrument(object):
reply = self.readline().strip()
res=re.search(response_regex, reply, flags=re_flags)
if res is not None:
warnings.warn("Query suceeded after initially receieving unmatched response ('%s') to '%s'. Match pattern /%s/ (generated regex /%s/)"%(original_reply, query_string, response_string, response_regex),RuntimeWarning)
warnings.warn("Query suceeded after initially receieving unmatched response ('%s') to '%s'. Match pattern /%s/ (generated regex /%s/)"%(original_reply, query_string, response_string, response_regex), RuntimeWarning)
else:
raise ValueError("Stage response to '%s' ('%s') wasn't matched by /%s/ (generated regex /%s/)" % (query_string, original_reply, response_string, response_regex))
try:
@ -242,9 +249,8 @@ class ExtensibleSerialInstrument(object):
else:
return parsed_result
except ValueError:
logging.error("Parsing Error")
logging.info("Matched Groups:", res.groups())
logging.info("Parsing Functions:", parse_function)
logging.info("Matched Groups: {}".format(res.groups()))
logging.info("Parsing Functions {}:".format(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)"""
@ -261,7 +267,32 @@ 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:
logging.info("Trying port {}".format(port_name))
self.open(port_name)
success = True
logging.info("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.

View file

@ -81,13 +81,8 @@ class Sangaboard(ExtensibleSerialInstrument):
it doesn't need to be named.
"""
# 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)
ExtensibleSerialInstrument.__init__(self, port, **kwargs)
try:
# Make absolutely sure that whatever port we're on is valid
@ -119,37 +114,11 @@ class Sangaboard(ExtensibleSerialInstrument):
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 test_communications(self):
"""
Overrides superclass, used in self.open(), and port scanning
"""
return self.check_valid_firmware()
def check_valid_firmware(self):
logging.debug("Running firmware checks")

12
poetry.lock generated
View file

@ -180,6 +180,7 @@ version = "1.13.1b0"
reference = "b39c2b6e42f5f7f57bb46eafcb5c9e2bbdb5d0cb"
type = "git"
url = "https://github.com/rwb27/picamera.git"
[[package]]
category = "main"
description = "Python Imaging Library (Fork)"
@ -242,6 +243,14 @@ chardet = ">=3.0.2,<3.1.0"
idna = ">=2.5,<2.9"
urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26"
[[package]]
category = "dev"
description = "a python refactoring library..."
name = "rope"
optional = false
python-versions = "*"
version = "0.14.0"
[[package]]
category = "main"
description = "A module to control Raspberry Pi GPIO channels"
@ -381,7 +390,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.15.4"
[metadata]
content-hash = "33585d30bda3df47511879542dd7c778bb1922947b650a5554c231a0be7c8b4a"
content-hash = "c2fee81479442877ae94095d15f05018dc5290eee42edeb7d79d067fdf0ffab6"
python-versions = "^3.5"
[metadata.hashes]
@ -411,6 +420,7 @@ pyserial = ["6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627",
pytz = ["303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", "d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"]
pyyaml = ["3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", "3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", "40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", "558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", "a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", "aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", "bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", "d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", "d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", "e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", "e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"]
requests = ["11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"]
rope = ["6b728fdc3e98a83446c27a91fc5d56808a004f8beab7a31ab1d7224cecc7d969", "c5c5a6a87f7b1a2095fb311135e2a3d1f194f5ecb96900fdd0a9100881f48aaf", "f0dcf719b63200d492b85535ebe5ea9b29e0d0b8aebeb87fe03fc1a65924fdaf"]
"rpi.gpio" = ["a4210ad63bfe844e43995286de0d3950dfacfa0f3799bb9392770ac54a7d2e47"]
scipy = ["03b1e0775edbe6a4c64effb05fff2ce1429b76d29d754aa5ee2d848b60033351", "09d008237baabf52a5d4f5a6fcf9b3c03408f3f61a69c404472a16861a73917e", "10325f0ffac2400b1ec09537b7e403419dcd25d9fee602a44e8a32119af9079e", "1db9f964ed9c52dc5bd6127f0dd90ac89791daa690a5665cc01eae185912e1ba", "409846be9d6bdcbd78b9e5afe2f64b2da5a923dd7c1cd0615ce589489533fdbb", "4907040f62b91c2e170359c3d36c000af783f0fa1516a83d6c1517cde0af5340", "6c0543f2fdd38dee631fb023c0f31c284a532d205590b393d72009c14847f5b1", "826b9f5fbb7f908a13aa1efd4b7321e36992f5868d5d8311c7b40cf9b11ca0e7", "a7695a378c2ce402405ea37b12c7a338a8755e081869bd6b95858893ceb617ae", "a84c31e8409b420c3ca57fd30c7589378d6fdc8d155d866a7f8e6e80dec6fd06", "adadeeae5500de0da2b9e8dd478520d0a9945b577b2198f2462555e68f58e7ef", "b283a76a83fe463c9587a2c88003f800e08c3929dfbeba833b78260f9c209785", "c19a7389ab3cd712058a8c3c9ffd8d27a57f3d84b9c91a931f542682bb3d269d", "c3bb4bd2aca82fb498247deeac12265921fe231502a6bc6edea3ee7fe6c40a7a", "c5ea60ece0c0c1c849025bfc541b60a6751b491b6f11dd9ef37ab5b8c9041921", "db61a640ca20f237317d27bc658c1fc54c7581ff7f6502d112922dc285bdabee"]
six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"]

View file

@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
[tool.poetry]
name = "openflexure_microscope"
version = "1.1.0"
version = "1.1.1"
description = "Python module, and Flask-based web API, to run the OpenFlexure Microscope."
authors = [
@ -37,3 +37,4 @@ picamera = { git = "https://github.com/rwb27/picamera.git", branch = "lens-shadi
[tool.poetry.dev-dependencies]
sphinxcontrib-httpdomain = "^1.7"
rope = "^0.14.0"

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python
from openflexure_microscope.camera.pi import StreamingCamera, CaptureObject
from openflexure_microscope.camera.pi import PiCameraStreamer, CaptureObject
import os
import io
@ -279,7 +279,7 @@ class TestThreadStarting(unittest.TestCase):
if __name__ == '__main__':
with StreamingCamera() as camera:
with PiCameraStreamer() as camera:
suites = [
unittest.TestLoader().loadTestsFromTestCase(TestCaptureMethods),

View file

@ -1,5 +1,5 @@
#!/usr/bin/env python
from openflexure_microscope.camera.pi import StreamingCamera
from openflexure_microscope.camera.pi import PiCameraStreamer
from openflexure_stage import OpenFlexureStage
from openflexure_microscope import Microscope, config
@ -31,7 +31,9 @@ class TestPluginMethods(unittest.TestCase):
if __name__ == '__main__':
with Microscope(StreamingCamera(), OpenFlexureStage("/dev/ttyUSB0")) as microscope:
with Microscope() as microscope:
microscope.attach(PiCameraStreamer(), OpenFlexureStage())
microscope.plugin.attach("openflexure_microscope.plugins.testing:Plugin")