From 1bc26cfd05ddda8a579d56a6e0a949a96bc58b8f Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 5 Aug 2014 17:44:05 +0200 Subject: [PATCH 01/50] Created driver for NewPort powermeter 1830c. --- __init__.py | 19 +++++ powermeter1830c.py | 198 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 __init__.py create mode 100644 powermeter1830c.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..507bc53 --- /dev/null +++ b/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.tektronix + ~~~~~~~~~~~~~~~~~~~~~~~ + + :company: Newport. + :description: Test and Measurement Equipment. + :website: http://www.newport.com/ + + --- + + :copyright: 2012 by Lantz Authors, see AUTHORS for more details. + :license: BSD, + +""" + +from .powermeter1830c import powermeter1830c + +__all__ = ['powermeter1830c'] diff --git a/powermeter1830c.py b/powermeter1830c.py new file mode 100644 index 0000000..8e8c787 --- /dev/null +++ b/powermeter1830c.py @@ -0,0 +1,198 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.newport.powermeter1830c + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implements the drivers to control an Optical Power Meter. + + :copyright: 2012 by Lantz Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. + + Source: Instruction Manual (Newport) +""" + +from lantz.feat import Feat +from lantz.action import Action +from lantz.serial import SerialDriver +from lantz.errors import InvalidCommand + +class powermeter1830c(SerialDriver): + """ Newport 1830c Power Meter + """ + + ENCODING = 'ascii' + RECV_TERMINATION = '\n' + SEND_TERMINATION = '\n' + TIMEOUT = 2000 + + + def __init__(self, port=1,baudrate=9600): + super().__init__(port, baudrate, bytesize=8, parity='None', stopbits=1) + + def initialize(self): + super().initialize() + + @Feat(values={True: 1, False: 0}) + def attenuator(self): + """ Attenuator. + 1: Attenuator present + 0: Attenuator not present + """ + return int(self.query('A?')) + + @attenuator.setter + def attenuator(self, value): + self.send('A{}'.format(value)) + + @Feat(values={True: 1, False: 0}) + def beeper(self): + """ Checks whether the audio output is on or off. + """ + return int(self.query('B?')) + + @beeper.setter + def beeper(self,value): + self.send('B{}'.format(value)) + + @Feat + def data(self): + """ Retrieves the value from the power meter. + """ + return float(self.query('D?')) + + @Feat(values={True: 1, False: 0}) + def echo(self): + """ Returns echo mode. Only applied to RS232 communication + """ + return int(self.query('E?')) + + @echo.setter + def echo(self,value): + self.send('E{}'.format(value)) + + @Feat(values={'Slow': 1, 'Medium': 2, 'Fast': 3}) + def filter(self): + """ How many measurements are averaged for the displayed reading. + slow: 16 measurements + medium: 4 measurements + fast: 1 measurement. + """ + return int(self.query('F?')) + + @filter.setter + def filter(self,value): + self.send('F{}'.format(value)) + + @Feat(values={True: 1, False: 0}) + def go(self): + """ Enable or disable the power meter from taking new measurements. + """ + return int(self.query('G?')) + + @go.setter + def go(self,value): + self.send('G{}'.format(value)) + + @Feat(values={'Off': 0, 'Medium': 1, 'High': 2}) + def keypad(self): + """ Keypad/Display backlight intensity levels. + """ + return int(self.query('K?')) + + @keypad.setter + def keypad(self,value): + self.send('K{}'.format(value)) + + @Feat(values={True: 1, False: 0}) + def lockout(self): + """ Enable/Disable the lockout. When the lockout is enabled, any front panel key presses would have no effect on system operation. + """ + return int(self.query('L?')) + + @lockout.setter + def lockout(self,value): + self.send('L{}'.format(value)) + + @Action() + def autocalibration(self): + """ Autocalibration of the power meter. This procedure disconnects the input signal. + It should be performed at least 60 minutes after warm-up. + """ + self.send('O') + + @Feat(values=set(range(0,9))) + def range(self): + """ Set the signal range for the input signal. + 0 means auto setting the range. 1 is the lowest signal range and 8 the highest. + """ + return int(self.query('R?')) + + @range.setter + def range(self,value): + self.send('R{}'.format(value)) + + @Action() + def store_reference(self): + """ Sets the current input signal power level as the power reference level. + Each time the S command is sent, the current input signal becomes the new reference level. + """ + self.send('S') + + @Feat(values={'Watts': 1, 'dB': 2, 'dBm': 3, 'REL': 4}) + def units(self): + """ Sets and gets the units of the measurements. + """ + return int(self.query('U?')) + + @units.setter + def units(self,value): + self.send('U{}'.format(value)) + + @Feat(limits=(1,10000,1)) + def wavelength(self): + """ Sets and gets the wavelength of the input signal. + """ + return int(self.query('W?')) + + @wavelength.setter + def wavelength(self,value): + self.send('W{}'.format(int(value))) + + @Feat(values={True: 1, False: 0}) + def zero(self): + """ Turn the zero function on/off. Zero function is used for subtracting any background power levels in future measurements. + """ + return int(self.query('Z?')) + + @zero.setter + def zero(self,value): + self.send('Z{}'.format(value)) + +if __name__ == '__main__': + import argparse + import lantz.log + + parser = argparse.ArgumentParser(description='Test Kentech HRI') + parser.add_argument('-p', '--port', type=str, default='1', + help='Serial port to connect to') + + args = parser.parse_args() + lantz.log.log_to_socket(lantz.log.DEBUG) + + with powermeter1830c(args.port) as inst: + + inst.initialize() # Initialize the communication with the power meter + + inst.lockout = True # Blocks the front panel + inst.keypad = 'Off' # Switches the keypad off + inst.attenuator = True # The attenuator is on + inst.wavelength = 633 # Sets the wavelength to 633nm + inst.units = "Watts" # Sets the units to Watts + inst.filter = 'Slow' # Averages 16 measurements + + if not inst.go: + inst.go = True # If the instrument is not running, enables it + + inst.range = 0 # Auto-sets the range + + print('The measured power is {} Watts'.format(inst.data)) \ No newline at end of file From ce29ba3ba64b6f35ba2d5045ee39fc43690622e2 Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Tue, 5 Aug 2014 12:56:50 -0300 Subject: [PATCH 02/50] Fixed docstring header --- __init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 507bc53..9ee120b 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - lantz.drivers.tektronix - ~~~~~~~~~~~~~~~~~~~~~~~ + lantz.drivers.newport + ~~~~~~~~~~~~~~~~~~~~~ :company: Newport. :description: Test and Measurement Equipment. From f221f2bfc56c9959f7c38b00477b845746bfdc7d Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Wed, 31 Dec 2014 01:32:56 -0300 Subject: [PATCH 03/50] Migrated drivers to messagebased --- powermeter1830c.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/powermeter1830c.py b/powermeter1830c.py index 8e8c787..3413191 100644 --- a/powermeter1830c.py +++ b/powermeter1830c.py @@ -11,27 +11,28 @@ Source: Instruction Manual (Newport) """ +from pyvisa import constants + from lantz.feat import Feat from lantz.action import Action -from lantz.serial import SerialDriver -from lantz.errors import InvalidCommand +from lantz.messagebased import MessageBasedDriver + -class powermeter1830c(SerialDriver): +class PowerMeter1830c(MessageBasedDriver): """ Newport 1830c Power Meter """ - - ENCODING = 'ascii' - RECV_TERMINATION = '\n' - SEND_TERMINATION = '\n' - TIMEOUT = 2000 - - def __init__(self, port=1,baudrate=9600): - super().__init__(port, baudrate, bytesize=8, parity='None', stopbits=1) - - def initialize(self): - super().initialize() - + DEFAULTS_KWARGS = {'ASRL': {'write_termination': '\n', + 'read_termination': '\n', + 'baud_rate': 9600, + 'bytesize': 8, + 'parity': constants.Parity.none, + 'stop_bits': constants.StopBits.one, + 'encoding': 'ascii', + 'timeout': 2000 + }} + + @Feat(values={True: 1, False: 0}) def attenuator(self): """ Attenuator. @@ -179,7 +180,7 @@ def zero(self,value): args = parser.parse_args() lantz.log.log_to_socket(lantz.log.DEBUG) - with powermeter1830c(args.port) as inst: + with PowerMeter1830c.from_serial_port(args.port) as inst: inst.initialize() # Initialize the communication with the power meter @@ -195,4 +196,4 @@ def zero(self,value): inst.range = 0 # Auto-sets the range - print('The measured power is {} Watts'.format(inst.data)) \ No newline at end of file + print('The measured power is {} Watts'.format(inst.data)) From 809591d4aee2508f8eb4a263d50d92eccac6ae88 Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Wed, 31 Dec 2014 03:09:56 -0300 Subject: [PATCH 04/50] Mock modules to allow Docs to be rendered online --- __init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 9ee120b..2a66abf 100644 --- a/__init__.py +++ b/__init__.py @@ -14,6 +14,6 @@ """ -from .powermeter1830c import powermeter1830c +from .powermeter1830c import PowerMeter1830c -__all__ = ['powermeter1830c'] +__all__ = ['PowerMeter1830c'] From 2dae115dbd3d91cfbe7f6dbd004e183755b4ee66 Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Wed, 31 Dec 2014 15:10:04 -0300 Subject: [PATCH 05/50] Documentation fixes --- powermeter1830c.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powermeter1830c.py b/powermeter1830c.py index 3413191..87624ae 100644 --- a/powermeter1830c.py +++ b/powermeter1830c.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ lantz.drivers.newport.powermeter1830c - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Implements the drivers to control an Optical Power Meter. From 1aae25bea5aeefbb014067e34b93079e0e3c5c4d Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Tue, 6 Jan 2015 15:03:09 -0300 Subject: [PATCH 06/50] Adjusted instruments to changes in MessageBasedDriver --- powermeter1830c.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/powermeter1830c.py b/powermeter1830c.py index 87624ae..57358a9 100644 --- a/powermeter1830c.py +++ b/powermeter1830c.py @@ -22,15 +22,15 @@ class PowerMeter1830c(MessageBasedDriver): """ Newport 1830c Power Meter """ - DEFAULTS_KWARGS = {'ASRL': {'write_termination': '\n', - 'read_termination': '\n', - 'baud_rate': 9600, - 'bytesize': 8, - 'parity': constants.Parity.none, - 'stop_bits': constants.StopBits.one, - 'encoding': 'ascii', - 'timeout': 2000 - }} + DEFAULTS = {'ASRL': {'write_termination': '\n', + 'read_termination': '\n', + 'baud_rate': 9600, + 'bytesize': 8, + 'parity': constants.Parity.none, + 'stop_bits': constants.StopBits.one, + 'encoding': 'ascii', + 'timeout': 2000 + }} @Feat(values={True: 1, False: 0}) From 3f2d53160a4c5a9ca0387af5d795fe198eb9f332 Mon Sep 17 00:00:00 2001 From: Hernan Grecco Date: Fri, 9 Jan 2015 21:25:30 -0300 Subject: [PATCH 07/50] Bumped copyright year to 2015 --- __init__.py | 2 +- powermeter1830c.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index 2a66abf..3f0424a 100644 --- a/__init__.py +++ b/__init__.py @@ -9,7 +9,7 @@ --- - :copyright: 2012 by Lantz Authors, see AUTHORS for more details. + :copyright: 2015 by Lantz Authors, see AUTHORS for more details. :license: BSD, """ diff --git a/powermeter1830c.py b/powermeter1830c.py index 57358a9..8b37fb0 100644 --- a/powermeter1830c.py +++ b/powermeter1830c.py @@ -5,7 +5,7 @@ Implements the drivers to control an Optical Power Meter. - :copyright: 2012 by Lantz Authors, see AUTHORS for more details. + :copyright: 2015 by Lantz Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. Source: Instruction Manual (Newport) From 0ae135a587915f5970c85ac46b5ab85e5ae402db Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 26 Jan 2015 18:01:22 +0100 Subject: [PATCH 08/50] Rudimentary driver for Newport ESP301 motion controllers --- __init__.py | 3 +- motionesp301.py | 298 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 motionesp301.py diff --git a/__init__.py b/__init__.py index 9ee120b..be7bbdf 100644 --- a/__init__.py +++ b/__init__.py @@ -15,5 +15,6 @@ """ from .powermeter1830c import powermeter1830c +from .motionesp301 import ESP301USB, ESP301GPIB -__all__ = ['powermeter1830c'] +__all__ = ['powermeter1830c', 'ESP301USB', 'ESP301GPIB'] diff --git a/motionesp301.py b/motionesp301.py new file mode 100644 index 0000000..9fa3f2c --- /dev/null +++ b/motionesp301.py @@ -0,0 +1,298 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.newport.motionesp301 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implements the drivers to control an ESP301 motion controller via USB or serial. + + For USB, one first have to install the windows driver from newport. + + :copyright: 2014 by Lantz Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. + + Source: Instruction Manual (Newport) +""" + + +from lantz.feat import Feat +from lantz.action import Action +from lantz.serial import SerialDriver +from lantz.visa import GPIBVisaDriver +from lantz import Q_, ureg +from lantz.processors import convert_to +import time + +# Add generic units: +#ureg.define('unit = unit') +#ureg.define('encodercount = count') +#ureg.define('motorstep = step') + + +class ESP301(): + """ Newport ESP301 motion controller. It assumes all axes to have units mm""" + + RECV_TERMINATION = '\r\n' + SEND_TERMINATION = '\r\n' + + def initialize(self): + super().initialize() + + self.axes = [] + self.detect_axis() + + @Action() + def detect_axis(self): + """ Find the number of axis available """ + i = 0 + self.axes = [] + while self.scan_axes: + try: + i += 1 + id = self.query('%dID?' % i) + axis = ESP301Axis(self, i, id) + self.axes.append(axis) + except: + err = int(self.query('TE?')) + if err == 37: # Axis number missing + self.axes.append(None) + elif err == 9: # Axis number out of range + self.scan_axes = False + else: # Dunno... + raise + + def finalize(self): + for axis in self.axes: + if axis is not None: + del (axis) + super().finalize() + + +class ESP301USB(ESP301,SerialDriver): + """ Newport ESP301 motion controller. It assumes all axes to have units mm + + :param scan_axes: Should one detect and add axes to the controller + :param port: Which serial port should be used. 0 for Serial (COM1), 2 for USB (COM3) + :param usb: True if connection is made by USB + :param *args, **kwargs: Passed to lantz.serial.SerialDriver + """ + + ENCODING = 'ascii' + TIMEOUT = 4 + + BAUDRATE = 19200 + BYTESIZE = 8 + PARITY = 'none' + STOPBITS = 1 + + #: flow control flags + RTSCTS = False + DSRDTR = False + XONXOFF = False + + + def __init__(self, scan_axes=True, port=0, usb=True, *args, **kwargs): + if usb: + self.BAUDRATE = 921600 + else: + self.BAUDRATE = 19200 + + super().__init__(port, 4, write_timeout=4, *args, **kwargs) + + self.scan_axes = scan_axes + + +class ESP301GPIB( ESP301, GPIBVisaDriver): + """ Untested! + """ + def __init__(self, scan_axes=True, resource_name= 'GPIB0::2::INSTR', *args, **kwargs): + # Read number of axes and add axis objects + self.scan_axes = scan_axes + super().__init__(resource_name=resource_name, *args, **kwargs) + + +class ESP301Axis(SerialDriver,object): + def __init__(self, parent, num, id, *args, **kwargs): + super(ESP301Axis, self).__init__(*args, **kwargs) + self.parent = parent + self.num = num + self.id = id + self.wait_time = 0.1 # in seconds * Q_(1, 's') + self.backlash = 0 + self.wait_until_done = True + + def __del__(self): + self.parent = None + self.num = None + + def id(self): + return self.id + + @Action() + def on(self): + """Put axis on""" + self.parent.send('%dMO' % self.num) + + @Action() + def off(self): + """Put axis on""" + self.parent.send('%dMF' % self.num) + + @Feat(values={True: '1', False: '0'}) + def is_on(self): + """ + :return: True is axis on, else false + """ + return self.parent.query('%dMO?' % self.num) + + @Action(units='mm') + def define_home(self, val=0): + """Remap current position to home (0), or to new position + + :param val: new position""" + self.parent.send('%dDH%f' % (self.num, val)) + + @Feat(units='mm') + def position(self): + return float(self.parent.query('%dTP?' % self.num)) + + @position.setter + def position(self, pos): + """ + Waits until movement is done if self.wait_until_done = True. + + :param pos: new position + """ + + # First do move to extra position if necessary + self._set_position(pos) + + @Action(units=['mm',None]) + def _set_position(self, pos, wait=None): + """ + Move to an absolute position, taking into account backlash. + + When self.backlash is to a negative value the stage will always move + from low to high values. If necessary, a extra step with length + self.backlash is set. + + :param pos: New position in mm + :param wait: wait until stage is finished + """ + if wait is None: + wait = self.wait_until_done + + # First do move to extra position if necessary + position = self.position + if ( self.backlash < 0 and position > pos) or\ + ( self.backlash > 0 and position < pos): + self.__set_position(pos + self.backlash) + if wait: + self._wait_until_done() + + # Than move to final position + self.__set_position(pos) + if wait: + self._wait_until_done() + + def __set_position(self, pos): + """ + Move stage to a certain position + :param pos: New position + """ + self.parent.send('%dPA%f' % (self.num, pos)) + + @Feat(units='mm/s') + def max_velocity(self): + return float(self.parent.query('%dVU?' % self.num)) + + @max_velocity.setter + def max_velocity(self, velocity): + self.parent.send('%dVU%f' % (self.num, velocity)) + + @Feat(units='mm/s') + def velocity(self): + return float(self.parent.query('%dVA?' % self.num)) + + @velocity.setter + def velocity(self, velocity): + """ + :param velocity: Set the velocity that the axis should use when moving + :return: + """ + self.parent.send('%dVA%f' % (self.num, velocity)) + + @Feat(units='mm/s') + def actual_velocity(self): + return float(self.parent.query('%dTV' % self.num)) + + @actual_velocity.setter + def actual_velocity(self, val): + raise NotImplementedError + + @Action() + def stop(self): + """Emergency stop""" + self.parent.send(u'{0:d}ST'.format(self.num)) + + @Feat(values={True: '1', False: '0'}) + def motion_done(self): + return self.parent.query('%dMD?' % self.num) + + # Not working yet, see https://github.com/hgrecco/lantz/issues/35 + # @Feat(values={Q_('encodercount'): 0, + # Q_('motor step'): 1, + # Q_('millimeter'): 2, + # Q_('micrometer'): 3, + # Q_('inches'): 4, + # Q_('milli-inches'): 5, + # Q_('micro-inches'): 6, + # Q_('degree'): 7, + # Q_('gradian'): 8, + # Q_('radian'): 9, + # Q_('milliradian'): 10, + # Q_('microradian'): 11}) + def units(self): + ret = int(self.parent.query(u'{}SN?'.format(self.num))) + vals = {0 :'encoder count', + 1 :'motor step', + 2 :'millimeter', + 3 :'micrometer', + 4 :'inches', + 5 :'milli-inches', + 6 :'micro-inches', + 7 :'degree', + 8 :'gradian', + 9 :'radian', + 10:'milliradian', + 11:'microradian',} + return vals[ret] + + # @units.setter + # def units(self, val): + # self.parent.send('%SN%' % (self.num, val)) + + def _wait_until_done(self): + #wait_time = convert_to('seconds', on_dimensionless='warn')(self.wait_time) + while not self.motion_done: + time.sleep(self.wait_time) #wait_time.magnitude) + + + +if __name__ == '__main__': + import argparse + import lantz.log + + parser = argparse.ArgumentParser(description='Test ESP301 driver') + parser.add_argument('-p', '--port', type=str, default='1', + help='Serial port to connect to') + + args = parser.parse_args() + lantz.log.log_to_socket(lantz.log.DEBUG) + + with ESP301(args.port) as inst: + # inst.initialize() # Initialize the communication with the power meter + # Find the status of all axes: + for axis in inst.axes: + print('Axis {} Position {} is_on {} max_velocity {} velocity {}'.format(axis.num, axis.position, + axis.is_on, axis.max_velocity, + axis.velocity)) From 6ac136c9c3cd2b66e590e8c382baa52c200cf220 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Wed, 9 Sep 2015 10:01:42 +0200 Subject: [PATCH 09/50] Newport ESP301 driver now works with new-style lantz driver --- __init__.py | 6 +-- motionesp301.py | 127 ++++++++++++++++++++++++++---------------------- 2 files changed, 72 insertions(+), 61 deletions(-) diff --git a/__init__.py b/__init__.py index bff09d9..12f3806 100644 --- a/__init__.py +++ b/__init__.py @@ -14,7 +14,7 @@ """ -from .powermeter1830c import powermeter1830c -from .motionesp301 import ESP301USB, ESP301GPIB +from .powermeter1830c import PowerMeter1830c +from .motionesp301 import ESP301, ESP301Axis -__all__ = ['powermeter1830c', 'ESP301USB', 'ESP301GPIB'] +__all__ = ['PowerMeter1830c', 'ESP301', 'ESP301Axis'] diff --git a/motionesp301.py b/motionesp301.py index 9fa3f2c..68008cd 100644 --- a/motionesp301.py +++ b/motionesp301.py @@ -16,11 +16,14 @@ from lantz.feat import Feat from lantz.action import Action -from lantz.serial import SerialDriver -from lantz.visa import GPIBVisaDriver +#from lantz.serial import SerialDriver +from lantz.messagebased import MessageBasedDriver +from pyvisa import constants +#from lantz.visa import GPIBVisaDriver from lantz import Q_, ureg from lantz.processors import convert_to import time +import numpy as np # Add generic units: #ureg.define('unit = unit') @@ -28,18 +31,40 @@ #ureg.define('motorstep = step') -class ESP301(): - """ Newport ESP301 motion controller. It assumes all axes to have units mm""" +class ESP301(MessageBasedDriver): + """ Newport ESP301 motion controller. It assumes all axes to have units mm + + :param scan_axes: Should one detect and add axes to the controller + """ - RECV_TERMINATION = '\r\n' - SEND_TERMINATION = '\r\n' + DEFAULTS = { + 'COMMON': {'write_termination': '\r\n', + 'read_termination': '\r\n',}, + 'ASRL':{ + 'timeout': 4000, #ms + 'encoding': 'ascii', + 'data_bits': 8, + 'baud_rate': 19200, + 'parity': constants.Parity.none, + 'stop_bits': constants.StopBits.one, + 'flow_control': constants.VI_ASRL_FLOW_RTS_CTS,#constants.VI_ASRL_FLOW_NONE, + }, + } def initialize(self): super().initialize() + self.scan_axes = True self.axes = [] self.detect_axis() + @classmethod + def via_usb(cls, port, name=None, **kwargs): + """Connect to the ESP301 via USB. Internally this goes via serial""" + cls.DEFAULTS['ASRL'].update({'baud_rate': 921600}) + return cls.via_serial(port=port, name=name, **kwargs) + + @Action() def detect_axis(self): """ Find the number of axis available """ @@ -58,7 +83,24 @@ def detect_axis(self): elif err == 9: # Axis number out of range self.scan_axes = False else: # Dunno... - raise + raise Exception(err) + + @Feat() + def position(self): + return [axis.position for axis in self.axes] + + @position.setter + def position(self, pos): + """Move to position (x,y,...)""" + for p, axis in zip(pos, self.axes): + if not p is None: + axis._set_position(p, wait=False) + for p, axis in zip(pos, self.axes): + if not p is None: + axis._wait_until_done() + pos = [axis.position for axis in self.axes] + return pos + def finalize(self): for axis in self.axes: @@ -67,52 +109,20 @@ def finalize(self): super().finalize() -class ESP301USB(ESP301,SerialDriver): - """ Newport ESP301 motion controller. It assumes all axes to have units mm - - :param scan_axes: Should one detect and add axes to the controller - :param port: Which serial port should be used. 0 for Serial (COM1), 2 for USB (COM3) - :param usb: True if connection is made by USB - :param *args, **kwargs: Passed to lantz.serial.SerialDriver - """ - - ENCODING = 'ascii' - TIMEOUT = 4 - - BAUDRATE = 19200 - BYTESIZE = 8 - PARITY = 'none' - STOPBITS = 1 - #: flow control flags - RTSCTS = False - DSRDTR = False - XONXOFF = False - - def __init__(self, scan_axes=True, port=0, usb=True, *args, **kwargs): - if usb: - self.BAUDRATE = 921600 - else: - self.BAUDRATE = 19200 - - super().__init__(port, 4, write_timeout=4, *args, **kwargs) - - self.scan_axes = scan_axes - - -class ESP301GPIB( ESP301, GPIBVisaDriver): - """ Untested! - """ - def __init__(self, scan_axes=True, resource_name= 'GPIB0::2::INSTR', *args, **kwargs): - # Read number of axes and add axis objects - self.scan_axes = scan_axes - super().__init__(resource_name=resource_name, *args, **kwargs) +#class ESP301GPIB( ESP301, GPIBVisaDriver): +# """ Untested! +# """ +# def __init__(self, scan_axes=True, resource_name= 'GPIB0::2::INSTR', *args, **kwargs): +# # Read number of axes and add axis objects +# self.scan_axes = scan_axes +# super().__init__(resource_name=resource_name, *args, **kwargs) -class ESP301Axis(SerialDriver,object): +class ESP301Axis(ESP301): def __init__(self, parent, num, id, *args, **kwargs): - super(ESP301Axis, self).__init__(*args, **kwargs) + #super(ESP301Axis, self).__init__(*args, **kwargs) self.parent = parent self.num = num self.id = id @@ -130,12 +140,12 @@ def id(self): @Action() def on(self): """Put axis on""" - self.parent.send('%dMO' % self.num) + self.parent.write('%dMO' % self.num) @Action() def off(self): """Put axis on""" - self.parent.send('%dMF' % self.num) + self.parent.write('%dMF' % self.num) @Feat(values={True: '1', False: '0'}) def is_on(self): @@ -149,11 +159,12 @@ def define_home(self, val=0): """Remap current position to home (0), or to new position :param val: new position""" - self.parent.send('%dDH%f' % (self.num, val)) + self.parent.write('%dDH%f' % (self.num, val)) @Feat(units='mm') def position(self): - return float(self.parent.query('%dTP?' % self.num)) + self._position_cached = float(self.parent.query('%dTP?' % self.num)) + return self._position_cached @position.setter def position(self, pos): @@ -199,7 +210,7 @@ def __set_position(self, pos): Move stage to a certain position :param pos: New position """ - self.parent.send('%dPA%f' % (self.num, pos)) + self.parent.write('%dPA%f' % (self.num, pos)) @Feat(units='mm/s') def max_velocity(self): @@ -207,7 +218,7 @@ def max_velocity(self): @max_velocity.setter def max_velocity(self, velocity): - self.parent.send('%dVU%f' % (self.num, velocity)) + self.parent.write('%dVU%f' % (self.num, velocity)) @Feat(units='mm/s') def velocity(self): @@ -219,7 +230,7 @@ def velocity(self, velocity): :param velocity: Set the velocity that the axis should use when moving :return: """ - self.parent.send('%dVA%f' % (self.num, velocity)) + self.parent.write('%dVA%f' % (self.num, velocity)) @Feat(units='mm/s') def actual_velocity(self): @@ -232,7 +243,7 @@ def actual_velocity(self, val): @Action() def stop(self): """Emergency stop""" - self.parent.send(u'{0:d}ST'.format(self.num)) + self.parent.write(u'{0:d}ST'.format(self.num)) @Feat(values={True: '1', False: '0'}) def motion_done(self): @@ -269,7 +280,7 @@ def units(self): # @units.setter # def units(self, val): - # self.parent.send('%SN%' % (self.num, val)) + # self.parent.write('%SN%' % (self.num, val)) def _wait_until_done(self): #wait_time = convert_to('seconds', on_dimensionless='warn')(self.wait_time) @@ -289,7 +300,7 @@ def _wait_until_done(self): args = parser.parse_args() lantz.log.log_to_socket(lantz.log.DEBUG) - with ESP301(args.port) as inst: + with ESP301.via_usb(port=args.port) as inst: # inst.initialize() # Initialize the communication with the power meter # Find the status of all axes: for axis in inst.axes: From 2832967a406b166804a5ac48814ef2cbeb634907 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 21 Sep 2015 17:42:42 +0200 Subject: [PATCH 10/50] More improvements on newport motion controller --- motionesp301.py | 648 +++++++++++++++++++++++++----------------------- 1 file changed, 339 insertions(+), 309 deletions(-) diff --git a/motionesp301.py b/motionesp301.py index 68008cd..e207b0d 100644 --- a/motionesp301.py +++ b/motionesp301.py @@ -1,309 +1,339 @@ -# -*- coding: utf-8 -*- -""" - lantz.drivers.newport.motionesp301 - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Implements the drivers to control an ESP301 motion controller via USB or serial. - - For USB, one first have to install the windows driver from newport. - - :copyright: 2014 by Lantz Authors, see AUTHORS for more details. - :license: BSD, see LICENSE for more details. - - Source: Instruction Manual (Newport) -""" - - -from lantz.feat import Feat -from lantz.action import Action -#from lantz.serial import SerialDriver -from lantz.messagebased import MessageBasedDriver -from pyvisa import constants -#from lantz.visa import GPIBVisaDriver -from lantz import Q_, ureg -from lantz.processors import convert_to -import time -import numpy as np - -# Add generic units: -#ureg.define('unit = unit') -#ureg.define('encodercount = count') -#ureg.define('motorstep = step') - - -class ESP301(MessageBasedDriver): - """ Newport ESP301 motion controller. It assumes all axes to have units mm - - :param scan_axes: Should one detect and add axes to the controller - """ - - DEFAULTS = { - 'COMMON': {'write_termination': '\r\n', - 'read_termination': '\r\n',}, - 'ASRL':{ - 'timeout': 4000, #ms - 'encoding': 'ascii', - 'data_bits': 8, - 'baud_rate': 19200, - 'parity': constants.Parity.none, - 'stop_bits': constants.StopBits.one, - 'flow_control': constants.VI_ASRL_FLOW_RTS_CTS,#constants.VI_ASRL_FLOW_NONE, - }, - } - - def initialize(self): - super().initialize() - - self.scan_axes = True - self.axes = [] - self.detect_axis() - - @classmethod - def via_usb(cls, port, name=None, **kwargs): - """Connect to the ESP301 via USB. Internally this goes via serial""" - cls.DEFAULTS['ASRL'].update({'baud_rate': 921600}) - return cls.via_serial(port=port, name=name, **kwargs) - - - @Action() - def detect_axis(self): - """ Find the number of axis available """ - i = 0 - self.axes = [] - while self.scan_axes: - try: - i += 1 - id = self.query('%dID?' % i) - axis = ESP301Axis(self, i, id) - self.axes.append(axis) - except: - err = int(self.query('TE?')) - if err == 37: # Axis number missing - self.axes.append(None) - elif err == 9: # Axis number out of range - self.scan_axes = False - else: # Dunno... - raise Exception(err) - - @Feat() - def position(self): - return [axis.position for axis in self.axes] - - @position.setter - def position(self, pos): - """Move to position (x,y,...)""" - for p, axis in zip(pos, self.axes): - if not p is None: - axis._set_position(p, wait=False) - for p, axis in zip(pos, self.axes): - if not p is None: - axis._wait_until_done() - pos = [axis.position for axis in self.axes] - return pos - - - def finalize(self): - for axis in self.axes: - if axis is not None: - del (axis) - super().finalize() - - - - -#class ESP301GPIB( ESP301, GPIBVisaDriver): -# """ Untested! -# """ -# def __init__(self, scan_axes=True, resource_name= 'GPIB0::2::INSTR', *args, **kwargs): -# # Read number of axes and add axis objects -# self.scan_axes = scan_axes -# super().__init__(resource_name=resource_name, *args, **kwargs) - - -class ESP301Axis(ESP301): - def __init__(self, parent, num, id, *args, **kwargs): - #super(ESP301Axis, self).__init__(*args, **kwargs) - self.parent = parent - self.num = num - self.id = id - self.wait_time = 0.1 # in seconds * Q_(1, 's') - self.backlash = 0 - self.wait_until_done = True - - def __del__(self): - self.parent = None - self.num = None - - def id(self): - return self.id - - @Action() - def on(self): - """Put axis on""" - self.parent.write('%dMO' % self.num) - - @Action() - def off(self): - """Put axis on""" - self.parent.write('%dMF' % self.num) - - @Feat(values={True: '1', False: '0'}) - def is_on(self): - """ - :return: True is axis on, else false - """ - return self.parent.query('%dMO?' % self.num) - - @Action(units='mm') - def define_home(self, val=0): - """Remap current position to home (0), or to new position - - :param val: new position""" - self.parent.write('%dDH%f' % (self.num, val)) - - @Feat(units='mm') - def position(self): - self._position_cached = float(self.parent.query('%dTP?' % self.num)) - return self._position_cached - - @position.setter - def position(self, pos): - """ - Waits until movement is done if self.wait_until_done = True. - - :param pos: new position - """ - - # First do move to extra position if necessary - self._set_position(pos) - - @Action(units=['mm',None]) - def _set_position(self, pos, wait=None): - """ - Move to an absolute position, taking into account backlash. - - When self.backlash is to a negative value the stage will always move - from low to high values. If necessary, a extra step with length - self.backlash is set. - - :param pos: New position in mm - :param wait: wait until stage is finished - """ - if wait is None: - wait = self.wait_until_done - - # First do move to extra position if necessary - position = self.position - if ( self.backlash < 0 and position > pos) or\ - ( self.backlash > 0 and position < pos): - self.__set_position(pos + self.backlash) - if wait: - self._wait_until_done() - - # Than move to final position - self.__set_position(pos) - if wait: - self._wait_until_done() - - def __set_position(self, pos): - """ - Move stage to a certain position - :param pos: New position - """ - self.parent.write('%dPA%f' % (self.num, pos)) - - @Feat(units='mm/s') - def max_velocity(self): - return float(self.parent.query('%dVU?' % self.num)) - - @max_velocity.setter - def max_velocity(self, velocity): - self.parent.write('%dVU%f' % (self.num, velocity)) - - @Feat(units='mm/s') - def velocity(self): - return float(self.parent.query('%dVA?' % self.num)) - - @velocity.setter - def velocity(self, velocity): - """ - :param velocity: Set the velocity that the axis should use when moving - :return: - """ - self.parent.write('%dVA%f' % (self.num, velocity)) - - @Feat(units='mm/s') - def actual_velocity(self): - return float(self.parent.query('%dTV' % self.num)) - - @actual_velocity.setter - def actual_velocity(self, val): - raise NotImplementedError - - @Action() - def stop(self): - """Emergency stop""" - self.parent.write(u'{0:d}ST'.format(self.num)) - - @Feat(values={True: '1', False: '0'}) - def motion_done(self): - return self.parent.query('%dMD?' % self.num) - - # Not working yet, see https://github.com/hgrecco/lantz/issues/35 - # @Feat(values={Q_('encodercount'): 0, - # Q_('motor step'): 1, - # Q_('millimeter'): 2, - # Q_('micrometer'): 3, - # Q_('inches'): 4, - # Q_('milli-inches'): 5, - # Q_('micro-inches'): 6, - # Q_('degree'): 7, - # Q_('gradian'): 8, - # Q_('radian'): 9, - # Q_('milliradian'): 10, - # Q_('microradian'): 11}) - def units(self): - ret = int(self.parent.query(u'{}SN?'.format(self.num))) - vals = {0 :'encoder count', - 1 :'motor step', - 2 :'millimeter', - 3 :'micrometer', - 4 :'inches', - 5 :'milli-inches', - 6 :'micro-inches', - 7 :'degree', - 8 :'gradian', - 9 :'radian', - 10:'milliradian', - 11:'microradian',} - return vals[ret] - - # @units.setter - # def units(self, val): - # self.parent.write('%SN%' % (self.num, val)) - - def _wait_until_done(self): - #wait_time = convert_to('seconds', on_dimensionless='warn')(self.wait_time) - while not self.motion_done: - time.sleep(self.wait_time) #wait_time.magnitude) - - - -if __name__ == '__main__': - import argparse - import lantz.log - - parser = argparse.ArgumentParser(description='Test ESP301 driver') - parser.add_argument('-p', '--port', type=str, default='1', - help='Serial port to connect to') - - args = parser.parse_args() - lantz.log.log_to_socket(lantz.log.DEBUG) - - with ESP301.via_usb(port=args.port) as inst: - # inst.initialize() # Initialize the communication with the power meter - # Find the status of all axes: - for axis in inst.axes: - print('Axis {} Position {} is_on {} max_velocity {} velocity {}'.format(axis.num, axis.position, - axis.is_on, axis.max_velocity, - axis.velocity)) +# -*- coding: utf-8 -*- +""" + lantz.drivers.newport.motionesp301 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implements the drivers to control an ESP301 motion controller via USB or serial. + + For USB, one first have to install the windows driver from newport. + + :copyright: 2015, see AUTHORS for more details. + :license: GPL, see LICENSE for more details. + + Source: Instruction Manual (Newport) +""" + + +from lantz.feat import Feat +from lantz.action import Action +#from lantz.serial import SerialDriver +from lantz.messagebased import MessageBasedDriver +from pyvisa import constants +#from lantz.visa import GPIBVisaDriver +from lantz import Q_, ureg +from lantz.processors import convert_to +import time +import numpy as np + +# Add generic units: +#ureg.define('unit = unit') +#ureg.define('encodercount = count') +#ureg.define('motorstep = step') + + +class ESP301(MessageBasedDriver): + """ Newport ESP301 motion controller. It assumes all axes to have units mm + + :param scan_axes: Should one detect and add axes to the controller + """ + + DEFAULTS = { + 'COMMON': {'write_termination': '\r\n', + 'read_termination': '\r\n',}, + 'ASRL':{ + 'timeout': 4000, #ms + 'encoding': 'ascii', + 'data_bits': 8, + 'baud_rate': 19200, + 'parity': constants.Parity.none, + 'stop_bits': constants.StopBits.one, + 'flow_control': constants.VI_ASRL_FLOW_RTS_CTS,#constants.VI_ASRL_FLOW_NONE, + }, + } + + def initialize(self): + super().initialize() + + self.scan_axes = True + self.axes = [] + self.detect_axis() + + @classmethod + def via_usb(cls, port, name=None, **kwargs): + """Connect to the ESP301 via USB. Internally this goes via serial""" + cls.DEFAULTS['ASRL'].update({'baud_rate': 921600}) + return cls.via_serial(port=port, name=name, **kwargs) + + + @Action() + def detect_axis(self): + """ Find the number of axis available """ + i = 0 + self.axes = [] + while self.scan_axes: + try: + i += 1 + id = self.query('%dID?' % i) + axis = ESP301Axis(self, i, id) + self.axes.append(axis) + except: + err = int(self.query('TE?')) + if err == 37: # Axis number missing + self.axes.append(None) + elif err == 9: # Axis number out of range + self.scan_axes = False + else: # Dunno... + raise Exception(err) + + @Feat() + def position(self): + return [axis.position for axis in self.axes] + + @Feat() + def _position_cached(self): + return [axis._position_cached for axis in self.axes] + + @position.setter + def position(self, pos): + """Move to position (x,y,...)""" + return self._position(pos, read_pos=True) + + @Action() + def _position(self, pos, read_pos=False): + """Move to position (x,y,...)""" + for p, axis in zip(pos, self.axes): + if not p is None: + axis._set_position(p, wait=False) + for p, axis in zip(pos, self.axes): + if not p is None: + axis._wait_until_done() + if read_pos: + pos = [axis.position for axis in self.axes] + else: + for p, axis in zip(pos, self.axes): + if not p is None: + axis._position_cached = pos + return pos + + def finalize(self): + for axis in self.axes: + if axis is not None: + del (axis) + super().finalize() + + + + +#class ESP301GPIB( ESP301, GPIBVisaDriver): +# """ Untested! +# """ +# def __init__(self, scan_axes=True, resource_name= 'GPIB0::2::INSTR', *args, **kwargs): +# # Read number of axes and add axis objects +# self.scan_axes = scan_axes +# super().__init__(resource_name=resource_name, *args, **kwargs) + + +class ESP301Axis(ESP301): + def __init__(self, parent, num, id, *args, **kwargs): + #super(ESP301Axis, self).__init__(*args, **kwargs) + self.parent = parent + self.num = num + self.id = id + self.wait_time = 0.01 # in seconds * Q_(1, 's') + self.backlash = 0 + self.wait_until_done = True + self.position + + def __del__(self): + self.parent = None + self.num = None + + def id(self): + return self.id + + @Action() + def on(self): + """Put axis on""" + self.parent.write('%dMO' % self.num) + + @Action() + def off(self): + """Put axis on""" + self.parent.write('%dMF' % self.num) + + @Feat(values={True: '1', False: '0'}) + def is_on(self): + """ + :return: True is axis on, else false + """ + return self.parent.query('%dMO?' % self.num) + + @Action(units='mm') + def define_home(self, val=0): + """Remap current position to home (0), or to new position + + :param val: new position""" + self.parent.write('%dDH%f' % (self.num, val)) + + @Feat(units='mm') + def _position_cached(self): + return self.__position_cached + + @_position_cached.setter + def _position_cached(self, pos): + self.__position_cached = pos + + @Feat(units='mm') + def position(self): + self._position_cached = float(self.parent.query('%dTP?' % self.num)) + return self._position_cached + + @position.setter + def position(self, pos): + """ + Waits until movement is done if self.wait_until_done = True. + + :param pos: new position + """ + + # First do move to extra position if necessary + self._set_position(pos) + + @Action(units=['mm',None]) + def _set_position(self, pos, wait=None): + """ + Move to an absolute position, taking into account backlash. + + When self.backlash is to a negative value the stage will always move + from low to high values. If necessary, a extra step with length + self.backlash is set. + + :param pos: New position in mm + :param wait: wait until stage is finished + """ + if wait is None: + wait = self.wait_until_done + + # First do move to extra position if necessary + if self.backlash: + position = self.position + if ( self.backlash < 0 and position > pos) or\ + ( self.backlash > 0 and position < pos): + self.__set_position(pos + self.backlash) + if wait: + self._wait_until_done() + + # Than move to final position + self.__set_position(pos) + if wait: + self._wait_until_done() + + def __set_position(self, pos): + """ + Move stage to a certain position + :param pos: New position + """ + self.parent.write('%dPA%f' % (self.num, pos)) + + @Feat(units='mm/s') + def max_velocity(self): + return float(self.parent.query('%dVU?' % self.num)) + + @max_velocity.setter + def max_velocity(self, velocity): + self.parent.write('%dVU%f' % (self.num, velocity)) + + @Feat(units='mm/s') + def velocity(self): + return float(self.parent.query('%dVA?' % self.num)) + + @velocity.setter + def velocity(self, velocity): + """ + :param velocity: Set the velocity that the axis should use when moving + :return: + """ + self.parent.write('%dVA%f' % (self.num, velocity)) + + @Feat(units='mm/s') + def actual_velocity(self): + return float(self.parent.query('%dTV' % self.num)) + + @actual_velocity.setter + def actual_velocity(self, val): + raise NotImplementedError + + @Action() + def stop(self): + """Emergency stop""" + self.parent.write(u'{0:d}ST'.format(self.num)) + + @Feat(values={True: '1', False: '0'}) + def motion_done(self): + while True: + ret = self.parent.query('%dMD?' % self.num) + if ret in ['1','0']: + break + else: + time.sleep(self.wait_time) + return ret + + # Not working yet, see https://github.com/hgrecco/lantz/issues/35 + # @Feat(values={Q_('encodercount'): 0, + # Q_('motor step'): 1, + # Q_('millimeter'): 2, + # Q_('micrometer'): 3, + # Q_('inches'): 4, + # Q_('milli-inches'): 5, + # Q_('micro-inches'): 6, + # Q_('degree'): 7, + # Q_('gradian'): 8, + # Q_('radian'): 9, + # Q_('milliradian'): 10, + # Q_('microradian'): 11}) + def units(self): + ret = int(self.parent.query(u'{}SN?'.format(self.num))) + vals = {0 :'encoder count', + 1 :'motor step', + 2 :'millimeter', + 3 :'micrometer', + 4 :'inches', + 5 :'milli-inches', + 6 :'micro-inches', + 7 :'degree', + 8 :'gradian', + 9 :'radian', + 10:'milliradian', + 11:'microradian',} + return vals[ret] + + # @units.setter + # def units(self, val): + # self.parent.write('%SN%' % (self.num, val)) + + def _wait_until_done(self): + #wait_time = convert_to('seconds', on_dimensionless='warn')(self.wait_time) + time.sleep(self.wait_time) + while not self.motion_done: + time.sleep(self.wait_time) #wait_time.magnitude) + + + +if __name__ == '__main__': + import argparse + import lantz.log + + parser = argparse.ArgumentParser(description='Test ESP301 driver') + parser.add_argument('-p', '--port', type=str, default='1', + help='Serial port to connect to') + + args = parser.parse_args() + lantz.log.log_to_socket(lantz.log.DEBUG) + + with ESP301.via_usb(port=args.port) as inst: + # inst.initialize() # Initialize the communication with the power meter + # Find the status of all axes: + for axis in inst.axes: + print('Axis {} Position {} is_on {} max_velocity {} velocity {}'.format(axis.num, axis.position, + axis.is_on, axis.max_velocity, + axis.velocity)) From e8189f831d3b8f74050e22a9798e5a97b0df5f8c Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 21 Sep 2015 17:52:12 +0200 Subject: [PATCH 11/50] Initial commit --- .gitignore | 57 +++++ LICENSE | 675 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 732 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba74660 --- /dev/null +++ b/.gitignore @@ -0,0 +1,57 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..733c072 --- /dev/null +++ b/LICENSE @@ -0,0 +1,675 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + From 647e41245f5f9f534ec6f8ba14c369e1fed19493 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 21 Sep 2015 17:55:38 +0200 Subject: [PATCH 12/50] Added AUTHORS and removed powermeter from the stage repository --- AUTHORS | 1 + powermeter1830c.py | 199 --------------------------------------------- 2 files changed, 1 insertion(+), 199 deletions(-) create mode 100644 AUTHORS delete mode 100644 powermeter1830c.py diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..44c8c01 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Vasco Tenner diff --git a/powermeter1830c.py b/powermeter1830c.py deleted file mode 100644 index 8b37fb0..0000000 --- a/powermeter1830c.py +++ /dev/null @@ -1,199 +0,0 @@ -# -*- coding: utf-8 -*- -""" - lantz.drivers.newport.powermeter1830c - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Implements the drivers to control an Optical Power Meter. - - :copyright: 2015 by Lantz Authors, see AUTHORS for more details. - :license: BSD, see LICENSE for more details. - - Source: Instruction Manual (Newport) -""" - -from pyvisa import constants - -from lantz.feat import Feat -from lantz.action import Action -from lantz.messagebased import MessageBasedDriver - - -class PowerMeter1830c(MessageBasedDriver): - """ Newport 1830c Power Meter - """ - - DEFAULTS = {'ASRL': {'write_termination': '\n', - 'read_termination': '\n', - 'baud_rate': 9600, - 'bytesize': 8, - 'parity': constants.Parity.none, - 'stop_bits': constants.StopBits.one, - 'encoding': 'ascii', - 'timeout': 2000 - }} - - - @Feat(values={True: 1, False: 0}) - def attenuator(self): - """ Attenuator. - 1: Attenuator present - 0: Attenuator not present - """ - return int(self.query('A?')) - - @attenuator.setter - def attenuator(self, value): - self.send('A{}'.format(value)) - - @Feat(values={True: 1, False: 0}) - def beeper(self): - """ Checks whether the audio output is on or off. - """ - return int(self.query('B?')) - - @beeper.setter - def beeper(self,value): - self.send('B{}'.format(value)) - - @Feat - def data(self): - """ Retrieves the value from the power meter. - """ - return float(self.query('D?')) - - @Feat(values={True: 1, False: 0}) - def echo(self): - """ Returns echo mode. Only applied to RS232 communication - """ - return int(self.query('E?')) - - @echo.setter - def echo(self,value): - self.send('E{}'.format(value)) - - @Feat(values={'Slow': 1, 'Medium': 2, 'Fast': 3}) - def filter(self): - """ How many measurements are averaged for the displayed reading. - slow: 16 measurements - medium: 4 measurements - fast: 1 measurement. - """ - return int(self.query('F?')) - - @filter.setter - def filter(self,value): - self.send('F{}'.format(value)) - - @Feat(values={True: 1, False: 0}) - def go(self): - """ Enable or disable the power meter from taking new measurements. - """ - return int(self.query('G?')) - - @go.setter - def go(self,value): - self.send('G{}'.format(value)) - - @Feat(values={'Off': 0, 'Medium': 1, 'High': 2}) - def keypad(self): - """ Keypad/Display backlight intensity levels. - """ - return int(self.query('K?')) - - @keypad.setter - def keypad(self,value): - self.send('K{}'.format(value)) - - @Feat(values={True: 1, False: 0}) - def lockout(self): - """ Enable/Disable the lockout. When the lockout is enabled, any front panel key presses would have no effect on system operation. - """ - return int(self.query('L?')) - - @lockout.setter - def lockout(self,value): - self.send('L{}'.format(value)) - - @Action() - def autocalibration(self): - """ Autocalibration of the power meter. This procedure disconnects the input signal. - It should be performed at least 60 minutes after warm-up. - """ - self.send('O') - - @Feat(values=set(range(0,9))) - def range(self): - """ Set the signal range for the input signal. - 0 means auto setting the range. 1 is the lowest signal range and 8 the highest. - """ - return int(self.query('R?')) - - @range.setter - def range(self,value): - self.send('R{}'.format(value)) - - @Action() - def store_reference(self): - """ Sets the current input signal power level as the power reference level. - Each time the S command is sent, the current input signal becomes the new reference level. - """ - self.send('S') - - @Feat(values={'Watts': 1, 'dB': 2, 'dBm': 3, 'REL': 4}) - def units(self): - """ Sets and gets the units of the measurements. - """ - return int(self.query('U?')) - - @units.setter - def units(self,value): - self.send('U{}'.format(value)) - - @Feat(limits=(1,10000,1)) - def wavelength(self): - """ Sets and gets the wavelength of the input signal. - """ - return int(self.query('W?')) - - @wavelength.setter - def wavelength(self,value): - self.send('W{}'.format(int(value))) - - @Feat(values={True: 1, False: 0}) - def zero(self): - """ Turn the zero function on/off. Zero function is used for subtracting any background power levels in future measurements. - """ - return int(self.query('Z?')) - - @zero.setter - def zero(self,value): - self.send('Z{}'.format(value)) - -if __name__ == '__main__': - import argparse - import lantz.log - - parser = argparse.ArgumentParser(description='Test Kentech HRI') - parser.add_argument('-p', '--port', type=str, default='1', - help='Serial port to connect to') - - args = parser.parse_args() - lantz.log.log_to_socket(lantz.log.DEBUG) - - with PowerMeter1830c.from_serial_port(args.port) as inst: - - inst.initialize() # Initialize the communication with the power meter - - inst.lockout = True # Blocks the front panel - inst.keypad = 'Off' # Switches the keypad off - inst.attenuator = True # The attenuator is on - inst.wavelength = 633 # Sets the wavelength to 633nm - inst.units = "Watts" # Sets the units to Watts - inst.filter = 'Slow' # Averages 16 measurements - - if not inst.go: - inst.go = True # If the instrument is not running, enables it - - inst.range = 0 # Auto-sets the range - - print('The measured power is {} Watts'.format(inst.data)) From 077cf1ec3e4597966396fd63ceda0dedc9ed9eea Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 21 Sep 2015 17:56:54 +0200 Subject: [PATCH 13/50] Also removed powermeter from __init__ --- __init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/__init__.py b/__init__.py index 12f3806..15392d5 100644 --- a/__init__.py +++ b/__init__.py @@ -9,12 +9,11 @@ --- - :copyright: 2015 by Lantz Authors, see AUTHORS for more details. - :license: BSD, + :copyright: 2015, see AUTHORS for more details. + :license: GPLv3, """ -from .powermeter1830c import PowerMeter1830c from .motionesp301 import ESP301, ESP301Axis -__all__ = ['PowerMeter1830c', 'ESP301', 'ESP301Axis'] +__all__ = ['ESP301', 'ESP301Axis'] From b189a126f0c56b3cc0b15e5c64f36ff0243de117 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Fri, 13 Nov 2015 18:17:00 +0100 Subject: [PATCH 14/50] Problems with backlash and units. Reduced some caching --- motionesp301.py | 47 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/motionesp301.py b/motionesp301.py index e207b0d..2835e32 100644 --- a/motionesp301.py +++ b/motionesp301.py @@ -24,6 +24,7 @@ from lantz.processors import convert_to import time import numpy as np +import copy # Add generic units: #ureg.define('unit = unit') @@ -61,6 +62,7 @@ def initialize(self): @classmethod def via_usb(cls, port, name=None, **kwargs): """Connect to the ESP301 via USB. Internally this goes via serial""" + cls.DEFAULTS = copy.deepcopy(cls.DEFAULTS) cls.DEFAULTS['ASRL'].update({'baud_rate': 921600}) return cls.via_serial(port=port, name=name, **kwargs) @@ -85,11 +87,11 @@ def detect_axis(self): else: # Dunno... raise Exception(err) - @Feat() + @Feat(read_once=False) def position(self): return [axis.position for axis in self.axes] - @Feat() + @Feat(read_once=False) def _position_cached(self): return [axis._position_cached for axis in self.axes] @@ -175,7 +177,7 @@ def define_home(self, val=0): :param val: new position""" self.parent.write('%dDH%f' % (self.num, val)) - @Feat(units='mm') + @Feat(units='mm', read_once=False) def _position_cached(self): return self.__position_cached @@ -185,7 +187,7 @@ def _position_cached(self, pos): @Feat(units='mm') def position(self): - self._position_cached = float(self.parent.query('%dTP?' % self.num)) + self._position_cached = float(self.parent.query('%dTP?' % self.num))*ureg.mm return self._position_cached @position.setter @@ -216,12 +218,16 @@ def _set_position(self, pos, wait=None): # First do move to extra position if necessary if self.backlash: - position = self.position - if ( self.backlash < 0 and position > pos) or\ - ( self.backlash > 0 and position < pos): - self.__set_position(pos + self.backlash) - if wait: - self._wait_until_done() + position = self.position.magnitude + #backlash = self.backlash.to('mm').magnitude + backlash = convert_to('mm', on_dimensionless='ignore')(self.backlash) + if ( backlash < 0 and position > pos) or\ + ( backlash > 0 and position < pos): + + self.log_info('Using backlash') + self.__set_position(pos + backlash) + if wait: + self._wait_until_done() # Than move to final position self.__set_position(pos) @@ -242,6 +248,15 @@ def max_velocity(self): @max_velocity.setter def max_velocity(self, velocity): self.parent.write('%dVU%f' % (self.num, velocity)) + + @Feat(units='mm/s**2') + def max_acceleration(self): + return float(self.parent.query('%dAU?' % self.num)) + + @max_acceleration.setter + def max_acceleration(self, velocity): + self.parent.write('%dAU%f' % (self.num, velocity)) + @Feat(units='mm/s') def velocity(self): @@ -254,6 +269,18 @@ def velocity(self, velocity): :return: """ self.parent.write('%dVA%f' % (self.num, velocity)) + + @Feat(units='mm/s**2') + def acceleration(self): + return float(self.parent.query('%dVA?' % self.num)) + + @acceleration.setter + def acceleration(self, acceleration): + """ + :param acceleration: Set the acceleration that the axis should use when starting + :return: + """ + self.parent.write('%dAC%f' % (self.num, acceleration)) @Feat(units='mm/s') def actual_velocity(self): From 6f6b779d0509a5423df6d7721bff0b716332c106 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Thu, 3 Dec 2015 15:13:06 +0100 Subject: [PATCH 15/50] Axes detection more robust and backlash working --- motionesp301.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/motionesp301.py b/motionesp301.py index 2835e32..18ee4d7 100644 --- a/motionesp301.py +++ b/motionesp301.py @@ -84,6 +84,8 @@ def detect_axis(self): self.axes.append(None) elif err == 9: # Axis number out of range self.scan_axes = False + elif err == 6: # Axis number out of range, but wrong errorcode + self.scan_axes = False else: # Dunno... raise Exception(err) @@ -220,7 +222,7 @@ def _set_position(self, pos, wait=None): if self.backlash: position = self.position.magnitude #backlash = self.backlash.to('mm').magnitude - backlash = convert_to('mm', on_dimensionless='ignore')(self.backlash) + backlash = convert_to('mm', on_dimensionless='ignore')(self.backlash).magnitude if ( backlash < 0 and position > pos) or\ ( backlash > 0 and position < pos): From f801793382da2c3ab9adb99974dce200f208dbaf Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Fri, 8 Apr 2016 12:03:56 +0200 Subject: [PATCH 16/50] The implementation of backlash and wait until done is problematic. Now always wait until done. --- motionesp301.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/motionesp301.py b/motionesp301.py index 18ee4d7..d3f3300 100644 --- a/motionesp301.py +++ b/motionesp301.py @@ -215,8 +215,6 @@ def _set_position(self, pos, wait=None): :param pos: New position in mm :param wait: wait until stage is finished """ - if wait is None: - wait = self.wait_until_done # First do move to extra position if necessary if self.backlash: @@ -228,8 +226,7 @@ def _set_position(self, pos, wait=None): self.log_info('Using backlash') self.__set_position(pos + backlash) - if wait: - self._wait_until_done() + self._wait_until_done() # Than move to final position self.__set_position(pos) From 2b34d126cd609f531791e5edfeadae1d42c92494 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 25 Apr 2016 13:20:35 +0200 Subject: [PATCH 17/50] Moved more into detect_axis function --- motionesp301.py | 17 +-- motionesp301.py~ | 368 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 377 insertions(+), 8 deletions(-) create mode 100644 motionesp301.py~ diff --git a/motionesp301.py b/motionesp301.py index d3f3300..024e672 100644 --- a/motionesp301.py +++ b/motionesp301.py @@ -54,9 +54,6 @@ class ESP301(MessageBasedDriver): def initialize(self): super().initialize() - - self.scan_axes = True - self.axes = [] self.detect_axis() @classmethod @@ -69,10 +66,14 @@ def via_usb(cls, port, name=None, **kwargs): @Action() def detect_axis(self): - """ Find the number of axis available """ - i = 0 + """ Find the number of axis available. + + The detection stops as soon as an empty controller is found. + """ self.axes = [] - while self.scan_axes: + i = 0 + scan_axes = True + while scan_axes: try: i += 1 id = self.query('%dID?' % i) @@ -83,9 +84,9 @@ def detect_axis(self): if err == 37: # Axis number missing self.axes.append(None) elif err == 9: # Axis number out of range - self.scan_axes = False + scan_axes = False elif err == 6: # Axis number out of range, but wrong errorcode - self.scan_axes = False + scan_axes = False else: # Dunno... raise Exception(err) diff --git a/motionesp301.py~ b/motionesp301.py~ new file mode 100644 index 0000000..0f4c6df --- /dev/null +++ b/motionesp301.py~ @@ -0,0 +1,368 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.newport.motionesp301 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implements the drivers to control an ESP301 motion controller via USB or serial. + + For USB, one first have to install the windows driver from newport. + + :copyright: 2015, see AUTHORS for more details. + :license: GPL, see LICENSE for more details. + + Source: Instruction Manual (Newport) +""" + + +from lantz.feat import Feat +from lantz.action import Action +#from lantz.serial import SerialDriver +from lantz.messagebased import MessageBasedDriver +from pyvisa import constants +#from lantz.visa import GPIBVisaDriver +from lantz import Q_, ureg +from lantz.processors import convert_to +import time +import numpy as np +import copy + +# Add generic units: +#ureg.define('unit = unit') +#ureg.define('encodercount = count') +#ureg.define('motorstep = step') + + +class ESP301(MessageBasedDriver): + """ Newport ESP301 motion controller. It assumes all axes to have units mm + + :param scan_axes: Should one detect and add axes to the controller + """ + + DEFAULTS = { + 'COMMON': {'write_termination': '\r\n', + 'read_termination': '\r\n',}, + 'ASRL':{ + 'timeout': 4000, #ms + 'encoding': 'ascii', + 'data_bits': 8, + 'baud_rate': 19200, + 'parity': constants.Parity.none, + 'stop_bits': constants.StopBits.one, + 'flow_control': constants.VI_ASRL_FLOW_RTS_CTS,#constants.VI_ASRL_FLOW_NONE, + }, + } + + def initialize(self): + super().initialize() + + self.scan_axes = True + self.axes = [] + self.detect_axis() + + @classmethod + def via_usb(cls, port, name=None, **kwargs): + """Connect to the ESP301 via USB. Internally this goes via serial""" + cls.DEFAULTS = copy.deepcopy(cls.DEFAULTS) + cls.DEFAULTS['ASRL'].update({'baud_rate': 921600}) + return cls.via_serial(port=port, name=name, **kwargs) + + + @Action() + def detect_axis(self): + """ Find the number of axis available """ + i = 0 + self.axes = [] + while self.scan_axes: + try: + i += 1 + id = self.query('%dID?' % i) + axis = ESP301Axis(self, i, id) + self.axes.append(axis) + except: + err = int(self.query('TE?')) + if err == 37: # Axis number missing + self.axes.append(None) + elif err == 9: # Axis number out of range + self.scan_axes = False + elif err == 6: # Axis number out of range, but wrong errorcode + self.scan_axes = False + else: # Dunno... + raise Exception(err) + + @Feat(read_once=False) + def position(self): + return [axis.position for axis in self.axes] + + @Feat(read_once=False) + def _position_cached(self): + return [axis._position_cached for axis in self.axes] + + @position.setter + def position(self, pos): + """Move to position (x,y,...)""" + return self._position(pos, read_pos=True) + + @Action() + def _position(self, pos, read_pos=False): + """Move to position (x,y,...)""" + for p, axis in zip(pos, self.axes): + if not p is None: + axis._set_position(p, wait=False) + for p, axis in zip(pos, self.axes): + if not p is None: + axis._wait_until_done() + if read_pos: + pos = [axis.position for axis in self.axes] + else: + for p, axis in zip(pos, self.axes): + if not p is None: + axis._position_cached = pos + return pos + + def finalize(self): + for axis in self.axes: + if axis is not None: + del (axis) + super().finalize() + + + + +#class ESP301GPIB( ESP301, GPIBVisaDriver): +# """ Untested! +# """ +# def __init__(self, scan_axes=True, resource_name= 'GPIB0::2::INSTR', *args, **kwargs): +# # Read number of axes and add axis objects +# self.scan_axes = scan_axes +# super().__init__(resource_name=resource_name, *args, **kwargs) + + +class ESP301Axis(ESP301): + def __init__(self, parent, num, id, *args, **kwargs): + #super(ESP301Axis, self).__init__(*args, **kwargs) + self.parent = parent + self.num = num + self.id = id + self.wait_time = 0.01 # in seconds * Q_(1, 's') + self.backlash = 0 + self.wait_until_done = True + self.position + + def __del__(self): + self.parent = None + self.num = None + + def id(self): + return self.id + + @Action() + def on(self): + """Put axis on""" + self.parent.write('%dMO' % self.num) + + @Action() + def off(self): + """Put axis on""" + self.parent.write('%dMF' % self.num) + + @Feat(values={True: '1', False: '0'}) + def is_on(self): + """ + :return: True is axis on, else false + """ + return self.parent.query('%dMO?' % self.num) + + @Action(units='mm') + def define_home(self, val=0): + """Remap current position to home (0), or to new position + + :param val: new position""" + self.parent.write('%dDH%f' % (self.num, val)) + + @Feat(units='mm', read_once=False) + def _position_cached(self): + return self.__position_cached + + @_position_cached.setter + def _position_cached(self, pos): + self.__position_cached = pos + + @Feat(units='mm') + def position(self): + self._position_cached = float(self.parent.query('%dTP?' % self.num))*ureg.mm + return self._position_cached + + @position.setter + def position(self, pos): + """ + Waits until movement is done if self.wait_until_done = True. + + :param pos: new position + """ + + # First do move to extra position if necessary + self._set_position(pos) + + @Action(units=['mm',None]) + def _set_position(self, pos, wait=None): + """ + Move to an absolute position, taking into account backlash. + + When self.backlash is to a negative value the stage will always move + from low to high values. If necessary, a extra step with length + self.backlash is set. + + :param pos: New position in mm + :param wait: wait until stage is finished + """ + if wait is None: + wait = self.wait_until_done + + # First do move to extra position if necessary + if self.backlash: + position = self.position.magnitude + #backlash = self.backlash.to('mm').magnitude + backlash = convert_to('mm', on_dimensionless='ignore')(self.backlash).magnitude + if ( backlash < 0 and position > pos) or\ + ( backlash > 0 and position < pos): + + self.log_info('Using backlash') + self.__set_position(pos + backlash) + if wait: + self._wait_until_done() + + # Than move to final position + self.__set_position(pos) + if wait: + self._wait_until_done() + + def __set_position(self, pos): + """ + Move stage to a certain position + :param pos: New position + """ + self.parent.write('%dPA%f' % (self.num, pos)) + + @Feat(units='mm/s') + def max_velocity(self): + return float(self.parent.query('%dVU?' % self.num)) + + @max_velocity.setter + def max_velocity(self, velocity): + self.parent.write('%dVU%f' % (self.num, velocity)) + + @Feat(units='mm/s**2') + def max_acceleration(self): + return float(self.parent.query('%dAU?' % self.num)) + + @max_acceleration.setter + def max_acceleration(self, velocity): + self.parent.write('%dAU%f' % (self.num, velocity)) + + + @Feat(units='mm/s') + def velocity(self): + return float(self.parent.query('%dVA?' % self.num)) + + @velocity.setter + def velocity(self, velocity): + """ + :param velocity: Set the velocity that the axis should use when moving + :return: + """ + self.parent.write('%dVA%f' % (self.num, velocity)) + + @Feat(units='mm/s**2') + def acceleration(self): + return float(self.parent.query('%dVA?' % self.num)) + + @acceleration.setter + def acceleration(self, acceleration): + """ + :param acceleration: Set the acceleration that the axis should use when starting + :return: + """ + self.parent.write('%dAC%f' % (self.num, acceleration)) + + @Feat(units='mm/s') + def actual_velocity(self): + return float(self.parent.query('%dTV' % self.num)) + + @actual_velocity.setter + def actual_velocity(self, val): + raise NotImplementedError + + @Action() + def stop(self): + """Emergency stop""" + self.parent.write(u'{0:d}ST'.format(self.num)) + + @Feat(values={True: '1', False: '0'}) + def motion_done(self): + while True: + ret = self.parent.query('%dMD?' % self.num) + if ret in ['1','0']: + break + else: + time.sleep(self.wait_time) + return ret + + # Not working yet, see https://github.com/hgrecco/lantz/issues/35 + # @Feat(values={Q_('encodercount'): 0, + # Q_('motor step'): 1, + # Q_('millimeter'): 2, + # Q_('micrometer'): 3, + # Q_('inches'): 4, + # Q_('milli-inches'): 5, + # Q_('micro-inches'): 6, + # Q_('degree'): 7, + # Q_('gradian'): 8, + # Q_('radian'): 9, + # Q_('milliradian'): 10, + # Q_('microradian'): 11}) + def units(self): + ret = int(self.parent.query(u'{}SN?'.format(self.num))) + vals = {0 :'encoder count', + 1 :'motor step', + 2 :'millimeter', + 3 :'micrometer', + 4 :'inches', + 5 :'milli-inches', + 6 :'micro-inches', + 7 :'degree', + 8 :'gradian', + 9 :'radian', + 10:'milliradian', + 11:'microradian',} + return vals[ret] + + # @units.setter + # def units(self, val): + # self.parent.write('%SN%' % (self.num, val)) + + def _wait_until_done(self): + #wait_time = convert_to('seconds', on_dimensionless='warn')(self.wait_time) + time.sleep(self.wait_time) + while not self.motion_done: + time.sleep(self.wait_time) #wait_time.magnitude) + + + +if __name__ == '__main__': + import argparse + import lantz.log + + parser = argparse.ArgumentParser(description='Test ESP301 driver') + parser.add_argument('-p', '--port', type=str, default='1', + help='Serial port to connect to') + + args = parser.parse_args() + lantz.log.log_to_socket(lantz.log.DEBUG) + + with ESP301.via_usb(port=args.port) as inst: + # inst.initialize() # Initialize the communication with the power meter + # Find the status of all axes: + for axis in inst.axes: + print('Axis {} Position {} is_on {} max_velocity {} velocity {}'.format(axis.num, axis.position, + axis.is_on, axis.max_velocity, + axis.velocity)) From 49d32037a5a0f1d6eb08c3c9f4a496388450b6a7 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Thu, 23 Feb 2017 14:16:37 +0100 Subject: [PATCH 18/50] Added ESP300 to docstring --- motionesp301.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/motionesp301.py b/motionesp301.py index 024e672..4def0fc 100644 --- a/motionesp301.py +++ b/motionesp301.py @@ -3,7 +3,8 @@ lantz.drivers.newport.motionesp301 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Implements the drivers to control an ESP301 motion controller via USB or serial. + Implements the drivers to control ESP300 and ESP301 motion controller + via USB or serial. For USB, one first have to install the windows driver from newport. @@ -80,7 +81,7 @@ def detect_axis(self): axis = ESP301Axis(self, i, id) self.axes.append(axis) except: - err = int(self.query('TE?')) + err = self.get_errors() if err == 37: # Axis number missing self.axes.append(None) elif err == 9: # Axis number out of range @@ -89,7 +90,11 @@ def detect_axis(self): scan_axes = False else: # Dunno... raise Exception(err) - + @Action() + def get_errors(self): + err = int(self.query('TE?')) + return err + @Feat(read_once=False) def position(self): return [axis.position for axis in self.axes] @@ -119,7 +124,11 @@ def _position(self, pos, read_pos=False): if not p is None: axis._position_cached = pos return pos - + + @Action() + def motion_done(self): + for axis in self.axes: axis._wait_until_done() + def finalize(self): for axis in self.axes: if axis is not None: @@ -202,7 +211,7 @@ def position(self, pos): """ # First do move to extra position if necessary - self._set_position(pos) + self._set_position(pos, wait=self.wait_until_done) @Action(units=['mm',None]) def _set_position(self, pos, wait=None): From adb1be7d9be1c913e48897c80a306cae75569a28 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Thu, 23 Feb 2017 16:24:35 +0100 Subject: [PATCH 19/50] Check position of stage after movement --- motionesp301.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/motionesp301.py b/motionesp301.py index 4def0fc..7a1f96c 100644 --- a/motionesp301.py +++ b/motionesp301.py @@ -106,23 +106,24 @@ def _position_cached(self): @position.setter def position(self, pos): """Move to position (x,y,...)""" - return self._position(pos, read_pos=True) + return self._position(pos) @Action() - def _position(self, pos, read_pos=False): + def _position(self, pos, read_pos=None, wait_until_done=True): """Move to position (x,y,...)""" + if read_pos is not None: + self.log_error('kwargs read_pos for function _position is deprecated') + for p, axis in zip(pos, self.axes): if not p is None: axis._set_position(p, wait=False) - for p, axis in zip(pos, self.axes): - if not p is None: - axis._wait_until_done() - if read_pos: - pos = [axis.position for axis in self.axes] - else: + if wait_until_done: for p, axis in zip(pos, self.axes): if not p is None: - axis._position_cached = pos + axis._wait_until_done() + axis.check_position(p) + return self.position + return pos @Action() @@ -156,6 +157,8 @@ def __init__(self, parent, num, id, *args, **kwargs): self.wait_time = 0.01 # in seconds * Q_(1, 's') self.backlash = 0 self.wait_until_done = True + self.accuracy = 0.001 # in units reported by axis + # Fill position cache: self.position def __del__(self): @@ -209,6 +212,9 @@ def position(self, pos): :param pos: new position """ + if not self.is_on: + self.log_error('Axis not enabled. Not moving!') + return # First do move to extra position if necessary self._set_position(pos, wait=self.wait_until_done) @@ -242,6 +248,7 @@ def _set_position(self, pos, wait=None): self.__set_position(pos) if wait: self._wait_until_done() + self.check_position(pos) def __set_position(self, pos): """ @@ -250,6 +257,14 @@ def __set_position(self, pos): """ self.parent.write('%dPA%f' % (self.num, pos)) + def check_position(self, pos): + '''Check is stage is at expected position''' + if np.isclose(self.position, pos, atol=self.accuracy): + return True + self.log_error('Position accuracy {} is not reached.' + 'Expected: {}, measured: {}'.format(self.accuracy, pos, self._position_cached)) + return False + @Feat(units='mm/s') def max_velocity(self): return float(self.parent.query('%dVU?' % self.num)) From d744cb7270c56d7ef9f5b1cbdc6342f27719ed81 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 27 Feb 2017 14:54:01 +0100 Subject: [PATCH 20/50] Added vim backup and swap files to gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index ba74660..abf4293 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,7 @@ docs/_build/ # PyBuilder target/ + +#Vim backups and swapfiles +*~ +.*.sw? From 2a42718fa0cf1d5a1399bc0337da7ad0f8fa7128 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Tue, 2 Jan 2018 15:01:24 +0100 Subject: [PATCH 21/50] Fixed errounous test and changed depricated warn --- lantz/processors.py | 2 +- lantz/testsuite/test_feat.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lantz/processors.py b/lantz/processors.py index 65ee831..c6eb158 100644 --- a/lantz/processors.py +++ b/lantz/processors.py @@ -88,7 +88,7 @@ def _inner(value): elif on_dimensionless == 'warn': msg = 'Assuming units `{1.units}` for {0}'.format(value, units) warnings.warn(msg, DimensionalityWarning) - _LOG.warn(msg) + _LOG.warning(msg) # on_incompatible == 'ignore' return float(value) diff --git a/lantz/testsuite/test_feat.py b/lantz/testsuite/test_feat.py index 752a68b..e0b464d 100644 --- a/lantz/testsuite/test_feat.py +++ b/lantz/testsuite/test_feat.py @@ -261,14 +261,15 @@ def eggs(self_, value): obj.eggs = x obj.eggs = x + 1 - self.assertEqual(hdl.history, ['Created Spam17', + #First item number depends on number of calls of Spam(), hence ignore + self.assertEqual(hdl.history[1:], ['Created Spam17', 'Getting eggs', '(raw) Got 9 for eggs', 'Got 9 for eggs', 'No need to set eggs = 9 (current=9, force=False)', 'Setting eggs = 10 (current=9, force=False)', '(raw) Setting eggs = 10', - 'eggs was set to 10']) + 'eggs was set to 10'][1:]) def test_units(self): From 46ed66bbbcfc4ad23bdf21fac3a035ff13075887 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Fri, 12 Jan 2018 17:47:13 +0100 Subject: [PATCH 22/50] First version of driver for PI piezo driver --- lantz/drivers/pi/__init__.py | 18 ++++ lantz/drivers/pi/piezo.py | 173 +++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 lantz/drivers/pi/__init__.py create mode 100644 lantz/drivers/pi/piezo.py diff --git a/lantz/drivers/pi/__init__.py b/lantz/drivers/pi/__init__.py new file mode 100644 index 0000000..4e6f16c --- /dev/null +++ b/lantz/drivers/pi/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.pi + ~~~~~~~~~~~~~~~~~~~~~~ + + :company: PhysikInstrumente + :description: Motion and positioning components + :website: https://www.physikinstrumente.com + + ---- + + :copyright: 2017 by Vasco Tenner + :license: BSD, see LICENSE for more details. +""" + +from .piezo import Piezo + +__all__ = ['Piezo'] diff --git a/lantz/drivers/pi/piezo.py b/lantz/drivers/pi/piezo.py new file mode 100644 index 0000000..d773f48 --- /dev/null +++ b/lantz/drivers/pi/piezo.py @@ -0,0 +1,173 @@ +""" + lantz.drivers.pi.piezo + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Implements the drivers to control pi piezo motion controller + via serial. It uses the PI General Command Set. + + For USB, one first have to install the windows driver from PI. + :copyright: 2017, see AUTHORS for more details. + :license: GPL, see LICENSE for more details. + + Todo: via network? + Source: Instruction Manual (PI) +""" + +from lantz.feat import Feat +from lantz.action import Action +#from lantz.serial import SerialDriver +from lantz.messagebased import MessageBasedDriver +from pyvisa import constants +#from lantz.visa import GPIBVisaDriver +from lantz import Q_, ureg +from lantz.processors import convert_to +import time +import numpy as np +import copy + +#TODO +#Replace all send with write and remove \n +#Ask for errors often + +def to_int(val): + if not is_numeric(val): + val = val.strip() + if val == '': + val = 0 + return int(val) + +def to_float(val): + if not is_numeric(val): + val = val.strip() + if val == '': + val = 0 + return float(val) + +class Piezo(MessageBasedDriver): + """ PI piezo motion controller. It assumes all axes to have units um + + Params: + axis: axis number to controll""" + + DEFAULTS = { + 'COMMON': {'write_termination': '\r\n', + 'read_termination': '\r\n',}, +# TODO: set via PI software +# 'ASRL':{ +# 'timeout': 4000, #ms +# 'encoding': 'ascii', +# 'data_bits': 8, +# 'baud_rate': 19200, +# 'parity': constants.Parity.none, +# 'stop_bits': constants.StopBits.one, +# 'flow_control': constants.VI_ASRL_FLOW_RTS_CTS,#constants.VI_ASRL_FLOW_NONE, +# }, + } + + def initialize(self, axis=1, sleeptime_after_move=0*ureg.ms): + super().initialize() + self.axis = 1 + self.sleeptime_after_move = sleep_after_move + + def recv_numeric(self,length): + try: + message = self.stage.recv(length) + except (socket.timeout, socket.error), error: + print 'Stage not responsive! ', error + if self.stayconnected: + self.retries = self.retries + 1 + if self.retries < 10: + print 'Retry to connect %i/%i' % (self.retries, 10) + self.connect() + else: + print 'To many retries. Not connected to socket' + self.retries = 0 + message = '' + message = message.strip() + if message == '': + message = 0 + return message + + @Action() + def errors(self): + error = int(self.query('ERR?')) + if self.error != 0: + self.log_error('Stage error: code {}'.format(self.error)) + return self.error + + @Feat() + def idn(self): + self.stage.send("*IDN?\n") + idn = self.recv(256) + return idn + + @Action() + def stop(): + '''Stop all motions''' + self.servo = False + self.write("#24") + + def finalize(self): + """ Disconnects stage """ + self.stop() + super().finalize() + + @Feat(values={True: '1', False: '0'}) + def servo(self, state): + ''' Set the stage control in open- or closed-loop (state = False or True)''' + return to_int(self.query('SVO?') + + @serco.setter + def servo(self, state): + self.send('SVO 1 %d\n' % state) + + @Feat(units='um/s') + def velocity(self): + ''' Set the stage velocity (closed-loop only)''' + return self.query('VEL?') + + @velocity.setter + def velocity(self, velocity): + return self.write('VEL 1 %f' % velocity) + + @Feat(units='um'): + def position(self): + ''' Move to an absolute position the stage (closed-loop only)''' + return self.query('POS?') + + @position.setter + def position(self, position): + self.move_to(position) + + @Action(units=('um','ms')) + def move_to(self, position, timeout=None): + ''' Move to an absolute position the stage (closed-loop only)''' + ret = self.write('MOV %i %f' % self.axis, position) + timeout = self.sleeptime_after_move if timeout is None: + time.sleep(timeout.to('s')) # Give the stage time to move! (in seconds!) + return ret + + @Feat(units='um'): + def read_stage_position(self, nr_avg = 1): + ''' Read the current position from the stage''' + positions = [self.position for n in nr_avg] + return np.avg(positions) + +# before operating, ensure that the notch filters are set to the right frequencies. These frequencies depend on the load on the piezo, and can be determined in NanoCapture. +if __name__=='__main__': + stage = Piezo('') + stage.sleeptime_after_move = 15*ureg.ms + + time.sleep(1) + + stage.servo = True + time.sleep(1) + stage.velocity = 0.5*ureg.um/ureg.s + + stage.position = 0 + print 'stage position measured: ', stage.position + + steps = 200 + stepsize = 100.0*ureg.nm + for n in range(steps): + stage.position = n*stepsize + print 'stage position measured: ', stage.position From b4a0c9aaaa78c5f7973a635b119635945d5c55bd Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Wed, 17 Jan 2018 17:12:33 +0100 Subject: [PATCH 23/50] Added support for PI E-709 piezo driver --- lantz/drivers/pi/__init__.py | 4 +- lantz/drivers/pi/error_codes.py | 219 +++++++++++++++++++++ lantz/drivers/pi/piezo.py | 339 ++++++++++++++++++++------------ 3 files changed, 438 insertions(+), 124 deletions(-) create mode 100644 lantz/drivers/pi/error_codes.py diff --git a/lantz/drivers/pi/__init__.py b/lantz/drivers/pi/__init__.py index 4e6f16c..320ab6a 100644 --- a/lantz/drivers/pi/__init__.py +++ b/lantz/drivers/pi/__init__.py @@ -13,6 +13,6 @@ :license: BSD, see LICENSE for more details. """ -from .piezo import Piezo +from .piezo import Piezo, parse_line, parse_multi -__all__ = ['Piezo'] +__all__ = ['Piezo','parse_line','parse_multi'] diff --git a/lantz/drivers/pi/error_codes.py b/lantz/drivers/pi/error_codes.py new file mode 100644 index 0000000..c81e79b --- /dev/null +++ b/lantz/drivers/pi/error_codes.py @@ -0,0 +1,219 @@ +error_codes = {0: "PI_CNTR_NO_ERROR No error", +1: "PI_CNTR_PARAM_SYNTAX Parameter syntax error", +2: "PI_CNTR_UNKNOWN_COMMAND Unknown command", +3: "PI_CNTR_COMMAND_TOO_LONG Command length out of limits orcommand buffer overrun", +4: "PI_CNTR_SCAN_ERROR Error while scanning", +5: "PI_CNTR_MOVE_WITHOUT_REF_OR_NO_SERVO Unallowable move attempted onunreferenced axis, or move attemptedwith servo off", +6: "PI_CNTR_INVALID_SGA_PARAM Parameter for SGA not valid", +7: "PI_CNTR_POS_OUT_OF_LIMITS Position out of limits", +8: "PI_CNTR_VEL_OUT_OF_LIMITS Velocity out of limits", +9: "PI_CNTR_SET_PIVOT_NOT_POSSIBLE Attempt to set pivot point while U,V andW not all 0", +10: "PI_CNTR_STOP Controller was stopped by command", +11: "PI_CNTR_SST_OR_SCAN_RANGE Parameter for SST or for one of theembedded scan algorithms out of range", +12: "PI_CNTR_INVALID_SCAN_AXES Invalid axis combination for fast scan", +13: "PI_CNTR_INVALID_NAV_PARAM Parameter for NAV out of range", +14: "PI_CNTR_INVALID_ANALOG_INPUT Invalid analog channel", +15: "PI_CNTR_INVALID_AXIS_IDENTIFIER Invalid axis identifier", +16: "PI_CNTR_INVALID_STAGE_NAME Unknown stage name", +17: "PI_CNTR_PARAM_OUT_OF_RANGE Parameter out of range", +18: "PI_CNTR_INVALID_MACRO_NAME Invalid macro name", +19: "PI_CNTR_MACRO_RECORD Error while recording macro", +20: "PI_CNTR_MACRO_NOT_FOUND Macro not found", +21: "PI_CNTR_AXIS_HAS_NO_BRAKE Axis has no brake", +22: "PI_CNTR_DOUBLE_AXIS Axis identifier specified more than once", +23: "PI_CNTR_ILLEGAL_AXIS Illegal axis", +24: "PI_CNTR_PARAM_NR Incorrect number of parameters", +25: "PI_CNTR_INVALID_REAL_NR Invalid floating point number", +26: "PI_CNTR_MISSING_PARAM Parameter missing", +27: "PI_CNTR_SOFT_LIMIT_OUT_OF_RANGE Soft limit out of range", +28: "PI_CNTR_NO_MANUAL_PAD No manual pad found", +29: "PI_CNTR_NO_JUMP No more step-response values", +30: "PI_CNTR_INVALID_JUMP No step-response values recorded", +31: "PI_CNTR_AXIS_HAS_NO_REFERENCE Axis has no reference sensor", +32: "PI_CNTR_STAGE_HAS_NO_LIM_SWITCH Axis has no limit switch", +33: "PI_CNTR_NO_RELAY_CARD No relay card installed", +34: "PI_CNTR_CMD_NOT_ALLOWED_FOR_STAGE Command not allowed for selectedstage(s)", +35: "PI_CNTR_NO_DIGITAL_INPUT No digital input installed", +36: "PI_CNTR_NO_DIGITAL_OUTPUT No digital output configured", +37: "PI_CNTR_NO_MCM No more MCM responses", +38: "PI_CNTR_INVALID_MCM No MCM values recorded", +39: "PI_CNTR_INVALID_CNTR_NUMBER Controller number invalid", +40: "PI_CNTR_NO_JOYSTICK_CONNECTED No joystick configured", +41: "PI_CNTR_INVALID_EGE_AXIS Invalid axis for electronic gearing, axiscannot be slave", +42: "PI_CNTR_SLAVE_POSITION_OUT_OF_RANGE Position of slave axis is out of range", +43: "PI_CNTR_COMMAND_EGE_SLAVE Slave axis cannot be commandeddirectly when electronic gearing isenabled", +44: "PI_CNTR_JOYSTICK_CALIBRATION_FAILED Calibration of joystick failed", +45: "PI_CNTR_REFERENCING_FAILED Referencing failed", +46: "PI_CNTR_OPM_MISSING OPM (Optical Power Meter) missing", +47: "PI_CNTR_OPM_NOT_INITIALIZED OPM (Optical Power Meter) notinitialized or cannot be initialized", +48: "PI_CNTR_OPM_COM_ERROR OPM (Optical Power Meter)communication error", +49: "PI_CNTR_MOVE_TO_LIMIT_SWITCH_FAILED Move to limit switch failed", +50: "PI_CNTR_REF_WITH_REF_DISABLED Attempt to reference axis withreferencing disabled", +51: "PI_CNTR_AXIS_UNDER_JOYSTICK_CONTROL Selected axis is controlled by joystick", +52: "PI_CNTR_COMMUNICATION_ERROR Controller detected communicationerror", +53: "PI_CNTR_DYNAMIC_MOVE_IN_PROCESS MOV! motion still in progress", +54: "PI_CNTR_UNKNOWN_PARAMETER Unknown parameter", +55: "PI_CNTR_NO_REP_RECORDED No commands were recorded with REP", +56: "PI_CNTR_INVALID_PASSWORD Password invalid", +57: "PI_CNTR_INVALID_RECORDER_CHAN Data record table does not exist", +58: "PI_CNTR_INVALID_RECORDER_SRC_OPT Source does not exist; number too lowor too high", +59: "PI_CNTR_INVALID_RECORDER_SRC_CHAN Source record table number too low ortoo high", +60: "PI_CNTR_PARAM_PROTECTION Protected Param: Current CommandLevel (CCL) too low", +61: "PI_CNTR_AUTOZERO_RUNNING Command execution not possible whileautozero is running", +62: "PI_CNTR_NO_LINEAR_AXIS Autozero requires at least one linearaxis", +63: "PI_CNTR_INIT_RUNNING Initialization still in progress", +64: "PI_CNTR_READ_ONLY_PARAMETER Parameter is read-only", +65: "PI_CNTR_PAM_NOT_FOUND Parameter not found in nonvolatilememory", +66: "PI_CNTR_VOL_OUT_OF_LIMITS Voltage out of limits", +67: "PI_CNTR_WAVE_TOO_LARGE Not enough memory available forrequested wave curve", +68: "PI_CNTR_NOT_ENOUGH_DDL_MEMORY Not enough memory available for DDLtable; DDL cannot be started", +69: "PI_CNTR_DDL_TIME_DELAY_TOO_LARGE Time delay larger than DDL table; DDLcannot be started", +70: "PI_CNTR_DIFFERENT_ARRAY_LENGTH The requested arrays have differentlengths; query them separately", +71: "PI_CNTR_GEN_SINGLE_MODE_RESTART Attempt to restart the generator while itis running in single step mode", +72: "PI_CNTR_ANALOG_TARGET_ACTIVE Motion commands and wave generatoractivation are not allowed when analogtarget is active", +73: "PI_CNTR_WAVE_GENERATOR_ACTIVE Motion commands are not allowedwhen wave generator is active", +74: "PI_CNTR_AUTOZERO_DISABLED No sensor channel or no piezo channelconnected to selected axis (sensor andpiezo matrix)", +75: "PI_CNTR_NO_WAVE_SELECTED Generator started (WGO) withouthaving selected a wave table (WSL).", +76: "PI_CNTR_IF_BUFFER_OVERRUN Interface buffer overran and commandcouldn't be received correctly", +77: "PI_CNTR_NOT_ENOUGH_RECORDED_DATA Data record table does not hold enoughrecorded data", +78: "PI_CNTR_TABLE_DEACTIVATED Data record table is not configured forrecording", +79: "PI_CNTR_OPENLOOP_VALUE_SET_WHEN_SERVO_ON Open-loop commands (SVA, SVR) arenot allowed when servo is on", +80: "PI_CNTR_RAM_ERROR Hardware error affecting RAM", +81: "PI_CNTR_MACRO_UNKNOWN_COMMAND Not macro command", +82: "PI_CNTR_MACRO_PC_ERROR Macro counter out of range", +83: "PI_CNTR_JOYSTICK_ACTIVE Joystick is active", +84: "PI_CNTR_MOTOR_IS_OFF Motor is off", +85: "PI_CNTR_ONLY_IN_MACRO Macro-only command", +86: "PI_CNTR_JOYSTICK_UNKNOWN_AXISInvalid joystick axis", +87: "PI_CNTR_JOYSTICK_UNKNOWN_IDJoystick unknown", +88: "PI_CNTR_REF_MODE_IS_ONMove without referenced stage", +89: "PI_CNTR_NOT_ALLOWED_IN_CURRENT_MOTION_MODE Command not allowed in current motionmode", +90: "PI_CNTR_DIO_AND_TRACING_NOT_POSSIBLENo tracing possible while digital IOs areused on this HW revision. Reconnect toswitch operation mode.", +91: "PI_CNTR_COLLISIONMove not possible, would causecollision", +92: "PI_CNTR_SLAVE_NOT_FAST_ENOUGH Stage is not capable of following themaster. Check the gear ratio.", +93: "PI_CNTR_CMD_NOT_ALLOWED_WHILE_AXIS_IN_MOTION This command is not allowed while theaffected axis or its master is in motion.", +94: "PI_CNTR_OPEN_LOOP_JOYSTICK_ENABLED Servo cannot be switched on whenopen-loop joystick control is enabled.", +95: "PI_CNTR_INVALID_SERVO_STATE_FOR_PARAMETER This parameter cannot be changed incurrent servo mode.", +96: "PI_CNTR_UNKNOWN_STAGE_NAME Unknown stage name", +100: "PI_LABVIEW_ERROR PI LabVIEW driver reports error. Seesource control for details.", +200: "PI_CNTR_NO_AXIS No stage connected to axis", +201: "PI_CNTR_NO_AXIS_PARAM_FILE File with axis parameters not found", +202: "PI_CNTR_INVALID_AXIS_PARAM_FILE Invalid axis parameter file", +203: "PI_CNTR_NO_AXIS_PARAM_BACKUP Backup file with axis parameters notfound", +204: "PI_CNTR_RESERVED_204 PI internal error code 204", +205: "PI_CNTR_SMO_WITH_SERVO_ON SMO with servo on", +206: "PI_CNTR_UUDECODE_INCOMPLETE_HEADER uudecode: incomplete header", +207: "PI_CNTR_UUDECODE_NOTHING_TO_DECODE uudecode: nothing to decode", +208: "PI_CNTR_UUDECODE_ILLEGAL_FORMAT uudecode: illegal UUE format", +209: "PI_CNTR_CRC32_ERROR CRC32 error", +210: "PI_CNTR_ILLEGAL_FILENAME Illegal file name (must be 8-0 format)", +211: "PI_CNTR_FILE_NOT_FOUND File not found on controller", +212: "PI_CNTR_FILE_WRITE_ERROR Error writing file on controller", +213: "PI_CNTR_DTR_HINDERS_VELOCITY_CHANGE VEL command not allowed in DTRcommand mode", +214: "PI_CNTR_POSITION_UNKNOWN Position calculations failed", +215: "PI_CNTR_CONN_POSSIBLY_BROKEN The connection between controller andstage may be broken", +216: "PI_CNTR_ON_LIMIT_SWITCH The connected stage has driven into alimit switch, some controllers need CLRto resume operation", +217: "PI_CNTR_UNEXPECTED_STRUT_STOP Strut test command failed because ofan unexpected strut stop", +218: "PI_CNTR_POSITION_BASED_ON_ESTIMATION While MOV! is running position can onlybe estimated!", +219: "PI_CNTR_POSITION_BASED_ON_INTERPOLATION Position was calculated during MOVmotion", +230: "PI_CNTR_INVALID_HANDLE Invalid handle", +231: "PI_CNTR_NO_BIOS_FOUND No bios found", +232: "PI_CNTR_SAVE_SYS_CFG_FAILED Save system configuration failed", +233: "PI_CNTR_LOAD_SYS_CFG_FAILED Load system configuration failed", +301: "PI_CNTR_SEND_BUFFER_OVERFLOW Send buffer overflow", +302: "PI_CNTR_VOLTAGE_OUT_OF_LIMITS Voltage out of limits", +303: "PI_CNTR_OPEN_LOOP_MOTION_SET_WHEN_SERVO_ON Open-loop motion attempted whenservo ON", +304: "PI_CNTR_RECEIVING_BUFFER_OVERFLOW Received command is too long", +305: "PI_CNTR_EEPROM_ERROR Error while reading/writing EEPROM", +306: "PI_CNTR_I2C_ERROR Error on I2C bus", +307: "PI_CNTR_RECEIVING_TIMEOUT Timeout while receiving command", +308: "PI_CNTR_TIMEOUT A lengthy operation has not finished inthe expected time", +309: "PI_CNTR_MACRO_OUT_OF_SPACE Insufficient space to store macro", +310: "PI_CNTR_EUI_OLDVERSION_CFGDATA Configuration data has old versionnumber", +311: "PI_CNTR_EUI_INVALID_CFGDATA Invalid configuration data", +333: "PI_CNTR_HARDWARE_ERROR Internal hardware error", +400: "PI_CNTR_WAV_INDEX_ERROR Wave generator index error", +401: "PI_CNTR_WAV_NOT_DEFINED Wave table not defined", +402: "PI_CNTR_WAV_TYPE_NOT_SUPPORTED Wave type not supported", +403: "PI_CNTR_WAV_LENGTH_EXCEEDS_LIMIT Wave length exceeds limit", +404: "PI_CNTR_WAV_PARAMETER_NR Wave parameter number error", +405: "PI_CNTR_WAV_PARAMETER_OUT_OF_LIMIT Wave parameter out of range", +406: "PI_CNTR_WGO_BIT_NOT_SUPPORTED WGO command bit not supported", +500: "PI_CNTR_EMERGENCY_STOP_BUTTON_ACTIVATED The \"red knob\" is still set and disablessystem", +501: "PI_CNTR_EMERGENCY_STOP_BUTTON_WAS_ACTIVATE The \"red knob\" was activated and stillDdisables system - reanimation required", +502: "PI_CNTR_REDUNDANCY_LIMIT_EXCEEDED Position consistency check failed", +503: "PI_CNTR_COLLISION_SWITCH_ACTIVATED Hardware collision sensor(s) areactivated", +504: "PI_CNTR_FOLLOWING_ERROR Strut following error occurred, e.g.caused by overload or encoder failure", +505: "PI_CNTR_SENSOR_SIGNAL_INVALID One sensor signal is not valid", +506: "PI_CNTR_SERVO_LOOP_UNSTABLE Servo loop was unstable due to wrongparameter setting and switched off toavoid damage.", +530: "PI_CNTR_NODE_DOES_NOT_EXIST A command refers to a node that doesnot exist", +531: "PI_CNTR_PARENT_NODE_DOES_NOT_EXIST A command refers to a node that hasno parent node", +532: "PI_CNTR_NODE_IN_USE Attempt to delete a node that is in use", +533: "PI_CNTR_NODE_DEFINITION_IS_CYCLIC Definition of a node is cyclic", +534: "PI_CNTR_NODE_CHAIN_INVALID The node chain does not end in the \"0\"node", +535: "PI_CNTR_NODE_DEFINITION_NOT_CONSISTENT The definition of a coordinatetransformation is erroneous", +536: "PI_CNTR_HEXAPOD_IN_MOTION Transformation cannot be defined aslong as Hexapod is in motion", +537: "PI_CNTR_TRANSFORMATION_TYPE_NOT_SUPPORTED Transformation node cannot beactivated", +538: "PI_CNTR_NODE_TYPE_DIFFERS A node can only be replaced by a nodeof the same type", +539: "PI_CNTR_NODE_PARENT_IDENTICAL_TO_CHILD A node cannot be linked to itself", +540: "PI_CNTR_NODE_DEFINITION_INCONSISTENT Node definition is erroneous or notcomplete (replace or delete it)", +541: "PI_CNTR_ZERO_NODE_CANNOT_BE_CHANGED_OR_REPLACED 0 is the root node and cannot bemodified", +542: "PI_CNTR_NODES_NOT_IN_SAME_CHAIN The nodes are not part of the samechain", +543: "PI_CNTR_NODE_MEMORY_FULL Unused nodes must be deleted beforenew nodes can be stored", +544: "PI_CNTR_PIVOT_POINT_FEATURE_NOT_SUPPORTED With some transformations pivot pointusage is not supported", +555: "PI_CNTR_UNKNOWN_ERROR BasMac: unknown controller error", +601: "PI_CNTR_NOT_ENOUGH_MEMORY Not enough memory", +602: "PI_CNTR_HW_VOLTAGE_ERROR Hardware voltage error", +603: "PI_CNTR_HW_TEMPERATURE_ERROR Hardware temperature out of range", +604: "PI_CNTR_POSITION_ERROR_TOO_HIGH Position error of any axis in the systemis too high", +606: "PI_CNTR_INPUT_OUT_OF_RANGE Maximum value of input signal hasbeen exceeded", +1000: "PI_CNTR_TOO_MANY_NESTED_MACROS Too many nested macros", +1001: "PI_CNTR_MACRO_ALREADY_DEFINED Macro already defined", +1002: "PI_CNTR_NO_MACRO_RECORDING Macro recording not activated", +1003: "PI_CNTR_INVALID_MAC_PARAM Invalid parameter for MAC", +1004: "PI_CNTR_RESERVED_1004 PI internal error code 1004", +1005: "PI_CNTR_CONTROLLER_BUSY Controller is busy with some lengthyoperation (e.g. reference move, fastscan algorithm)", +1006: "PI_CNTR_INVALID_IDENTIFIER Invalid identifier (invalid specialcharacters, ...)", +1007: "PI_CNTR_UNKNOWN_VARIABLE_OR_ARGUMENT Variable or argument not defined", +1008: "PI_CNTR_RUNNING_MACRO Controller is (already) running a macro", +1009: "PI_CNTR_MACRO_INVALID_OPERATOR Invalid or missing operator for condition.Check necessary spaces aroundoperator.", +1010: "PI_CNTR_MACRO_NO_ANSWER No answer was received whileexecuting WAC/MEX/JRC/...", +1011: "PI_CMD_NOT_VALID_IN_MACRO_MODE Command not valid during macroexecution", +1024: "PI_CNTR_MOTION_ERROR Motion error: position error too large,servo is switched off automatically", +1063: "PI_CNTR_EXT_PROFILE_UNALLOWED_CMD User profile mode: command is notallowed, check for required preparatorycommands", +1064: "PI_CNTR_EXT_PROFILE_EXPECTING_MOTION_ERROR User profile mode: first target position inuser profile is too far from currentposition", +1065: "PI_CNTR_PROFILE_ACTIVE Controller is (already) in user profilemode", +1066: "PI_CNTR_PROFILE_INDEX_OUT_OF_RANGE User profile mode: block or data setindex out of allowed range", +1071: "PI_CNTR_PROFILE_OUT_OF_MEMORY User profile mode: out of memory", +1072: "PI_CNTR_PROFILE_WRONG_CLUSTER User profile mode: cluster is notassigned to this axis", +1073: "PI_CNTR_PROFILE_UNKNOWN_CLUSTER_IDENTIFIER Unknown cluster identifier", +1090: "PI_CNTR_TOO_MANY_TCP_CONNECTIONS_OPEN There are too many open tcpipconnections", +2000: "PI_CNTR_ALREADY_HAS_SERIAL_NUMBER Controller already has a serial number", +4000: "PI_CNTR_SECTOR_ERASE_FAILED Sector erase failed", +4001: "PI_CNTR_FLASH_PROGRAM_FAILED Flash program failed", +4002: "PI_CNTR_FLASH_READ_FAILED Flash read failed", +4003: "PI_CNTR_HW_MATCHCODE_ERROR HW match code missing/invalid", +4004: "PI_CNTR_FW_MATCHCODE_ERROR FW match code missing/invalid", +4005: "PI_CNTR_HW_VERSION_ERROR HW version missing/invalid", +4006: "PI_CNTR_FW_VERSION_ERROR FW version missing/invalid", +4007: "PI_CNTR_FW_UPDATE_ERROR FW update failed", +5000: "PI_CNTR_INVALID_PCC_SCAN_DATA PicoCompensation scan data is notvalid", +5001: "PI_CNTR_PCC_SCAN_RUNNING PicoCompensation is running, someactions cannot be executed duringscanning/recording", +5002: "PI_CNTR_INVALID_PCC_AXIS Given axis cannot be defined as PPCaxis", +5003: "PI_CNTR_PCC_SCAN_OUT_OF_RANGE Defined scan area is larger than thetravel range", +5004: "PI_CNTR_PCC_TYPE_NOT_EXISTING Given PicoCompensation type is notdefined", +5005: "PI_CNTR_PCC_PAM_ERROR PicoCompensation parameter error", +5006: "PI_CNTR_PCC_TABLE_ARRAY_TOO_LARGE PicoCompensation table is larger thanmaximum table length", +5100: "PI_CNTR_NEXLINE_ERROR Common error in NEXLINE® firmwaremodule", +5101: "PI_CNTR_CHANNEL_ALREADY_USED Output channel for NEXLINE® cannotbe redefined for other usage", +5102: "PI_CNTR_NEXLINE_TABLE_TOO_SMALL Memory for NEXLINE® signals is toosmall", +5103: "PI_CNTR_RNP_WITH_SERVO_ON RNP cannot be executed if axis is inclosed loop", +5104: "PI_CNTR_RNP_NEEDED Relax procedure (RNP) needed", +5200: "PI_CNTR_AXIS_NOT_CONFIGURED Axis must be configured for this action", +6000: "PI_CNTR_SENSOR_ABS_INVALID_VALUE Invalid preset value of absolute sensor", +6001: "PI_CNTR_SENSOR_ABS_WRITE_ERROR Error while writing to sensor", +6002: "PI_CNTR_SENSOR_ABS_READ_ERROR Error while reading from sensor", +6003: "PI_CNTR_SENSOR_ABS_CRC_ERROR Checksum error of absolute sensor", +6004: "PI_CNTR_SENSOR_ABS_ERROR General error of absolute sensor", +6005: "PI_CNTR_SENSOR_ABS_OVERFLOW Overflow of absolute sensor position", +} diff --git a/lantz/drivers/pi/piezo.py b/lantz/drivers/pi/piezo.py index d773f48..b3e3d9f 100644 --- a/lantz/drivers/pi/piezo.py +++ b/lantz/drivers/pi/piezo.py @@ -1,173 +1,268 @@ """ lantz.drivers.pi.piezo ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Implements the drivers to control pi piezo motion controller - via serial. It uses the PI General Command Set. - - For USB, one first have to install the windows driver from PI. - :copyright: 2017, see AUTHORS for more details. + Implements the drivers to control pi E-709 piezo motion controller + via USB or via serial. It uses the PI General Command Set. + + For USB, one first have to install the driver from PI. + :copyright: 2018, see AUTHORS for more details. :license: GPL, see LICENSE for more details. - - Todo: via network? + Source: Instruction Manual (PI) """ from lantz.feat import Feat from lantz.action import Action -#from lantz.serial import SerialDriver from lantz.messagebased import MessageBasedDriver -from pyvisa import constants -#from lantz.visa import GPIBVisaDriver +# from pyvisa import constants from lantz import Q_, ureg -from lantz.processors import convert_to +# from lantz.processors import convert_to import time import numpy as np -import copy - -#TODO -#Replace all send with write and remove \n -#Ask for errors often - -def to_int(val): - if not is_numeric(val): - val = val.strip() - if val == '': - val = 0 - return int(val) - -def to_float(val): - if not is_numeric(val): - val = val.strip() - if val == '': - val = 0 - return float(val) +# import copy +from collections import OrderedDict + +if __name__=='__main__': + from error_codes import error_codes +else: + from .error_codes import error_codes + + +def parse_line(line): + line = line.strip() # remove whitespace at end + return line.split('=') + + +def parse_multi(message): + '''Return an ordered dictionary containing the returned parameters''' + assert isinstance(message, list) + return OrderedDict([parse_line(line) for line in message]) + class Piezo(MessageBasedDriver): """ PI piezo motion controller. It assumes all axes to have units um + + Important: + before operating, ensure that the notch filters are set to the right + frequencies. These frequencies depend on the load on the piezo, and can + be determined in NanoCapture. + + Example: + import numpy as np + import lantz + import visa + import lantz.drivers.pi.piezo as pi + rm = visa.ResourceManager('@py') + lantz.messagebased._resource_manager = rm + ureg = lantz.ureg + try: + lantzlog + except NameError: + #lantzlog = lantz.log.log_to_screen(level=lantz.log.INFO) + lantzlog = lantz.log.log_to_screen(level=lantz.log.ERROR) + + import warnings + warnings.filterwarnings(action='ignore') + + print('Available devices:', lantz.messagebased._resource_manager.list_resources()) + print('Connecting to: ASRL/dev/ttyUSB0::INSTR') + stage = pi.Piezo('ASRL/dev/ttyUSB0::INSTR') + stage.initialize() + stage.sleeptime_after_move = 10*ureg.ms + stage.axis = 'X' + idn = stage.idn + + stage.servo = True + stage.velocity = 10*ureg.um/ureg.ms + + stage.position = 0*ureg.um + print('stage position measured: ', stage.position) + + steps = 10 + stepsize = 100.0*ureg.nm + for n in range(steps): + stage.position = n*stepsize + print('stage position measured: ', stage.position) + + print('Measuring step response') + + # For jupyter notebooks + %matplotlib inline + + stage.servo = True + stage.position = 0 + import time + time.sleep(1) + + stepsize = 10*lantz.ureg.um + timepoints = 100 + + timepos = stage.measure_step_response(stepsize, timepoints) + + lines = stage.plot_step_response(timepos) + + stage.finalize() # nicely close stage and turn of servo - Params: - axis: axis number to controll""" - - DEFAULTS = { - 'COMMON': {'write_termination': '\r\n', - 'read_termination': '\r\n',}, -# TODO: set via PI software -# 'ASRL':{ -# 'timeout': 4000, #ms -# 'encoding': 'ascii', -# 'data_bits': 8, -# 'baud_rate': 19200, -# 'parity': constants.Parity.none, -# 'stop_bits': constants.StopBits.one, -# 'flow_control': constants.VI_ASRL_FLOW_RTS_CTS,#constants.VI_ASRL_FLOW_NONE, -# }, - } - - def initialize(self, axis=1, sleeptime_after_move=0*ureg.ms): + + """ + + DEFAULTS = {'COMMON': {'write_termination': '\n', + 'read_termination': '\n', + 'baud_rate': 57600, + 'timeout': 20}, } + + def initialize(self, axis='X', sleeptime_after_move=0*ureg.ms): + self.axis = axis + self.sleeptime_after_move = sleeptime_after_move super().initialize() - self.axis = 1 - self.sleeptime_after_move = sleep_after_move - - def recv_numeric(self,length): + + def finalize(self): + """ Disconnects stage """ + self.stop() + super().finalize() + + def query_multiple(self, query): + """Read a multi line response""" + self.write(query) + loop = True + ans = [] + while loop: + ret = self.read() + if ret != '': + ans.append(ret) + else: + return ans + + def parse_multiaxis(self, message, axis=None): + "Parse a multi-axis message, return only value for self.axis" + if axis is None: + axis = self.axis + message_parts = message.split(' ') try: - message = self.stage.recv(length) - except (socket.timeout, socket.error), error: - print 'Stage not responsive! ', error - if self.stayconnected: - self.retries = self.retries + 1 - if self.retries < 10: - print 'Retry to connect %i/%i' % (self.retries, 10) - self.connect() - else: - print 'To many retries. Not connected to socket' - self.retries = 0 - message = '' - message = message.strip() - if message == '': - message = 0 - return message - - @Action() + message_axis = [part for part in message_parts if part[0] == axis][0] + except IndexError: + self.log_error('Axis {} not found in returned message: {}'.format(axis, message)) + values = message_axis.split('=') + return values[-1] + + @Feat() def errors(self): error = int(self.query('ERR?')) - if self.error != 0: - self.log_error('Stage error: code {}'.format(self.error)) - return self.error - + if error != 0: + self.log_error('Stage error code {}: {}'.format(error, error_codes[error])) + return error + @Feat() def idn(self): - self.stage.send("*IDN?\n") - idn = self.recv(256) + idn = self.query("*IDN?") + self.errors return idn - + @Action() - def stop(): + def stop(self): '''Stop all motions''' self.servo = False - self.write("#24") - - def finalize(self): - """ Disconnects stage """ - self.stop() - super().finalize() - - @Feat(values={True: '1', False: '0'}) - def servo(self, state): + self.write('STP') + self.errors + + @Feat(values={True: '1', False: '0'}, read_once=True) + def servo(self): ''' Set the stage control in open- or closed-loop (state = False or True)''' - return to_int(self.query('SVO?') - - @serco.setter + return self.parse_multiaxis(self.query('SVO?')) + + @servo.setter def servo(self, state): - self.send('SVO 1 %d\n' % state) + self.write('SVO {} {}'.format(self.axis, state) ) + return self.errors @Feat(units='um/s') def velocity(self): ''' Set the stage velocity (closed-loop only)''' - return self.query('VEL?') - + return self.parse_multiaxis(self.query('VEL?')) + @velocity.setter def velocity(self, velocity): - return self.write('VEL 1 %f' % velocity) + self.write('VEL {} {}'.format(self.axis, velocity)) + return self.errors - @Feat(units='um'): + @Feat(units='um') def position(self): ''' Move to an absolute position the stage (closed-loop only)''' - return self.query('POS?') + return self.parse_multiaxis(self.query('POS?')) @position.setter def position(self, position): - self.move_to(position) + return self.move_to(position, self.sleeptime_after_move) @Action(units=('um','ms')) def move_to(self, position, timeout=None): ''' Move to an absolute position the stage (closed-loop only)''' - ret = self.write('MOV %i %f' % self.axis, position) - timeout = self.sleeptime_after_move if timeout is None: - time.sleep(timeout.to('s')) # Give the stage time to move! (in seconds!) - return ret - - @Feat(units='um'): + self.write('MOV {} {}'.format(self.axis, position)) + time.sleep(timeout * 1e-3) # Give the stage time to move! (in seconds!) + return self.errors + + @Feat(units='um') def read_stage_position(self, nr_avg = 1): ''' Read the current position from the stage''' - positions = [self.position for n in nr_avg] - return np.avg(positions) + positions = [self.position.magnitude for n in range(nr_avg)] + return np.mean(positions) -# before operating, ensure that the notch filters are set to the right frequencies. These frequencies depend on the load on the piezo, and can be determined in NanoCapture. -if __name__=='__main__': - stage = Piezo('') - stage.sleeptime_after_move = 15*ureg.ms - - time.sleep(1) + @Action(units=('um',None)) + def measure_step_response(self, stepsize, points): + '''Python implementation to measure a step response of size stepsize. + Measure number of points as fast as possible. - stage.servo = True - time.sleep(1) - stage.velocity = 0.5*ureg.um/ureg.s + Servo should be on - stage.position = 0 - print 'stage position measured: ', stage.position + Note: a higher temporal resolution can be aquired by the data-recorder + in the driver + + Example: + stage.servo = True + stage.position = 0 + time.sleep(1) + + stepsize = 10*lantz.ureg.um + timepoints = 100 + + timepos = stage.measure_step_response(stepsize, timepoints) + + lines = stage.plot_step_response(timepos) + ''' + if not self.servo: + self.log.error('Servo should be on') + return + import time + self.move_to(self.position + stepsize*ureg.um, 0) + timepos = np.array([[time.time(), self.position.magnitude] for i in range(points)]) + timepos[:,0] -= timepos[0,0] #correct time offset + timepos[:,0] *= 1000 # make it milliseconds + + return timepos + + def plot_step_response(self, timepos): + '''Helper function to visualize a step respons''' + import matplotlib.pyplot as plt + lines = plt.plot(*timepos.T) + plt.xlabel('Time [ms]') + plt.ylabel('Position [um]') + return lines + +if __name__=='__main__': + # before operating, ensure that the notch filters are set to the right + # frequencies. These frequencies depend on the load on the piezo, and can + # be determined in NanoCapture. + from lantz.ui.app import start_test_app + + import lantz + import visa + import lantz.drivers.pi.piezo as pi + rm = visa.ResourceManager('@py') + lantz.messagebased._resource_manager = rm + try: + lantzlog + except NameError: + lantzlog = lantz.log.log_to_screen(level=lantz.log.INFO) + with Piezo('ASRL/dev/ttyUSB0::INSTR') as inst: + start_test_app(inst) - steps = 200 - stepsize = 100.0*ureg.nm - for n in range(steps): - stage.position = n*stepsize - print 'stage position measured: ', stage.position From ffda6461888c0e098802c084d4d9304838f478b3 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Wed, 17 Jan 2018 17:15:45 +0100 Subject: [PATCH 24/50] Added my to authors --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index a6ad339..292c7fa 100644 --- a/AUTHORS +++ b/AUTHORS @@ -7,3 +7,4 @@ Martin Masip Pablo Jais Martin Caldarola Federico Barabas +Vasco Tenner From 4be72164d7c9740987e3b823aa162503fdc506ff Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 29 Jan 2018 13:19:45 +0100 Subject: [PATCH 25/50] Action: log of data is optional --- lantz/action.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lantz/action.py b/lantz/action.py index a86c74c..b21bc5d 100644 --- a/lantz/action.py +++ b/lantz/action.py @@ -63,10 +63,11 @@ class Action(object): changed but only tested to belong to the container. :param units: `Quantity` or string that can be interpreted as units. :param procs: Other callables to be applied to input arguments. + :param log_output: Store the functions output in log """ - def __init__(self, func=None, *, values=None, units=None, limits=None, procs=None): + def __init__(self, func=None, *, values=None, units=None, limits=None, procs=None, log_output=True): #: instance: key: value self.modifiers = WeakKeyDictionary() @@ -76,6 +77,7 @@ def __init__(self, func=None, *, values=None, units=None, limits=None, procs=Non 'units': units, 'limits': limits, 'processors': procs} + self.log_output = log_output self.func = func self.args = () @@ -126,7 +128,10 @@ def call(self, instance, *args, **kwargs): tic = time.time() out = self.func(instance, *t_values) instance.timing.add(name, time.time() - tic) - instance.log_info('{} returned {}', name, out) + if self.log_output: + instance.log_info('{} returned {}', name, out) + else: + instance.log_info('{} returned a value of type {}', name, type(out)) return out except Exception as e: From 7c85676c44d0d46b31322d7f71682463ede22106 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 29 Jan 2018 14:15:23 +0100 Subject: [PATCH 26/50] Support for Basler imaging cameras via PyPylon --- lantz/drivers/basler/__init__.py | 17 +++ lantz/drivers/basler/pylon.py | 180 +++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 lantz/drivers/basler/__init__.py create mode 100644 lantz/drivers/basler/pylon.py diff --git a/lantz/drivers/basler/__init__.py b/lantz/drivers/basler/__init__.py new file mode 100644 index 0000000..b009c34 --- /dev/null +++ b/lantz/drivers/basler/__init__.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.basler + ~~~~~~~~~~~~~~~~~~~~~~ + + :company: Basler + :description: Basler cameras + :website: + + ---- + + :copyright: 2018 by Vasco Tenner + :license: BSD, see LICENSE for more details. +""" +from .pylon import Cam + +__all__ = ['Cam'] diff --git a/lantz/drivers/basler/pylon.py b/lantz/drivers/basler/pylon.py new file mode 100644 index 0000000..69b4087 --- /dev/null +++ b/lantz/drivers/basler/pylon.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.basler + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implementation for a basler camera via pypylon and pylon + + Requires: + - pylon https://www.baslerweb.com/en/support/downloads/software-downloads/ + + Log: + Tried to implement PyPylon, but did not compile with python 3.5 + - PyPylon https://github.com/dihm/PyPylon (tested with version: + f5b5f8dfb179af6c23340fe81a10bb75f2f467f7) + + + Author: Vasco Tenner + Date: 20171208 + + TODO: + - 12 bit packet readout + - Bandwith control + - Dynamically add available feats + - Dynamically set available values for feats +""" + +from lantz.driver import Driver +# from lantz.foreign import LibraryDriver +from lantz import Feat, DictFeat, Action +# import ctypes as ct +import pypylon +import numpy as np + + +def todict(listitems): + 'Helper function to create dicts usable in feats' + d = {} + for item in listitems: + d.update({item:item}) + return d + +class Cam(Driver): + # LIBRARY_NAME = '/opt/pylon5/lib64/libpylonc.so' + + def __init__(self, camera=0, + *args, **kwargs): + """ + @params + :type camera_num: int, The camera device index: 0,1,.. + + Example: + import lantz + from lantz.drivers.basler import Cam + import time + try: + lantzlog + except NameError: + lantzlog = lantz.log.log_to_screen(level=lantz.log.DEBUG) + + cam = Cam(camera='Basler acA4112-8gm (40006341)') + cam.initialize() + cam.exposure_time + cam.exposure_time = 3010 + cam.exposure_time + next(cam.grab_images()) + cam.grab_image() + print('Speedtest:') + nr = 10 + start = time.time() + for n in cam.grab_images(nr): + n + duration = (time.time()-start)*1000*lantz.Q_('ms') + print('Read {} images in {}. Reading alone took {}. Framerate {}'.format(nr, + duration, duration - nr* cam.exposure_time, nr / duration.to('s'))) + cam.finalize() + """ + super().__init__(*args, **kwargs) + self.camera = camera + + def initialize(self): + ''' + Params: + camera -- number in list of show_cameras or friendly_name + ''' + + cameras = pypylon.factory.find_devices() + self.log_debug('Available cameras are:' + str(cameras)) + + try: + if isinstance(self.camera, int): + cam = cameras[self.camera] + self.cam = pypylon.factory.create_device(cam) + else: + try: + cam = [c for c in cameras if c.friendly_name == self.camera][0] + self.cam = pypylon.factory.create_device(cam) + except IndexError: + self.log_error('Camera {} not found in cameras: {}'.format(self.camera, cameras)) + return + except RuntimeError as err: + self.log_error(err) + raise RuntimeError(err) + + self.camera = cam.friendly_name + + # First Open camera before anything is accessable + self.cam.open() + + # get rid of Mono12Packed and give a log error: + fmt = self.pixel_format + if fmt == str('Mono12Packed'): + self.log_error('PixelFormat {} not supported. Using Mono12 instead'.format(fmt)) + self.pixel_format = 'Mono12' + + # Go to full available speed + # cam.properties['DeviceLinkThroughputLimitMode'] = 'Off' + + def finalize(self): + self.cam.close() + return + + @Feat() + def info(self): + # We can still get information of the camera back + return 'Camera info of camera object:', self.cam.device_info + + @Feat(units=['us']) + def exposure_time(self): + return self.cam.properties['ExposureTimeAbs'] + + @exposure_time.setter + def exposure_time(self, time): + self.cam.properties['ExposureTimeAbs'] = time + + @Feat() + def gain(self): + return self.cam.properties['GainRaw'] + + @gain.setter + def gain(self, value): + self.cam.properties['GainRaw'] = value + + @Feat(values=todict(['Mono8','Mono12','Mono12Packed'])) + def pixel_format(self): + fmt = self.cam.properties['PixelFormat'] + if fmt == 'Mono12Packed': + self.log_error('PixelFormat {} not supported. Use Mono12 instead'.format(fmt)) + return fmt + + @pixel_format.setter + def pixel_format(self, value): + if value == 'Mono12Packed': + self.log_error('PixelFormat {} not supported. Using Mono12 instead'.format(value)) + value = 'Mono12' + self.cam.properties['PixelFormat'] = value + + @Feat() + def properties(self): + 'Dict with all properties supported by pylon dll driver' + return self.cam.properties + + @Action() + def list_properties(self): + 'List all properties and their values' + for key in self.cam.properties.keys(): + try: + value = self.cam.properties[key] + except IOError: + value = '' + + print('{0} ({1}):\t{2}'.format(key, + self.cam.properties.get_description(key), value)) + + @Action(log_output=False) + def grab_image(self): + return next(self.cam.grab_images(1)) + + @Action(log_output=False) + def grab_images(self, num=1): + return self.cam.grab_images(num) From 5f36d8d23dc43ec3079e9ebf6601223d4f94b3de Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 29 Jan 2018 14:52:33 +0100 Subject: [PATCH 27/50] Added module newport_esp motion controllers --- .gitmodules | 3 +++ lantz/drivers/newport_esp | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 lantz/drivers/newport_esp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..638d01f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lantz/drivers/newport_esp"] + path = lantz/drivers/newport_esp + url = git@github.com:vascotenner/lantz_driver_newport_esp301.git diff --git a/lantz/drivers/newport_esp b/lantz/drivers/newport_esp new file mode 160000 index 0000000..d744cb7 --- /dev/null +++ b/lantz/drivers/newport_esp @@ -0,0 +1 @@ +Subproject commit d744cb7270c56d7ef9f5b1cbdc6342f27719ed81 From a0095426097015af5b7820c701f4f3dfb3ba269b Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Wed, 28 Feb 2018 13:40:11 +0100 Subject: [PATCH 28/50] Moved newton esp to newton_motion --- .gitmodules | 2 +- lantz/drivers/{newport_esp => newport_motion} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename lantz/drivers/{newport_esp => newport_motion} (100%) diff --git a/.gitmodules b/.gitmodules index 638d01f..607ecf9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "lantz/drivers/newport_esp"] - path = lantz/drivers/newport_esp + path = lantz/drivers/newport_motion url = git@github.com:vascotenner/lantz_driver_newport_esp301.git diff --git a/lantz/drivers/newport_esp b/lantz/drivers/newport_motion similarity index 100% rename from lantz/drivers/newport_esp rename to lantz/drivers/newport_motion From 8a3e48000387b852f060c0f8d1495229a141d465 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Wed, 28 Feb 2018 13:51:06 +0100 Subject: [PATCH 29/50] Seperated most generic newport motion controller in own class. Added support for SMC100 controller. --- __init__.py | 3 +- motion.py | 337 ++++++++++++++++++++++++++++++++++++++++++++++++ motionsmc100.py | 197 ++++++++++++++++++++++++++++ 3 files changed, 536 insertions(+), 1 deletion(-) create mode 100644 motion.py create mode 100644 motionsmc100.py diff --git a/__init__.py b/__init__.py index 15392d5..45abba4 100644 --- a/__init__.py +++ b/__init__.py @@ -15,5 +15,6 @@ """ from .motionesp301 import ESP301, ESP301Axis +from .motionsmc100 import SMC100 -__all__ = ['ESP301', 'ESP301Axis'] +__all__ = ['ESP301', 'ESP301Axis', 'SMC100'] diff --git a/motion.py b/motion.py new file mode 100644 index 0000000..c7e1fee --- /dev/null +++ b/motion.py @@ -0,0 +1,337 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.newport.motion axis + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + General class that implements the commands used for several newport motion + drivers + + :copyright: 2018, see AUTHORS for more details. + :license: GPL, see LICENSE for more details. + + Source: Instruction Manual (Newport) + +""" + + +from lantz.feat import Feat +from lantz.action import Action +from lantz.messagebased import MessageBasedDriver +from pyvisa import constants +from lantz import Q_, ureg +from lantz.processors import convert_to +import time +import numpy as np + +# Add generic units: +# ureg.define('unit = unit') +# ureg.define('encodercount = count') +# ureg.define('motorstep = step') + + +class MotionController(MessageBasedDriver): + """ Newport motion controller. It assumes all axes to have units mm + + """ + + DEFAULTS = { + 'COMMON': {'write_termination': '\r\n', + 'read_termination': '\r\n', }, + 'ASRL': { + 'timeout': 10, # ms + 'encoding': 'ascii', + 'data_bits': 8, + 'baud_rate': 57600, + 'parity': constants.Parity.none, + 'stop_bits': constants.StopBits.one, + 'flow_control': constants.VI_ASRL_FLOW_RTS_CTS, + }, + } + + def initialize(self): + super().initialize() + + @Feat() + def idn(self): + raise AttributeError('Not implemented') + # return self.query('ID?') + + @Action() + def detect_axis(self): + """ Find the number of axis available. + + The detection stops as soon as an empty controller is found. + """ + pass + + @Action() + def get_errors(self): + raise AttributeError('Not implemented') + + @Feat(read_once=False) + def position(self): + return [axis.position for axis in self.axes] + + @Feat(read_once=False) + def _position_cached(self): + return [axis.recall('position') for axis in self.axes] + + @position.setter + def position(self, pos): + """Move to position (x,y,...)""" + return self._position(pos) + + @Action() + def _position(self, pos, read_pos=None, wait_until_done=True): + """Move to position (x,y,...)""" + if read_pos is not None: + self.log_error('kwargs read_pos for function _position is deprecated') + + for p, axis in zip(pos, self.axes): + if p is not None: + axis._set_position(p, wait=False) + if wait_until_done: + for p, axis in zip(pos, self.axes): + if p is not None: + axis._wait_until_done() + axis.check_position(p) + return self.position + + return pos + + @Action() + def motion_done(self): + for axis in self.axes: + axis._wait_until_done() + + def finalize(self): + for axis in self.axes: + if axis is not None: + del (axis) + super().finalize() + + +class MotionAxis(MotionController): + def __init__(self, parent, num, id, *args, **kwargs): + self.parent = parent + self.num = num + self._idn = id + self.wait_time = 0.01 # in seconds * Q_(1, 's') + self.backlash = 0 + self.wait_until_done = True + self.accuracy = 0.001 # in units reported by axis + # Fill position cache: + self.position + self.last_set_position = self.position.magnitude + + def __del__(self): + self.parent = None + self.num = None + + def query(self, command, *, send_args=(None, None), recv_args=(None, None)): + return self.parent.query('{:d}{}'.format(self.num, command), + send_args=send_args, recv_args=recv_args) + + def write(self, command, *args, **kwargs): + return self.parent.write('{:d}{}'.format(self.num, command), + *args, **kwargs) + + @Feat() + def idn(self): + return self.query('ID?') + + @Action() + def on(self): + """Put axis on""" + self.write('MO') + + @Action() + def off(self): + """Put axis on""" + self.write('MF') + + @Feat(values={True: '1', False: '0'}) + def is_on(self): + """ + :return: True is axis on, else false + """ + return self.query('MO?') + + @Action(units='mm') + def define_home(self, val=0): + """Remap current position to home (0), or to new position + + :param val: new position""" + self.write('DH%f' % val) + + @Action() + def home(self): + """Execute the HOME command""" + self.write('OR') + + @Feat(units='mm') + def position(self): + return self.query('TP?') + + @position.setter + def position(self, pos): + """ + Waits until movement is done if self.wait_until_done = True. + + :param pos: new position + """ + if not self.is_on: + self.log_error('Axis not enabled. Not moving!') + return + + # First do move to extra position if necessary + self._set_position(pos, wait=self.wait_until_done) + + @Action(units=['mm', None]) + def _set_position(self, pos, wait=None): + """ + Move to an absolute position, taking into account backlash. + + When self.backlash is to a negative value the stage will always move + from low to high values. If necessary, a extra step with length + self.backlash is set. + + :param pos: New position in mm + :param wait: wait until stage is finished + """ + + # First do move to extra position if necessary + if self.backlash: + position = self.position.magnitude + backlash = convert_to('mm', on_dimensionless='ignore' + )(self.backlash).magnitude + if (backlash < 0 and position > pos) or\ + (backlash > 0 and position < pos): + + self.log_info('Using backlash') + self.__set_position(pos + backlash) + self._wait_until_done() + + # Than move to final position + self.__set_position(pos) + if wait: + self._wait_until_done() + self.check_position(pos) + + def __set_position(self, pos): + """ + Move stage to a certain position + :param pos: New position + """ + self.write('PA%f' % (pos)) + self.last_set_position = pos + + @Action(units='mm') + def check_position(self, pos): + '''Check is stage is at expected position''' + if np.isclose(self.position, pos, atol=self.accuracy): + return True + self.log_error('Position accuracy {} is not reached.' + 'Expected: {}, measured: {}'.format(self.accuracy, + pos, + self.position)) + return False + + @Feat(units='mm/s') + def max_velocity(self): + return float(self.query('VU?')) + + @max_velocity.setter + def max_velocity(self, velocity): + self.write('VU%f' % (velocity)) + + @Feat(units='mm/s**2') + def max_acceleration(self): + return float(self.query('AU?')) + + @max_acceleration.setter + def max_acceleration(self, velocity): + self.write('AU%f' % (velocity)) + + @Feat(units='mm/s') + def velocity(self): + return float(self.query('VA?')) + + @velocity.setter + def velocity(self, velocity): + """ + :param velocity: Set the velocity that the axis should use when moving + :return: + """ + self.write('VA%f' % (velocity)) + + @Feat(units='mm/s**2') + def acceleration(self): + return float(self.query('VA?')) + + @acceleration.setter + def acceleration(self, acceleration): + """ + :param acceleration: Set the acceleration that the axis should use + when starting + :return: + """ + self.write('AC%f' % (acceleration)) + + @Feat(units='mm/s') + def actual_velocity(self): + return float(self.query('TV')) + + @actual_velocity.setter + def actual_velocity(self, val): + raise NotImplementedError + + @Action() + def stop(self): + """Emergency stop""" + self.write('ST') + + @Feat(values={True: '1', False: '0'}) + def motion_done(self): + return self.query('MD?') + + # Not working yet, see https://github.com/hgrecco/lantz/issues/35 + # @Feat(values={Q_('encodercount'): 0, + # Q_('motor step'): 1, + # Q_('millimeter'): 2, + # Q_('micrometer'): 3, + # Q_('inches'): 4, + # Q_('milli-inches'): 5, + # Q_('micro-inches'): 6, + # Q_('degree'): 7, + # Q_('gradian'): 8, + # Q_('radian'): 9, + # Q_('milliradian'): 10, + # Q_('microradian'): 11}) + @Feat() + def units(self): + ret = int(self.query(u'SN?')) + vals = {0 :'encoder count', + 1 :'motor step', + 2 :'millimeter', + 3 :'micrometer', + 4 :'inches', + 5 :'milli-inches', + 6 :'micro-inches', + 7 :'degree', + 8 :'gradian', + 9 :'radian', + 10:'milliradian', + 11:'microradian',} + return vals[ret] + + # @units.setter + # def units(self, val): + # self.parent.write('%SN%' % (self.num, val)) + + def _wait_until_done(self): + # wait_time = convert_to('seconds', on_dimensionless='warn')(self.wait_time) + time.sleep(self.wait_time) + while not self.motion_done: + time.sleep(self.wait_time) + return True diff --git a/motionsmc100.py b/motionsmc100.py new file mode 100644 index 0000000..f38e438 --- /dev/null +++ b/motionsmc100.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.newport.motionsmc100 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implements the drivers to control SMC100 controller + + :copyright: 2018, see AUTHORS for more details. + :license: GPL, see LICENSE for more details. + + Source: Instruction Manual (Newport) + +""" + + +from lantz.feat import Feat +from lantz.action import Action +from pyvisa import constants +from lantz import Q_, ureg +from lantz.processors import convert_to +if __name__ == '__main__': + from motion import MotionController, MotionAxis +else: + from .motion import MotionController, MotionAxis +import time +import numpy as np + +ERRORS = {"@": "", + "A": "Unknown message code or floating point controller address.", + "B": "Controller address not correct.", + "C": "Parameter missing or out of range.", + "D": "Execution not allowed.", + "E": "home sequence already started.", + "I": "Execution not allowed in CONFIGURATION state.", + "J": "Execution not allowed in DISABLE state.", + "K": "Execution not allowed in READY state.", + "L": "Execution not allowed in HOMING state.", + "M": "Execution not allowed in MOVING state.",} + + +class SMC100(MotionController): + """ Newport SMC100 motion controller. It assumes all axes to have units mm + + + Example: + import numpy as np + import lantz + import visa + import lantz.drivers.pi.piezo as pi + from lantz.drivers.newport_motion import SMC100 + from pyvisa import constants + rm = visa.ResourceManager('@py') + lantz.messagebased._resource_manager = rm + ureg = lantz.ureg + try: + lantzlog + except NameError: + lantzlog = lantz.log.log_to_screen(level=lantz.log.DEBUG) + lantz.log.log_to_socket(level=lantz.log.DEBUG) + + import time + import numpy as np + import warnings + #warnings.filterwarnings(action='ignore') + print(lantz.messagebased._resource_manager.list_resources()) + stage = SMC100('ASRL/dev/ttyUSB0::INSTR') + stage.initialize() + axis0 = stage.axes[0] + print('Axis id:' + axis0.idn) + print('Axis position: {}'.format(axis0.position)) + axis0.keypad_disable() + axis0.position += 0.1 * ureg.mm + print('Errors: {}'.format(axis0.get_errors())) + stage.finalize() + """ + + DEFAULTS = { + 'COMMON': {'write_termination': '\r\n', + 'read_termination': '\r\n', }, + 'ASRL': { + 'timeout': 100, # ms + 'encoding': 'ascii', + 'data_bits': 8, + 'baud_rate': 57600, + 'parity': constants.Parity.none, + 'stop_bits': constants.StopBits.one, + #'flow_control': constants.VI_ASRL_FLOW_NONE, + 'flow_control': constants.VI_ASRL_FLOW_XON_XOFF, # constants.VI_ASRL_FLOW_NONE, + }, + } + + def __init__(self, *args, **kwargs): + self.motionaxis_class = kwargs.pop('motionaxis_class', MotionAxisSMC100) + super().__init__(*args, **kwargs) + + def initialize(self): + super().initialize() + self.detect_axis() + + @Action() + def detect_axis(self): + """ Find the number of axis available. + + The detection stops as soon as an empty controller is found. + """ + self.axes = [] + i = 0 + scan_axes = True + while scan_axes: + i += 1 + id = self.query('%dID?' % i) + if id == '': + scan_axes = False + else: + axis = self.motionaxis_class(self, i, id) + self.axes.append(axis) + + + + + +class MotionAxisSMC100(MotionAxis): + def query(self, command, *, send_args=(None, None), recv_args=(None, None)): + respons = super().query(command,send_args=send_args, recv_args=recv_args) + #check for command: + if not respons[:3] == '{:d}{}'.format(self.num, command[:2]): + self.log_error('Axis {}: Expected to return command {} instead of {}'.format(self.num, command[:3],respons[:3])) + return respons[3:] + + def write(self, command, *args, **kwargs): + super().write(command, *args, **kwargs) + return self.get_errors() + + @Action() + def on(self): + """Put axis on""" + pass + + @Action() + def off(self): + """Put axis on""" + pass + + @Action() + def get_errors(self): + ret = self.query('TE?') + err = ERRORS.get(ret, 'Error {}. Lookup in manual: https://www.newport.com/medias/sys_master/images/images/h11/he1/9117182525470/SMC100CC-SMC100PP-User-s-Manual.pdf'.format(ret)) + if err: + self.log_error('Axis {} error: {}'.format(self.num, err)) + return err + + @Feat(values={True: '1', False: '0'}) + def is_on(self): + """ + :return: True is axis on, else false + """ + return '1' + + @Feat() + def motion_done(self): + return self.check_position(self.last_set_position) + + @Action() + def keypad_disable(self): + return self.write('JD') + + +if __name__ == '__main__': + import argparse + import lantz.log + + parser = argparse.ArgumentParser(description='Test SMC100 driver') + parser.add_argument('-p', '--port', type=str, default='1', + help='Serial port to connect to') + + args = parser.parse_args() + lantzlog = lantz.log.log_to_screen(level=lantz.log.INFO) + lantz.log.log_to_socket(lantz.log.DEBUG) + + import lantz + import visa + import lantz.drivers.newport_motion + sm = lantz.drivers.newport_motion.SMC100 + rm = visa.ResourceManager('@py') + lantz.messagebased._resource_manager = rm + + print(lantz.messagebased._resource_manager.list_resources()) + + with sm(args.port) as inst: + #with sm.via_serial(port=args.port) as inst: + inst.idn + # inst.initialize() # Initialize the communication with the power meter + # Find the status of all axes: + #for axis in inst.axes: + # print('Axis {} Position {} is_on {} max_velocity {} velocity {}'.format(axis.num, axis.position, + # axis.is_on, axis.max_velocity, + # axis.velocity)) From 498544465240f76d0ef2811c4033626c2939ff06 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Wed, 28 Feb 2018 14:04:20 +0100 Subject: [PATCH 30/50] Fixed units for exposuretime --- lantz/drivers/basler/pylon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lantz/drivers/basler/pylon.py b/lantz/drivers/basler/pylon.py index 69b4087..20ecfbc 100644 --- a/lantz/drivers/basler/pylon.py +++ b/lantz/drivers/basler/pylon.py @@ -124,7 +124,7 @@ def info(self): # We can still get information of the camera back return 'Camera info of camera object:', self.cam.device_info - @Feat(units=['us']) + @Feat(units='us') def exposure_time(self): return self.cam.properties['ExposureTimeAbs'] From e07402495ff39d35c8a31492b625461fef4ad5d6 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Wed, 28 Feb 2018 16:33:01 +0100 Subject: [PATCH 31/50] Dynamically load feats to driver --- lantz/drivers/basler/pylon.py | 98 ++++++++++++++++++++++++++++------- 1 file changed, 78 insertions(+), 20 deletions(-) diff --git a/lantz/drivers/basler/pylon.py b/lantz/drivers/basler/pylon.py index 20ecfbc..5e2fd1f 100644 --- a/lantz/drivers/basler/pylon.py +++ b/lantz/drivers/basler/pylon.py @@ -9,7 +9,9 @@ - pylon https://www.baslerweb.com/en/support/downloads/software-downloads/ Log: - Tried to implement PyPylon, but did not compile with python 3.5 + - Dynamically add available feats + - Collect single and multiple images + - Set Gain and exposure time - PyPylon https://github.com/dihm/PyPylon (tested with version: f5b5f8dfb179af6c23340fe81a10bb75f2f467f7) @@ -20,8 +22,8 @@ TODO: - 12 bit packet readout - Bandwith control - - Dynamically add available feats - Dynamically set available values for feats + - Set ROI """ from lantz.driver import Driver @@ -32,6 +34,13 @@ import numpy as np +beginner_controls = ['ExposureTimeAbs', 'GainRaw', 'Width', 'Height', + 'OffsetX', 'OffsetY'] +property_units = {'ExposureTimeAbs': 'us', } +aliases = {'exposure_time': 'ExposureTimeAbs', + 'gain': 'GainRaw', + } + def todict(listitems): 'Helper function to create dicts usable in feats' d = {} @@ -39,14 +48,38 @@ def todict(listitems): d.update({item:item}) return d +def attach_dyn_propr(instance, prop_name, propr): + """Attach property proper to instance with name prop_name. + + Reference: + * https://stackoverflow.com/a/1355444/509706 + * https://stackoverflow.com/questions/48448074 + """ + class_name = instance.__class__.__name__ + 'C' + child_class = type(class_name, (instance.__class__,), {prop_name: propr}) + + instance.__class__ = child_class + +def create_getter(p): + def tmpfunc(self): + return self.cam.properties[p] + return tmpfunc + +def create_setter(p): + def tmpfunc(self, val): + self.cam.properties[p] = val + return tmpfunc + + class Cam(Driver): # LIBRARY_NAME = '/opt/pylon5/lib64/libpylonc.so' - def __init__(self, camera=0, + def __init__(self, camera=0, level='beginner', *args, **kwargs): """ @params :type camera_num: int, The camera device index: 0,1,.. + :type level: str, Level of controls to show ['beginner', 'expert'] Example: import lantz @@ -76,6 +109,7 @@ def __init__(self, camera=0, """ super().__init__(*args, **kwargs) self.camera = camera + self.level = level def initialize(self): ''' @@ -102,10 +136,13 @@ def initialize(self): raise RuntimeError(err) self.camera = cam.friendly_name - + # First Open camera before anything is accessable self.cam.open() + self._dynamically_add_properties() + self._aliases() + # get rid of Mono12Packed and give a log error: fmt = self.pixel_format if fmt == str('Mono12Packed'): @@ -119,26 +156,47 @@ def finalize(self): self.cam.close() return - @Feat() - def info(self): - # We can still get information of the camera back - return 'Camera info of camera object:', self.cam.device_info + def _dynamically_add_properties(self): + '''Add all properties available on driver as Feats''' + # What about units? + props = self.properties.keys() if self.level == 'expert' else beginner_controls + for p in props: + feat = Feat(fget=create_getter(p), + fset=create_setter(p), + doc=self.cam.properties.get_description(p), + units=property_units.get(p, None), + ) + feat.name = p + attach_dyn_propr(self, p, feat) + + def _aliases(self): + """Add easy to use aliases to strange internal pylon names - @Feat(units='us') - def exposure_time(self): - return self.cam.properties['ExposureTimeAbs'] + Note that in the Logs, the original is renamed to the alias""" + for alias, orig in aliases.items(): + attach_dyn_propr(self, alias, self.feats[orig].feat) - @exposure_time.setter - def exposure_time(self, time): - self.cam.properties['ExposureTimeAbs'] = time @Feat() - def gain(self): - return self.cam.properties['GainRaw'] + def info(self): + # We can still get information of the camera back + return 'Camera info of camera object:', self.cam.device_info - @gain.setter - def gain(self, value): - self.cam.properties['GainRaw'] = value +# @Feat(units='us') +# def exposure_time(self): +# return self.cam.properties['ExposureTimeAbs'] +# +# @exposure_time.setter +# def exposure_time(self, time): +# self.cam.properties['ExposureTimeAbs'] = time +# +# @Feat() +# def gain(self): +# return self.cam.properties['GainRaw'] +# +# @gain.setter +# def gain(self, value): +# self.cam.properties['GainRaw'] = value @Feat(values=todict(['Mono8','Mono12','Mono12Packed'])) def pixel_format(self): @@ -153,7 +211,7 @@ def pixel_format(self, value): self.log_error('PixelFormat {} not supported. Using Mono12 instead'.format(value)) value = 'Mono12' self.cam.properties['PixelFormat'] = value - + @Feat() def properties(self): 'Dict with all properties supported by pylon dll driver' From e2825101fbf978d7f0795fa92b86cf76df19a432 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 5 Mar 2018 14:01:37 +0100 Subject: [PATCH 32/50] Submodule for newport motion and support for smc100 motion stage --- lantz/drivers/newport_motion | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lantz/drivers/newport_motion b/lantz/drivers/newport_motion index d744cb7..8a3e480 160000 --- a/lantz/drivers/newport_motion +++ b/lantz/drivers/newport_motion @@ -1 +1 @@ -Subproject commit d744cb7270c56d7ef9f5b1cbdc6342f27719ed81 +Subproject commit 8a3e48000387b852f060c0f8d1495229a141d465 From 73f4d6ff3a05a73515c4e043f10a74b19c0120da Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 5 Mar 2018 14:28:44 +0100 Subject: [PATCH 33/50] Added reference list of possible properties --- lantz/drivers/basler/props.txt | 361 +++++++++++++++++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 lantz/drivers/basler/props.txt diff --git a/lantz/drivers/basler/props.txt b/lantz/drivers/basler/props.txt new file mode 100644 index 0000000..dbb7d46 --- /dev/null +++ b/lantz/drivers/basler/props.txt @@ -0,0 +1,361 @@ +#This files contains the properties of a acA4112-8gm camera on 20180305 +Build against pylon library version: 5.0.11.build_10914 +Available cameras are [] +Camera info of camera object: +Bpp8 +SequenceEnable (Enables the sequencer.): False +SequenceCurrentSet (Current sequence set.): +SequenceConfigurationMode (Sets whether the sequencer can be configured.): Off +SequenceAsyncRestart (Allows to restart the sequence of sequence sets, starting with the sequence set that has the lowest index number. The restart is asynchronous to the camera's frame trigger. Only available in Auto and Controlled sequence advance mode.): +SequenceAsyncAdvance (Allows to advance from the current sequence set to the next one. The advance is asynchronous to the camera's frame trigger. Only available in Controlled sequence advance mode.): +SequenceSetTotalNumber (Total number of sequence sets in the sequence.): 2 +SequenceSetIndex (Index number of a sequence set.): 0 +SequenceSetStore (Stores the current sequence set. Storing the current sequence set will overwrite any already existing sequence set bearing the same index number. The sequence set is stored in the volatile memory and will therefore be lost if the camera is reset or if power is switched off.): +SequenceSetLoad (Loads an existing sequence set to make it the current sequence set.): +SequenceSetExecutions (Number of consecutive executions per sequence cycle for the selected sequence set. Only available in auto sequence advance mode.): 1 +SequenceAdvanceMode (Sets the sequence set advance mode.): Auto +SequenceControlSelector (Sets whether the sequence control source should be set for sequence advance or for sequence restart. Once this value has been set, a control source must be chosen using the SequenceControlSource parameter.): +SequenceControlSource (Sets the source for sequence control.): +SequenceAddressBitSelector (Sets which bit of the sequence set address can be controlled. Once a bit has been set, an input line can be set as the control source for this bit using the SequenceAddressBitSource parameter.): +SequenceAddressBitSource (Sets an input line as the control source for the currently selected sequence set address bit. The bit can be selected using the SequenceAddressBitSelector.): +GainAuto (Sets the operation mode of the gain auto function. The gain auto function automatically adjusts the gain within set limits until a target brightness value is reached.): Off +GainSelector (Sets the gain channel or tap to be adjusted. Once a gain channel or tap has been selected, all changes to the Gain parameter will be applied to the selected channel or tap.): All +GainRaw (Raw value of the selected gain control. The raw value is an integer value that sets the selected gain control in units specific to the camera.): 0 +BlackLevelSelector (Sets the black level channel or tap to be adjusted. Once a black level channel or tap has been selected, all changes to the BlackLevel parameter will be applied to the selected channel or tap.): All +BlackLevelRaw (Value of the selected black level control.): 0 +GammaEnable (Enables gamma correction.): False +GammaSelector (Sets the type of gamma to apply.): User +Gamma (Gamma correction value. Gamma correction lets you modify the brightness of the pixel values to account for a non-linearity in the human perception of brightness.): 1.0 +DigitalShift (Value set for digital shift. When the parameter is set to zero, digital shift will be disabled. When the parameter is set to 1, 2, 3, or 4, digital shift will be set to shift by 1, shift by 2, shift by 3, or shift by 4 respectively.): 0 +PixelFormat (Sets the format of the pixel data transmitted by the camera. The available pixel formats depend on the camera model and whether the camera is monochrome or color.): Mono8 +PixelSize (Returns the depth of the pixel values in the image (in bits per pixel). The value will always be coherent with the pixel format setting.): Bpp8 +PixelColorFilter (Returns the alignment of the camera's Bayer filter to the pixels in the acquired images.): None +PixelDynamicRangeMin (Minimum possible pixel value that could be transferred from the camera.): 0 +PixelDynamicRangeMax (Maximum possible pixel value that could be transferred from the camera.): 255 +ReverseX (Enables horizontal flipping of the image. The AOI is applied after the flipping.): False +ReverseY (Enables vertical flipping of the image. The AOI is applied after the flipping.): False +TestImageSelector (Sets the test image to display.): Off +TestImageResetAndHold (Holds all moving test images at their starting position. All test images will be displayed at their starting positions and will stay fixed.): False +SensorWidth (Width of the device's sensor in pixels.): 4112 +SensorHeight (Height of the device's sensor in pixels.): 3008 +WidthMax (Maximum allowed width of the image in pixels, taking into account any function that may limit the allowed width.): 4112 +HeightMax (Maximum allowed height of the image in pixels, taking into account any function that may limit the allowed height.): 3008 +LightSourceSelector (Sets the type of light source to be considered for matrix color transformation.): +BalanceWhiteReset (Allows returning to the color adjustment settings extant before the latest changes of the settings.): +BalanceWhiteAuto (Sets the operation mode of the balance white auto function.): +BalanceRatioSelector (Sets the color channel to be adjusted for manual white balance. Once a color intensity has been selected, all changes to the balance ratio parameter will be applied to the selected color intensity.): +BalanceRatioAbs (Value of the currently selected balance ratio channel or tap.): +BalanceRatioRaw (Value of the currently selected balance ratio control.): +ColorTransformationSelector (Sets the type of color transformation to be performed.): +ColorTransformationValueSelector (Sets the element to be entered in the color transformation matrix for custom color transformation. Note: Depending on the camera model, some elements in the color transformation matrix may be preset and can not be changed.): +ColorTransformationValue (Transformation value for the selected element in the color transformation matrix.): +ColorTransformationValueRaw (Raw transformation value for the selected element in the color transformation matrix.): +ColorTransformationMatrixFactor (Extent to which the selected light source will be considered in color matrix transformation.): +ColorTransformationMatrixFactorRaw (Extent to which the selected light source will be considered in color matrix transformation. If the value is set to 65536, the selected light source will be fully considered. If the value is set to 0, the selected light source will not be considered.): +ColorAdjustmentEnable (Enables color adjustment.): +ColorAdjustmentReset (Allows returning to the color adjustment settings extant before the latest changes of the settings.): +ColorAdjustmentSelector (Sets the color for color adjustment.): +ColorAdjustmentHue (Hue adjustment value for the currently selected color.): +ColorAdjustmentHueRaw (Adjustment of hue for the selected color.): +ColorAdjustmentSaturation (Saturation adjustment value for the currently selected color.): +ColorAdjustmentSaturationRaw (Adjustment of saturation for the selected color.): +DemosaicingMode (Sets the demosaicing mode.): +NoiseReductionAbs (Amount of noise reduction to apply. The higher the value, the less chroma noise will be visible in your images. However, too high values may result in image information loss. To enable this feature, the DemosaicingMode parameter must be set to BaslerPGI.): +NoiseReductionRaw (Amount of noise reduction to apply. The higher the value, the less chroma noise will be visible in your images. However, too high values may result in image information loss. To enable this feature, the DemosaicingMode parameter must be set to BaslerPGI.): +SharpnessEnhancementAbs (Amount of sharpening to apply. The higher the sharpness, the more distinct the image subject's contours will be. However, too high values may result in image information loss. To enable this feature, the DemosaicingMode parameter must be set to BaslerPGI.): +SharpnessEnhancementRaw (Amount of sharpening to apply. The higher the sharpness, the more distinct the image subject's contours will be. However, too high values may result in image information loss. To enable this feature, the DemosaicingMode parameter must be set to BaslerPGI.): +Width (Width of the area of interest in pixels.): 4096 +Height (Height of the area of interest in pixels.): 3000 +OffsetX (Horizontal offset from the left side of the sensor to the area of interest (in pixels).): 8 +OffsetY (Vertical offset from the top of the sensor to the area of interest (in pixels).): 4 +CenterX (Enables horizontal centering of the image.): False +CenterY (Enables vertical centering of the image.): False +BinningModeHorizontal (Sets the horizontal binning mode.): Summing +BinningHorizontalMode (Sets the binning horizontal mode.): Sum +BinningHorizontal (Number of adjacent horizontal pixels to be summed. Their charges will be summed and reported out of the camera as a single pixel.): 1 +BinningModeVertical (Sets the vertical binning mode.): Summing +BinningVerticalMode (Sets the binning vertical mode.): Sum +BinningVertical (Number of adjacent vertical pixels to be summed. Their charges will be summed and reported out of the camera as a single pixel.): 1 +DecimationHorizontal (Horizontal decimation factor. It specifies the extent of horizontal sub-sampling of the acquired frame, i.e. it defines how many pixel columns are left out of transmission. This has the net effect of reducing the horizontal resolution (width) of the image by the specified decimation factor. A value of 1 indicates that the camera performs no horizontal decimation.): +DecimationVertical (Vertical decimation factor. It specifies the extent of vertical sub-sampling of the acquired frame, i.e. it defines how many rows are left out of transmission. This has the net effect of reducing the vertical resolution (height) of the image by the specified decimation factor. A value of 1 indicates that the camera performs no vertical decimation.): +ScalingHorizontalAbs (Horizontal scaling factor.): +ScalingVerticalAbs (Vertical scaling factor): +EnableBurstAcquisition (Enables burst acquisition. When enabled, the maximum frame rate only depends on sensor timing and timing of the trigger sequence, and not on the image transfer rate out of the camera.): +AcquisitionMode (Sets the image acquisition mode.): Continuous +AcquisitionStart (Starts the acquisition of images. If the camera is set for single frame acquisition, it will start acquisition of one frame. If the camera is set for continuous frame acquisition, it will start continuous acquisition of frames.): +AcquisitionStop (Stops the acquisition of images if the camera is set for continuous image acquisition and acquisition has been started.): +AcquisitionFrameCount (Number of frames acquired in the multiframe acquisition mode.): 1 +TriggerSelector (Sets the trigger type to be configured. Once a trigger type has been set, all changes to the trigger settings will be applied to the selected trigger.): FrameStart +TriggerMode (Sets the mode for the currently selected trigger.): Off +TriggerSoftware (Generates a software trigger signal. The software trigger signal will be used if the TriggerSource parameter is set to Software.): +TriggerSource (Sets the signal source for the selected trigger.): Line1 +TriggerActivation (Sets the signal transition that activates the selected trigger.): RisingEdge +TriggerDelayAbs (Trigger delay time in microseconds. The delay is applied after the trigger reception and before effectively activating the trigger.): 0.0 +ExposureMode (Sets the exposure mode.): Timed +ExposureAuto (Sets the operation mode of the exposure auto function. The exposure auto function automatically adjusts the exposure time within set limits until a target brightness value is reached.): Off +ExposureTimeAbs (Exposure time of the camera in microseconds.): 3000.0 +ExposureTimeRaw (Raw exposure time of the camera. This value sets an integer that will be used as a multiplier for the exposure timebase. The actual exposure time equals the current ExposureTimeRaw setting multiplied with the current ExposureTimeBaseAbs setting.): 3000 +ReadoutTimeAbs (Sensor readout time given the current settings.): 104075.0 +ExposureOverlapTimeMode (Sets the exposure overlap time mode.): +ExposureOverlapTimeMaxAbs (Maximum overlap of the sensor exposure with sensor readout in TriggerWidth exposure mode (in microseconds).): +ExposureOverlapTimeMaxRaw (Maximum overlap time of the sensor exposure with sensor readout in TriggerWidth exposure mode (in raw units).): +ShutterMode (Sets the shutter mode.): Global +SensorReadoutMode (Sets the sensor readout mode.): +AcquisitionFrameRateEnable (Enables setting the camera's acquisition frame rate to a specified value.): False +AcquisitionFrameRateAbs (Acquisition frame rate of the camera in frames per second.): 8.0 +ResultingFramePeriodAbs (Minimum allowed frame acquisition period (in microseconds) given the current settings for the area of interest, exposure time, and bandwidth.): 115056.0 +ResultingFrameRateAbs (Maximum allowed frame acquisition rate given the current camera settings (in frames per second).): 8.691419830343484 +AcquisitionStatusSelector (Sets the acquisition status to be checked. Once a status has been set, the status can be checked by reading the AcquisitionStatus parameter value.): FrameTriggerWait +AcquisitionStatus (Indicates the status (true or false) of the currently selected acquisition signal. The acquisition signal can be selected using AcquisitionStatusSelector.): False +SyncFreeRunTimerEnable (Enables the synchronous free run mode. When enabled, the camera will generate all required frame start or line start trigger signals internally, and you do not need to apply frame start or line start trigger signals to the camera.): False +SyncFreeRunTimerStartTimeLow (Low 32 bits of the synchronous free run trigger start time.): 0 +SyncFreeRunTimerStartTimeHigh (High 32 bits of the synchronous free run trigger start time.): 0 +SyncFreeRunTimerTriggerRateAbs (Synchronous free run trigger rate.): 10.0 +SyncFreeRunTimerUpdate (Activates changed settings for the synchronous free run.): +TLParamsLocked (Indicates whether a live grab is under way.): 0 +LineSelector (Sets the I/O line to be configured. Once a line has been set, all changes to the line settings will be applied to the selected line.): Line1 +LineMode (Sets the mode for the selected line. This controls whether the physical line is used to input or output a signal.): Input +LineLogic (Returns the line logic of the currently selected line.): Positive +LineFormat (Returns the electrical configuration of the currently selected line.): OptoCoupled +LineSource (Sets the source signal for the currently selected line. The currently selected line must be an output line.): +LineInverter (Enables the signal inverter function for the currently selected input or output line.): False +LineTermination (Enables the termination resistor of the selected input line.): +LineDebouncerTimeAbs (Value of the selected line debouncer time in microseconds.): 0.0 +LineDebouncerTimeRaw (Raw value of the selected line debouncer time.): 0 +MinOutPulseWidthRaw (Raw value for the minimum signal width of a signal that is received from the frequency converter or from the shaft encoder module and that is associated with a digital output line.): +MinOutPulseWidthAbs (Value for the minimum signal width of an output signal (in microseconds) .): +LineStatus (Indicates the current logical state of the selected line.): False +LineStatusAll (A single bit field indicating the current logical state of all available line signals at time of polling.): 6 +UserOutputSelector (Sets the user settable output signal to be configured. Once a user settable output signal has been set, all changes to the user settable output signal settings will be applied to the selected user settable output signal.): UserOutput1 +UserOutputValue (Enables the selected user settable output line.): False +UserOutputValueAll (A single bit field that sets the state of all user settable output signals in one access): 0 +SyncUserOutputSelector (Sets the user settable synchronous output signal to be configured.): SyncUserOutput1 +SyncUserOutputValue (Enables the selected user settable synchronous output line.): False +SyncUserOutputValueAll (A single bit field that sets the state of all user settable synchronous output signals in one access.): 0 +TimerDelayTimebaseAbs (Time base (in microseconds) that is used when a timer delay is set using the TimerDelayRaw parameter.): 1.0 +TimerDurationTimebaseAbs (Time base (in microseconds) that is used when a timer duration is set using the TimerDurationRaw parameter.): 1.0 +TimerSelector (Sets the timer to be configured. Once a timer has been set, all changes to the timer settings will be applied to the selected timer.): Timer1 +TimerDelayAbs (Delay of the currently selected timer in microseconds.): 0.0 +TimerDelayRaw (Raw delay for the selected timer. This value sets an integer that will be used as a multiplier for the timer delay timebase. The actual delay time equals the current TimerDelayRaw setting multiplied with the current TimerDelayTimeBaseAbs setting.): 0 +TimerDurationAbs (Duration of the currently selected timer in microseconds.): 10.0 +TimerDurationRaw (Raw duration for the selected timer. This value sets an integer that will be used as a multiplier for the timer duration timebase. The actual duration time equals the current TimerDurationRaw setting multiplied with the current TimerDurationTimeBaseAbs setting.): 10 +TimerTriggerSource (Sets the internal camera signal used to trigger the selected timer.): ExposureStart +TimerTriggerActivation (Sets the type of signal transition that will start the timer.): RisingEdge +CounterSelector (Sets the counter to be configured. Once a counter has been set, all changes to the counter settings will be applied to this counter.): Counter1 +CounterEventSource (Sets the event that increments the currently selected counter.): FrameTrigger +CounterResetSource (Sets the source signal that can reset the currently selected counter.): Off +CounterReset (Immediately resets the selected counter. The counter starts counting immediately after the reset.): +LUTSelector (Sets the lookup table (LUT) to be configured. Once a LUT has been selected, all changes to the LUT settings will be applied to the selected LUT.): Luminance +LUTEnable (Enables the selected lookup table (LUT).): False +LUTIndex (Index of the LUT element to access.): 0 +LUTValue (Value of the LUT element at the LUT index position.): 0 +LUTValueAll (A single register that lets you access all LUT coefficients without the need to repeatedly use the LUTIndex parameter.): 0x00000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000280000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003800000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000580000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000006800000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000078000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000880000000000000000000000000000000000000000000000000000000000000090000000000000000000000000000000000000000000000000000000000000009800000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000a800000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000b800000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000c800000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000000d800000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000e800000000000000000000000000000000000000000000000000000000000000f000000000000000000000000000000000000000000000000000000000000000f80000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000010800000000000000000000000000000000000000000000000000000000000001100000000000000000000000000000000000000000000000000000000000000118000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000001280000000000000000000000000000000000000000000000000000000000000130000000000000000000000000000000000000000000000000000000000000013800000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000148000000000000000000000000000000000000000000000000000000000000015000000000000000000000000000000000000000000000000000000000000001580000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000016800000000000000000000000000000000000000000000000000000000000001700000000000000000000000000000000000000000000000000000000000000178000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000001880000000000000000000000000000000000000000000000000000000000000190000000000000000000000000000000000000000000000000000000000000019800000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000001a800000000000000000000000000000000000000000000000000000000000001b000000000000000000000000000000000000000000000000000000000000001b800000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000001c800000000000000000000000000000000000000000000000000000000000001d000000000000000000000000000000000000000000000000000000000000001d800000000000000000000000000000000000000000000000000000000000001e000000000000000000000000000000000000000000000000000000000000001e800000000000000000000000000000000000000000000000000000000000001f000000000000000000000000000000000000000000000000000000000000001f80000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020800000000000000000000000000000000000000000000000000000000000002100000000000000000000000000000000000000000000000000000000000000218000000000000000000000000000000000000000000000000000000000000022000000000000000000000000000000000000000000000000000000000000002280000000000000000000000000000000000000000000000000000000000000230000000000000000000000000000000000000000000000000000000000000023800000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000248000000000000000000000000000000000000000000000000000000000000025000000000000000000000000000000000000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000260000000000000000000000000000000000000000000000000000000000000026800000000000000000000000000000000000000000000000000000000000002700000000000000000000000000000000000000000000000000000000000000278000000000000000000000000000000000000000000000000000000000000028000000000000000000000000000000000000000000000000000000000000002880000000000000000000000000000000000000000000000000000000000000290000000000000000000000000000000000000000000000000000000000000029800000000000000000000000000000000000000000000000000000000000002a000000000000000000000000000000000000000000000000000000000000002a800000000000000000000000000000000000000000000000000000000000002b000000000000000000000000000000000000000000000000000000000000002b800000000000000000000000000000000000000000000000000000000000002c000000000000000000000000000000000000000000000000000000000000002c800000000000000000000000000000000000000000000000000000000000002d000000000000000000000000000000000000000000000000000000000000002d800000000000000000000000000000000000000000000000000000000000002e000000000000000000000000000000000000000000000000000000000000002e800000000000000000000000000000000000000000000000000000000000002f000000000000000000000000000000000000000000000000000000000000002f80000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000030800000000000000000000000000000000000000000000000000000000000003100000000000000000000000000000000000000000000000000000000000000318000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000003280000000000000000000000000000000000000000000000000000000000000330000000000000000000000000000000000000000000000000000000000000033800000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000348000000000000000000000000000000000000000000000000000000000000035000000000000000000000000000000000000000000000000000000000000003580000000000000000000000000000000000000000000000000000000000000360000000000000000000000000000000000000000000000000000000000000036800000000000000000000000000000000000000000000000000000000000003700000000000000000000000000000000000000000000000000000000000000378000000000000000000000000000000000000000000000000000000000000038000000000000000000000000000000000000000000000000000000000000003880000000000000000000000000000000000000000000000000000000000000390000000000000000000000000000000000000000000000000000000000000039800000000000000000000000000000000000000000000000000000000000003a000000000000000000000000000000000000000000000000000000000000003a800000000000000000000000000000000000000000000000000000000000003b000000000000000000000000000000000000000000000000000000000000003b800000000000000000000000000000000000000000000000000000000000003c000000000000000000000000000000000000000000000000000000000000003c800000000000000000000000000000000000000000000000000000000000003d000000000000000000000000000000000000000000000000000000000000003d800000000000000000000000000000000000000000000000000000000000003e000000000000000000000000000000000000000000000000000000000000003e800000000000000000000000000000000000000000000000000000000000003f000000000000000000000000000000000000000000000000000000000000003f80000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040800000000000000000000000000000000000000000000000000000000000004100000000000000000000000000000000000000000000000000000000000000418000000000000000000000000000000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000004280000000000000000000000000000000000000000000000000000000000000430000000000000000000000000000000000000000000000000000000000000043800000000000000000000000000000000000000000000000000000000000004400000000000000000000000000000000000000000000000000000000000000448000000000000000000000000000000000000000000000000000000000000045000000000000000000000000000000000000000000000000000000000000004580000000000000000000000000000000000000000000000000000000000000460000000000000000000000000000000000000000000000000000000000000046800000000000000000000000000000000000000000000000000000000000004700000000000000000000000000000000000000000000000000000000000000478000000000000000000000000000000000000000000000000000000000000048000000000000000000000000000000000000000000000000000000000000004880000000000000000000000000000000000000000000000000000000000000490000000000000000000000000000000000000000000000000000000000000049800000000000000000000000000000000000000000000000000000000000004a000000000000000000000000000000000000000000000000000000000000004a800000000000000000000000000000000000000000000000000000000000004b000000000000000000000000000000000000000000000000000000000000004b800000000000000000000000000000000000000000000000000000000000004c000000000000000000000000000000000000000000000000000000000000004c800000000000000000000000000000000000000000000000000000000000004d000000000000000000000000000000000000000000000000000000000000004d800000000000000000000000000000000000000000000000000000000000004e000000000000000000000000000000000000000000000000000000000000004e800000000000000000000000000000000000000000000000000000000000004f000000000000000000000000000000000000000000000000000000000000004f80000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000050800000000000000000000000000000000000000000000000000000000000005100000000000000000000000000000000000000000000000000000000000000518000000000000000000000000000000000000000000000000000000000000052000000000000000000000000000000000000000000000000000000000000005280000000000000000000000000000000000000000000000000000000000000530000000000000000000000000000000000000000000000000000000000000053800000000000000000000000000000000000000000000000000000000000005400000000000000000000000000000000000000000000000000000000000000548000000000000000000000000000000000000000000000000000000000000055000000000000000000000000000000000000000000000000000000000000005580000000000000000000000000000000000000000000000000000000000000560000000000000000000000000000000000000000000000000000000000000056800000000000000000000000000000000000000000000000000000000000005700000000000000000000000000000000000000000000000000000000000000578000000000000000000000000000000000000000000000000000000000000058000000000000000000000000000000000000000000000000000000000000005880000000000000000000000000000000000000000000000000000000000000590000000000000000000000000000000000000000000000000000000000000059800000000000000000000000000000000000000000000000000000000000005a000000000000000000000000000000000000000000000000000000000000005a800000000000000000000000000000000000000000000000000000000000005b000000000000000000000000000000000000000000000000000000000000005b800000000000000000000000000000000000000000000000000000000000005c000000000000000000000000000000000000000000000000000000000000005c800000000000000000000000000000000000000000000000000000000000005d000000000000000000000000000000000000000000000000000000000000005d800000000000000000000000000000000000000000000000000000000000005e000000000000000000000000000000000000000000000000000000000000005e800000000000000000000000000000000000000000000000000000000000005f000000000000000000000000000000000000000000000000000000000000005f80000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000060800000000000000000000000000000000000000000000000000000000000006100000000000000000000000000000000000000000000000000000000000000618000000000000000000000000000000000000000000000000000000000000062000000000000000000000000000000000000000000000000000000000000006280000000000000000000000000000000000000000000000000000000000000630000000000000000000000000000000000000000000000000000000000000063800000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000648000000000000000000000000000000000000000000000000000000000000065000000000000000000000000000000000000000000000000000000000000006580000000000000000000000000000000000000000000000000000000000000660000000000000000000000000000000000000000000000000000000000000066800000000000000000000000000000000000000000000000000000000000006700000000000000000000000000000000000000000000000000000000000000678000000000000000000000000000000000000000000000000000000000000068000000000000000000000000000000000000000000000000000000000000006880000000000000000000000000000000000000000000000000000000000000690000000000000000000000000000000000000000000000000000000000000069800000000000000000000000000000000000000000000000000000000000006a000000000000000000000000000000000000000000000000000000000000006a800000000000000000000000000000000000000000000000000000000000006b000000000000000000000000000000000000000000000000000000000000006b800000000000000000000000000000000000000000000000000000000000006c000000000000000000000000000000000000000000000000000000000000006c800000000000000000000000000000000000000000000000000000000000006d000000000000000000000000000000000000000000000000000000000000006d800000000000000000000000000000000000000000000000000000000000006e000000000000000000000000000000000000000000000000000000000000006e800000000000000000000000000000000000000000000000000000000000006f000000000000000000000000000000000000000000000000000000000000006f80000000000000000000000000000000000000000000000000000000000000700000000000000000000000000000000000000000000000000000000000000070800000000000000000000000000000000000000000000000000000000000007100000000000000000000000000000000000000000000000000000000000000718000000000000000000000000000000000000000000000000000000000000072000000000000000000000000000000000000000000000000000000000000007280000000000000000000000000000000000000000000000000000000000000730000000000000000000000000000000000000000000000000000000000000073800000000000000000000000000000000000000000000000000000000000007400000000000000000000000000000000000000000000000000000000000000748000000000000000000000000000000000000000000000000000000000000075000000000000000000000000000000000000000000000000000000000000007580000000000000000000000000000000000000000000000000000000000000760000000000000000000000000000000000000000000000000000000000000076800000000000000000000000000000000000000000000000000000000000007700000000000000000000000000000000000000000000000000000000000000778000000000000000000000000000000000000000000000000000000000000078000000000000000000000000000000000000000000000000000000000000007880000000000000000000000000000000000000000000000000000000000000790000000000000000000000000000000000000000000000000000000000000079800000000000000000000000000000000000000000000000000000000000007a000000000000000000000000000000000000000000000000000000000000007a800000000000000000000000000000000000000000000000000000000000007b000000000000000000000000000000000000000000000000000000000000007b800000000000000000000000000000000000000000000000000000000000007c000000000000000000000000000000000000000000000000000000000000007c800000000000000000000000000000000000000000000000000000000000007d000000000000000000000000000000000000000000000000000000000000007d800000000000000000000000000000000000000000000000000000000000007e000000000000000000000000000000000000000000000000000000000000007e800000000000000000000000000000000000000000000000000000000000007f000000000000000000000000000000000000000000000000000000000000007f80000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000080800000000000000000000000000000000000000000000000000000000000008100000000000000000000000000000000000000000000000000000000000000818000000000000000000000000000000000000000000000000000000000000082000000000000000000000000000000000000000000000000000000000000008280000000000000000000000000000000000000000000000000000000000000830000000000000000000000000000000000000000000000000000000000000083800000000000000000000000000000000000000000000000000000000000008400000000000000000000000000000000000000000000000000000000000000848000000000000000000000000000000000000000000000000000000000000085000000000000000000000000000000000000000000000000000000000000008580000000000000000000000000000000000000000000000000000000000000860000000000000000000000000000000000000000000000000000000000000086800000000000000000000000000000000000000000000000000000000000008700000000000000000000000000000000000000000000000000000000000000878000000000000000000000000000000000000000000000000000000000000088000000000000000000000000000000000000000000000000000000000000008880000000000000000000000000000000000000000000000000000000000000890000000000000000000000000000000000000000000000000000000000000089800000000000000000000000000000000000000000000000000000000000008a000000000000000000000000000000000000000000000000000000000000008a800000000000000000000000000000000000000000000000000000000000008b000000000000000000000000000000000000000000000000000000000000008b800000000000000000000000000000000000000000000000000000000000008c000000000000000000000000000000000000000000000000000000000000008c800000000000000000000000000000000000000000000000000000000000008d000000000000000000000000000000000000000000000000000000000000008d800000000000000000000000000000000000000000000000000000000000008e000000000000000000000000000000000000000000000000000000000000008e800000000000000000000000000000000000000000000000000000000000008f000000000000000000000000000000000000000000000000000000000000008f80000000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000000090800000000000000000000000000000000000000000000000000000000000009100000000000000000000000000000000000000000000000000000000000000918000000000000000000000000000000000000000000000000000000000000092000000000000000000000000000000000000000000000000000000000000009280000000000000000000000000000000000000000000000000000000000000930000000000000000000000000000000000000000000000000000000000000093800000000000000000000000000000000000000000000000000000000000009400000000000000000000000000000000000000000000000000000000000000948000000000000000000000000000000000000000000000000000000000000095000000000000000000000000000000000000000000000000000000000000009580000000000000000000000000000000000000000000000000000000000000960000000000000000000000000000000000000000000000000000000000000096800000000000000000000000000000000000000000000000000000000000009700000000000000000000000000000000000000000000000000000000000000978000000000000000000000000000000000000000000000000000000000000098000000000000000000000000000000000000000000000000000000000000009880000000000000000000000000000000000000000000000000000000000000990000000000000000000000000000000000000000000000000000000000000099800000000000000000000000000000000000000000000000000000000000009a000000000000000000000000000000000000000000000000000000000000009a800000000000000000000000000000000000000000000000000000000000009b000000000000000000000000000000000000000000000000000000000000009b800000000000000000000000000000000000000000000000000000000000009c000000000000000000000000000000000000000000000000000000000000009c800000000000000000000000000000000000000000000000000000000000009d000000000000000000000000000000000000000000000000000000000000009d800000000000000000000000000000000000000000000000000000000000009e000000000000000000000000000000000000000000000000000000000000009e800000000000000000000000000000000000000000000000000000000000009f000000000000000000000000000000000000000000000000000000000000009f80000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000a080000000000000000000000000000000000000000000000000000000000000a100000000000000000000000000000000000000000000000000000000000000a180000000000000000000000000000000000000000000000000000000000000a200000000000000000000000000000000000000000000000000000000000000a280000000000000000000000000000000000000000000000000000000000000a300000000000000000000000000000000000000000000000000000000000000a380000000000000000000000000000000000000000000000000000000000000a400000000000000000000000000000000000000000000000000000000000000a480000000000000000000000000000000000000000000000000000000000000a500000000000000000000000000000000000000000000000000000000000000a580000000000000000000000000000000000000000000000000000000000000a600000000000000000000000000000000000000000000000000000000000000a680000000000000000000000000000000000000000000000000000000000000a700000000000000000000000000000000000000000000000000000000000000a780000000000000000000000000000000000000000000000000000000000000a800000000000000000000000000000000000000000000000000000000000000a880000000000000000000000000000000000000000000000000000000000000a900000000000000000000000000000000000000000000000000000000000000a980000000000000000000000000000000000000000000000000000000000000aa00000000000000000000000000000000000000000000000000000000000000aa80000000000000000000000000000000000000000000000000000000000000ab00000000000000000000000000000000000000000000000000000000000000ab80000000000000000000000000000000000000000000000000000000000000ac00000000000000000000000000000000000000000000000000000000000000ac80000000000000000000000000000000000000000000000000000000000000ad00000000000000000000000000000000000000000000000000000000000000ad80000000000000000000000000000000000000000000000000000000000000ae00000000000000000000000000000000000000000000000000000000000000ae80000000000000000000000000000000000000000000000000000000000000af00000000000000000000000000000000000000000000000000000000000000af80000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000b080000000000000000000000000000000000000000000000000000000000000b100000000000000000000000000000000000000000000000000000000000000b180000000000000000000000000000000000000000000000000000000000000b200000000000000000000000000000000000000000000000000000000000000b280000000000000000000000000000000000000000000000000000000000000b300000000000000000000000000000000000000000000000000000000000000b380000000000000000000000000000000000000000000000000000000000000b400000000000000000000000000000000000000000000000000000000000000b480000000000000000000000000000000000000000000000000000000000000b500000000000000000000000000000000000000000000000000000000000000b580000000000000000000000000000000000000000000000000000000000000b600000000000000000000000000000000000000000000000000000000000000b680000000000000000000000000000000000000000000000000000000000000b700000000000000000000000000000000000000000000000000000000000000b780000000000000000000000000000000000000000000000000000000000000b800000000000000000000000000000000000000000000000000000000000000b880000000000000000000000000000000000000000000000000000000000000b900000000000000000000000000000000000000000000000000000000000000b980000000000000000000000000000000000000000000000000000000000000ba00000000000000000000000000000000000000000000000000000000000000ba80000000000000000000000000000000000000000000000000000000000000bb00000000000000000000000000000000000000000000000000000000000000bb80000000000000000000000000000000000000000000000000000000000000bc00000000000000000000000000000000000000000000000000000000000000bc80000000000000000000000000000000000000000000000000000000000000bd00000000000000000000000000000000000000000000000000000000000000bd80000000000000000000000000000000000000000000000000000000000000be00000000000000000000000000000000000000000000000000000000000000be80000000000000000000000000000000000000000000000000000000000000bf00000000000000000000000000000000000000000000000000000000000000bf80000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000c080000000000000000000000000000000000000000000000000000000000000c100000000000000000000000000000000000000000000000000000000000000c180000000000000000000000000000000000000000000000000000000000000c200000000000000000000000000000000000000000000000000000000000000c280000000000000000000000000000000000000000000000000000000000000c300000000000000000000000000000000000000000000000000000000000000c380000000000000000000000000000000000000000000000000000000000000c400000000000000000000000000000000000000000000000000000000000000c480000000000000000000000000000000000000000000000000000000000000c500000000000000000000000000000000000000000000000000000000000000c580000000000000000000000000000000000000000000000000000000000000c600000000000000000000000000000000000000000000000000000000000000c680000000000000000000000000000000000000000000000000000000000000c700000000000000000000000000000000000000000000000000000000000000c780000000000000000000000000000000000000000000000000000000000000c800000000000000000000000000000000000000000000000000000000000000c880000000000000000000000000000000000000000000000000000000000000c900000000000000000000000000000000000000000000000000000000000000c980000000000000000000000000000000000000000000000000000000000000ca00000000000000000000000000000000000000000000000000000000000000ca80000000000000000000000000000000000000000000000000000000000000cb00000000000000000000000000000000000000000000000000000000000000cb80000000000000000000000000000000000000000000000000000000000000cc00000000000000000000000000000000000000000000000000000000000000cc80000000000000000000000000000000000000000000000000000000000000cd00000000000000000000000000000000000000000000000000000000000000cd80000000000000000000000000000000000000000000000000000000000000ce00000000000000000000000000000000000000000000000000000000000000ce80000000000000000000000000000000000000000000000000000000000000cf00000000000000000000000000000000000000000000000000000000000000cf80000000000000000000000000000000000000000000000000000000000000d000000000000000000000000000000000000000000000000000000000000000d080000000000000000000000000000000000000000000000000000000000000d100000000000000000000000000000000000000000000000000000000000000d180000000000000000000000000000000000000000000000000000000000000d200000000000000000000000000000000000000000000000000000000000000d280000000000000000000000000000000000000000000000000000000000000d300000000000000000000000000000000000000000000000000000000000000d380000000000000000000000000000000000000000000000000000000000000d400000000000000000000000000000000000000000000000000000000000000d480000000000000000000000000000000000000000000000000000000000000d500000000000000000000000000000000000000000000000000000000000000d580000000000000000000000000000000000000000000000000000000000000d600000000000000000000000000000000000000000000000000000000000000d680000000000000000000000000000000000000000000000000000000000000d700000000000000000000000000000000000000000000000000000000000000d780000000000000000000000000000000000000000000000000000000000000d800000000000000000000000000000000000000000000000000000000000000d880000000000000000000000000000000000000000000000000000000000000d900000000000000000000000000000000000000000000000000000000000000d980000000000000000000000000000000000000000000000000000000000000da00000000000000000000000000000000000000000000000000000000000000da80000000000000000000000000000000000000000000000000000000000000db00000000000000000000000000000000000000000000000000000000000000db80000000000000000000000000000000000000000000000000000000000000dc00000000000000000000000000000000000000000000000000000000000000dc80000000000000000000000000000000000000000000000000000000000000dd00000000000000000000000000000000000000000000000000000000000000dd80000000000000000000000000000000000000000000000000000000000000de00000000000000000000000000000000000000000000000000000000000000de80000000000000000000000000000000000000000000000000000000000000df00000000000000000000000000000000000000000000000000000000000000df80000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000e080000000000000000000000000000000000000000000000000000000000000e100000000000000000000000000000000000000000000000000000000000000e180000000000000000000000000000000000000000000000000000000000000e200000000000000000000000000000000000000000000000000000000000000e280000000000000000000000000000000000000000000000000000000000000e300000000000000000000000000000000000000000000000000000000000000e380000000000000000000000000000000000000000000000000000000000000e400000000000000000000000000000000000000000000000000000000000000e480000000000000000000000000000000000000000000000000000000000000e500000000000000000000000000000000000000000000000000000000000000e580000000000000000000000000000000000000000000000000000000000000e600000000000000000000000000000000000000000000000000000000000000e680000000000000000000000000000000000000000000000000000000000000e700000000000000000000000000000000000000000000000000000000000000e780000000000000000000000000000000000000000000000000000000000000e800000000000000000000000000000000000000000000000000000000000000e880000000000000000000000000000000000000000000000000000000000000e900000000000000000000000000000000000000000000000000000000000000e980000000000000000000000000000000000000000000000000000000000000ea00000000000000000000000000000000000000000000000000000000000000ea80000000000000000000000000000000000000000000000000000000000000eb00000000000000000000000000000000000000000000000000000000000000eb80000000000000000000000000000000000000000000000000000000000000ec00000000000000000000000000000000000000000000000000000000000000ec80000000000000000000000000000000000000000000000000000000000000ed00000000000000000000000000000000000000000000000000000000000000ed80000000000000000000000000000000000000000000000000000000000000ee00000000000000000000000000000000000000000000000000000000000000ee80000000000000000000000000000000000000000000000000000000000000ef00000000000000000000000000000000000000000000000000000000000000ef80000000000000000000000000000000000000000000000000000000000000f000000000000000000000000000000000000000000000000000000000000000f080000000000000000000000000000000000000000000000000000000000000f100000000000000000000000000000000000000000000000000000000000000f180000000000000000000000000000000000000000000000000000000000000f200000000000000000000000000000000000000000000000000000000000000f280000000000000000000000000000000000000000000000000000000000000f300000000000000000000000000000000000000000000000000000000000000f380000000000000000000000000000000000000000000000000000000000000f400000000000000000000000000000000000000000000000000000000000000f480000000000000000000000000000000000000000000000000000000000000f500000000000000000000000000000000000000000000000000000000000000f580000000000000000000000000000000000000000000000000000000000000f600000000000000000000000000000000000000000000000000000000000000f680000000000000000000000000000000000000000000000000000000000000f700000000000000000000000000000000000000000000000000000000000000f780000000000000000000000000000000000000000000000000000000000000f800000000000000000000000000000000000000000000000000000000000000f880000000000000000000000000000000000000000000000000000000000000f900000000000000000000000000000000000000000000000000000000000000f980000000000000000000000000000000000000000000000000000000000000fa00000000000000000000000000000000000000000000000000000000000000fa80000000000000000000000000000000000000000000000000000000000000fb00000000000000000000000000000000000000000000000000000000000000fb80000000000000000000000000000000000000000000000000000000000000fc00000000000000000000000000000000000000000000000000000000000000fc80000000000000000000000000000000000000000000000000000000000000fd00000000000000000000000000000000000000000000000000000000000000fd80000000000000000000000000000000000000000000000000000000000000fe00000000000000000000000000000000000000000000000000000000000000fe80000000000000000000000000000000000000000000000000000000000000ff00000000000000000000000000000000000000000000000000000000000000ff800000000000000000000000000000000000000000000000000000000 +PayloadSize (Size of the payload in bytes. This is the total number of bytes sent in the payload. Image data + chunk data if present. No packet headers.): 12288000 +GevSCPSPacketSize (Packet size in bytes for the selected stream channel. Excludes data leader and data trailer. (The last packet may be smaller because the packet size is not necessarily a multiple of the block size for the stream channel.)): 1500 +GevSCPD (Delay between the transmission of each packet for the selected stream channel. The delay is measured in ticks.): 0 +GevSCFTD (Frame transmission delay for the selected stream channel (in ticks). This value sets a delay before transmitting the acquired image.): 0 +GevSCBWR (Percentage of the Ethernet bandwidth assigned to the camera to be held in reserve for packet resends and for the transmission of control data between the camera and the host PC. The setting is expressed as a percentage of the bandwidth assigned parameter. For example, if the Bandwidth Assigned parameter indicates that 30 MBytes/s have been assigned to the camera and the Bandwidth Reserve parameter is set to 5%, the bandwidth reserve will be 1.5 MBytes/s.): 10 +GevSCBWRA (Multiplier for the Bandwidth Reserve parameter. The multiplier is used to establish an extra pool of reserved bandwidth that can be used if an unusually large burst of packet resends is needed.): 10 +GevSCBWA (Base bandwidth in bytes per second that will be used by the camera to transmit image and chunk feature data and to handle resends and control data transmissions. This parameter represents a combination of the packet size and the inter-packet delay.): 125000000 +GevSCDMT (Maximum amount of data (in bytes per second) that the camera could generate given its current settings and ideal conditions, i.e., unlimited bandwidth and no packet resends.): 124369426 +GevSCDCT (Actual bandwidth (in bytes per second) that the camera will use to transmit image data and chunk data given the current AOI settings, chunk feature settings, and the pixel format setting.): 112499549 +GevSCFJM (Maximum time (in ticks) that the next frame transmission could be delayed due to a burst of resends. If the Bandwidth Reserve Accumulation parameter is set to a high value, the camera can experience periods where there is a large burst of data resends. This burst of resends will delay the start of transmission of the next acquired image. ): 14382234 +GevVersionMajor (Major version number of the GigE Vision specification supported by this device.): 2 +GevVersionMinor (Minor version number of the GigE Vision specification supported by this device.): 0 +GevDeviceModeIsBigEndian (Indicates whether the bootstrap register is in big-endian format.): True +GevDeviceModeCharacterSet (Character set used by all strings of the bootstrap registers (1 = UTF8).): 1 +GevInterfaceSelector (Sets the physical network interface to be configured. Once a network interface has been selected, all changes to the network interface settings will be applied to the selected interface.): NetworkInterface0 +GevMACAddress (MAC address for the selected network interface.): 207554443575 +GevGVSPExtendedIDMode (Sets the extended ID mode for GVSP (64 bit block_id64, 32 bit packet_id32). This bit cannot be reset if the stream channels do not support the standard ID mode.): Off +GevSupportedIPConfigurationLLA (Indicates whether the selected network interface supports auto IP addressing (also known as LLA).): True +GevSupportedIPConfigurationDHCP (Indicates whether the selected network interface supports DHCP IP addressing.): True +GevSupportedIPConfigurationPersistentIP (Indicates whether the selected network interface supports fixed IP addressing (also known as persistent IP addressing).): True +GevCurrentIPConfiguration (IP configuration of the selected network interface, e.g. fixed IP, DHCP, or auto IP.): 5 +GevCurrentIPAddress (Current IP address for the selected network interface.): 3232236037 +GevCurrentSubnetMask (Current subnet mask for the selected network interface.): 4294967040 +GevCurrentDefaultGateway (Current default gateway for the selected network interface.): 0 +GevPersistentIPAddress (Fixed IP address for the selected network interface (if fixed IP addressing is supported by the device and enabled).): 3232236037 +GevPersistentSubnetMask (Fixed subnet mask for the selected network interface (if fixed IP addressing is supported by the device and enabled).): 4294967040 +GevPersistentDefaultGateway (Fixed default gateway for the selected network interface (if fixed IP addressing is supported by the device and enabled).): 0 +GevLinkSpeed (Connection speed in Mbps for the selected network interface.): 1000 +GevLinkMaster (Indicates whether the selected network interface is the clock master.): True +GevLinkFullDuplex (Indicates whether the selected network interface operates in full-duplex mode.): True +GevLinkCrossover (Indicates the state of medium-dependent interface crossover (MDIX) for the selected network interface.): False +GevFirstURL (First URL reference to the GenICam XML file. ): Local:Basler_Ace_GigE_81a5a494_Version_3_8.zip;c0000000;10ea0 +GevSecondURL (Second URL reference to the GenICam XML file. ): http://www.baslerweb.com/camera/Basler_Ace_GigE_81a5a494_Version_3_8.xml +GevNumberOfInterfaces (Number of network interfaces on the device.): 1 +GevMessageChannelCount (Number of message channels supported by the device.): 1 +GevStreamChannelCount (Number of stream channels supported by the device.): 1 +GevSupportedOptionalLegacy16BitBlockID (Indicates whether this GVSP transmitter or receiver can support 16-bit block_id.): True +GevSupportedIEEE1588 (Indicates whether the IEEE 1588 V2 Precision Time Protocol (PTP) is supported.): True +GevSupportedOptionalCommandsEVENTDATA (Indicates whether EVENTDATA_CMD and EVENTDATA_ACK are supported.): False +GevSupportedOptionalCommandsEVENT (Indicates whether EVENT_CMD and EVENT_ACK are supported.): True +GevSupportedOptionalCommandsPACKETRESEND (Indicates whether PACKETRESEND_CMD is supported.): True +GevSupportedOptionalCommandsWRITEMEM (Indicates whether WRITEMEM_CMD and WRITEMEM_ACK are supported.): True +GevSupportedOptionalCommandsConcatenation (Indicates whether multiple operations in a single message are supported.): True +GevHeartbeatTimeout (Heartbeat timeout in milliseconds.): 3000 +GevTimestampTickFrequency (Number of timestamp clock ticks in 1 second.): 125000000 +GevTimestampControlLatch (Latches the current timestamp value of the device.): +GevTimestampControlReset (Resets the timestamp value for the device.): +GevTimestampControlLatchReset (Resets the timestamp control latch.): +GevTimestampValue (Latched value of the timestamp. (The timestamp must first be latched using the Timestamp Control Latch command.)): 0 +GevCCP (Sets the control channel privilege feature.): Control +GevStreamChannelSelector (Sets the stream channels to be configured. Once a stream channel has been selected, all changes to the stream channel settings will be applied to the selected stream channel.): StreamChannel0 +GevSCPInterfaceIndex (Index of the network interface to use.): 0 +GevSCDA (Stream channel destination IPv4 address for the selected stream channel. The destination can be a unicast or a multicast.): 0 +GevSCPHostPort (Port to which the device must send data streams.): 0 +GevSCPSFireTestPacket (Fires a GigE Vision streaming test packet. When this bit is set and the stream channel is a transmitter, the transmitter will fire one test packet of size specified by GevSCPSPacketSize. The "do not fragment" bit of the IP header must be set for this test packet (see GevSCPSDoNotFragment).): +GevSCPSDoNotFragment (Disables IP fragmentation of packets on the stream channel. This bit is copied into the "do not fragment" bit of the IP header of each stream packet.): True +GevSCPSBigEndian (Returns the endianess of multi-byte pixel data for this stream. True = big endian.): False +GevIEEE1588 (Enables the IEEE 1588 V2 Precision Time Protocol for the timestamp register. Only available when the IEEE1588_support bit of the GVCP Capability register is set. When PTP is enabled, the Timestamp Control register cannot be used to reset the timestamp. Factory default is device specific. When PTP is enabled or disabled, the value of Timestamp Tick Frequency and Timestamp Value registers might change to reflect the new time domain.): False +GevIEEE1588Status (Provides the state of the IEEE 1588 clock. Values of this field must match the IEEE 1588 PTP port state enumeration (INITIALIZING, FAULTY, DISABLED, LISTENING, PRE_MASTER, MASTER, PASSIVE, UNCALIBRATED, SLAVE). Please refer to IEEE 1588 for additional information.): +GevIEEE1588DataSetLatch (Latches the current IEEE 1588 related values of the device.): +GevIEEE1588StatusLatched (Returns the latched state of the IEEE 1588 clock. (The state must first be latched using the IEEE 1588 Latch command.) The state is indicated by values 1 to 9, corresponding to the states INITIALIZING, FAULTY, DISABLED, LISTENING, PRE_MASTER, MASTER, PASSIVE, UNCALIBRATED, and SLAVE. Refer to the IEEE 1588 specification for additional information.): +GevIEEE1588OffsetFromMaster (Latched offset from the IEEE 1588 master clock in nanoseconds. (The offset must first be latched using the IEEE 1588 Latch command.)): +GevIEEE1588ClockIdLow (Low part of the latched clock ID of the IEEE 1588 device.): +GevIEEE1588ClockIdHigh (High part of the latched clock ID of the IEEE 1588 device.): +GevIEEE1588ClockId (Latched clock ID of the IEEE 1588 device. (The clock ID must first be latched using the IEEE 1588 Latch command.) The clock ID is an array of eight octets which is displayed as hexadecimal number. Leading zeros are omitted.): +GevIEEE1588ParentClockIdLow (Low part of the latched parent clock ID of the IEEE 1588 device.): +GevIEEE1588ParentClockIdHigh (High part of the latched parent clock ID of the IEEE 1588 device.): +GevIEEE1588ParentClockId (Latched parent clock ID of the IEEE 1588 device. (The parent clock ID must first be latched using the IEEE 1588 Latch command.) The parent clock ID is the clock ID of the current master clock. A clock ID is an array of eight octets which is displayed as hexadecimal number. Leading zeros are omitted.): +NumberOfActionSignals (Number of separate action signals supported by the device. Determines how many action signals the device can handle in parallel, i.e. how many different action commands can be set up for the device.): 1 +ActionCommandCount (Number of separate action signals supported by the device. Determines how many action signals the device can handle in parallel, i.e. how many different action commands can be set up for the device.): 1 +ActionDeviceKey (Device key used to authorize the execution of an action command. If the action device key in the camera and the action device key in the protocol message are identical, the camera will execute the corresponding action.): 0 +ActionSelector (Sets the action command to be configured. Because you cannot assign more than one action command to a Basler camera at a time, ActionSelector should always be set to 1.): 1 +ActionGroupKey (Group key used to define a group of devices on which action commands can be executed.): 0 +ActionGroupMask (Group mask used to filter out a sub-group of cameras belonging to a group of cameras. The cameras belonging to a sub-group execute an action command at the same time. The filtering is done using a logical bitwise And operation against the group mask number of the action command and the group mask number of a camera. If both binary numbers have at least one common bit set to 1 (i.e. the result of the And operation is non-zero), the corresponding camera belongs to the sub-group.): 0 +DeviceRegistersStreamingStart (Prepare the device for registers streaming.): +DeviceRegistersStreamingEnd (Announce the end of registers streaming.): +UserSetSelector (Sets the user set or the factory set to load, save or configure.): Default +UserSetLoad (Loads the selected set into the camera's volatile memory and makes it the active configuration set. Once the selected set is loaded, the parameters in the selected set will control the camera.): +UserSetSave (Saves the current active set into the selected user set.): +UserSetDefaultSelector (Sets the user set or the factory set to be used as the startup set. The default startup set will be loaded as the active set whenever the camera is powered on or reset.): Default +DefaultSetSelector (Sets the factory set that will be used as the default set.): Standard +AutoTargetValue (Target average brightness for the gain auto function and the exposure auto function.): 128 +GrayValueAdjustmentDampingAbs (Gray value adjustment damping factor. The factor controls the rate by which pixel gray values are changed when the exposure auto function or the gain auto function or both are enabled. This can be useful, for example, when objects move into the camera's view area and the light conditions are gradually changing due to the moving objects.): 0.68359375 +GrayValueAdjustmentDampingRaw (Gray value adjustment damping factor. The factor controls the rate by which pixel gray values are changed when the exposure auto function or the gain auto function or both are enabled. This can be useful, for example, when objects move into the camera's view area and the light conditions are gradually changing due to the moving objects.): 700 +BalanceWhiteAdjustmentDampingAbs (Balance White adjustment damping factor. The factor controls the rate by which colors are adjusted when the balance white auto function is enabled. This can be useful, for example, when objects move into the camera's view area and the light conditions are gradually changing due to the moving objects.): +BalanceWhiteAdjustmentDampingRaw (Balance White adjustment damping factor. The factor controls the rate by which colors are adjusted when the balance white auto function is enabled. This can be useful, for example, when objects move into the camera's view area and the light conditions are gradually changing due to the moving objects.): +AutoGainRawLowerLimit (Lower limit for the Gain parameter when the gain auto function is active.): 0 +AutoGainRawUpperLimit (Upper limit for the Gain parameter when the gain auto function is active.): 240 +AutoExposureTimeAbsLowerLimit (Lower limit for the ExposureTime parameter when the exposure auto function is active.): 80.0 +AutoExposureTimeAbsUpperLimit (Upper limit for the ExposureTime parameter when the exposure auto function is active.): 100000.0 +AutoFunctionProfile (Sets how gain and exposure time will be balanced when the device is making automatic adjustments.): GainMinimum +AutoFunctionAOISelector (Sets which auto function AOI can be adjusted.): AOI1 +AutoFunctionAOIWidth (Width of the auto function AOI (in pixels).): 4096 +AutoFunctionAOIHeight (Height of the auto function AOI (in pixels).): 3000 +AutoFunctionAOIOffsetX (Horizontal offset from the left side of the sensor to the auto function AOI (in pixels).): 8 +AutoFunctionAOIOffsetY (Vertical offset from the top of the sensor to the auto function AOI (in pixels).): 4 +AutoFunctionAOIUsageIntensity (Assigns the gain auto and the exposure auto functions to the currently selected auto function AOI. For this parameter, gain auto and exposure auto are considered as a single intensity" auto function."): True +AutoFunctionAOIUsageWhiteBalance (Assigns the balance white auto function to the currently selected auto function AOI.): False +UserDefinedValueSelector (Sets the user-defined value to set or read.): Value1 +UserDefinedValue (A user defined value. The value can serve as storage location for the camera user. It has no impact on the operation of the camera.): 0 +DeviceVendorName (Name of the device's vendor.): Basler +DeviceModelName (Model name of the device.): acA4112-8gm +DeviceManufacturerInfo (Additional information from the vendor about the camera.): none +DeviceVersion (Version of the device.): 107411-02 +DeviceFirmwareVersion (Version of the device's firmware.): 107411-02;U;acA4112_8g;V1.0-0;0 +DeviceID (ID of the device.): 40006341 +DeviceUserID (User-settable ID of the device.): +DeviceScanType (Returns the scan type of the device's sensor (area or line scan).): Areascan +DeviceReset (Immediately resets and reboots the device.): +TemperatureSelector (Sets the location within the device where the temperature will be measured.): Coreboard +TemperatureAbs (Temperature of the selected location within the device (in degrees centigrade). The temperature is measured at the location set by TemperatureSelector.): 57.0 +TemperatureState (Returns the temperature state.): Ok +CriticalTemperature (Indicates whether the critical temperature has been reached.): False +OverTemperature (An over temperature state has been detected.): False +LastError (Returns the last occurred error.): NoError +ClearLastError (Clears the last error. If a previous error exists, the previous error can be retrieved.): +ParameterSelector (Sets the parameter whose factory limits should be removed. Once a parameter has been set, the factory limits can be removed using RemoveLimits.): Gain +RemoveLimits (Removes the factory-set limits of the selected parameter. Having removed the factory-set limits, you may set the parameter within extended limits. These are only defined by technical restrictions. Note: Inferior image quality may result.): False +ExpertFeatureAccessSelector (Sets the expert feature to be configured. Once a feature has been set, all changes made using the feature enable feature will be applied to this feature.): ExpertFeature1 +ExpertFeatureAccessKey (Key to access the selected expert feature.): 0 +ExpertFeatureEnable (Enables the currently selected expert feature.): +ChunkModeActive (Enables the chunk mode.): False +ChunkSelector (Sets the chunk to be enabled. Once a chunk has been set, the chunk can be enabled using the ChunkEnable parameter.): +ChunkEnable (Enables the inclusion of the currently selected chunk in the payload data.): +ChunkStride (Number of bytes of data between the beginning of one line in the acquired image and the beginning of the next line in the acquired image.): +ChunkSequenceSetIndex (Sequence set index number related to the acquired image.): +ChunkOffsetX (X offset of the area of interest set for the acquired image.): +ChunkOffsetY (Y offset of the area of interest set for the acquired image.): +ChunkWidth (Width of the area of interest set for the acquired image.): +ChunkHeight (Height of the area of interest set for the acquired image.): +ChunkDynamicRangeMin (Minimum possible pixel value in the acquired image.): +ChunkDynamicRangeMax (Maximum possible pixel value in the acquired image.): +ChunkPixelFormat (Returns the pixel format of the acquired image.): +ChunkTimestamp (Value of the timestamp when the image was acquired.): +ChunkFramecounter (Value of the frame counter when the image was acquired.): +ChunkLineStatusAll (Status of all of the camera's input and output lines when the image was acquired.): +ChunkVirtLineStatusAll (Status of all of the camera's virtual input and output lines when the image was acquired.): +ChunkTriggerinputcounter (Value of the trigger input counter when the image was acquired.): +ChunkLineTriggerIgnoredCounter (Value of the line trigger ignored counter when the image was acquired.): +ChunkFrameTriggerIgnoredCounter (Value of the frame trigger ignored counter when the image was acquired.): +ChunkFrameTriggerCounter (Value of the frame trigger counter when the image was acquired.): +ChunkFramesPerTriggerCounter (Value of the frames per trigger counter when the image was acquired.): +ChunkLineTriggerEndToEndCounter (Value of the line trigger end to end counter when the image was acquired.): +ChunkInputStatusAtLineTriggerBitsPerLine (Number of bits per line used for the input line status at line trigger feature.): +ChunkInputStatusAtLineTriggerIndex (Index number used for the InputStatusAtLineTrigger feature. The index number can be used to get the state of the camera's input lines when a specific line was acquired. For example, if you want to know the state of the camera's input lines when line 30 was acquired, set the index to 30, then retrieve the value of ChunkInputStatusAtLineTriggerValue.): +ChunkInputStatusAtLineTriggerValue (Value indicating the status of the camera's input lines when a specific line was acquired. The information is stored in a 4 bit value (bit 0 = input line 1 state, bit 1 = input line 2 state etc.). For more information, see the ChunkInputStatusAtLineTriggerIndex documentation.): +ChunkShaftEncoderCounter (Value of the shaft encoder counter when the image was acquired.): +ChunkExposureTime (Exposure time used to acquire the image.): +ChunkPayloadCRC16 (CRC checksum of the acquired image. The checksum is calculated using all of the image data and all of the appended chunks except for the checksum itself.): +ChunkGainAll (Gain all setting of the acquired image.): +ChunkLineTriggerCounter (Value of the line trigger counter when the image was acquired.): +EventSelector (Sets the event notification to be enabled. Once an event notification has been set, the notification can be enabled using the EventNotification parameter.): ExposureEnd +EventNotification (Enables event notifications for the currently selected event. The event can selected using the EventSelector parameter.): Off +ExposureEndEventStreamChannelIndex (Stream channel index of the exposure end event.): +ExposureEndEventFrameID (Frame ID for an exposure end event.): +ExposureEndEventTimestamp (Time stamp of the exposure end event.): +LineStartOvertriggerEventStreamChannelIndex (Stream channel index of the line start overtrigger event.): +LineStartOvertriggerEventTimestamp (Time stamp of the line start overtrigger event.): +FrameStartOvertriggerEventStreamChannelIndex (Stream channel index of the frame start overtrigger event.): +FrameStartOvertriggerEventTimestamp (Time stamp of the frame start overtrigger event.): +FrameStartEventStreamChannelIndex (Stream channel index of the frame start event.): +FrameStartEventTimestamp (Time stamp of the frame start event.): +AcquisitionStartEventStreamChannelIndex (Stream channel index of the acquisition start event.): +AcquisitionStartEventTimestamp (Time stamp of the acquisition start event.): +AcquisitionStartOvertriggerEventStreamChannelIndex (Stream channel index of the acquisition start overtrigger event.): +AcquisitionStartOvertriggerEventTimestamp (Time stamp of the acquisition start overtrigger event.): +FrameTimeoutEventStreamChannelIndex (Stream channel index of the frame timeout event.): +FrameTimeoutEventTimestamp (Time stamp of the frame timeout event.): +EventOverrunEventStreamChannelIndex (Stream channel index of the event overrun event.): +EventOverrunEventFrameID (Frame ID for an event overrun event.): +EventOverrunEventTimestamp (Time stamp of the event overrun event.): +CriticalTemperatureEventStreamChannelIndex (Stream channel index of the critical temperature event.): +CriticalTemperatureEventTimestamp (Time stamp of the critical temperature event.): +OverTemperatureEventStreamChannelIndex (Stream channel index of the over temperature event.): +OverTemperatureEventTimestamp (Time stamp of the over temperature event.): +ActionLateEventStreamChannelIndex (Stream channel index of the action late event. A action late event is raised when a scheduled action command with a timestamp in the past is received.): +ActionLateEventTimestamp (Time stamp of the action late event. A action late event is raised when a scheduled action command with a timestamp in the past is received.): +LateActionEventStreamChannelIndex (): +LateActionEventTimestamp (): +Line1RisingEdgeEventStreamChannelIndex (Stream channel index of the I/O line 1 rising edge event.): +Line1RisingEdgeEventTimestamp (Time stamp of the line 1 rising edge event.): +Line2RisingEdgeEventStreamChannelIndex (Stream channel index of the I/O line 2 rising edge event.): +Line2RisingEdgeEventTimestamp (Time stamp of the line 2 rising edge event.): +Line3RisingEdgeEventStreamChannelIndex (Stream channel index of the I/O line 3 rising edge event.): +Line3RisingEdgeEventTimestamp (Time stamp of the line 3 rising edge event.): +Line4RisingEdgeEventStreamChannelIndex (Stream channel index of the I/O line 4 rising edge event.): +Line4RisingEdgeEventTimestamp (Time stamp of the line 4 rising edge event.): +VirtualLine1RisingEdgeEventStreamChannelIndex (Stream channel index of the virtual line 1 rising edge event.): +VirtualLine1RisingEdgeEventTimestamp (Time stamp of the virtual line 1 rising edge event.): +VirtualLine2RisingEdgeEventStreamChannelIndex (Stream channel index of the virtual line 2 rising edge event.): +VirtualLine2RisingEdgeEventTimestamp (Time stamp of the virtual line 2 rising edge event.): +VirtualLine3RisingEdgeEventStreamChannelIndex (Stream channel index of the virtual line 3 rising edge event.): +VirtualLine3RisingEdgeEventTimestamp (Time stamp of the virtual line 3 rising edge event.): +VirtualLine4RisingEdgeEventStreamChannelIndex (Stream channel index of the virtual line 4 rising edge event.): +VirtualLine4RisingEdgeEventTimestamp (Time stamp of the virtual line 4 rising edge event.): +FrameStartWaitEventStreamChannelIndex (Stream channel index of the frame start wait event.): +FrameStartWaitEventTimestamp (Time stamp of the frame start wait event.): +AcquisitionStartWaitEventStreamChannelIndex (Stream channel index of the acquisition start wait event.): +AcquisitionStartWaitEventTimestamp (Time stamp of the acquisition start wait event.): +FileSelector (Sets the target file in the device.): UserSet1 +FileOperationSelector (Sets the target operation for the currently selected file. After an operation has been selected, the operation can be executed using the FileOperationExecute command.): Open +FileOpenMode (Sets the access mode in which a file is opened in the device.): Read +FileAccessBuffer (Access buffer for file operations.): 0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +FileAccessOffset (File access offset. Controls the mapping between the device file storage and the FileAccessBuffer.): 0 +FileAccessLength (File access length. Controls the mapping between the device file storage and the FileAccessBuffer.): 0 +FileOperationStatus (Returns the file operation execution status.): Success +FileOperationResult (File operation result. For read or write operations, the number of successfully read/written bytes is returned.): 0 +FileSize (Size of the currently selected file in bytes.): 16 +FileOperationExecute (Executes the operation selected by FileOperationSelector on the selected file.): From 0937df34dc33804752215d6fe451853e69cea9d9 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 26 Mar 2018 10:46:09 +0200 Subject: [PATCH 34/50] Basler camera: roi selection --- lantz/drivers/basler/pylon.py | 175 +++++++++++++++++++++++++++++++--- 1 file changed, 161 insertions(+), 14 deletions(-) diff --git a/lantz/drivers/basler/pylon.py b/lantz/drivers/basler/pylon.py index 5e2fd1f..5d2fea6 100644 --- a/lantz/drivers/basler/pylon.py +++ b/lantz/drivers/basler/pylon.py @@ -31,7 +31,8 @@ from lantz import Feat, DictFeat, Action # import ctypes as ct import pypylon -import numpy as np +# import numpy as np +import threading beginner_controls = ['ExposureTimeAbs', 'GainRaw', 'Width', 'Height', @@ -41,17 +42,19 @@ 'gain': 'GainRaw', } + def todict(listitems): 'Helper function to create dicts usable in feats' d = {} for item in listitems: - d.update({item:item}) + d.update({item: item}) return d + def attach_dyn_propr(instance, prop_name, propr): """Attach property proper to instance with name prop_name. - Reference: + Reference: * https://stackoverflow.com/a/1355444/509706 * https://stackoverflow.com/questions/48448074 """ @@ -60,11 +63,13 @@ def attach_dyn_propr(instance, prop_name, propr): instance.__class__ = child_class + def create_getter(p): def tmpfunc(self): return self.cam.properties[p] return tmpfunc + def create_setter(p): def tmpfunc(self, val): self.cam.properties[p] = val @@ -110,6 +115,8 @@ def __init__(self, camera=0, level='beginner', super().__init__(*args, **kwargs) self.camera = camera self.level = level + # Some actions cannot be performed while reading + self._grabbing_lock = threading.RLock() def initialize(self): ''' @@ -126,10 +133,12 @@ def initialize(self): self.cam = pypylon.factory.create_device(cam) else: try: - cam = [c for c in cameras if c.friendly_name == self.camera][0] + cam = [c for c in cameras + if c.friendly_name == self.camera][0] self.cam = pypylon.factory.create_device(cam) except IndexError: - self.log_error('Camera {} not found in cameras: {}'.format(self.camera, cameras)) + self.log_error('Camera {} not found in cameras: {}' + ''.format(self.camera, cameras)) return except RuntimeError as err: self.log_error(err) @@ -146,7 +155,8 @@ def initialize(self): # get rid of Mono12Packed and give a log error: fmt = self.pixel_format if fmt == str('Mono12Packed'): - self.log_error('PixelFormat {} not supported. Using Mono12 instead'.format(fmt)) + self.log_error('PixelFormat {} not supported. Using Mono12 ' + 'instead'.format(fmt)) self.pixel_format = 'Mono12' # Go to full available speed @@ -176,7 +186,6 @@ def _aliases(self): for alias, orig in aliases.items(): attach_dyn_propr(self, alias, self.feats[orig].feat) - @Feat() def info(self): # We can still get information of the camera back @@ -198,17 +207,19 @@ def info(self): # def gain(self, value): # self.cam.properties['GainRaw'] = value - @Feat(values=todict(['Mono8','Mono12','Mono12Packed'])) + @Feat(values=todict(['Mono8', 'Mono12', 'Mono12Packed'])) def pixel_format(self): fmt = self.cam.properties['PixelFormat'] if fmt == 'Mono12Packed': - self.log_error('PixelFormat {} not supported. Use Mono12 instead'.format(fmt)) + self.log_error('PixelFormat {} not supported. Use Mono12 instead' + ''.format(fmt)) return fmt @pixel_format.setter def pixel_format(self, value): if value == 'Mono12Packed': - self.log_error('PixelFormat {} not supported. Using Mono12 instead'.format(value)) + self.log_error('PixelFormat {} not supported. Using Mono12 ' + 'instead'.format(value)) value = 'Mono12' self.cam.properties['PixelFormat'] = value @@ -216,7 +227,7 @@ def pixel_format(self, value): def properties(self): 'Dict with all properties supported by pylon dll driver' return self.cam.properties - + @Action() def list_properties(self): 'List all properties and their values' @@ -226,8 +237,8 @@ def list_properties(self): except IOError: value = '' - print('{0} ({1}):\t{2}'.format(key, - self.cam.properties.get_description(key), value)) + description = self.cam.properties.get_description(key) + print('{0} ({1}):\t{2}'.format(key, description, value)) @Action(log_output=False) def grab_image(self): @@ -235,4 +246,140 @@ def grab_image(self): @Action(log_output=False) def grab_images(self, num=1): - return self.cam.grab_images(num) + with self._grabbing_lock: + img = self.cam.grab_images(num) + return img + + @Action() + def set_roi(self, height, width, yoffset, xoffset): + # Validation: + if width+xoffset > self.properties['WidthMax']: + self.log_error('Not setting ROI: Width + xoffset = {} exceeding ' + 'max width of camera {}.'.format(width+xoffset, + self.properties['WidthMax'])) + return + if height+yoffset > self.properties['HeightMax']: + self.log_error('Not setting ROI: Height + yoffset = {} exceeding ' + 'max height of camera {}.'.format(height+yoffset, + self.properties['HeightMax'])) + return + + # Offset should be multiple of 2: + xoffset -= xoffset % 2 + yoffset -= yoffset % 2 + + if height < 16: + self.log_error('Height {} too small, smaller than 16. Adjusting ' + 'to 16'.format(height)) + height = 16 + if width < 16: + self.log_error('Width {} too small, smaller than 16. Adjusting ' + 'to 16'.format(width)) + width = 16 + + with self._grabbing_lock: + # Order matters! + if self.OffsetY > yoffset: + self.OffsetY = yoffset + self.Height = height + else: + self.Height = height + self.OffsetY = yoffset + if self.OffsetX > xoffset: + self.OffsetX = xoffset + self.Width = width + else: + self.Width = width + self.OffsetX = xoffset + + @Action() + def reset_roi(self): + '''Sets ROI to maximum camera size''' + self.set_roi(self.properties['HeightMax'], + self.properties['WidthMax'], + 0, + 0) + + # Helperfunctions for ROI settings + def limit_width(self, dx): + if dx > self.properties['WidthMax']: + dx = self.properties['WidthMax'] + elif dx < 16: + dx = 16 + return dx + + def limit_height(self, dy): + if dy > self.properties['HeightMax']: + dy = self.properties['HeightMax'] + elif dy < 16: + dy = 16 + return dy + + def limit_xoffset(self, xoffset, dx): + if xoffset < 0: + xoffset = 0 + if xoffset + dx > self.properties['WidthMax']: + xoffset = self.properties['WidthMax'] - dx + return xoffset + + def limit_yoffset(self, yoffset, dy): + if yoffset < 0: + yoffset = 0 + if yoffset + dy > self.properties['HeightMax']: + yoffset = self.properties['HeightMax'] - dy + return yoffset + + @Action() + def calc_roi(self, center=None, size=None, coords=None): + '''Calculate the left bottom corner and the width and height + of a box with center (x,y) and size x [(x,y)]. Respects device + size''' + if center and size: + y, x = center + try: + dy, dx = size + except (TypeError): + dx = dy = size + + # Make sizes never exceed camera sizes + dx = self.limit_width(dx) + dy = self.limit_width(dy) + + xoffset = x - dx // 2 + yoffset = y - dy // 2 + + xoffset = self.limit_xoffset(xoffset, dx) + yoffset = self.limit_yoffset(yoffset, dy) + + return dy, dx, yoffset, xoffset + + elif coords: + xoffset = int(coords[1][0]) + dx = int(coords[1][1] - xoffset) + + yoffset = int(coords[0][0]) + dy = int(coords[0][1] - yoffset) + + # print(dy,dx) + dx = self.limit_width(dx) + dy = self.limit_height(dy) + + # print(yoffset, xoffset) + xoffset = self.limit_xoffset(xoffset, dx) + yoffset = self.limit_yoffset(yoffset, dy) + + return dy, dx, yoffset, xoffset + + else: + raise ValueError('center&size or coords should be supplied') + + def calc_roi_from_rel_coords(self, relcoords): + '''Calculate the new ROI from coordinates relative to the current + viewport''' + + coords = ((self.OffsetX + relcoords[0][0], + self.OffsetX + relcoords[0][1]), + (self.OffsetY + relcoords[1][0], + self.OffsetY + relcoords[1][1])) + # print('Rel_coords says new coords are', coords) + return self.calc_roi(coords=coords) From 9135b50933943f13e96e374ffd9db270aca1017f Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Wed, 4 Apr 2018 09:55:53 +0200 Subject: [PATCH 35/50] Fixed ROI selection in basler pylon camera driver --- lantz/drivers/basler/pylon.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lantz/drivers/basler/pylon.py b/lantz/drivers/basler/pylon.py index 5d2fea6..09a73d9 100644 --- a/lantz/drivers/basler/pylon.py +++ b/lantz/drivers/basler/pylon.py @@ -377,9 +377,9 @@ def calc_roi_from_rel_coords(self, relcoords): '''Calculate the new ROI from coordinates relative to the current viewport''' - coords = ((self.OffsetX + relcoords[0][0], - self.OffsetX + relcoords[0][1]), - (self.OffsetY + relcoords[1][0], - self.OffsetY + relcoords[1][1])) + coords = ((self.OffsetY + relcoords[0][0], + self.OffsetY + relcoords[0][1]), + (self.OffsetX + relcoords[1][0], + self.OffsetX + relcoords[1][1])) # print('Rel_coords says new coords are', coords) return self.calc_roi(coords=coords) From 4f17d96f3eb31dce2182bec004cd6f3b75ab414e Mon Sep 17 00:00:00 2001 From: AlexLCrook Date: Wed, 27 Jun 2018 11:42:00 -0500 Subject: [PATCH 36/50] Added Sacher Drivers --- lantz/drivers/sacher/EposCmd.dll | Bin 0 -> 663552 bytes lantz/drivers/sacher/Sacher.py | 697 +++++++++++++++++++++++++++++++ 2 files changed, 697 insertions(+) create mode 100644 lantz/drivers/sacher/EposCmd.dll create mode 100644 lantz/drivers/sacher/Sacher.py diff --git a/lantz/drivers/sacher/EposCmd.dll b/lantz/drivers/sacher/EposCmd.dll new file mode 100644 index 0000000000000000000000000000000000000000..8865e3cf1f233b42e1cd892cb5281629d878570f GIT binary patch literal 663552 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�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£%n@@n|&O3aJ+kD_Y-TK6!gu`N=`8!`(KRSPo8=kFnp zpL6qDTP9&?e+B;#FSy9y2%Kw!qkjK-MxwwYroeqt0F9)yGYkprpf9dOiEw2Vn`x+% zQhB9c;k?y>jMiiIrslehY_d<7aqv|qm1wYmm6u8@|@P4+fJR$ugveKqvA(x;*zXBin6@Z^1 zS!{+bR+pk4&~GG;z1ogA%X0kk{zjDHAs4@V{0i_3;TOh_#5}2S8QC<<6NM@PMu-X$Yl2f{-zAuX5Y_XlAbk1G6NIr%_ z2zQDGiZ;iizeDuwu-VrQ7Rs2c6WAvoQ-#z3^mM0e~f{_ywu}ZJ$egLPWVfnW#UxU8u+2 zA*L{@t{da|tVBL*pMjp~0oS-GPb-TRMeAxyzcg2a=HYKHcPil*!Y>TuA4t0$)#kKb z9*H7mRRyL9m+fErB|3hf)p0F78(++Op+NKV%6cBb9I8?`RCvE96wdG2X+R}cgXWQ< zYg7dkK?tyFz?ECNvi@|6@=)or`csg=qx?q7LD&^?U@liE{hmsgi+jkR*UY^Sca-cX z-Pw?f2wjlP-FHFWNY@ila}@wIlc7k-ZU1pFZnzJ7YT_j@zso+D22Eo@*Vq_@sm3WFUtwgZ&)^1>F0G4eMSaPjZ5@9yJ zqPvZ+yP+UM@O8*vg&uqzeVX@sH@*t}@U;mu8~AIS!B^D6*9as;fshKpSCqJcAVFG3 zPh}dLm0+^jf9MvI$)MO2b)v5@#m3jj-Nx4p0i|Png+2HRoZ30Q!hZN#{8Pc#XoIie z7QRkI!bmce;AOC zEP_<3W^x{Dq-b-kXdW(&16oI}(K-sCb?l#*fu#;}+IH>>IVpo6O`Z7(mrUb}%(o;h z=kr@88A`dd1xs@9Yhao>i5zy1`&dVKzjuxVQ@d9s`4AHqhFf8x%MO*qC=Dl#P6#Y% zmf^BcRiLIYFko-~DTv0airF6-yQ^fPgr1b
~jO4~650~W2~YvA?Vq~*u|2YmO} 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 z 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

Ej-;7@A{99&jzZ$GtUC7s= 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 0: + self._is_open = True + self._keyhandle = ret + return + +#3) + """ + This uses u32todouble three times, for the three coefficients + + Okay here's the important part: + self._coefA = CastedObjectData[0] + + self._doubleA = self._u32todouble(self._coefA) + self._doubleB = self._u32todouble(self._coefB) + self._doubleC = self._u32todouble(self._coefC) + self._currentwl = self._doubleA*(self._offset)**2.0 + self._doubleB*self._offset + self._doubleC + + """ + def initialize(self): + nodeID = ctypes.wintypes.WORD(0) + buf = ctypes.wintypes.DWORD(0) + BaudRate = DWORD(38400) + Timeout = DWORD(100) + ret = self.lib.SetProtocolStackSettings(self._keyhandle,BaudRate,Timeout,ctypes.byref(buf)) + if ret == 0: + errbuf = ctypes.create_string_buffer(64) + raise ValueError(errbuf.value) + buf = ctypes.wintypes.DWORD(0) + ret = self.lib.ClearFault(self._keyhandle,nodeID,ctypes.byref(buf)) + if ret == 0: + errbuf = ctypes.create_string_buffer(64) + self.lib.GetErrorInfo(buf, errbuf, WORD(64)) + raise ValueError(errbuf.value) + buf = ctypes.wintypes.DWORD(0) + plsenabled = ctypes.wintypes.DWORD(0) + ret = self.lib.GetEnableState(self._keyhandle,nodeID,ctypes.byref(plsenabled),ctypes.byref(buf)) + if ret == 0: + errbuf = ctypes.create_string_buffer(64) + self.lib.GetErrorInfo(buf, errbuf, WORD(64)) + raise ValueError(errbuf.value) + if int(plsenabled.value) != 0: + logging.warning(__name__ + ' EPOS motor enabled, disabling before proceeding.') + ret = self.lib.SetDisableState(self._keyhandle,nodeID,ctypes.byref(buf)) + if int(ret) != 0: + logging.warning(__name__ + ' EPOS motor successfully disabled, proceeding') + else: + logging.error(__name__ + ' EPOS motor was not successfully disabled!') + buf = ctypes.wintypes.DWORD(0) + Counts = WORD(512) # incremental encoder counts in pulses per turn + PositionSensorType = WORD(4) + ret = self.lib.SetEncoderParameter(self._keyhandle,nodeID,Counts,PositionSensorType,ctypes.byref(buf)) + + # Get operation mode, check if it's 1 -- this is "profile position mode" + buf = ctypes.wintypes.DWORD(0) + pMode = ctypes.pointer(ctypes.c_int8()) + self.lib.GetOperationMode.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.POINTER(ctypes.c_int8), ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.GetOperationMode.restype = ctypes.wintypes.BOOL + ret = self.lib.GetOperationMode(self._keyhandle, nodeID, pMode, ctypes.byref(buf)) + # if mode is not 1, make it 1 + if pMode.contents.value != 1: + self.lib.SetOperationMode.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.c_int8, ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.SetOperationMode.restype = ctypes.wintypes.BOOL + pMode_setting = ctypes.c_int8(1) + ret = self.lib.SetOperationMode(self._keyhandle, nodeID, pMode_setting, ctypes.byref(buf)) + self.lib.GetPositionProfile.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.POINTER(ctypes.wintypes.DWORD), ctypes.POINTER(ctypes.wintypes.DWORD), ctypes.POINTER(ctypes.wintypes.DWORD), ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.GetPositionProfile.restype = ctypes.wintypes.BOOL + pProfileVelocity = ctypes.pointer(ctypes.wintypes.DWORD()) + pProfileAcceleration = ctypes.pointer(ctypes.wintypes.DWORD()) + pProfileDeceleration = ctypes.pointer(ctypes.wintypes.DWORD()) + ret = self.lib.GetPositionProfile(self._keyhandle, nodeID, pProfileVelocity, pProfileAcceleration, pProfileDeceleration,ctypes.byref(buf)) + + #print(pProfileVelocity.contents.value, pProfileAcceleration.contents.value, pProfileDeceleration.contents.value) + + if (int(pProfileVelocity.contents.value) > int(11400) or int(pProfileAcceleration.contents.value) > int(60000) or int(pProfileDeceleration.contents.value) > int(60000)): + self.lib.GetPositionProfile.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.GetPositionProfile.restype = ctypes.wintypes.BOOL + pProfileVelocity = ctypes.wintypes.DWORD(429) + pProfileAcceleration = ctypes.wintypes.DWORD(429) + pProfileDeceleration = ctypes.wintypes.DWORD(429) + logging.warning(__name__ + ' GetPositionProfile out of bounds, resetting...') + ret = self.lib.SetPositionProfile(self._keyhandle, nodeID, pProfileVelocity, pProfileAcceleration, pProfileDeceleration,ctypes.byref(buf)) + + self._offset = self.get_offset() + + """DC - These are hardcoded values I got from the LabVIEW program -- I don't think any documentation exists on particular object indices""" + + """Coefficient A""" + self.lib.GetObject.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.wintypes.WORD, ctypes.c_uint8, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD), ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.GetObject.restype = ctypes.wintypes.BOOL + StoredPositionObject = ctypes.wintypes.WORD(8204) + StoredPositionObjectSubindex = ctypes.c_uint8(1) + StoredPositionNbBytesToRead = ctypes.wintypes.DWORD(4) + ObjectData = ctypes.c_void_p() + ObjectDataArray = (ctypes.c_uint32*1)() + ObjectData = ctypes.cast(ObjectDataArray, ctypes.POINTER(ctypes.c_uint32)) + StoredPositionNbBytesRead = ctypes.pointer(ctypes.wintypes.DWORD(0)) + ret = self.lib.GetObject(self._keyhandle, nodeID, StoredPositionObject, StoredPositionObjectSubindex, ObjectData, StoredPositionNbBytesToRead, StoredPositionNbBytesRead, ctypes.byref(buf)) + # Cast the object data to uint32 + CastedObjectData = ctypes.cast(ObjectData, ctypes.POINTER(ctypes.c_uint32)) + self._coefA = CastedObjectData[0] + + """Coefficient B""" + self.lib.GetObject.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.wintypes.WORD, ctypes.c_uint8, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD), ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.GetObject.restype = ctypes.wintypes.BOOL + StoredPositionObject = ctypes.wintypes.WORD(8204) + StoredPositionObjectSubindex = ctypes.c_uint8(2) + StoredPositionNbBytesToRead = ctypes.wintypes.DWORD(4) + ObjectData = ctypes.c_void_p() + ObjectDataArray = (ctypes.c_uint32*1)() + ObjectData = ctypes.cast(ObjectDataArray, ctypes.POINTER(ctypes.c_uint32)) + StoredPositionNbBytesRead = ctypes.pointer(ctypes.wintypes.DWORD(0)) + ret = self.lib.GetObject(self._keyhandle, nodeID, StoredPositionObject, StoredPositionObjectSubindex, ObjectData, StoredPositionNbBytesToRead, StoredPositionNbBytesRead, ctypes.byref(buf)) + # Cast the object data to uint32 + CastedObjectData = ctypes.cast(ObjectData, ctypes.POINTER(ctypes.c_uint32)) + self._coefB = CastedObjectData[0] + + """Coefficient C""" + self.lib.GetObject.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.wintypes.WORD, ctypes.c_uint8, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD), ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.GetObject.restype = ctypes.wintypes.BOOL + StoredPositionObject = ctypes.wintypes.WORD(8204) + StoredPositionObjectSubindex = ctypes.c_uint8(3) + StoredPositionNbBytesToRead = ctypes.wintypes.DWORD(4) + ObjectData = ctypes.c_void_p() + ObjectDataArray = (ctypes.c_uint32*1)() + ObjectData = ctypes.cast(ObjectDataArray, ctypes.POINTER(ctypes.c_uint32)) + StoredPositionNbBytesRead = ctypes.pointer(ctypes.wintypes.DWORD(0)) + ret = self.lib.GetObject(self._keyhandle, nodeID, StoredPositionObject, StoredPositionObjectSubindex, ObjectData, StoredPositionNbBytesToRead, StoredPositionNbBytesRead, ctypes.byref(buf)) + # Cast the object data to uint32 + CastedObjectData = ctypes.cast(ObjectData, ctypes.POINTER(ctypes.c_uint32)) + self._coefC = CastedObjectData[0] + + """Coefficient D""" + self.lib.GetObject.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.wintypes.WORD, ctypes.c_uint8, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD), ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.GetObject.restype = ctypes.wintypes.BOOL + StoredPositionObject = ctypes.wintypes.WORD(8204) + StoredPositionObjectSubindex = ctypes.c_uint8(4) + StoredPositionNbBytesToRead = ctypes.wintypes.DWORD(4) + ObjectData = ctypes.c_void_p() + ObjectDataArray = (ctypes.c_uint32*1)() + ObjectData = ctypes.cast(ObjectDataArray, ctypes.POINTER(ctypes.c_uint32)) + StoredPositionNbBytesRead = ctypes.pointer(ctypes.wintypes.DWORD(0)) + ret = self.lib.GetObject(self._keyhandle, nodeID, StoredPositionObject, StoredPositionObjectSubindex, ObjectData, StoredPositionNbBytesToRead, StoredPositionNbBytesRead, ctypes.byref(buf)) + # Cast the object data to uint32 + CastedObjectData = ctypes.cast(ObjectData, ctypes.POINTER(ctypes.c_uint32)) + self._coefD = CastedObjectData[0] + + """ + print('coefficients are %s %s %s %s' % (self._coefA, self._coefB, self._coefC, self._coefD)) + This gives the coefficients in some weird form, they're not what you expect them to be + """ + + self._doubleA = self._u32todouble(self._coefA) + self._doubleB = self._u32todouble(self._coefB) + self._doubleC = self._u32todouble(self._coefC) + firstHalf = np.int16(self._coefD >> 16) + secondHalf = np.int16(self._coefD & 0xffff) + # Set the minimum and maximum wavelengths for the motor + self._minwl = float(firstHalf)/10.0 + self._maxwl = float(secondHalf)/10.0 + # print 'first %s second %s' % (firstHalf, secondHalf) + # This returns '10871' and '11859' for the Sacher, which are the correct + # wavelength ranges in Angstroms + #print 'Now calculate the current wavelength position:' + self._currentwl = self._doubleA*(self._offset)**2.0 + self._doubleB*self._offset + self._doubleC + #print('Current wavelength: %.3f nm' % self._currentwl) + print('initializing done') + print("") + return True + + + +#4) + def get_offset(self): + nodeID = ctypes.wintypes.WORD(0) + buf = ctypes.wintypes.DWORD(0) + self.lib.GetObject.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.wintypes.WORD, ctypes.c_uint8, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD), ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.GetObject.restype = ctypes.wintypes.BOOL + #DC - These are hardcoded values I got from the LabVIEW program -- I don't think any documentation exists on particular object indices + StoredPositionObject = ctypes.wintypes.WORD(8321) + StoredPositionObjectSubindex = ctypes.c_uint8(0) + StoredPositionNbBytesToRead = ctypes.wintypes.DWORD(4) + ObjectData = ctypes.c_void_p() + ObjectDataArray = (ctypes.c_uint32*1)() + ObjectData = ctypes.cast(ObjectDataArray, ctypes.POINTER(ctypes.c_int32)) + StoredPositionNbBytesRead = ctypes.pointer(ctypes.wintypes.DWORD(0)) + ret = self.lib.GetObject(self._keyhandle, nodeID, StoredPositionObject, StoredPositionObjectSubindex, ObjectData, StoredPositionNbBytesToRead, StoredPositionNbBytesRead, ctypes.byref(buf)) + # Cast the object data to uint32 + CastedObjectData = ctypes.cast(ObjectData, ctypes.POINTER(ctypes.c_int32)) + if ret == 0: + logging.error(__name__ + ' Could not read stored position from Sacher EPOS motor') + print('motor offset value is: %s' % CastedObjectData[0]) + return CastedObjectData[0] + + +#5) + def get_motor_position(self): + nodeID = ctypes.wintypes.WORD(0) + buf = ctypes.wintypes.DWORD(0) + pPosition = ctypes.pointer(ctypes.c_long()) + self.lib.GetPositionIs.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.POINTER(ctypes.c_long), ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.GetPositionIs.restype = ctypes.wintypes.BOOL + ret = self.lib.GetPositionIs(self._keyhandle, nodeID, pPosition, ctypes.byref(buf)) + print('motor position value is: %s' % pPosition.contents.value) + return pPosition.contents.value + +#6) + @Feat() + def wavelength(self): + self._offset = self.get_offset() + self._motor_position = self.get_motor_position() + self._currentwl1 = self._doubleA*(self._offset)**2.0 + self._doubleB*self._offset + self._doubleC + self._currentwl2 = self._doubleA*(self._motor_position)**2.0 + self._doubleB*self._motor_position + self._doubleC + print('Current wavelength according to offset: %.3f nm' % self._currentwl1) + print('Current wavelength according to motor position: %.3f nm' % self._currentwl2) + return self._currentwl1 + + """ + And now we move on to setting things + """ + @wavelength.setter + def wavelength(self, wavelength): + """ + Here's the basic procedure: + 1) Convert the desired target wavelength into a motor position, keeping in mind that we're using the offset as the motor position + x is what the motor position should be + 2) Calculate difference between the target position and the stored offset + 3) Prompt some confirmation before we crash this plane + 4) Then actually move the motor + """ + current_wavelength = self.wavelength + current_offset = self.get_offset() + + nodeID = ctypes.wintypes.WORD(0) + buf = ctypes.wintypes.DWORD(0) + x = (-1.0*self._doubleB + np.sqrt(self._doubleB**2.0 - 4.0*self._doubleA*(self._doubleC - wavelength))) / (2.0*self._doubleA) + wavelength_to_pos = int(round(x)) + diff_wavelength_offset = wavelength_to_pos - int(self._offset) + + print('') + print("The current wavelength, as according to the stored offset, is: %s" % current_wavelength) + print("You're about to set the wavelength to: %s" % wavelength) + print("This means moving the motor by %s steps" % diff_wavelength_offset) + print("Where currently, the stored offset is: %s" % current_offset) + # confirm = str(input("Is this ok? (y/n)")) + # + # if confirm == 'y': + # print('Ok then, setting wavelength...') + # print('') + # else: + # print("ok then, shutting the whole thing down!") + # sys.exit(-1) + + if self._HPM and diff_wavelength_offset < 0: + self.set_target_position(diff_wavelength_offset - 10000, False, True) + self.set_target_position(10000, False, True) + else: + self.set_target_position(diff_wavelength_offset, False, True) + + self.set_new_offset(current_offset + diff_wavelength_offset) + current_offset = self.get_offset() + print("Now the stored offset is: %s" % current_offset) + + return + +#7) + def set_new_offset(self, new_offset): + """ + This is NOT using the function "self.lib.MoveToPosition" + So there's no literal motor movement is going on here + It's just storing a value in some sort of saved memory on the instrument itself + """ + nodeID = ctypes.wintypes.WORD(0) + buf = ctypes.wintypes.DWORD(0) + self.lib.SetObject.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.wintypes.WORD, ctypes.c_uint8, ctypes.POINTER(ctypes.wintypes.DWORD), ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD), ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.SetObject.restype = ctypes.wintypes.BOOL + StoredPositionObject = ctypes.wintypes.WORD(8321) + StoredPositionObjectSubindex = ctypes.c_uint8(0) + StoredPositionNbBytesToWrite = ctypes.wintypes.DWORD(4) + ObjectDataArray = (ctypes.c_uint32*1)(new_offset) + ObjectData = ctypes.cast(ObjectDataArray, ctypes.POINTER(ctypes.c_uint32)) + StoredPositionNbBytesWritten = ctypes.pointer(ctypes.wintypes.DWORD(0)) + ret = self.lib.SetObject(self._keyhandle, nodeID, StoredPositionObject, StoredPositionObjectSubindex, ObjectData, StoredPositionNbBytesToWrite, StoredPositionNbBytesWritten, ctypes.byref(buf)) + if ret == 0: + logging.error(__name__ + ' Could not write stored position from Sacher EPOS motor') + return + +#8) + def set_coeffs(self, a, b, c, min_wl, max_wl): + print('') + print("setting coefficients...") + nodeID = ctypes.wintypes.WORD(0) + buf = ctypes.wintypes.DWORD(0) + self.lib.SetObject.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.wintypes.WORD, ctypes.c_uint8, ctypes.POINTER(ctypes.wintypes.DWORD), ctypes.wintypes.DWORD, ctypes.POINTER(ctypes.wintypes.DWORD), ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.SetObject.restype = ctypes.wintypes.BOOL + d = (min_wl << 16) + max_wl + StoredPositionObject = ctypes.wintypes.WORD(8204) + + for subidx, coeff in enumerate([a, b, c]): + print(subidx, coeff) + StoredPositionObjectSubindex = ctypes.c_uint8(subidx + 1) + StoredPositionNbBytesToWrite = ctypes.wintypes.DWORD(4) + ObjectDataArray = (ctypes.c_uint32*1)(self._doubletou32(coeff)) + ObjectData = ctypes.cast(ObjectDataArray, ctypes.POINTER(ctypes.c_uint32)) + StoredPositionNbBytesWritten = ctypes.pointer(ctypes.wintypes.DWORD(0)) + ret = self.lib.SetObject(self._keyhandle, nodeID, StoredPositionObject, StoredPositionObjectSubindex, ObjectData, StoredPositionNbBytesToWrite, StoredPositionNbBytesWritten, ctypes.byref(buf)) + + StoredPositionObjectSubindex = ctypes.c_uint8(4) + StoredPositionNbBytesToWrite = ctypes.wintypes.DWORD(4) + ObjectDataArray = (ctypes.c_uint32*1)(d) + ObjectData = ctypes.cast(ObjectDataArray, ctypes.POINTER(ctypes.c_uint32)) + StoredPositionNbBytesWritten = ctypes.pointer(ctypes.wintypes.DWORD(0)) + ret = self.lib.SetObject(self._keyhandle, nodeID, StoredPositionObject, StoredPositionObjectSubindex, ObjectData, StoredPositionNbBytesToWrite, StoredPositionNbBytesWritten, ctypes.byref(buf)) + + print('Coefficients are %s %s %s' % (self._doubleA, self._doubleB, self._doubleC)) + + if ret == 0: + logging.error(__name__ + ' Could not write stored position from Sacher EPOS motor') + return + + +#9) + def set_target_position(self, target, absolute, immediately): + """ + This is the function that actually moves the motor + Since the motor position this thing reads can't be trusted, we're only doing relative movements and not absolute ones + This means the "absolute" argument should always be set as "false" + + "target" is the target motor position, but be very careful what you want to set this to depending on whether you're doing an absolute or relative movement + + In the "absolute" category, + True starts an absolute movement, False starts a relative movement + Since the absolute position can't be trusted at all, we should always do "False", the relative movement + + In the "immediately" category, + True starts immediately, False waits to end of last positioning + We typically do True for this without any real issues + + The actual money function is "self.lib.MoveToPosition" + """ + + nodeID = ctypes.wintypes.WORD(0) + buf = ctypes.wintypes.DWORD(0) + ret = self.lib.SetEnableState(self._keyhandle,nodeID,ctypes.byref(buf)) + pTarget = ctypes.c_long(target) + pAbsolute = ctypes.wintypes.BOOL(absolute) + pImmediately = ctypes.wintypes.BOOL(immediately) + self.lib.MoveToPosition.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.c_long, ctypes.wintypes.BOOL, ctypes.wintypes.BOOL, ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.MoveToPosition.restype = ctypes.wintypes.BOOL + ret = self.lib.MoveToPosition(self._keyhandle, nodeID, pTarget, pAbsolute, pImmediately, ctypes.byref(buf)) + steps_per_second = 14494.0 # hardcoded, estimated roughly, unused now + nchecks = 0 + while nchecks < 1000: + self._motor_position = self.get_motor_position() + self._offset = self.get_offset() + pMovementState = ctypes.pointer(ctypes.wintypes.BOOL()) + self.lib.GetMovementState.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.POINTER(ctypes.wintypes.BOOL), ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.GetMovementState.restype = ctypes.wintypes.BOOL + ret = self.lib.GetMovementState(self._keyhandle, nodeID, pMovementState, ctypes.byref(buf)) + if pMovementState.contents.value == 1: + break + nchecks = nchecks + 1 + time.sleep(0.01) + ret = self.lib.SetDisableState(self._keyhandle,nodeID,ctypes.byref(buf)) + return ret + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +#11) + @staticmethod + def get_bit(byteval, idx): + # def get_bit(self, byteval,idx): + return ((byteval&(1<< idx ))!=0) + """ + We take the two input numbers "byteval" and "idx" and do bitwise operations with them + but it's not clear what the purpose of these bitwise operations are + I also don't know what the @staticmethod decorator is supposed to be doing + """ + +#12) + @staticmethod + def _u32todouble(uinput): + # def _u32todouble(self, uinput): + + # get sign of number + sign = Sacher_EPOS.get_bit(uinput,31) + if sign == False: + mantissa_sign = 1 + elif sign == True: + mantissa_sign = -1 + exp_mask = 0b111111 + #print 'uin u is %d' % uinput + #print 'type uin %s' % type(uinput) + #print 'binary input is %s' % bin(long(uinput)) + # get sign of exponent + if Sacher_EPOS.get_bit(uinput,7) == False: + exp_sign = 1 + elif Sacher_EPOS.get_bit(uinput,7) == True: + exp_sign = -1 + + #print 'exp extract %s' % bin(int(uinput & exp_mask)) + #print 'exp conv %s' % (exp_sign*int(uinput & exp_mask)) + #print 'sign of exponent %s' % self.get_bit(uinput,7) + #print 'binary constant is %s' % bin(int(0b10000000000000000000000000000000)) + mantissa_mask = 0b01111111111111111111111100000000 + # mantissa_mask = 0b0111111111111111111111110000000 + + + #print 'mantissa extract is %s' % bin((uinput & mantissa_mask) >> 8) + mantissa = 1.0/1000000.0*float(mantissa_sign)*float((uinput & mantissa_mask) >> 8) + #print 'mantissa is %.12f' % mantissa + # print(1 if Sacher_EPOS.get_bit(uinput,31) else 0, mantissa, 1 if Sacher_EPOS.get_bit(uinput,7) else 0, uinput & exp_mask) + output = mantissa*2.0**(float(exp_sign)*float(int(uinput & exp_mask))) + #print 'output is %s' % output + return output + """ + ok dc gave some slight explanations here + This function implements the "really weird/non-standard U32 to floating point conversion in the sacher VIs" + It'd be gr8 if I knew what U32's were + unsigned 32 bit something something? + ah whatever + Also I'm seeing mantissas and masks, this is bad + """ + +#13) + @staticmethod + def _doubletou32(dinput): + mantissa_bit = 0 if int(dinput / abs(dinput)) > 0 else 1 + exp_bit = 1 if -1 < dinput < 1 else 0 + + b = np.ceil(np.log10(abs(dinput))) + a = dinput / 10 ** b + if dinput < 0: + a = -a + # print('a:\t{}\tb:\t{}'.format(a, b)) + + d = np.log2(10) * b + d_ = np.ceil(d) + c = a * 2 ** (d - d_) + # print('c:\t{}\td_:{}\toriginal:\t{}'.format(c, d_, c * 2 ** d_)) + + return (int(mantissa_bit) << 31) + (int(c * 1e6) << 8) + (int(exp_bit) << 7) + int(abs(d_)) + + """ + I think this was a new function that we made + + There might be a labview VI that does this correctly + """ + +#14) + def __del__(self): + # execute disconnect + self.close() + return + """ + this might be the only self explanatory one + it disconnects + """ + +#15) + def close(self): + print('closing EPOS motor.') + + self.lib.CloseDevice.argtypes = [ctypes.wintypes.HANDLE, ctypes.POINTER(DWORD)] + self.lib.CloseDevice.restype = ctypes.wintypes.BOOL + buf = ctypes.pointer(DWORD(0)) + ret = ctypes.wintypes.BOOL() + + ret = self.lib.CloseDevice(self._keyhandle, buf) + + #print 'close device returned %s' % buf + + if int(buf.contents.value) >= 0: + self._is_open = False + else: + logging.error(__name__ + ' did not close Sacher EPOS motor correctly.') + return + """ + Apparently this closes the EPOS motor + I don't know what "opening" and "closing" the motor means though + and yeah also these random variables don't make any sense to me + """ + + +#16) + def get_motor_current(self): + nodeID = ctypes.wintypes.WORD(0) + self.lib.GetCurrentIs.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.POINTER(ctypes.c_uint8), ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.GetCurrentIs.restype = ctypes.wintypes.BOOL + + motorCurrent = ctypes.c_uint8(0) + buf = ctypes.wintypes.DWORD(0) + ret = self.lib.GetCurrentIs(self._keyhandle, nodeID, ctypes.byref(motorCurrent), ctypes.byref(buf)) + return motorCurrent.value + + """ + Not sure what this is doing yet + """ + + +#17) + def find_home(self): + nodeID = ctypes.wintypes.WORD(0) + self.lib.FindHome.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.c_uint8, ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.FindHome.restype = ctypes.wintypes.BOOL + + buf = ctypes.wintypes.DWORD(0) + ret = self.lib.FindHome(self._keyhandle, nodeID, ctypes.c_uint8(35), ctypes.byref(buf)) + print('Homing: {}'.format(ret)) + return ret + + """ + Not sure what this is doing yet + """ + + +#18) + def restore(self): + nodeID = ctypes.wintypes.WORD(0) + self.lib.FindHome.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD, ctypes.POINTER(ctypes.wintypes.DWORD)] + self.lib.FindHome.restype = ctypes.wintypes.BOOL + buf = ctypes.wintypes.DWORD(0) + ret = self.lib.Restore(self._keyhandle, nodeID, ctypes.byref(buf)) + print('Restore: {}'.format(ret)) + return ret + + """ + Not sure what this is doing yet + """ + + + +#19) + def fine_tuning_steps(self, steps): + current_motor_pos = self.get_motor_position() + self._offset = self.get_offset() + self.set_target_position(steps, False, True) + new_motor_pos = self.get_motor_position() + #print('New motor position is %s' % new_motor_pos) + #print 'new offset is %s' % (new_motor_pos-current_motor_pos+self._offset) + self.set_new_offset(new_motor_pos-current_motor_pos+self._offset) + + """ + Not sure what this is doing yet + """ + +#20) + def is_open(self): + return self._is_open + +#21) + def clear_fault(self): + nodeID = ctypes.wintypes.WORD(0) + buf = ctypes.wintypes.DWORD(0) + ret = self.lib.ClearFault(self._keyhandle,nodeID,ctypes.byref(buf)) + print('clear fault buf %s, ret %s' % (buf, ret)) + if ret == 0: + errbuf = ctypes.create_string_buffer(64) + self.lib.GetErrorInfo(buf, errbuf, WORD(64)) + raise ValueError(errbuf.value) + """ + Not sure what this is doing yet + """ + + + + +""" +We're done with the Sacher_EPOS() class at this point +""" From 82b1438f8fd8ee185f4440c455b9c606d3c7cb4a Mon Sep 17 00:00:00 2001 From: Kevin Miao Date: Fri, 31 Aug 2018 11:02:27 -0500 Subject: [PATCH 37/50] latest lantz server --- lantz/drivers/lantz_server.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/lantz/drivers/lantz_server.py b/lantz/drivers/lantz_server.py index 912dc8d..6203aff 100644 --- a/lantz/drivers/lantz_server.py +++ b/lantz/drivers/lantz_server.py @@ -135,12 +135,14 @@ def query(self, data): sock.sendall(encode_data(data)) #Read back ans from the server - return receive_all(sock.recv, self.timeout) + ans = receive_all(sock.recv, self.timeout) + + sock.close() + return ans class Device_Client(): - def __new__(cls, device_driver_class, host, port, timeout=1): - + def __new__(cls, device_driver_class, host, port, timeout=1, allow_initialize_finalize=True): if type(device_driver_class) is str: class_name = device_driver_class.split('.')[-1] mod = import_module(device_driver_class.replace('.'+class_name, '')) @@ -148,7 +150,14 @@ def __new__(cls, device_driver_class, host, port, timeout=1): class Device_Client_Instance(Lantz_Base_Client): __name__ = '_Device_Client.' + device_driver_class.__name__ - __qualname__ = 'Device_Client.' + device_driver_class.__name__ + __qualname__ = 'Device_Client.' + device_driver_class.__name__ + _allow_initialize_finalize = allow_initialize_finalize + def initialize(self): + if self._allow_initialize_finalize: + self._initialize() + def finalize(self): + if self._allow_initialize_finalize: + self._finalize() for feat_name, feat in device_driver_class._lantz_features.items(): if isinstance(feat, Feat): @@ -176,16 +185,18 @@ def f_(_self, val): for action_name, action in device_driver_class._lantz_actions.items(): def execute(_action_name): def f_(_self, *args, **kwargs): - print(_action_name) - print(args) - print(kwargs) reply = _self.query(build_query('Action', _action_name, args=args, kwargs=kwargs)) if not reply['error'] is None: raise reply['error'] else: return reply['msg'] return f_ - setattr(Device_Client_Instance, action_name, execute(action_name)) + if action_name in ['initialize', 'finalize']: + setattr(Device_Client_Instance, '_'+action_name, execute(action_name)) + else: + setattr(Device_Client_Instance, action_name, execute(action_name)) + + obj = Device_Client_Instance.__new__(Device_Client_Instance) obj.__init__(host, port, timeout=timeout) From ef4fa08c2d1e1a359cb6be8f95fd5aca4d40ce27 Mon Sep 17 00:00:00 2001 From: Kevin Miao Date: Fri, 31 Aug 2018 11:03:30 -0500 Subject: [PATCH 38/50] Added safe fail back on import for lightfield drivers (for remote users) --- .../princetoninstruments/lightfield.py | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/lantz/drivers/princetoninstruments/lightfield.py b/lantz/drivers/princetoninstruments/lightfield.py index b807cb3..cc0c9d2 100644 --- a/lantz/drivers/princetoninstruments/lightfield.py +++ b/lantz/drivers/princetoninstruments/lightfield.py @@ -1,28 +1,32 @@ import numpy as np from time import sleep -import clr + import sys import os -# Import DLLs for running spectrometer via LightField -lf_root = os.environ['LIGHTFIELD_ROOT'] -automation_path = lf_root + '\PrincetonInstruments.LightField.AutomationV4.dll' -addin_path = lf_root + '\AddInViews\PrincetonInstruments.LightFieldViewV4.dll' -support_path = lf_root + '\PrincetonInstruments.LightFieldAddInSupportServices.dll' - -addin_class = clr.AddReference(addin_path); -automation_class = clr.AddReference(automation_path); -support_class = clr.AddReference(support_path); - -import PrincetonInstruments.LightField as lf - -# Import some system functions for interfacing with LightField code -clr.AddReference("System.Collections") -clr.AddReference("System.IO") -from System.Collections.Generic import List -from System import String -from System.IO import FileAccess +try: + import clr + # Import DLLs for running spectrometer via LightField + lf_root = os.environ['LIGHTFIELD_ROOT'] + automation_path = lf_root + '\PrincetonInstruments.LightField.AutomationV4.dll' + addin_path = lf_root + '\AddInViews\PrincetonInstruments.LightFieldViewV4.dll' + support_path = lf_root + '\PrincetonInstruments.LightFieldAddInSupportServices.dll' + + addin_class = clr.AddReference(addin_path); + automation_class = clr.AddReference(automation_path); + support_class = clr.AddReference(support_path); + + import PrincetonInstruments.LightField as lf + + # Import some system functions for interfacing with LightField code + clr.AddReference("System.Collections") + clr.AddReference("System.IO") + from System.Collections.Generic import List + from System import String + from System.IO import FileAccess +except: + pass # Lantz imports from lantz import Driver, Feat, DictFeat, Action From d49ee68b72fcec519fb9ea68d5a065a616bef68e Mon Sep 17 00:00:00 2001 From: AlexBourassa Date: Fri, 12 Oct 2018 16:29:09 -0500 Subject: [PATCH 39/50] Made Device_Client dynamically created classes subclasses of the device_driver_class --- lantz/drivers/lantz_server.py | 48 ++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/lantz/drivers/lantz_server.py b/lantz/drivers/lantz_server.py index 6203aff..a13833d 100644 --- a/lantz/drivers/lantz_server.py +++ b/lantz/drivers/lantz_server.py @@ -121,24 +121,24 @@ def handle(self): super().__init__((host, port), Lantz_Handler) -class Lantz_Base_Client(Driver): - def __init__(self, host, port, timeout=1): - self.host = host - self.port = port - self.timeout = timeout +# class Lantz_Base_Client(Driver): +# def __init__(self, host, port, timeout=1): +# self.host = host +# self.port = port +# self.timeout = timeout - def query(self, data): - #Initialize and send query - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.connect((self.host, self.port)) - sock.sendall(encode_data(data)) +# def query(self, data): +# #Initialize and send query +# sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +# sock.connect((self.host, self.port)) +# sock.sendall(encode_data(data)) - #Read back ans from the server - ans = receive_all(sock.recv, self.timeout) +# #Read back ans from the server +# ans = receive_all(sock.recv, self.timeout) - sock.close() - return ans +# sock.close() +# return ans class Device_Client(): @@ -148,7 +148,7 @@ def __new__(cls, device_driver_class, host, port, timeout=1, allow_initialize_fi mod = import_module(device_driver_class.replace('.'+class_name, '')) device_driver_class = getattr(mod, class_name) - class Device_Client_Instance(Lantz_Base_Client): + class Device_Client_Instance(device_driver_class): __name__ = '_Device_Client.' + device_driver_class.__name__ __qualname__ = 'Device_Client.' + device_driver_class.__name__ _allow_initialize_finalize = allow_initialize_finalize @@ -159,6 +159,24 @@ def finalize(self): if self._allow_initialize_finalize: self._finalize() + def __init__(self, host, port, timeout=1): + self.host = host + self.port = port + self.timeout = timeout + + + def query(self, data): + #Initialize and send query + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((self.host, self.port)) + sock.sendall(encode_data(data)) + + #Read back ans from the server + ans = receive_all(sock.recv, self.timeout) + + sock.close() + return ans + for feat_name, feat in device_driver_class._lantz_features.items(): if isinstance(feat, Feat): def get_fun(_feat_name): From e466886d7789051a96f8a8899aa83464529811c7 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Tue, 16 Oct 2018 15:46:36 +0200 Subject: [PATCH 40/50] lantz-monitor: ignore mallformed messaged --- scripts/lantz-monitor | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) mode change 100644 => 100755 scripts/lantz-monitor diff --git a/scripts/lantz-monitor b/scripts/lantz-monitor old mode 100644 new mode 100755 index 40d2aa3..37ef80d --- a/scripts/lantz-monitor +++ b/scripts/lantz-monitor @@ -444,15 +444,20 @@ class LantzMonitor(SocketListener): if record.levelno < self.level: return with self._lock: - queue = record.lantz_driver, record.lantz_name - if queue not in self.queues: - self.queues[queue] = InstrumentState() - self.retitle() - if len(self.queues) == 1: - for formatter in self.formatters: - formatter.current = queue - self.queues[queue].handle(record) - self.handler.handle(record) + try: + queue = record.lantz_driver, record.lantz_name + except AttributeError: + #TODO + pass + else: + if queue not in self.queues: + self.queues[queue] = InstrumentState() + self.retitle() + if len(self.queues) == 1: + for formatter in self.formatters: + formatter.current = queue + self.queues[queue].handle(record) + self.handler.handle(record) if __name__ == '__main__': From 50bd1385884fd8f3d481edd8161e6beb14ea0689 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Tue, 16 Oct 2018 17:58:29 +0200 Subject: [PATCH 41/50] Extended support for SMC100 controllor --- motion.py | 84 +++++++---------- motionsmc100.py | 242 +++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 234 insertions(+), 92 deletions(-) diff --git a/motion.py b/motion.py index c7e1fee..3ebdf91 100644 --- a/motion.py +++ b/motion.py @@ -29,7 +29,7 @@ # ureg.define('motorstep = step') -class MotionController(MessageBasedDriver): +class MotionControllerBase(MessageBasedDriver): """ Newport motion controller. It assumes all axes to have units mm """ @@ -48,6 +48,8 @@ class MotionController(MessageBasedDriver): }, } + +class MotionController(MotionControllerBase): def initialize(self): super().initialize() @@ -111,7 +113,7 @@ def finalize(self): super().finalize() -class MotionAxis(MotionController): +class MotionAxis(MotionControllerBase): def __init__(self, parent, num, id, *args, **kwargs): self.parent = parent self.num = num @@ -180,15 +182,11 @@ def position(self, pos): :param pos: new position """ - if not self.is_on: - self.log_error('Axis not enabled. Not moving!') - return - # First do move to extra position if necessary self._set_position(pos, wait=self.wait_until_done) - @Action(units=['mm', None]) - def _set_position(self, pos, wait=None): + @Action(units=['mm', None, None]) + def _set_position(self, pos, wait=None, force=False): """ Move to an absolute position, taking into account backlash. @@ -198,8 +196,22 @@ def _set_position(self, pos, wait=None): :param pos: New position in mm :param wait: wait until stage is finished + :param force: Do not check if pos is close to current position """ + if not self.is_on: + self.log_error('Axis not enabled. Not moving!') + return + + # Check if setting position is really nececcary + current_value = self.refresh('position') + if not force and np.isclose(pos, current_value, atol=self.accuracy): + self.log_info('No need to set {} = {} (current={}, force={}, ' + 'accuracy={})', + 'position', pos, current_value, force, + self.accuracy) + return + # First do move to extra position if necessary if self.backlash: position = self.position.magnitude @@ -237,22 +249,6 @@ def check_position(self, pos): self.position)) return False - @Feat(units='mm/s') - def max_velocity(self): - return float(self.query('VU?')) - - @max_velocity.setter - def max_velocity(self, velocity): - self.write('VU%f' % (velocity)) - - @Feat(units='mm/s**2') - def max_acceleration(self): - return float(self.query('AU?')) - - @max_acceleration.setter - def max_acceleration(self, velocity): - self.write('AU%f' % (velocity)) - @Feat(units='mm/s') def velocity(self): return float(self.query('VA?')) @@ -278,14 +274,6 @@ def acceleration(self, acceleration): """ self.write('AC%f' % (acceleration)) - @Feat(units='mm/s') - def actual_velocity(self): - return float(self.query('TV')) - - @actual_velocity.setter - def actual_velocity(self, val): - raise NotImplementedError - @Action() def stop(self): """Emergency stop""" @@ -308,22 +296,22 @@ def motion_done(self): # Q_('radian'): 9, # Q_('milliradian'): 10, # Q_('microradian'): 11}) - @Feat() - def units(self): - ret = int(self.query(u'SN?')) - vals = {0 :'encoder count', - 1 :'motor step', - 2 :'millimeter', - 3 :'micrometer', - 4 :'inches', - 5 :'milli-inches', - 6 :'micro-inches', - 7 :'degree', - 8 :'gradian', - 9 :'radian', - 10:'milliradian', - 11:'microradian',} - return vals[ret] + #@Feat() + #def units(self): + # ret = int(self.query(u'SN?')) + # vals = {0 :'encoder count', + # 1 :'motor step', + # 2 :'millimeter', + # 3 :'micrometer', + # 4 :'inches', + # 5 :'milli-inches', + # 6 :'micro-inches', + # 7 :'degree', + # 8 :'gradian', + # 9 :'radian', + # 10:'milliradian', + # 11:'microradian',} + # return vals[ret] # @units.setter # def units(self, val): diff --git a/motionsmc100.py b/motionsmc100.py index f38e438..3a3763e 100644 --- a/motionsmc100.py +++ b/motionsmc100.py @@ -16,26 +16,65 @@ from lantz.feat import Feat from lantz.action import Action from pyvisa import constants -from lantz import Q_, ureg -from lantz.processors import convert_to +import pyvisa.errors +# from lantz import Q_, ureg +# from lantz.processors import convert_to +import time +# import numpy as np if __name__ == '__main__': from motion import MotionController, MotionAxis else: from .motion import MotionController, MotionAxis -import time -import numpy as np ERRORS = {"@": "", - "A": "Unknown message code or floating point controller address.", - "B": "Controller address not correct.", - "C": "Parameter missing or out of range.", - "D": "Execution not allowed.", - "E": "home sequence already started.", - "I": "Execution not allowed in CONFIGURATION state.", - "J": "Execution not allowed in DISABLE state.", - "K": "Execution not allowed in READY state.", - "L": "Execution not allowed in HOMING state.", - "M": "Execution not allowed in MOVING state.",} + "A": "Unknown message code or floating point controller address.", + "B": "Controller address not correct.", + "C": "Parameter missing or out of range.", + "D": "Execution not allowed.", + "E": "home sequence already started.", + "I": "Execution not allowed in CONFIGURATION state.", + "J": "Execution not allowed in DISABLE state.", + "H": "Execution not allowed in NOT REFERENCED state.", + "K": "Execution not allowed in READY state.", + "L": "Execution not allowed in HOMING state.", + "M": "Execution not allowed in MOVING state.", + } + +positioner_errors = { + 0b1000000000: '80 W output power exceeded', + 0b0100000000: 'DC voltage too low', + 0b0010000000: 'Wrong ESP stage', + 0b0001000000: 'Homing time out', + 0b0000100000: 'Following error', + 0b0000010000: 'Short circuit detection', + 0b0000001000: 'RMS current limit', + 0b0000000100: 'Peak current limit', + 0b0000000010: 'Positive end of run', + 0b0000000001: 'Negative end of run', + } +controller_states = { + '0A': 'NOT REFERENCED from reset.', + '0B': 'NOT REFERENCED from HOMING.', + '0C': 'NOT REFERENCED from CONFIGURATION.', + '0D': 'NOT REFERENCED from DISABLE.', + '0E': 'NOT REFERENCED from READY.', + '0F': 'NOT REFERENCED from MOVING.', + '10': 'NOT REFERENCED ESP stage error.', + '11': 'NOT REFERENCED from JOGGING.', + '14': 'CONFIGURATION.', + '1E': 'HOMING commanded from RS-232-C.', + '1F': 'HOMING commanded by SMC-RC.', + '28': 'MOVING.', + '32': 'READY from HOMING.', + '33': 'READY from MOVING.', + '34': 'READY from DISABLE.', + '35': 'READY from JOGGING.', + '3C': 'DISABLE from READY.', + '3D': 'DISABLE from MOVING.', + '3E': 'DISABLE from JOGGING.', + '46': 'JOGGING from READY.', + '47': 'JOGGING from DISABLE.', + } class SMC100(MotionController): @@ -57,7 +96,7 @@ class SMC100(MotionController): except NameError: lantzlog = lantz.log.log_to_screen(level=lantz.log.DEBUG) lantz.log.log_to_socket(level=lantz.log.DEBUG) - + import time import numpy as np import warnings @@ -84,23 +123,35 @@ class SMC100(MotionController): 'baud_rate': 57600, 'parity': constants.Parity.none, 'stop_bits': constants.StopBits.one, - #'flow_control': constants.VI_ASRL_FLOW_NONE, - 'flow_control': constants.VI_ASRL_FLOW_XON_XOFF, # constants.VI_ASRL_FLOW_NONE, + 'flow_control': constants.VI_ASRL_FLOW_XON_XOFF, }, } def __init__(self, *args, **kwargs): - self.motionaxis_class = kwargs.pop('motionaxis_class', MotionAxisSMC100) + self.motionaxis_class = kwargs.pop('motionaxis_class', + MotionAxisSMC100) super().__init__(*args, **kwargs) def initialize(self): super().initialize() + + # Clear read buffer + self.clear_read_buffer() self.detect_axis() + @Action() + def clear_read_buffer(self): + '''Read all data that was still in the read buffer and discard this''' + try: + while True: + self.read() + except pyvisa.errors.VisaIOError: + pass # readbuffer was empty already + @Action() def detect_axis(self): """ Find the number of axis available. - + The detection stops as soon as an empty controller is found. """ self.axes = [] @@ -108,57 +159,152 @@ def detect_axis(self): scan_axes = True while scan_axes: i += 1 - id = self.query('%dID?' % i) - if id == '': + try: + idn = self.query('%dID?' % i) + except pyvisa.errors.VisaIOError: scan_axes = False else: - axis = self.motionaxis_class(self, i, id) - self.axes.append(axis) - + if idn == '': + scan_axes = False + else: + axis = self.motionaxis_class(self, i, idn) + self.axes.append(axis) - - class MotionAxisSMC100(MotionAxis): - def query(self, command, *, send_args=(None, None), recv_args=(None, None)): - respons = super().query(command,send_args=send_args, recv_args=recv_args) - #check for command: + def query(self, command, *, send_args=(None, None), + recv_args=(None, None)): + respons = super().query(command, send_args=send_args, + recv_args=recv_args) + # check for command: if not respons[:3] == '{:d}{}'.format(self.num, command[:2]): - self.log_error('Axis {}: Expected to return command {} instead of {}'.format(self.num, command[:3],respons[:3])) + self.log_error('Axis {}: Expected to return command {} instead of' + '{}'.format(self.num, command[:3], respons[:3])) return respons[3:] def write(self, command, *args, **kwargs): super().write(command, *args, **kwargs) return self.get_errors() + @Feat(units='mm') + def software_limit_positive(self): + '''Make sure that software limits are tighter than hardware limits, + else the stage will go to not reference mode''' + return self.query('SR?') + + @software_limit_positive.setter + def software_limit_positive(self, val): + return self._software_limit_setter(val, limit='positive') + + @Feat(units='mm') + def software_limit_negative(self): + return self.query('SL?') + + @software_limit_negative.setter + def software_limit_negative(self, val): + return self._software_limit_setter(val, limit='negative') + + def _software_limit_setter(self, val, limit='positive'): + self.enter_config_state() + if limit == 'positive': + ret = self.write('SR{}'.format(val)) + elif limit == 'negative': + ret = self.write('SL{}'.format(val)) + else: + self.log_error("Limit {} not in ('postive', 'negative')." + "".format(limit)) + self.leave_and_save_config_state() + return ret + + @Action() + def enter_config_state(self): + return self.write('PW1') + + @Action() + def leave_and_save_config_state(self): + '''Takes up to 10s, controller is unresposive in that time''' + super().write('PW0') + start = time.time() + # do-while loop + cont = True + while cont: + try: + self.status + except ValueError: + if (time.time() - start > 10): + self.log_error('Controller was going to CONFIGURATION ' + 'state but it took more than 10s. Trying ' + 'to continue anyway') + cont = False + else: + time.sleep(0.001) + else: + cont = False + @Action() def on(self): """Put axis on""" pass + self.write('MM1') @Action() def off(self): - """Put axis on""" + """Put axis off""" pass - + self.write('MM0') + @Action() def get_errors(self): ret = self.query('TE?') - err = ERRORS.get(ret, 'Error {}. Lookup in manual: https://www.newport.com/medias/sys_master/images/images/h11/he1/9117182525470/SMC100CC-SMC100PP-User-s-Manual.pdf'.format(ret)) + err = ERRORS.get(ret, 'Error {}. Lookup in manual: https://www.newpor' + 't.com/medias/sys_master/images/images/h11/he1/91171' + '82525470/SMC100CC-SMC100PP-User-s-Manual.pdf' + ''.format(ret)) if err: self.log_error('Axis {} error: {}'.format(self.num, err)) return err + @Feat() + def status(self): + '''Read and parse controller and axis status. This gives usefull error + messages''' + res = self.query('TS?') + positioner_error = [val for key, val in positioner_errors.items() if + int(res[:4], base=16) & key == key] + controller_state = controller_states[res[-2:]] + return positioner_error, controller_state + @Feat(values={True: '1', False: '0'}) def is_on(self): """ :return: True is axis on, else false """ return '1' - + # return self.query('MM?') + + @Action() + def home(self): + super().home() + self._wait_until_done() + + def _wait_until_done(self): + er, st = self.status + if st == 'MOVING.': + time.sleep(self.wait_time) + return self._wait_until_done() + elif st[:5] == 'READY': + return True + else: + self.log_error('Not reached position. Controller state: {} ' + 'Positioner errors: {}' + ''.format(st, ','.join(er))) + return False + @Feat() def motion_done(self): - return self.check_position(self.last_set_position) + if self.status[1][:5] == 'READY': + return True + return False @Action() def keypad_disable(self): @@ -170,8 +316,10 @@ def keypad_disable(self): import lantz.log parser = argparse.ArgumentParser(description='Test SMC100 driver') - parser.add_argument('-p', '--port', type=str, default='1', + parser.add_argument('-p', '--port', type=str, default='', help='Serial port to connect to') + parser.add_argument('-t', '--test', type=bool, default=False, + help='Move stage 100 times and read position') args = parser.parse_args() lantzlog = lantz.log.log_to_screen(level=lantz.log.INFO) @@ -179,6 +327,7 @@ def keypad_disable(self): import lantz import visa + from lantz import ureg import lantz.drivers.newport_motion sm = lantz.drivers.newport_motion.SMC100 rm = visa.ResourceManager('@py') @@ -186,12 +335,17 @@ def keypad_disable(self): print(lantz.messagebased._resource_manager.list_resources()) - with sm(args.port) as inst: - #with sm.via_serial(port=args.port) as inst: - inst.idn - # inst.initialize() # Initialize the communication with the power meter + with sm(args.port, timeout=100) as inst: # Find the status of all axes: - #for axis in inst.axes: - # print('Axis {} Position {} is_on {} max_velocity {} velocity {}'.format(axis.num, axis.position, - # axis.is_on, axis.max_velocity, - # axis.velocity)) + for axis in inst.axes: + msg = ['{}: {}'.format(name, val) for name, val in + axis.refresh().items()] + print('Axis {} properties:'.format(axis.num)) + print(' ' + '\n '.join(msg)) + if args.test: + print('### Reading stability test ###') + for i in range(100): + print(i, inst.axes[0].position) + inst.axes[0].position += 0.5 * ureg.um + print(i, inst.axes[0].position) + inst.axes[0].position -= 0.5 * ureg.um From d2461080590c7799d42fcd561094d101bd5c00cb Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 3 Dec 2018 10:25:48 +0100 Subject: [PATCH 42/50] Extended piezo support --- lantz/drivers/pi/piezo.py | 14 ++++++++------ lantz/feat.py | 25 ++++++++++++++++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lantz/drivers/pi/piezo.py b/lantz/drivers/pi/piezo.py index b3e3d9f..6349835 100644 --- a/lantz/drivers/pi/piezo.py +++ b/lantz/drivers/pi/piezo.py @@ -66,10 +66,9 @@ class Piezo(MessageBasedDriver): print('Available devices:', lantz.messagebased._resource_manager.list_resources()) print('Connecting to: ASRL/dev/ttyUSB0::INSTR') - stage = pi.Piezo('ASRL/dev/ttyUSB0::INSTR') + stage = pi.Piezo('ASRL/dev/ttyUSB0::INSTR', axis='X', + sleeptime_after_move=10*ureg.ms) stage.initialize() - stage.sleeptime_after_move = 10*ureg.ms - stage.axis = 'X' idn = stage.idn stage.servo = True @@ -111,9 +110,12 @@ class Piezo(MessageBasedDriver): 'baud_rate': 57600, 'timeout': 20}, } - def initialize(self, axis='X', sleeptime_after_move=0*ureg.ms): - self.axis = axis - self.sleeptime_after_move = sleeptime_after_move + def __init__(self, *args, **kwargs): + self.sleeptime_after_move = kwargs.pop('sleeptime_after_move', 0*ureg.ms) + self.axis = kwargs.pop('axis', 'X') + super().__init__(*args, **kwargs) + + def initialize(self): super().initialize() def finalize(self): diff --git a/lantz/feat.py b/lantz/feat.py index d950364..1cc9617 100644 --- a/lantz/feat.py +++ b/lantz/feat.py @@ -12,6 +12,7 @@ import time import copy +import numpy as np from weakref import WeakKeyDictionary from . import Q_ @@ -87,6 +88,8 @@ class Feat(object): changed but only tested to belong to the container. :param units: `Quantity` or string that can be interpreted as units. :param procs: Other callables to be applied to input arguments. + :precision: Only write to an instrument if the set value is more than one + precision away from last known value """ @@ -94,7 +97,7 @@ class Feat(object): def __init__(self, fget=MISSING, fset=None, doc=None, *, values=None, units=None, limits=None, procs=None, - read_once=False): + read_once=False, precision=0): self.fget = fget self.fset = fset self.__doc__ = doc @@ -119,7 +122,8 @@ def __init__(self, fget=MISSING, fset=None, doc=None, *, self.modifiers[MISSING] = {MISSING: {'values': values, 'units': units, 'limits': limits, - 'processors': procs}} + 'processors': procs, + 'precision': precision}} self.get_processors[MISSING] = {MISSING: ()} self.set_processors[MISSING] = {MISSING: ()} @@ -135,6 +139,7 @@ def rebuild(self, instance=MISSING, key=MISSING, build_doc=False, modifiers=None units = modifiers['units'] limits = modifiers['limits'] processors = modifiers['processors'] + precision = modifiers['precision'] get_processors = [] set_processors = [] @@ -155,6 +160,14 @@ def rebuild(self, instance=MISSING, key=MISSING, build_doc=False, modifiers=None get_processors.append(Processor(getp)) if setp is not None: set_processors.append(Processor(setp)) + if precision: + if isinstance(precision, str): + if not units: + raise ValueError('Units have to be set for precision of ' + 'type {}', type(precision)) + precision = ToQuantityProcessor(units)(precision) + if isinstance(precision, Q_): + precision = FromQuantityProcessor(units)(precision) if build_doc: _dochelper(self) @@ -255,9 +268,11 @@ def set(self, instance, value, force=False, key=MISSING): # and timing, caching, logging and error handling with instance._lock: current_value = self.get_cache(instance, key) - - if not force and value == current_value: - instance.log_info('No need to set {} = {} (current={}, force={})', name, value, current_value, force) + modifiers = _dget(self.modifiers, instance, key) + precision = modifiers['precision'] + if not force and (current_value is MISSING or + np.isclose(value, current_value, atol=precision)): + instance.log_info('No need to set {} = {} (current={}, force={}, precision={})', name, value, current_value, force, precision) return instance.log_info('Setting {} = {} (current={}, force={})', name, value, current_value, force) From ccdf99d31ebe58c368780589530919731891176c Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Mon, 3 Dec 2018 11:43:56 +0100 Subject: [PATCH 43/50] second part of copy --- .gitmodules | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 607ecf9..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "lantz/drivers/newport_esp"] - path = lantz/drivers/newport_motion - url = git@github.com:vascotenner/lantz_driver_newport_esp301.git From 6f4c068f1112a64d1a8b8941f20918d84d432168 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Tue, 4 Dec 2018 17:40:24 +0100 Subject: [PATCH 44/50] Extended requirements. Updated docstring of Feat --- lantz/feat.py | 7 ++++++- requirements-full.txt | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lantz/feat.py b/lantz/feat.py index 1cc9617..bb7d125 100644 --- a/lantz/feat.py +++ b/lantz/feat.py @@ -87,8 +87,13 @@ class Feat(object): If a list/tuple instead of a dict is given, the value is not changed but only tested to belong to the container. :param units: `Quantity` or string that can be interpreted as units. + :param limits: ([start,] stop[, step]) Define range in which value has to be. + Including start and stop. If you provide a value outside the valid + range, Lantz will raise a ValueError. If the steps parameter is set + but you provide a value not compatible with it, it will be silently + rounded. :param procs: Other callables to be applied to input arguments. - :precision: Only write to an instrument if the set value is more than one + :param precision: Only write to an instrument if the set value is more than one precision away from last known value """ diff --git a/requirements-full.txt b/requirements-full.txt index 3e98d8e..4dad18d 100644 --- a/requirements-full.txt +++ b/requirements-full.txt @@ -2,4 +2,5 @@ colorama pyserial pyusb numpy +stringparser -r requirements-doc.txt From 505ce1973b11eedbe36f6ffbbc1a219be3194650 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Tue, 4 Dec 2018 18:04:53 +0100 Subject: [PATCH 45/50] Extended driver for allied vision cameras. --- lantz/drivers/alliedvision/__init__.py | 4 +- lantz/drivers/alliedvision/tests/vimbatest.py | 4 + lantz/drivers/alliedvision/vimba.py | 372 +++++++++++++++++- 3 files changed, 367 insertions(+), 13 deletions(-) create mode 100644 lantz/drivers/alliedvision/tests/vimbatest.py diff --git a/lantz/drivers/alliedvision/__init__.py b/lantz/drivers/alliedvision/__init__.py index 1806fce..84eded0 100644 --- a/lantz/drivers/alliedvision/__init__.py +++ b/lantz/drivers/alliedvision/__init__.py @@ -1,3 +1,3 @@ -from .vimba import VimbaCam +from .vimba2 import VimbaCam, list_cameras -__all__ = ['VimbaCam'] +__all__ = ['VimbaCam', 'list_cameras'] diff --git a/lantz/drivers/alliedvision/tests/vimbatest.py b/lantz/drivers/alliedvision/tests/vimbatest.py new file mode 100644 index 0000000..1ea3937 --- /dev/null +++ b/lantz/drivers/alliedvision/tests/vimbatest.py @@ -0,0 +1,4 @@ +from lantz.drivers.alliedvision import list_cameras + +if __name__ == '__main__': + print(list_cameras()) \ No newline at end of file diff --git a/lantz/drivers/alliedvision/vimba.py b/lantz/drivers/alliedvision/vimba.py index 82b9267..4784799 100644 --- a/lantz/drivers/alliedvision/vimba.py +++ b/lantz/drivers/alliedvision/vimba.py @@ -3,40 +3,243 @@ lantz.drivers.alliedvision ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Implementation for a single vimba cam (could be modified to work with multiple camera if need be) + Implementation for a alliedvision camera via pymba and pylon - Requires pymba + Requires: + - pymba: https://github.com/morefigs/pymba + - vimba: https://www.alliedvision.com/en/products/software.html - Author: AlexBourassa and Kevin Miao - Date: 24/07/2017 + Log: + - create same API as lantz.drivers.basler + + + Author: Vasco Tenner + Date: 20181204 + + TODO: + - Test + - 12 bit packet readout + - Bandwith control + - Dynamically set available values for feats """ from lantz.driver import Driver from lantz import Feat, DictFeat, Action from pymba import Vimba import numpy as np +import threading +import time + + +beginner_controls = ['ExposureTimeAbs', 'GainRaw', 'Width', 'Height', + 'OffsetX', 'OffsetY'] +aliases = {'exposure_time': 'ExposureTimeAbs', + 'gain': 'GainRaw', + } + + +def todict(listitems): + 'Helper function to create dicts usable in feats' + d = {} + for item in listitems: + d.update({item: item}) + return d + + +def attach_dyn_propr(instance, prop_name, propr): + """Attach property proper to instance with name prop_name. + + Reference: + * https://stackoverflow.com/a/1355444/509706 + * https://stackoverflow.com/questions/48448074 + """ + class_name = instance.__class__.__name__ + 'C' + child_class = type(class_name, (instance.__class__,), {prop_name: propr}) + + instance.__class__ = child_class + + +def create_getter(p): + def tmpfunc(self): + return self.cam[p] + return tmpfunc + + +def create_setter(p): + def tmpfunc(self, val): + self.cam[p] = val + return tmpfunc + + +def list_cameras(): + with Vimba() as vimba: + # get system object + system = vimba.getSystem() + + # list available cameras (after enabling discovery for GigE cameras) + if system.GeVTLIsPresent: + system.runFeatureCommand("GeVDiscoveryAllOnce") + time.sleep(0.2) + cameraIds = vimba.getCameraIds() + for cameraId in cameraIds: + print(cameraId) class VimbaCam(Driver): + def __init__(self, camera=0, level='beginner', + *args, **kwargs): + """ + @params + :type camera_num: int, The camera device index: 0,1,.. + :type level: str, Level of controls to show ['beginner', 'expert'] + + Example: + import lantz + from lantz.drivers.alliedvision import Cam + import time + try: + lantzlog + except NameError: + lantzlog = lantz.log.log_to_screen(level=lantz.log.DEBUG) + + cam = Cam(camera='Basler acA4112-8gm (40006341)') + cam.initialize() + cam.exposure_time + cam.exposure_time = 3010 + cam.exposure_time + cam.grab_image() + print('Speedtest:') + nr = 10 + start = time.time() + for n in range(nr): + cam.grab_image() + duration = (time.time()-start)*1000*lantz.Q_('ms') + print('Read {} images in {}. Reading alone took {}. Framerate {}'.format(nr, + duration, duration - nr* cam.exposure_time, nr / duration.to('s'))) + cam.finalize() + """ + super().__init__(*args, **kwargs) + self.camera = camera + self.level = level + # Some actions cannot be performed while reading + self._grabbing_lock = threading.RLock() + def initialize(self): + """ + Params: + camera -- number in list of show_cameras or friendly_name + """ + self.vimba = Vimba() self.vimba.startup() - self.cam = self.vimba.getCamera(self.vimba.getCameraIds()[0]) - self.cam.openCamera() - self.cam.PixelFormat = 'Mono8' + cameras = self.vimba.getCameraIds() + self.log_debug('Available cameras are:' + str(cameras)) + + try: + if isinstance(self.camera, int): + cam = cameras[self.camera] + self.cam = self.vimba.getCamera(cam) + else: + self.cam = self.vimba.getCamera(self.camera) + except RuntimeError as err: + self.log_error(err) + raise RuntimeError(err) + self.frame = self.cam.getFrame() self.frame.announceFrame() self.cam.startCapture() - return + + self._dynamically_add_properties() + self._aliases() + + # get rid of Mono12Packed and give a log error: + fmt = self.pixel_format + if fmt == str('Mono12Packed'): + self.log_error('PixelFormat {} not supported. Using Mono12 ' + 'instead'.format(fmt)) + self.pixel_format = 'Mono12' + + # Go to full available speed + # cam.properties['DeviceLinkThroughputLimitMode'] = 'Off' def finalize(self): - self.vimba.shutdown() + self.cam.endCapture() + self.cam.revokeAllFrames() return + def _dynamically_add_properties(self): + """Add all properties available on driver as Feats""" + props = self.getFeatureNames() if self.level == 'expert' else beginner_controls + for p in props: + info = self.cam.getFeatureInfo(p) + range_ = self.cam.getFeatureRange(p) + limits = range_ if isinstance(tuple, range_) else None + values = range_ if isinstance(list, range_) else None + + feat = Feat(fget=create_getter(p), + fset=create_setter(p), + doc=info.description, + units=info.unit, + limits=limits, + values=values, + ) + feat.name = p + attach_dyn_propr(self, p, feat) + + def _aliases(self): + """Add easy to use aliases to strange internal pylon names + + Note that in the Logs, the original is renamed to the alias""" + for alias, orig in aliases.items(): + attach_dyn_propr(self, alias, self.feats[orig].feat) + + @Feat() + def info(self): + # We can still get information of the camera back + return 'Camera info of camera object:', self.cam.getInfo() # TODO TEST + + # Most properties are added automatically by _dynamically_add_properties + + @Feat(values=todict(['Mono8', 'Mono12', 'Mono12Packed'])) + def pixel_format(self): + fmt = self.cam['PixelFormat'] + if fmt == 'Mono12Packed': + self.log_error('PixelFormat {} not supported. Use Mono12 instead' + ''.format(fmt)) + return fmt + + @pixel_format.setter + def pixel_format(self, value): + if value == 'Mono12Packed': + self.log_error('PixelFormat {} not supported. Using Mono12 ' + 'instead'.format(value)) + value = 'Mono12' + self.cam['PixelFormat'] = value + + @Feat() + def properties(self): + """Dict with all properties supported by pylon dll driver""" + return self.cam.getFeatureNames() + @Action() - def getFrame(self): + def list_properties(self): + """List all properties and their values""" + for key in self.cam.getFeatureNames(): + try: + value = self.cam[key] + except IOError: + value = '' + + description = self.cam.getFeatureInfo(key) + range_ = self.cam.getFeatureRange(key) + print('{0} ({1}):\t{2}\t{3}'.format(key, description, value, range_)) + + @Action(log_output=False) + def grab_image(self): + """Record a single image from the camera""" + self.cam.AcquisitionMode = 'SingleFrame' try: self.frame.queueFrameCapture() success = True @@ -54,6 +257,153 @@ def getFrame(self): 'shape': (self.frame.height, self.frame.width, 1), } img = np.ndarray(**img_config) - return img[...,0] + return img[..., 0] else: return None + + @Action(log_output=False) + def grab_images(self, num=1): + # ΤΟDO see https://gist.github.com/npyoung/1c160c9eee91fd44c587 + raise NotImplemented() + with self._grabbing_lock: + img = self.cam.grab_images(num) + return img + + @Action(log_output=False) + def getFrame(self): + """Backward compatibility""" + return self.grab_image + + @Action() + def set_roi(self, height, width, yoffset, xoffset): + # Validation: + if width + xoffset > self.properties['WidthMax']: + self.log_error('Not setting ROI: Width + xoffset = {} exceeding ' + 'max width of camera {}.'.format(width + xoffset, + self.properties['WidthMax'])) + return + if height + yoffset > self.properties['HeightMax']: + self.log_error('Not setting ROI: Height + yoffset = {} exceeding ' + 'max height of camera {}.'.format(height + yoffset, + self.properties['HeightMax'])) + return + + # Offset should be multiple of 2: + xoffset -= xoffset % 2 + yoffset -= yoffset % 2 + + if height < 16: + self.log_error('Height {} too small, smaller than 16. Adjusting ' + 'to 16'.format(height)) + height = 16 + if width < 16: + self.log_error('Width {} too small, smaller than 16. Adjusting ' + 'to 16'.format(width)) + width = 16 + + with self._grabbing_lock: + # Order matters! + if self.OffsetY > yoffset: + self.Height = height + self.OffsetY = yoffset + else: + self.Height = height + self.OffsetY = yoffset + if self.OffsetX > xoffset: + self.OffsetX = xoffset + self.Width = width + else: + self.Width = width + self.OffsetX = xoffset + + @Action() + def reset_roi(self): + """Sets ROI to maximum camera size""" + self.set_roi(self.properties['HeightMax'], + self.properties['WidthMax'], + 0, + 0) + + # Helperfunctions for ROI settings + def limit_width(self, dx): + if dx > self.properties['WidthMax']: + dx = self.properties['WidthMax'] + elif dx < 16: + dx = 16 + return dx + + def limit_height(self, dy): + if dy > self.properties['HeightMax']: + dy = self.properties['HeightMax'] + elif dy < 16: + dy = 16 + return dy + + def limit_xoffset(self, xoffset, dx): + if xoffset < 0: + xoffset = 0 + if xoffset + dx > self.properties['WidthMax']: + xoffset = self.properties['WidthMax'] - dx + return xoffset + + def limit_yoffset(self, yoffset, dy): + if yoffset < 0: + yoffset = 0 + if yoffset + dy > self.properties['HeightMax']: + yoffset = self.properties['HeightMax'] - dy + return yoffset + + @Action() + def calc_roi(self, center=None, size=None, coords=None): + """Calculate the left bottom corner and the width and height + of a box with center (x,y) and size x [(x,y)]. Respects device + size""" + if center and size: + y, x = center + try: + dy, dx = size + except TypeError: + dx = dy = size + + # Make sizes never exceed camera sizes + dx = self.limit_width(dx) + dy = self.limit_width(dy) + + xoffset = x - dx // 2 + yoffset = y - dy // 2 + + xoffset = self.limit_xoffset(xoffset, dx) + yoffset = self.limit_yoffset(yoffset, dy) + + return dy, dx, yoffset, xoffset + + elif coords: + xoffset = int(coords[1][0]) + dx = int(coords[1][1] - xoffset) + + yoffset = int(coords[0][0]) + dy = int(coords[0][1] - yoffset) + + # print(dy,dx) + dx = self.limit_width(dx) + dy = self.limit_height(dy) + + # print(yoffset, xoffset) + xoffset = self.limit_xoffset(xoffset, dx) + yoffset = self.limit_yoffset(yoffset, dy) + + return dy, dx, yoffset, xoffset + + else: + raise ValueError('center&size or coords should be supplied') + + def calc_roi_from_rel_coords(self, relcoords): + """Calculate the new ROI from coordinates relative to the current + viewport""" + + coords = ((self.OffsetY + relcoords[0][0], + self.OffsetY + relcoords[0][1]), + (self.OffsetX + relcoords[1][0], + self.OffsetX + relcoords[1][1])) + # print('Rel_coords says new coords are', coords) + return self.calc_roi(coords=coords) \ No newline at end of file From 06d3670f26c308049c75ad6b4798bc027abb349f Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Tue, 11 Dec 2018 14:41:03 +0100 Subject: [PATCH 46/50] Add option to change unit of feats and actions easily --- lantz/action.py | 8 ++++++-- lantz/driver.py | 32 ++++++++++++++++++++++++++++++++ lantz/feat.py | 42 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/lantz/action.py b/lantz/action.py index b21bc5d..1308fab 100644 --- a/lantz/action.py +++ b/lantz/action.py @@ -20,7 +20,7 @@ from .processors import (Processor, FromQuantityProcessor, MapProcessor, RangeProcessor) -from .feat import MISSING +from .feat import MISSING, FeatActionUpdateModifiersMixing def _dget(adict, instance=MISSING): @@ -46,7 +46,7 @@ def _dset(adict, value, instance=MISSING): adict[instance] = value -class Action(object): +class ActionBase(object): """Wraps a Driver method with Lantz. Can be used as a decorator. Processors can registered for each arguments to modify their values before @@ -201,6 +201,10 @@ def rebuild(self, instance=MISSING, build_doc=False, modifiers=None, store=False return procs +class Action(ActionBase, FeatActionUpdateModifiersMixing): + pass + + class ActionProxy(object): """Proxy object for Actions that allows to store instance specific modifiers. diff --git a/lantz/driver.py b/lantz/driver.py index 803e9c6..10dd953 100644 --- a/lantz/driver.py +++ b/lantz/driver.py @@ -15,12 +15,14 @@ from functools import wraps from concurrent import futures from collections import defaultdict +import itertools from .utils.qt import MetaQObject, SuperQObject, QtCore from .feat import Feat, DictFeat, MISSING, FeatProxy from .action import Action, ActionProxy from .stats import RunningStats from .log import get_logger +from . import Q_ logger = get_logger('lantz.driver', False) @@ -42,6 +44,17 @@ def _merge_dicts(*args): return out +def unit_to_string(units): + return str(Q_(units).units) + +def unit_replace(unit, old, new): + unit = unit.split(' ') + for i, part in enumerate(unit): + if part == old: + unit[i] = new + + return ' '.join(unit) + class MetaSelf(type): """Metaclass for Self object @@ -466,6 +479,25 @@ def feats(self): def actions(self): return Proxy(self, self._lantz_actions, ActionProxy) + def update_units(self, old_units, units): + """Updates the units of all feats and actions and actions from self.units to units. + + E.g. mm/s -> radian.s + + :param old_units: string or units + :param units: string or units + :return: + """ + + old_units_str = unit_to_string(old_units) + units_str = unit_to_string(units) + + for featname, feat in itertools.chain(self.feats.items(), self.actions.items()): + old_feat_units_str = unit_to_string(feat.feat.modifiers[MISSING][MISSING]['units']) + new_feat_units = unit_replace(old_feat_units_str, old_units_str, units_str) + self.log_debug("Updating units of feat {} from {} to {}", featname, old_feat_units_str, new_feat_units) + feat.feat.change_units(units=new_feat_units) + def _solve_dependencies(dependencies, all_members=None): """Solve a dependency graph. diff --git a/lantz/feat.py b/lantz/feat.py index bb7d125..7efd1f5 100644 --- a/lantz/feat.py +++ b/lantz/feat.py @@ -66,7 +66,43 @@ def _dset(adict, value, instance=MISSING, key=MISSING): adict[instance][key] = value -class Feat(object): +class FeatActionUpdateModifiersMixing(): + """ + Adds functionality to a class to change its modifiers + """ + def change_units(self, units=MISSING): + """Changes the units of a self""" + return self.change_modifiers( units=units) + + def change_values(self, values=MISSING): + """Changes the values of a self""" + return self.change_modifiers( values=values) + + def change_limits(self, limits=MISSING): + """Changes the values of a self""" + return self.change_modifiers( limits=limits) + + def change_processors(self, processors=MISSING): + """Changes the values of a self""" + return self.change_modifiers( processors=processors) + + def change_precision(self, precision=MISSING): + """Changes the values of a self""" + return self.change_modifiers(precision=precision) + + def change_modifiers(self, **kwargs): + """Updates the modfieres of a self""" + + # Keep self.modifiers and _get_processors in sync + modifiers = self.modifiers[MISSING][MISSING] + modifiers.update(kwargs) + print('Update', dict(modifiers)) + retval = self.rebuild(build_doc=True, modifiers=modifiers, store=True) + + print('After update', self.modifiers[MISSING][MISSING]) + + +class FeatBase(object): """Pimped Python property for interfacing with instruments. Can be used as a decorator. @@ -334,6 +370,10 @@ def set_cache(self, instance, value, key=MISSING): getattr(instance, self.name + '_changed').emit(value, old_value) +class Feat(FeatBase, FeatActionUpdateModifiersMixing): + pass + + class DictFeat(Feat): """Pimped Python property with getitem access for interfacing with instruments. Can be used as a decorator. From 4f7fabe664c5c8e7180f49239369fa173413fa48 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Tue, 11 Dec 2018 17:17:18 +0100 Subject: [PATCH 47/50] Added seperate driver for translation and rotation stages --- lantz/drivers/motion/__init__.py | 20 + lantz/drivers/motion/axis.py | 177 ++++++ lantz/drivers/motion/motioncontroller.py | 105 ++++ lantz/drivers/newport_motion/motion.py | 551 +++++++------------ lantz/drivers/newport_motion/motionsmc100.py | 537 +++++++++++------- 5 files changed, 856 insertions(+), 534 deletions(-) create mode 100644 lantz/drivers/motion/__init__.py create mode 100644 lantz/drivers/motion/axis.py create mode 100644 lantz/drivers/motion/motioncontroller.py diff --git a/lantz/drivers/motion/__init__.py b/lantz/drivers/motion/__init__.py new file mode 100644 index 0000000..fa83208 --- /dev/null +++ b/lantz/drivers/motion/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.motion + ~~~~~~~~~~~~~~~~~~~~~ + + :company: Motion Controller + :description: General class for equipment that can translate or rotate + :website: + + --- + + :copyright: 2015, see AUTHORS for more details. + :license: GPLv3, + +""" + +from .motioncontroller import MotionControllerMultiAxis, MotionControllerSingleAxis +from .axis import MotionAxisSingle, MotionAxisMultiple, BacklashMixing + +__all__ = ['MotionControllerMultiAxis', 'MotionControllerSingleAxis', 'MotionAxisSingle', 'MotionAxisMultiple', 'BacklashMixing'] diff --git a/lantz/drivers/motion/axis.py b/lantz/drivers/motion/axis.py new file mode 100644 index 0000000..757eec5 --- /dev/null +++ b/lantz/drivers/motion/axis.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.newport.motion axis + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + General class that implements the commands used for motion + drivers + + :copyright: 2018, see AUTHORS for more details. + :license: GPL, see LICENSE for more details. + + +""" + + +from lantz.feat import Feat +from lantz.action import Action +from lantz.driver import Driver +from lantz import Q_ +from lantz.processors import convert_to +import time +import numpy as np + +# Add generic units: +# ureg.define('unit = unit') +# ureg.define('encodercount = encodercount') +# ureg.define('motorstep = motorstep') + + +class MotionAxisSingle(Driver): + def __init__(self, *args, **kwargs): + self.wait_time = 0.01 # in seconds * Q_(1, 's') + self.wait_until_done = True + self.accuracy = 0.001 # in units reported by axis + self._units = 'mm' + + @Feat() + def idn(self): + return self.query('ID?') + + @Feat() + def position(self): + raise NotImplementedError + + @position.setter + def position(self, pos): + """ + Waits until movement is done if self.wait_until_done = True. + + :param pos: new position + """ + self._set_position(pos, wait=self.wait_until_done) + + @Action(units=['mm', None]) + def _set_position(self, pos, wait=None): + """ + Move to an absolute position, taking into account backlash. + + When self.backlash is to a negative value the stage will always move + from low to high values. If necessary, a extra step with length + self.backlash is set. + + :param pos: New position in mm + :param wait: wait until stage is finished + """ + + # First do move to extra position if necessary + + self.__set_position(pos) + if wait: + self._wait_until_done() + self.check_position(pos) + + def __set_position(self, pos): + """ + Move stage to a certain position + :param pos: New position + """ + self.write('PA%f' % (pos)) + + @Action(units='mm') + def check_position(self, pos): + '''Check is stage is at expected position''' + if np.isclose(self.position, pos, atol=self.accuracy): + return True + self.log_error('Position accuracy {} is not reached.' + 'Expected: {}, measured: {}'.format(self.accuracy, + pos, + self.position)) + return False + + + @Feat(values={True: '1', False: '0'}) + def motion_done(self): + raise NotImplementedError + + @Feat() + def units(self): + return self._units + + @units.setter + def units(self, units): + super().update_units(self._units, units) + self._units = units + + def _wait_until_done(self): + wait_time = convert_to('seconds', on_dimensionless='ignore')(self.wait_time) + time.sleep(wait_time) + while not self.motion_done: + time.sleep(wait_time) + return True + + +class MotionAxisMultiple(MotionAxisSingle): + def __init__(self, parent, num, id, *args, **kwargs): + super().__init__(*args, **kwargs) + self.parent = parent + self.num = num + self._idn = id + + def __del__(self): + self.parent = None + self.num = None + + def query(self, command, *, send_args=(None, None), recv_args=(None, None)): + return self.parent.query('{:d}{}'.format(self.num, command), + send_args=send_args, recv_args=recv_args) + + def write(self, command, *args, **kwargs): + return self.parent.write('{:d}{}'.format(self.num, command), + *args, **kwargs) + +class BacklashMixing(): + '''Adds functionality to a motionaxis: blacklash correction''' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.backlash = 0 + + @Action(units=['mm', None]) + def _set_position(self, pos, wait=None): + """ + Move to an absolute position, taking into account backlash. + + When self.backlash is to a negative value the stage will always move + from low to high values. If necessary, a extra step with length + self.backlash is set. + + :param pos: New position in mm + :param wait: wait until stage is finished + """ + + # First do move to extra position if necessary + if self.backlash: + position = self.position.magnitude + backlash = convert_to(self.units, on_dimensionless='ignore' + )(self.backlash).magnitude + if (backlash < 0 and position > pos) or\ + (backlash > 0 and position < pos): + + self.log_info('Using backlash') + self.__set_position(pos + backlash) + self._wait_until_done() + + # Than move to final position + self.__set_position(pos) + if wait: + self._wait_until_done() + self.check_position(pos) + + def __set_position(self, pos): + """ + Move stage to a certain position + :param pos: New position + """ + self.write('PA%f' % (pos)) + self.last_set_position = pos \ No newline at end of file diff --git a/lantz/drivers/motion/motioncontroller.py b/lantz/drivers/motion/motioncontroller.py new file mode 100644 index 0000000..d572a72 --- /dev/null +++ b/lantz/drivers/motion/motioncontroller.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.newport.motion axis + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + General class that implements the commands used for motion + drivers + + :copyright: 2018, see AUTHORS for more details. + :license: GPL, see LICENSE for more details. + + +""" + +import time +import numpy as np + +from lantz.feat import Feat +from lantz.action import Action +from lantz.driver import Driver +from pyvisa import constants +from lantz import Q_, ureg +from lantz.processors import convert_to + +from .axis import MotionAxisSingle, MotionAxisMultiple + +# Add generic units: +# ureg.define('unit = unit') +# ureg.define('encodercount = count') +# ureg.define('motorstep = step') + + +class MotionControllerMultiAxis(Driver): + """ Motion controller that can detect multiple axis + + """ + def initialize(self): + super().initialize() + + @Feat() + def idn(self): + raise AttributeError('Not implemented') + + @Action() + def detect_axis(self): + """ Find the number of axis available. + + The detection stops as soon as an empty controller is found. + """ + pass + + @Action() + def get_errors(self): + raise AttributeError('Not implemented') + + @Feat(read_once=False) + def position(self): + return [axis.position for axis in self.axes] + + @Feat(read_once=False) + def _position_cached(self): + return [axis.recall('position') for axis in self.axes] + + @position.setter + def position(self, pos): + """Move to position (x,y,...)""" + return self._position(pos) + + @Action() + def _position(self, pos, read_pos=None, wait_until_done=True): + """Move to position (x,y,...)""" + if read_pos is not None: + self.log_error('kwargs read_pos for function _position is deprecated') + + for p, axis in zip(pos, self.axes): + if p is not None: + axis._set_position(p, wait=False) + if wait_until_done: + for p, axis in zip(pos, self.axes): + if p is not None: + axis._wait_until_done() + axis.check_position(p) + return self.position + + return pos + + @Action() + def motion_done(self): + for axis in self.axes: + axis._wait_until_done() + + def finalize(self): + for axis in self.axes: + if axis is not None: + del (axis) + super().finalize() + + + +class MotionControllerSingleAxis(MotionAxisSingle): + """ Motion controller that can only has sinlge axis + + """ + def initialize(self): + super().initialize() \ No newline at end of file diff --git a/lantz/drivers/newport_motion/motion.py b/lantz/drivers/newport_motion/motion.py index c7e1fee..8bad148 100644 --- a/lantz/drivers/newport_motion/motion.py +++ b/lantz/drivers/newport_motion/motion.py @@ -1,337 +1,214 @@ -# -*- coding: utf-8 -*- -""" - lantz.drivers.newport.motion axis - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - General class that implements the commands used for several newport motion - drivers - - :copyright: 2018, see AUTHORS for more details. - :license: GPL, see LICENSE for more details. - - Source: Instruction Manual (Newport) - -""" - - -from lantz.feat import Feat -from lantz.action import Action -from lantz.messagebased import MessageBasedDriver -from pyvisa import constants -from lantz import Q_, ureg -from lantz.processors import convert_to -import time -import numpy as np - -# Add generic units: -# ureg.define('unit = unit') -# ureg.define('encodercount = count') -# ureg.define('motorstep = step') - - -class MotionController(MessageBasedDriver): - """ Newport motion controller. It assumes all axes to have units mm - - """ - - DEFAULTS = { - 'COMMON': {'write_termination': '\r\n', - 'read_termination': '\r\n', }, - 'ASRL': { - 'timeout': 10, # ms - 'encoding': 'ascii', - 'data_bits': 8, - 'baud_rate': 57600, - 'parity': constants.Parity.none, - 'stop_bits': constants.StopBits.one, - 'flow_control': constants.VI_ASRL_FLOW_RTS_CTS, - }, - } - - def initialize(self): - super().initialize() - - @Feat() - def idn(self): - raise AttributeError('Not implemented') - # return self.query('ID?') - - @Action() - def detect_axis(self): - """ Find the number of axis available. - - The detection stops as soon as an empty controller is found. - """ - pass - - @Action() - def get_errors(self): - raise AttributeError('Not implemented') - - @Feat(read_once=False) - def position(self): - return [axis.position for axis in self.axes] - - @Feat(read_once=False) - def _position_cached(self): - return [axis.recall('position') for axis in self.axes] - - @position.setter - def position(self, pos): - """Move to position (x,y,...)""" - return self._position(pos) - - @Action() - def _position(self, pos, read_pos=None, wait_until_done=True): - """Move to position (x,y,...)""" - if read_pos is not None: - self.log_error('kwargs read_pos for function _position is deprecated') - - for p, axis in zip(pos, self.axes): - if p is not None: - axis._set_position(p, wait=False) - if wait_until_done: - for p, axis in zip(pos, self.axes): - if p is not None: - axis._wait_until_done() - axis.check_position(p) - return self.position - - return pos - - @Action() - def motion_done(self): - for axis in self.axes: - axis._wait_until_done() - - def finalize(self): - for axis in self.axes: - if axis is not None: - del (axis) - super().finalize() - - -class MotionAxis(MotionController): - def __init__(self, parent, num, id, *args, **kwargs): - self.parent = parent - self.num = num - self._idn = id - self.wait_time = 0.01 # in seconds * Q_(1, 's') - self.backlash = 0 - self.wait_until_done = True - self.accuracy = 0.001 # in units reported by axis - # Fill position cache: - self.position - self.last_set_position = self.position.magnitude - - def __del__(self): - self.parent = None - self.num = None - - def query(self, command, *, send_args=(None, None), recv_args=(None, None)): - return self.parent.query('{:d}{}'.format(self.num, command), - send_args=send_args, recv_args=recv_args) - - def write(self, command, *args, **kwargs): - return self.parent.write('{:d}{}'.format(self.num, command), - *args, **kwargs) - - @Feat() - def idn(self): - return self.query('ID?') - - @Action() - def on(self): - """Put axis on""" - self.write('MO') - - @Action() - def off(self): - """Put axis on""" - self.write('MF') - - @Feat(values={True: '1', False: '0'}) - def is_on(self): - """ - :return: True is axis on, else false - """ - return self.query('MO?') - - @Action(units='mm') - def define_home(self, val=0): - """Remap current position to home (0), or to new position - - :param val: new position""" - self.write('DH%f' % val) - - @Action() - def home(self): - """Execute the HOME command""" - self.write('OR') - - @Feat(units='mm') - def position(self): - return self.query('TP?') - - @position.setter - def position(self, pos): - """ - Waits until movement is done if self.wait_until_done = True. - - :param pos: new position - """ - if not self.is_on: - self.log_error('Axis not enabled. Not moving!') - return - - # First do move to extra position if necessary - self._set_position(pos, wait=self.wait_until_done) - - @Action(units=['mm', None]) - def _set_position(self, pos, wait=None): - """ - Move to an absolute position, taking into account backlash. - - When self.backlash is to a negative value the stage will always move - from low to high values. If necessary, a extra step with length - self.backlash is set. - - :param pos: New position in mm - :param wait: wait until stage is finished - """ - - # First do move to extra position if necessary - if self.backlash: - position = self.position.magnitude - backlash = convert_to('mm', on_dimensionless='ignore' - )(self.backlash).magnitude - if (backlash < 0 and position > pos) or\ - (backlash > 0 and position < pos): - - self.log_info('Using backlash') - self.__set_position(pos + backlash) - self._wait_until_done() - - # Than move to final position - self.__set_position(pos) - if wait: - self._wait_until_done() - self.check_position(pos) - - def __set_position(self, pos): - """ - Move stage to a certain position - :param pos: New position - """ - self.write('PA%f' % (pos)) - self.last_set_position = pos - - @Action(units='mm') - def check_position(self, pos): - '''Check is stage is at expected position''' - if np.isclose(self.position, pos, atol=self.accuracy): - return True - self.log_error('Position accuracy {} is not reached.' - 'Expected: {}, measured: {}'.format(self.accuracy, - pos, - self.position)) - return False - - @Feat(units='mm/s') - def max_velocity(self): - return float(self.query('VU?')) - - @max_velocity.setter - def max_velocity(self, velocity): - self.write('VU%f' % (velocity)) - - @Feat(units='mm/s**2') - def max_acceleration(self): - return float(self.query('AU?')) - - @max_acceleration.setter - def max_acceleration(self, velocity): - self.write('AU%f' % (velocity)) - - @Feat(units='mm/s') - def velocity(self): - return float(self.query('VA?')) - - @velocity.setter - def velocity(self, velocity): - """ - :param velocity: Set the velocity that the axis should use when moving - :return: - """ - self.write('VA%f' % (velocity)) - - @Feat(units='mm/s**2') - def acceleration(self): - return float(self.query('VA?')) - - @acceleration.setter - def acceleration(self, acceleration): - """ - :param acceleration: Set the acceleration that the axis should use - when starting - :return: - """ - self.write('AC%f' % (acceleration)) - - @Feat(units='mm/s') - def actual_velocity(self): - return float(self.query('TV')) - - @actual_velocity.setter - def actual_velocity(self, val): - raise NotImplementedError - - @Action() - def stop(self): - """Emergency stop""" - self.write('ST') - - @Feat(values={True: '1', False: '0'}) - def motion_done(self): - return self.query('MD?') - - # Not working yet, see https://github.com/hgrecco/lantz/issues/35 - # @Feat(values={Q_('encodercount'): 0, - # Q_('motor step'): 1, - # Q_('millimeter'): 2, - # Q_('micrometer'): 3, - # Q_('inches'): 4, - # Q_('milli-inches'): 5, - # Q_('micro-inches'): 6, - # Q_('degree'): 7, - # Q_('gradian'): 8, - # Q_('radian'): 9, - # Q_('milliradian'): 10, - # Q_('microradian'): 11}) - @Feat() - def units(self): - ret = int(self.query(u'SN?')) - vals = {0 :'encoder count', - 1 :'motor step', - 2 :'millimeter', - 3 :'micrometer', - 4 :'inches', - 5 :'milli-inches', - 6 :'micro-inches', - 7 :'degree', - 8 :'gradian', - 9 :'radian', - 10:'milliradian', - 11:'microradian',} - return vals[ret] - - # @units.setter - # def units(self, val): - # self.parent.write('%SN%' % (self.num, val)) - - def _wait_until_done(self): - # wait_time = convert_to('seconds', on_dimensionless='warn')(self.wait_time) - time.sleep(self.wait_time) - while not self.motion_done: - time.sleep(self.wait_time) - return True +# -*- coding: utf-8 -*- +""" + lantz.drivers.newport.motion axis + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + General class that implements the commands used for several newport motion + drivers + + :copyright: 2018, see AUTHORS for more details. + :license: GPL, see LICENSE for more details. + + Source: Instruction Manual (Newport) + +""" + + +from lantz.feat import Feat +from lantz.action import Action +from lantz.messagebased import MessageBasedDriver +from pyvisa import constants +from lantz import Q_, ureg +from lantz.processors import convert_to +from lantz.drivers.motion import MotionAxisMultiple, MotionControllerMultiAxis, BacklashMixing +import time +import numpy as np + +# Add generic units: +# ureg.define('unit = unit') +# ureg.define('encodercount = count') +# ureg.define('motorstep = step') + + +class MotionController(): + """ Newport motion controller. It assumes all axes to have units mm + + """ + print("newton_motion.MotionController is deprecated. Use lantz.drivers.motion.MotionControllerMultiAxis instead") + + +UNITS = {0: 'encoder count', + 1: 'motor step', + 2: 'millimeter', + 3: 'micrometer', + 4: 'inches', + 5: 'milli-inches', + 6: 'micro-inches', + 7: 'degree', + 8: 'gradian', + 9: 'radian', + 10: 'milliradian', + 11: 'microradian', } + + +class MotionAxis(MotionAxisMultiple, BacklashMixing): + def __del__(self): + self.parent = None + self.num = None + + def query(self, command, *, send_args=(None, None), recv_args=(None, None)): + return self.parent.query('{:d}{}'.format(self.num, command), + send_args=send_args, recv_args=recv_args) + + def write(self, command, *args, **kwargs): + return self.parent.write('{:d}{}'.format(self.num, command), + *args, **kwargs) + + @Feat() + def idn(self): + return self.query('ID?') + + @Action() + def on(self): + """Put axis on""" + self.write('MO') + + @Action() + def off(self): + """Put axis on""" + self.write('MF') + + @Feat(values={True: '1', False: '0'}) + def is_on(self): + """ + :return: True is axis on, else false + """ + return self.query('MO?') + + @Action(units='mm') + def define_home(self, val=0): + """Remap current position to home (0), or to new position + + :param val: new position""" + self.write('DH%f' % val) + + @Action() + def home(self): + """Execute the HOME command""" + self.write('OR') + + @Feat(units='mm') + def position(self): + return self.query('TP?') + + @position.setter + def position(self, pos): + """ + Waits until movement is done if self.wait_until_done = True. + + :param pos: new position + """ + if not self.is_on: + self.log_error('Axis not enabled. Not moving!') + return + + # First do move to extra position if necessary + self._set_position(pos, wait=self.wait_until_done) + + + def __set_position(self, pos): + """ + Move stage to a certain position + :param pos: New position + """ + self.write('PA%f' % (pos)) + self.last_set_position = pos + + @Feat(units='mm/s') + def max_velocity(self): + return float(self.query('VU?')) + + @max_velocity.setter + def max_velocity(self, velocity): + self.write('VU%f' % (velocity)) + + @Feat(units='mm/s**2') + def max_acceleration(self): + return float(self.query('AU?')) + + @max_acceleration.setter + def max_acceleration(self, velocity): + self.write('AU%f' % (velocity)) + + @Feat(units='mm/s') + def velocity(self): + return float(self.query('VA?')) + + @velocity.setter + def velocity(self, velocity): + """ + :param velocity: Set the velocity that the axis should use when moving + :return: + """ + self.write('VA%f' % (velocity)) + + @Feat(units='mm/s**2') + def acceleration(self): + return float(self.query('VA?')) + + @acceleration.setter + def acceleration(self, acceleration): + """ + :param acceleration: Set the acceleration that the axis should use + when starting + :return: + """ + self.write('AC%f' % (acceleration)) + + @Feat(units='mm/s') + def actual_velocity(self): + return float(self.query('TV')) + + @actual_velocity.setter + def actual_velocity(self, val): + raise NotImplementedError + + @Action() + def stop(self): + """Emergency stop""" + self.write('ST') + + @Feat(values={True: '1', False: '0'}) + def motion_done(self): + return self.query('MD?') + + # Not working yet, see https://github.com/hgrecco/lantz/issues/35 + # @Feat(values={Q_('encodercount'): 0, + # Q_('motor step'): 1, + # Q_('millimeter'): 2, + # Q_('micrometer'): 3, + # Q_('inches'): 4, + # Q_('milli-inches'): 5, + # Q_('micro-inches'): 6, + # Q_('degree'): 7, + # Q_('gradian'): 8, + # Q_('radian'): 9, + # Q_('milliradian'): 10, + # Q_('microradian'): 11}) + @Feat() + def units(self): + ret = int(self.query(u'SN?')) + return UNITS[ret] + + @units.setter + def units(self, val): + # No check implemented yet + self.write('%SN%' % (self.num, UNITS.index(val))) + super().units = val + + def _wait_until_done(self): + # wait_time = convert_to('seconds', on_dimensionless='warn')(self.wait_time) + time.sleep(self.wait_time) + while not self.motion_done: + time.sleep(self.wait_time) + return True diff --git a/lantz/drivers/newport_motion/motionsmc100.py b/lantz/drivers/newport_motion/motionsmc100.py index f38e438..127f7de 100644 --- a/lantz/drivers/newport_motion/motionsmc100.py +++ b/lantz/drivers/newport_motion/motionsmc100.py @@ -1,197 +1,340 @@ -# -*- coding: utf-8 -*- -""" - lantz.drivers.newport.motionsmc100 - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Implements the drivers to control SMC100 controller - - :copyright: 2018, see AUTHORS for more details. - :license: GPL, see LICENSE for more details. - - Source: Instruction Manual (Newport) - -""" - - -from lantz.feat import Feat -from lantz.action import Action -from pyvisa import constants -from lantz import Q_, ureg -from lantz.processors import convert_to -if __name__ == '__main__': - from motion import MotionController, MotionAxis -else: - from .motion import MotionController, MotionAxis -import time -import numpy as np - -ERRORS = {"@": "", - "A": "Unknown message code or floating point controller address.", - "B": "Controller address not correct.", - "C": "Parameter missing or out of range.", - "D": "Execution not allowed.", - "E": "home sequence already started.", - "I": "Execution not allowed in CONFIGURATION state.", - "J": "Execution not allowed in DISABLE state.", - "K": "Execution not allowed in READY state.", - "L": "Execution not allowed in HOMING state.", - "M": "Execution not allowed in MOVING state.",} - - -class SMC100(MotionController): - """ Newport SMC100 motion controller. It assumes all axes to have units mm - - - Example: - import numpy as np - import lantz - import visa - import lantz.drivers.pi.piezo as pi - from lantz.drivers.newport_motion import SMC100 - from pyvisa import constants - rm = visa.ResourceManager('@py') - lantz.messagebased._resource_manager = rm - ureg = lantz.ureg - try: - lantzlog - except NameError: - lantzlog = lantz.log.log_to_screen(level=lantz.log.DEBUG) - lantz.log.log_to_socket(level=lantz.log.DEBUG) - - import time - import numpy as np - import warnings - #warnings.filterwarnings(action='ignore') - print(lantz.messagebased._resource_manager.list_resources()) - stage = SMC100('ASRL/dev/ttyUSB0::INSTR') - stage.initialize() - axis0 = stage.axes[0] - print('Axis id:' + axis0.idn) - print('Axis position: {}'.format(axis0.position)) - axis0.keypad_disable() - axis0.position += 0.1 * ureg.mm - print('Errors: {}'.format(axis0.get_errors())) - stage.finalize() - """ - - DEFAULTS = { - 'COMMON': {'write_termination': '\r\n', - 'read_termination': '\r\n', }, - 'ASRL': { - 'timeout': 100, # ms - 'encoding': 'ascii', - 'data_bits': 8, - 'baud_rate': 57600, - 'parity': constants.Parity.none, - 'stop_bits': constants.StopBits.one, - #'flow_control': constants.VI_ASRL_FLOW_NONE, - 'flow_control': constants.VI_ASRL_FLOW_XON_XOFF, # constants.VI_ASRL_FLOW_NONE, - }, - } - - def __init__(self, *args, **kwargs): - self.motionaxis_class = kwargs.pop('motionaxis_class', MotionAxisSMC100) - super().__init__(*args, **kwargs) - - def initialize(self): - super().initialize() - self.detect_axis() - - @Action() - def detect_axis(self): - """ Find the number of axis available. - - The detection stops as soon as an empty controller is found. - """ - self.axes = [] - i = 0 - scan_axes = True - while scan_axes: - i += 1 - id = self.query('%dID?' % i) - if id == '': - scan_axes = False - else: - axis = self.motionaxis_class(self, i, id) - self.axes.append(axis) - - - - - -class MotionAxisSMC100(MotionAxis): - def query(self, command, *, send_args=(None, None), recv_args=(None, None)): - respons = super().query(command,send_args=send_args, recv_args=recv_args) - #check for command: - if not respons[:3] == '{:d}{}'.format(self.num, command[:2]): - self.log_error('Axis {}: Expected to return command {} instead of {}'.format(self.num, command[:3],respons[:3])) - return respons[3:] - - def write(self, command, *args, **kwargs): - super().write(command, *args, **kwargs) - return self.get_errors() - - @Action() - def on(self): - """Put axis on""" - pass - - @Action() - def off(self): - """Put axis on""" - pass - - @Action() - def get_errors(self): - ret = self.query('TE?') - err = ERRORS.get(ret, 'Error {}. Lookup in manual: https://www.newport.com/medias/sys_master/images/images/h11/he1/9117182525470/SMC100CC-SMC100PP-User-s-Manual.pdf'.format(ret)) - if err: - self.log_error('Axis {} error: {}'.format(self.num, err)) - return err - - @Feat(values={True: '1', False: '0'}) - def is_on(self): - """ - :return: True is axis on, else false - """ - return '1' - - @Feat() - def motion_done(self): - return self.check_position(self.last_set_position) - - @Action() - def keypad_disable(self): - return self.write('JD') - - -if __name__ == '__main__': - import argparse - import lantz.log - - parser = argparse.ArgumentParser(description='Test SMC100 driver') - parser.add_argument('-p', '--port', type=str, default='1', - help='Serial port to connect to') - - args = parser.parse_args() - lantzlog = lantz.log.log_to_screen(level=lantz.log.INFO) - lantz.log.log_to_socket(lantz.log.DEBUG) - - import lantz - import visa - import lantz.drivers.newport_motion - sm = lantz.drivers.newport_motion.SMC100 - rm = visa.ResourceManager('@py') - lantz.messagebased._resource_manager = rm - - print(lantz.messagebased._resource_manager.list_resources()) - - with sm(args.port) as inst: - #with sm.via_serial(port=args.port) as inst: - inst.idn - # inst.initialize() # Initialize the communication with the power meter - # Find the status of all axes: - #for axis in inst.axes: - # print('Axis {} Position {} is_on {} max_velocity {} velocity {}'.format(axis.num, axis.position, - # axis.is_on, axis.max_velocity, - # axis.velocity)) +# -*- coding: utf-8 -*- +""" + lantz.drivers.newport.motionsmc100 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implements the drivers to control SMC100 controller + + :copyright: 2018, see AUTHORS for more details. + :license: GPL, see LICENSE for more details. + + Source: Instruction Manual (Newport) + +""" + + +from lantz.feat import Feat +from lantz.action import Action +import pyvisa +from pyvisa import constants + +from lantz import Q_, ureg +from lantz.processors import convert_to +from lantz.messagebased import MessageBasedDriver + +if __name__ == '__main__': + from motion import MotionController, MotionAxis +else: + from .motion import MotionController, MotionAxis +import time +import numpy as np + +ERRORS = {"@": "", + "A": "Unknown message code or floating point controller address.", + "B": "Controller address not correct.", + "C": "Parameter missing or out of range.", + "D": "Execution not allowed.", + "E": "home sequence already started.", + "I": "Execution not allowed in CONFIGURATION state.", + "J": "Execution not allowed in DISABLE state.", + "H": "Execution not allowed in NOT REFERENCED state.", + "K": "Execution not allowed in READY state.", + "L": "Execution not allowed in HOMING state.", + "M": "Execution not allowed in MOVING state.", + } + +positioner_errors = { + 0b1000000000: '80 W output power exceeded', + 0b0100000000: 'DC voltage too low', + 0b0010000000: 'Wrong ESP stage', + 0b0001000000: 'Homing time out', + 0b0000100000: 'Following error', + 0b0000010000: 'Short circuit detection', + 0b0000001000: 'RMS current limit', + 0b0000000100: 'Peak current limit', + 0b0000000010: 'Positive end of run', + 0b0000000001: 'Negative end of run', + } +controller_states = { + '0A': 'NOT REFERENCED from reset.', + '0B': 'NOT REFERENCED from HOMING.', + '0C': 'NOT REFERENCED from CONFIGURATION.', + '0D': 'NOT REFERENCED from DISABLE.', + '0E': 'NOT REFERENCED from READY.', + '0F': 'NOT REFERENCED from MOVING.', + '10': 'NOT REFERENCED ESP stage error.', + '11': 'NOT REFERENCED from JOGGING.', + '14': 'CONFIGURATION.', + '1E': 'HOMING commanded from RS-232-C.', + '1F': 'HOMING commanded by SMC-RC.', + '28': 'MOVING.', + '32': 'READY from HOMING.', + '33': 'READY from MOVING.', + '34': 'READY from DISABLE.', + '35': 'READY from JOGGING.', + '3C': 'DISABLE from READY.', + '3D': 'DISABLE from MOVING.', + '3E': 'DISABLE from JOGGING.', + '46': 'JOGGING from READY.', + '47': 'JOGGING from DISABLE.', + } + + +class SMC100(MessageBasedDriver, MotionAxis): + """ Newport SMC100 motion controller. It assumes all axes to have units mm + + + Example: + import numpy as np + import lantz + import visa + import lantz.drivers.pi.piezo as pi + from lantz.drivers.newport_motion import SMC100 + from pyvisa import constants + rm = visa.ResourceManager('@py') + lantz.messagebased._resource_manager = rm + ureg = lantz.ureg + try: + lantzlog + except NameError: + lantzlog = lantz.log.log_to_screen(level=lantz.log.DEBUG) + lantz.log.log_to_socket(level=lantz.log.DEBUG) + + import time + import numpy as np + import warnings + #warnings.filterwarnings(action='ignore') + print(lantz.messagebased._resource_manager.list_resources()) + stage = SMC100('ASRL/dev/ttyUSB0::INSTR') + stage.initialize() + axis0 = stage.axes[0] + print('Axis id:' + axis0.idn) + print('Axis position: {}'.format(axis0.position)) + axis0.keypad_disable() + axis0.position += 0.1 * ureg.mm + print('Errors: {}'.format(axis0.get_errors())) + stage.finalize() + """ + + DEFAULTS = { + 'COMMON': {'write_termination': '\r\n', + 'read_termination': '\r\n', }, + 'ASRL': { + 'timeout': 100, # ms + 'encoding': 'ascii', + 'data_bits': 8, + 'baud_rate': 57600, + 'parity': constants.Parity.none, + 'stop_bits': constants.StopBits.one, + #'flow_control': constants.VI_ASRL_FLOW_NONE, + 'flow_control': constants.VI_ASRL_FLOW_XON_XOFF, # constants.VI_ASRL_FLOW_NONE, + }, + } + + def __init__(self, *args, **kwargs): + self.motionaxis_class = kwargs.pop('motionaxis_class', MotionAxisSMC100) + super().__init__(*args, **kwargs) + + def initialize(self): + super().initialize() + + # Clear read buffer + self.clear_read_buffer() + + self.detect_axis() + + @Action() + def detect_axis(self): + """ Find the number of axis available. + + The detection stops as soon as an empty controller is found. + """ + self.axes = [] + i = 0 + scan_axes = True + while scan_axes: + i += 1 + try: + idn = self.query('%dID?' % i) + except pyvisa.errors.VisaIOError: + scan_axes = False + else: + if idn == '': + scan_axes = False + else: + axis = self.motionaxis_class(self, i, idn) + self.axes.append(axis) + + + + +class MotionAxisSMC100(MotionAxis): + def query(self, command, *, send_args=(None, None), + recv_args=(None, None)): + respons = super().query(command, send_args=send_args, + recv_args=recv_args) + # check for command: + if not respons[:3] == '{:d}{}'.format(self.num, command[:2]): + self.log_error('Axis {}: Expected to return command {} instead of' + '{}'.format(self.num, command[:3], respons[:3])) + return respons[3:] + + def write(self, command, *args, **kwargs): + super().write(command, *args, **kwargs) + return self.get_errors() + + @Feat(units='mm') + def software_limit_positive(self): + '''Make sure that software limits are tighter than hardware limits, + else the stage will go to not reference mode''' + return self.query('SR?') + + @software_limit_positive.setter + def software_limit_positive(self, val): + return self._software_limit_setter(val, limit='positive') + + @Feat(units='mm') + def software_limit_negative(self): + return self.query('SL?') + + @software_limit_negative.setter + def software_limit_negative(self, val): + return self._software_limit_setter(val, limit='negative') + + def _software_limit_setter(self, val, limit='positive'): + self.enter_config_state() + if limit == 'positive': + ret = self.write('SR{}'.format(val)) + elif limit == 'negative': + ret = self.write('SL{}'.format(val)) + else: + self.log_error("Limit {} not in ('postive', 'negative')." + "".format(limit)) + self.leave_and_save_config_state() + return ret + + @Action() + def enter_config_state(self): + return self.write('PW1') + + @Action() + def leave_and_save_config_state(self): + '''Takes up to 10s, controller is unresposive in that time''' + super().write('PW0') + start = time.time() + # do-while loop + cont = True + while cont: + try: + self.status + except ValueError: + if (time.time() - start > 10): + self.log_error('Controller was going to CONFIGURATION ' + 'state but it took more than 10s. Trying ' + 'to continue anyway') + cont = False + else: + time.sleep(0.001) + else: + cont = False + + @Action() + def on(self): + """Put axis on""" + pass + self.write('MM1') + + @Action() + def off(self): + """Put axis off""" + pass + self.write('MM0') + + @Action() + def get_errors(self): + ret = self.query('TE?') + err = ERRORS.get(ret, 'Error {}. Lookup in manual: https://www.newpor' + 't.com/medias/sys_master/images/images/h11/he1/91171' + '82525470/SMC100CC-SMC100PP-User-s-Manual.pdf' + ''.format(ret)) + if err: + self.log_error('Axis {} error: {}'.format(self.num, err)) + return err + + @Feat() + def status(self): + '''Read and parse controller and axis status. This gives usefull error + messages''' + res = self.query('TS?') + positioner_error = [val for key, val in positioner_errors.items() if + int(res[:4], base=16) & key == key] + controller_state = controller_states[res[-2:]] + return positioner_error, controller_state + + @Feat(values={True: '1', False: '0'}) + def is_on(self): + """ + :return: True is axis on, else false + """ + return '1' + # return self.query('MM?') + + @Action() + def home(self): + super().home() + self._wait_until_done() + + def _wait_until_done(self): + er, st = self.status + if st == 'MOVING.': + time.sleep(self.wait_time) + return self._wait_until_done() + elif st[:5] == 'READY': + return True + else: + self.log_error('Not reached position. Controller state: {} ' + 'Positioner errors: {}' + ''.format(st, ','.join(er))) + return False + + @Feat() + def motion_done(self): + if self.status[1][:5] == 'READY': + return True + return False + + @Action() + def keypad_disable(self): + return self.write('JD') + + +if __name__ == '__main__': + import argparse + import lantz.log + + parser = argparse.ArgumentParser(description='Test SMC100 driver') + parser.add_argument('-p', '--port', type=str, default='1', + help='Serial port to connect to') + + args = parser.parse_args() + lantzlog = lantz.log.log_to_screen(level=lantz.log.INFO) + lantz.log.log_to_socket(lantz.log.DEBUG) + + import lantz + import visa + import lantz.drivers.newport_motion + sm = lantz.drivers.newport_motion.SMC100 + rm = visa.ResourceManager('@py') + lantz.messagebased._resource_manager = rm + + print(lantz.messagebased._resource_manager.list_resources()) + + with sm(args.port) as inst: + #with sm.via_serial(port=args.port) as inst: + inst.idn + # inst.initialize() # Initialize the communication with the power meter + # Find the status of all axes: + #for axis in inst.axes: + # print('Axis {} Position {} is_on {} max_velocity {} velocity {}'.format(axis.num, axis.position, + # axis.is_on, axis.max_velocity, + # axis.velocity)) From a3b515daa6aefba033aeceb172508b85c33fb046 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Tue, 11 Dec 2018 17:25:39 +0100 Subject: [PATCH 48/50] Fixed classes newport motion --- lantz/drivers/newport_motion/motionsmc100.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lantz/drivers/newport_motion/motionsmc100.py b/lantz/drivers/newport_motion/motionsmc100.py index 127f7de..d942fd4 100644 --- a/lantz/drivers/newport_motion/motionsmc100.py +++ b/lantz/drivers/newport_motion/motionsmc100.py @@ -22,10 +22,9 @@ from lantz.processors import convert_to from lantz.messagebased import MessageBasedDriver -if __name__ == '__main__': - from motion import MotionController, MotionAxis -else: - from .motion import MotionController, MotionAxis +from lantz.drivers.newport_motion.motion import MotionAxis +from lantz.drivers.motion import MotionControllerMultiAxis + import time import numpy as np @@ -80,7 +79,7 @@ } -class SMC100(MessageBasedDriver, MotionAxis): +class SMC100(MessageBasedDriver, MotionControllerMultiAxis): """ Newport SMC100 motion controller. It assumes all axes to have units mm From 6218d11ff75a3b5056d8b5d0fbca4f9f95affb73 Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Tue, 11 Dec 2018 17:28:20 +0100 Subject: [PATCH 49/50] Fixed classes newport motion --- lantz/drivers/newport_motion/motionsmc100.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lantz/drivers/newport_motion/motionsmc100.py b/lantz/drivers/newport_motion/motionsmc100.py index d942fd4..f2f872c 100644 --- a/lantz/drivers/newport_motion/motionsmc100.py +++ b/lantz/drivers/newport_motion/motionsmc100.py @@ -142,6 +142,15 @@ def initialize(self): self.detect_axis() + @Action() + def clear_read_buffer(self): + '''Read all data that was still in the read buffer and discard this''' + try: + while True: + self.read() + except pyvisa.errors.VisaIOError: + pass # readbuffer was empty already + @Action() def detect_axis(self): """ Find the number of axis available. From 0a23e8827fcea5e415821397d7829221d471accd Mon Sep 17 00:00:00 2001 From: Vasco Tenner Date: Wed, 12 Dec 2018 19:40:49 +0100 Subject: [PATCH 50/50] First version of driver for Smaract piezo stages --- lantz/drivers/smaract/smaract_motion.py | 233 ++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 lantz/drivers/smaract/smaract_motion.py diff --git a/lantz/drivers/smaract/smaract_motion.py b/lantz/drivers/smaract/smaract_motion.py new file mode 100644 index 0000000..ff85f31 --- /dev/null +++ b/lantz/drivers/smaract/smaract_motion.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- +""" + lantz.drivers.newport.motion axis + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + General class that implements the commands used for several smaract motion + drivers using the ASCII mode (via serial or serial via USB). + + :copyright: 2018, see AUTHORS for more details. + :license: GPL, see LICENSE for more details. + + Source: Instruction Manual (Newport) + +""" + + +from lantz.feat import Feat +from lantz.action import Action +from lantz.messagebased import MessageBasedDriver +from pyvisa import constants +from lantz import Q_, ureg +from lantz.processors import convert_to +from lantz.drivers.motion import MotionAxisMultiple, MotionControllerMultiAxis, BacklashMixing +import time +import numpy as np + +formats = {'one_param': ''} + +class SCU(MessageBasedDriver, MotionControllerMultiAxis): + """ Driver for SCU controller with multiple axis + + """ + DEFAULTS = { + 'COMMON': {'write_termination': '\n', + 'read_termination': '\n', }, + 'ASRL': { + 'timeout': 100, # ms + 'encoding': 'ascii', + 'data_bits': 8, + 'baud_rate': 9600, + 'parity': constants.Parity.none, + 'stop_bits': constants.StopBits.one, + #'flow_control': constants.VI_ASRL_FLOW_NONE, + 'flow_control': constants.VI_ASRL_FLOW_XON_XOFF, # constants.VI_ASRL_FLOW_NONE, + }, + } + + def initialize(self): + super().initialize() + self.detect_axis() + + def query(self, command, *, send_args=(None, None), recv_args=(None, None)): + return MotionControllerMultiAxis.query(self, ':{}'.format(command), + send_args=send_args, recv_args=recv_args) + + def write(self, command, *args, **kwargs): + return MotionControllerMultiAxis.write(self,':{}'.format(command), + *args, **kwargs) + + @Feat() + def idn(self): + return self.parse_query('I', format='I{:s}') + + @Action() + def detect_axis(self): + """ Find the number of axis available. + + The detection stops as soon as an empty controller is found. + """ + pass + + +class MotionAxis(MotionAxisMultiple, BacklashMixing): + def __del__(self): + self.parent = None + self.num = None + + def query(self, command, *, send_args=(None, None), recv_args=(None, None)): + return self.parent.query('{:d}{}'.format(self.num, command), + send_args=send_args, recv_args=recv_args) + + def write(self, command, *args, **kwargs): + return self.parent.write('{:d}{}'.format(self.num, command), + *args, **kwargs) + + @Feat() + def idn(self): + return self.query('ID?') + + @Action() + def on(self): + """Put axis on""" + self.write('MO') + + @Action() + def off(self): + """Put axis on""" + self.write('MF') + + @Feat(values={True: '1', False: '0'}) + def is_on(self): + """ + :return: True is axis on, else false + """ + return self.query('MO?') + + @Action(units='mm') + def define_home(self, val=0): + """Remap current position to home (0), or to new position + + :param val: new position""" + self.write('DH%f' % val) + + @Action() + def home(self): + """Execute the HOME command""" + self.write('OR') + + @Feat(units='mm') + def position(self): + return self.query('TP?') + + @position.setter + def position(self, pos): + """ + Waits until movement is done if self.wait_until_done = True. + + :param pos: new position + """ + if not self.is_on: + self.log_error('Axis not enabled. Not moving!') + return + + # First do move to extra position if necessary + self._set_position(pos, wait=self.wait_until_done) + + + def __set_position(self, pos): + """ + Move stage to a certain position + :param pos: New position + """ + self.write('PA%f' % (pos)) + self.last_set_position = pos + + @Feat(units='mm/s') + def max_velocity(self): + return float(self.query('VU?')) + + @max_velocity.setter + def max_velocity(self, velocity): + self.write('VU%f' % (velocity)) + + @Feat(units='mm/s**2') + def max_acceleration(self): + return float(self.query('AU?')) + + @max_acceleration.setter + def max_acceleration(self, velocity): + self.write('AU%f' % (velocity)) + + @Feat(units='mm/s') + def velocity(self): + return float(self.query('VA?')) + + @velocity.setter + def velocity(self, velocity): + """ + :param velocity: Set the velocity that the axis should use when moving + :return: + """ + self.write('VA%f' % (velocity)) + + @Feat(units='mm/s**2') + def acceleration(self): + return float(self.query('VA?')) + + @acceleration.setter + def acceleration(self, acceleration): + """ + :param acceleration: Set the acceleration that the axis should use + when starting + :return: + """ + self.write('AC%f' % (acceleration)) + + @Feat(units='mm/s') + def actual_velocity(self): + return float(self.query('TV')) + + @actual_velocity.setter + def actual_velocity(self, val): + raise NotImplementedError + + @Action() + def stop(self): + """Emergency stop""" + self.write('ST') + + @Feat(values={True: '1', False: '0'}) + def motion_done(self): + return self.query('MD?') + + # Not working yet, see https://github.com/hgrecco/lantz/issues/35 + # @Feat(values={Q_('encodercount'): 0, + # Q_('motor step'): 1, + # Q_('millimeter'): 2, + # Q_('micrometer'): 3, + # Q_('inches'): 4, + # Q_('milli-inches'): 5, + # Q_('micro-inches'): 6, + # Q_('degree'): 7, + # Q_('gradian'): 8, + # Q_('radian'): 9, + # Q_('milliradian'): 10, + # Q_('microradian'): 11}) + @Feat() + def units(self): + ret = int(self.query(u'SN?')) + return UNITS[ret] + + @units.setter + def units(self, val): + # No check implemented yet + self.write('%SN%' % (self.num, UNITS.index(val))) + super().units = val + + def _wait_until_done(self): + # wait_time = convert_to('seconds', on_dimensionless='warn')(self.wait_time) + time.sleep(self.wait_time) + while not self.motion_done: + time.sleep(self.wait_time) + return True