Optional config-based-setup for PiezoAxis, OutputChannel, and InputChannel.
authorW. Trevor King <wking@drexel.edu>
Thu, 15 Mar 2012 14:13:20 +0000 (10:13 -0400)
committerW. Trevor King <wking@drexel.edu>
Thu, 15 Mar 2012 14:13:20 +0000 (10:13 -0400)
See the module docstring for details on why this is useful.

pypiezo/base.py

index 3e5a07918aef74047ab480790e5e7bf7e2030f0e..c21dd3f0d8d09d5aca8b1ee5e20b718cc568d2ba 100644 (file)
 # You should have received a copy of the GNU General Public License along with
 # pypiezo.  If not, see <http://www.gnu.org/licenses/>.
 
-"Basic piezo control."
+"""Basic piezo control.
+
+Several of the classes defined in this module are simple wrappers for
+combining a `pycomedi` class instance (e.g. `Channel`) with the
+appropriate config data (e.g. `InputChannelConfig`).  The idea is that
+the `h5config`-based config data will make it easy for you to save
+your hardware configuration to disk (so that you have a record of what
+you did).  It should also make it easy to load your configuration from
+the disk (so that you can do the same thing again).  Because this
+`h5config` <-> `pycomedi` communication works both ways, you have two
+options when you're initializing a class:
+
+1) On your first run, it's probably easiest to setup your channels and
+   such using the usual `pycomedi` interface
+   (`device.find_subdevice_by_type`, etc.)  After you have setup your
+   channels, you can initialize them with a stock config instance, and
+   call the `setup_config` method to copy the channel configuration
+   into the config file.  Now the config instance is ready to be saved
+   to disk.
+2) On later runs, you have the option of loading the `pycomedi`
+   objects from your old configuration.  After loading the config data
+   from disk, initialize your class by passing in a `devices` list,
+   but without passing in the `Channel` instances.  The class will
+   take care of setting up the channel instances internally,
+   recreating your earlier setup.
+
+For examples of how to apply either approach to a particular class,
+see that class' docstring.
+"""
 
 import math as _math
 from time import sleep as _sleep
@@ -30,7 +58,10 @@ except (ImportError, RuntimeError), e:
     _matplotlib = None
     _matplotlib_import_error = e
 
-from pycomedi.constant import AREF, TRIG_SRC, SDF
+from pycomedi.device import Device
+from pycomedi.subdevice import StreamingSubdevice
+from pycomedi.channel import AnalogChannel
+from pycomedi.constant import AREF, TRIG_SRC, SDF, SUBDEVICE_TYPE, UNIT
 from pycomedi.utility import inttrig_insn, Reader, Writer
 
 from . import LOG as _LOG
@@ -170,6 +201,95 @@ def get_axis_name(axis_config):
     channel_config = axis_config['channel']
     return channel_config['name']
 
