calibration: add Caldac, CalibrationSetting, and Calibration.
authorW. Trevor King <wking@tremily.us>
Tue, 16 Oct 2012 19:11:37 +0000 (15:11 -0400)
committerW. Trevor King <wking@tremily.us>
Wed, 17 Oct 2012 15:04:34 +0000 (11:04 -0400)
Add Python wrappers around comedi_calibration_t and its
sub-structures.  This allows you to load calibration files (soft and
hard) and manipulate their contents in Python.

pycomedi/calibration.pxd
pycomedi/calibration.pyx
pycomedi/device.pyx

index 8f7c6aff98f5776aa96c4fd4cf566006100730f8..047bf0bd8d6ee8d583b71890041621589379d142 100644 (file)
@@ -17,6 +17,8 @@
 "Expose `CalibratedConverter` internals at the C level for other Cython modules"
 
 cimport _comedilib_h
+from device cimport Device as _Device
+from subdevice cimport Subdevice as _Subdevice
 
 
 cdef class CalibratedConverter (object):
@@ -29,3 +31,21 @@ cdef class CalibratedConverter (object):
     cpdef get_to_physical_coefficients(self)
     cpdef get_from_physical_expansion_origin(self)
     cpdef get_from_physical_coefficients(self)
+
+
+cdef class Caldac (object):
+    cdef _comedilib_h.comedi_caldac_t *caldac
+
+
+cdef class CalibrationSetting (object):
+    cdef _comedilib_h.comedi_calibration_setting_t *setting
+    cdef public _Subdevice subdevice
+
+    cpdef _soft_calibration_set(self, CalibratedConverter value)
+
+
+cdef class Calibration (object):
+    cdef _comedilib_h.comedi_calibration_t *calibration
+    cdef public _Device device
+
+    cpdef from_file(self, path)
index 7d5dd80f8e2ec61f8d1d8914198f209da2916342..6e688b375b3fd86449ded838d0eb813aa002665d 100644 (file)
@@ -21,14 +21,42 @@ For one-off conversions, use the functions `comedi_to_physical` and
 `CalibratedConverter`.
 """
 
+from libc cimport stdlib as _stdlib
+from libc cimport string as _string
 cimport numpy as _numpy
 import numpy as _numpy
 
 cimport _comedi_h
 cimport _comedilib_h
+import _error
 import constant as _constant
 import utility as _utility
 
+
+cdef void _python_to_charp(char **charp, object obj, object encoding):
+    """Convert a Python string into a `char *`.
+
+    Cython automatically converts string or byte array to a `char *`
+    for use with external C libraries.  However, the resulting
+    pointers are only valid until the Python object is garbage
+    collected.  For the `Calibration` class, we need persistent
+    pointers that will be manually freed later.  This function creates
+    these manual copies.
+    """
+    cdef char *ret
+    cdef char *src
+    if charp[0] is not NULL:  # charp[0] is the same as *charp
+        _stdlib.free(charp[0])
+        charp[0] = NULL
+    if hasattr(obj, 'encode'):
+        obj = obj.encode(encoding, 'strict')
+    src = obj
+    ret = <char *> _stdlib.malloc(len(obj) + 1)
+    if ret is NULL:
+        raise MemoryError()
+    _string.strncpy(ret, src, len(obj) + 1)
+    charp[0] = ret
+
 cdef void _setup_comedi_polynomial_t(
     _comedilib_h.comedi_polynomial_t *p, coefficients, expansion_origin):
     """Setup the `comedi_polynomial_t` at `p`
@@ -188,4 +216,420 @@ cdef class CalibratedConverter (object):
         return ret
 
 
