From de497a3734372f5fa7c92d6ff7bdb6b2e327c345 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Thu, 15 Mar 2012 10:13:20 -0400 Subject: [PATCH] Optional config-based-setup for PiezoAxis, OutputChannel, and InputChannel. See the module docstring for details on why this is useful. --- pypiezo/base.py | 245 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 220 insertions(+), 25 deletions(-) diff --git a/pypiezo/base.py b/pypiezo/base.py index 3e5a079..c21dd3f 100644 --- a/pypiezo/base.py +++ b/pypiezo/base.py @@ -14,7 +14,35 @@ # You should have received a copy of the GNU General Public License along with # pypiezo. If not, see . -"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 + + >>> p.monitor_channel # doctest: +ELLIPSIS + + >>> 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 + + + >>> 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 + + >>> 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) -- 2.26.2