+def _load_device(filename, devices):
+    """Return an open device from `devices` which has a given `filename`.
+
+    Sometimes a caller will already have the required `Device`, in
+    which case we just pull that instance out of `devices`, check that
+    it's open, and return it.  Other times, the caller may want us to
+    open the device ourselves, so if we can't find an appropriate
+    device in `devices`, we create a new one, append it to `devices`
+    (so the caller can close it later), and return it.
+
+    You will have to open the `Device` yourself, though, because the
+    open device instance should not be held by a particular
+    `PiezoAxis` instance.  If you don't want to open devices yourself,
+    you can pass in a blank list of devices, and the initialization
+    routine will append any necessary-but-missing devices to it.
+
+    >>> from pycomedi.device import Device
+
+    >>> devices = [Device('/dev/comedi0')]
+    >>> device = _load_device(filename='/dev/comedi0', devices=devices)
+    >>> device.filename
+    '/dev/comedi0'
+    >>> device.file is not None
+    True
+    >>> device.close()
+
+    >>> devices = []
+    >>> device = _load_device(filename='/dev/comedi0', devices=devices)
+    >>> devices == [device]
+    True
+    >>> device.filename
+    '/dev/comedi0'
+    >>> device.file is not None
+    True
+    >>> device.close()
+
+    We try and return helpful errors when things go wrong:
+
+    >>> device = _load_device(filename='/dev/comedi0', devices=None)
+    Traceback (most recent call last):
+      ...
+    TypeError: 'NoneType' object is not iterable
+    >>> device = _load_device(filename='/dev/comedi0', devices=tuple())
+    Traceback (most recent call last):
+      ...
+    ValueError: none of the available devices ([]) match /dev/comedi0, and we cannot append to ()
+    >>> device = _load_device(filename='/dev/comediX', devices=[])
+    Traceback (most recent call last):
+      ...
+    PyComediError: comedi_open (/dev/comediX): No such file or directory (None)
+    """
+    try:
+        matching_devices = [d for d in devices if d.filename == filename]
+    except TypeError:
+        _LOG.error('non-iterable devices? ({})'.format(devices))
+        raise
+    if matching_devices:
+        device = matching_devices[0]
+        if device.file is None:
+            device.open()
+    else:
+        device = Device(filename)
+        device.open()
+        try:
+            devices.append(device)  # pass new device back to caller
+        except AttributeError:
+            device.close()
+            raise ValueError(
+                ('none of the available devices ({}) match {}, and we '
+                 'cannot append to {}').format(
+                    [d.filename for d in devices], filename, devices))
+    return device
+
+def _load_channel_from_config(channel, devices, subdevice_type):
+    c = channel.config  # reduce verbosity
+    if not channel.channel:
+        device = _load_device(filename=c['device'], devices=devices)
+        if c['subdevice'] < 0:
+            subdevice = device.find_subdevice_by_type(
+                subdevice_type, factory=StreamingSubdevice)
+        else:
+            subdevice = device.subdevice(
+                index=c['subdevice'], factory=StreamingSubdevice)
+        channel.channel = subdevice.channel(
+            index=c['channel'], factory=AnalogChannel,
+            aref=c['analog-reference'])
+        channel.channel.range = channel.channel.get_range(index=c['range'])
+    channel.name = c['name']
+
 def _setup_channel_config(config, channel):
     """Initialize the `ChannelConfig` `config` using the
     `AnalogChannel` `channel`.
@@ -264,28 +384,38 @@ class PiezoAxis (object):
     ... # doctest: +ELLIPSIS
     -1.6...e-06
 
+    Opening from the config alone:
+
+    >>> p = PiezoAxis(config=config, devices=[d])
+    >>> p.axis_channel  # doctest: +ELLIPSIS
+    <pycomedi.channel.AnalogChannel object at 0x...>
+    >>> p.monitor_channel  # doctest: +ELLIPSIS
+    <pycomedi.channel.AnalogChannel object at 0x...>
+
     >>> d.close()
     """
-    def __init__(self, config, axis_channel=None, monitor_channel=None):
+    def __init__(self, config, axis_channel=None, monitor_channel=None,
+                 devices=None):
         self.config = config
-        if (config['monitor'] and
-            config['channel']['device'] != config['monitor']['device']):
-            raise NotImplementedError(
-                ('piezo axis control and monitor on different devices '
-                 '(%s and %s)') % (
-                    config['channel']['device'],
-                    config['monitor']['device']))
-        if not axis_channel:
-            raise NotImplementedError(
-                'pypiezo not yet capable of opening its own axis channel')
-            #axis_channel = pycomedi...
         self.axis_channel = axis_channel
-        if config['monitor'] and not monitor_channel:
-            raise NotImplementedError(
-                'pypiezo not yet capable of opening its own monitor channel')
-            #monitor_channel = pycomedi...
         self.monitor_channel = monitor_channel
