From e94dee0c4bd68c5b2e3ada8dd86fd43e5f24ff54 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 15 Mar 2012 16:16:38 -0400 Subject: [PATCH] Add DigitalPort, Stepper, and Temperature classes. Also flesh out the config classes and handling. This brings pyafm up to speed with the new pypiezo, with respect to config- or instance-lead initialization. --- pyafm/afm.py | 131 +++++++++++++++++++++++++++++++++++++++++- pyafm/config.py | 129 +++++++++++++++++++++++++++++++++++++++-- pyafm/digital_port.py | 65 +++++++++++++++++++++ pyafm/stepper.py | 65 +++++++++++++++++++++ pyafm/temperature.py | 54 +++++++++++++++-- 5 files changed, 433 insertions(+), 11 deletions(-) create mode 100644 pyafm/digital_port.py create mode 100644 pyafm/stepper.py diff --git a/pyafm/afm.py b/pyafm/afm.py index 284349a..2681b56 100644 --- a/pyafm/afm.py +++ b/pyafm/afm.py @@ -23,6 +23,7 @@ for controlling the piezo (`pypiezo`) and stepper (`stepper`), this module only contains methods that require the capabilities of both. """ +from pypiezo.afm import AFMPiezo as _AFMPiezo from pypiezo.base import convert_bits_to_meters as _convert_bits_to_meters from pypiezo.base import convert_meters_to_bits as _convert_meters_to_bits from pypiezo.base import convert_volts_to_bits as _convert_volts_to_bits @@ -30,6 +31,8 @@ from pypiezo.surface import FlatFit as _FlatFit from pypiezo.surface import SurfaceError as _SurfaceError from . import LOG as _LOG +from .stepper import Stepper as _Stepper +from .temperature import Temperature as _Temperature class AFM (object): @@ -46,12 +49,136 @@ class AFM (object): Coarse positioning. temperature | temperature.Controller instance or None Optional temperature monitoring and control. + + >>> from pycomedi.device import Device + >>> from pycomedi import constant as _constant + >>> import pypiezo.config as _pypiezo_config + >>> import pyafm.config as _config + + >>> device = Device('/dev/comedi0') + >>> device.open() + + >>> config = _config.AFMConfig() + >>> config['piezo'] = _pypiezo_config.PiezoConfig() + >>> config['piezo']['name'] = 'test piezo' + >>> config['piezo']['axes'] = [_pypiezo_config.AxisConfig()] + >>> config['piezo']['axes'][0]['channel'] = ( + ... _pypiezo_config.OutputChannelConfig()) + >>> config['piezo']['axes'][0]['channel']['name'] = 'z' + >>> config['piezo']['inputs'] = [_pypiezo_config.InputChannelConfig()] + >>> config['piezo']['inputs'][0]['name'] = 'deflection' + >>> config['stepper'] = _config.StepperConfig() + >>> config['stepper']['port'] = _config.DigitalPortConfig() + >>> config['stepper']['port']['channels'] = [1, 2, 3, 4] + >>> config['stepper']['port']['direction'] = _constant.IO_DIRECTION.output + >>> config['stepper']['port']['name'] = 'stepper port' + >>> config['stepper']['name'] = 'test stepper' + >>> config['temperature'] = _config.TemperatureConfig() + >>> config['temperature']['name'] = 'test temperature' + + >>> afm = AFM(config=config, devices=[device]) + >>> afm.setup_config() + + >>> afm.get_temperature() # doctest: +SKIP + 297.37 + + >>> print(afm.config.dump()) # doctest: +REPORT_UDIFF + name: + main-axis: + piezo: + name: test piezo + axes: + 0: + gain: 1.0 + sensitivity: 1.0 + minimum: -10.0 + maximum: 10.0 + channel: + name: z + device: /dev/comedi0 + subdevice: 1 + channel: 0 + maxdata: 65535 + range: 0 + analog-reference: ground + conversion-coefficients: -10.0,0.000305180437934 + conversion-origin: 0.0 + inverse-conversion-coefficients: 0.0,3276.75 + inverse-conversion-origin: -10.0 + monitor: + inputs: + 0: + name: deflection + device: /dev/comedi0 + subdevice: 0 + channel: 0 + maxdata: 65535 + range: 0 + analog-reference: ground + conversion-coefficients: -10.0,0.000305180437934 + conversion-origin: 0.0 + inverse-conversion-coefficients: 0.0,3276.75 + inverse-conversion-origin: -10.0 + stepper: + name: test stepper + full-step: yes + logic: yes + delay: 0.01 + step-size: 1.7e-07 + backlash: 100 + port: + name: stepper port + device: /dev/comedi0 + subdevice: 2 + subdevice-type: dio + channels: 1,2,3,4 + direction: output + temperature: + name: test temperature + units: Celsius + controller: 1 + device: /dev/ttyS0 + baudrate: 9600 + max-current: 0.0 + far: 3e-05 + + It's hard to test anything else without pugging into an actual AFM. + + >>> device.close() """ - def __init__(self, piezo, stepper, temperature=None, axis_name='z'): + def __init__(self, config, piezo=None, stepper=None, temperature=None, + devices=None): + self.config = config self.piezo = piezo self.stepper = stepper self.temperature = temperature - self.axis_name = axis_name + self.load_from_config(devices=devices) + + def load_from_config(self, devices): + c = self.config # reduce verbosity + if self.piezo is None and c['piezo']: + self.piezo = _AFMPiezo(config=c['piezo'], devices=devices) + if self.stepper is None and c['stepper']: + self.stepper = _Stepper(config=c['stepper'], devices=devices) + if self.temperature is None and c['temperature']: + self.temperature = _Temperature(config=c['temperature']) + + def setup_config(self): + if self.piezo: + self.piezo.setup_config() + self.config['piezo'] = self.piezo.config + else: + self.config['piezo'] = None + if self.stepper: + self.stepper.setup_config() + self.config['stepper'] = self.stepper.config + else: + self.config['stepper'] = None + if self.temperature: + self.temperature.setup_config() + self.config['temperature'] = self.temperature.config + else: + self.config['temperature'] = None def get_temperature(self): """Measure the sample temperature. diff --git a/pyafm/config.py b/pyafm/config.py index 1391556..c3af39a 100644 --- a/pyafm/config.py +++ b/pyafm/config.py @@ -19,6 +19,8 @@ import h5config.config as _config import h5config.tools as _h5config_tools +import pycomedi.constant as _constant +import pypiezo.config as _pypiezo_config class PackageConfig (_h5config_tools.PackageConfig): @@ -39,8 +41,12 @@ class Kelvin (_TemperatureUnit): class TemperatureConfig (_config.Config): - "Configure `calibcant` temperature operation" + "Configure a temperature monitor" settings = [ + _config.Setting( + name='name', + help='Monitor name (so the user will know what is measured).', + default=None), _config.ChoiceSetting( name='units', help='Units of raw temperature measurements.', @@ -49,9 +55,124 @@ class TemperatureConfig (_config.Config): ('Celsius', Celsius), ('Kelvin', Kelvin), ]), + _config.IntegerSetting( + name='controller', + help='MTCA controller ID.', + default=1), + _config.Setting( + name='device', + help="Serial port you're using to connect to the controller.", + default='/dev/ttyS0'), + _config.IntegerSetting( + name='baudrate', + help="Baud rate for which you've configured your controller.", + default=9600), + _config.IntegerSetting( + name='max-current', + help="Maxium current (in amps) output by the controller.", + default=0), + ] + + +class DigitalPortConfig (_config.Config): + "Configure a digital input/output port." + settings = [ + _config.Setting( + name='name', + help="Port name (so the user will know what it's used for).", + default=None), + _config.Setting( + name='device', + help='Comedi device.', + default='/dev/comedi0'), + _config.IntegerSetting( + name='subdevice', + help='Comedi subdevice index. -1 for automatic detection.', + default=-1), + _config.ChoiceSetting( + name='subdevice-type', + help='Comedi subdevice type for autodetection.', + choices=[(x.name, x) for x in _constant.SUBDEVICE_TYPE], + default=_constant.SUBDEVICE_TYPE.dio), + _config.IntegerListSetting( + name='channels', + help='Subdevice channels to control by index.', + default=[0]), + _config.ChoiceSetting( + name='direction', + help='Port direction.', + choices=[(x.name, x) for x in _constant.IO_DIRECTION]), + ] + + +class StepperConfig (_config.Config): + "Configure a stepper motor." + settings = [ + _config.Setting( + name='name', + help="Motor name (so the user will know what it's used for).", + default=None), + _config.BooleanSetting( + name='full-step', + help='Place the stepper in full-step mode (vs. half-step)', + default=True), _config.BooleanSetting( - name='default', - help=('The temperature values are defaults (vs. real ' - 'measurements).'), + name='logic', + help='Place the stepper in active-high mode (vs. active-low)', default=True), + _config.FloatSetting( + name='delay', + help=('Time delay between steps in seconds, in case the motor ' + 'response is slower that the digital output driver.'), + default=1e-2), + _config.FloatSetting( + name='step-size', + help= 'Approximate step size in meters.' , + default=170e-9), + _config.IntegerSetting( + name='backlash', + help= 'Generous estimate of the backlash length in half-steps.', + default=100), + _config.ConfigSetting( + name='port', + help=('Configure the digital port used to communicate with the ' + 'stepper.'), + config_class=DigitalPortConfig, + default=None), + ] + + +class AFMConfig (_config.Config): + "Configure an Atomic Force Microscope (AFM)." + settings = [ + _config.Setting( + name='name', + help="AFM name (so the user will know what it's used for).", + default=None), + _config.Setting( + name='main-axis', + help=("Name of the piezo axis controlling distance from the " + "surface."), + default=None), + _config.ConfigSetting( + name='piezo', + help='Configure the underlying piezo (fine adjustment).', + config_class=_pypiezo_config.PiezoConfig, + default=None), + _config.ConfigSetting( + name='stepper', + help='Configure the underlying stepper motor (coarse adjustment).', + config_class=StepperConfig, + default=None), + _config.ConfigSetting( + name='temperature', + help='Configure the underlying temperature sensor.', + config_class=TemperatureConfig, + default=None), + _config.FloatSetting( + name='far', + help=('Approximate distance in meters to move away to get "far" ' + 'from the surface. For possible stepper adjustments while ' + 'initially locating the surface.'), + default=3e-5), ] diff --git a/pyafm/digital_port.py b/pyafm/digital_port.py new file mode 100644 index 0000000..bdce8f8 --- /dev/null +++ b/pyafm/digital_port.py @@ -0,0 +1,65 @@ +# Copyright + +from pycomedi.channel import DigitalChannel as _DigitalChannel +import pypiezo.base as _base + + +class DigitalPort (object): + """A digital input/output port (i.e. cluster of channels). + + >>> from pycomedi.device import Device + >>> from pyafm.config import DigitalPortConfig + + >>> device = Device('/dev/comedi0') + >>> device.open() + + >>> config = DigitalPortConfig() + >>> config['channels'] = [1, 2, 3, 4] + >>> config['name'] = 'test port' + + >>> port = DigitalPort(config=config, devices=[device]) + >>> port.write_bitfield(13) + >>> port.write([1, 0, 1, 0]) + >>> port.write_bitfield(0) + + >>> device.close() + """ + def __init__(self, config, devices=None): + self.config = config + self.subdevice = None + self.load_from_config(devices=devices) + + def load_from_config(self, devices): + c = self.config # reduce verbosity + if self.subdevice is None: + device = _base.load_device(filename=c['device'], devices=devices) + if c['subdevice'] < 0: + self.subdevice = device.find_subdevice_by_type( + c['subdevice-type']) + else: + self.subdevice = device.subdevice(index=c['subdevice']) + self.channels = [] + self.write_mask = 0 + for index in c['channels']: + channel = self.subdevice.channel( + index=index, factory=_DigitalChannel) + channel.dio_config(c['direction']) + self.write_mask &= 1 << index + self.channels.append(channel) + self.name = c['name'] + + def setup_config(self): + self.config['device'] = self.subdevice.device.filename + self.config['subdevice'] = self.subdevice.index + self.config['channels'] = [c.index for c in self.channels] + if self.channels: + self.config['direction'] = self.channels[0].dio_get_config() + + def write(self, values): + value = 0 + for channel,val in zip(self.channels, values): + value &= val * (1 << channel.index) + self.write_bitfield(value) + + def write_bitfield(self, value): + self.subdevice.dio_bitfield(bits=value, write_mask=self.write_mask) diff --git a/pyafm/stepper.py b/pyafm/stepper.py new file mode 100644 index 0000000..cc2573d --- /dev/null +++ b/pyafm/stepper.py @@ -0,0 +1,65 @@ +# Copyright + +from __future__ import absolute_import + +import stepper as _stepper +from .digital_port import DigitalPort as _DigitalPort + + +class Stepper(_stepper.Stepper): + """Extend `stepper.Stepper` for easy configuration via `h5config`. + + Uses `DigitalPort` for transmitting the output. + + >>> from pycomedi.device import Device + >>> from pycomedi import constant as _constant + >>> from pyafm.config import StepperConfig, DigitalPortConfig + + >>> device = Device('/dev/comedi0') + >>> device.open() + + >>> config = StepperConfig() + >>> config['port'] = DigitalPortConfig() + >>> config['port']['channels'] = [1, 2, 3, 4] + >>> config['port']['direction'] = _constant.IO_DIRECTION.output + >>> config['port']['name'] = 'stepper port' + >>> config['name'] = 'test stepper' + + >>> s = Stepper(config=config, devices=[device]) + >>> s.position + 0 + >>> s.single_step(1) + >>> s.position + 2 + + >>> device.close() + """ + def __init__(self, config, devices=None): + self.config = config + self.port = None + self.load_from_config(devices=devices) + c = self.config # reduce verbosity + super(Stepper, self).__init__( + write=self.port.write_bitfield, full_step=c['full-step'], + logic=c['logic'], delay=c['delay'], step_size=c['step-size'], + backlash=c['backlash']) + + def load_from_config(self, devices): + c = self.config # reduce verbosity + if self.port is None: + self.port = _DigitalPort(config=c['port'], devices=devices) + self._write = self.port.write_bitfield + self.full_step = c['full-step'] + self.logic = c['logic'] + self.delay = c['delay'] + self.step_size = c['step-size'] + self.backlash = c['backlash'] + + def setup_config(self): + self.port.setup_config() + self.config['port'] = self.port.config + self.config['full-step'] = self.full_step + self.config['logic'] = self.logic + self.config['delay'] = self.delay + self.config['step-size'] = self.step_size + self.config['backlash'] = self.backlash diff --git a/pyafm/temperature.py b/pyafm/temperature.py index 0b0e15e..9d85a7e 100644 --- a/pyafm/temperature.py +++ b/pyafm/temperature.py @@ -19,15 +19,59 @@ from pypid.backend.melcor import MelcorBackend as _TemperatureBackend from . import LOG as _LOG +from .config import Celsius, Kelvin -class Temperature (_TemperatureBackend): - def __init__(self, **kwargs): +class Temperature (object): + """A temperature monitor based on the Melcor controller. + + >>> from pyafm.config import TemperatureConfig + + >>> config = TemperatureConfig() + >>> t = Temperature(config=config) + >>> t.get_temperature() # doctest: +SKIP + 297.37 + >>> t.cleanup() + """ + def __init__(self, config, backend=None): _LOG.debug('setup temperature monitor') - super(Temperature, self).__init__(**kwargs) - self.set_max_mv(max=1) # amp + self.config = config + self.backend = backend + self.load_from_config() + + def load_from_config(self): + c = self.config # reduce verbosity + _LOG.critical(type(c)) + _LOG.critical(c.keys()) + if self.backend is None: + self.backend = _TemperatureBackend( + controller=c['controller'], + device=c['device'], + baudrate=c['baudrate']) + self.backend.set_max_mv(max=c['max-current']) # amp + self.name = self.config['name'] + + def setup_config(self): + self.config['controller'] = self.backend._controller + self.config['device'] = self.backend._client.port + self.config['baudrate'] = self.backend._client.baudrate + self.config['max-current'] = self.backend.get_max_mv() + + def cleanup(self): + try: + self.backend.cleanup() + except Exception: + pass + self.backend = None def get_temperature(self): - temp = self.get_pv() + 273.15 # return temperature in kelvin + temp = self.backend.get_pv() + unit = self.config['units'] + if unit == Kelvin: # convert K -> K + pass + elif unit == Celsius: # convert C -> K + temp += 273.15 + else: + raise NotImplementedError(unit) _LOG.info('measured temperature of {:g} K'.format(temp)) return temp -- 2.26.2