Add DigitalPort, Stepper, and Temperature classes.
authorW. Trevor King <wking@drexel.edu>
Thu, 15 Mar 2012 20:16:38 +0000 (16:16 -0400)
committerW. Trevor King <wking@drexel.edu>
Thu, 15 Mar 2012 20:16:38 +0000 (16:16 -0400)
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
pyafm/config.py
pyafm/digital_port.py [new file with mode: 0644]
pyafm/stepper.py [new file with mode: 0644]
pyafm/temperature.py

index 284349aaa0aedbff07b5dfdf2a6b84aada4ef8b1..2681b56f0ee1d0f3b095ebb75ff192bd61c1f470 100644 (file)
@@ -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.
index 1391556593e45247fa8ea2e78ef75bd2ecaa5a26..c3af39a543c29f3f8fdab6be93cae6ecce06ea55 100644 (file)
@@ -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 (file)
index 0000000..bdce8f8
--- /dev/null
@@ -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 (file)
index 0000000..cc2573d
--- /dev/null
@@ -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
index 0b0e15ef01d1e7842691e7936a09db56093b1eeb..9d85a7ed4905ed71cd7c51070b9a4310640249ce 100644 (file)
 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