# A CAN signal.
from typing import TYPE_CHECKING, List, Optional, Union
from ...typechecking import ByteOrder, Choices, Comments, SignalValueType
from ..conversion import BaseConversion, IdentityConversion
from ..namedsignalvalue import NamedSignalValue
if TYPE_CHECKING:
from ...database.can.formats.dbc import DbcSpecifics
[docs]class Signal:
"""A CAN signal with position, size, unit and other information. A
signal is part of a message.
Signal bit numbering in a message:
.. code:: text
Byte: 0 1 2 3 4 5 6 7
+--------+--------+--------+--------+--------+--------+--------+--------+--- - -
| | | | | | | | |
+--------+--------+--------+--------+--------+--------+--------+--------+--- - -
Bit: 7 0 15 8 23 16 31 24 39 32 47 40 55 48 63 56
Big endian signal with start bit 2 and length 5 (0=LSB, 4=MSB):
.. code:: text
Byte: 0 1 2 3
+--------+--------+--------+--- - -
| |432|10| | |
+--------+--------+--------+--- - -
Bit: 7 0 15 8 23 16 31
Little endian signal with start bit 2 and length 9 (0=LSB, 8=MSB):
.. code:: text
Byte: 0 1 2 3
+--------+--------+--------+--- - -
|543210| | |876| |
+--------+--------+--------+--- - -
Bit: 7 0 15 8 23 16 31
"""
def __init__(
self,
name: str,
start: int,
length: int,
byte_order: ByteOrder = "little_endian",
is_signed: bool = False,
raw_initial: Optional[Union[int, float]] = None,
raw_invalid: Optional[Union[int, float]] = None,
conversion: Optional[BaseConversion] = None,
minimum: Optional[float] = None,
maximum: Optional[float] = None,
unit: Optional[str] = None,
dbc_specifics: Optional["DbcSpecifics"] = None,
comment: Optional[Union[str, Comments]] = None,
receivers: Optional[List[str]] = None,
is_multiplexer: bool = False,
multiplexer_ids: Optional[List[int]] = None,
multiplexer_signal: Optional[str] = None,
spn: Optional[int] = None,
) -> None:
# avoid using properties to improve encoding/decoding performance
#: The signal name as a string.
self.name: str = name
#: The conversion instance, which is used to convert
#: between raw and scaled/physical values.
self.conversion: BaseConversion = conversion or IdentityConversion(is_float=False)
#: The scaled minimum value of the signal, or ``None`` if unavailable.
self.minimum: Optional[float] = minimum
#: The scaled maximum value of the signal, or ``None`` if unavailable.
self.maximum: Optional[float] = maximum
#: The start bit position of the signal within its message.
self.start: int = start
#: The length of the signal in bits.
self.length: int = length
#: Signal byte order as ``'little_endian'`` or ``'big_endian'``.
self.byte_order: ByteOrder = byte_order
#: ``True`` if the signal is signed, ``False`` otherwise. Ignore this
#: attribute if :attr:`is_float` is ``True``.
self.is_signed: bool = is_signed
#: The internal representation of the initial value of the signal,
#: or ``None`` if unavailable.
self.raw_initial: Optional[Union[int, float]] = raw_initial
#: The initial value of the signal in units of the physical world,
#: or ``None`` if unavailable.
self.initial: Optional[SignalValueType] = (
self.conversion.raw_to_scaled(raw_initial) if raw_initial is not None else None
)
#: The raw value representing that the signal is invalid,
#: or ``None`` if unavailable.
self.raw_invalid: Optional[Union[int, float]] = raw_invalid
#: The scaled value representing that the signal is invalid,
#: or ``None`` if unavailable.
self.invalid: Optional[SignalValueType] = (
self.conversion.raw_to_scaled(raw_invalid) if raw_invalid is not None else None
)
#: The unit of the signal as a string, or ``None`` if unavailable.
self.unit: Optional[str] = unit
#: An object containing dbc specific properties like e.g. attributes.
self.dbc: Optional["DbcSpecifics"] = dbc_specifics
#: A list of all receiver nodes of this signal.
self.receivers: List[str] = receivers or []
#: ``True`` if this is the multiplexer signal in a message, ``False``
#: otherwise.
self.is_multiplexer: bool = is_multiplexer
#: The multiplexer ids list if the signal is part of a multiplexed
#: message, ``None`` otherwise.
self.multiplexer_ids: Optional[List[int]] = multiplexer_ids
#: The multiplexer signal if the signal is part of a multiplexed
#: message, ``None`` otherwise.
self.multiplexer_signal: Optional[str] = multiplexer_signal
#: The J1939 Suspect Parameter Number (SPN) value if the signal
#: has this attribute, ``None`` otherwise.
self.spn: Optional[int] = spn
#: The dictionary with the descriptions of the signal in multiple
#: languages. ``None`` if unavailable.
self.comments: Optional[Comments]
# if the 'comment' argument is a string, we assume that is an
# english comment. this is slightly hacky because the
# function's behavior depends on the type of the passed
# argument, but it is quite convenient...
if isinstance(comment, str):
# use the first comment in the dictionary as "The" comment
self.comments = {None: comment}
else:
# assume that we have either no comment at all or a
# multilingual dictionary
self.comments = comment
[docs] def raw_to_scaled(
self, raw_value: Union[int, float], decode_choices: bool = True
) -> SignalValueType:
"""Convert an internal raw value according to the defined scaling or value table.
:param raw_value:
The raw value
:param decode_choices:
If `decode_choices` is ``False`` scaled values are not
converted to choice strings (if available).
:return:
The calculated scaled value
"""
return self.conversion.raw_to_scaled(raw_value, decode_choices)
[docs] def scaled_to_raw(self, scaled_value: SignalValueType) -> Union[int, float]:
"""Convert a scaled value to the internal raw value.
:param scaled_value:
The scaled value.
:return:
The internal raw value.
"""
return self.conversion.scaled_to_raw(scaled_value)
@property
def scale(self) -> Union[int, float]:
"""The scale factor of the signal value."""
return self.conversion.scale
@scale.setter
def scale(self, value: Union[int, float]) -> None:
self.conversion = self.conversion.factory(
scale=value,
offset=self.conversion.offset,
choices=self.conversion.choices,
is_float=self.conversion.is_float,
)
@property
def offset(self) -> Union[int, float]:
"""The offset of the signal value."""
return self.conversion.offset
@offset.setter
def offset(self, value: Union[int, float]) -> None:
self.conversion = self.conversion.factory(
scale=self.conversion.scale,
offset=value,
choices=self.conversion.choices,
is_float=self.conversion.is_float,
)
@property
def choices(self) -> Optional[Choices]:
"""A dictionary mapping signal values to enumerated choices, or
``None`` if unavailable."""
return self.conversion.choices
@choices.setter
def choices(self, choices: Optional[Choices]) -> None:
self.conversion = self.conversion.factory(
scale=self.conversion.scale,
offset=self.conversion.offset,
choices=choices,
is_float=self.conversion.is_float,
)
@property
def is_float(self) -> bool:
"""``True`` if the raw signal value is a float, ``False`` otherwise."""
return self.conversion.is_float
@is_float.setter
def is_float(self, is_float: bool) -> None:
self.conversion = self.conversion.factory(
scale=self.conversion.scale,
offset=self.conversion.offset,
choices=self.conversion.choices,
is_float=is_float,
)
@property
def comment(self) -> Optional[str]:
"""The signal comment, or ``None`` if unavailable.
Note that we implicitly try to return the English comment if
multiple languages were specified.
"""
if self.comments is None:
return None
elif self.comments.get(None) is not None:
return self.comments.get(None)
elif self.comments.get("FOR-ALL") is not None:
return self.comments.get("FOR-ALL")
return self.comments.get("EN")
@comment.setter
def comment(self, value: Optional[str]) -> None:
if value is None:
self.comments = None
else:
self.comments = {None: value}
def choice_to_number(self, choice: Union[str, NamedSignalValue]) -> int:
try:
return self.conversion.choice_to_number(choice)
except KeyError as exc:
err_msg = f"Choice {choice} not found in Signal {self.name}."
raise KeyError(err_msg) from exc
def __repr__(self) -> str:
if self.choices is None:
choices = None
else:
list_of_choices = ", ".join(
[f"{value}: '{text}'" for value, text in self.choices.items()]
)
choices = f"{{{list_of_choices}}}"
return (
f"signal("
f"'{self.name}', "
f"{self.start}, "
f"{self.length}, "
f"'{self.byte_order}', "
f"{self.is_signed}, "
f"{self.raw_initial}, "
f"{self.conversion.scale}, "
f"{self.conversion.offset}, "
f"{self.minimum}, "
f"{self.maximum}, "
f"'{self.unit}', "
f"{self.is_multiplexer}, "
f"{self.multiplexer_ids}, "
f"{choices}, "
f"{self.spn}, "
f"{self.comments})"
)