# 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
_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
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`.
... # 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."
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.
>>> 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)