Source code for canlib.canlib.iopin

"""Experimental support for accessing IO-pins on sub modules of the Kvaser DIN
Rail SE 400S and variants that was added to CANlib v5.26.

.. versionadded:: 1.8

"""

import ctypes as ct

from ..cenum import CEnum
from ..versionnumber import VersionNumber
from . import wrapper

dll = wrapper.dll


def get_io_pin(channel, index):
    """Return io pin object for `index`

    Arguments:
        index (`int`): The global pin number

    Returns subclass of `IoPin` depending on pin type and direction:
    `AnalogIn`, `AnalogOut`, `DigitalIn`, `DigitalOut` or `Relay`.

    """
    io_pin = IoPin(channel, index)
    pin_class = _PIN_CLASSES[io_pin.pin_type][io_pin.direction]
    my_io_pin = pin_class(channel, index)
    return my_io_pin


def module_pin_names(module_type, prefix=''):
    """Return a list of names for `module_type`

    Returns a list of label names for the given type of module::

        >>> iopin.module_pin_names(iopin.ModuleType.ANALOG)
        ['AO1', 'AO2', 'AO3', 'AO4', 'AI1', 'AI2', 'AI3', 'AI4']

        >>> iopin.module_pin_names(iopin.ModuleType.RELAY)
        ['R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'DI1', 'DI2', 'DI3',
        'DI4', 'DI5', 'DI6', 'DI7', 'DI8']

    Args:
        module_type (`iopin.ModuleType`) : Type of module

    """
    if module_type == ModuleType.DIGITAL:
        pin_names = [f'{prefix}DO{x}' for x in range(1, 17)]
        pin_names += [f'{prefix}DI{x}' for x in range(1, 17)]
    elif module_type == ModuleType.ANALOG:
        pin_names = [f'{prefix}AO{x}' for x in range(1, 5)]
        pin_names += [f'{prefix}AI{x}' for x in range(1, 5)]
    elif module_type == ModuleType.RELAY:
        pin_names = [f'{prefix}R{x}' for x in range(1, 9)]
        pin_names += [f'{prefix}DI{x}' for x in range(1, 9)]
    elif module_type == ModuleType.INTERNAL:
        pin_names = ['DO1', 'DI1']
    else:
        raise AttributeError(f"{module_type} is an unknown ModuleType")
    return pin_names


def _create_pin_names(io_pins):
    """Create a list of names for the given list of `iopin.IoPin`.

    Used by `iopin.Configuration` to create a list of label pin names
    from a given list of `iopin.IoPin` s

    """
    module_index = 0
    pin_index = 0
    pin_names = []
    while pin_index < len(io_pins):
        module_index += 1
        names = module_pin_names(io_pins[pin_index].module_type, prefix=f'{module_index}:')
        pin_names += names
        pin_index += len(names)
    return pin_names


