Sangaboard now handles port scanning and firmware checking
This commit is contained in:
parent
5f4af1e17a
commit
5823a87341
3 changed files with 124 additions and 81 deletions
|
|
@ -186,7 +186,8 @@ class SangaStage(BaseStage):
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Cleanly close communication with the stage"""
|
"""Cleanly close communication with the stage"""
|
||||||
self.board.close()
|
if hasattr(self, 'board'):
|
||||||
|
self.board.close()
|
||||||
|
|
||||||
# Methods specific to Sangaboard
|
# Methods specific to Sangaboard
|
||||||
def release_motors(self):
|
def release_motors(self):
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,11 @@ 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
|
import re
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import threading
|
import threading
|
||||||
import serial
|
import serial
|
||||||
import serial.tools.list_ports
|
|
||||||
from serial import FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS
|
from serial import FIVEBITS, SIXBITS, SEVENBITS, EIGHTBITS
|
||||||
from serial import PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE
|
from serial import PARITY_NONE, PARITY_EVEN, PARITY_ODD, PARITY_MARK, PARITY_SPACE
|
||||||
from serial import STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO
|
from serial import STOPBITS_ONE, STOPBITS_ONE_POINT_FIVE, STOPBITS_TWO
|
||||||
|
|
@ -55,17 +54,17 @@ class ExtensibleSerialInstrument(object):
|
||||||
ignore_echo = False
|
ignore_echo = False
|
||||||
port_settings = {}
|
port_settings = {}
|
||||||
|
|
||||||
def __init__(self, port=None, **kwargs):
|
def __init__(self, port, **kwargs):
|
||||||
"""
|
"""
|
||||||
Set up the serial port and so on.
|
Set up the serial port and so on.
|
||||||
"""
|
"""
|
||||||
logging.info("Updating BSI port settings")
|
logging.info("Updating ESI port settings")
|
||||||
self.port_settings.update(kwargs)
|
self.port_settings.update(kwargs)
|
||||||
logging.info("Opening BSI connection to port {}".format(port))
|
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 BSI connection to port {}".format(port))
|
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.
|
"""Open communications with the serial port.
|
||||||
|
|
||||||
If no port is specified, it will attempt to autodetect. If quiet=True
|
If no port is specified, it will attempt to autodetect. If quiet=True
|
||||||
|
|
@ -73,10 +72,9 @@ class ExtensibleSerialInstrument(object):
|
||||||
"""
|
"""
|
||||||
with self.communications_lock:
|
with self.communications_lock:
|
||||||
if hasattr(self,'_ser') and self._ser.isOpen():
|
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
|
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?"
|
||||||
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?"
|
|
||||||
self._ser = serial.Serial(port,**self.port_settings)
|
self._ser = serial.Serial(port,**self.port_settings)
|
||||||
#the block above wraps the serial IO layer with a text IO layer
|
#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
|
#this allows us to read/write in neat lines. NB the buffer size must
|
||||||
|
|
@ -90,7 +88,7 @@ class ExtensibleSerialInstrument(object):
|
||||||
try:
|
try:
|
||||||
self._ser.close()
|
self._ser.close()
|
||||||
except Exception as e:
|
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")
|
logging.debug("Connection closed")
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
|
|
@ -170,7 +168,7 @@ class ExtensibleSerialInstrument(object):
|
||||||
if first_line == queryString:
|
if first_line == queryString:
|
||||||
return self.readline(timeout).strip()
|
return self.readline(timeout).strip()
|
||||||
else:
|
else:
|
||||||
print('This command did not echo!!!')
|
logging.info('This command did not echo!!!')
|
||||||
return first_line
|
return first_line
|
||||||
|
|
||||||
if termination_line is not None:
|
if termination_line is not None:
|
||||||
|
|
@ -244,9 +242,9 @@ class ExtensibleSerialInstrument(object):
|
||||||
else:
|
else:
|
||||||
return parsed_result
|
return parsed_result
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("Parsing Error")
|
logging.error("Parsing Error")
|
||||||
print("Matched Groups:", res.groups())
|
logging.info("Matched Groups:", res.groups())
|
||||||
print("Parsing Functions:", parse_function)
|
logging.info("Parsing Functions:", 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):
|
def int_query(self, query_string, **kwargs):
|
||||||
"""Perform a query and return the result(s) as integer(s) (see parsedQuery)"""
|
"""Perform a query and return the result(s) as integer(s) (see parsedQuery)"""
|
||||||
|
|
@ -263,31 +261,6 @@ class ExtensibleSerialInstrument(object):
|
||||||
Usually this function sends a command and checks for a known reply."""
|
Usually this function sends a command and checks for a known reply."""
|
||||||
with self.communications_lock:
|
with self.communications_lock:
|
||||||
return True
|
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):
|
class OptionalModule(object):
|
||||||
"""This allows a `ExtensibleSerialInstrument` to have optional features.
|
"""This allows a `ExtensibleSerialInstrument` to have optional features.
|
||||||
|
|
@ -369,7 +342,6 @@ class QueriedProperty(object):
|
||||||
|
|
||||||
# TODO: standardise the return (single value only vs parsed result), consider bool
|
# TODO: standardise the return (single value only vs parsed result), consider bool
|
||||||
def __get__(self, obj, objtype=None):
|
def __get__(self, obj, objtype=None):
|
||||||
#print 'get', obj, objtype
|
|
||||||
if issubclass(type(obj),OptionalModule):
|
if issubclass(type(obj),OptionalModule):
|
||||||
obj.confirm_available()
|
obj.confirm_available()
|
||||||
obj=obj._parent
|
obj=obj._parent
|
||||||
|
|
@ -392,7 +364,6 @@ class QueriedProperty(object):
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def __set__(self, obj, value):
|
def __set__(self, obj, value):
|
||||||
#print 'set', obj, value
|
|
||||||
if issubclass(type(obj),OptionalModule):
|
if issubclass(type(obj),OptionalModule):
|
||||||
obj.confirm_available()
|
obj.confirm_available()
|
||||||
obj=obj._parent
|
obj=obj._parent
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ It is (c) Richard Bowman & Julian Stirling 2019 and released under GNU GPL v3
|
||||||
from __future__ import print_function, division
|
from __future__ import print_function, division
|
||||||
import time
|
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 re
|
||||||
import warnings
|
import warnings
|
||||||
import logging
|
import logging
|
||||||
|
|
@ -29,17 +30,27 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
|
|
||||||
This class can be used as a context manager, i.e. it's encouraged to use it as::
|
This class can be used as a context manager, i.e. it's encouraged to use it as::
|
||||||
|
|
||||||
with Sangaboard() as sb:
|
with Sangaboard() as sb:
|
||||||
sb.move_rel([1000,0,0])
|
sb.move_rel([1000,0,0])
|
||||||
|
|
||||||
In that case, the serial port will automatically be closed at the end of the block,
|
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
|
even if an error occurs. Otherwise, be sure to call the
|
||||||
:meth:`~.ExtensibleSerialInstrument.close()` method to release the serial port.
|
: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}
|
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
|
# position, step time and ramp time are get/set using simple serial commands.
|
||||||
# commands.
|
|
||||||
position = QueriedProperty(get_cmd="p?", response_string=r"%d %d %d",
|
position = QueriedProperty(get_cmd="p?", response_string=r"%d %d %d",
|
||||||
doc="Get the position of the axes as a tuple of 3 integers.")
|
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",
|
step_time = QueriedProperty(get_cmd="dt?", set_cmd="dt %d", response_string="minimum step delay %d",
|
||||||
|
|
@ -53,10 +64,12 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
"control. Small moves may last less than `2*ramp_time`, in which case the acceleration "
|
"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 "
|
"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 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')
|
||||||
"""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
|
firmware = None
|
||||||
"""Once initialised, `firmware` is a string that identifies the firmware version."""
|
|
||||||
|
|
||||||
def __init__(self, port=None, **kwargs):
|
def __init__(self, port=None, **kwargs):
|
||||||
"""Create a sangaboard object.
|
"""Create a sangaboard object.
|
||||||
|
|
@ -67,32 +80,18 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
you will use to communicate with the motor controller. That's the first argument so
|
you will use to communicate with the motor controller. That's the first argument so
|
||||||
it doesn't need to be named.
|
it doesn't need to be named.
|
||||||
"""
|
"""
|
||||||
ExtensibleSerialInstrument.__init__(self, port, **kwargs)
|
|
||||||
|
# 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:
|
try:
|
||||||
logging.debug("Running firmware checks")
|
# Make absolutely sure that whatever port we're on is valid
|
||||||
# Request firmware version from the board
|
self.check_valid_firmware()
|
||||||
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
|
|
||||||
|
|
||||||
#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)
|
self.light_sensor = LightSensor(False)
|
||||||
|
|
@ -104,11 +103,11 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
if module_model in self.supported_light_sensors:
|
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:
|
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'):
|
elif module_type.startswith('Endstops'):
|
||||||
self.endstops = Endstops(True, parent=self, model=module_model)
|
self.endstops = Endstops(True, parent=self, model=module_model)
|
||||||
else:
|
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()
|
self.board = self.query("board",timeout=2).rstrip()
|
||||||
|
|
||||||
|
|
@ -116,9 +115,79 @@ class Sangaboard(ExtensibleSerialInstrument):
|
||||||
# If an error occurred while setting up (e.g. because the board isn't connected or something)
|
# 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).
|
# make sure we close the serial port cleanly (otherwise it hangs open).
|
||||||
self.close()
|
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
|
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):
|
def move_rel(self, displacement, axis=None):
|
||||||
"""Make a relative move.
|
"""Make a relative move.
|
||||||
|
|
||||||
|
|
@ -173,10 +242,12 @@ class LightSensor(OptionalModule):
|
||||||
"""
|
"""
|
||||||
valid_gains = None
|
valid_gains = None
|
||||||
_valid_gains_int = None
|
_valid_gains_int = None
|
||||||
integration_time = QueriedProperty(get_cmd="light_sensor_integration_time?",
|
integration_time = QueriedProperty(
|
||||||
set_cmd="light_sensor_integration_time %d",
|
get_cmd="light_sensor_integration_time?",
|
||||||
response_string="light sensor integration time %d ms",
|
set_cmd="light_sensor_integration_time %d",
|
||||||
doc="Get or set the integration time of the light sensor in milliseconds.")
|
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",
|
intensity = QueriedProperty(get_cmd="light_sensor_intensity?", response_string="%d",
|
||||||
doc="Read the current intensity measured by the light sensor (arbitrary units).")
|
doc="Read the current intensity measured by the light sensor (arbitrary units).")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue