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