[docs]class AddonModule: """Contains information about one add-on module Args: module_type (`ModuleType`): The type of the add-on module. sw_version (`canlib.VersionNumber`): The software version in the add-on module. serial (int): The serial number of the add-on module. first_index (int): The index of the add-on modules first pin. .. versionadded:: 1.9 """ def __init__(self, module_type, fw_version=None, serial=None, first_pin_index=None): self.module_type = module_type self.fw_version = fw_version self.serial = serial self.first_pin_index = first_pin_index def __repr__(self): return "AddonModule(module_type={typ!r}, fw_version={fw!r}, serial={sn}), first_pin_index={fp}".format( typ=self.module_type, fw=self.fw_version, sn=self.serial, fp=self.first_pin_index )
[docs] def issubset(self, spec): """Check if current attributes are fulfilling attributes in *spec*. Any attribute in spec that is set to None is automatically considered fulfilled. The `fw_version` attribute is considered fulfilled when `self.fw_version >= spec.fw_version`. This can be used to check if a specific module fulfills a manually created specification:: >>> module_spec = [iopin.AddonModule(module_type=iopin.ModuleType.DIGITAL)] ... config = iopin.Configuration(channel) >>> config.modules [AddonModule(module_type=<ModuleType.DIGITAL: 1>, fw_version=VersionNumber(major=2, minor=5, release=None, build=None), serial=2342), first_pin_index=0] >>> config.issubset(module_spec) True >>> module_spec = [iopin.AddonModule( module_type=iopin.ModuleType.DIGITAL, fw_version=VersionNumber(major=3, minor=1), serial=2342)] >>> config.issubset(module_spec) False >>> module_spec = [ iopin.AddonModule(module_type=iopin.ModuleType.ANALOG), iopin.AddonModule(module_type=iopin.ModuleType.DIGITAL, fw_version=VersionNumber(major=3, minor=1), serial=2342)] >>> config.issubset(module_spec) False """ if spec.module_type is not None and self.module_type != spec.module_type: return False if spec.fw_version is not None and self.fw_version is None: return False if spec.fw_version is not None and self.fw_version < spec.fw_version: return False if spec.serial is not None and self.serial != spec.serial: return False if spec.first_pin_index is not None and self.first_pin_index != spec.first_pin_index: return False return True
[docs]class Configuration: """Contains I/O pins and the `.canlib.Channel` to find them on Creating this object may take some time depending on the number of I/O pins availably on the given `.canlib.Channel`. Args: channel (`~.canlib.Channel`): The channel where the discovery of I/O pins should take place. Attributes: io_pins (list(`IoPin`)): All discovered I/O pins. modules (list(`AddonModule`)): All included add-on-modules. pin_names (list(str)): List of label I/O pin names. pin_index (dict(str: int)): Dictionary with I/O pin label name as key, and pin index as value. To create an `.iopin.Configuration` you need to supply the `.canlib.Channel`, which is were we look for I/O pins: >>> from canlib.canlib import iopin ... from canlib import canlib, Device, EAN ... device = Device.find(ean=EAN('01059-8'), serial=225) ... channel = canlib.openChannel(device.channel_number(), canlib.Open.EXCLUSIVE) ... config = iopin.Configuration(channel) Now we can investigate a specific pin by index:: >>> config.pin(index=80) Pin 80: <PinType.ANALOG: 2> <Direction.OUT: 8> bits=12 range=0.0-10.0 (<ModuleType.ANALOG: 2>) It is also possible to find the label name from the index and vice verse for a pin, as well as access the pin using the label name:: >>> config.name(80) '4:AO1' >>> config.index('4:AO1') 80 >>> config.pin(name='4:AO1') Pin 80: <PinType.ANALOG: 2> <Direction.OUT: 8> bits=12 range=0.0-10.0 (<ModuleType.ANALOG: 2>) Note: A configuration needs to be confirmed using `.iopin.Configuration.confirm` (which calls `.Channel.io_confirm_config`) before accessing pin values:: >>> config.pin(name='4:AO1').value = 4 Traceback (most recent call last): File "<stdin>", line 1, in <module> File "...\\canlib\\canlib\\iopin.py", line 271, in value File "...\\canlib\\canlib\\dll.py", line 94, in _error_check raise can_error(result) canlib.canlib.exceptions.IoPinConfigurationNotConfirmed: I/O pin configuration is not confirmed (-45) I/O pin configuration is not confirmed (-45) >>> config.confirm() >>> config.pin(name='4:AO1').value = 4 A `Configuration` may be compared with an expected ordered list of `AddonModule` before confirming using `AddonModule.issubset` .. versionchanged:: 1.9 `Configuration.modules` is now an attribute, containing an ordered list of `AddonModule` objects. """ def __init__(self, channel): self._channel = channel self.io_pins = [] for index in range(self._channel.number_of_io_pins()): self.io_pins.append(self._channel.get_io_pin(index)) self.pin_names = _create_pin_names(self.io_pins) self.pin_index = {v: i for i, v in enumerate(self.pin_names)} self.modules = self._modules() def __iter__(self): """Returns an iterator for all pins known in configuration""" return iter(self.io_pins) def _modules(self): """Return an up-to-date list of modules in the configuration""" modules = [] pin_index = 0 while pin_index < len(self.io_pins): module = AddonModule( module_type=self.io_pins[pin_index].module_type, fw_version=self.io_pins[pin_index].fw_version, serial=self.io_pins[pin_index].serial, first_pin_index=pin_index, ) modules.append(module) pin_index += len(module_pin_names(module.module_type)) return modules
[docs] def confirm(self): """Confirm current configuration Convenience function that calls `.Channel.io_confirm_config`. """ self._channel.io_confirm_config()
[docs] def index(self, name): """Return index for pin with the given label name""" return self.pin_index[name]
[docs] def issubset(self, spec): """Check if attributes of modules in self is fulfilled by given spec This is a convenience method that calls `AddonModule.issubset` on all modules given by `self.modules` which can be used to check if the current configuration fulfills a manually created specification:: >>> config = iopin.Configuration(channel) >>> config_spec = [iopin.AddonModule(module_type=iopin.ModuleType.ANALOG), iopin.AddonModule(module_type=iopin.ModuleType.DIGITAL, fw_version=VersionNumber(major=3, minor=1), serial=2342)] >>> config.issubset(config_spec) False .. versionadded:: 1.9 """ if len(self.modules) != len(spec): return False return all((m.issubset(s) for m, s in zip(self.modules, spec)))
[docs] def name(self, index): """Return label name for pin with given index""" return self.pin_names[index]
[docs] def pin(self, index=None, name=None): """Return `IoPin` object using index or name Either `index` or `name` must be given, if both are given, the name will be used. Args: index (int): I/O pin index name (str): I/O pin name """ if name is not None: index = self.pin_index[name] return self.io_pins[index]
[docs]class Info(CEnum): """Enum used internally in `IoPin` for calls to `kvIoPinGetInfo` and `kvIoPinSetInfo`""" MODULE_TYPE = 1 #: One of `ModuleType` DIRECTION = 2 #: One of `Direction` PIN_TYPE = 4 #: One of `PinType` NUMBER_OF_BITS = 5 #: Resolution in number of bits. Read-only. RANGE_MIN = 6 #: A float that contains the lower range limit in volts. Read-only. RANGE_MAX = 7 #: A float that contains the upper range limit in volts. Read-only. DI_LOW_HIGH_FILTER = 8 """Time when a digital input pin goes from HIGH to LOW. Filter time in micro seconds when a digital input pin goes from HIGH to LOW. Range: 0 - 65000, Default 5000 us """ DI_HIGH_LOW_FILTER = 9 """Time when a digital input pin goes from LOW to HIGH. Filter time in micro seconds when a digital input pin goes from LOW to HIGH. Range: 0 - 65000, Default 5000 us """ AI_LP_FILTER_ORDER = 10 """The low-pass filter order for an analog input pin. 0 - 16, default 3 (sample time is 1 ms) """ AI_HYSTERESIS = 11 """The hysteresis in volt. The hysteresis in volt for an analog input pin, i.e. the amount the input have to change before the sampled value is updated. 0.0 - 10.0, default 0.3 """ MODULE_NUMBER = 14 #: The module number the pin belongs to. The number starts from 0. Read-only. SERIAL_NUMBER = 15 #: Serial number of the submodule the pin belongs to. Read-only. FW_VERSION = 16 #: Software version number of the submodule the pin belongs to. Read-only.
[docs]class ModuleType(CEnum): """Enum used for return values in `kvIoPinGetInfo`""" DIGITAL = 1 #: Digital Add-on (16 inputs, 16 outputs). ANALOG = 2 #: Analog Add-on (4 inputs, 4 outputs). RELAY = 3 #: Relay Add-on (8 inputs, 8 outputs). INTERNAL = 4 #: Internal Digital module (1 input, 1 output).
[docs]class PinType(CEnum): """Enum used for values in `Info`""" DIGITAL = 1 ANALOG = 2 RELAY = 3
[docs]class Direction(CEnum): """Enum used for values in `Info`""" IN = 4 #: Input OUT = 8 #: Output
[docs]class DigitalValue(CEnum): """Enum used digital values""" LOW = 0 HIGH = 1
[docs]class IoPin: """Base class of I/O ports""" def __init__(self, channel, pin): self.channel = channel self.pin = pin def __repr__(self): txt = "Pin {p}: {pt!r} {d!r} bits={nb} range={rmin}-{rmax} ({mt!r})".format( p=self.pin, pt=self.pin_type, d=self.direction, nb=self.number_of_bits, rmin=self.range_min, rmax=self.range_max, mt=self.module_type, ) return txt def _get_info(self, info, buf_type): buf = buf_type() dll.kvIoPinGetInfo(self.channel.handle, self.pin, info, ct.byref(buf), ct.sizeof(buf)) return buf.value def _set_info(self, info, c_value): dll.kvIoPinSetInfo( self.channel.handle, self.pin, info, ct.byref(c_value), ct.sizeof(c_value) ) @property def fw_version(self): """VersionNumber: Firmware version in module (Read-only)""" buf_type = ct.c_uint32 value = self._get_info(Info.FW_VERSION, buf_type) fw_version = VersionNumber((value >> 16) & 0xFFFF, value & 0xFFFF) return fw_version @property def direction(self): """`Direction`: Pin direction (Read-only)""" buf_type = ct.c_uint32 value = self._get_info(Info.DIRECTION, buf_type) return Direction(value) @property def module_type(self): """`ModuleType`: Type of module (Read-only)""" buf_type = ct.c_uint32 value = self._get_info(Info.MODULE_TYPE, buf_type) return ModuleType(value) @property def number_of_bits(self): """int: Resolution in number of bits (Read-only)""" buf_type = ct.c_uint32 value = self._get_info(Info.NUMBER_OF_BITS, buf_type) return value @property def pin_type(self): """`PinType`: Type of pin (Read-only)""" buf_type = ct.c_uint32 value = self._get_info(Info.PIN_TYPE, buf_type) return PinType(value) @property def range_min(self): """float: Lower range limit in volts (Read-only)""" buf_type = ct.c_float value = self._get_info(Info.RANGE_MIN, buf_type) return value @property def range_max(self): """float: Upper range limit in volts (Read-only)""" buf_type = ct.c_float value = self._get_info(Info.RANGE_MAX, buf_type) return value @property def serial(self): """int: Module serial number (Read-only)""" buf_type = ct.c_uint32 value = self._get_info(Info.SERIAL_NUMBER, buf_type) return value @property def value(self): """Base class does not implement value attribute""" raise AttributeError("can't get attribute value") @value.setter def value(self, value): raise AttributeError("can't set attribute value") @property def hysteresis(self): """Base class does not implement hysteresis attribute""" raise AttributeError("can't get attribute hysteresis") @hysteresis.setter def hysteresis(self, value): raise AttributeError("can't set attribute hysteresis") @property def lp_filter_order(self): """Base class does not implement lp_filter_order attribute""" raise AttributeError("can't get attribute lp_filter_order") @lp_filter_order.setter def lp_filter_order(self, value): raise AttributeError("can't set attribute lp_filter_order")
[docs]class AnalogIn(IoPin): def __repr__(self): txt = "Pin {p}: {pt!r} {d!r} bits={nb} range={rmin}-{rmax} LP_filter_order={lpfo} hysteresis={h} ({mt!r})".format( p=self.pin, pt=self.pin_type, d=self.direction, nb=self.number_of_bits, rmin=self.range_min, rmax=self.range_max, lpfo=self.lp_filter_order, h=self.hysteresis, mt=self.module_type, ) return txt @property def hysteresis(self): """The hysteresis in Volt for analog input pin""" buf_type = ct.c_float value = self._get_info(Info.AI_HYSTERESIS, buf_type) return value @hysteresis.setter def hysteresis(self, value): c_value = ct.c_float(value) self._set_info(Info.AI_HYSTERESIS, c_value) @property def lp_filter_order(self): """The low-pass filter order for analog input pin""" buf_type = ct.c_uint32 value = self._get_info(Info.AI_LP_FILTER_ORDER, buf_type) return value @lp_filter_order.setter def lp_filter_order(self, value): c_value = ct.c_uint32(value) self._set_info(Info.AI_LP_FILTER_ORDER, c_value) @property def value(self): """Voltage level on the Analog input pin""" voltage = ct.c_float() dll.kvIoPinGetAnalog(self.channel.handle, self.pin, ct.byref(voltage)) return voltage.value
[docs]class AnalogOut(IoPin): @property def value(self): """Voltage level on the Analog output pin""" voltage = ct.c_float() dll.kvIoPinGetOutputAnalog(self.channel.handle, self.pin, ct.byref(voltage)) return voltage.value @value.setter def value(self, value): voltage = ct.c_float(value) dll.kvIoPinSetAnalog(self.channel.handle, self.pin, voltage)
[docs]class DigitalIn(IoPin): def __repr__(self): txt = "Pin {p}: {pt!r} {d!r} bits={nb} range={rmin}-{rmax} HL_filter={hlf} LH_filter={lhf} ({mt!r})".format( p=self.pin, pt=self.pin_type, d=self.direction, nb=self.number_of_bits, rmin=self.range_min, rmax=self.range_max, lhf=self.low_high_filter, hlf=self.high_low_filter, mt=self.module_type, ) return txt @property def high_low_filter(self): """Filter time in micro seconds when a digital pin goes from HIGH to LOW""" buf_type = ct.c_uint32 value = self._get_info(Info.DI_HIGH_LOW_FILTER, buf_type) return value @high_low_filter.setter def high_low_filter(self, value): c_value = ct.c_uint32(value) self._set_info(Info.DI_HIGH_LOW_FILTER, c_value) @property def low_high_filter(self): """Filter time in micro seconds when a digital pin goes from LOW to HIGH""" buf_type = ct.c_uint32 value = self._get_info(Info.DI_LOW_HIGH_FILTER, buf_type) return value @low_high_filter.setter def low_high_filter(self, value): c_value = ct.c_uint32(value) self._set_info(Info.DI_LOW_HIGH_FILTER, c_value) @property def value(self): """Value on digital input pin (0 or 1)""" value = ct.c_uint() dll.kvIoPinGetDigital(self.channel.handle, self.pin, ct.byref(value)) return value.value
[docs]class DigitalOut(IoPin): @property def value(self): """Value on digital output pin (0 or 1)""" value = ct.c_uint() dll.kvIoPinGetOutputDigital(self.channel.handle, self.pin, ct.byref(value)) return value.value @value.setter def value(self, value): c_value = ct.c_uint(value) dll.kvIoPinSetDigital(self.channel.handle, self.pin, c_value)
[docs]class Relay(IoPin): @property def value(self): """Value on relay, `0` for off, `1` for on""" value = ct.c_uint() dll.kvIoPinGetOutputRelay(self.channel.handle, self.pin, ct.byref(value)) return value.value @value.setter def value(self, value): c_value = ct.c_uint(value) dll.kvIoPinSetRelay(self.channel.handle, self.pin, c_value) def __repr__(self): txt = "Pin {p}: {pt!r} {d!r} bits={nb} ({mt!r})".format( p=self.pin, pt=self.pin_type, d=self.direction, nb=self.number_of_bits, mt=self.module_type, ) return txt
_PIN_CLASSES = { PinType.RELAY: {Direction.IN: Relay, Direction.OUT: Relay}, PinType.ANALOG: {Direction.IN: AnalogIn, Direction.OUT: AnalogOut}, PinType.DIGITAL: {Direction.IN: DigitalIn, Direction.OUT: DigitalOut}, }