5845ed15ddd9a495989dd278e4d0f78e5106f7ff
[pycomedi.git] / pycomedi / channel.pyx
1 # Copyright (C) 2011-2012 W. Trevor King <wking@tremily.us>
2 #                         Éric Piel <piel@delmic.com>
3 #
4 # This file is part of pycomedi.
5 #
6 # pycomedi is free software: you can redistribute it and/or modify it under the
7 # terms of the GNU General Public License as published by the Free Software
8 # Foundation, either version 2 of the License, or (at your option) any later
9 # version.
10 #
11 # pycomedi is distributed in the hope that it will be useful, but WITHOUT ANY
12 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
13 # A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along with
16 # pycomedi.  If not, see <http://www.gnu.org/licenses/>.
17
18 "Wrap channel-wide Comedi functions in `Channel` and related classes"
19
20 cimport cython
21 cimport numpy as _numpy
22 import numpy as _numpy
23
24 cimport _comedi_h
25 cimport _comedilib_h
26 from calibration cimport CalibratedConverter as _CalibratedConverter
27 from calibration cimport Calibration as _Calibration
28 from range cimport Range as _Range
29 from subdevice cimport Subdevice as _Subdevice
30
31 from pycomedi import LOG as _LOG
32 from chanspec import ChanSpec as _ChanSpec
33 from pycomedi import PyComediError as _PyComediError
34 import _error
35 import constant as _constant
36
37
38 cdef class Channel (object):
39     """Class bundling channel-related functions
40
41     >>> from .device import Device
42     >>> from . import constant
43
44     >>> d = Device('/dev/comedi0')
45     >>> d.open()
46     >>> s = d.get_read_subdevice()
47     >>> c = s.channel(index=0)
48
49     >>> c.get_maxdata()
50     65535L
51     >>> c.get_n_ranges()
52     16
53     >>> c.get_range(index=0)
54     <Range unit:volt min:-10.0 max:10.0>
55     >>> c.find_range(constant.UNIT.volt, 0, 5)
56     <Range unit:volt min:0.0 max:5.0>
57
58     >>> d.close()
59     """
60     cdef public _Subdevice subdevice
61     cdef public int index
62
63     def __cinit__(self):
64         self.index = -1
65
66     def __init__(self, subdevice, index):
67         super(Channel, self).__init__()
68         self.subdevice = subdevice
69         self.index = index
70
71     def get_maxdata(self):
72         ret = _comedilib_h.comedi_get_maxdata(
73             self.subdevice.device.device,
74             self.subdevice.index, self.index)
75         if ret < 0:
76             _error.raise_error(function_name='comedi_get_maxdata', ret=ret)
77         return ret
78
79     def get_n_ranges(self):
80         ret = _comedilib_h.comedi_get_n_ranges(
81             self.subdevice.device.device,
82             self.subdevice.index, self.index)
83         if ret < 0:
84             _error.raise_error(function_name='comedi_get_n_ranges', ret=ret)
85         return ret
86
87     cdef _get_range(self, index):
88         cdef _comedilib_h.comedi_range *rng
89         cdef _Range ret
90         # Memory pointed to by the return value is freed on Device.close().
91         rng = _comedilib_h.comedi_get_range(
92             self.subdevice.device.device,
93             self.subdevice.index, self.index, index)
94         if rng is NULL:
95             _error.raise_error(function_name='comedi_get_range')
96         ret = _Range(value=index)
97         ret.set_comedi_range(rng[0])
98         # rng[0] is a sneaky way to dereference rng, since Cython
99         # doesn't support *rng.
100         return ret
101
102     @cython.always_allow_keywords(True)
103     def get_range(self, index):
104         "`Range` instance for the `index`\ed range."
105         return self._get_range(index)
106
107     def _find_range(self, unit, min, max):
108         "Search for range"
109         ret = _comedilib_h.comedi_find_range(
110             self.subdevice.device.device,
111             self.subdevice.index, self.index,
112             _constant.bitwise_value(unit), min, max)
113         if ret < 0:
114             _error.raise_error(function_name='comedi_find_range', ret=ret)
115         return ret
116
117     def find_range(self, unit, min, max):
118         """Search for range
119
120         `unit` should be an item from `constants.UNIT`.
121         """
122         return self.get_range(self._find_range(unit, min, max))
123
124     def ranges(self, **kwargs):
125         "Iterate through all available ranges."
126         ret = []
127         for i in range(self.get_n_ranges()):
128             #yield self.subdevice(i, **kwargs)
129             # Generators are not supported in Cython 0.14.1
130             ret.append(self.get_range(i, **kwargs))
131         return ret
132
133
134 cdef class DigitalChannel (Channel):
135     """Channel configured for reading or writing digital data.
136
137     >>> from .device import Device
138     >>> from . import constant
139
140     >>> d = Device('/dev/comedi0')
141     >>> d.open()
142     >>> s = d.find_subdevice_by_type(constant.SUBDEVICE_TYPE.dio)
143     >>> c = s.channel(0, factory=DigitalChannel)
144
145     >>> c.get_maxdata()
146     1L
147     >>> c.get_n_ranges()
148     1
149     >>> c.get_range(0)
150     <Range unit:volt min:0.0 max:5.0>
151
152     >>> direction = c.dio_get_config()
153     >>> direction  # doctest: +SKIP
154     <_NamedInt input>
155
156     >>> c.dio_config(_constant.IO_DIRECTION.input)
157     >>> data = c.dio_read()
158     >>> data
159     1
160
161     >>> c.dio_config(_constant.IO_DIRECTION.output)
162     >>> c.dio_write(1)
163
164     >>> c.dio_config(direction)
165
166     >>> d.close()
167     """
168     def dio_config(self, dir):
169         """Change input/output properties
170
171         `dir` should be an item from `constants.IO_DIRECTION`.
172         """
173         ret = _comedilib_h.comedi_dio_config(
174             self.subdevice.device.device,
175             self.subdevice.index, self.index,
176             _constant.bitwise_value(dir))
177         if ret < 0:
178             _error.raise_error(function_name='comedi_dio_config', ret=ret)
179
180     def dio_get_config(self):
181         """Query input/output properties
182
183         Return an item from `constant.IO_DIRECTION`.
184         """
185         cpdef unsigned int dir
186         ret = _comedilib_h.comedi_dio_get_config(
187             self.subdevice.device.device,
188            self.subdevice.index, self.index, &dir)
189         if ret < 0:
190             _error.raise_error(function_name='comedi_dio_get_config', ret=ret)
191         return _constant.IO_DIRECTION.index_by_value(dir)
192
193     def dio_read(self):
194         "Read a single bit"
195         cpdef unsigned int bit
196         ret = _comedilib_h.comedi_dio_read(
197             self.subdevice.device.device,
198             self.subdevice.index, self.index, &bit)
199         if ret < 0:
200             _error.raise_error(function_name='comedi_dio_read', ret=ret)
201         return int(bit)
202
203     def dio_write(self, bit):
204         "Write a single bit"
205         ret = _comedilib_h.comedi_dio_write(
206             self.subdevice.device.device,
207             self.subdevice.index, self.index, bit)
208         if ret < 0:
209             _error.raise_error(function_name='comedi_dio_write', ret=ret)
210
211
212 cdef class AnalogChannel (Channel):
213     """Channel configured for reading or writing analog data.
214
215     `range` should be a `Range` instance, `aref` should be an
216     `constants.AREF` instance.  If not specified, defaults are chosen
217     based on the capabilities of the subdevice.
218
219     >>> from .device import Device
220     >>> from . import constant
221
222     >>> d = Device('/dev/comedi0')
223     >>> d.open()
224     >>> s = d.get_read_subdevice()
225     >>> c = s.channel(0, factory=AnalogChannel)
226
227     >>> c.range
228     <Range unit:volt min:-10.0 max:10.0>
229     >>> c.aref
230     <_NamedInt ground>
231
232     You can calibrate the channel with the default channel calibration.
233
234     >>> c.apply_calibration()
235
236     Or you can use a parsed (and possibly altered) calibration.
237
238     >>> calibration = d.parse_calibration()
239     >>> c.apply_calibration(calibration=calibration)
240
241     >>> data = c.data_read()
242     >>> data  # doctest: +SKIP
243     32670L
244     >>> converter = c.get_converter()
245     >>> converter  # doctest: +NORMALIZE_WHITESPACE
246     <CalibratedConverter
247      to_physical:{coefficients:[-10.0, 0.00030518043793392844] origin:0.0}
248      from_physical:{coefficients:[0.0, 3276.75] origin:-10.0}>
249     >>> physical_data = converter.to_physical(data)
250     >>> physical_data  # doctest: +SKIP
251     -0.029755092698558021
252     >>> converter.from_physical(physical_data) == data
253     True
254
255     >>> data = c.data_read_n(5)
256     >>> data  # doctest: +SKIP
257     array([32674, 32673, 32674, 32672, 32675], dtype=uint32)
258
259     >>> c.data_read_hint()
260     >>> c.data_read()  # doctest: +SKIP
261     32672L
262
263     >>> data = c.data_read_delayed(nano_sec=1e3)
264     >>> data  # doctest: +SKIP
265     32672L
266
267     >>> s = d.get_write_subdevice()
268     >>> c = s.channel(0, factory=AnalogChannel)
269
270     >>> converter = c.get_converter()
271     >>> converter  # doctest: +NORMALIZE_WHITESPACE
272     <CalibratedConverter
273      to_physical:{coefficients:[-10.0, 0.00030518043793392844] origin:0.0}
274      from_physical:{coefficients:[0.0, 3276.75] origin:-10.0}>
275
276     >>> c.data_write(converter.from_physical(0))
277
278     >>> d.close()
279
280     Even after the device is closed, the range information is
281     retained.
282
283     >>> c.range
284     <Range unit:volt min:-10.0 max:10.0>
285     """
286     cdef public _Range range
287     cdef public object aref
288
289     def __init__(self, range=None, aref=None, **kwargs):
290         super(AnalogChannel, self).__init__(**kwargs)
291         if range == None:
292             range = self.get_range(0)
293         elif isinstance(range, int):
294             range = self.get_range(range)
295         self.range = range
296         if aref == None:
297             flags = self.subdevice.get_flags()
298             for ar in _constant.AREF:
299                 if getattr(flags, ar.name):
300                     aref = ar
301                     break
302                 raise _PyComediError(
303                     '%s does not support any known analog reference type (%s)'
304                     % (self.subdevice, flags))
305         self.aref = aref
306
307     # syncronous stuff
308
309     def data_read(self):
310         "Read one sample"
311         cdef _comedi_h.lsampl_t data
312         ret = _comedilib_h.comedi_data_read(
313             self.subdevice.device.device,
314             self.subdevice.index, self.index,
315             _constant.bitwise_value(self.range),
316             _constant.bitwise_value(self.aref),
317             &data)
318         if ret < 0:
319             _error.raise_error(function_name='comedi_data_read', ret=ret)
320         return data
321
322     def data_read_n(self, n):
323         "Read `n` samples (timing between samples is undefined)."
324         data = _numpy.ndarray(shape=(n,), dtype=_numpy.uint32)
325         ret = _comedilib_h.comedi_data_read_n(
326             self.subdevice.device.device,
327             self.subdevice.index, self.index,
328             _constant.bitwise_value(self.range),
329             _constant.bitwise_value(self.aref),
330             <_comedilib_h.lsampl_t *>_numpy.PyArray_DATA(data), n)
331         if ret < 0:
332             _error.raise_error(function_name='comedi_data_read_n', ret=ret)
333         return data
334
335     def data_read_hint(self):
336         """Tell driver which channel/range/aref you will read next
337
338         Used to prepare an analog input for a subsequent call to
339         comedi_data_read.  It is not necessary to use this function,
340         but it can be useful for eliminating inaccuaracies caused by
341         insufficient settling times when switching the channel or gain
342         on an analog input.  This function sets an analog input to the
343         channel, range, and aref specified but does not perform an
344         actual analog to digital conversion.
345
346         Alternatively, one can simply use `.data_read_delayed()`,
347         which sets up the input, pauses to allow settling, then
348         performs a conversion.
349         """
350         ret = _comedilib_h.comedi_data_read_hint(
351             self.subdevice.device.device,
352             self.subdevice.index, self.index,
353             _constant.bitwise_value(self.range),
354             _constant.bitwise_value(self.aref))
355         if ret < 0:
356             _error.raise_error(function_name='comedi_data_read_hint', ret=ret)
357
358     def data_read_delayed(self, nano_sec=0):
359         """Read single sample after delaying specified settling time.
360
361         Although the settling time is specified in integer
362         nanoseconds, the actual settling time will be rounded up to
363         the nearest microsecond.
364         """
365         cdef _comedi_h.lsampl_t data
366         ret = _comedilib_h.comedi_data_read_delayed(
367             self.subdevice.device.device,
368             self.subdevice.index, self.index,
369             _constant.bitwise_value(self.range),
370             _constant.bitwise_value(self.aref),
371             &data, int(nano_sec))
372         if ret < 0:
373             _error.raise_error(function_name='comedi_data_read_delayed',
374                                ret=ret)
375         return data
376
377     def data_write(self, data):
378         """Write one sample
379
380         Returns 1 (the number of data samples written).
381         """
382         ret = _comedilib_h.comedi_data_write(
383             self.subdevice.device.device,
384             self.subdevice.index, self.index,
385             _constant.bitwise_value(self.range),
386             _constant.bitwise_value(self.aref),
387             int(data))
388         if ret != 1:
389             _error.raise_error(function_name='comedi_data_write', ret=ret)
390
391     def chanspec(self):
392         return _ChanSpec(chan=self.index, range=self.range, aref=self.aref)
393
394
395     cdef _comedilib_h.comedi_polynomial_t get_softcal_converter(
396         self, direction, calibration):
397         """
398
399         `direction` should be a value from `constant.CONVERSION_DIRECTION`.
400         """
401         cdef _comedilib_h.comedi_polynomial_t poly
402         rc = _comedilib_h.comedi_get_softcal_converter(
403             self.subdevice.index, self.index,
404             _constant.bitwise_value(self.range),
405             _constant.bitwise_value(direction),
406             <_comedilib_h.comedi_calibration_t*> calibration, &poly)
407         if rc < 0:
408             _error.raise_error(function_name='comedi_get_softcal_converter',
409                                ret=rc)
410         return poly
411
412     cdef _comedilib_h.comedi_polynomial_t get_hardcal_converter(
413         self, direction):
414         """
415
416         `direction` should be a value from `constant.CONVERSION_DIRECTION`.
417         """
418         cdef _comedilib_h.comedi_polynomial_t poly
419         rc = _comedilib_h.comedi_get_hardcal_converter(
420             self.subdevice.device.device,
421             self.subdevice.index, self.index,
422             _constant.bitwise_value(self.range),
423             _constant.bitwise_value(direction), &poly)
424         if rc < 0:
425             _error.raise_error(function_name='comedi_get_hardcal_converter',
426                                ret=rc)
427         return poly
428
429     cdef _get_converter(self, calibration):
430         cdef _comedilib_h.comedi_polynomial_t to_physical, from_physical
431         cdef _CalibratedConverter ret
432         flags = self.subdevice.get_flags()
433         if flags.soft_calibrated:
434             if calibration is None:
435                 calibration = self.subdevice.device.parse_calibration()
436             to_physical = self.get_softcal_converter(
437                 _constant.CONVERSION_DIRECTION.to_physical,
438                 calibration)
439             from_physical = self.get_softcal_converter(
440                 _constant.CONVERSION_DIRECTION.from_physical,
441                 calibration)
442         else:
443             to_physical = self.get_hardcal_converter(
444                 _constant.CONVERSION_DIRECTION.to_physical)
445             from_physical = self.get_hardcal_converter(
446                 _constant.CONVERSION_DIRECTION.from_physical)
447         ret = _CalibratedConverter()
448         ret._to_physical = to_physical
449         ret._from_physical = from_physical
450         return ret
451
452     def get_converter(self, calibration=None):
453         return self._get_converter(calibration)
454
455     cdef _apply_calibration(self, char *path):
456         if path is NULL:
457             p = self.subdevice.device.get_default_calibration_path()
458             # automatically get a char * refernce into the Python string p
459             path = p
460         ret = _comedilib_h.comedi_apply_calibration(
461             self.subdevice.device.device,
462             self.subdevice.index, self.index,
463             _constant.bitwise_value(self.range),
464             _constant.bitwise_value(self.aref),
465             path)
466         if ret < 0:
467             _error.raise_error(
468                 function_name='comedi_apply_calibration', ret=ret)
469         return ret
470
471     cdef _apply_parsed_calibration(self, _Calibration calibration):
472         ret = _comedilib_h.comedi_apply_parsed_calibration(
473             self.subdevice.device.device,
474             self.subdevice.index, self.index,
475             _constant.bitwise_value(self.range),
476             _constant.bitwise_value(self.aref),
477             calibration.calibration)
478         if ret < 0:
479             _error.raise_error(
480                 function_name='comedi_apply_parsed_calibration', ret=ret)
481         return ret
482
483     def apply_calibration(self, calibration=None, path=None):
484         """Apply a calibration to this channel configuration
485
486         `calibration` may None or a `Calibration` instance.  If it is
487         a `Calibration` instance, that instance is used for the
488         calibration.  Otherwise we look at `path`.  If `path` is None,
489         we use the default device calibration, otherwise we try and
490         use the calibration file located at `path`.
491         """
492         if calibration is not None:
493             self._apply_parsed_calibration(calibration)
494         elif path is not None:
495             self._apply_calibration(path)
496         else:
497             self._apply_calibration(NULL)