# Copyright (C) 2020 Alex Sonea
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import inspect
from ..utils import check_type, check_options, check_not_empty
from .device import BaseDevice
from .sync import BaseSync
logger = logging.getLogger(__name__)
[docs]class BaseRegister():
"""A minimal representation of a device register.
Parameters
----------
name: str
The name of the register
device: BaseDevice or subclass
The device where the register is attached to
address: int (typpically but some devices might use other addressing)
The register address
size: int
The register size in bytes; defaults to 1
minim: int
Minimum value represented in register in internal format; defaults to 0
maxim: int
Maximum value represented in register; defaults to 2^size - 1.
The setter method for internal value will check that the desired
value is within the [min, max] and trim it accordingly
access: str
Read ('R') or read-write ('RW'); default 'R'
clone: BaseRegister or subclass or ``None``
Indicates if the register is a clone; this value provides the
reference to the register object that acts as the main register
in interation with the communication bus. This allows you to define
multiple represtnations of the same physical register (at a given
address) with the purpose of having different external
representations. For example:
- you can have a position register that can provide the external
value in degrees or radians,
- a velocity register that can provide the external value in degrees
per second, radians per second or rotations per minute,
- a byte register that reads 8 inputs and mask them each as a
:py:class:`BoolRegister` with a different bit mask
In the device definition YAML file use ``True`` to indicate if a
register is a clone. The device constructor will replace the reference
of the main register with the same address in the constructor of this
register.
sync: bool
``True`` if the register will be updated from the real device
using a sync loop. If `sync` is ``False`` access to the register
through the value property will invoke reading / writing to the real
register; default ``False``
word: bool
Indicates that the register is a ``word`` register (16 bits) instead
of a usual 8 bits. Some I2C and SPI devices use 16bit registers
and need to use separate access functions to read them as opposed to
the 8 bit registers. Default is ``False`` which effectively makes it
an 8 bit register
order: ``LH`` or ``HL``
Applicable only for registers with size > 1 that represent a value
over successive internal registers, but for convenience are groupped
as one single register with size 2 (or higher).
``LH`` means low-high and indicates the bytes in the registry are
organized starting with the low byte first. ``HL`` indicates that
the registers are with the high byte first. Technically the ``read``
and ``write`` functions always read the bytes in the order they
are stored in the device and if the register is marked as ``HL`` the
list is reversed before being returned to the requester or processed
as a number in case the ``bulk`` is ``False``. Default is ``LH``.
default: int
The default value for the register; implicit 0
"""
[docs] def __init__(self, name='REGISTER', device=None, address=0, clone=None,
size=1, minim=0, maxim=None, access='R', sync=False,
word=False, bulk=True, order='LH', default=0, **kwargs):
# these are already checked by the device
self.__name = name
# device
check_not_empty(device, 'device', 'register', self.name, logger)
check_type(device, BaseDevice, 'register', self.name, logger)
self.__device = device
# address
if address != 0:
check_not_empty(address, 'address', 'register', self.name, logger)
self.__address = address
# clone
self.__clone = clone
if clone:
check_type(clone, BaseRegister, 'register', self.name, logger)
# clone registers inherit the settings from the original
self.__size = clone.size
self.__minim = clone.minim
self.__maxim = clone.maxim
self.__access = clone.access
self.__word = clone.word
self.__order = clone.order
else:
# size
check_not_empty(size, 'size', 'register', self.name, logger)
check_type(size, int, 'register', self.name, logger)
self.__size = size
# minim
check_type(minim, int, 'register', self.name, logger)
self.__minim = minim
# maxim
if maxim:
check_type(maxim, int, 'register', self.name, logger)
self.__maxim = maxim
else:
self.__maxim = pow(2, self.size * 8) - 1
# access
check_options(access, ['R', 'RW'], 'register', self.name, logger)
self.__access = access
# sync
check_options(sync, [True, False], 'register', self.name, logger)
self.__sync = sync
# word
check_options(word, [True, False], 'register', self.name, logger)
self.__word = word
# bulk
check_options(bulk, [True, False], 'register', self.name, logger)
self.__bulk = bulk
# order
check_options(order, ['LH', 'HL'], 'register', self.name, logger)
self.__order = order
# default
check_type(default, int, 'register', self.name, logger)
self.__default = default
self.__int_value = self.default
@property
def name(self):
"""Register's name."""
return self.__name
@property
def device(self):
"""The device the register belongs to."""
return self.__device
@property
def address(self):
"""The register's address in the device."""
return self.__address
@property
def clone(self):
"""Indicates the register is a clone of another."""
return self.__clone
@property
def size(self):
"""The regster's size in Bytes."""
return self.__size
@property
def minim(self):
"""The register's minimum value in internal format."""
return self.__minim
@property
def maxim(self):
"""The register's maximum value in internal format."""
return self.__maxim
@property
def range(self):
"""Tuple with (minim, maxim) values in internal format."""
return (self.__minim, self.__maxim)
@property
def min_ext(self):
"""The register's minimum value in external format."""
return self.value_to_external(self.minim)
@property
def max_ext(self):
"""The register's maximum value in external format."""
return self.value_to_external(self.maxim)
@property
def range_ext(self):
"""Tuple with (minim, maxim) values in external format."""
return (self.min_ext, self.max_ext)
@property
def access(self):
"""Register's access mode."""
return self.__access
@property
def sync(self):
"""Register is subject to a sync loop update."""
if self.clone:
return self.clone.sync
return self.__sync
@sync.setter
def sync(self, value):
"""Sets the register as being synced by a loop. Only subclasses
of :py:class:`BaseSync` are allowed to do this change."""
caller = inspect.stack()[1].frame.f_locals['self']
if isinstance(caller, (BaseSync, BaseRegister)):
if self.clone:
self.clone.sync = (value is True)
else:
self.__sync = (value is True)
else:
logger.error('only BaseSync subclasses can chance the sync '
'flag of a register')
@property
def word(self):
"""Indicates if the register is an 16 bit register (``True``) or
an 8 bit register.
"""
return self.__word
@property
def order(self):
"""Indicates the order of the data representartion; low-high (LH)
or high-low (HL)
"""
return self.__order
@property
def default(self):
"""The register's default value in internal format."""
if self.clone:
return self.clone.default
return self.__default
@property
def int_value(self):
"""Internal value of register, if a clone return the value of the
main register."""
if self.clone:
return self.clone.int_value
return self.__int_value
@int_value.setter
def int_value(self, value):
"""If clone, store the value in the main register."""
if self.clone:
self.clone.int_value = value
else:
self.__int_value = value
[docs] def value_to_external(self, value):
"""Converts the presented value to external format according to
register's settings. This method should be overridden by subclasses
in case they have specific conversions to do.
.. see also: :py:class:`BoolRegister`,
:py:class:`RegisterWithConversion`,
:py:class:`RegisterWithThreshold`
Parameters
----------
value: int
A value (internal representation) to be converted.
Returns
-------
int
For ``BaseRegister`` it returns the same value unchanged.
"""
return value
[docs] def value_to_internal(self, value):
"""Converts the presented value to internal format according to
register's settings. This method should be overridden by subclasses
in case they have specific conversions to do.
.. see also: :py:class:`BoolRegister`,
:py:class:`RegisterWithConversion`,
:py:class:`RegisterWithThreshold`
Parameters
----------
value: int
A value (external representation) to be converted.
Returns
-------
int
For ``BaseRegister`` it returns the same value unchanged.
"""
return value
@property
def value(self):
"""Provides the value of the register in external format. If the
register is not marked for ``sync`` then it requests the device
to perform a ``read`` in order to refresh the content of the
register.
Returns
-------
any
The value of the register in the external format. It invokes
:py:meth:`value_to_external` which can be overridden by
subclasses to provide different representations of the register's
value (hence the ``any`` return type).
"""
if not self.sync:
self.read()
return self.value_to_external(self.int_value)
@value.setter
def value(self, value):
"""Updates the internal value of the register with a value provided
in external format. It invokes the :py:meth:`value_to_internal`
method to perform the conversion. If the register's ``sync`` is not
``True`` it will ask the device to initiate a ``write`` of the data
to the device. The method also checks if the converted value sits in
the allowed range defined by the ``minim`` and ``maxim`` attributes
of the register before updating. If the register is with access 'R'
(read-only) it will ignore the request and log a warning.
Parameters
----------
value: any
The value in external format needed to be stored.
"""
# trim according to min and max for the register
if self.access != 'R':
int_value = self.value_to_internal(value)
self.int_value = max(self.minim, min(self.maxim, int_value))
if not self.sync: # pragma: no branch
self.write()
else:
logging.warning(f'Attempted to write in RO register {self.name} '
f'of device {self.device.name}')
[docs] def write(self):
"""Performs the actual writing of the internal value of the register
to the device. Calls the device's method to write the value of
register.
"""
self.device.write_register(self, self.int_value)
[docs] def read(self):
"""Performs the actual reading of the internal value of the register
from the device. Calls the device's method to read the value of
register.
"""
value = self.device.read_register(self)
# only update the internal value if the read value from device
# is not None
# a value of None indicates that there was an issue with readind
# the data from the device
if value is not None: # pragma: no branch
self.int_value = value
[docs] def __str__(self):
"""Representation of the register [name]: value."""
return f'[{self.name}]: {self.value} ({self.int_value})'
[docs]class BoolRegister(BaseRegister):
"""A register with BOOL representation (true/false).
Inherits from :py:class:`BaseRegister` all methods.
Overrides `value_to_external` and `value_to_internal` to process
a bool value.
Parameters
----------
bits: int or ``None``
An optional bit pattern to use in the determination of the output of
the register. Default is None and in this case we simply compare the
internal value with 0.
mode: str ('all' or 'any')
Indicates how the bit pattern should be used: 'all' means all the bits
in the pattern must match while 'any'
means any bit that matches the pattern is enough to result in a
``True`` external value. Only used if bits is not ``None``. Default
is 'any'.
mask: int or ``None``
An optional mask that allows for partial bit handling on the
internal values. This mask permits handling only the specified bits
without affecting the other ones in the internal value. For instance
if the mask is 0b00001111 then the operations (setter, getter) will
only affect the most significant 4 bits of the register.
"""
[docs] def __init__(self, bits=None, mode='any', mask=None, **kwargs):
super().__init__(**kwargs)
if bits:
check_type(bits, int, 'register', self.name, logger)
check_options(mode, ['all', 'any'], 'register', self.name, logger)
if mask:
check_type(mask, int, 'register', self.name, logger)
self.__bits = bits
self.__mode = mode
self.__mask = mask
@property
def bits(self):
"""The bit pattern used."""
return self.__bits
@property
def mode(self):
"""The bitmasking mode ('all' or 'any')."""
return self.__mode
@property
def mask(self):
"""The partial bitmask for the handling of the bits."""
return self.__mask
[docs] def value_to_external(self, value):
"""The external representation of bool register.
"""
if self.bits is None:
return bool(value)
# this assumes that if a mask is used the bits in the ``bits``
# attribute are all 0 already and we don't need to AND the ``mask``
# with the bits
if self.mode == 'any':
return bool(value & self.bits)
if self.mode == 'all':
return (value & self.bits) == self.bits
raise NotImplementedError
[docs] def value_to_internal(self, value):
"""The internal representation of the register's value.
"""
if not self.mask:
# no mask used
if not value: # False
ret = 0
elif self.bits: # True and bits
ret = self.bits
else: # True and no bits
ret = 1
else:
# mask used
# the int() below is to remove a linter error
masked_int_value = self.int_value & (~ int(self.mask))
if not value: # False; reset
# equivalent to reseting the bits
ret = masked_int_value
else: # True; set
# setting the bits
ret = self.bits | masked_int_value
return ret
[docs]class RegisterWithConversion(BaseRegister):
"""A register with an external representation that is produced by
using a linear transformation::
external = (internal - offset) / factor
internal = external * factor + offset
The ``RegisterWithConversion`` inherits all the paramters from
:py:class:`BaseRegister` and in addition includes the following
specific parameters that are used when converting the data from internal
to external format.
Parameters
----------
factor: float
A factor used for conversion. Defaults to 1.0.
offset: int
The offset for the conversion; defaults to 0 (int)
sign_bit: int or None
If a number is given it means that the register is "signed" and that
bit represents the sign. Bits are numbered from 1 meaning that if
``sign_bit`` is 1 the less significant bit is used and if we have
a 2 bytes register the most significant bit would be 16.
The convention is that numbers having 0 in this bit are positive
and the ones having 1 are negative numbers.
Raises:
KeyError: if any of the mandatory fields are not provided
ValueError: if value provided are wrong or the wrong type
"""
[docs] def __init__(self, factor=1.0, offset=0, sign_bit=None, **kwargs):
super().__init__(**kwargs)
check_type(factor, float, 'register', self.name, logger)
self.__factor = factor
check_type(offset, int, 'register', self.name, logger)
self.__offset = offset
if sign_bit:
check_type(sign_bit, int, 'register', self.name, logger)
self.__sign_bit = pow(2, sign_bit)
else:
self.__sign_bit = None
@property
def factor(self):
"""The conversion factor for external value."""
return self.__factor
@property
def offset(self):
"""The offset for external value."""
return self.__offset
@property
def sign_bit(self):
"""The sign bit, if any."""
return self.__sign_bit
[docs] def value_to_external(self, value):
"""
The external representation of the register's value.
Performs the translation of the value according to::
external = (internal - offset) / factor
"""
if self.sign_bit and value > (self.sign_bit / 2):
# negative number
value = value - self.sign_bit
return (float(value) - self.offset) / self.factor
[docs] def value_to_internal(self, value):
"""
The internal representation of the register's value.
Performs the translation of the value according to::
internal = external * factor + offset
The resulting value is rounded to produce an integer suitable
to be stored in the register.
"""
value = round(float(value) * self.factor + self.offset)
if value < 0 and self.sign_bit:
value = value + self.sign_bit
return value
[docs]class RegisterWithDynamicConversion(RegisterWithConversion):
"""A register that, in addition to the conversions provided by
:py:class:`RegisterWithConversion` can use the value provided
by another register in the device as a factor adjustment.
Parameters
----------
factor_reg: str
The name of the register that provides the additional factor
adjustment.
Raises:
KeyError: if any of the mandatory fields are not provided
ValueError: if value provided are wrong or the wrong type
"""
[docs] def __init__(self, factor_reg=None, **kwargs):
super().__init__(**kwargs)
check_type(factor_reg, str, 'register', self.name, logger)
# the registers may not be in order and the referenced register
# might have not been setup yet; so we need to delay the access to
# it for when all registers in the device are setup
self.__factor_reg_name = factor_reg
self.__factor_reg = None
@property
def factor_reg(self):
"""The register providing the additional conversion."""
if self.__factor_reg is None:
self.__factor_reg = getattr(self.device, self.__factor_reg_name)
return self.__factor_reg
[docs] def value_to_external(self, value):
"""
The external representation of the register's value.
Performs the translation of the value according to::
external = (internal - offset) / factor * dynamic_factor
"""
# we read directly from the int_value to avoid triggering a
# read of the register every time we make the conversion
extra_int_val = self.factor_reg.int_value
extra_factor = self.factor_reg.value_to_external(extra_int_val)
if self.sign_bit and value > (self.sign_bit / 2):
# negative number
value = value - self.sign_bit
return (float(value) - self.offset) / self.factor * extra_factor
[docs] def value_to_internal(self, value):
"""
The internal representation of the register's value.
Performs the translation of the value according to::
internal = external * factor / dynamic_factor + offset
The resulting value is rounded to produce an integer suitable
to be stored in the register.
"""
extra_int_val = self.factor_reg.int_value
extra_factor = self.factor_reg.value_to_external(extra_int_val)
value = round(float(value) * self.factor / extra_factor + self.offset)
if value < 0 and self.sign_bit:
value = value + self.sign_bit
return value
[docs]class RegisterWithThreshold(BaseRegister):
"""A register with an external representation that is represented by
a threshold between negative and positive values::
if internal >= threshold:
external = (internal - threshold) / factor
else:
external = - internal / factor
and for conversion from external to internal:
if external >= 0:
internal = external * factor + threshold
else:
internal = - external * factor
The ``RegisterWithThreshold`` inherits all the paramters from
:py:class:`BaseRegister` and in addition includes the following
specific parameters that are used when converting the data from internal
to external format.
Parameters
----------
factor: float
A factor used for conversion. Defaults to 1.0
threshold: int
A threshold that separates the positive from negative
values. Must be supplied.
Raises:
KeyError: if any of the mandatory fields are not proviced
ValueError: if value provided are wrong or the wrong type
"""
[docs] def __init__(self, factor=1.0, threshold=None, **kwargs):
super().__init__(**kwargs)
check_type(factor, float, 'register', self.name, logger)
self.__factor = factor
check_not_empty(threshold, 'threshold', 'register', self.name, logger)
check_type(threshold, int, 'register', self.name, logger)
self.__threshold = threshold
@property
def factor(self):
"""Conversion factor."""
return self.__factor
@property
def threshold(self):
"""The threshold for conversion."""
return self.__threshold
[docs] def value_to_external(self, value):
"""The external representation of the register's value.
Performs the translation of the value according to::
if value < threshold:
external = value / factor
else:
external = (threshold - value) / factor
"""
if value < self.threshold:
return value / self.factor
return (self.threshold - value) / self.factor
[docs] def value_to_internal(self, value):
"""The internal representation of the register's value.
Performs the translation of the value according to::
if value > 0:
internal = value * factor
else:
internal = (-value) * factor + threshold
"""
if value >= 0:
return value * self.factor
return (-value) * self.factor + self.threshold
[docs]class RegisterWithMapping(BaseRegister):
"""A register that can specify a 1:1 mapping of internal values to
external values.
Parameters
----------
mask: int or ``None``
Optional, can indicate that only certain bits from the value of the
register are used in the mapping. Ex. using 0b11110000 as a mask
indicates that only the most significant 4 bits of the internal
value are significant for the conversion to external values.
mapping: dict
A dictionary that provides {internal : external} mapping. Internally
the register will construct a reverse mapping that is used in
converting external values to internal ones.
"""
[docs] def __init__(self, mask=None, mapping={}, **kwargs):
super().__init__(**kwargs)
check_not_empty(mapping, 'mapping', 'register', self.name, logger)
check_type(mapping, dict, 'register', self.name, logger)
self.__mapping = mapping
self.__inv_mapping = {v: k for k, v in list(self.__mapping.items())}
if mask:
check_type(mask, int, 'register', self.name, logger)
self.__mask = mask
@property
def mapping(self):
"""The mapping {internal: external}."""
return self.__mapping
@property
def inv_mapping(self):
"""The mapping {external: internal}."""
return self.__inv_mapping
@property
def mask(self):
"""The bit mask is any."""
return self.__mask
[docs] def value_to_external(self, value):
"""Converts the internal value of the register to external format.
Applies mask on the internal value if one specified before checking
the mapping. If no entry is found returns 0.
"""
if self.mask:
value = value & self.mask
return self.mapping.get(value, 0)
[docs] def value_to_internal(self, value):
"""Converts the external value into an internal value using the
inverse mapping dictionary. If no entry is found logs a warning and
returns the already existing value in the ``int_value``.
If mask was specified it only affects the bits specified in the mask.
"""
int_val = self.inv_mapping.get(value, None)
if int_val is None:
logger.warning(f'Incorrect value {value} when converting to '
f'internal for register "{self.name}" of '
f'device "{self.device.name}"')
return self.int_value
# else
if self.mask:
masked_int_value = self.int_value & (~int(self.mask))
int_val = int_val | masked_int_value
return int_val