Add `-*- coding: utf-8 -*-` comments where Éric Piel is in authors.
[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 cimport Subdevice as _Subdevice
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 _Subdevice subdevice
62     cdef public int index
63
64     def __cinit__(self):
65         self.index = -1
66
67     def __init__(self, subdevice, index):
68         super(Channel, self).__init__()
69         self.subdevice = subdevice
70         self.index = index
71
72     def get_maxdata(self):
73         ret = _comedilib_h.comedi_get_maxdata(
74             self.subdevice.device.device,
75             self.subdevice.index, self.index)
76         if ret < 0:
77             _error.raise_error(function_name='comedi_get_maxdata', ret=ret)
78         return ret
79
80     def get_n_ranges(self):
81         ret = _comedilib_h.comedi_get_n_ranges(
82             self.subdevice.device.device,
83             self.subdevice.index, self.index)
84         if ret < 0:
85             _error.raise_error(function_name='comedi_get_n_ranges', ret=ret)
86         return ret
87
88     cdef _get_range(self, index):
89         cdef _comedilib_h.comedi_range *rng
90         cdef _Range ret
91         # Memory pointed to by the return value is freed on Device.close().
92         rng = _comedilib_h.comedi_get_range(
93             self.subdevice.device.device,
94             self.subdevice.index, self.index, index)
95         if rng is NULL:
96             _error.raise_error(function_name='comedi_get_range')
97         ret = _Range(value=index)
98         ret.set_comedi_range(rng[0])
99         # rng[0] is a sneaky way to dereference rng, since Cython
100         # doesn't support *rng.
101         return ret
102
103     @cython.always_allow_keywords(True)
104     def get_range(self, index):
105         "`Range` instance for the `index`\ed range."
106         return self._get_range(index)
107
108     def _find_range(self, unit, min, max):
109         "Search for range"
110         ret = _comedilib_h.comedi_find_range(
111             self.subdevice.device.device,
112             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.subdevice.device.device,
176             self.subdevice.index, self.index,
177             _constant.bitwise_value(dir))
178         if ret < 0:
179             _error.raise_error(function_name='comedi_dio_config', ret=ret)
180
181     def dio_get_config(self):
182         """Query input/output properties
183
184         Return an item from `constant.IO_DIRECTION`.
185         """
186         cpdef unsigned int dir
187         ret = _comedilib_h.comedi_dio_get_config(
188             self.subdevice.device.device,
189            self.subdevice.index, self.index, &dir)
190         if ret < 0:
191             _error.raise_error(function_name='comedi_dio_get_config', ret=ret)
192         return _constant.IO_DIRECTION.index_by_value(dir)
193
194     def dio_read(self):
195         "Read a single bit"
196         cpdef unsigned int bit
197         ret = _comedilib_h.comedi_dio_read(
198             self.subdevice.device.device,
199             self.subdevice.index, self.index, &bit)
200         if ret < 0:
201             _error.raise_error(function_name='comedi_dio_read', ret=ret)
202         return int(bit)
203
204     def dio_write(self, bit):
205         "Write a single bit"
206         ret = _comedilib_h.comedi_dio_write(
207             self.subdevice.device.device,
208             self.subdevice.index, self.index, bit)
209         if ret < 0:
210             _error.raise_error(function_name='comedi_dio_write', ret=ret)
211
212
213 cdef class AnalogChannel (Channel):
214     """Channel configured for reading or writing analog data.
215
216     `range` should be a `Range` instance, `aref` should be an
217     `constants.AREF` instance.  If not specified, defaults are chosen
218     based on the capabilities of the subdevice.
219
220     >>> from .device import Device
221     >>> from . import constant
222
223     >>> d = Device('/dev/comedi0')
224     >>> d.open()
225     >>> s = d.get_read_subdevice()
226     >>> c = s.channel(0, factory=AnalogChannel)
227
228     >>> c.range
229     <Range unit:volt min:-10.0 max:10.0>
230     >>> c.aref
231     <_NamedInt ground>
232
233     You can calibrate the channel with the default channel calibration.
234
235     >>> c.apply_calibration()
236
237     Or you can use a parsed (and possibly altered) calibration.
238
239     >>> calibration = d.parse_calibration()
240     >>> c.apply_calibration(calibration=calibration)
241
242     >>> data = c.data_read()
243     >>> data  # doctest: +SKIP
244     32670L
245     >>> converter = c.get_converter()
246     >>> converter  # doctest: +NORMALIZE_WHITESPACE
247     <CalibratedConverter
248      to_physical:{coefficients:[-10.0, 0.00030518043793392844] origin:0.0}
249      from_physical:{coefficients:[0.0, 3276.75] origin:-10.0}>
250     >>> physical_data = converter.to_physical(data)
251     >>> physical_data  # doctest: +SKIP
252     -0.029755092698558021
253     >>> converter.from_physical(physical_data) == data
254     True
255
256     >>> data = c.data_read_n(5)
257     >>> data  # doctest: +SKIP
258     array([32674, 32673, 32674, 32672, 32675], dtype=uint32)
259
260     >>> c.data_read_hint()
261     >>> c.data_read()  # doctest: +SKIP
262     32672L
263
264     >>> data = c.data_read_delayed(nano_sec=1e3)
265     >>> data  # doctest: +SKIP
266     32672L
267
268     >>> s = d.get_write_subdevice()
269     >>> c = s.channel(0, factory=AnalogChannel)
270
271     >>> converter = c.get_converter()
272     >>> converter  # doctest: +NORMALIZE_WHITESPACE
273     <CalibratedConverter
274      to_physical:{coefficients:[-10.0, 0.00030518043793392844] origin:0.0}
275      from_physical:{coefficients:[0.0, 3276.75] origin:-10.0}>
276
277     >>> c.data_write(converter.from_physical(0))
278
279     >>> d.close()
280
281     Even after the device is closed, the range information is
282     retained.
283
284     >>> c.range
285     <Range unit:volt min:-10.0 max:10.0>
286     """
287     cdef public _Range range
288     cdef public object aref
289
290     def __init__(self, range=None, aref=None, **kwargs):
291         super(AnalogChannel, self).__init__(**kwargs)
292         if range == None:
293             range = self.get_range(0)
294         elif isinstance(range, int):
295             range = self.get_range(range)
296         self.range = range
297         if aref == None:
298             flags = self.subdevice.get_flags()
299             for ar in _constant.AREF:
300                 if getattr(flags, ar.name):
301                     aref = ar
302                     break
303                 raise _PyComediError(
304                     '%s does not support any known analog reference type (%s)'
305                     % (self.subdevice, flags))
306         self.aref = aref
307
308     # syncronous stuff
309
310     def data_read(self):
311         "Read one sample"
312         cdef _comedi_h.lsampl_t data
313         ret = _comedilib_h.comedi_data_read(
314             self.subdevice.device.device,
315             self.subdevice.index, self.index,
316             _constant.bitwise_value(self.range),
317             _constant.bitwise_value(self.aref),
318             &data)
319         if ret < 0:
320             _error.raise_error(function_name='comedi_data_read', ret=ret)
321         return data
322
323     def data_read_n(self, n):
324         "Read `n` samples (timing between samples is undefined)."
325         data = _numpy.ndarray(shape=(n,), dtype=_numpy.uint32)
326         ret = _comedilib_h.comedi_data_read_n(
327             self.subdevice.device.device,
328             self.subdevice.index, self.index,
329             _constant.bitwise_value(self.range),
330             _constant.bitwise_value(self.aref),
331             <_comedilib_h.lsampl_t *>_numpy.PyArray_DATA(data), n)
332         if ret < 0:
333             _error.raise_error(function_name='comedi_data_read_n', ret=ret)
334         return data
335
336     def data_read_hint(self):
337         """Tell driver which channel/range/aref you will read next
338
339         Used to prepare an analog input for a subsequent call to
340         comedi_data_read.  It is not necessary to use this function,
341         but it can be useful for eliminating inaccuaracies caused by
342         insufficient settling times when switching the channel or gain
343         on an analog input.  This function sets an analog input to the
344         channel, range, and aref specified but does not perform an
345         actual analog to digital conversion.
346
347         Alternatively, one can simply use `.data_read_delayed()`,
348         which sets up the input, pauses to allow settling, then
349         performs a conversion.
350         """
351         ret = _comedilib_h.comedi_data_read_hint(
352             self.subdevice.device.device,
353             self.subdevice.index, self.index,
354             _constant.bitwise_value(self.range),
355             _constant.bitwise_value(self.aref))
356         if ret < 0:
357             _error.raise_error(function_name='comedi_data_read_hint', ret=ret)
358
359     def data_read_delayed(self, nano_sec=0):
360         """Read single sample after delaying specified settling time.
361
362         Although the settling time is specified in integer
363         nanoseconds, the actual settling time will be rounded up to
364         the nearest microsecond.
365         """
366         cdef _comedi_h.lsampl_t data
367         ret = _comedilib_h.comedi_data_read_delayed(
368             self.subdevice.device.device,
369             self.subdevice.index, self.index,
370             _constant.bitwise_value(self.range),
371             _constant.bitwise_value(self.aref),
372             &data, int(nano_sec))
373         if ret < 0:
374             _error.raise_error(function_name='comedi_data_read_delayed',
375                                ret=ret)
376         return data
377
378     def data_write(self, data):
379         """Write one sample
380
381         Returns 1 (the number of data samples written).
382         """
383         ret = _comedilib_h.comedi_data_write(
384             self.subdevice.device.device,
385             self.subdevice.index, self.index,
386             _constant.bitwise_value(self.range),
387             _constant.bitwise_value(self.aref),
388             int(data))
389         if ret != 1:
390             _error.raise_error(function_name='comedi_data_write', ret=ret)
391
392     def chanspec(self):
393         return _ChanSpec(chan=self.index, range=self.range, aref=self.aref)
394
395
396     cdef _comedilib_h.comedi_polynomial_t get_softcal_converter(
397         self, direction, calibration):
398         """
399
400         `direction` should be a value from `constant.CONVERSION_DIRECTION`.
401         """
402         cdef _comedilib_h.comedi_polynomial_t poly
403         rc = _comedilib_h.comedi_get_softcal_converter(
404             self.subdevice.index, self.index,
405             _constant.bitwise_value(self.range),
406             _constant.bitwise_value(direction),
407             <_comedilib_h.comedi_calibration_t*> calibration, &poly)
408         if rc < 0:
409             _error.raise_error(function_name='comedi_get_softcal_converter',
410                                ret=rc)
411         return poly
412
413     cdef _comedilib_h.comedi_polynomial_t get_hardcal_converter(
414         self, direction):
415         """
416
417         `direction` should be a value from `constant.CONVERSION_DIRECTION`.
418         """
419         cdef _comedilib_h.comedi_polynomial_t poly
420         rc = _comedilib_h.comedi_get_hardcal_converter(
421             self.subdevice.device.device,
422             self.subdevice.index, self.index,
423             _constant.bitwise_value(self.range),
424             _constant.bitwise_value(direction), &poly)
425         if rc < 0:
426             _error.raise_error(function_name='comedi_get_hardcal_converter',
427                                ret=rc)
428         return poly
429
430     cdef _get_converter(self, calibration):
431         cdef _comedilib_h.comedi_polynomial_t to_physical, from_physical
432         cdef _CalibratedConverter ret
433         flags = self.subdevice.get_flags()
434         if flags.soft_calibrated:
435             if calibration is None:
436                 calibration = self.subdevice.device.parse_calibration()
437             to_physical = self.get_softcal_converter(
438                 _constant.CONVERSION_DIRECTION.to_physical,
439                 calibration)
440             from_physical = self.get_softcal_converter(
441                 _constant.CONVERSION_DIRECTION.from_physical,
442                 calibration)
443         else:
444             to_physical = self.get_hardcal_converter(
445                 _constant.CONVERSION_DIRECTION.to_physical)
446             from_physical = self.get_hardcal_converter(
447                 _constant.CONVERSION_DIRECTION.from_physical)
448         ret = _CalibratedConverter()
449         ret._to_physical = to_physical
450         ret._from_physical = from_physical
451         return ret
452
453     def get_converter(self, calibration=None):
454         return self._get_converter(calibration)
455
456     cdef _apply_calibration(self, char *path):
457         if path is NULL:
458             p = self.subdevice.device.get_default_calibration_path()
459             # automatically get a char * refernce into the Python string p
460             path = p
461         ret = _comedilib_h.comedi_apply_calibration(
462             self.subdevice.device.device,
463             self.subdevice.index, self.index,
464             _constant.bitwise_value(self.range),
465             _constant.bitwise_value(self.aref),
466             path)
467         if ret < 0:
468             _error.raise_error(
469                 function_name='comedi_apply_calibration', ret=ret)
470         return ret
471
472     cdef _apply_parsed_calibration(self, _Calibration calibration):
473         ret = _comedilib_h.comedi_apply_parsed_calibration(
474             self.subdevice.device.device,
475             self.subdevice.index, self.index,
476             _constant.bitwise_value(self.range),
477             _constant.bitwise_value(self.aref),
478             calibration.calibration)
479         if ret < 0:
480             _error.raise_error(
481                 function_name='comedi_apply_parsed_calibration', ret=ret)
482         return ret
483
484     def apply_calibration(self, calibration=None, path=None):
485         """Apply a calibration to this channel configuration
486
487         `calibration` may None or a `Calibration` instance.  If it is
488         a `Calibration` instance, that instance is used for the
489         calibration.  Otherwise we look at `path`.  If `path` is None,
490         we use the default device calibration, otherwise we try and
491         use the calibration file located at `path`.
492         """
493         if calibration is not None:
494             self._apply_parsed_calibration(calibration)
495         elif path is not None:
496             self._apply_calibration(path)
497         else:
498             self._apply_calibration(NULL)