1 # Copyright (C) 2011-2012 W. Trevor King <wking@tremily.us>
3 # This file is part of pycomedi.
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
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.
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/>.
17 """Pythonic wrappers for converting between Comedilib and physical units
19 For one-off conversions, use the functions `comedi_to_physical` and
20 `comedi_from_physical`. For repeated conversions, use an instance of
21 `CalibratedConverter`.
24 from libc cimport stdlib as _stdlib
25 from libc cimport string as _string
26 cimport numpy as _numpy
27 import numpy as _numpy
32 import constant as _constant
33 import utility as _utility
36 cdef void _python_to_charp(char **charp, object obj, object encoding):
37 """Convert a Python string into a `char *`.
39 Cython automatically converts string or byte array to a `char *`
40 for use with external C libraries. However, the resulting
41 pointers are only valid until the Python object is garbage
42 collected. For the `Calibration` class, we need persistent
43 pointers that will be manually freed later. This function creates
48 if charp[0] is not NULL: # charp[0] is the same as *charp
49 _stdlib.free(charp[0])
51 if hasattr(obj, 'encode'):
52 obj = obj.encode(encoding, 'strict')
54 ret = <char *> _stdlib.malloc(len(obj) + 1)
57 _string.strncpy(ret, src, len(obj) + 1)
60 cdef void _setup_comedi_polynomial_t(
61 _comedilib_h.comedi_polynomial_t *p, coefficients, expansion_origin):
62 """Setup the `comedi_polynomial_t` at `p`
64 * `coefficients` is an iterable containing polynomial coefficients
65 * `expansion_origin` is the center of the polynomial expansion
67 for i,x in enumerate(coefficients):
69 p.order = len(coefficients)-1
70 p.expansion_origin = expansion_origin
73 _comedilib_h.comedi_polynomial_t *p, object data, object direction):
74 """Apply the polynomial conversion `p` to `data`.
76 `direction` should be a value from `constant.CONVERSION_DIRECTION`.
78 to_physical = (_constant.bitwise_value(direction)
79 == _constant.CONVERSION_DIRECTION.to_physical.value)
80 if _numpy.isscalar(data):
82 return _comedilib_h.comedi_to_physical(data, p)
84 return _comedilib_h.comedi_from_physical(data, p)
88 dtype = _utility.lsampl
89 array = _numpy.array(data, dtype=dtype)
90 for i,d in enumerate(data):
92 array[i] = _comedilib_h.comedi_to_physical(d, p)
94 array[i] = _comedilib_h.comedi_from_physical(d, p)
97 cpdef comedi_to_physical(data, coefficients, expansion_origin):
98 """Convert Comedi bit values (`lsampl_t`) to physical units (`double`)
100 * `data` is the value to be converted (scalar or array-like)
101 * `coefficients` and `expansion_origin` should be appropriate
102 for `_setup_comedi_polynomial_t`. TODO: expose it's docstring?
104 The conversion algorithm is::
106 x = sum_i c_i * (d-d_o)^i
108 where `x` is the returned physical value, `d` is the supplied data,
109 `c_i` is the `i`\th coefficient, and `d_o` is the expansion origin.
111 >>> print comedi_to_physical.__doc__ # doctest: +ELLIPSIS
112 Convert Comedi bit values (`lsampl_t`) to physical units (`double`)
114 >>> comedi_to_physical(1, [1, 2, 3], 2)
116 >>> comedi_to_physical([1, 2, 3], [1, 2, 3], 2)
119 cdef _comedilib_h.comedi_polynomial_t p
120 _setup_comedi_polynomial_t(&p, coefficients, expansion_origin)
121 return _convert(&p, data, _constant.CONVERSION_DIRECTION.to_physical)
123 cpdef comedi_from_physical(data, coefficients, expansion_origin):
124 """Convert physical units to Comedi bit values
126 Like `comedi_to_physical` but converts `double` -> `lsampl_t`.
128 >>> comedi_from_physical(1, [1,2,3], 2)
130 >>> comedi_from_physical([1, 2, 3], [1, 2, 3], 2)
131 array([2, 1, 6], dtype=uint32)
133 cdef _comedilib_h.comedi_polynomial_t p
134 _setup_comedi_polynomial_t(&p, coefficients, expansion_origin)
135 return _convert(&p, data, _constant.CONVERSION_DIRECTION.from_physical)
138 cdef class CalibratedConverter (object):
139 """Apply a converion polynomial
141 Usually you would get the this converter from
142 `DataChannel.get_converter()` or similar. but for testing, we'll
143 just create one out of thin air.
145 >>> c = CalibratedConverter(
146 ... to_physical_coefficients=[1, 2, 3],
147 ... to_physical_expansion_origin=1)
148 >>> c # doctest: +NORMALIZE_WHITESPACE
150 to_physical:{coefficients:[1.0, 2.0, 3.0] origin:1.0}
151 from_physical:{coefficients:[0.0] origin:0.0}>
155 >>> c.to_physical([0, 1, 2])
157 >>> c.to_physical(_numpy.array([0, 1, 2, 3], dtype=_numpy.uint))
158 array([ 2., 1., 6., 17.])
160 >>> c.get_to_physical_expansion_origin()
162 >>> c.get_to_physical_coefficients()
165 For some soft-calibrated boards, there is no from_physical
166 conversion polynomial.
168 >>> c = CalibratedConverter(
169 ... from_physical_error=Exception('no conversion polynomial'))
170 >>> c.from_physical(1.0)
171 Traceback (most recent call last):
173 Exception: no conversion polynomial
175 However, even with the error, you can extract dummy coefficients.
177 >>> c.get_from_physical_expansion_origin()
179 >>> c.get_from_physical_coefficients()
183 self._from_physical_error = None
185 def __init__(self, to_physical_coefficients=None,
186 to_physical_expansion_origin=0,
187 from_physical_coefficients=None,
188 from_physical_expansion_origin=0,
189 from_physical_error=None):
190 if to_physical_coefficients:
191 _setup_comedi_polynomial_t(
192 &self._to_physical, to_physical_coefficients,
193 to_physical_expansion_origin)
194 if from_physical_coefficients:
195 _setup_comedi_polynomial_t(
196 &self._from_physical, from_physical_coefficients,
197 from_physical_expansion_origin)
198 self._from_physical_error = from_physical_error
200 cdef _str_poly(self, _comedilib_h.comedi_polynomial_t polynomial):
201 return '{coefficients:%s origin:%s}' % (
202 [float(polynomial.coefficients[i])
203 for i in range(polynomial.order+1)],
204 float(polynomial.expansion_origin))
207 return '<%s to_physical:%s from_physical:%s>' % (
208 self.__class__.__name__, self._str_poly(self._to_physical),
209 self._str_poly(self._from_physical))
212 return self.__str__()
214 cpdef to_physical(self, data):
215 return _convert(&self._to_physical, data,
216 _constant.CONVERSION_DIRECTION.to_physical)
218 cpdef from_physical(self, data):
219 if self._from_physical_error is not None:
220 raise self._from_physical_error
221 return _convert(&self._from_physical, data,
222 _constant.CONVERSION_DIRECTION.from_physical)
224 cpdef get_to_physical_expansion_origin(self):
225 return self._to_physical.expansion_origin
227 cpdef get_to_physical_coefficients(self):
228 ret = _numpy.ndarray((self._to_physical.order+1,), _numpy.double)
229 for i in xrange(len(ret)):
230 ret[i] = self._to_physical.coefficients[i]
233 cpdef get_from_physical_expansion_origin(self):
234 return self._from_physical.expansion_origin
236 cpdef get_from_physical_coefficients(self):
237 ret = _numpy.ndarray((self._from_physical.order+1,), _numpy.double)
238 for i in xrange(len(ret)):
239 ret[i] = self._from_physical.coefficients[i]
243 cdef class Caldac (object):
244 """Class wrapping comedi_caldac_t
246 >>> from .device import Device
247 >>> from . import constant
249 >>> d = Device('/dev/comedi0')
252 >>> c = d.parse_calibration()
253 >>> s = c.settings[0]
255 <CalibrationSetting device:/dev/comedi0 subdevice:0>
256 >>> caldac = s.caldacs[0]
258 <Caldac subdevice:5 channel:4 value:255>
262 You can also allocate `Caldac` instances on your own. The
263 allocated memory will be automatically freed when the instance is
266 >>> caldac = Caldac()
267 >>> caldac.subdevice == None
269 >>> caldac.subdevice = 1
270 Traceback (most recent call last):
272 AssertionError: load caldac first
273 >>> caldac.allocate()
276 >>> caldac.subdevice = 1
282 def __dealloc__(self):
283 if self.caldac is not NULL and self._local:
284 _stdlib.free(self.caldac)
289 fields = ['{}:{}'.format(f, getattr(self, f))
290 for f in ['subdevice', 'channel', 'value']]
291 return '<{} {}>'.format(self.__class__.__name__, ' '.join(fields))
294 return self.__str__()
297 assert not self._local, 'already allocated'
298 self.caldac = <_comedilib_h.comedi_caldac_t *> _stdlib.malloc(
299 sizeof(_comedilib_h.comedi_caldac_t *))
300 if self.caldac is NULL:
307 def _subdevice_get(self):
308 if self.caldac is not NULL:
309 return self.caldac.subdevice
310 def _subdevice_set(self, value):
311 assert self.caldac is not NULL, 'load caldac first'
312 self.caldac.subdevice = value
313 subdevice = property(fget=_subdevice_get, fset=_subdevice_set)
315 def _channel_get(self):
316 if self.caldac is not NULL:
317 return self.caldac.channel
318 def _channel_set(self, value):
319 assert self.caldac is not NULL, 'load caldac first'
320 self.caldac.channel = value
321 channel = property(fget=_channel_get, fset=_channel_set)
323 def _value_get(self):
324 if self.caldac is not NULL:
325 return self.caldac.value
326 def _value_set(self, value):
327 assert self.caldac is not NULL, 'load caldac first'
328 self.caldac.value = value
329 value = property(fget=_value_get, fset=_value_set)
332 cdef class CalibrationSetting (object):
333 """Class wrapping comedi_calibration_setting_t
335 >>> from .device import Device
336 >>> from . import constant
338 >>> d = Device('/dev/comedi0')
341 >>> c = d.parse_calibration()
342 >>> s = c.settings[0]
344 <CalibrationSetting device:/dev/comedi0 subdevice:0>
345 >>> print(s.subdevice) # doctest: +ELLIPSIS
346 <pycomedi.subdevice.Subdevice object at 0x...>
348 >>> for s in c.settings:
349 ... print('{} {}'.format(s.subdevice.index, s.subdevice.get_type()))
350 ... print(' channels: {}'.format(s.channels))
351 ... print(' ranges: {}'.format(s.ranges))
352 ... print(' arefs: {}'.format(s.arefs))
353 ... print(' caldacs:')
354 ... for caldac in s.caldacs:
355 ... print(' {}'.format(caldac))
356 ... print(' soft_calibration:')
357 ... sc = s.soft_calibration
358 ... print(' to physical coefficients: {}'.format(
359 ... sc.get_to_physical_coefficients()))
360 ... print(' to physical origin: {}'.format(
361 ... sc.get_to_physical_expansion_origin()))
362 ... print(' from physical coefficients: {}'.format(
363 ... sc.get_from_physical_coefficients()))
364 ... print(' from physical origin: {}'.format(
365 ... sc.get_from_physical_expansion_origin()))
366 ... # doctest: +REPORT_UDIFF
372 <Caldac subdevice:5 channel:4 value:255>
373 <Caldac subdevice:5 channel:2 value:255>
374 <Caldac subdevice:5 channel:3 value:255>
375 <Caldac subdevice:5 channel:0 value:255>
376 <Caldac subdevice:5 channel:5 value:255>
377 <Caldac subdevice:5 channel:1 value:1>
379 to physical coefficients: [ 0.]
380 to physical origin: 0.0
381 from physical coefficients: [ 0.]
382 from physical origin: 0.0
385 ranges: [ 8 9 10 11 12 13 14 15]
388 <Caldac subdevice:5 channel:6 value:255>
389 <Caldac subdevice:5 channel:7 value:0>
391 to physical coefficients: [ 0.]
392 to physical origin: 0.0
393 from physical coefficients: [ 0.]
394 from physical origin: 0.0
400 <Caldac subdevice:5 channel:16 value:255>
401 <Caldac subdevice:5 channel:19 value:0>
402 <Caldac subdevice:5 channel:17 value:0>
403 <Caldac subdevice:5 channel:18 value:0>
405 to physical coefficients: [ 0.]
406 to physical origin: 0.0
407 from physical coefficients: [ 0.]
408 from physical origin: 0.0
414 <Caldac subdevice:5 channel:16 value:239>
415 <Caldac subdevice:5 channel:19 value:0>
416 <Caldac subdevice:5 channel:17 value:0>
417 <Caldac subdevice:5 channel:18 value:0>
419 to physical coefficients: [ 0.]
420 to physical origin: 0.0
421 from physical coefficients: [ 0.]
422 from physical origin: 0.0
428 <Caldac subdevice:5 channel:20 value:255>
429 <Caldac subdevice:5 channel:23 value:0>
430 <Caldac subdevice:5 channel:21 value:0>
431 <Caldac subdevice:5 channel:22 value:0>
433 to physical coefficients: [ 0.]
434 to physical origin: 0.0
435 from physical coefficients: [ 0.]
436 from physical origin: 0.0
442 <Caldac subdevice:5 channel:20 value:249>
443 <Caldac subdevice:5 channel:23 value:0>
444 <Caldac subdevice:5 channel:21 value:0>
445 <Caldac subdevice:5 channel:22 value:0>
447 to physical coefficients: [ 0.]
448 to physical origin: 0.0
449 from physical coefficients: [ 0.]
450 from physical origin: 0.0
452 Test setting various attributes.
454 >>> s = c.settings[-1]
455 >>> s.channels = [0, 1, 2]
458 >>> s.ranges = [0, 1]
465 >>> for i in range(3):
466 ... caldac = Caldac()
467 ... caldac.allocate()
468 ... caldac.subdevice = i
469 ... caldac.channel = 2*i
470 ... caldac.value = 3*i
471 ... caldacs.append(caldac)
472 >>> s.caldacs = caldacs
478 self.subdevice = None
480 def __init__(self, subdevice):
481 super(CalibrationSetting, self).__init__()
482 self.subdevice = subdevice
486 'device:{}'.format(self.subdevice.device.filename),
487 'subdevice:{}'.format(self.subdevice.index),
489 return '<{} {}>'.format(self.__class__.__name__, ' '.join(fields))
491 def _channels_get(self):
492 if self.setting is NULL:
494 ret = _numpy.ndarray(shape=(self.setting.num_channels,), dtype=int)
495 # TODO: point into existing data array?
496 for i in range(self.setting.num_channels):
497 ret[i] = self.setting.channels[i]
499 def _channels_set(self, value):
500 assert self.setting is not NULL, 'load setting first'
501 if self.setting.channels is not NULL:
502 _stdlib.free(self.setting.channels)
504 self.setting.channels = <unsigned int *> _stdlib.malloc(
505 length * sizeof(unsigned int))
506 if self.setting.channels is NULL:
507 self.setting.num_channels = 0
509 self.setting.num_channels = length
510 for i,x in enumerate(value):
512 raise ValueError((i, length))
513 self.setting.channels[i] = x
514 channels = property(fget=_channels_get, fset=_channels_set)
516 def _ranges_get(self):
517 if self.setting is NULL:
519 ret = _numpy.ndarray(shape=(self.setting.num_ranges,), dtype=int)
520 # TODO: point into existing data array?
521 for i in range(self.setting.num_ranges):
522 ret[i] = self.setting.ranges[i]
524 def _ranges_set(self, value):
525 assert self.setting is not NULL, 'load setting first'
526 if self.setting.ranges is not NULL:
527 _stdlib.free(self.setting.ranges)
529 self.setting.ranges = <unsigned int *> _stdlib.malloc(
530 length * sizeof(unsigned int))
531 if self.setting.ranges is NULL:
532 self.setting.num_ranges = 0
534 self.setting.num_ranges = length
535 for i,x in enumerate(value):
537 raise ValueError((i, length))
538 self.setting.ranges[i] = x
539 ranges = property(fget=_ranges_get, fset=_ranges_set)
541 def _arefs_get(self):
542 if self.setting is NULL:
544 ret = _numpy.ndarray(shape=(self.setting.num_arefs,), dtype=int)
545 # TODO: point into existing data array?
546 for i in range(self.setting.num_arefs):
547 ret[i] = self.setting.arefs[i]
549 def _arefs_set(self, value):
550 assert self.setting is not NULL, 'load setting first'
552 for i,x in enumerate(value):
553 if i >= _comedilib_h.CS_MAX_AREFS_LENGTH:
554 raise ValueError((i, _comedilib_h.CS_MAX_AREFS_LENGTH))
555 self.setting.arefs[i] = x
556 for i in range(length, _comedilib_h.CS_MAX_AREFS_LENGTH):
557 self.setting.arefs[i] = 0
558 self.setting.num_arefs = length
559 arefs = property(fget=_arefs_get, fset=_arefs_set)
561 def _caldacs_get(self):
562 if self.setting is NULL:
564 if not self.setting.num_caldacs:
566 # TODO: point into existing data array?
568 for i in range(self.setting.num_caldacs):
570 c.caldac = &self.setting.caldacs[i]
573 cdef _caldacs_set_single(self, index, Caldac caldac):
574 self.setting.caldacs[index] = caldac.caldac[0]
575 def _caldacs_set(self, value):
576 assert self.setting is not NULL, 'load setting first'
577 if self.setting.caldacs is not NULL:
578 _stdlib.free(self.setting.caldacs)
580 self.setting.caldacs = <_comedilib_h.comedi_caldac_t *> _stdlib.malloc(
581 length * sizeof(_comedilib_h.comedi_caldac_t))
582 if self.setting.caldacs is NULL:
583 self.setting.num_caldacs = 0
585 self.setting.num_caldacs = length
586 for i,x in enumerate(value):
588 raise ValueError((i, length))
589 self._caldacs_set_single(i, x)
590 caldacs = property(fget=_caldacs_get, fset=_caldacs_set)
592 def _soft_calibration_get(self):
593 cdef CalibratedConverter ret
594 if self.setting is NULL:
596 ret = CalibratedConverter()
597 if self.setting.soft_calibration.to_phys is not NULL:
598 ret._to_physical = self.setting.soft_calibration.to_phys[0]
599 if self.setting.soft_calibration.from_phys is not NULL:
600 ret._from_physical = self.setting.soft_calibration.from_phys[0]
602 cpdef _soft_calibration_set(self, CalibratedConverter value):
603 assert self.setting is not NULL, 'load setting first'
604 if (self.setting.soft_calibration.to_phys is NULL and
605 (value._to_physical.expansion_origin or
606 value._to_physical.order >= 0)):
607 self.setting.soft_calibration.to_phys = (
608 <_comedilib_h.comedi_polynomial_t *> _stdlib.malloc(
609 sizeof(_comedilib_h.comedi_polynomial_t)))
610 self.setting.soft_calibration.to_phys[0] = value._to_physical
611 if (self.setting.soft_calibration.from_phys is NULL and
612 (value._from_physical.expansion_origin or
613 value._from_physical.order >= 0)):
614 self.setting.soft_calibration.from_phys = (
615 <_comedilib_h.comedi_polynomial_t *> _stdlib.malloc(
616 sizeof(_comedilib_h.comedi_polynomial_t)))
617 self.setting.soft_calibration.from_phys[0] = value._from_physical
618 soft_calibration = property(
619 fget=_soft_calibration_get, fset=_soft_calibration_set)
622 cdef class Calibration (object):
623 """A board calibration configuration.
625 Wraps comedi_calibration_t.
627 Warning: You probably want to use the `.from_file()` method or
628 `device.parse_calibration()` rather than initializing this
631 >>> from .device import Device
632 >>> from . import constant
634 >>> d = Device('/dev/comedi0')
637 >>> c = d.parse_calibration()
640 <Calibration device:/dev/comedi0>
646 >>> c.settings # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
647 [<pycomedi.calibration.CalibrationSetting object at 0x...>,
649 <pycomedi.calibration.CalibrationSetting object at 0x...>]
650 >>> print(c.settings[0])
651 <CalibrationSetting device:/dev/comedi0 subdevice:0>
653 >>> name = c.driver_name
654 >>> c.driver_name = "Override with your own value"
655 >>> c.driver_name = name
660 self.calibration = NULL
663 def __init__(self, device):
664 super(Calibration, self).__init__()
667 def __dealloc__(self):
668 if self.calibration is not NULL:
669 _comedilib_h.comedi_cleanup_calibration(self.calibration)
670 self.calibration = NULL
673 fields = ['device:{}'.format(self.device.filename)]
674 return '<{} {}>'.format(self.__class__.__name__, ' '.join(fields))
677 return '<{} {}>'.format(self.__class__.__name__, id(self))
679 def _driver_name_get(self):
680 if self.calibration is NULL:
682 return self.calibration.driver_name
683 def _driver_name_set(self, value):
684 assert self.calibration is not NULL, 'load calibration first'
685 _python_to_charp(&self.calibration.driver_name, value, 'ascii')
686 driver_name = property(fget=_driver_name_get, fset=_driver_name_set)
688 def _board_name_get(self):
689 if self.calibration is NULL:
691 return self.calibration.board_name
692 def _board_name_set(self, value):
693 assert self.calibration is not NULL, 'load calibration first'
694 _python_to_charp(&self.calibration.board_name, value, 'ascii')
695 board_name = property(fget=_board_name_get, fset=_board_name_set)
697 def _settings_get(self):
698 if self.calibration is NULL:
701 for i in range(self.calibration.num_settings):
702 s = <CalibrationSetting> CalibrationSetting(
703 subdevice=self.device.subdevice(
704 index=self.calibration.settings[i].subdevice))
705 s.setting = &self.calibration.settings[i]
708 def _settings_set(self, value):
709 assert self.calibration is not NULL, 'load calibration first'
711 settings = property(fget=_settings_get, fset=_settings_set)
713 cpdef from_file(self, path):
714 self.calibration = _comedilib_h.comedi_parse_calibration_file(path)
715 if self.calibration == NULL:
717 function_name='comedi_parse_calibration_file')
720 # TODO: see comedi_caldac_t and related at end of comedilib.h