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