+cdef class Caldac (object):
+    """Class wrapping comedi_caldac_t
+
+    >>> from .device import Device
+    >>> from . import constant
+
+    >>> d = Device('/dev/comedi0')
+    >>> d.open()
+
+    >>> c = d.parse_calibration()
+    >>> s = c.settings[0]
+    >>> print(s)
+    <CalibrationSetting device:/dev/comedi0 subdevice:0>
+    >>> caldac = s.caldacs[0]
+    >>> print(caldac)
+    <Caldac subdevice:5 channel:4 value:255>
+
+    >>> d.close()
+    """
+    def __cinit__(self):
+        self.caldac = NULL
+
+    def __str__(self):
+        fields = ['{}:{}'.format(f, getattr(self, f))
+                  for f in ['subdevice', 'channel', 'value']]
+        return '<{} {}>'.format(self.__class__.__name__, ' '.join(fields))
+
+    def __repr__(self):
+        return self.__str__()
+
+    def _subdevice_get(self):
+        if self.caldac is not NULL:
+            return self.caldac.subdevice
+    def _subdevice_set(self, value):
+        assert self.caldac is not NULL, 'load caldac first'
+        self.caldac.subdevice = value
+    subdevice = property(fget=_subdevice_get, fset=_subdevice_set)
+
+    def _channel_get(self):
+        if self.caldac is not NULL:
+            return self.caldac.channel
+    def _channel_set(self, value):
+        assert self.caldac is not NULL, 'load caldac first'
+        self.caldac.channel = value
+    channel = property(fget=_channel_get, fset=_channel_set)
+
+    def _value_get(self):
+        if self.caldac is not NULL:
+            return self.caldac.value
+    def _value_set(self, value):
+        assert self.caldac is not NULL, 'load caldac first'
+        self.caldac.value = value
+    value = property(fget=_value_get, fset=_value_set)
+
+
+cdef class CalibrationSetting (object):
+    """Class wrapping comedi_calibration_setting_t
+
+    >>> from .device import Device
+    >>> from . import constant
+
+    >>> d = Device('/dev/comedi0')
+    >>> d.open()
+
+    >>> c = d.parse_calibration()
+    >>> s = c.settings[0]
+
+    >>> print(s)
+    <CalibrationSetting device:/dev/comedi0 subdevice:0>
+    >>> print(s.subdevice)  # doctest: +ELLIPSIS
+    <pycomedi.subdevice.Subdevice object at 0x...>
+    >>> for s in c.settings:
+    ...     print('{} {}'.format(s.subdevice.index, s.subdevice.get_type()))
+    ...     print('  channels: {}'.format(s.channels))
+    ...     print('  ranges: {}'.format(s.ranges))
+    ...     print('  arefs: {}'.format(s.arefs))
+    ...     print('  caldacs:')
+    ...     for caldac in s.caldacs:
+    ...         print('    {}'.format(caldac))
+    ...     print('  soft_calibration:')
+    ...     sc = s.soft_calibration
+    ...     print('    to physical coefficients: {}'.format(
+    ...         sc.get_to_physical_coefficients()))
+    ...     print('    to physical origin: {}'.format(
+    ...         sc.get_to_physical_expansion_origin()))
+    ...     print('    from physical coefficients: {}'.format(
+    ...         sc.get_from_physical_coefficients()))
+    ...     print('    from physical origin: {}'.format(
+    ...         sc.get_from_physical_expansion_origin()))
+    ... # doctest: +REPORT_UDIFF
+    0 ai
+      channels: []
+      ranges: []
+      arefs: []
+      caldacs:
+        <Caldac subdevice:5 channel:4 value:255>
+        <Caldac subdevice:5 channel:2 value:255>
+        <Caldac subdevice:5 channel:3 value:255>
+        <Caldac subdevice:5 channel:0 value:255>
+        <Caldac subdevice:5 channel:5 value:255>
+        <Caldac subdevice:5 channel:1 value:1>
+      soft_calibration:
+        to physical coefficients: [ 0.]
+        to physical origin: 0.0
+        from physical coefficients: [ 0.]
+        from physical origin: 0.0
+    0 ai
+      channels: []
+      ranges: [ 8  9 10 11 12 13 14 15]
+      arefs: []
+      caldacs:
+        <Caldac subdevice:5 channel:6 value:255>
+        <Caldac subdevice:5 channel:7 value:0>
+      soft_calibration:
+        to physical coefficients: [ 0.]
+        to physical origin: 0.0
+        from physical coefficients: [ 0.]
+        from physical origin: 0.0
+    1 ao
+      channels: [0]
+      ranges: [0 2]
+      arefs: []
+      caldacs:
+        <Caldac subdevice:5 channel:16 value:255>
+        <Caldac subdevice:5 channel:19 value:0>
+        <Caldac subdevice:5 channel:17 value:0>
+        <Caldac subdevice:5 channel:18 value:0>
+      soft_calibration:
+        to physical coefficients: [ 0.]
+        to physical origin: 0.0
+        from physical coefficients: [ 0.]
+        from physical origin: 0.0
+    1 ao
+      channels: [0]
+      ranges: [1 3]
+      arefs: []
+      caldacs:
+        <Caldac subdevice:5 channel:16 value:239>
+        <Caldac subdevice:5 channel:19 value:0>
+        <Caldac subdevice:5 channel:17 value:0>
+        <Caldac subdevice:5 channel:18 value:0>
+      soft_calibration:
+        to physical coefficients: [ 0.]
+        to physical origin: 0.0
+        from physical coefficients: [ 0.]
+        from physical origin: 0.0
+    1 ao
+      channels: [1]
+      ranges: [0 2]
+      arefs: []
+      caldacs:
+        <Caldac subdevice:5 channel:20 value:255>
+        <Caldac subdevice:5 channel:23 value:0>
+        <Caldac subdevice:5 channel:21 value:0>
+        <Caldac subdevice:5 channel:22 value:0>
+      soft_calibration:
+        to physical coefficients: [ 0.]
+        to physical origin: 0.0
+        from physical coefficients: [ 0.]
+        from physical origin: 0.0
+    1 ao
+      channels: [1]
+      ranges: [1 3]
+      arefs: []
+      caldacs:
+        <Caldac subdevice:5 channel:20 value:249>
+        <Caldac subdevice:5 channel:23 value:0>
+        <Caldac subdevice:5 channel:21 value:0>
+        <Caldac subdevice:5 channel:22 value:0>
+      soft_calibration:
+        to physical coefficients: [ 0.]
+        to physical origin: 0.0
+        from physical coefficients: [ 0.]
+        from physical origin: 0.0
+
+    >>> d.close()
+    """
+    def __cinit__(self):
+        self.setting = NULL
+
+    def __init__(self, subdevice):
+        super(CalibrationSetting, self).__init__()
+        self.subdevice = subdevice
+
+    def __str__(self):
+        fields = [
+            'device:{}'.format(self.subdevice.device.filename),
+            'subdevice:{}'.format(self.subdevice.index),
+            ]
+        return '<{} {}>'.format(self.__class__.__name__, ' '.join(fields))
+
+    def _channels_get(self):
+        if self.setting is NULL:
+            return None
+        ret = _numpy.ndarray(shape=(self.setting.num_channels,), dtype=int)
+        # TODO: point into existing data array?
+        for i in range(self.setting.num_channels):
+            ret[i] = self.setting.channels[i]
+        return ret
+    def _channels_set(self, value):
+        assert self.setting is not NULL, 'load setting first'
+        if self.setting.channels is not NULL:
+            _stdlib.free(self.setting.channels)
+        length = len(value)
+        self.setting.channels = <unsigned int *> _stdlib.malloc(
+            length * sizeof(unsigned int))
+        if self.setting.channels is NULL:
+            self.setting.num_channels = 0
+            raise MemoryError()
+        self.setting.num_channels = length
+        for i,x in enumerate(value):
+            if i >= length:
+                raise ValueError((i, length))
+            self.setting.channels[i] = x
+    channels = property(fget=_channels_get, fset=_channels_set)
+
+    def _ranges_get(self):
+        if self.setting is NULL:
+            return None
+        ret = _numpy.ndarray(shape=(self.setting.num_ranges,), dtype=int)
+        # TODO: point into existing data array?
+        for i in range(self.setting.num_ranges):
+            ret[i] = self.setting.ranges[i]
+        return ret
+    def _ranges_set(self, value):
+        assert self.setting is not NULL, 'load setting first'
+        if self.setting.ranges is not NULL:
+            _stdlib.free(self.setting.ranges)
+        length = len(value)
+        self.setting.ranges = <unsigned int *> _stdlib.malloc(
+            length * sizeof(unsigned int))
+        if self.setting.ranges is NULL:
+            self.setting.num_ranges = 0
+            raise MemoryError()
+        self.setting.num_ranges = length
+        for i,x in enumerate(value):
+            if i >= length:
+                raise ValueError((i, length))
+            self.setting.ranges[i] = x
+    ranges = property(fget=_ranges_get, fset=_ranges_set)
+
+    def _arefs_get(self):
+        if self.setting is NULL:
+            return None
+        ret = _numpy.ndarray(shape=(self.setting.num_arefs,), dtype=int)
+        # TODO: point into existing data array?
+        for i in range(self.setting.num_arefs):
+            ret[i] = self.setting.arefs[i]
+        return ret
+    def _arefs_set(self, value):
+        assert self.setting is not NULL, 'load setting first'
+        length = len(value)
+        for i,x in enumerate(value):
+            if i >= _comedilib_h.CS_MAX_AREFS_LENGTH:
+                raise ValueError((i, _comedilib_h.CS_MAX_AREFS_LENGTH))
+            self.setting.arefs[i] = x
+        for i in range(length, _comedilib_h.CS_MAX_AREFS_LENGTH):
+            self.setting.arefs[i] = 0
+    arefs = property(fget=_arefs_get, fset=_arefs_set)
+
+    def _caldacs_get(self):
+        if self.setting is NULL:
+            return None
+        if not self.setting.num_caldacs:
+            return []
+        # TODO: point into existing data array?
+        ret = []
+        for i in range(self.setting.num_caldacs):
+            c = Caldac()
+            c.caldac = &self.setting.caldacs[i]
+            ret.append(c)
+        return ret
+    def _caldacs_set(self, value):
+        assert self.setting is not NULL, 'load setting first'
+        if self.setting.caldacs is not NULL:
+            _stdlib.free(self.setting.caldacs)
+        length = len(value)
+        self.setting.caldacs = <_comedilib_h.comedi_caldac_t *> _stdlib.malloc(
+            length * sizeof(_comedilib_h.comedi_caldac_t))
+        if self.setting.caldacs is NULL:
+            self.setting.num_caldacs = 0
+            raise MemoryError()
+        self.setting.num_caldacs = length
+        for i,x in enumerate(value):
+            if i >= length:
+                raise ValueError((i, length))
+            self.setting.caldacs[i] = x
+    caldacs = property(fget=_caldacs_get, fset=_caldacs_set)
+
+    def _soft_calibration_get(self):
+        cdef CalibratedConverter ret
+        if self.setting is NULL:
+            return None
+        ret = CalibratedConverter()
+        if self.setting.soft_calibration.to_phys is not NULL:
+            ret._to_physical = self.setting.soft_calibration.to_phys[0]
+        if self.setting.soft_calibration.from_phys is not NULL:
+            ret._from_physical = self.setting.soft_calibration.from_phys[0]
+        return ret
+    cpdef _soft_calibration_set(self, CalibratedConverter value):
+        assert self.setting is not NULL, 'load setting first'
+        if (self.setting.soft_calibration.to_phys is NULL and
+            (value._to_physical.expansion_origin or
+             value._to_physical.order >= 0)):
+            self.setting.soft_calibration.to_phys = (
+                <_comedilib_h.comedi_polynomial_t *> _stdlib.malloc(
+                    sizeof(_comedilib_h.comedi_polynomial_t)))
+        self.setting.soft_calibration.to_phys[0] = value._to_physical
+        if (self.setting.soft_calibration.from_phys is NULL and
+            (value._from_physical.expansion_origin or
+             value._from_physical.order >= 0)):
+            self.setting.soft_calibration.from_phys = (
+                <_comedilib_h.comedi_polynomial_t *> _stdlib.malloc(
+                    sizeof(_comedilib_h.comedi_polynomial_t)))
+        self.setting.soft_calibration.from_phys[0] = value._from_physical
+    soft_calibration = property(
+        fget=_soft_calibration_get, fset=_soft_calibration_set)
+
+
+cdef class Calibration (object):
+    """A board calibration configuration.
+
+    Wraps comedi_calibration_t.
+
+    Warning: You probably want to use the `.from_file()` method or
+    `device.parse_calibration()` rather than initializing this
+    stucture by hand.
+
+    >>> from .device import Device
+    >>> from . import constant
+
+    >>> d = Device('/dev/comedi0')
+    >>> d.open()
+
+    >>> c = d.parse_calibration()
+
+    >>> print(c)
+    <Calibration device:/dev/comedi0>
+    >>> c.driver_name
+    'ni_pcimio'
+    >>> c.board_name
+    'pci-6052e'
+
+    >>> c.settings  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
+    [<pycomedi.calibration.CalibrationSetting object at 0x...>,
+     ...
+     <pycomedi.calibration.CalibrationSetting object at 0x...>]
+    >>> print(c.settings[0])
+    <CalibrationSetting device:/dev/comedi0 subdevice:0>
+
+    >>> name = c.driver_name
+    >>> c.driver_name = "Override with your own value"
+    >>> c.driver_name = name
+
+    >>> d.close()
+    """
+    def __cinit__(self):
+        self.calibration = NULL
+
+    def __init__(self, device):
+        super(Calibration, self).__init__()
+        self.device = device
+
+    def __dealloc__(self):
+        if self.calibration is not NULL:
+            _comedilib_h.comedi_cleanup_calibration(self.calibration)
+            self.calibration = NULL
+
+    def __str__(self):
+        fields = ['device:{}'.format(self.device.filename)]
+        return '<{} {}>'.format(self.__class__.__name__, ' '.join(fields))
+
+    def __repr__(self):
+        return '<{} {}>'.format(self.__class__.__name__, id(self))
+
+    def _driver_name_get(self):
+        if self.calibration is NULL:
+            return None
+        return self.calibration.driver_name
+    def _driver_name_set(self, value):
+        assert self.calibration is not NULL, 'load calibration first'
+        _python_to_charp(&self.calibration.driver_name, value, 'ascii')
+    driver_name = property(fget=_driver_name_get, fset=_driver_name_set)
+
+    def _board_name_get(self):
+        if self.calibration is NULL:
+            return None
+        return self.calibration.board_name
+    def _board_name_set(self, value):
+        assert self.calibration is not NULL, 'load calibration first'
+        _python_to_charp(&self.calibration.board_name, value, 'ascii')
+    board_name = property(fget=_board_name_get, fset=_board_name_set)
+
+    def _settings_get(self):
+        if self.calibration is NULL:
+            return None
+        ret = []
+        for i in range(self.calibration.num_settings):
+            s = CalibrationSetting(
+                subdevice=self.device.subdevice(
+                    index=self.calibration.settings[i].subdevice))
+            s.setting = &self.calibration.settings[i]
+            ret.append(s)
+        return ret
+    def _settings_set(self, value):
+        assert self.calibration is not NULL, 'load calibration first'
+        return None
+    settings = property(fget=_settings_get, fset=_settings_set)
+
+    cpdef from_file(self, path):
+        self.calibration = _comedilib_h.comedi_parse_calibration_file(path)
+        if self.calibration == NULL:
+            _error.raise_error(
+                function_name='comedi_parse_calibration_file')
+
+
 # TODO: see comedi_caldac_t and related at end of comedilib.h
index a7d6d16638afb2881e477798a9d91b1ba8d95e46..e8329d1423a9a987b5bd7267dbb448ce145cd76b 100644 (file)
@@ -24,6 +24,7 @@ from pycomedi import PyComediError as _PyComediError
 cimport _comedi_h
 cimport _comedilib_h
 import _error
+from calibration import Calibration as _Calibration
 from instruction cimport Insn as _Insn
 from instruction import Insn as _Insn
 from subdevice import Subdevice as _Subdevice
@@ -280,12 +281,9 @@ cdef class Device (object):
         """
         if path is None:
             path = self.get_default_calibration_path()
-
-        ret = _comedilib_h.comedi_parse_calibration_file(path)
-        if ret == NULL:
-            _error.raise_error(
-                function_name='comedi_parse_calibration_file')
-        return ret
+        c = _Calibration(device=self)
+        c.from_file(path)
+        return c
 
     # extensions to make a more idomatic Python interface