calibration: add Caldac.allocate() for stand-alone caldac instances.
[pycomedi.git] / pycomedi / calibration.pyx
1 # Copyright (C) 2011-2012 W. Trevor King <wking@tremily.us>
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 """Pythonic wrappers for converting between Comedilib and physical units
18
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`.
22 """
23
24 from libc cimport stdlib as _stdlib
25 from libc cimport string as _string
26 cimport numpy as _numpy
27 import numpy as _numpy
28
29 cimport _comedi_h
30 cimport _comedilib_h
31 import _error
32 import constant as _constant
33 import utility as _utility
34
35
36 cdef void _python_to_charp(char **charp, object obj, object encoding):
37     """Convert a Python string into a `char *`.
38
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
44     these manual copies.
45     """
46     cdef char *ret
47     cdef char *src
48     if charp[0] is not NULL:  # charp[0] is the same as *charp
49         _stdlib.free(charp[0])
50         charp[0] = NULL
51     if hasattr(obj, 'encode'):
52         obj = obj.encode(encoding, 'strict')
53     src = obj
54     ret = <char *> _stdlib.malloc(len(obj) + 1)
55     if ret is NULL:
56         raise MemoryError()
57     _string.strncpy(ret, src, len(obj) + 1)
58     charp[0] = ret
59
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`
63
64     * `coefficients` is an iterable containing polynomial coefficients
65     * `expansion_origin` is the center of the polynomial expansion
66     """
67     for i,x in enumerate(coefficients):
68         p.coefficients[i] = x
69     p.order = len(coefficients)-1
70     p.expansion_origin = expansion_origin
71
72 cdef object _convert(
73     _comedilib_h.comedi_polynomial_t *p, object data, object direction):
74     """Apply the polynomial conversion `p` to `data`.
75
76     `direction` should be a value from `constant.CONVERSION_DIRECTION`.
77     """
78     to_physical = (_constant.bitwise_value(direction)
79                    == _constant.CONVERSION_DIRECTION.to_physical.value)
80     if _numpy.isscalar(data):
81         if to_physical:
82             return _comedilib_h.comedi_to_physical(data, p)
83         else:
84             return _comedilib_h.comedi_from_physical(data, p)
85     if to_physical:
86         dtype = _numpy.double
87     else:
88         dtype = _utility.lsampl
89     array = _numpy.array(data, dtype=dtype)
90     for i,d in enumerate(data):
91         if to_physical:
92             array[i] = _comedilib_h.comedi_to_physical(d, p)
93         else:
94             array[i] = _comedilib_h.comedi_from_physical(d, p)
95     return array
96
97 cpdef comedi_to_physical(data, coefficients, expansion_origin):
98     """Convert Comedi bit values (`lsampl_t`) to physical units (`double`)
99
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?
103
104     The conversion algorithm is::
105
106         x = sum_i c_i * (d-d_o)^i
107
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.
110
111     >>> print comedi_to_physical.__doc__  # doctest: +ELLIPSIS
112     Convert Comedi bit values (`lsampl_t`) to physical units (`double`)
113     ...
114     >>> comedi_to_physical(1, [1, 2, 3], 2)
115     2.0
116     >>> comedi_to_physical([1, 2, 3], [1, 2, 3], 2)
117     array([ 2.,  1.,  6.])
118     """
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)
122
123 cpdef comedi_from_physical(data, coefficients, expansion_origin):
124     """Convert physical units to Comedi bit values
125
126     Like `comedi_to_physical` but converts `double` -> `lsampl_t`.
127
128     >>> comedi_from_physical(1, [1,2,3], 2)
129     2L
130     >>> comedi_from_physical([1, 2, 3], [1, 2, 3], 2)
131     array([2, 1, 6], dtype=uint32)
132     """
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)
136
137
138 cdef class CalibratedConverter (object):
139     """Apply a converion polynomial
140
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.
144
145     >>> c = CalibratedConverter(
146     ...     to_physical_coefficients=[1, 2, 3],
147     ...     to_physical_expansion_origin=1)
148     >>> c  # doctest: +NORMALIZE_WHITESPACE
149     <CalibratedConverter
150      to_physical:{coefficients:[1.0, 2.0, 3.0] origin:1.0}
151      from_physical:{coefficients:[0.0] origin:0.0}>
152
153     >>> c.to_physical(1)
154     1.0
155     >>> c.to_physical([0, 1, 2])
156     array([ 2.,  1.,  6.])
157     >>> c.to_physical(_numpy.array([0, 1, 2, 3], dtype=_numpy.uint))
158     array([  2.,   1.,   6.,  17.])
159
160     >>> c.get_to_physical_expansion_origin()
161     1.0
162     >>> c.get_to_physical_coefficients()
163     array([ 1.,  2.,  3.])
164     """
165     def __init__(self, to_physical_coefficients=None,
166                  to_physical_expansion_origin=0,
167                  from_physical_coefficients=None,
168                  from_physical_expansion_origin=0):
169         if to_physical_coefficients:
170             _setup_comedi_polynomial_t(
171                 &self._to_physical, to_physical_coefficients,
172                  to_physical_expansion_origin)
173         if from_physical_coefficients:
174             _setup_comedi_polynomial_t(
175                 &self._from_physical, from_physical_coefficients,
176                  from_physical_expansion_origin)
177
178     cdef _str_poly(self, _comedilib_h.comedi_polynomial_t polynomial):
179         return '{coefficients:%s origin:%s}' % (
180             [float(polynomial.coefficients[i])
181              for i in range(polynomial.order+1)],
182             float(polynomial.expansion_origin))
183
184     def __str__(self):
185         return '<%s to_physical:%s from_physical:%s>' % (
186             self.__class__.__name__, self._str_poly(self._to_physical),
187             self._str_poly(self._from_physical))
188
189     def __repr__(self):
190         return self.__str__()
191
192     cpdef to_physical(self, data):
193         return _convert(&self._to_physical, data,
194                         _constant.CONVERSION_DIRECTION.to_physical)
195
196     cpdef from_physical(self, data):
197         return _convert(&self._from_physical, data,
198                         _constant.CONVERSION_DIRECTION.from_physical)
199
200     cpdef get_to_physical_expansion_origin(self):
201         return self._to_physical.expansion_origin
202
203     cpdef get_to_physical_coefficients(self):
204         ret = _numpy.ndarray((self._to_physical.order+1,), _numpy.double)
205         for i in xrange(len(ret)):
206             ret[i] = self._to_physical.coefficients[i]
207         return ret
208
209     cpdef get_from_physical_expansion_origin(self):
210         return self._from_physical.expansion_origin
211
212     cpdef get_from_physical_coefficients(self):
213         ret = _numpy.ndarray((self._from_physical.order+1,), _numpy.double)
214         for i in xrange(len(ret)):
215             ret[i] = self._from_physical.coefficients[i]
216         return ret
217
218
219 cdef class Caldac (object):
220     """Class wrapping comedi_caldac_t
221
222     >>> from .device import Device
223     >>> from . import constant
224
225     >>> d = Device('/dev/comedi0')
226     >>> d.open()
227
228     >>> c = d.parse_calibration()
229     >>> s = c.settings[0]
230     >>> print(s)
231     <CalibrationSetting device:/dev/comedi0 subdevice:0>
232     >>> caldac = s.caldacs[0]
233     >>> print(caldac)
234     <Caldac subdevice:5 channel:4 value:255>
235
236     >>> d.close()
237
238     You can also allocate `Caldac` instances on your own.  The
239     allocated memory will be automatically freed when the instance is
240     garbage collected.
241
242     >>> caldac = Caldac()
243     >>> caldac.subdevice == None
244     True
245     >>> caldac.subdevice = 1
246     Traceback (most recent call last):
247       ...
248     AssertionError: load caldac first
249     >>> caldac.allocate()
250     >>> caldac.subdevice
251     0L
252     >>> caldac.subdevice = 1
253     """
254     def __cinit__(self):
255         self.caldac = NULL
256         self._local = False
257
258     def __dealloc__(self):
259         if self.caldac is not NULL and self._local:
260             _stdlib.free(self.caldac)
261             self.caldac = NULL
262             self._local = False
263
264     def __str__(self):
265         fields = ['{}:{}'.format(f, getattr(self, f))
266                   for f in ['subdevice', 'channel', 'value']]
267         return '<{} {}>'.format(self.__class__.__name__, ' '.join(fields))
268
269     def __repr__(self):
270         return self.__str__()
271
272     def allocate(self):
273         assert not self._local, 'already allocated'
274         self.caldac = <_comedilib_h.comedi_caldac_t *> _stdlib.malloc(
275             sizeof(_comedilib_h.comedi_caldac_t *))
276         if self.caldac is NULL:
277             raise MemoryError()
278         self._local = True
279         self.subdevice = 0
280         self.channel = 0
281         self.value = 0
282
283     def _subdevice_get(self):
284         if self.caldac is not NULL:
285             return self.caldac.subdevice
286     def _subdevice_set(self, value):
287         assert self.caldac is not NULL, 'load caldac first'
288         self.caldac.subdevice = value
289     subdevice = property(fget=_subdevice_get, fset=_subdevice_set)
290
291     def _channel_get(self):
292         if self.caldac is not NULL:
293             return self.caldac.channel
294     def _channel_set(self, value):
295         assert self.caldac is not NULL, 'load caldac first'
296         self.caldac.channel = value
297     channel = property(fget=_channel_get, fset=_channel_set)
298
299     def _value_get(self):
300         if self.caldac is not NULL:
301             return self.caldac.value
302     def _value_set(self, value):
303         assert self.caldac is not NULL, 'load caldac first'
304         self.caldac.value = value
305     value = property(fget=_value_get, fset=_value_set)
306
307
308 cdef class CalibrationSetting (object):
309     """Class wrapping comedi_calibration_setting_t
310
311     >>> from .device import Device
312     >>> from . import constant
313
314     >>> d = Device('/dev/comedi0')
315     >>> d.open()
316
317     >>> c = d.parse_calibration()
318     >>> s = c.settings[0]
319
320     >>> print(s)
321     <CalibrationSetting device:/dev/comedi0 subdevice:0>
322     >>> print(s.subdevice)  # doctest: +ELLIPSIS
323     <pycomedi.subdevice.Subdevice object at 0x...>
324     >>> for s in c.settings:
325     ...     print('{} {}'.format(s.subdevice.index, s.subdevice.get_type()))
326     ...     print('  channels: {}'.format(s.channels))
327     ...     print('  ranges: {}'.format(s.ranges))
328     ...     print('  arefs: {}'.format(s.arefs))
329     ...     print('  caldacs:')
330     ...     for caldac in s.caldacs:
331     ...         print('    {}'.format(caldac))
332     ...     print('  soft_calibration:')
333     ...     sc = s.soft_calibration
334     ...     print('    to physical coefficients: {}'.format(
335     ...         sc.get_to_physical_coefficients()))
336     ...     print('    to physical origin: {}'.format(
337     ...         sc.get_to_physical_expansion_origin()))
338     ...     print('    from physical coefficients: {}'.format(
339     ...         sc.get_from_physical_coefficients()))
340     ...     print('    from physical origin: {}'.format(
341     ...         sc.get_from_physical_expansion_origin()))
342     ... # doctest: +REPORT_UDIFF
343     0 ai
344       channels: []
345       ranges: []
346       arefs: []
347       caldacs:
348         <Caldac subdevice:5 channel:4 value:255>
349         <Caldac subdevice:5 channel:2 value:255>
350         <Caldac subdevice:5 channel:3 value:255>
351         <Caldac subdevice:5 channel:0 value:255>
352         <Caldac subdevice:5 channel:5 value:255>
353         <Caldac subdevice:5 channel:1 value:1>
354       soft_calibration:
355         to physical coefficients: [ 0.]
356         to physical origin: 0.0
357         from physical coefficients: [ 0.]
358         from physical origin: 0.0
359     0 ai
360       channels: []
361       ranges: [ 8  9 10 11 12 13 14 15]
362       arefs: []
363       caldacs:
364         <Caldac subdevice:5 channel:6 value:255>
365         <Caldac subdevice:5 channel:7 value:0>
366       soft_calibration:
367         to physical coefficients: [ 0.]
368         to physical origin: 0.0
369         from physical coefficients: [ 0.]
370         from physical origin: 0.0
371     1 ao
372       channels: [0]
373       ranges: [0 2]
374       arefs: []
375       caldacs:
376         <Caldac subdevice:5 channel:16 value:255>
377         <Caldac subdevice:5 channel:19 value:0>
378         <Caldac subdevice:5 channel:17 value:0>
379         <Caldac subdevice:5 channel:18 value:0>
380       soft_calibration:
381         to physical coefficients: [ 0.]
382         to physical origin: 0.0
383         from physical coefficients: [ 0.]
384         from physical origin: 0.0
385     1 ao
386       channels: [0]
387       ranges: [1 3]
388       arefs: []
389       caldacs:
390         <Caldac subdevice:5 channel:16 value:239>
391         <Caldac subdevice:5 channel:19 value:0>
392         <Caldac subdevice:5 channel:17 value:0>
393         <Caldac subdevice:5 channel:18 value:0>
394       soft_calibration:
395         to physical coefficients: [ 0.]
396         to physical origin: 0.0
397         from physical coefficients: [ 0.]
398         from physical origin: 0.0
399     1 ao
400       channels: [1]
401       ranges: [0 2]
402       arefs: []
403       caldacs:
404         <Caldac subdevice:5 channel:20 value:255>
405         <Caldac subdevice:5 channel:23 value:0>
406         <Caldac subdevice:5 channel:21 value:0>
407         <Caldac subdevice:5 channel:22 value:0>
408       soft_calibration:
409         to physical coefficients: [ 0.]
410         to physical origin: 0.0
411         from physical coefficients: [ 0.]
412         from physical origin: 0.0
413     1 ao
414       channels: [1]
415       ranges: [1 3]
416       arefs: []
417       caldacs:
418         <Caldac subdevice:5 channel:20 value:249>
419         <Caldac subdevice:5 channel:23 value:0>
420         <Caldac subdevice:5 channel:21 value:0>
421         <Caldac subdevice:5 channel:22 value:0>
422       soft_calibration:
423         to physical coefficients: [ 0.]
424         to physical origin: 0.0
425         from physical coefficients: [ 0.]
426         from physical origin: 0.0
427
428     >>> d.close()
429     """
430     def __cinit__(self):
431         self.setting = NULL
432
433     def __init__(self, subdevice):
434         super(CalibrationSetting, self).__init__()
435         self.subdevice = subdevice
436
437     def __str__(self):
438         fields = [
439             'device:{}'.format(self.subdevice.device.filename),
440             'subdevice:{}'.format(self.subdevice.index),
441             ]
442         return '<{} {}>'.format(self.__class__.__name__, ' '.join(fields))
443
444     def _channels_get(self):
445         if self.setting is NULL:
446             return None
447         ret = _numpy.ndarray(shape=(self.setting.num_channels,), dtype=int)
448         # TODO: point into existing data array?
449         for i in range(self.setting.num_channels):
450             ret[i] = self.setting.channels[i]
451         return ret
452     def _channels_set(self, value):
453         assert self.setting is not NULL, 'load setting first'
454         if self.setting.channels is not NULL:
455             _stdlib.free(self.setting.channels)
456         length = len(value)
457         self.setting.channels = <unsigned int *> _stdlib.malloc(
458             length * sizeof(unsigned int))
459         if self.setting.channels is NULL:
460             self.setting.num_channels = 0
461             raise MemoryError()
462         self.setting.num_channels = length
463         for i,x in enumerate(value):
464             if i >= length:
465                 raise ValueError((i, length))
466             self.setting.channels[i] = x
467     channels = property(fget=_channels_get, fset=_channels_set)
468
469     def _ranges_get(self):
470         if self.setting is NULL:
471             return None
472         ret = _numpy.ndarray(shape=(self.setting.num_ranges,), dtype=int)
473         # TODO: point into existing data array?
474         for i in range(self.setting.num_ranges):
475             ret[i] = self.setting.ranges[i]
476         return ret
477     def _ranges_set(self, value):
478         assert self.setting is not NULL, 'load setting first'
479         if self.setting.ranges is not NULL:
480             _stdlib.free(self.setting.ranges)
481         length = len(value)
482         self.setting.ranges = <unsigned int *> _stdlib.malloc(
483             length * sizeof(unsigned int))
484         if self.setting.ranges is NULL:
485             self.setting.num_ranges = 0
486             raise MemoryError()
487         self.setting.num_ranges = length
488         for i,x in enumerate(value):
489             if i >= length:
490                 raise ValueError((i, length))
491             self.setting.ranges[i] = x
492     ranges = property(fget=_ranges_get, fset=_ranges_set)
493
494     def _arefs_get(self):
495         if self.setting is NULL:
496             return None
497         ret = _numpy.ndarray(shape=(self.setting.num_arefs,), dtype=int)
498         # TODO: point into existing data array?
499         for i in range(self.setting.num_arefs):
500             ret[i] = self.setting.arefs[i]
501         return ret
502     def _arefs_set(self, value):
503         assert self.setting is not NULL, 'load setting first'
504         length = len(value)
505         for i,x in enumerate(value):
506             if i >= _comedilib_h.CS_MAX_AREFS_LENGTH:
507                 raise ValueError((i, _comedilib_h.CS_MAX_AREFS_LENGTH))
508             self.setting.arefs[i] = x
509         for i in range(length, _comedilib_h.CS_MAX_AREFS_LENGTH):
510             self.setting.arefs[i] = 0
511     arefs = property(fget=_arefs_get, fset=_arefs_set)
512
513     def _caldacs_get(self):
514         if self.setting is NULL:
515             return None
516         if not self.setting.num_caldacs:
517             return []
518         # TODO: point into existing data array?
519         ret = []
520         for i in range(self.setting.num_caldacs):
521             c = Caldac()
522             c.caldac = &self.setting.caldacs[i]
523             ret.append(c)
524         return ret
525     def _caldacs_set(self, value):
526         assert self.setting is not NULL, 'load setting first'
527         if self.setting.caldacs is not NULL:
528             _stdlib.free(self.setting.caldacs)
529         length = len(value)
530         self.setting.caldacs = <_comedilib_h.comedi_caldac_t *> _stdlib.malloc(
531             length * sizeof(_comedilib_h.comedi_caldac_t))
532         if self.setting.caldacs is NULL:
533             self.setting.num_caldacs = 0
534             raise MemoryError()
535         self.setting.num_caldacs = length
536         for i,x in enumerate(value):
537             if i >= length:
538                 raise ValueError((i, length))
539             self.setting.caldacs[i] = x
540     caldacs = property(fget=_caldacs_get, fset=_caldacs_set)
541
542     def _soft_calibration_get(self):
543         cdef CalibratedConverter ret
544         if self.setting is NULL:
545             return None
546         ret = CalibratedConverter()
547         if self.setting.soft_calibration.to_phys is not NULL:
548             ret._to_physical = self.setting.soft_calibration.to_phys[0]
549         if self.setting.soft_calibration.from_phys is not NULL:
550             ret._from_physical = self.setting.soft_calibration.from_phys[0]
551         return ret
552     cpdef _soft_calibration_set(self, CalibratedConverter value):
553         assert self.setting is not NULL, 'load setting first'
554         if (self.setting.soft_calibration.to_phys is NULL and
555             (value._to_physical.expansion_origin or
556              value._to_physical.order >= 0)):
557             self.setting.soft_calibration.to_phys = (
558                 <_comedilib_h.comedi_polynomial_t *> _stdlib.malloc(
559                     sizeof(_comedilib_h.comedi_polynomial_t)))
560         self.setting.soft_calibration.to_phys[0] = value._to_physical
561         if (self.setting.soft_calibration.from_phys is NULL and
562             (value._from_physical.expansion_origin or
563              value._from_physical.order >= 0)):
564             self.setting.soft_calibration.from_phys = (
565                 <_comedilib_h.comedi_polynomial_t *> _stdlib.malloc(
566                     sizeof(_comedilib_h.comedi_polynomial_t)))
567         self.setting.soft_calibration.from_phys[0] = value._from_physical
568     soft_calibration = property(
569         fget=_soft_calibration_get, fset=_soft_calibration_set)
570
571
572 cdef class Calibration (object):
573     """A board calibration configuration.
574
575     Wraps comedi_calibration_t.
576
577     Warning: You probably want to use the `.from_file()` method or
578     `device.parse_calibration()` rather than initializing this
579     stucture by hand.
580
581     >>> from .device import Device
582     >>> from . import constant
583
584     >>> d = Device('/dev/comedi0')
585     >>> d.open()
586
587     >>> c = d.parse_calibration()
588
589     >>> print(c)
590     <Calibration device:/dev/comedi0>
591     >>> c.driver_name
592     'ni_pcimio'
593     >>> c.board_name
594     'pci-6052e'
595
596     >>> c.settings  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
597     [<pycomedi.calibration.CalibrationSetting object at 0x...>,
598      ...
599      <pycomedi.calibration.CalibrationSetting object at 0x...>]
600     >>> print(c.settings[0])
601     <CalibrationSetting device:/dev/comedi0 subdevice:0>
602
603     >>> name = c.driver_name
604     >>> c.driver_name = "Override with your own value"
605     >>> c.driver_name = name
606
607     >>> d.close()
608     """
609     def __cinit__(self):
610         self.calibration = NULL
611
612     def __init__(self, device):
613         super(Calibration, self).__init__()
614         self.device = device
615
616     def __dealloc__(self):
617         if self.calibration is not NULL:
618             _comedilib_h.comedi_cleanup_calibration(self.calibration)
619             self.calibration = NULL
620
621     def __str__(self):
622         fields = ['device:{}'.format(self.device.filename)]
623         return '<{} {}>'.format(self.__class__.__name__, ' '.join(fields))
624
625     def __repr__(self):
626         return '<{} {}>'.format(self.__class__.__name__, id(self))
627
628     def _driver_name_get(self):
629         if self.calibration is NULL:
630             return None
631         return self.calibration.driver_name
632     def _driver_name_set(self, value):
633         assert self.calibration is not NULL, 'load calibration first'
634         _python_to_charp(&self.calibration.driver_name, value, 'ascii')
635     driver_name = property(fget=_driver_name_get, fset=_driver_name_set)
636
637     def _board_name_get(self):
638         if self.calibration is NULL:
639             return None
640         return self.calibration.board_name
641     def _board_name_set(self, value):
642         assert self.calibration is not NULL, 'load calibration first'
643         _python_to_charp(&self.calibration.board_name, value, 'ascii')
644     board_name = property(fget=_board_name_get, fset=_board_name_set)
645
646     def _settings_get(self):
647         if self.calibration is NULL:
648             return None
649         ret = []
650         for i in range(self.calibration.num_settings):
651             s = CalibrationSetting(
652                 subdevice=self.device.subdevice(
653                     index=self.calibration.settings[i].subdevice))
654             s.setting = &self.calibration.settings[i]
655             ret.append(s)
656         return ret
657     def _settings_set(self, value):
658         assert self.calibration is not NULL, 'load calibration first'
659         return None
660     settings = property(fget=_settings_get, fset=_settings_set)
661
662     cpdef from_file(self, path):
663         self.calibration = _comedilib_h.comedi_parse_calibration_file(path)
664         if self.calibration == NULL:
665             _error.raise_error(
666                 function_name='comedi_parse_calibration_file')
667
668
669 # TODO: see comedi_caldac_t and related at end of comedilib.h