Blackened everything

This commit is contained in:
Joel Collins 2019-09-15 14:17:52 +01:00
parent e213647217
commit 5966ce29be
57 changed files with 1938 additions and 1414 deletions

View file

@ -1,3 +1,3 @@
__all__ = ['base', 'mock', 'sanga']
__all__ = ["base", "mock", "sanga"]
from . import base, mock, sanga

View file

@ -8,6 +8,7 @@ class BaseStage(metaclass=ABCMeta):
lock (:py:class:`openflexure_microscope.lock.StrictLock`): Strict lock controlling thread
access to camera hardware
"""
def __init__(self):
self.lock = StrictLock(timeout=5)

View file

@ -4,6 +4,7 @@ from openflexure_microscope.utilities import axes_to_array
from collections.abc import Iterable
import numpy as np
class MockStage(BaseStage):
def __init__(self, port=None, **kwargs):
BaseStage.__init__(self)
@ -15,13 +16,13 @@ class MockStage(BaseStage):
def state(self):
"""The general state dictionary of the board."""
state = {
'position': {
'x': self.position[0],
'y': self.position[1],
'z': self.position[2],
"position": {
"x": self.position[0],
"y": self.position[1],
"z": self.position[2],
},
'board': None,
'version': '0'
"board": None,
"version": "0",
}
return state
@ -29,21 +30,15 @@ class MockStage(BaseStage):
"""Update settings from a config dictionary"""
# Set backlash. Expects a dictionary with axis labels
if 'backlash' in config:
if "backlash" in config:
# Construct backlash array
backlash = axes_to_array(config['backlash'], ['x', 'y', 'z'], [0, 0, 0])
backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0])
self.backlash = backlash
def read_config(self) -> dict:
"""Return the current settings as a dictionary"""
blsh = self.backlash.tolist()
config = {
'backlash': {
'x': blsh[0],
'y': blsh[1],
'z': blsh[2],
}
}
config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}}
return config
@property
@ -69,7 +64,7 @@ class MockStage(BaseStage):
assert len(blsh) == self.n_axes
self._backlash = np.array(blsh)
else:
self._backlash = np.array([int(blsh)]*self.n_axes, dtype=np.int)
self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int)
def move_rel(self, displacement, axis=None, backlash=True):
pass

View file

@ -18,26 +18,29 @@ class SangaStage(BaseStage):
board (:py:class:`openflexure_microscope.stage.sangaboard.Sangaboard`): Parent Sangaboard object.
_backlash (list): 3-element (element-per-axis) list of backlash compensation in steps.
"""
def __init__(self, port=None, **kwargs):
"""Class managing serial communications with the motors for an Openflexure stage"""
BaseStage.__init__(self)
self.board = Sangaboard(port, **kwargs)
self._backlash = None # Initialise backlash storage, used by property setter/getter
self.axis_names = ['x', 'y', 'z'] # Assume all sangaboards are 3 axis
self._backlash = (
None
) # Initialise backlash storage, used by property setter/getter
self.axis_names = ["x", "y", "z"] # Assume all sangaboards are 3 axis
@property
def state(self):
"""The general state dictionary of the board."""
state = {
'position': {
'x': self.position[0],
'y': self.position[1],
'z': self.position[2],
"position": {
"x": self.position[0],
"y": self.position[1],
"z": self.position[2],
},
'board': self.board.board,
'firmware': self.board.firmware
"board": self.board.board,
"firmware": self.board.firmware,
}
return state
@ -80,27 +83,21 @@ class SangaStage(BaseStage):
assert len(blsh) == self.n_axes
self._backlash = np.array(blsh)
else:
self._backlash = np.array([int(blsh)]*self.n_axes, dtype=np.int)
self._backlash = np.array([int(blsh)] * self.n_axes, dtype=np.int)
def apply_config(self, config: dict):
"""Update settings from a config dictionary"""
# Set backlash. Expects a dictionary with axis labels
if 'backlash' in config:
if "backlash" in config:
# Construct backlash array
backlash = axes_to_array(config['backlash'], ['x', 'y', 'z'], [0, 0, 0])
backlash = axes_to_array(config["backlash"], ["x", "y", "z"], [0, 0, 0])
self.backlash = backlash
def read_config(self) -> dict:
"""Return the current settings as a dictionary"""
blsh = self.backlash.tolist()
config = {
'backlash': {
'x': blsh[0],
'y': blsh[1],
'z': blsh[2],
}
}
config = {"backlash": {"x": blsh[0], "y": blsh[1], "z": blsh[2]}}
return config
@ -117,7 +114,9 @@ class SangaStage(BaseStage):
if axis is not None:
# backlash correction is easier if we're always in 3D
# so this code just converts single-axis moves into all-axis moves.
assert axis in self.axis_names, "axis must be one of {}".format(self.axis_names)
assert axis in self.axis_names, "axis must be one of {}".format(
self.axis_names
)
move = np.zeros(self.n_axes, dtype=np.int)
move[np.argmax(np.array(self.axis_names) == axis)] = int(displacement)
displacement = move
@ -134,7 +133,7 @@ class SangaStage(BaseStage):
initial_move -= np.where(
self.backlash * displacement < 0,
self.backlash,
np.zeros(self.n_axes, dtype=self.backlash.dtype)
np.zeros(self.n_axes, dtype=self.backlash.dtype),
)
self.board.move_rel(initial_move)
if np.any(displacement - initial_move != 0):
@ -160,7 +159,9 @@ class SangaStage(BaseStage):
"""
starting_position = self.position
rel_positions = np.array(rel_positions)
assert rel_positions.shape[1] == 3, ValueError("Positions should be 3 elements long.")
assert rel_positions.shape[1] == 3, ValueError(
"Positions should be 3 elements long."
)
try:
self.move_rel(rel_positions[0], backlash=backlash)
yield 0
@ -186,7 +187,7 @@ class SangaStage(BaseStage):
def close(self):
"""Cleanly close communication with the stage"""
if hasattr(self, 'board'):
if hasattr(self, "board"):
self.board.close()
# Methods specific to Sangaboard
@ -205,12 +206,16 @@ class SangaStage(BaseStage):
need to worry about that here.
"""
if type is not None:
print("An exception occurred inside a with block, resetting position \
to its value at the start of the with block")
print(
"An exception occurred inside a with block, resetting position \
to its value at the start of the with block"
)
try:
time.sleep(0.5)
self.move_abs(self._position_on_enter)
except Exception as e:
print("A further exception occurred when resetting position: {}".format(e))
print(
"A further exception occurred when resetting position: {}".format(e)
)
print("Move completed, raising exception...")
raise value # Propagate the exception

View file

@ -29,6 +29,7 @@ import time
import logging
import warnings
class ExtensibleSerialInstrument(object):
"""
An instrument that communicates by sending strings back and forth over serial
@ -49,8 +50,13 @@ class ExtensibleSerialInstrument(object):
blocked for a long time. The lock is reentrant so there's no issue with
acquiring it twice.
"""
termination_character = "\n" #: All messages to or from the instrument end with this character.
termination_line = None #: If multi-line responses are recieved, they must end with this string
termination_character = (
"\n"
) #: All messages to or from the instrument end with this character.
termination_line = (
None
) #: If multi-line responses are recieved, they must end with this string
ignore_echo = False
port_settings = {}
@ -61,7 +67,7 @@ class ExtensibleSerialInstrument(object):
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...
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):
@ -71,17 +77,22 @@ class ExtensibleSerialInstrument(object):
then we don't warn when ports are opened multiple times.
"""
with self.communications_lock:
if hasattr(self,'_ser') and self._ser.isOpen():
if not quiet: logging.warning("Attempted to open an already-open port!")
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
#this allows us to read/write in neat lines. NB the buffer size must
#be set to 1 byte for maximum responsiveness.
assert self.test_communications(), "The instrument doesn't seem to be responding. Did you specify the right port?"
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
# this allows us to read/write in neat lines. NB the buffer size must
# be set to 1 byte for maximum responsiveness.
assert (
self.test_communications()
), "The instrument doesn't seem to be responding. Did you specify the right port?"
def close(self):
"""Cleanly close the device. Includes proper logging statements."""
@ -95,11 +106,12 @@ class ExtensibleSerialInstrument(object):
def __del__(self):
"""Emergency close the device. Try to avoid having to use this."""
if hasattr(self, '_ser') and self._ser.isOpen():
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.")
"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:
@ -111,30 +123,39 @@ class ExtensibleSerialInstrument(object):
def __exit__(self, type, value, traceback):
"""Cleanly close down the instrument at end of a with block."""
self.close()
def write(self,query_string):
def write(self, query_string):
"""Write a string to the serial port"""
with self.communications_lock:
assert self._ser.isOpen(), "Attempted to write to the serial port before it was opened. Perhaps you need to call the 'open' method first?"
#TODO: Check if this code is needed and if not kill it
# try:
# if self._ser.outWaiting()>0: self._ser.flushOutput() #ensure there's nothing waiting
# except AttributeError:
# if self._ser.out_waiting>0: self._ser.flushOutput() #ensure there's nothing waiting
data=query_string+self.termination_character
data=data.encode()
assert (
self._ser.isOpen()
), "Attempted to write to the serial port before it was opened. Perhaps you need to call the 'open' method first?"
# TODO: Check if this code is needed and if not kill it
# try:
# if self._ser.outWaiting()>0: self._ser.flushOutput() #ensure there's nothing waiting
# except AttributeError:
# if self._ser.out_waiting>0: self._ser.flushOutput() #ensure there's nothing waiting
data = query_string + self.termination_character
data = data.encode()
self._ser.write(data)
def flush_input_buffer(self):
"""Make sure there's nothing waiting to be read, and clear the buffer if there is."""
with self.communications_lock:
if self._ser.inWaiting()>0: self._ser.flushInput()
if self._ser.inWaiting() > 0:
self._ser.flushInput()
def readline(self, timeout=None):
"""Read one line from the serial port."""
with self.communications_lock:
return self._ser.readline().decode('utf8').replace(self.termination_character,"\n")
return (
self._ser.readline()
.decode("utf8")
.replace(self.termination_character, "\n")
)
_communications_lock = None
@property
def communications_lock(self):
"""A lock object used to protect access to the communications bus"""
@ -143,7 +164,7 @@ class ExtensibleSerialInstrument(object):
if self._communications_lock is None:
self._communications_lock = threading.RLock()
return self._communications_lock
def read_multiline(self, termination_line=None, timeout=None):
"""Read one line from the underlying bus. Must be overriden.
@ -152,15 +173,19 @@ class ExtensibleSerialInstrument(object):
with self.communications_lock:
if termination_line is None:
termination_line = self.termination_line
assert isinstance(termination_line, str), "If you perform a multiline query, you must specify a termination line either through the termination_line keyword argument or the termination_line property of the NPSerialInstrument."
assert isinstance(
termination_line, str
), "If you perform a multiline query, you must specify a termination line either through the termination_line keyword argument or the termination_line property of the NPSerialInstrument."
response = ""
last_line = "dummy"
while termination_line not in last_line and len(last_line) > 0: #read until we get the termination line.
while (
termination_line not in last_line and len(last_line) > 0
): # read until we get the termination line.
last_line = self.readline(timeout)
response += last_line
return response
def query(self,queryString,multiline=False,termination_line=None,timeout=None):
def query(self, queryString, multiline=False, termination_line=None, timeout=None):
"""
Write a string to the stage controller and return its response.
@ -170,22 +195,31 @@ class ExtensibleSerialInstrument(object):
with self.communications_lock:
self.flush_input_buffer()
self.write(queryString)
if self.ignore_echo == True: # Needs Implementing for a multiline read!
if self.ignore_echo == True: # Needs Implementing for a multiline read!
first_line = self.readline(timeout).strip()
if first_line == queryString:
return self.readline(timeout).strip()
else:
logging.info('This command did not echo!!!')
logging.info("This command did not echo!!!")
return first_line
if termination_line is not None:
multiline = True
if multiline:
return self.read_multiline(termination_line)
else:
return self.readline(timeout).strip() #question: should we strip the final newline?
def parsed_query(self, query_string, response_string=r"%d", re_flags=0, parse_function=None, **kwargs):
return self.readline(
timeout
).strip() # question: should we strip the final newline?
def parsed_query(
self,
query_string,
response_string=r"%d",
re_flags=0,
parse_function=None,
**kwargs
):
"""
Perform a query, returning a parsed form of the response.
@ -204,46 +238,81 @@ class ExtensibleSerialInstrument(object):
"""
response_regex = response_string
noop = lambda x: x #placeholder null parse function
placeholders = [ #tuples of (regex matching placeholder, regex to replace it with, parse function)
noop = lambda x: x # placeholder null parse function
placeholders = [ # tuples of (regex matching placeholder, regex to replace it with, parse function)
(r"%c", r".", noop),
(r"%(\d+)c", r".{\1}", noop), #TODO support %cn where n is a number of chars
(
r"%(\d+)c",
r".{\1}",
noop,
), # TODO support %cn where n is a number of chars
(r"%d", r"[-+]?\\d+", int),
(r"%[eEfg]", r"[-+]?(?:\\d+(?:\.\\d*)?|\.\\d+)(?:[eE][-+]?\\d+)?", float),
(r"%i", r"[-+]?(?:0[xX][\\dA-Fa-f]+|0[0-7]*|\\d+)", lambda x: int(x, 0)), #0=autodetect base
(r"%o", r"[-+]?[0-7]+", lambda x: int(x, 8)), #8 means octal
(
r"%i",
r"[-+]?(?:0[xX][\\dA-Fa-f]+|0[0-7]*|\\d+)",
lambda x: int(x, 0),
), # 0=autodetect base
(r"%o", r"[-+]?[0-7]+", lambda x: int(x, 8)), # 8 means octal
(r"%s", r"\\s+", noop),
(r"%u", r"\\d+", int),
(r"%[xX]", r"[-+]?(?:0[xX])?[\\dA-Fa-f]+", lambda x: int(x, 16)), #16 forces hexadecimal
(
r"%[xX]",
r"[-+]?(?:0[xX])?[\\dA-Fa-f]+",
lambda x: int(x, 16),
), # 16 forces hexadecimal
]
matched_placeholders = []
for placeholder, regex, parse_fun in placeholders:
response_regex = re.sub(placeholder, '('+regex+')', response_regex) #substitute regex for placeholder
matched_placeholders.extend([(parse_fun, m.start()) for m in re.finditer(placeholder, response_string)]) #save the positions of the placeholders
response_regex = re.sub(
placeholder, "(" + regex + ")", response_regex
) # substitute regex for placeholder
matched_placeholders.extend(
[
(parse_fun, m.start())
for m in re.finditer(placeholder, response_string)
]
) # save the positions of the placeholders
if parse_function is None:
parse_function = [f for f, s in sorted(matched_placeholders, key=lambda m: m[1])] #order parse functions by their occurrence in the original string
if not hasattr(parse_function,'__iter__'):
parse_function = [parse_function] #make sure it's a list.
reply = self.query(query_string, **kwargs) #do the query
#if match this could be because another response entered the buffer between write and read. Sleep for short while then
#check if something is now in the buffer, while the buffer is not empty repeat regex
waited=False
res=re.search(response_regex, reply, flags=re_flags)
parse_function = [
f for f, s in sorted(matched_placeholders, key=lambda m: m[1])
] # order parse functions by their occurrence in the original string
if not hasattr(parse_function, "__iter__"):
parse_function = [parse_function] # make sure it's a list.
reply = self.query(query_string, **kwargs) # do the query
# if match this could be because another response entered the buffer between write and read. Sleep for short while then
# check if something is now in the buffer, while the buffer is not empty repeat regex
waited = False
res = re.search(response_regex, reply, flags=re_flags)
while res is None:
if not waited:
time.sleep(.1)
waited=True
time.sleep(0.1)
waited = True
original_reply = reply
if self._ser.inWaiting():
reply = self.readline().strip()
res=re.search(response_regex, reply, flags=re_flags)
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))
raise ValueError(
"Stage response to '%s' ('%s') wasn't matched by /%s/ (generated regex /%s/)"
% (query_string, original_reply, response_string, response_regex)
)
try:
parsed_result= [f(g) for f, g in zip(parse_function, res.groups())] #try to apply each parse function to its argument
parsed_result = [
f(g) for f, g in zip(parse_function, res.groups())
] # try to apply each parse function to its argument
if len(parsed_result) == 1:
return parsed_result[0]
else:
@ -251,10 +320,15 @@ class ExtensibleSerialInstrument(object):
except ValueError:
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))
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)"""
return self.parsed_query(query_string, "%d", **kwargs)
def float_query(self, query_string, **kwargs):
"""Perform a query and return the result(s) as float(s) (see parsedQuery)"""
return self.parsed_query(query_string, "%f", **kwargs)
@ -273,7 +347,13 @@ class ExtensibleSerialInstrument(object):
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?!
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)
@ -285,14 +365,15 @@ class ExtensibleSerialInstrument(object):
try:
self.close()
except:
pass #we don't care if there's an error closing the port...
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
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.
@ -300,32 +381,42 @@ class OptionalModule(object):
for interfacing with optional modules which may or may not be included with
the serial instrument, and can be added or removed at run-time.
"""
def __init__(self,available,parent=None,module_type="Undefined",model="Generic"):
assert type(available) is bool, 'Option module availablity should be a boolean not a {}'.format(type(available))
self._available=available
self._parent=parent
assert type(module_type) is str, 'Option module type should be a string not a {}'.format(type(module_typ))
self.module_type=module_type
def __init__(
self, available, parent=None, module_type="Undefined", model="Generic"
):
assert (
type(available) is bool
), "Option module availablity should be a boolean not a {}".format(
type(available)
)
self._available = available
self._parent = parent
assert (
type(module_type) is str
), "Option module type should be a string not a {}".format(type(module_typ))
self.module_type = module_type
if available:
assert type(model) is str, 'Option module type should be a string not a {}'.format(type(model))
self.model=model
assert (
type(model) is str
), "Option module type should be a string not a {}".format(type(model))
self.model = model
else:
self.model=None
self.model = None
@property
def available(self):
return self._available
def confirm_available(self):
"""Check if module is available, no return, will raise exception if not available!"""
assert self._available, "No \"{}\" supported on firmware".format(self.module_type)
assert self._available, 'No "{}" supported on firmware'.format(self.module_type)
def describe(self):
"""Consistently spaced desciption for listing modules"""
return self.module_type+" "*(25-len(self.module_type))+"- "+self.model
return self.module_type + " " * (25 - len(self.module_type)) + "- " + self.model
class QueriedProperty(object):
"""A Property interface that reads and writes from the instrument on the bus.
@ -359,8 +450,18 @@ class QueriedProperty(object):
:ack_writes:
set to "readline" to discard a line of input after writing.
"""
def __init__(self, get_cmd=None, set_cmd=None, validate=None, valrange=None,
fdel=None, doc=None, response_string=None, ack_writes="no"):
def __init__(
self,
get_cmd=None,
set_cmd=None,
validate=None,
valrange=None,
fdel=None,
doc=None,
response_string=None,
ack_writes="no",
):
self.response_string = response_string
self.get_cmd = get_cmd
self.set_cmd = set_cmd
@ -369,48 +470,54 @@ class QueriedProperty(object):
self.fdel = fdel
self.ack_writes = ack_writes
self.__doc__ = doc
# TODO: standardise the return (single value only vs parsed result), consider bool
def __get__(self, obj, objtype=None):
if issubclass(type(obj),OptionalModule):
if issubclass(type(obj), OptionalModule):
obj.confirm_available()
obj=obj._parent
obj = obj._parent
if obj is None:
return self
assert issubclass(type(obj),ExtensibleSerialInstrument)
assert issubclass(type(obj), ExtensibleSerialInstrument)
if self.get_cmd is None:
raise AttributeError("unreadable attribute")
# Allow certain "magic" values to set the response string
for key, val in [('float',r"%f"),
('int',r"%d"),]:
for key, val in [("float", r"%f"), ("int", r"%d")]:
if self.response_string == key:
self.response_string = val
if self.response_string in ['bool', 'raw', None]:
if self.response_string in ["bool", "raw", None]:
value = obj.query(self.get_cmd)
if self.response_string == 'bool':
if self.response_string == "bool":
value = bool(value)
else:
value = obj.parsed_query(self.get_cmd, self.response_string)
return value
def __set__(self, obj, value):
if issubclass(type(obj),OptionalModule):
if issubclass(type(obj), OptionalModule):
obj.confirm_available()
obj=obj._parent
assert issubclass(type(obj),ExtensibleSerialInstrument)
obj = obj._parent
assert issubclass(type(obj), ExtensibleSerialInstrument)
if self.set_cmd is None:
raise AttributeError("can't set attribute")
if self.validate is not None:
if value not in self.validate:
raise ValueError('invalid value supplied - value must be one of {}'.format(self.validate))
raise ValueError(
"invalid value supplied - value must be one of {}".format(
self.validate
)
)
if self.valrange is not None:
if value < min(self.valrange) or value > max(self.valrange):
raise ValueError('invalid value supplied - value must be in the range {}-{}'.format(*self.valrange))
raise ValueError(
"invalid value supplied - value must be in the range {}-{}".format(
*self.valrange
)
)
message = self.set_cmd
if '{0' in message:
if "{0" in message:
message = message.format(value)
elif '%' in message:
elif "%" in message:
message = message % value
obj.write(message)
if self.ack_writes == "readline":