-        self.name = config['channel']['name']
+        self.load_from_config(devices=devices)
+
+    def load_from_config(self, devices):
+        c = self.config  # reduce verbosity
+        if (c['monitor'] and
+            c['channel']['device'] != c['monitor']['device']):
+            raise NotImplementedError(
+                ('piezo axis control and monitor on different devices '
+                 '({} and {})').format(
+                    c['channel']['device'], c['monitor']['device']))
+        if not self.axis_channel:
+            self.axis_channel = OutputChannel(
+                config=c['channel'], devices=devices).channel
+        if c['monitor'] and not self.monitor_channel:
+            self.monitor_channel = InputChannel(
+                config=c['monitor'], devices=devices).channel
+        self.name = c['channel']['name']
 
     def setup_config(self):
         "Initialize the axis (and monitor) configs."
@@ -301,6 +431,65 @@ class PiezoAxis (object):
                 self.config['channel'], self.axis_channel.get_maxdata())
 
 
+
+class OutputChannel(object):
+    """An input channel monitoring some interesting parameter.
+
+    >>> from pycomedi.device import Device
+    >>> from pycomedi.subdevice import StreamingSubdevice
+    >>> from pycomedi.channel import AnalogChannel
+    >>> from pycomedi.constant import AREF, SUBDEVICE_TYPE, UNIT
+
+    >>> d = Device('/dev/comedi0')
+    >>> d.open()
+
+    >>> s = d.find_subdevice_by_type(SUBDEVICE_TYPE.ao,
+    ...     factory=StreamingSubdevice)
+
+    >>> channel = s.channel(0, factory=AnalogChannel, aref=AREF.diff)
+    >>> channel.range = channel.find_range(unit=UNIT.volt, min=-10, max=10)
+
+    >>> channel_config = _config.OutputChannelConfig()
+
+    >>> c = OutputChannel(config=channel_config, channel=channel)
+    >>> c.setup_config()
+    >>> print(channel_config.dump())
+    name: 
+    device: /dev/comedi0
+    subdevice: 1
+    channel: 0
+    maxdata: 65535
+    range: 0
+    analog-reference: diff
+    conversion-coefficients: -10.0, 0.000305180437934
+    conversion-origin: 0.0
+    inverse-conversion-coefficients: 0.0, 3276.75
+    inverse-conversion-origin: -10.0
+
+    >>> convert_volts_to_bits(c.config, -10)
+    0.0
+
+    Opening from the config alone:
+
+    >>> c = OutputChannel(config=channel_config, devices=[d])
+    >>> c.channel  # doctest: +ELLIPSIS
+    <pycomedi.channel.AnalogChannel object at 0x...>
+
+    >>> d.close()
+    """
+    def __init__(self, config, channel=None, devices=None):
+        self.config = config
+        self.channel = channel
+        self.load_from_config(devices=devices)
+
+    def load_from_config(self, devices):
+        _load_channel_from_config(
+            channel=self, devices=devices, subdevice_type=SUBDEVICE_TYPE.ao)
+
+    def setup_config(self):
+        _setup_channel_config(self.config, self.channel)
+
+
 class InputChannel(object):
     """An input channel monitoring some interesting parameter.
 
@@ -338,16 +527,22 @@ class InputChannel(object):
     >>> convert_bits_to_volts(c.config, 0)
     -10.0
 
+    Opening from the config alone:
+
+    >>> c = InputChannel(config=channel_config, devices=[d])
+    >>> c.channel  # doctest: +ELLIPSIS
+    <pycomedi.channel.AnalogChannel object at 0x...>
+
     >>> d.close()
     """
-    def __init__(self, config, channel=None):
+    def __init__(self, config, channel=None, devices=None):
         self.config = config
-        if not channel:
-            raise NotImplementedError(
-                'pypiezo not yet capable of opening its own channel')
-            #channel = pycomedi...
         self.channel = channel
-        self.name = config['name']
+        self.load_from_config(devices=devices)
+
+    def load_from_config(self, devices):
+        _load_channel_from_config(
+            channel=self, devices=devices, subdevice_type=SUBDEVICE_TYPE.ai)
 
     def setup_config(self):
         _setup_channel_config(self.config, self.channel)