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
29 from pycomedi cimport _comedi_h
30 from pycomedi cimport _comedilib_h
32 from . import constant as _constant
33 from . import utility as _utility
36 cdef void _python_to_charp(
37 char **charp, object obj, object encoding) except *:
38 """Convert a Python string into a `char *`.
40 Cython automatically converts string or byte array to a `char *`
41 for use with external C libraries. However, the resulting
42 pointers are only valid until the Python object is garbage
43 collected. For the `Calibration` class, we need persistent
44 pointers that will be manually freed later. This function creates
49 if charp[0] is not NULL: # charp[0] is the same as *charp
50 _stdlib.free(charp[0])
52 if hasattr(obj, 'encode'):
53 obj = obj.encode(encoding, 'strict')
55 ret = <char *> _stdlib.malloc(len(obj) + 1)
58 _string.strncpy(ret, src, len(obj) + 1)
61 cdef void _setup_comedi_polynomial_t(
62 _comedilib_h.comedi_polynomial_t *p, coefficients, expansion_origin
64 """Setup the `comedi_polynomial_t` at `p`
66 * `coefficients` is an iterable containing polynomial coefficients
67 * `expansion_origin` is the center of the polynomial expansion
69 for i,x in enumerate(coefficients):
71 p.order = len(coefficients)-1
72 p.expansion_origin = expansion_origin
75 _comedilib_h.comedi_polynomial_t *p, object data, object direction):
76 """Apply the polynomial conversion `p` to `data`.
78 `direction` should be a value from `constant.CONVERSION_DIRECTION`.
80 to_physical = (_constant.bitwise_value(direction)
81 == _constant.CONVERSION_DIRECTION.to_physical.value)
82 if _numpy.isscalar(data):
84 return _comedilib_h.comedi_to_physical(data, p)
86 return _comedilib_h.comedi_from_physical(data, p)
90 dtype = _utility.lsampl
91 array = _numpy.array(data, dtype=dtype)
92 for i,d in enumerate(data):
94 array[i] = _comedilib_h.comedi_to_physical(d, p)
96 array[i] = _comedilib_h.comedi_from_physical(d, p)
99 cpdef comedi_to_physical(data, coefficients, expansion_origin):
100 """Convert Comedi bit values (`lsampl_t`) to physical units (`double`)
102 * `data` is the value to be converted (scalar or array-like)
103 * `coefficients` and `expansion_origin` should be appropriate
104 for `_setup_comedi_polynomial_t`. TODO: expose it's docstring?
106 The conversion algorithm is::
108 x = sum_i c_i * (d-d_o)^i
110 where `x` is the returned physical value, `d` is the supplied data,
111 `c_i` is the `i`\th coefficient, and `d_o` is the expansion origin.
113 >>> print comedi_to_physical.__doc__ # doctest: +ELLIPSIS
114 Convert Comedi bit values (`lsampl_t`) to physical units (`double`)
116 >>> comedi_to_physical(1, [1, 2, 3], 2)
118 >>> comedi_to_physical([1, 2, 3], [1, 2, 3], 2)
121 cdef _comedilib_h.comedi_polynomial_t p
122 _setup_comedi_polynomial_t(&p, coefficients, expansion_origin)
123 return _convert(&p, data, _constant.CONVERSION_DIRECTION.to_physical)
125 cpdef comedi_from_physical(data, coefficients, expansion_origin):
126 """Convert physical units to Comedi bit values
128 Like `comedi_to_physical` but converts `double` -> `lsampl_t`.
130 >>> comedi_from_physical(1, [1,2,3], 2)
132 >>> comedi_from_physical([1, 2, 3], [1, 2, 3], 2)
133 array([2, 1, 6], dtype=uint32)
135 cdef _comedilib_h.comedi_polynomial_t p
136 _setup_comedi_polynomial_t(&p, coefficients, expansion_origin)
137 return _convert(&p, data, _constant.CONVERSION_DIRECTION.from_physical)
140 cdef class CalibratedConverter (object):
141 """Apply a converion polynomial
143 Usually you would get the this converter from
144 `DataChannel.get_converter()` or similar. but for testing, we'll
145 just create one out of thin air.
147 >>> c = CalibratedConverter(
148 ... to_physical_coefficients=[1, 2, 3],
149 ... to_physical_expansion_origin=1)
150 >>> c # doctest: +NORMALIZE_WHITESPACE
152 to_physical:{coefficients:[1.0, 2.0, 3.0] origin:1.0}
153 from_physical:{coefficients:[0.0] origin:0.0}>
157 >>> c.to_physical([0, 1, 2])
159 >>> c.to_physical(_numpy.array([0, 1, 2, 3], dtype=_numpy.uint))
160 array([ 2., 1., 6., 17.])
162 >>> c.get_to_physical_expansion_origin()
164 >>> c.get_to_physical_coefficients()
167 For some soft-calibrated boards, there is no from_physical
168 conversion polynomial.
170 >>> c = CalibratedConverter(
171 ... from_physical_error=Exception('no conversion polynomial'))
172 >>> c.from_physical(1.0)
173 Traceback (most recent call last):
175 Exception: no conversion polynomial
177 However, even with the error, you can extract dummy coefficients.
179 >>> c.get_from_physical_expansion_origin()
181 >>> c.get_from_physical_coefficients()
185 self._from_physical_error = None
187 def __init__(self, to_physical_coefficients=None,
188 to_physical_expansion_origin=0,
189 from_physical_coefficients=None,
190 from_physical_expansion_origin=0,
191 from_physical_error=None):
192 if to_physical_coefficients:
193 _setup_comedi_polynomial_t(
194 &self._to_physical, to_physical_coefficients,
195 to_physical_expansion_origin)
196 if from_physical_coefficients:
197 _setup_comedi_polynomial_t(
198 &self._from_physical, from_physical_coefficients,
199 from_physical_expansion_origin)
200 self._from_physical_error = from_physical_error
202 cdef _str_poly(self, _comedilib_h.comedi_polynomial_t polynomial):
203 return '{coefficients:%s origin:%s}' % (
204 [float(polynomial.coefficients[i])
205 for i in range(polynomial.order+1)],
206 float(polynomial.expansion_origin))
209 return '<%s to_physical:%s from_physical:%s>' % (
210 self.__class__.__name__, self._str_poly(self._to_physical),
211 self._str_poly(self._from_physical))
214 return self.__str__()
216 cpdef to_physical(self, data):
217 return _convert(&self._to_physical, data,
218 _constant.CONVERSION_DIRECTION.to_physical)
220 cpdef from_physical(self, data):
221 if self._from_physical_error is not None:
222 raise self._from_physical_error
223 return _convert(&self._from_physical, data,
224 _constant.CONVERSION_DIRECTION.from_physical)
226 cpdef get_to_physical_expansion_origin(self):
227 return self._to_physical.expansion_origin
229 cpdef get_to_physical_coefficients(self):
230 ret = _numpy.ndarray((self._to_physical.order+1,), _numpy.double)
231 for i in xrange(len(ret)):
232 ret[i] = self._to_physical.coefficients[i]
235 cpdef get_from_physical_expansion_origin(self):
236 return self._from_physical.expansion_origin
238 cpdef get_from_physical_coefficients(self):
239 ret = _numpy.ndarray((self._from_physical.order+1,), _numpy.double)
240 for i in xrange(len(ret)):
241 ret[i] = self._from_physical.coefficients[i]
245 cdef class Caldac (object):
246 """Class wrapping comedi_caldac_t
248 >>> from .device import Device
249 >>> from . import constant
251 >>> d = Device('/dev/comedi0')
254 >>> c = d.parse_calibration()
255 >>> s = c.settings[0]
257 <CalibrationSetting device:/dev/comedi0 subdevice:0>
258 >>> caldac = s.caldacs[0]
260 <Caldac subdevice:5 channel:4 value:255>
264 You can also allocate `Caldac` instances on your own. The
265 allocated memory will be automatically freed when the instance is
268 >>> caldac = Caldac()
269 >>> caldac.subdevice == None
271 >>> caldac.subdevice = 1
272 Traceback (most recent call last):
274 AssertionError: load caldac first
275 >>> caldac.allocate()
278 >>> caldac.subdevice = 1
284 def __dealloc__(self):
285 if self.caldac is not NULL and self._local:
286 _stdlib.free(self.caldac)
291 fields = ['{}:{}'.format(f, getattr(self, f))
292 for f in ['subdevice', 'channel', 'value']]
293 return '<{} {}>'.format(self.__class__.__name__, ' '.join(fields))
296 return self.__str__()
299 assert not self._local, 'already allocated'
300 self.caldac = <_comedilib_h.comedi_caldac_t *> _stdlib.malloc(
301 sizeof(_comedilib_h.comedi_caldac_t *))
302 if self.caldac is NULL:
309 def _subdevice_get(self):
310 if self.caldac is not NULL:
311 return self.caldac.subdevice
312 def _subdevice_set(self, value):
313 assert self.caldac is not NULL, 'load caldac first'
314 self.caldac.subdevice = value
315 subdevice = property(fget=_subdevice_get, fset=_subdevice_set)
317 def _channel_get(self):
318 if self.caldac is not NULL:
319 return self.caldac.channel
320 def _channel_set(self, value):
321 assert self.caldac is not NULL, 'load caldac first'
322 self.caldac.channel = value
323 channel = property(fget=_channel_get, fset=_channel_set)
325 def _value_get(self):
326 if self.caldac is not NULL:
327 return self.caldac.value
328 def _value_set(self, value):
329 assert self.caldac is not NULL, 'load caldac first'
330 self.caldac.value = value
331 value = property(fget=_value_get, fset=_value_set)
334 cdef class CalibrationSetting (object):
335 """Class wrapping comedi_calibration_setting_t
337 >>> from .device import Device
338 >>> from . import constant
340 >>> d = Device('/dev/comedi0')
343 >>> c = d.parse_calibration()
344 >>> s = c.settings[0]
346 <CalibrationSetting device:/dev/comedi0 subdevice:0>
347 >>> print(s.subdevice) # doctest: +ELLIPSIS
348 <pycomedi.subdevice.Subdevice object at 0x...>
350 >>> for s in c.settings:
351 ... print('{} {}'.format(s.subdevice.index, s.subdevice.get_type()))
352 ... print(' channels: {}'.format(s.channels))
353 ... print(' ranges: {}'.format(s.ranges))
354 ... print(' arefs: {}'.format(s.arefs))
355 ... print(' caldacs:')
356 ... for caldac in s.caldacs:
357 ... print(' {}'.format(caldac))
358 ... print(' soft_calibration:')
359 ... sc = s.soft_calibration
360 ... print(' to physical coefficients: {}'.format(
361 ... sc.get_to_physical_coefficients()))
362 ... print(' to physical origin: {}'.format(
363 ... sc.get_to_physical_expansion_origin()))
364 ... print(' from physical coefficients: {}'.format(
365 ... sc.get_from_physical_coefficients()))
366 ... print(' from physical origin: {}'.format(
367 ... sc.get_from_physical_expansion_origin()))
368 ... # doctest: +REPORT_UDIFF
374 <Caldac subdevice:5 channel:4 value:255>
375 <Caldac subdevice:5 channel:2 value:255>
376 <Caldac subdevice:5 channel:3 value:255>
377 <Caldac subdevice:5 channel:0 value:255>
378 <Caldac subdevice:5 channel:5 value:255>
379 <Caldac subdevice:5 channel:1 value:1>
381 to physical coefficients: [ 0.]
382 to physical origin: 0.0
383 from physical coefficients: [ 0.]
384 from physical origin: 0.0
387 ranges: [ 8 9 10 11 12 13 14 15]
390 <Caldac subdevice:5 channel:6 value:255>
391 <Caldac subdevice:5 channel:7 value:0>
393 to physical coefficients: [ 0.]
394 to physical origin: 0.0
395 from physical coefficients: [ 0.]
396 from physical origin: 0.0
402 <Caldac subdevice:5 channel:16 value:255>
403 <Caldac subdevice:5 channel:19 value:0>
404 <Caldac subdevice:5 channel:17 value:0>
405 <Caldac subdevice:5 channel:18 value:0>
407 to physical coefficients: [ 0.]
408 to physical origin: 0.0
409 from physical coefficients: [ 0.]
410 from physical origin: 0.0
416 <Caldac subdevice:5 channel:16 value:239>
417 <Caldac subdevice:5 channel:19 value:0>
418 <Caldac subdevice:5 channel:17 value:0>
419 <Caldac subdevice:5 channel:18 value:0>
421 to physical coefficients: [ 0.]
422 to physical origin: 0.0
423 from physical coefficients: [ 0.]
424 from physical origin: 0.0
430 <Caldac subdevice:5 channel:20 value:255>
431 <Caldac subdevice:5 channel:23 value:0>
432 <Caldac subdevice:5 channel:21 value:0>
433 <Caldac subdevice:5 channel:22 value:0>
435 to physical coefficients: [ 0.]
436 to physical origin: 0.0
437 from physical coefficients: [ 0.]
438 from physical origin: 0.0
444 <Caldac subdevice:5 channel:20 value:249>
445 <Caldac subdevice:5 channel:23 value:0>
446 <Caldac subdevice:5 channel:21 value:0>
447 <Caldac subdevice:5 channel:22 value:0>
449 to physical coefficients: [ 0.]
450 to physical origin: 0.0
451 from physical coefficients: [ 0.]
452 from physical origin: 0.0
454 Test setting various attributes.
456 >>> s = c.settings[-1]
457 >>> s.channels = [0, 1, 2]
460 >>> s.ranges = [0, 1]
467 >>> for i in range(3):
468 ... caldac = Caldac()
469 ... caldac.allocate()
470 ... caldac.subdevice = i
471 ... caldac.channel = 2*i
472 ... caldac.value = 3*i
473 ... caldacs.append(caldac)
474 >>> s.caldacs = caldacs
480 self.subdevice = None
482 def __init__(self, subdevice):
483 super(CalibrationSetting, self).__init__()
484 self.subdevice = subdevice
488 'device:{}'.format(self.subdevice.device.filename),
489 'subdevice:{}'.format(self.subdevice.index),
491 return '<{} {}>'.format(self.__class__.__name__, ' '.join(fields))
493 def _channels_get(self):
494 if self.setting is NULL:
496 ret = _numpy.ndarray(shape=(self.setting.num_channels,), dtype=int)
497 # TODO: point into existing data array?
498 for i in range(self.setting.num_channels):
499 ret[i] = self.setting.channels[i]
501 def _channels_set(self, value):
502 assert self.setting is not NULL, 'load setting first'
503 if self.setting.channels is not NULL:
504 _stdlib.free(self.setting.channels)
506 self.setting.channels = <unsigned int *> _stdlib.malloc(
507 length * sizeof(unsigned int))
508 if self.setting.channels is NULL:
509 self.setting.num_channels = 0
511 self.setting.num_channels = length
512 for i,x in enumerate(value):
514 raise ValueError((i, length))
515 self.setting.channels[i] = x
516 channels = property(fget=_channels_get, fset=_channels_set)
518 def _ranges_get(self):
519 if self.setting is NULL:
521 ret = _numpy.ndarray(shape=(self.setting.num_ranges,), dtype=int)
522 # TODO: point into existing data array?
523 for i in range(self.setting.num_ranges):
524 ret[i] = self.setting.ranges[i]
526 def _ranges_set(self, value):
527 assert self.setting is not NULL, 'load setting first'
528 if self.setting.ranges is not NULL:
529 _stdlib.free(self.setting.ranges)
531 self.setting.ranges = <unsigned int *> _stdlib.malloc(
532 length * sizeof(unsigned int))
533 if self.setting.ranges is NULL:
534 self.setting.num_ranges = 0
536 self.setting.num_ranges = length
537 for i,x in enumerate(value):
539 raise ValueError((i, length))
540 self.setting.ranges[i] = x
541 ranges = property(fget=_ranges_get, fset=_ranges_set)
543 def _arefs_get(self):
544 if self.setting is NULL:
546 ret = _numpy.ndarray(shape=(self.setting.num_arefs,), dtype=int)
547 # TODO: point into existing data array?
548 for i in range(self.setting.num_arefs):
549 ret[i] = self.setting.arefs[i]
551 def _arefs_set(self, value):
552 assert self.setting is not NULL, 'load setting first'
554 for i,x in enumerate(value):
555 if i >= _comedilib_h.CS_MAX_AREFS_LENGTH:
556 raise ValueError((i, _comedilib_h.CS_MAX_AREFS_LENGTH))
557 self.setting.arefs[i] = x
558 for i in range(length, _comedilib_h.CS_MAX_AREFS_LENGTH):
559 self.setting.arefs[i] = 0
560 self.setting.num_arefs = length
561 arefs = property(fget=_arefs_get, fset=_arefs_set)
563 def _caldacs_get(self):
564 if self.setting is NULL:
566 if not self.setting.num_caldacs:
568 # TODO: point into existing data array?
570 for i in range(self.setting.num_caldacs):
572 c.caldac = &self.setting.caldacs[i]
575 cdef _caldacs_set_single(self, index, Caldac caldac):
576 self.setting.caldacs[index] = caldac.caldac[0]
577 def _caldacs_set(self, value):
578 assert self.setting is not NULL, 'load setting first'
579 if self.setting.caldacs is not NULL:
580 _stdlib.free(self.setting.caldacs)
582 self.setting.caldacs = <_comedilib_h.comedi_caldac_t *> _stdlib.malloc(
583 length * sizeof(_comedilib_h.comedi_caldac_t))
584 if self.setting.caldacs is NULL:
585 self.setting.num_caldacs = 0
587 self.setting.num_caldacs = length
588 for i,x in enumerate(value):
590 raise ValueError((i, length))
591 self._caldacs_set_single(i, x)
592 caldacs = property(fget=_caldacs_get, fset=_caldacs_set)
594 def _soft_calibration_get(self):
595 cdef CalibratedConverter ret
596 if self.setting is NULL:
598 ret = CalibratedConverter()
599 if self.setting.soft_calibration.to_phys is not NULL:
600 ret._to_physical = self.setting.soft_calibration.to_phys[0]
601 if self.setting.soft_calibration.from_phys is not NULL:
602 ret._from_physical = self.setting.soft_calibration.from_phys[0]
604 cpdef _soft_calibration_set(self, CalibratedConverter value):
605 assert self.setting is not NULL, 'load setting first'
606 if (self.setting.soft_calibration.to_phys is NULL and
607 (value._to_physical.expansion_origin or
608 value._to_physical.order >= 0)):
609 self.setting.soft_calibration.to_phys = (
610 <_comedilib_h.comedi_polynomial_t *> _stdlib.malloc(
611 sizeof(_comedilib_h.comedi_polynomial_t)))
612 self.setting.soft_calibration.to_phys[0] = value._to_physical
613 if (self.setting.soft_calibration.from_phys is NULL and
614 (value._from_physical.expansion_origin or
615 value._from_physical.order >= 0)):
616 self.setting.soft_calibration.from_phys = (
617 <_comedilib_h.comedi_polynomial_t *> _stdlib.malloc(
618 sizeof(_comedilib_h.comedi_polynomial_t)))
619 self.setting.soft_calibration.from_phys[0] = value._from_physical
620 soft_calibration = property(
621 fget=_soft_calibration_get, fset=_soft_calibration_set)
624 cdef class Calibration (object):
625 """A board calibration configuration.
627 Wraps comedi_calibration_t.
629 Warning: You probably want to use the `.from_file()` method or
630 `device.parse_calibration()` rather than initializing this
633 >>> from .device import Device
634 >>> from . import constant
636 >>> d = Device('/dev/comedi0')
639 >>> c = d.parse_calibration()
642 <Calibration device:/dev/comedi0>
648 >>> c.settings # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
649 [<pycomedi.calibration.CalibrationSetting object at 0x...>,
651 <pycomedi.calibration.CalibrationSetting object at 0x...>]
652 >>> print(c.settings[0])
653 <CalibrationSetting device:/dev/comedi0 subdevice:0>
655 >>> name = c.driver_name
656 >>> c.driver_name = "Override with your own value"
657 >>> c.driver_name = name
662 self.calibration = NULL
665 def __init__(self, device):
666 super(Calibration, self).__init__()
669 def __dealloc__(self):
670 if self.calibration is not NULL:
671 _comedilib_h.comedi_cleanup_calibration(self.calibration)
672 self.calibration = NULL
675 fields = ['device:{}'.format(self.device.filename)]
676 return '<{} {}>'.format(self.__class__.__name__, ' '.join(fields))
679 return '<{} {}>'.format(self.__class__.__name__, id(self))
681 def _driver_name_get(self):
682 if self.calibration is NULL:
684 return self.calibration.driver_name
685 def _driver_name_set(self, value):
686 assert self.calibration is not NULL, 'load calibration first'
687 _python_to_charp(&self.calibration.driver_name, value, 'ascii')
688 driver_name = property(fget=_driver_name_get, fset=_driver_name_set)
690 def _board_name_get(self):
691 if self.calibration is NULL:
693 return self.calibration.board_name
694 def _board_name_set(self, value):
695 assert self.calibration is not NULL, 'load calibration first'
696 _python_to_charp(&self.calibration.board_name, value, 'ascii')
697 board_name = property(fget=_board_name_get, fset=_board_name_set)
699 def _settings_get(self):
700 if self.calibration is NULL:
703 for i in range(self.calibration.num_settings):
704 s = <CalibrationSetting> CalibrationSetting(
705 subdevice=self.device.subdevice(
706 index=self.calibration.settings[i].subdevice))
707 s.setting = &self.calibration.settings[i]
710 def _settings_set(self, value):
711 assert self.calibration is not NULL, 'load calibration first'
713 settings = property(fget=_settings_get, fset=_settings_set)
715 cpdef from_file(self, path):
716 self.calibration = _comedilib_h.comedi_parse_calibration_file(path)
717 if self.calibration == NULL:
719 function_name='comedi_parse_calibration_file')
722 # TODO: see comedi_caldac_t and related at end of comedilib.h