View file

@ -9,12 +9,20 @@ 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
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
@ -39,34 +47,45 @@ class Sangaboard(ExtensibleSerialInstrument):
"""
# List of valid and deprecated firmwares, for communication checks
valid_firmwares = [
(0, 5),
(0, 4)
]
deprecated_firmwares = [
(0, 4)
]
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}
port_settings = {
"baudrate": 115200,
"bytesize": EIGHTBITS,
"parity": PARITY_NONE,
"stopbits": STOPBITS_ONE,
}
# 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",
doc="Get or set the minimum time between steps of the motors in microseconds.\n\n"
"The step time is ``1000000/max speed`` in steps/second. It is saved to EEPROM on "
"the sangaboard, so it will be persistent even if the motor controller is turned off.")
ramp_time = QueriedProperty(get_cmd="ramp_time?", set_cmd="ramp_time %d", response_string="ramp time %d",
doc="Get or set the acceleration time in microseconds.\n\n"
"The motors will accelerate/decelerate between stationary and maximum speed over `ramp_time` "
"microseconds. Zero means the motor runs at full speed initially, with no accleration "
"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.")
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",
doc="Get or set the minimum time between steps of the motors in microseconds.\n\n"
"The step time is ``1000000/max speed`` in steps/second. It is saved to EEPROM on "
"the sangaboard, so it will be persistent even if the motor controller is turned off.",
)
ramp_time = QueriedProperty(
get_cmd="ramp_time?",
set_cmd="ramp_time %d",
response_string="ramp time %d",
doc="Get or set the acceleration time in microseconds.\n\n"
"The motors will accelerate/decelerate between stationary and maximum speed over `ramp_time` "
"microseconds. Zero means the motor runs at full speed initially, with no accleration "
"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')
axis_names = ("x", "y", "z")
# Once initialised, `firmware` is a string that identifies the firmware version
firmware = None
@ -88,30 +107,38 @@ class Sangaboard(ExtensibleSerialInstrument):
# 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.
# Bit messy: Defining all valid modules as not available, then overwriting with available information if available.
self.light_sensor = LightSensor(False)
for module in self.list_modules():
module_type=module.split(':')[0].strip()
module_model=module.split(':')[1].strip()
if module_type.startswith('Light Sensor'):
module_type = module.split(":")[0].strip()
module_model = module.split(":")[1].strip()
if module_type.startswith("Light Sensor"):
if module_model in self.supported_light_sensors:
self.light_sensor = LightSensor(True,parent=self,model=module_model)
self.light_sensor = LightSensor(
True, parent=self, model=module_model
)
else:
logging.warning("Light sensor model \"%s\" not recognised."%(module_model))
elif module_type.startswith('Endstops'):
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:
logging.warning("Module type \"{}\" not recognised.".format(module_type))
logging.warning(
'Module type "{}" not recognised.'.format(module_type)
)
self.board = self.query("board",timeout=2).rstrip()
self.board = self.query("board", timeout=2).rstrip()
except Exception as e:
# 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()
logging.error(e)
logging.error("You may need to update the firmware running on the Sangaboard.")
logging.error(
"You may need to update the firmware running on the Sangaboard."
)
raise e
def test_communications(self):
@ -124,16 +151,22 @@ class Sangaboard(ExtensibleSerialInstrument):
logging.debug("Running firmware checks")
# Request firmware version from the board
self.firmware = self.query("version",timeout=2).rstrip()
self.firmware = self.query("version", timeout=2).rstrip()
logging.info("Firmware response: {}".format(self.firmware))
# Check for valid firmware string
if self.firmware:
match = re.match(r"Sangaboard Firmware v(([\d]+)(?:\.([\d]+))+)", 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)
match = re.match(
r"OpenFlexure Motor Board v(([\d]+)(?:\.([\d]+))+)", self.firmware
)
if not match:
logging.error("Version string \"{}\" not recognised.".format(self.firmware))
logging.error(
'Version string "{}" not recognised.'.format(self.firmware)
)
return False
else:
logging.error("No firmware version string was returned.")
@ -146,15 +179,17 @@ class Sangaboard(ExtensibleSerialInstrument):
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)")
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):
@ -164,10 +199,12 @@ class Sangaboard(ExtensibleSerialInstrument):
axis: None (for 3-axis moves) or one of 'x','y','z'
"""
if axis is not None:
assert axis in self.axis_names, "axis must be one of {}".format(self.axis_names)
assert axis in self.axis_names, "axis must be one of {}".format(
self.axis_names
)
self.query("mr{} {}".format(axis, int(displacement)))
else:
#TODO: assert displacement is 3 integers
# TODO: assert displacement is 3 integers
self.query("mr {} {} {}".format(*list(displacement)))
def release_motors(self):
@ -181,12 +218,12 @@ class Sangaboard(ExtensibleSerialInstrument):
queries the board for its position, then instructs it to make about
relative move.
"""
rel_mov = [f_pos-i_pos for f_pos,i_pos in zip(final,self.position)]
rel_mov = [f_pos - i_pos for f_pos, i_pos in zip(final, self.position)]
return self.move_rel(rel_mov, **kwargs)
def query(self, message, *args, **kwargs):
"""Send a message and read the response. See ExtensibleSerialInstrument.query()"""
time.sleep(0.001) # This is to protect the stage from us talking too fast!
time.sleep(0.001) # This is to protect the stage from us talking too fast!
return ExtensibleSerialInstrument.query(self, message, *args, **kwargs)
def list_modules(self):
@ -194,12 +231,15 @@ class Sangaboard(ExtensibleSerialInstrument):
Each module will correspond to a string of the form ``Module Name: Model``
"""
modules = self.query("list_modules",multiline=True,termination_line="--END--\r\n").split('\r\n')[:-2]
modules = self.query(
"list_modules", multiline=True, termination_line="--END--\r\n"
).split("\r\n")[:-2]
return [str(module) for module in modules]
def print_help(self):
"""Print the stage's built-in help message."""
print(self.query("help",multiline=True,termination_line="--END--\r\n"))
print(self.query("help", multiline=True, termination_line="--END--\r\n"))
class LightSensor(OptionalModule):
"""An optional module giving access to the light sensor.
@ -209,19 +249,25 @@ class LightSensor(OptionalModule):
module which is an instance of this class. It can be used to access
the light sensor (usually via the I2C bus).
"""
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."
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).",
)
intensity = QueriedProperty(get_cmd="light_sensor_intensity?", response_string="%d",
doc="Read the current intensity measured by the light sensor (arbitrary units).")
def __init__(self,available,parent=None,model="Generic"):
super(LightSensor, self).__init__(available,parent=parent,module_type="LightSensor",model=model)
def __init__(self, available, parent=None, model="Generic"):
super(LightSensor, self).__init__(
available, parent=parent, module_type="LightSensor", model=model
)
if available:
self.valid_gains = self.__get_gain_values()
self._valid_gains_int = [int(g) for g in self.valid_gains]
@ -232,21 +278,25 @@ class LightSensor(OptionalModule):
Valid gain values are defined in the `valid_gains` property, and should be floating-point numbers."""
self.confirm_available()
gain = self._parent.query('light_sensor_gain?')
M = re.search('[0-9\.]+(?=x)',gain)
assert M is not None, "Cannot read gain string: \"{}\"".format(gain)
#gain is a float as non integer gains exist but are set with floor of value
gain = self._parent.query("light_sensor_gain?")
M = re.search("[0-9\.]+(?=x)", gain)
assert M is not None, 'Cannot read gain string: "{}"'.format(gain)
# gain is a float as non integer gains exist but are set with floor of value
return float(M.group())
@gain.setter
def gain(self,val):
def gain(self, val):
self.confirm_available()
assert int(val) in self._valid_gains_int, "Gain {} not valid must be one of: {}".format(val,self.valid_gains)
gain = self._parent.query('light_sensor_gain %d'%(int(val)))
M = re.search('[0-9\.]+(?=x)',gain)
assert M is not None, "Cannot read gain string: \"{}\"".format(gain)
#gain is a float as non integer gains exist but are set with floor of value
assert int(val) == int(float(M.group())), 'Gain of {} set, \"{}\" returned'.format(val,gain)
assert (
int(val) in self._valid_gains_int
), "Gain {} not valid must be one of: {}".format(val, self.valid_gains)
gain = self._parent.query("light_sensor_gain %d" % (int(val)))
M = re.search("[0-9\.]+(?=x)", gain)
assert M is not None, 'Cannot read gain string: "{}"'.format(gain)
# gain is a float as non integer gains exist but are set with floor of value
assert int(val) == int(
float(M.group())
), 'Gain of {} set, "{}" returned'.format(val, gain)
def __get_gain_values(self):
"""Read the allowable values for the light sensor's gain.
@ -256,15 +306,16 @@ class LightSensor(OptionalModule):
values, the list will be of strings.
"""
self.confirm_available()
gains = self._parent.query('light_sensor_gain_values?')
gains = self._parent.query("light_sensor_gain_values?")
try:
M = re.findall('[0-9\.]+(?=x)',gains)
M = re.findall("[0-9\.]+(?=x)", gains)
return [float(gain) for gain in M]
except:
# Fall back to strings if we don't get floats (unlikely)
gain_strings = gains[20:].split(", ")
return gain_strings
class Endstops(OptionalModule):
"""An optional module for use with endstops.
@ -273,35 +324,41 @@ class Endstops(OptionalModule):
the type, state of the endstops, read and write maximum positions, and home.
"""
installed=[]
installed = []
"""List of installed endstop types (min, max, soft)"""
def __init__(self,available,parent=None,model="min"):
super(Endstops, self).__init__(available,parent=parent,model="Endstops")
self.installed=model.split(' ')
def __init__(self, available, parent=None, model="min"):
super(Endstops, self).__init__(available, parent=parent, model="Endstops")
self.installed = model.split(" ")
status = QueriedProperty(get_cmd="endstops?", response_string=r"%d %d %d",
doc="Get endstops status as {-1,0,1} for {min,no,max} endstop triggered for each axis")
maxima = QueriedProperty(get_cmd="max_p?", set_cmd="max %d %d %d", response_string="%d %d %d",
doc="Vector of maximum positions, homing to max endstops will measure this, "+
"can be set to a known value for use with max only and min+soft endstops")
status = QueriedProperty(
get_cmd="endstops?",
response_string=r"%d %d %d",
doc="Get endstops status as {-1,0,1} for {min,no,max} endstop triggered for each axis",
)
maxima = QueriedProperty(
get_cmd="max_p?",
set_cmd="max %d %d %d",
response_string="%d %d %d",
doc="Vector of maximum positions, homing to max endstops will measure this, "
+ "can be set to a known value for use with max only and min+soft endstops",
)
def home(self, direction="min", axes=['x','y','z']):
def home(self, direction="min", axes=["x", "y", "z"]):
""" Home given/all axes in the given direction (min/max/both)
:param direction: one of {min,max,both}
:param axes: list of axes e.g. ['x','y']
"""
ax=0
if 'x' in axes:
ax+=1
if 'y' in axes:
ax+=2
if 'z' in axes:
ax+=3
ax = 0
if "x" in axes:
ax += 1
if "y" in axes:
ax += 2
if "z" in axes:
ax += 3
if direction == "min" or direction == "both":
self._parent.query('home_min {}'.format(ax))
self._parent.query("home_min {}".format(ax))
if direction == "max" or direction == "both":
self._parent.query('home_max {}'.format(ax))
self._parent.query("home_max {}".format(ax))