├── .gitignore ├── LICENSE ├── OBDII.dbc ├── README.md ├── example.py ├── setup.py └── vector_dbc ├── __init__.py ├── attribute.py ├── attribute_definition.py ├── bus.py ├── can_data.py ├── comment.py ├── database.py ├── dbc.py ├── ecu.py ├── environment_variable.py ├── errors.py ├── frame_id ├── __init__.py ├── gm_parameter_id.py └── j1939.py ├── high_precision_timer.py ├── internal_database.py ├── message.py ├── node.py ├── sg_mul_val.py ├── signal.py ├── signal_group.py ├── signal_value_type.py ├── unit_conversion.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .idea 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kevin Schlosser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /OBDII.dbc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdschlosser/vector_dbc/653a60509ad233315f2332cc614239d9ee8f1d22/OBDII.dbc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vector CANdb++ DBC file parser 2 | 3 | This is a modified version of the dbc file parser that is included with cantools written by Erik Moqvist 4 | https://github.com/eerimoq/cantools 5 | 6 | Changes made: 7 | 8 | * Removed everything except for the pieces needed to read and write DBC files. 9 | * Improved the overall performance of the code 10 | * Added 103 Vector defined attributes 11 | * Added handling of GMParameterId's 12 | * Added proper decoding of GMParameterId's and J1939 PDU Id's 13 | * Added support for ECU's 14 | * Fixed default attributes not propigating to the respective ovject types 15 | * Added classes that represent a couple of different types of frame id's 16 | * Added listing RX and TX signals that are attached to a node 17 | * Added receivers list to messages 18 | * Added encoding and decoding using a node 19 | * Added encoding via a signal 20 | 21 | This is not a drop in replacement for the cantools dbc parser, 22 | there are code changes that would need to be made in order for this to run 23 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import vector_dbc 2 | 3 | db = vector_dbc.Database.load('OBDII.dbc') 4 | 5 | print('=== Database Metadata ===') 6 | print() 7 | print('version_year:', db.version_year) 8 | print('version_day:', db.version_day) 9 | print('version_week:', db.version_week) 10 | print('version:', db.version) 11 | print('version_number:', db.version_number) 12 | print('db_name:', db.db_name) 13 | print('bus_type:', db.bus_type) 14 | print('protocol_type:', db.protocol_type) 15 | print('manufacturer:', db.manufacturer) 16 | print() 17 | print('=== End Database Metadata ===') 18 | print() 19 | print('=== OBDII Encoded Requests ===') 20 | print() 21 | for signal in db.get_message('TX').signals: 22 | 23 | if signal.name in ('length_tx', 'mode', 'pid'): 24 | continue 25 | 26 | data = signal.encode() 27 | print(signal.name + ':', data.frame_id_hex + ',', data.hex) 28 | 29 | print() 30 | print('=== End OBDII Encoded Requests ===') 31 | print() 32 | print('=== Simulated Response Frame ===') 33 | print() 34 | rx = db.get_message('RX') 35 | data = dict( 36 | mode='Live Data', 37 | response=2, 38 | length=3, 39 | VehicleSpeed=200, 40 | pid='VehicleSpeed' 41 | ) 42 | 43 | print('encoding a test response:', data) 44 | encoded_data = rx.encode(data) 45 | print('encoded data:', encoded_data.frame_id_hex + ',', encoded_data.hex) 46 | 47 | decoded_data = db.decode_message(encoded_data.frame_id, encoded_data) 48 | response = {} 49 | for signal in decoded_data: 50 | value = signal.value 51 | response[signal.name] = value 52 | if signal.name in ('mode', 'response', 'length', 'pid'): 53 | continue 54 | 55 | print() 56 | print(signal.name + ':', value, signal.unit) 57 | 58 | print() 59 | print('decoded test frame:', response) 60 | print('decoded data same as test data:', data == response) 61 | print() 62 | print('=== End Simulated Response Frame ===') 63 | print() 64 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from distutils.core import setup 24 | 25 | LONG_DESCRIPTION = '''\ 26 | Vector CANdb++ DBC file parser 27 | 28 | This is a modified version of the dbc file parser that is included with cantools written by Erik Moqvist 29 | 30 | https://github.com/eerimoq/cantools 31 | 32 | Changes made: 33 | Removed everything except for the pieces needed to read and write DBC files. 34 | Improved the overall performance of the code 35 | Added 103 Vector defined attributes 36 | Added handling of GMParameterId's 37 | Added proper decoding of GMParameterId's and J1939 PDU Id's 38 | Added support for ECU's 39 | Fixed default attributes not propigating to the respective ovject types 40 | Added classes that represent a couple of different types of frame id's 41 | Added listing RX and TX signals that are attached to a node 42 | Added receivers list to messages 43 | Added encoding and decoding using a node 44 | Added encoding via a signal 45 | ''' 46 | 47 | setup( 48 | name='vector_dbc', 49 | version='0.1.0b', 50 | author='Kevin Schlosser', 51 | maintainer='Kevin Schlosser', 52 | url='https://github.com/kdschlosser/vector_dbc', 53 | license='MIT', 54 | description='Vector CANdb++ DBC file parser', 55 | long_description=LONG_DESCRIPTION, 56 | keywords=['can', 'can bus', 'dbc', 'kcd', 'automotive', 'vector', 'CANdb', 'CANdb++'], 57 | classifiers=[ 58 | 'License :: OSI Approved :: MIT License', 59 | 'Programming Language :: Python :: 2', 60 | 'Programming Language :: Python :: 3', 61 | ], 62 | requires=['textparser', 'bitstruct'], 63 | ) 64 | -------------------------------------------------------------------------------- /vector_dbc/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | from .database import Database 25 | from .message import Message 26 | from .message import EncodeError 27 | from .message import DecodeError 28 | from .signal import Signal 29 | from .node import Node 30 | -------------------------------------------------------------------------------- /vector_dbc/attribute.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from . import attribute_definition 24 | 25 | 26 | class AttributeMixin(object): 27 | dbc = None 28 | _marker = '' 29 | 30 | def _get_attribute(self, attr_name): 31 | if attr_name in self.dbc.attributes: 32 | value = self.dbc.attributes[attr_name].value 33 | 34 | return value 35 | 36 | def _set_yes_no_attribute(self, attr_name, value): 37 | if attr_name in self.dbc.attributes: 38 | self.dbc.attributes[attr_name].value = value 39 | else: 40 | 41 | if attr_name in self.dbc.attribute_definitions: 42 | definition = self.dbc.attribute_definitions[attr_name] 43 | else: 44 | 45 | definition = attribute_definition.AttributeDefinition( 46 | attr_name, 47 | default_value=0, 48 | kind=self._marker, 49 | type_name='ENUM', 50 | choices={0: 'No', 1: 'Yes'} 51 | ) 52 | self.dbc.attribute_definitions[attr_name] = definition 53 | 54 | self.dbc.attributes[attr_name] = Attribute(int(value), definition) 55 | 56 | def _set_hex_attribute(self, attr_name, minimum, maximum, value): 57 | self._set_attribute(attr_name, minimum, maximum, value, 'HEX') 58 | 59 | def _set_int_attribute(self, attr_name, minimum, maximum, value): 60 | self._set_attribute(attr_name, minimum, maximum, value, 'INT') 61 | 62 | def _set_str_attribute(self, attr_name, value): 63 | if attr_name in self.dbc.attributes: 64 | self.dbc.attributes[attr_name].value = value 65 | else: 66 | if attr_name in self.dbc.attribute_definitions: 67 | definition = self.dbc.attribute_definitions[attr_name] 68 | else: 69 | 70 | definition = attribute_definition.AttributeDefinition( 71 | attr_name, 72 | default_value='', 73 | kind=self._marker, 74 | type_name='STRING', 75 | ) 76 | self.dbc.attribute_definitions[attr_name] = definition 77 | 78 | self.dbc.attributes[attr_name] = Attribute(value, definition) 79 | 80 | def _set_attribute(self, attr_name, minimum, maximum, value, type_): 81 | if attr_name in self.dbc.attributes: 82 | self.dbc.attributes[attr_name].value = value 83 | else: 84 | if attr_name in self.dbc.attribute_definitions: 85 | definition = self.dbc.attribute_definitions[attr_name] 86 | else: 87 | 88 | definition = attribute_definition.AttributeDefinition( 89 | attr_name, 90 | default_value=0, 91 | kind=self._marker, 92 | type_name=type_, 93 | minimum=minimum, 94 | maximum=maximum 95 | ) 96 | self.dbc.attribute_definitions[attr_name] = definition 97 | 98 | self.dbc.attributes[attr_name] = Attribute(value, definition) 99 | 100 | 101 | class Attribute(object): 102 | """An attribute that can be associated with nodes/messages/signals.""" 103 | 104 | def __init__(self, value, definition): 105 | self._value = value 106 | self._definition = definition 107 | 108 | @property 109 | def name(self): 110 | """The attribute name as a string.""" 111 | return self._definition.name 112 | 113 | @property 114 | def value(self): 115 | """The value that this attribute has.""" 116 | return self._value 117 | 118 | @value.setter 119 | def value(self, value): 120 | self._value = value 121 | 122 | @property 123 | def definition(self): 124 | """The attribute definition.""" 125 | return self._definition 126 | -------------------------------------------------------------------------------- /vector_dbc/attribute_definition.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | class AttributeDefinition(object): 25 | """A definition of an attribute that can be associated with attributes in nodes/messages/signals.""" 26 | 27 | def __init__( 28 | self, name, default_value=None, kind=None, type_name=None, 29 | minimum=None, maximum=None, choices=None 30 | ): 31 | self._name = name 32 | self._default_value = default_value 33 | self._kind = kind 34 | self._type_name = type_name 35 | self._minimum = minimum 36 | self._maximum = maximum 37 | self._choices = choices 38 | 39 | @property 40 | def name(self): 41 | """The attribute name as a string.""" 42 | return self._name 43 | 44 | @property 45 | def default_value(self): 46 | """The default value that this attribute has, or ``None`` if unavailable.""" 47 | return self._default_value 48 | 49 | @default_value.setter 50 | def default_value(self, value): 51 | self._default_value = value 52 | 53 | @property 54 | def kind(self): 55 | """The attribute kind (BU_, BO_, SG_, EV_), or ``None`` if unavailable.""" 56 | return self._kind 57 | 58 | @property 59 | def type_name(self): 60 | """The attribute type (INT, HEX, FLOAT, STRING, ENUM), or ``None`` if unavailable.""" 61 | return self._type_name 62 | 63 | @property 64 | def minimum(self): 65 | """The minimum value of the attribute, or ``None`` if unavailable.""" 66 | return self._minimum 67 | 68 | @minimum.setter 69 | def minimum(self, value): 70 | self._minimum = value 71 | 72 | @property 73 | def maximum(self): 74 | """The maximum value of the attribute, or ``None`` if unavailable.""" 75 | return self._maximum 76 | 77 | @maximum.setter 78 | def maximum(self, value): 79 | self._maximum = value 80 | 81 | @property 82 | def choices(self): 83 | """A dictionary mapping attribute values to enumerated choices, or ``None`` if unavailable.""" 84 | return self._choices 85 | 86 | @choices.setter 87 | def choices(self, value): 88 | self._choices = value 89 | -------------------------------------------------------------------------------- /vector_dbc/bus.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | from .comment import Comment 23 | 24 | 25 | class Bus(object): 26 | """A CAN bus.""" 27 | 28 | def __init__(self, name, comment=None, baudrate=None): 29 | self._name = name 30 | self._comment = comment 31 | self._baudrate = baudrate 32 | self._parent = None 33 | 34 | @property 35 | def parent(self): 36 | return self._parent 37 | 38 | @property 39 | def name(self): 40 | """The bus name as a string.""" 41 | return self._name 42 | 43 | @property 44 | def comment(self): 45 | """The bus comment, or ``None`` if unavailable.""" 46 | if self._comment is not None and not isinstance(self._comment, Comment): 47 | self._comment = Comment(self._comment) 48 | 49 | return self._comment 50 | 51 | @comment.setter 52 | def comment(self, value): 53 | if value is not None and not isinstance(value, (str, Comment)): 54 | value = str(value) 55 | 56 | self._comment = value 57 | 58 | @property 59 | def baudrate(self): 60 | """The bus baudrate, or ``None`` if unavailable.""" 61 | return self._baudrate 62 | -------------------------------------------------------------------------------- /vector_dbc/can_data.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from .frame_id import ( 24 | J1939FrameId, 25 | GMParameterIdExtended, 26 | ) 27 | 28 | 29 | class TXData(bytearray): 30 | _frame_id = None 31 | 32 | @property 33 | def hex(self): 34 | return ' '.join(hex(item)[2:].upper().zfill(2) for item in self) 35 | 36 | @property 37 | def frame_id_hex(self): 38 | return self._frame_id.hex 39 | 40 | @property 41 | def frame_id(self): 42 | return int(self._frame_id) 43 | 44 | @frame_id.setter 45 | def frame_id(self, value): 46 | self._frame_id = value 47 | 48 | def set_sending_node(self, node): 49 | tp_tx_indentfier = node.tp_tx_indentfier 50 | 51 | if tp_tx_indentfier is not None: 52 | if isinstance(self._frame_id, J1939FrameId): 53 | self._frame_id.source_address = tp_tx_indentfier 54 | 55 | elif isinstance(self._frame_id, GMParameterIdExtended): 56 | self._frame_id.source_id = tp_tx_indentfier 57 | 58 | 59 | class RXData(list): 60 | _frame_id = None 61 | 62 | @property 63 | def frame_id(self): 64 | return int(self._frame_id) 65 | 66 | @frame_id.setter 67 | def frame_id(self, value): 68 | self._frame_id = value 69 | 70 | def __getitem__(self, item): 71 | if isinstance(item, int): 72 | return list.__getitem__(self, item) 73 | 74 | for signal in self: 75 | if signal.name == item: 76 | return signal 77 | 78 | raise KeyError('"{0}" cannot be found.'.format(item)) 79 | 80 | def __setitem__(self, key, value): 81 | if isinstance(key, int): 82 | list.__setitem__(self, key, value) 83 | return 84 | 85 | for i, signal in enumerate(self): 86 | if signal.name == key: 87 | self[i] = value 88 | return 89 | 90 | self.append(value) 91 | 92 | def __contains__(self, item): 93 | if isinstance(item, bytes): 94 | item = item.decode('utf-8') 95 | 96 | if isinstance(item, str): 97 | for signal in self: 98 | if signal.name == item: 99 | return True 100 | 101 | return False 102 | 103 | return list.__contains__(self, item) 104 | -------------------------------------------------------------------------------- /vector_dbc/comment.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Comment(str): 4 | _fmt = 'CM_ "{comment}" ;' 5 | 6 | def format(self, *args, **kwargs): 7 | return self._fmt.format(comment=self.replace('"', '\\"')) 8 | 9 | 10 | class NodeComment(Comment): 11 | _fmt = 'CM_ BU_ {name} "{comment}" ;' 12 | 13 | def __init__(self, value): 14 | self._node = None 15 | 16 | try: 17 | super(NodeComment, self).__init__(value) 18 | except TypeError: 19 | super(NodeComment, self).__init__() 20 | 21 | def format(self, *args, **kwargs): 22 | return self._fmt.format( 23 | name=self._node.name, 24 | comment=self.replace('"', '\\"') 25 | ) 26 | 27 | @property 28 | def node(self): 29 | return self._node 30 | 31 | @node.setter 32 | def node(self, value): 33 | self._node = value 34 | 35 | 36 | class MessageComment(Comment): 37 | _fmt = 'CM_ BO_ {frame_id} "{comment}" ;' 38 | 39 | def __init__(self, value): 40 | self._message = None 41 | 42 | try: 43 | super(MessageComment, self).__init__(value) 44 | except TypeError: 45 | super(MessageComment, self).__init__() 46 | 47 | def format(self, *args, **kwargs): 48 | return self._fmt.format( 49 | frame_id=self._message.dbc_frame_id, 50 | comment=self.replace('"', '\\"') 51 | ) 52 | 53 | @property 54 | def message(self): 55 | return self._message 56 | 57 | @message.setter 58 | def message(self, value): 59 | self._message = value 60 | 61 | 62 | class SignalComment(Comment): 63 | _fmt = 'CM_ SG_ {frame_id} {name} "{comment}";' 64 | 65 | def __init__(self, value): 66 | self._signal = None 67 | 68 | try: 69 | super(SignalComment, self).__init__(value) 70 | except TypeError: 71 | super(SignalComment, self).__init__() 72 | 73 | def format(self, *args, **kwargs): 74 | return self._fmt.format( 75 | frame_id=self._signal.message.dbc_frame_id, 76 | name=self._signal.name, 77 | comment=self.replace('"', '\\"') 78 | ) 79 | 80 | @property 81 | def signal(self): 82 | return self._signal 83 | 84 | @signal.setter 85 | def signal(self, value): 86 | self._signal = value 87 | 88 | 89 | class EnvironmentVariableComment(Comment): 90 | _fmt = 'CM_ EV_ {name} "{comment}";' 91 | 92 | def __init__(self, value): 93 | self._environment_variable = None 94 | 95 | try: 96 | super(EnvironmentVariableComment, self).__init__(value) 97 | except TypeError: 98 | super(EnvironmentVariableComment, self).__init__() 99 | 100 | def format(self, *args, **kwargs): 101 | return self._fmt.format( 102 | name=self._environment_variable.name, 103 | comment=self.replace('"', '\\"') 104 | ) 105 | 106 | @property 107 | def environment_variable(self): 108 | return self._environment_variable 109 | 110 | @environment_variable.setter 111 | def environment_variable(self, value): 112 | self._environment_variable = value 113 | -------------------------------------------------------------------------------- /vector_dbc/database.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | 25 | from . import dbc 26 | from .internal_database import InternalDatabase 27 | from . import attribute 28 | from .frame_id import ( 29 | FrameId, 30 | J1939FrameId, 31 | GMParameterId, 32 | GMParameterIdExtended 33 | ) 34 | 35 | FRAME_IDS = ( 36 | FrameId, 37 | J1939FrameId, 38 | GMParameterId, 39 | GMParameterIdExtended 40 | ) 41 | 42 | LOGGER = logging.getLogger(__name__) 43 | 44 | 45 | class Database(attribute.AttributeMixin): 46 | _marker = '' 47 | """ 48 | This class contains all messages, signals and definitions of a CAN 49 | network. 50 | 51 | The factory functions :func:`load()`, 52 | :func:`load_file()` and 53 | :func:`load_string()` returns 54 | instances of this class. 55 | 56 | If `strict` is ``True`` an exception is raised if any signals are 57 | overlapping or if they don't fit in their message. 58 | """ 59 | 60 | def __init__( 61 | self, messages=None, nodes=None, buses=None, version=None, 62 | dbc_specifics=None, frame_id_mask=None, strict=True 63 | ): 64 | self._messages = messages if messages else [] 65 | self._nodes = nodes if nodes else [] 66 | self._buses = buses if buses else [] 67 | self._name_to_message = {} 68 | self._frame_id_to_message = {} 69 | self._version = version 70 | self._dbc = dbc_specifics 71 | 72 | if frame_id_mask is None: 73 | frame_id_mask = 0xffffffff 74 | 75 | self._frame_id_mask = frame_id_mask 76 | self._strict = strict 77 | self.refresh() 78 | 79 | @property 80 | def messages(self): 81 | """ 82 | A list of messages in the database. 83 | 84 | Use :meth:`.get_message_by_frame_id()` or 85 | :meth:`.get_message_by_name()` to find a message by its frame 86 | id or name. 87 | """ 88 | return self._messages 89 | 90 | @messages.setter 91 | def messages(self, value): 92 | for message in value: 93 | res = self._add_message(message) 94 | if res is True: 95 | self._messages += [message] 96 | else: 97 | self._messages.remove(res) 98 | self._messages += [message] 99 | 100 | message._parent = self 101 | message.refresh(self._strict) 102 | 103 | @property 104 | def nodes(self): 105 | """A list of nodes in the database.""" 106 | return self._nodes 107 | 108 | @nodes.setter 109 | def nodes(self, value): 110 | node_names = {node.name: node for node in self._nodes} 111 | 112 | for node in value: 113 | if node.name in node_names: 114 | LOGGER.warning( 115 | "Overwriting node '%s'", 116 | node.name 117 | ) 118 | 119 | index = self._nodes.index(node_names[node.name]) 120 | self._nodes[index] = node 121 | else: 122 | self._nodes += [node] 123 | 124 | @property 125 | def buses(self): 126 | """A list of CAN buses in the database.""" 127 | return self._buses 128 | 129 | @property 130 | def version(self): 131 | """The database version, or ``None`` if unavailable.""" 132 | return self._version 133 | 134 | @version.setter 135 | def version(self, value): 136 | self._version = value 137 | 138 | @property 139 | def dbc(self): 140 | """An object containing dbc specific properties like e.g. attributes.""" 141 | return self._dbc 142 | 143 | @dbc.setter 144 | def dbc(self, value): 145 | self._dbc = value 146 | 147 | @classmethod 148 | def load(cls, path): 149 | instance = cls() 150 | 151 | # noinspection PyArgumentEqualDefault 152 | with open(path, 'r', encoding='cp1252') as f: 153 | instance.add_string(f.read()) 154 | 155 | return instance 156 | 157 | def add_file(self, filename, encoding='cp1252'): 158 | """ 159 | Open, read and parse DBC data from given file and add the parsed 160 | data to the database. 161 | 162 | `encoding` specifies the file encoding. 163 | """ 164 | # noinspection PyArgumentEqualDefault 165 | with open(filename, 'r', encoding=encoding) as fin: 166 | self.add_string(fin.read()) 167 | 168 | def add_string(self, string): 169 | """Parse given DBC data string and add the parsed data to the database.""" 170 | database = dbc.load_string(self, string, self._strict) 171 | 172 | self._messages += database.messages 173 | self._nodes = database.nodes 174 | self._buses = database.buses 175 | self._version = database.version 176 | self._dbc = database.dbc 177 | self.refresh() 178 | 179 | def _add_message(self, message): 180 | """Add given message to the database.""" 181 | res = True 182 | 183 | if message.name in self._name_to_message: 184 | LOGGER.warning( 185 | "Overwriting message '%s' with '%s' in the " 186 | "name to message dictionary.", 187 | self._name_to_message[message.name].name, 188 | message.name) 189 | 190 | res = self._name_to_message[message.name] 191 | 192 | masked_frame_id = (message.frame_id.frame_id & self._frame_id_mask) 193 | 194 | if masked_frame_id in self._frame_id_to_message: 195 | LOGGER.warning( 196 | "Overwriting message '%s' with '%s' in the frame id to message " 197 | "dictionary because they have identical masked frame ids 0x%x.", 198 | self._frame_id_to_message[masked_frame_id].name, 199 | message.name, 200 | masked_frame_id) 201 | 202 | res = self._frame_id_to_message[masked_frame_id] 203 | 204 | self._name_to_message[message.name] = message 205 | self._frame_id_to_message[masked_frame_id] = message 206 | return res 207 | 208 | def as_string(self): 209 | """Return the database as a string formatted as a DBC file.""" 210 | return dbc.dump_string(InternalDatabase( 211 | self._messages, self._nodes, 212 | self._buses, self._version, self._dbc)) 213 | 214 | def get_message(self, frame_id_or_name): 215 | """Find the message object for given frame id `frame_id`.""" 216 | if isinstance(frame_id_or_name, (str, bytes)): 217 | if isinstance(frame_id_or_name, bytes): 218 | frame_id_or_name = frame_id_or_name.decode('utf-8') 219 | 220 | if frame_id_or_name in self._name_to_message: 221 | res = [self._name_to_message[frame_id_or_name]] 222 | else: 223 | res = [] 224 | else: 225 | res = [ 226 | message for message in self.messages 227 | if message.frame_id == frame_id_or_name 228 | ] 229 | 230 | if len(res) > 0: 231 | return res[0] 232 | 233 | raise KeyError(frame_id_or_name) 234 | 235 | def get_node(self, name_or_id): 236 | """Find the node object for given name `name`.""" 237 | 238 | if isinstance(name_or_id, int): 239 | res = [ 240 | node for node in self._nodes 241 | if name_or_id in (node.tp_rx_indentfier, node.tp_tx_indentfier) 242 | ] 243 | 244 | else: 245 | res = [ 246 | node for node in self._nodes 247 | if node.name == name_or_id 248 | ] 249 | 250 | if len(res) > 0: 251 | return res[0] 252 | 253 | raise KeyError(name_or_id) 254 | 255 | def get_bus(self, name): 256 | """Find the bus object for given name `name`.""" 257 | 258 | bus = [bus for bus in self._buses if bus.name == name] 259 | if len(bus) > 0: 260 | return bus[0] 261 | 262 | raise KeyError(name) 263 | 264 | def encode_message( 265 | self, frame_id_or_name, data, scaling=True, 266 | padding=False, strict=True 267 | ): 268 | """ 269 | Encode given signal data `data` as a message of given frame id or 270 | name `frame_id_or_name`. `data` is a dictionary of signal 271 | name-value entries. 272 | 273 | If `scaling` is ``False`` no scaling of signals is performed. 274 | 275 | If `padding` is ``True`` unused bits are encoded as 1. 276 | 277 | If `strict` is ``True`` all signal values must be within their 278 | allowed ranges, or an exception is raised. 279 | """ 280 | message = self.get_message(frame_id_or_name) 281 | return message.encode(data, scaling, padding, strict) 282 | 283 | def decode_message( 284 | self, frame_id_or_name, data, 285 | decode_choices=True, scaling=True 286 | ): 287 | """ 288 | Decode given signal data `data` as a message of given frame id or 289 | name `frame_id_or_name`. Returns a dictionary of signal 290 | name-value entries. 291 | 292 | If `decode_choices` is ``False`` scaled values are not 293 | converted to choice strings (if available). 294 | 295 | If `scaling` is ``False`` no scaling of signals is performed. 296 | """ 297 | message = self.get_message(frame_id_or_name) 298 | return message.decode(data, decode_choices, scaling) 299 | 300 | def refresh(self): 301 | """ 302 | Refresh the internal database state. 303 | 304 | This method must be called after modifying any message in the 305 | database to refresh the internal lookup tables used when 306 | encoding and decoding messages. 307 | """ 308 | self._name_to_message = {} 309 | self._frame_id_to_message = {} 310 | 311 | for message in self._messages: 312 | message._parent = self 313 | message.refresh(self._strict) 314 | self._add_message(message) 315 | 316 | for node in self._nodes: 317 | node._parent = self 318 | 319 | for bus in self._buses: 320 | bus._parent = self 321 | 322 | @property 323 | def nm_base_address(self): 324 | """ 325 | Defines the CAN ID of the first network management message. 326 | """ 327 | return self._get_attribute('NmBaseAddress') 328 | 329 | @nm_base_address.setter 330 | def nm_base_address(self, value): 331 | self._set_hex_attribute('NmBaseAddress', 0x0, 0x7FF, value) 332 | 333 | @property 334 | def tp_base_address(self): 335 | """ 336 | The base address that is used to determine the CAN ID for the TP messages (extended addressing mode only). 337 | """ 338 | return self._get_attribute('TpBaseAddress') 339 | 340 | @tp_base_address.setter 341 | def tp_base_address(self, value): 342 | self._set_hex_attribute('TpBaseAddress', 0x0, 0x7FF, value) 343 | 344 | @property 345 | def use_gm_parameter_ids(self): 346 | """ 347 | GM parameter ids derived from the frame id. 348 | """ 349 | return bool(self._get_attribute('UseGMParameterIDs')) 350 | 351 | @use_gm_parameter_ids.setter 352 | def use_gm_parameter_ids(self, value): 353 | self._set_int_attribute('UseGMParameterIDs', 0, 1, int(value)) 354 | 355 | @property 356 | def gen_nwm_sleep_time(self): 357 | """ 358 | If all nodes have the same wait time up to SleepRequest, set this time in this attribute in ms. 359 | """ 360 | return self._get_attribute('GenNWMSleepTime') 361 | 362 | @gen_nwm_sleep_time.setter 363 | def gen_nwm_sleep_time(self, value): 364 | self._set_int_attribute('GenNWMSleepTime', 0, 2147483647, value) 365 | 366 | @property 367 | def nm_message_count(self): 368 | """ 369 | Defines the number of CAN IDs used or reserved for network management messages. 370 | 371 | This is then the maximum number of network management message on the network. 372 | """ 373 | return self._get_attribute('NmMessageCount') 374 | 375 | @nm_message_count.setter 376 | def nm_message_count(self, value): 377 | self._set_int_attribute('NmMessageCount', 1, 255, value) 378 | 379 | @property 380 | def version_year(self): 381 | """ 382 | Specifies the year of the network release. 383 | """ 384 | return self._get_attribute('VersionYear') 385 | 386 | @version_year.setter 387 | def version_year(self, value): 388 | self._set_int_attribute('VersionYear', 0, 99, value) 389 | 390 | @property 391 | def version_month(self): 392 | """ 393 | Specifies the month of the network release. 394 | """ 395 | return self._get_attribute('VersionMonth') 396 | 397 | @version_month.setter 398 | def version_month(self, value): 399 | self._set_int_attribute('VersionMonth', 1, 12, value) 400 | 401 | @property 402 | def version_week(self): 403 | """ 404 | Specifies the week of the network release. 405 | """ 406 | return self._get_attribute('VersionWeek') 407 | 408 | @version_week.setter 409 | def version_week(self, value): 410 | self._set_int_attribute('VersionWeek', 0, 52, value) 411 | 412 | @property 413 | def version_day(self): 414 | """ 415 | Specifies the day of the network release. 416 | """ 417 | return self._get_attribute('VersionDay') 418 | 419 | @version_day.setter 420 | def version_day(self, value): 421 | self._set_int_attribute('VersionDay', 1, 31, value) 422 | 423 | @property 424 | def version_number(self): 425 | """ 426 | Specifies the version number of the network release. The numbers have to be given in BCD coding. 427 | """ 428 | return self._get_attribute('VersionNumber') 429 | 430 | @version_number.setter 431 | def version_number(self, value): 432 | self._set_int_attribute('VersionNumber', 0, 2147483647, value) 433 | 434 | @property 435 | def nm_type(self): 436 | """ 437 | Defines the type of network management used on the network e.g. Vector. 438 | """ 439 | return self._get_attribute('NmType') 440 | 441 | @nm_type.setter 442 | def nm_type(self, value): 443 | self._set_str_attribute('NmType', value) 444 | 445 | @property 446 | def manufacturer(self): 447 | """ 448 | Specifies the OEM. 449 | """ 450 | return self._get_attribute('Manufacturer') 451 | 452 | @manufacturer.setter 453 | def manufacturer(self, value): 454 | self._set_str_attribute('Manufacturer', value) 455 | 456 | @property 457 | def db_name(self): 458 | """ 459 | Specifies the OEM. 460 | """ 461 | return self._get_attribute('DBName') 462 | 463 | @db_name.setter 464 | def db_name(self, value): 465 | self._set_str_attribute('DBName', value) 466 | 467 | @property 468 | def bus_type(self): 469 | """ 470 | Defines the type of the network, e.g. "CAN", "LIN", "MOST", "Ethernet", "ARINC425", "AFDX" 471 | """ 472 | return self._get_attribute('BusType') 473 | 474 | @bus_type.setter 475 | def bus_type(self, value): 476 | self._set_str_attribute('BusType', value) 477 | 478 | @property 479 | def protocol_type(self): 480 | """ 481 | This attribute defines the protocol type. 482 | 483 | Several functions are activated in CANoe/CANalyzer by this attribute. 484 | The appropriate option has to be installed and a license must be available. 485 | 486 | J1939, CANopen, AFDX, ARINC825, CANaerospace, CANopenSafety, Aerospace, NMEA2000 487 | 488 | """ 489 | return self._get_attribute('ProtocolType') 490 | 491 | @protocol_type.setter 492 | def protocol_type(self, value): 493 | self._set_str_attribute('ProtocolType', value) 494 | 495 | @property 496 | def is_multiplex_ext_enabled(self): 497 | """ 498 | The extended multiplexor concept allows you to define several multiplexor signals in 499 | a single message. One multiplexed signal may be multiplexed through several multiplex 500 | values. If you want to use extended multiplexing, you must activate the option Enable 501 | extended multiplexing on the Edit page of the Settings dialog. If the bus type is not 502 | Ethernet, ARINC425 or AFDX and if the protocol type does not have one of the values 503 | J1939, NMEA2000, ISO11783, CANopen, CANopenSafety or Aerospace, you must set the 504 | MultiplexExtEnabled attribute to Yes for the network. 505 | 506 | """ 507 | return bool(self._get_attribute('MultiplexExtEnabled')) 508 | 509 | @is_multiplex_ext_enabled.setter 510 | def is_multiplex_ext_enabled(self, value): 511 | self._set_yes_no_attribute('MultiplexExtEnabled', value) 512 | 513 | -------------------------------------------------------------------------------- /vector_dbc/ecu.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | class ECU(object): 25 | """ 26 | An ECU is the physical representation of a node or a group of nodes. 27 | 28 | This is a way of organizing a module or device that has more then one node address. 29 | An Example would be thea PCM (Powertrain Control Module) in a vebicle, 30 | a PCM is an ECM (Engine Control Module), TCM (Transmission Control Module) and possibly 31 | others housed into a single physical unit. A node would represent the ECM or the TCM. 32 | 33 | This has no other purpose other then organization. 34 | 35 | To define an ecm you use the `ecm` property in a `Node` instance. 36 | """ 37 | 38 | def __init__(self, database, name): 39 | self._database = database 40 | self._name = name 41 | 42 | @property 43 | def nodes(self): 44 | return [ 45 | node for node in self._database.nodes 46 | if node.ecu is not None and node.ecu == self.name 47 | ] 48 | 49 | @property 50 | def database(self): 51 | return self._database 52 | 53 | @property 54 | def name(self): 55 | return self._name 56 | 57 | @name.setter 58 | def name(self, value): 59 | nodes = self.nodes 60 | self._name = value 61 | 62 | for node in nodes: 63 | node.ecu = self 64 | 65 | def __str__(self): 66 | return self._name 67 | 68 | def __eq__(self, other): 69 | if isinstance(other, bytes): 70 | other = other.decode('utf-8') 71 | 72 | if isinstance(other, str): 73 | return other == self._name 74 | 75 | if isinstance(other, ECU): 76 | return other.name == self._name 77 | 78 | return False 79 | 80 | def __ne__(self, other): 81 | return not self.__eq__(other) 82 | 83 | 84 | -------------------------------------------------------------------------------- /vector_dbc/environment_variable.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from . import attribute 24 | from . import attribute_definition 25 | from .comment import EnvironmentVariableComment 26 | 27 | 28 | class EnvironmentVariable(attribute.AttributeMixin): 29 | """A CAN environment variable.""" 30 | 31 | _marker = 'EV_' 32 | 33 | def __init__( 34 | self, name, env_type, minimum, maximum, unit, initial_value, 35 | env_id, access_type, access_node, comment 36 | ): 37 | self._name = name 38 | self._env_type = env_type 39 | self._minimum = minimum 40 | self._maximum = maximum 41 | self._unit = unit 42 | self._initial_value = initial_value 43 | self._env_id = env_id 44 | self._access_type = access_type 45 | self._access_node = access_node 46 | self._comment = comment 47 | 48 | @property 49 | def name(self): 50 | """The environment variable name as a string.""" 51 | return self._name 52 | 53 | @property 54 | def env_type(self): 55 | """The environment variable type value.""" 56 | return self._env_type 57 | 58 | @env_type.setter 59 | def env_type(self, value): 60 | self._env_type = value 61 | 62 | @property 63 | def minimum(self): 64 | """The minimum value of the environment variable.""" 65 | return self._minimum 66 | 67 | @minimum.setter 68 | def minimum(self, value): 69 | self._minimum = value 70 | 71 | @property 72 | def maximum(self): 73 | """The maximum value of the environment variable.""" 74 | return self._maximum 75 | 76 | @maximum.setter 77 | def maximum(self, value): 78 | self._maximum = value 79 | 80 | @property 81 | def unit(self): 82 | """ The units in which the environment variable is expressed as a string.""" 83 | return self._unit 84 | 85 | @unit.setter 86 | def unit(self, value): 87 | self._unit = value 88 | 89 | @property 90 | def initial_value(self): 91 | """The initial value of the environment variable.""" 92 | return self._initial_value 93 | 94 | @initial_value.setter 95 | def initial_value(self, value): 96 | self._initial_value = value 97 | 98 | @property 99 | def env_id(self): 100 | """The id value of the environment variable.""" 101 | return self._env_id 102 | 103 | @env_id.setter 104 | def env_id(self, value): 105 | self._env_id = value 106 | 107 | @property 108 | def access_type(self): 109 | """The environment variable access type as a string.""" 110 | return self._access_type 111 | 112 | @access_type.setter 113 | def access_type(self, value): 114 | self._access_type = value 115 | 116 | @property 117 | def access_node(self): 118 | """The environment variable access node as a string.""" 119 | return self._access_node 120 | 121 | @access_node.setter 122 | def access_node(self, value): 123 | self._access_node = value 124 | 125 | @property 126 | def comment(self): 127 | """The node comment, or ``None`` if unavailable.""" 128 | if ( 129 | self._comment is not None and 130 | not isinstance(self._comment, EnvironmentVariableComment) 131 | ): 132 | self._comment = EnvironmentVariableComment(self._comment) 133 | self._comment.environment_variable = self 134 | 135 | return self._comment 136 | 137 | @comment.setter 138 | def comment(self, value): 139 | if isinstance(value, bytes): 140 | value = value.decode('utf-8') 141 | 142 | if ( 143 | value is not None and 144 | not isinstance(value, (str, EnvironmentVariableComment)) 145 | ): 146 | value = str(value) 147 | 148 | self._comment = value 149 | 150 | @property 151 | def gen_env_auto_gen_ctrl(self): 152 | return bool(self._get_attribute('GenEnvAutoGenCtrl')) 153 | 154 | @gen_env_auto_gen_ctrl.setter 155 | def gen_env_auto_gen_ctrl(self, value): 156 | self._set_yes_no_attribute('GenEnvAutoGenCtrl', value) 157 | 158 | @property 159 | def gen_env_control_type(self): 160 | if 'GenEnvControlType' in self.dbc.attributes: 161 | value = self.dbc.attributes['GenEnvControlType'].value 162 | return self.dbc.attributes['GenEnvControlType'].definition.choices[value] 163 | 164 | @gen_env_control_type.setter 165 | def gen_env_control_type(self, value): 166 | if 'GenEnvControlType' in self.dbc.attributes: 167 | self.dbc.attributes['GenEnvControlType'].value = value 168 | else: 169 | 170 | if 'GenEnvControlType' in self.dbc.attribute_definitions: 171 | definition = self.dbc.attribute_definitions['GenEnvControlType'] 172 | else: 173 | 174 | definition = attribute_definition.AttributeDefinition( 175 | 'GenEnvControlType', 176 | default_value=0, 177 | kind='EV_', 178 | type_name='ENUM', 179 | choices={ 180 | 0: 'NoControl', 181 | 1: 'SliderHoriz', 182 | 2: 'SliderVert', 183 | 3: 'PushButton', 184 | 4: 'Edit', 185 | 5: 'BitmapSwitch' 186 | } 187 | ) 188 | 189 | choices = {v: k for k, v in definition.choices.items()} 190 | 191 | self.dbc.attributes['GenEnvControlType'] = attribute.Attribute(choices[value], definition) 192 | 193 | @property 194 | def gen_env_msg_name(self): 195 | return self._get_attribute('GenEnvMsgName') 196 | 197 | @gen_env_msg_name.setter 198 | def gen_env_msg_name(self, value): 199 | self._set_str_attribute('GenEnvMsgName', value) 200 | 201 | @property 202 | def gen_env_msg_offset(self): 203 | return self._get_attribute('GenEnvMsgOffset') 204 | 205 | @gen_env_msg_offset.setter 206 | def gen_env_msg_offset(self, value): 207 | self._set_int_attribute('GenEnvMsgOffset', 0, 2147483647, value) 208 | 209 | @property 210 | def gen_env_var_ending_dsp(self): 211 | return self._get_attribute('GenEnvVarEndingDsp') 212 | 213 | @gen_env_var_ending_dsp.setter 214 | def gen_env_var_ending_dsp(self, value): 215 | self._set_str_attribute('GenEnvVarEndingDsp', value) 216 | 217 | @property 218 | def gen_env_var_ending_snd(self): 219 | return self._get_attribute('GenEnvVarEndingSnd') 220 | 221 | @gen_env_var_ending_snd.setter 222 | def gen_env_var_ending_snd(self, value): 223 | self._set_str_attribute('GenEnvVarEndingSnd', value) 224 | 225 | @property 226 | def gen_env_var_prefix(self): 227 | return self._get_attribute('GenEnvVarPrefix') 228 | 229 | @gen_env_var_prefix.setter 230 | def gen_env_var_prefix(self, value): 231 | self._set_int_attribute('GenEnvVarPrefix', 0, 2147483647, value) 232 | 233 | @property 234 | def gen_env_is_generated_dsp(self): 235 | return bool(self._get_attribute('GenEnvIsGeneratedDsp')) 236 | 237 | @gen_env_is_generated_dsp.setter 238 | def gen_env_is_generated_dsp(self, value): 239 | self._set_yes_no_attribute('GenEnvIsGeneratedDsp', value) 240 | 241 | @property 242 | def gen_env_is_generated_snd(self): 243 | return bool(self._get_attribute('GenEnvIsGeneratedSnd')) 244 | 245 | @gen_env_is_generated_snd.setter 246 | def gen_env_is_generated_snd(self, value): 247 | self._set_yes_no_attribute('GenEnvIsGeneratedSnd', value) 248 | -------------------------------------------------------------------------------- /vector_dbc/errors.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | class Error(Exception): 25 | """Base exception for all exception in the package.""" 26 | pass 27 | 28 | 29 | class ParseError(Error): 30 | pass 31 | 32 | 33 | class EncodeError(Error): 34 | pass 35 | 36 | 37 | class DecodeError(Error): 38 | pass 39 | -------------------------------------------------------------------------------- /vector_dbc/frame_id/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from . import gm_parameter_id 24 | from . import j1939 25 | 26 | J1939FrameId = j1939.J1939FrameId 27 | GMParameterId = gm_parameter_id.GMParameterId 28 | GMParameterIdExtended = gm_parameter_id.GMParameterIdExtended 29 | 30 | 31 | class FrameId(object): 32 | 33 | def __init__(self, frame_id): 34 | self._frame_id = frame_id 35 | 36 | @property 37 | def hex(self): 38 | if self.frame_id > 0x7FF: 39 | return '0x' + hex(self.frame_id)[2:].upper().zfill(8) 40 | else: 41 | return '0x' + hex(self.frame_id)[2:].upper().zfill(3) 42 | 43 | def copy(self): 44 | return FrameId(self._frame_id) 45 | 46 | @classmethod 47 | def from_frame_id(cls, frame_id): 48 | return cls(frame_id) 49 | 50 | @property 51 | def frame_id(self): 52 | return self._frame_id 53 | 54 | def __eq__(self, other): 55 | return other == self._frame_id 56 | 57 | def __ne__(self, other): 58 | return not self.__eq__(other) 59 | 60 | def __str__(self): 61 | return str(self.frame_id) 62 | 63 | def __int__(self): 64 | return self._frame_id 65 | -------------------------------------------------------------------------------- /vector_dbc/frame_id/gm_parameter_id.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | class GMParameterIdExtended(object): 25 | 26 | def __init__(self, priority, parameter_id, source_id): 27 | self._priority = priority 28 | self._parameter_id = parameter_id 29 | self._source_id = source_id 30 | 31 | @property 32 | def hex(self): 33 | return '0x' + hex(self.frame_id)[2:].upper().zfill(8) 34 | 35 | def copy(self): 36 | return GMParameterIdExtended( 37 | self._priority, 38 | self._parameter_id, 39 | self._source_id, 40 | ) 41 | 42 | @property 43 | def priority(self): 44 | return self._priority 45 | 46 | @property 47 | def parameter_id(self): 48 | return self._parameter_id 49 | 50 | @property 51 | def source_id(self): 52 | return self._source_id 53 | 54 | @source_id.setter 55 | def source_id(self, source_id): 56 | 57 | self._source_id = source_id 58 | 59 | @property 60 | def frame_id(self): 61 | frame_id = self.priority & 0x7 62 | frame_id <<= 13 63 | frame_id |= self.parameter_id & 0x1FFF 64 | frame_id <<= 13 65 | frame_id |= self.source_id & 0x1FFF 66 | 67 | return frame_id 68 | 69 | @classmethod 70 | def from_frame_id(cls, frame_id): 71 | priority = (frame_id >> 26) & 0x7 72 | parameter_id = (frame_id >> 13) & 0x1FFF 73 | source_id = frame_id & 0x1FFF 74 | 75 | return cls(priority, parameter_id, source_id) 76 | 77 | def __eq__(self, other): 78 | if isinstance(other, int): 79 | other = self.from_frame_id(other) 80 | return other == self 81 | 82 | elif isinstance(other, GMParameterIdExtended): 83 | return other.parameter_id == self.parameter_id 84 | 85 | return False 86 | 87 | def __ne__(self, other): 88 | return not self.__eq__(other) 89 | 90 | def __int__(self): 91 | return self.frame_id 92 | 93 | def __str__(self): 94 | return str(int(self)) 95 | 96 | def __repr__(self): 97 | template = 'GMParameterIdExtended(priority=0x{0}, parameter_id=0x{1}, source_id=0x{3})' 98 | return template.format( 99 | hex(self.priority)[2:].upper(), 100 | hex(self.parameter_id)[2:].upper().zfill(4), 101 | hex(self.source_id)[2:].upper().zfill(4) 102 | ) 103 | 104 | 105 | class GMParameterId(object): 106 | 107 | def __init__(self, request_type, arbitration_id): 108 | self._request_type = request_type 109 | self._arbitration_id = arbitration_id 110 | 111 | @property 112 | def hex(self): 113 | return '0x' + hex(self.frame_id)[2:].upper().zfill(3) 114 | 115 | def copy(self): 116 | return GMParameterId( 117 | self._request_type, 118 | self._arbitration_id 119 | ) 120 | 121 | @property 122 | def request_type(self): 123 | return self._request_type 124 | 125 | @property 126 | def arbitration_id(self): 127 | return self._arbitration_id 128 | 129 | @property 130 | def frame_id(self): 131 | return self._request_type << 8 | self._arbitration_id 132 | 133 | @classmethod 134 | def from_frame_id(cls, frame_id): 135 | request_type = (frame_id >> 8) & 0xFF 136 | arbitration_id = frame_id & 0xFF 137 | return cls(request_type, arbitration_id) 138 | 139 | def __eq__(self, other): 140 | if isinstance(other, int): 141 | other = self.from_frame_id(other) 142 | return other == self 143 | 144 | elif isinstance(other, GMParameterId): 145 | return other.arbitration_id == self.arbitration_id 146 | 147 | return False 148 | 149 | def __ne__(self, other): 150 | return not self.__eq__(other) 151 | 152 | def __int__(self): 153 | return self.frame_id 154 | 155 | def __str__(self): 156 | return str(int(self)) 157 | 158 | def __repr__(self): 159 | template = 'GMParameterId(request_type=0x{0}, arbitration_id=0x{1})' 160 | return template.format( 161 | hex(self.request_type)[2:].upper().zfill(2), 162 | hex(self.arbitration_id)[2:].upper().zfill(2) 163 | ) 164 | -------------------------------------------------------------------------------- /vector_dbc/frame_id/j1939.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import bitstruct 24 | 25 | from ..errors import Error 26 | 27 | 28 | class J1939FrameId(object): 29 | def __init__( 30 | self, 31 | priority, 32 | reserved, 33 | data_page, 34 | pdu_format, 35 | pdu_specific, 36 | source_address 37 | ): 38 | self._priority = priority 39 | self._reserved = reserved 40 | self._data_page = data_page 41 | self._pdu_format = pdu_format 42 | self._pdu_specific = pdu_specific 43 | self._source_address = source_address 44 | 45 | def copy(self): 46 | return J1939FrameId( 47 | self._priority, 48 | self._reserved, 49 | self._data_page, 50 | self._pdu_format, 51 | self._pdu_specific, 52 | self._source_address 53 | ) 54 | 55 | @classmethod 56 | def from_pgn(cls, pgn): 57 | try: 58 | packed = bitstruct.pack('u18', pgn) 59 | except bitstruct.Error: 60 | raise Error( 61 | 'Expected a parameter group number 0..0x3ffff, ' 62 | 'but got {}.'.format(hex(pgn))) 63 | 64 | reserved, data_page, pdu_format, pdu_specific = bitstruct.unpack('u1u1u8u8', packed) 65 | return cls(0, reserved, data_page, pdu_format, pdu_specific, 0) 66 | 67 | @classmethod 68 | def from_frame_id(cls, frame_id): 69 | try: 70 | packed = bitstruct.pack('u29', frame_id) 71 | except bitstruct.Error: 72 | raise Error( 73 | 'Expected a frame id 0..0x1fffffff, ' 74 | 'but got {}.'.format(hex(frame_id))) 75 | 76 | return cls(*bitstruct.unpack('u3u1u1u8u8u8', packed)) 77 | 78 | @property 79 | def priority(self): 80 | return self._priority 81 | 82 | @property 83 | def reserved(self): 84 | return self._reserved 85 | 86 | @property 87 | def data_page(self): 88 | return self._data_page 89 | 90 | @property 91 | def pdu_format(self): 92 | return self._pdu_format 93 | 94 | @property 95 | def pdu_specific(self): 96 | return self._pdu_specific 97 | 98 | @property 99 | def source_address(self): 100 | return self._source_address 101 | 102 | @source_address.setter 103 | def source_address(self, source_address): 104 | self._source_address = source_address 105 | 106 | @property 107 | def frame_id(self): 108 | try: 109 | packed = bitstruct.pack( 110 | 'u3u1u1u8u8u8', self.priority, self.reserved, self.data_page, 111 | self.pdu_format, self.pdu_specific, self.source_address) 112 | except bitstruct.Error: 113 | if self.priority > 7: 114 | raise Error('Expected priority 0..7, but got {}.'.format(self.priority)) 115 | elif self.reserved > 1: 116 | raise Error('Expected reserved 0..1, but got {}.'.format(self.reserved)) 117 | elif self.data_page > 1: 118 | raise Error('Expected data page 0..1, but got {}.'.format(self.data_page)) 119 | elif self.pdu_format > 255: 120 | raise Error('Expected PDU format 0..255, but got {}.'.format(self.pdu_format)) 121 | elif self.pdu_specific > 255: 122 | raise Error('Expected PDU specific 0..255, but got {}.'.format(self.pdu_specific)) 123 | elif self.source_address > 255: 124 | raise Error('Expected source address 0..255, but got {}.'.format(self.source_address)) 125 | else: 126 | raise Error('Internal error.') 127 | 128 | return bitstruct.unpack('u29', packed)[0] 129 | 130 | @property 131 | def hex(self): 132 | return '0x' + hex(self.frame_id)[2:].upper().zfill(8) 133 | 134 | @property 135 | def pgn(self): 136 | if self.pdu_format < 240 and self.pdu_specific != 0: 137 | raise Error( 138 | 'Expected PDU specific 0 when PDU format is ' 139 | '0..239, but got {}.'.format(self.pdu_specific)) 140 | 141 | try: 142 | packed = bitstruct.pack( 143 | 'u1u1u8u8', self.reserved, self.data_page, 144 | self.pdu_format, self.pdu_specific) 145 | except bitstruct.Error: 146 | if self.reserved > 1: 147 | raise Error('Expected reserved 0..1, but got {}.'.format(self.reserved)) 148 | elif self.data_page > 1: 149 | raise Error('Expected data page 0..1, but got {}.'.format(self.data_page)) 150 | elif self.pdu_format > 255: 151 | raise Error('Expected PDU format 0..255, but got {}.'.format(self.pdu_format)) 152 | elif self.pdu_specific > 255: 153 | raise Error('Expected PDU specific 0..255, but got {}.'.format(self.pdu_specific)) 154 | else: 155 | raise Error('Internal error.') 156 | 157 | return bitstruct.unpack('u18', packed)[0] 158 | 159 | def __eq__(self, other): 160 | if isinstance(other, int): 161 | try: 162 | other = self.from_frame_id(other) 163 | except Error: 164 | return False 165 | 166 | return other == self 167 | elif isinstance(other, J1939FrameId): 168 | return other.frame_id == self.frame_id 169 | 170 | return False 171 | 172 | def __ne__(self, other): 173 | return not self.__eq__(other) 174 | 175 | def __int__(self): 176 | return self.frame_id 177 | 178 | def __str__(self): 179 | return str(int(self)) 180 | -------------------------------------------------------------------------------- /vector_dbc/high_precision_timer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2020 Kevin Schlosser 3 | 4 | import ctypes 5 | import sys 6 | import threading 7 | 8 | 9 | # OS-specific low-level timing functions: 10 | if sys.platform.startswith('win'): # for Windows: 11 | def micros(): 12 | """return a timestamp in microseconds (us)""" 13 | tics = ctypes.c_int64() 14 | freq = ctypes.c_int64() 15 | 16 | # get ticks on the internal ~2MHz QPC clock 17 | ctypes.windll.Kernel32.QueryPerformanceCounter(ctypes.byref(tics)) 18 | # get the actual freq. of the internal ~2MHz QPC clock 19 | ctypes.windll.Kernel32.QueryPerformanceFrequency(ctypes.byref(freq)) 20 | 21 | t_us = tics.value * 1e6 / freq.value 22 | return t_us 23 | 24 | def millis(): 25 | """return a timestamp in milliseconds (ms)""" 26 | tics = ctypes.c_int64() 27 | freq = ctypes.c_int64() 28 | 29 | # get ticks on the internal ~2MHz QPC clock 30 | ctypes.windll.Kernel32.QueryPerformanceCounter(ctypes.byref(tics)) 31 | # get the actual freq. of the internal ~2MHz QPC clock 32 | ctypes.windll.Kernel32.QueryPerformanceFrequency(ctypes.byref(freq)) 33 | 34 | t_ms = tics.value * 1e3 / freq.value 35 | return t_ms 36 | 37 | else: # for Linux: 38 | import os 39 | 40 | # Constants: 41 | # see here: 42 | # https://github.com/torvalds/linux/blob/master/include/uapi/linux/time.h 43 | CLOCK_MONOTONIC_RAW = 4 44 | # prepare ctype timespec structure of {long, long} 45 | 46 | class timespec(ctypes.Structure): 47 | _fields_ = [ 48 | ('tv_sec', ctypes.c_long), 49 | ('tv_nsec', ctypes.c_long) 50 | ] 51 | 52 | # Configure Python access to the clock_gettime C library, via ctypes: 53 | # Documentation: 54 | # -ctypes.CDLL: https://docs.python.org/3.2/library/ctypes.html 55 | # -librt.so.1 with clock_gettime: 56 | # https://docs.oracle.com/cd/E36784_01/html/E36873/librt-3lib.html 57 | # -Linux clock_gettime(): http://linux.die.net/man/3/clock_gettime 58 | librt = ctypes.CDLL('librt.so.1', use_errno=True) 59 | clock_gettime = librt.clock_gettime 60 | 61 | # specify input arguments and types to the C clock_gettime() function 62 | # (int clock_ID, timespec* t) 63 | clock_gettime.argtypes = [ctypes.c_int, ctypes.POINTER(timespec)] 64 | 65 | def monotonic_time(): 66 | """return a timestamp in seconds (sec)""" 67 | t = timespec() 68 | 69 | # (Note that clock_gettime() returns 0 for success, or -1 for failure, in 70 | # which case errno is set appropriately) 71 | # -see here: http://linux.die.net/man/3/clock_gettime 72 | if clock_gettime(CLOCK_MONOTONIC_RAW , ctypes.pointer(t)) != 0: 73 | # if clock_gettime() returns an error 74 | errno_ = ctypes.get_errno() 75 | 76 | raise OSError(errno_, os.strerror(errno_)) 77 | 78 | return t.tv_sec + t.tv_nsec * 1e-9 # sec 79 | 80 | def micros(): 81 | """return a timestamp in microseconds (us)""" 82 | return monotonic_time() * 1e6 # us 83 | 84 | def millis(): 85 | """eturn a timestamp in milliseconds (ms)""" 86 | return monotonic_time() * 1e3 # ms 87 | 88 | 89 | # Other timing functions: 90 | def delay(delay_ms): 91 | """delay for delay_ms milliseconds (ms)""" 92 | t_start = millis() 93 | while millis() - t_start < delay_ms: 94 | pass # do nothing 95 | 96 | return 97 | 98 | 99 | def delay_microseconds(delay_us): 100 | """delay for delay_us microseconds (us)""" 101 | t_start = micros() 102 | while micros() - t_start < delay_us: 103 | pass # do nothing 104 | 105 | return 106 | 107 | 108 | # Classes 109 | class TimerUS(object): 110 | def __init__(self): 111 | self.start = 0 112 | self.reset() 113 | 114 | def reset(self): 115 | self.start = micros() 116 | 117 | @property 118 | def elapsed(self): 119 | now = micros() 120 | return now - self.start 121 | 122 | 123 | class TimerMS(object): 124 | def __init__(self): 125 | self.start = 0 126 | self.reset() 127 | 128 | def reset(self): 129 | self.start = millis() 130 | 131 | def elapsed(self): 132 | now = millis() 133 | return now - self.start 134 | 135 | 136 | timer = None 137 | 138 | 139 | def function_timer(func): 140 | 141 | def _wrapper(*args, **kwargs): 142 | from .message import Message 143 | from .signal import Signal 144 | 145 | global timer 146 | 147 | if timer is None: 148 | timer = TimerUS() 149 | started = True 150 | else: 151 | started = False 152 | 153 | res = func(*args, **kwargs) 154 | if started: 155 | elapsed = timer.elapsed 156 | if args and isinstance(args[0], (Signal, Message)): 157 | if func.__name__ == '_do1' and elapsed > 300000: 158 | print(args[0].name) 159 | print(args[0].__class__.__name__ + '.' + func.__name__ + ':', elapsed, 'us') 160 | else: 161 | print(func.__name__ + ':', elapsed, 'us') 162 | 163 | timer = None 164 | 165 | return res 166 | 167 | return _wrapper 168 | 169 | 170 | -------------------------------------------------------------------------------- /vector_dbc/internal_database.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | class InternalDatabase(object): 25 | """Internal CAN database.""" 26 | 27 | def __init__( 28 | self, messages, nodes, buses, 29 | version, dbc_specifics=None 30 | ): 31 | self.messages = messages 32 | self.nodes = nodes 33 | self.buses = buses 34 | self.version = version 35 | self.dbc = dbc_specifics 36 | -------------------------------------------------------------------------------- /vector_dbc/message.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import binascii 24 | from copy import deepcopy 25 | from typing import Union 26 | 27 | from .utils import format_or 28 | from .utils import start_bit 29 | from .utils import encode_data 30 | from .utils import decode_data 31 | from .utils import create_encode_decode_formats 32 | from .errors import Error 33 | from .errors import EncodeError 34 | from .errors import DecodeError 35 | from .frame_id import J1939FrameId, GMParameterId, FrameId, GMParameterIdExtended 36 | from . import attribute 37 | from . import attribute_definition 38 | from . import can_data 39 | from .comment import MessageComment 40 | 41 | from collections import defaultdict 42 | 43 | 44 | class Message(attribute.AttributeMixin): 45 | """ 46 | A CAN message with frame id, comment, signals and other 47 | information. 48 | 49 | If `strict` is ``True`` an exception is raised if any signals are 50 | overlapping or if they don't fit in the message. 51 | """ 52 | 53 | _marker = 'BO_' 54 | 55 | def __init__( 56 | self, parent, frame_id, name, length, signals=None, comment=None, senders=None, 57 | dbc_specifics=None, is_extended_frame=False, signal_groups=None, strict=True 58 | ): 59 | frame_id_bit_length = frame_id.bit_length() 60 | 61 | if is_extended_frame: 62 | if frame_id_bit_length > 29: 63 | raise Error( 64 | 'Extended frame id 0x{:x} is more than 29 bits in ' 65 | 'message {}.'.format(frame_id, name)) 66 | elif frame_id_bit_length > 11: 67 | raise Error( 68 | 'Standard frame id 0x{:x} is more than 11 bits in ' 69 | 'message {}.'.format(frame_id, name)) 70 | 71 | self._frame_id = frame_id 72 | self._is_extended_frame = is_extended_frame 73 | self._name = name 74 | self._length = length 75 | self._signals = signals if signals else [] 76 | 77 | # if the 'comment' argument is a string, we assume that is an 78 | # english comment. this is slightly hacky because the 79 | # function's behavior depends on the type of the passed 80 | # argument, but it is quite convenient... 81 | if isinstance(comment, str): 82 | # use the first comment in the dictionary as "The" comment 83 | self._comments = {None: comment} 84 | elif comment is None: 85 | self._comments = {} 86 | else: 87 | # multi-lingual dictionary 88 | self._comments = comment 89 | 90 | self._senders = senders if senders else [] 91 | self._dbc = dbc_specifics 92 | self._signal_groups = signal_groups 93 | self._codecs = None 94 | self._signal_tree = None 95 | self._strict = strict 96 | self._parent = parent 97 | self.refresh() 98 | 99 | @property 100 | def database(self): 101 | return self._parent 102 | 103 | def _create_codec(self, parent_signal=None, multiplexer_id=None): 104 | """Create a codec of all signals with given parent signal. This is a recursive function.""" 105 | multiplexers = defaultdict(dict) 106 | 107 | # Find all signals matching given parent signal name and given 108 | # multiplexer id. Root signals' parent and multiplexer id are 109 | # both None. 110 | def _do1(signal): 111 | if signal.is_multiplexer: 112 | children_ids = [ 113 | item 114 | for s in self._signals 115 | for item in s.multiplexer 116 | if s.multiplexer.is_ok 117 | and s.multiplexer.multiplexer_signal.name != signal.name 118 | ] 119 | 120 | # Some CAN messages will have muxes containing only 121 | # the multiplexer and no additional signals. At Tesla 122 | # these are indicated in advance by assigning them an 123 | # enumeration. Here we ensure that any named 124 | # multiplexer is included, even if it has no child 125 | # signals. 126 | if signal.choices: 127 | children_ids += list(signal.choices.keys()) 128 | 129 | multiplexers[signal.name].update( 130 | {child_id: self._create_codec(signal.name, child_id) for child_id in set(children_ids)} 131 | ) 132 | 133 | return signal 134 | 135 | signals = [ 136 | _do1(signal) 137 | for signal in self._signals 138 | if ( 139 | (signal.multiplexer.is_ok is False or signal.multiplexer.multiplexer_signal.name == parent_signal) and 140 | (multiplexer_id is None or multiplexer_id in signal.multiplexer) 141 | ) 142 | ] 143 | 144 | return { 145 | 'signals': signals, 146 | 'formats': create_encode_decode_formats(signals, self._length), 147 | 'multiplexers': multiplexers 148 | } 149 | 150 | def _create_signal_tree(self, codec): 151 | """Create a multiplexing tree node of given codec. This is a recursive function.""" 152 | nodes = [ 153 | { 154 | signal.name: { 155 | mux: self._create_signal_tree(mux_codec) 156 | for mux, mux_codec in codec['multiplexers'][signal.name].items() 157 | } 158 | } 159 | if signal.name in codec['multiplexers'] 160 | else signal.name 161 | for signal in codec['signals'] 162 | ] 163 | 164 | return nodes 165 | 166 | @property 167 | def frame_id(self) -> Union[FrameId, J1939FrameId, GMParameterId, GMParameterIdExtended]: 168 | """The message frame id.""" 169 | if isinstance(self._frame_id, int): 170 | if self.database.use_gm_parameter_ids: 171 | if self._is_extended_frame: 172 | self._frame_id = GMParameterIdExtended.from_frame_id(self._frame_id) 173 | else: 174 | self._frame_id = GMParameterId.from_frame_id(self._frame_id) 175 | 176 | elif ( 177 | self.database.protocol_type is not None and 178 | self.database.protocol_type == 'J1939' 179 | ): 180 | self._frame_id = J1939FrameId.from_frame_id(self._frame_id) 181 | 182 | else: 183 | self._frame_id = FrameId(self._frame_id) 184 | 185 | return self._frame_id 186 | 187 | @frame_id.setter 188 | def frame_id(self, value): 189 | self._frame_id = value 190 | 191 | @property 192 | def is_extended_frame(self): 193 | """``True`` if the message is an extended frame, ``False`` otherwise.""" 194 | return self._is_extended_frame 195 | 196 | @is_extended_frame.setter 197 | def is_extended_frame(self, value): 198 | self._is_extended_frame = value 199 | 200 | @property 201 | def name(self): 202 | """The message name as a string.""" 203 | return self._name 204 | 205 | @name.setter 206 | def name(self, value): 207 | self._name = value 208 | 209 | @property 210 | def length(self): 211 | """The message data length in bytes.""" 212 | return self._length 213 | 214 | @length.setter 215 | def length(self, value): 216 | self._length = value 217 | 218 | @property 219 | def signals(self): 220 | """A list of all signals in the message.""" 221 | return self._signals 222 | 223 | @property 224 | def signal_groups(self): 225 | """A list of all signal groups in the message.""" 226 | return self._signal_groups 227 | 228 | @signal_groups.setter 229 | def signal_groups(self, value): 230 | self._signal_groups = value 231 | 232 | @property 233 | def comments(self): 234 | """The dictionary with the descriptions of the signal in multiple languages. ``None`` if unavailable.""" 235 | 236 | for key, val in list(self._comments.items())[:]: 237 | if val is not None: 238 | if isinstance(val, bytes): 239 | val = val.decode('utf-8') 240 | if not isinstance(val, MessageComment): 241 | val = MessageComment(val) 242 | val.message = self 243 | 244 | self._comments[key] = val 245 | 246 | return self._comments 247 | 248 | @comments.setter 249 | def comments(self, value): 250 | if value is None: 251 | value = {} 252 | 253 | if not isinstance(value, dict): 254 | raise TypeError('passed value is not a dictionary `dict`') 255 | 256 | for key, val in list(value.items())[:]: 257 | if val is not None: 258 | if isinstance(val, bytes): 259 | val = val.decode('utf-8') 260 | if not isinstance(val, (str, MessageComment)): 261 | val = str(val) 262 | 263 | value[key] = val 264 | 265 | self._comments = value 266 | 267 | @property 268 | def comment(self): 269 | """ 270 | The signal comment, or ``None`` if unavailable. 271 | 272 | Note that we implicitly try to return the comment's language 273 | to be English comment if multiple languages were specified. 274 | """ 275 | comment = self._comments.get(None, None) 276 | 277 | if comment is None: 278 | comment = self._comments.get('EN', None) 279 | 280 | if comment is not None: 281 | if isinstance(comment, bytes): 282 | comment = comment.decode('utf-8') 283 | 284 | if not isinstance(comment, MessageComment): 285 | comment = MessageComment(comment) 286 | comment.message = self 287 | self._comments['EN'] = comment 288 | 289 | else: 290 | if isinstance(comment, bytes): 291 | comment = comment.decode('utf-8') 292 | 293 | if not isinstance(comment, MessageComment): 294 | comment = MessageComment(comment) 295 | comment.message = self 296 | self._comments[None] = comment 297 | 298 | return comment 299 | 300 | @comment.setter 301 | def comment(self, value): 302 | if isinstance(value, bytes): 303 | value = value.decode('utf-8') 304 | 305 | if value is not None and not isinstance(value, (str, MessageComment)): 306 | value = str(value) 307 | 308 | self._comments = {None: value} 309 | 310 | @property 311 | def senders(self): 312 | """A list of all sender nodes of this message.""" 313 | senders = [node for node in self.database.nodes if node.name in self._senders] 314 | return senders 315 | 316 | @property 317 | def receivers(self): 318 | """A list of all receiver nodes attached to signals in this message""" 319 | receivers = set( 320 | receiver.name 321 | for signal in self.signals 322 | for receiver in signal.receivers 323 | ) 324 | return [node for node in self.database.nodes if node.name in receivers] 325 | 326 | @property 327 | def dbc(self): 328 | """An object containing dbc specific properties like e.g. attributes.""" 329 | return self._dbc 330 | 331 | @dbc.setter 332 | def dbc(self, value): 333 | self._dbc = value 334 | 335 | @property 336 | def signal_tree(self): 337 | """ 338 | All signal names and multiplexer ids as a tree. 339 | 340 | Multiplexer signals are dictionaries, while other signals are strings. 341 | """ 342 | return self._signal_tree 343 | 344 | def _get_mux_number(self, decoded, signal_name): 345 | mux = decoded[signal_name] 346 | 347 | if isinstance(mux, str): 348 | signal = self.get_signal_by_name(signal_name) 349 | mux = signal.choice_string_to_number(mux) 350 | 351 | return mux 352 | 353 | def _check_signals_ranges_scaling(self, signals, data): 354 | for signal in signals: 355 | value = data[signal.name] 356 | 357 | # Choices are checked later. 358 | if isinstance(value, str): 359 | continue 360 | 361 | if signal.minimum is not None and value < signal.minimum: 362 | raise EncodeError( 363 | "Expected signal '{}' value greater than or equal to " 364 | "{} in message '{}', but got {}.".format( 365 | signal.name, signal.minimum, self._name, value)) 366 | 367 | if signal.maximum is not None and value > signal.maximum: 368 | raise EncodeError( 369 | "Expected signal '{}' value less than or equal to " 370 | "{} in message '{}', but got {}.".format( 371 | signal.name, signal.maximum, self.name, value)) 372 | 373 | def _check_signals(self, signals, data, scaling): 374 | for signal in signals: 375 | if signal.name not in data: 376 | if signal.gen_sig_start_value is not None: 377 | data[signal.name] = signal.gen_sig_start_value + signal.offset 378 | else: 379 | raise EncodeError( 380 | "Expected signal value for '{}' in data, but " 381 | "got {}.".format(signal.name, data)) 382 | 383 | if scaling: 384 | self._check_signals_ranges_scaling(signals, data) 385 | 386 | def _encode(self, node, data, scaling, strict): 387 | if strict: 388 | self._check_signals(node['signals'], data, scaling) 389 | 390 | encoded = encode_data( 391 | data, node['signals'], node['formats'], scaling) 392 | padding_mask = node['formats'].padding_mask 393 | multiplexers = node['multiplexers'] 394 | 395 | for signal in multiplexers: 396 | mux = self._get_mux_number(data, signal) 397 | 398 | try: 399 | node = multiplexers[signal][mux] 400 | except KeyError: 401 | raise EncodeError( 402 | 'expected multiplexer id {}, but got ' 403 | '{}'.format(format_or(multiplexers[signal]), mux)) 404 | 405 | mux_encoded, mux_padding_mask = self._encode( 406 | node, data, scaling, strict) 407 | encoded |= mux_encoded 408 | padding_mask &= mux_padding_mask 409 | 410 | return encoded, padding_mask 411 | 412 | def encode(self, data, scaling=True, padding=False, strict=True): 413 | """ 414 | Encode given data as a message of this type. 415 | 416 | If `scaling` is ``False`` no scaling of signals is performed. 417 | 418 | If `padding` is ``True`` unused bits are encoded as 1. 419 | 420 | If `strict` is ``True`` all signal values must be within their 421 | allowed ranges, or an exception is raised. 422 | """ 423 | 424 | encoded, padding_mask = self._encode( 425 | self._codecs, data, scaling, strict) 426 | 427 | if padding: 428 | encoded |= padding_mask 429 | 430 | encoded |= (0x80 << (8 * self._length)) 431 | encoded = hex(encoded)[4:].rstrip('L') 432 | 433 | data = can_data.TXData(binascii.unhexlify(encoded)[:self._length]) 434 | data.frame_id = self.frame_id 435 | 436 | return data 437 | 438 | def _decode(self, node, data, decode_choices, scaling): 439 | decoded = decode_data( 440 | data, 441 | node['signals'], 442 | node['formats'], 443 | decode_choices, 444 | scaling 445 | ) 446 | 447 | multiplexers = node['multiplexers'] 448 | 449 | for signal in multiplexers: 450 | mux = self._get_mux_number(decoded, signal) 451 | 452 | try: 453 | node = multiplexers[signal][mux] 454 | except KeyError: 455 | raise DecodeError( 456 | 'expected multiplexer id {}, but got ' 457 | '{}'.format(format_or(multiplexers[signal]), mux)) 458 | 459 | decoded.update(self._decode( 460 | node, data, decode_choices, scaling)) 461 | 462 | return decoded 463 | 464 | def decode(self, data, decode_choices=True, scaling=True): 465 | """ 466 | Decode given data as a message of this type. 467 | 468 | If `decode_choices` is ``False`` scaled values are not 469 | converted to choice strings (if available). 470 | 471 | If `scaling` is ``False`` no scaling of signals is performed. 472 | """ 473 | data = data[:self._length] 474 | data = self._decode(self._codecs, data, decode_choices, scaling) 475 | 476 | res = can_data.RXData() 477 | res.frame_id = self.frame_id 478 | 479 | for key, value in list(data.items())[:]: 480 | signal = self.get_signal_by_name(key) 481 | signal._value = value 482 | res += [signal] 483 | 484 | return res 485 | 486 | def get_signal_by_name(self, name): 487 | signal = [signal for signal in self.signals if signal.name == name] 488 | if len(signal): 489 | return signal[0] 490 | 491 | raise KeyError(name) 492 | 493 | def is_multiplexed(self): 494 | """Returns ``True`` if the message is multiplexed, otherwise ``False``.""" 495 | return bool(self._codecs['multiplexers']) 496 | 497 | def _check_signal(self, message_bits, signal): 498 | signal_bits = signal.length * [signal.name] 499 | 500 | if signal.byte_order == 'big_endian': 501 | padding = start_bit(signal) * [None] 502 | signal_bits = padding + signal_bits 503 | else: 504 | signal_bits += signal.start * [None] 505 | 506 | if len(signal_bits) < len(message_bits): 507 | padding = (len(message_bits) - len(signal_bits)) * [None] 508 | reversed_signal_bits = padding + signal_bits 509 | else: 510 | reversed_signal_bits = signal_bits 511 | 512 | signal_bits = [] 513 | 514 | for i in range(0, len(reversed_signal_bits), 8): 515 | signal_bits = reversed_signal_bits[i:i + 8] + signal_bits 516 | 517 | # Check that the signal fits in the message. 518 | if len(signal_bits) > len(message_bits): 519 | print( 520 | 'The signal {} does not fit in message {}.'.format( 521 | signal.name, self.name)) 522 | return 523 | # raise Error( 524 | # 'The signal {} does not fit in message {}.'.format( 525 | # signal.name, 526 | # self.name)) 527 | 528 | # Check that the signal does not overlap with other 529 | # signals. 530 | for offset, signal_bit in enumerate(signal_bits): 531 | if signal_bit is not None: 532 | if message_bits[offset] is not None: 533 | print( 534 | 'The signals {} and {} are overlapping in message {}.'.format( 535 | signal.name, message_bits[offset], self.name)) 536 | 537 | for i, name in enumerate(message_bits): 538 | if name == signal_bit: 539 | message_bits[i] = None 540 | 541 | return 542 | 543 | message_bits[offset] = signal.name 544 | 545 | def _check_mux(self, message_bits, mux): 546 | signal_name, children = list(mux.items())[0] 547 | self._check_signal( 548 | message_bits, self.get_signal_by_name(signal_name)) 549 | children_message_bits = deepcopy(message_bits) 550 | 551 | for multiplexer_id in sorted(children): 552 | child_tree = children[multiplexer_id] 553 | child_message_bits = deepcopy(children_message_bits) 554 | self._check_signal_tree(child_message_bits, child_tree) 555 | 556 | for i, child_bit in enumerate(child_message_bits): 557 | if child_bit is not None: 558 | message_bits[i] = child_bit 559 | 560 | def _check_signal_tree(self, message_bits, signal_tree): 561 | [ 562 | self._check_mux(message_bits, signal_name) 563 | if isinstance(signal_name, dict) 564 | else self._check_signal(message_bits, self.get_signal_by_name(signal_name)) 565 | for signal_name in signal_tree 566 | ] 567 | 568 | def _check_signal_lengths(self): 569 | 570 | errors = [ 571 | signal 572 | for signal in self.signals 573 | if signal.length <= 0 574 | ] 575 | if errors: 576 | signal = errors[0] 577 | raise Error( 578 | 'The signal {} length {} is not greater than 0 in ' 579 | 'message {}.'.format(signal.name, signal.length, self.name) 580 | ) 581 | 582 | def refresh(self, strict=None): 583 | """ 584 | Refresh the internal message state. 585 | 586 | If `strict` is ``True`` an exception is raised if any signals 587 | are overlapping or if they don't fit in the message. This 588 | argument overrides the value of the same argument passed to 589 | the constructor. 590 | """ 591 | 592 | for signal in self._signals: 593 | signal._parent = self 594 | 595 | for signal_group in self._signal_groups: 596 | signal_group._parent = self 597 | 598 | self._signals.sort(key=start_bit) 599 | self._check_signal_lengths() 600 | self._codecs = self._create_codec() 601 | self._signal_tree = self._create_signal_tree(self._codecs) 602 | 603 | if strict is None: 604 | strict = self._strict 605 | 606 | if strict: 607 | message_bits = 8 * self.length * [None] 608 | self._check_signal_tree(message_bits, self.signal_tree) 609 | 610 | @property 611 | def gen_msg_delay_time(self): 612 | """ 613 | Defines the minimum time between two message transmissions. 614 | """ 615 | return self._get_attribute('GenMsgDelayTime') 616 | 617 | @gen_msg_delay_time.setter 618 | def gen_msg_delay_time(self, value): 619 | self._set_int_attribute('GenMsgDelayTime', 0, 2147483647, value) 620 | 621 | @property 622 | def gen_msg_cycle_time(self): 623 | """ 624 | Defines the fixed periodicity for cyclic message transmissions. 625 | """ 626 | return self._get_attribute('GenMsgCycleTime') 627 | 628 | @gen_msg_cycle_time.setter 629 | def gen_msg_cycle_time(self, value): 630 | self._set_int_attribute('GenMsgCycleTime', 0, 2147483647, value) 631 | 632 | @property 633 | def gen_msg_cycle_time_fast(self): 634 | """ 635 | Defines the periodicity for fast message transmissions. 636 | 637 | Messages are transmitted fast if one of the signals placed on the message is in an active state. 638 | """ 639 | return self._get_attribute('GenMsgCycleTimeFast') 640 | 641 | @gen_msg_cycle_time_fast.setter 642 | def gen_msg_cycle_time_fast(self, value): 643 | self._set_int_attribute('GenMsgCycleTimeFast', 0, 2147483647, value) 644 | 645 | @property 646 | def gen_msg_cycle_time_active(self): 647 | """ 648 | Same as GenMsgCycleTimeFast for the CAPL Generators interaction layer. 649 | """ 650 | return self._get_attribute('GenMsgCycleTimeActive') 651 | 652 | @gen_msg_cycle_time_active.setter 653 | def gen_msg_cycle_time_active(self, value): 654 | self._set_int_attribute('GenMsgCycleTimeActive', 0, 2147483647, value) 655 | 656 | @property 657 | def gen_msg_start_delay_time(self): 658 | """ 659 | Defines the delay after system start-up, the message is sent the first time. 660 | """ 661 | return self._get_attribute('GenMsgStartDelayTime') 662 | 663 | @gen_msg_start_delay_time.setter 664 | def gen_msg_start_delay_time(self, value): 665 | self._set_int_attribute('GenMsgStartDelayTime', 0, 2147483647, value) 666 | 667 | @property 668 | def gen_msg_nr_of_repetition(self): 669 | """ 670 | If the transmission of a message has to be repeated, 671 | this attribute defines the number how often it will be repeated. 672 | 673 | If a message transmission is repeated depends on the value of GenSigSendType 674 | of the signals placed on the message. 675 | """ 676 | return self._get_attribute('GenMsgNrOfRepetition') 677 | 678 | @gen_msg_nr_of_repetition.setter 679 | def gen_msg_nr_of_repetition(self, value): 680 | self._set_int_attribute('GenMsgNrOfRepetition', 0, 2147483647, value) 681 | 682 | @property 683 | def gen_msg_fast_on_start(self): 684 | """ 685 | Defines the time duration in milliseconds to send cyclic messages with a 686 | faster cycle time (GenMsgCycleTimeFast) after the IL is started. 687 | 688 | This works only if the normal as well as the fast cycle time are defined 689 | with values > 0. 690 | """ 691 | return self._get_attribute('GenMsgFastOnStart') 692 | 693 | @gen_msg_fast_on_start.setter 694 | def gen_msg_fast_on_start(self, value): 695 | self._set_int_attribute('GenMsgFastOnStart', 0, 2147483647, value) 696 | 697 | @property 698 | def gen_msg_il_support(self): 699 | """ 700 | Set to Yes if the message is handled by the interaction layer. 701 | """ 702 | return bool(self._get_attribute('GenMsgILSupport')) 703 | 704 | @gen_msg_il_support.setter 705 | def gen_msg_il_support(self, value): 706 | self._set_yes_no_attribute('GenMsgILSupport', int(value)) 707 | 708 | @property 709 | def tp_j1939_var_dlc(self): 710 | """ 711 | Set to Yes if the message is handled by the interaction layer. 712 | """ 713 | return bool(self._get_attribute('TpJ1939VarDlc')) 714 | 715 | @tp_j1939_var_dlc.setter 716 | def tp_j1939_var_dlc(self, value): 717 | self._set_yes_no_attribute('TpJ1939VarDlc', int(value)) 718 | 719 | @property 720 | def diag_request(self): 721 | """ 722 | Specifies that the message is used for a diagnostic request. 723 | """ 724 | return bool(self._get_attribute('DiagRequest')) 725 | 726 | @diag_request.setter 727 | def diag_request(self, value): 728 | self._set_yes_no_attribute('DiagRequest', int(value)) 729 | 730 | @property 731 | def diag_response(self): 732 | """ 733 | Specifies that the message is used for a diagnostic response. 734 | """ 735 | return bool(self._get_attribute('DiagResponse')) 736 | 737 | @diag_response.setter 738 | def diag_response(self, value): 739 | self._set_yes_no_attribute('DiagResponse', int(value)) 740 | 741 | @property 742 | def nm_message(self): 743 | """ 744 | Specifies that the message is used as a network management message of a particular node. 745 | """ 746 | return bool(self._get_attribute('NmMessage')) 747 | 748 | @nm_message.setter 749 | def nm_message(self, value): 750 | self._set_yes_no_attribute('NmMessage', int(value)) 751 | 752 | @property 753 | def gen_msg_auto_gen_dsp(self): 754 | return bool(self._get_attribute('GenMsgAutoGenDsp')) 755 | 756 | @gen_msg_auto_gen_dsp.setter 757 | def gen_msg_auto_gen_dsp(self, value): 758 | self._set_yes_no_attribute('GenMsgAutoGenDsp', int(value)) 759 | 760 | @property 761 | def gen_msg_auto_gen_snd(self): 762 | return bool(self._get_attribute('GenMsgAutoGenSnd')) 763 | 764 | @gen_msg_auto_gen_snd.setter 765 | def gen_msg_auto_gen_snd(self, value): 766 | self._set_yes_no_attribute('GenMsgAutoGenSnd', int(value)) 767 | 768 | @property 769 | def gen_msg_alt_setting(self): 770 | return self._get_attribute('GenMsgAltSetting') 771 | 772 | @gen_msg_alt_setting.setter 773 | def gen_msg_alt_setting(self, value): 774 | self._set_str_attribute('GenMsgAltSetting', value) 775 | 776 | @property 777 | def gen_msg_conditional_send(self): 778 | return self._get_attribute('GenMsgConditionalSend') 779 | 780 | @gen_msg_conditional_send.setter 781 | def gen_msg_conditional_send(self, value): 782 | self._set_str_attribute('GenMsgConditionalSend', value) 783 | 784 | @property 785 | def gen_msg_ev_name(self): 786 | return self._get_attribute('GenMsgEVName') 787 | 788 | @gen_msg_ev_name.setter 789 | def gen_msg_ev_name(self, value): 790 | self._set_str_attribute('GenMsgEVName', value) 791 | 792 | @property 793 | def gen_msg_post_if_setting(self): 794 | return self._get_attribute('GenMsgPostIfSetting') 795 | 796 | @gen_msg_post_if_setting.setter 797 | def gen_msg_post_if_setting(self, value): 798 | self._set_str_attribute('GenMsgPostIfSetting', value) 799 | 800 | @property 801 | def gen_msg_post_setting(self): 802 | return self._get_attribute('GenMsgPostSetting') 803 | 804 | @gen_msg_post_setting.setter 805 | def gen_msg_post_setting(self, value): 806 | self._set_str_attribute('GenMsgPostSetting', value) 807 | 808 | @property 809 | def gen_msg_pre_if_setting(self): 810 | return self._get_attribute('GenMsgPreIfSetting') 811 | 812 | @gen_msg_pre_if_setting.setter 813 | def gen_msg_pre_if_setting(self, value): 814 | self._set_str_attribute('GenMsgPreIfSetting', value) 815 | 816 | @property 817 | def gen_msg_pre_setting(self): 818 | return self._get_attribute('GenMsgPreSetting') 819 | 820 | @gen_msg_pre_setting.setter 821 | def gen_msg_pre_setting(self, value): 822 | self._set_str_attribute('GenMsgPreSetting', value) 823 | 824 | @property 825 | def gen_msg_send_type(self): 826 | """ 827 | Defines the message related send type. 828 | 829 | This attribute together with the send types of the signals placed on the 830 | message define the overall transmit behavior of the message. 831 | """ 832 | if 'GenMsgSendType' in self.dbc.attributes: 833 | value = self.dbc.attributes['GenMsgSendType'].value 834 | return self.dbc.attributes['GenMsgSendType'].definition.choices[value] 835 | 836 | @gen_msg_send_type.setter 837 | def gen_msg_send_type(self, value): 838 | if 'GenMsgSendType' in self.dbc.attributes: 839 | self.dbc.attributes['GenMsgSendType'].value = value 840 | else: 841 | if 'GenMsgSendType' in self.dbc.attribute_definitions: 842 | definition = self.dbc.attribute_definitions['GenMsgSendType'] 843 | else: 844 | definition = attribute_definition.AttributeDefinition( 845 | 'GenSigSendType', 846 | default_value=0, 847 | kind='SG_', 848 | type_name='ENUM', 849 | choices={ 850 | 0: 'cyclic', 851 | 1: 'spontaneous', 852 | 2: 'cyclicIfActive', 853 | 3: 'spontaneousWithDelay', 854 | 4: 'cyclicAndSpontaneous', 855 | 5: 'cyclicAndSpontaneousWithDelay', 856 | 6: 'spontaneousWithRepetition', 857 | 7: 'cyclicIfActiveAndSpontaneousWD' 858 | } 859 | ) 860 | 861 | choices = {v: k for k, v in definition.choices.items()} 862 | 863 | self.dbc.attributes['GenMsgSendType'] = attribute.Attribute(choices[value], definition) 864 | 865 | @property 866 | def dbc_frame_id(self): 867 | frame_id = int(self.frame_id) 868 | 869 | if self.is_extended_frame: 870 | frame_id |= 0x80000000 871 | 872 | return frame_id 873 | 874 | def __str__(self): 875 | res = [ 876 | 'BO_ {frame_id} {name}: {length} {senders}'.format( 877 | frame_id=int(self.frame_id), 878 | name=self.name, 879 | length=self.length, 880 | senders=self.senders[0].name if self.senders else 'Vector__XXX' 881 | ) 882 | ] 883 | 884 | res += [ 885 | str(signal) for signal in self.signals[::-1] 886 | ] 887 | 888 | return '\n'.join(res) 889 | 890 | -------------------------------------------------------------------------------- /vector_dbc/node.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from . import attribute 24 | from . import attribute_definition 25 | from . import ecu 26 | from .comment import NodeComment 27 | from .frame_id import ( 28 | J1939FrameId, 29 | GMParameterIdExtended 30 | ) 31 | 32 | 33 | class Node(attribute.AttributeMixin): 34 | """An NODE on the CAN bus.""" 35 | 36 | _marker = 'BU_' 37 | 38 | def __init__(self, parent, name, comment, dbc_specifics=None): 39 | self._name = name 40 | self._comment = comment 41 | self._dbc = dbc_specifics 42 | self._parent = parent 43 | 44 | def encode(self, frame_id_or_name, data, scaling=True, padding=False, strict=True): 45 | message = self._parent.get_message(frame_id_or_name) 46 | 47 | if self not in message.senders: 48 | raise KeyError(frame_id_or_name) 49 | 50 | data = message.encode(data, scaling, padding, strict) 51 | data.set_sending_node(self) 52 | 53 | return data 54 | 55 | def decode(self, frame_id_or_name, data, decode_choices=True, scaling=True): 56 | if isinstance(frame_id_or_name, bytes): 57 | frame_id_or_name = frame_id_or_name.decode('utf-8') 58 | 59 | message = self._parent.get_message(frame_id_or_name) 60 | 61 | if self not in message.receivers: 62 | raise KeyError(frame_id_or_name) 63 | 64 | tp_tx_indentfier = self.tp_tx_indentfier 65 | 66 | if isinstance(frame_id_or_name, int): 67 | frame_id = message.frame_id.from_frame_id(frame_id_or_name) 68 | 69 | elif isinstance(frame_id_or_name, str): 70 | frame_id = message.frame_id 71 | 72 | else: 73 | frame_id = frame_id_or_name 74 | 75 | if tp_tx_indentfier is not None: 76 | if ( 77 | (isinstance(frame_id, J1939FrameId) and frame_id.source_address != tp_tx_indentfier) or 78 | (isinstance(frame_id, GMParameterIdExtended) and frame_id.source_id != tp_tx_indentfier) 79 | ): 80 | raise KeyError(frame_id_or_name) 81 | 82 | data = message.decode(data, decode_choices, scaling) 83 | data.frame_id = frame_id 84 | 85 | return data 86 | 87 | @property 88 | def tx_signals(self): 89 | signals = [ 90 | signal for message in self._parent.messages 91 | for signal in message.signals 92 | if self in message.senders 93 | ] 94 | return signals 95 | 96 | @property 97 | def rx_signals(self): 98 | signals = [ 99 | signal for message in self._parent.messages 100 | for signal in message.signals 101 | if self in message.receivers 102 | ] 103 | return signals 104 | 105 | @property 106 | def database(self): 107 | return self._parent 108 | 109 | @property 110 | def name(self): 111 | """The node name as a string.""" 112 | return self._name 113 | 114 | @name.setter 115 | def name(self, value): 116 | self._name = value 117 | 118 | @property 119 | def comment(self): 120 | """The node comment, or ``None`` if unavailable.""" 121 | if self._comment is not None and not isinstance(self._comment, NodeComment): 122 | self._comment = NodeComment(self._comment) 123 | self._comment.node = self 124 | 125 | return self._comment 126 | 127 | @comment.setter 128 | def comment(self, value): 129 | if value is not None and not isinstance(value, (str, NodeComment)): 130 | value = str(value) 131 | 132 | self._comment = value 133 | 134 | @property 135 | def dbc(self): 136 | """An object containing dbc specific properties like e.g. attributes.""" 137 | return self._dbc 138 | 139 | @dbc.setter 140 | def dbc(self, value): 141 | self._dbc = value 142 | 143 | @property 144 | def nm_station_address(self): 145 | """ 146 | Defines the NM address of the node. 147 | 148 | This address is used directly to compute the identifier of the associated Network Management message 149 | 150 | message ID = NmStationAddress + NmBaseAddress 151 | 152 | Example Example 153 | 154 | NmStationAddress = 18, NmBaseAddress = 0x400 155 | => message ID = 0x412 156 | """ 157 | return self._get_attribute('NmStationAddress') 158 | 159 | @nm_station_address.setter 160 | def nm_station_address(self, value): 161 | self._set_hex_attribute('NmStationAddress', 0, 2147483647, value) 162 | 163 | @property 164 | def nm_j1939_aac(self): 165 | return self._get_attribute('NmJ1939AAC') 166 | 167 | @nm_j1939_aac.setter 168 | def nm_j1939_aac(self, value): 169 | self._set_int_attribute('NmJ1939AAC', 0, 1, value) 170 | 171 | @property 172 | def nm_j1939_industry_group(self): 173 | return self._get_attribute('NmJ1939IndustryGroup') 174 | 175 | @nm_j1939_industry_group.setter 176 | def nm_j1939_industry_group(self, value): 177 | self._set_int_attribute('NmJ1939IndustryGroup', 0, 7, value) 178 | 179 | @property 180 | def nm_j1939_system(self): 181 | return self._get_attribute('NmJ1939System') 182 | 183 | @nm_j1939_system.setter 184 | def nm_j1939_system(self, value): 185 | self._set_int_attribute('NmJ1939System', 0, 127, value) 186 | 187 | @property 188 | def nm_j1939_system_instance(self): 189 | return self._get_attribute('NmJ1939SystemInstance') 190 | 191 | @nm_j1939_system_instance.setter 192 | def nm_j1939_system_instance(self, value): 193 | self._set_int_attribute('NmJ1939SystemInstance', 0, 15, value) 194 | 195 | @property 196 | def nm_j1939_function(self): 197 | return self._get_attribute('NmJ1939Function') 198 | 199 | @nm_j1939_function.setter 200 | def nm_j1939_function(self, value): 201 | self._set_int_attribute('NmJ1939Function', 0, 255, value) 202 | 203 | @property 204 | def nm_j1939_function_instance(self): 205 | return self._get_attribute('NmJ1939FunctionInstance') 206 | 207 | @nm_j1939_function_instance.setter 208 | def nm_j1939_function_instance(self, value): 209 | self._set_int_attribute('NmJ1939FunctionInstance', 0, 7, value) 210 | 211 | @property 212 | def nm_j1939_ecu_instance(self): 213 | return self._get_attribute('NmJ1939ECUInstance') 214 | 215 | @nm_j1939_ecu_instance.setter 216 | def nm_j1939_ecu_instance(self, value): 217 | self._set_int_attribute('NmJ1939ECUInstance', 0, 3, value) 218 | 219 | @property 220 | def nm_j1939_manufacturer_code(self): 221 | return self._get_attribute('NmJ1939ManufacturerCode') 222 | 223 | @nm_j1939_manufacturer_code.setter 224 | def nm_j1939_manufacturer_code(self, value): 225 | self._set_int_attribute('NmJ1939ManufacturerCode', 0, 2047, value) 226 | 227 | @property 228 | def nm_j1939_identity_number(self): 229 | return self._get_attribute('NmJ1939IdentityNumber') 230 | 231 | @nm_j1939_identity_number.setter 232 | def nm_j1939_identity_number(self, value): 233 | self._set_int_attribute('NmJ1939IdentityNumber', 0, 2097151, value) 234 | 235 | @property 236 | def nm_can(self): 237 | """ 238 | Specifies the CAN channel (1 or 2) on which the NM should send and receive. 239 | 240 | Note that this attribute is only taken into consideration in older versions of 241 | CANoe or if the "compatible" mode of a newer version is used. 242 | """ 243 | return self._get_attribute('NmCAN') 244 | 245 | @nm_can.setter 246 | def nm_can(self, value): 247 | self._set_int_attribute('NmCAN', 1, 2, value) 248 | 249 | @property 250 | def gen_node_sleep_time(self): 251 | """ 252 | If the nodes have different wait times up to SleepRequest, 253 | set the time in this attribute in ms for each node. 254 | 255 | As soon as the attribute has a value>0, GenNWMSleepTime is not evaluated for this node. 256 | """ 257 | return self._get_attribute('GenNodSleepTime') 258 | 259 | @gen_node_sleep_time.setter 260 | def gen_node_sleep_time(self, value): 261 | self._set_int_attribute('GenNodSleepTime', 0, 2147483647, value) 262 | 263 | @property 264 | def nm_node(self): 265 | """Defines whether the node participates in the network management or not.""" 266 | if 'NmNode' in self.dbc.attributes: 267 | value = self.dbc.attributes['NmNode'].value 268 | return bool(value) 269 | 270 | @nm_node.setter 271 | def nm_node(self, value): 272 | self._set_yes_no_attribute('NmNode', value) 273 | 274 | @property 275 | def tp_node_base_address(self): 276 | """The base address that is used to determine the CAN ID for the TP messages (extended addressing mode only).""" 277 | return self._get_attribute('TpNodeBaseAddress') 278 | 279 | @tp_node_base_address.setter 280 | def tp_node_base_address(self, value): 281 | self._set_hex_attribute('TpNodeBaseAddress', 0x0, 0x7FF, value) 282 | 283 | @property 284 | def tp_tx_indentfier(self): 285 | """Transmit ID for normal and 11 bit mixed addressing.""" 286 | return self._get_attribute('TpTxIdentifier') 287 | 288 | @tp_tx_indentfier.setter 289 | def tp_tx_indentfier(self, value): 290 | self._set_hex_attribute('TpTxIdentifier', 0x0, 0x7FFFFFF, value) 291 | 292 | @property 293 | def tp_rx_indentfier(self): 294 | """Receive ID for normal and 11 bit mixed addressing.""" 295 | return self._get_attribute('TpRxIdentifier') 296 | 297 | @tp_rx_indentfier.setter 298 | def tp_rx_indentfier(self, value): 299 | self._set_hex_attribute('TpRxIdentifier', 0x0, 0x7FFFFFF, value) 300 | 301 | @property 302 | def tp_rx_mask(self): 303 | """Identifies the receive message.""" 304 | return self._get_attribute('TpRxMask') 305 | 306 | @tp_rx_mask.setter 307 | def tp_rx_mask(self, value): 308 | self._set_hex_attribute('TpRxMask', 0x0, 0x7FF, value) 309 | 310 | @property 311 | def tp_can_bus(self): 312 | """Identifies the CAN channel used.""" 313 | return self._get_attribute('TpCanBus') 314 | 315 | @tp_can_bus.setter 316 | def tp_can_bus(self, value): 317 | self._set_int_attribute('TpCanBus', 1, 2, value) 318 | 319 | @property 320 | def tp_tx_adr_mode(self): 321 | """ 322 | Defines whether the node uses physical (0) or functional (1) addressing 323 | (for address modes normal fixed and mixed). 324 | """ 325 | return self._get_attribute('TpTxAdrMode') 326 | 327 | @tp_tx_adr_mode.setter 328 | def tp_tx_adr_mode(self, value): 329 | self._set_int_attribute('TpTxAdrMode', 0, 1, value) 330 | 331 | @property 332 | def tp_address_extension(self): 333 | """Sets the address extension used for (11 bit) mixed addressing mode.""" 334 | return self._get_attribute('TpAddressExtension') 335 | 336 | @tp_address_extension.setter 337 | def tp_address_extension(self, value): 338 | self._set_int_attribute('TpAddressExtension', 0, 2147483647, value) 339 | 340 | @property 341 | def tp_st_min(self): 342 | """ 343 | Minimum Separation Time required for this node. 344 | 345 | This is the minimum time the node shall wait between the transmissions of two consecutive frames. 346 | """ 347 | return self._get_attribute('TpSTMin') 348 | 349 | @tp_st_min.setter 350 | def tp_st_min(self, value): 351 | self._set_int_attribute('TpSTMin', 0, 2147483647, value) 352 | 353 | @property 354 | def tp_block_size(self): 355 | """Block size for this node.""" 356 | return self._get_attribute('TpBlockSize') 357 | 358 | @tp_block_size.setter 359 | def tp_block_size(self, value): 360 | self._set_int_attribute('TpBlockSize', 0, 2147483647, value) 361 | 362 | @property 363 | def tp_addressing_mode(self): 364 | """ 365 | Defines the nodes addressing mode 366 | 367 | 0 (normal addressing), 368 | 1 (extended addressing), 369 | 2 (normal fixed addressing), 370 | 3 (mixed addressing), 371 | 4 (11 bit mixed addressing) 372 | """ 373 | return self._get_attribute('TpAddressingMode') 374 | 375 | @tp_addressing_mode.setter 376 | def tp_addressing_mode(self, value): 377 | self._set_int_attribute('TpAddressingMode', 0, 4, value) 378 | 379 | @property 380 | def tp_target_address(self): 381 | """This attribute is relevant for extended addressing only. It specifies the nodes target address.""" 382 | return self._get_attribute('TpTargetAddress') 383 | 384 | @tp_target_address.setter 385 | def tp_target_address(self, value): 386 | self._set_hex_attribute('TpTargetAddress', 0x0, 0xFF, value) 387 | 388 | @property 389 | def tp_use_fc(self): 390 | """ 391 | Indicates whether flow control messages should be used (1) or not (0). 392 | 393 | The flow control mechanism allows the receiver to inform the sender about the receivers capabilities. 394 | """ 395 | return self._get_attribute('TpUseFC') 396 | 397 | @tp_use_fc.setter 398 | def tp_use_fc(self, value): 399 | self._set_int_attribute('TpUseFC', 0, 1, value) 400 | 401 | @property 402 | def diag_station_address(self): 403 | """Specifies the nodes diagnostic address.""" 404 | return self._get_attribute('DiagStationAddress') 405 | 406 | @diag_station_address.setter 407 | def diag_station_address(self, value): 408 | self._set_hex_attribute('DiagStationAddress', 0x0, 0xFF, value) 409 | 410 | @property 411 | def node_layer_modules(self): 412 | """ 413 | List of node layer DLLs loaded in CANoe. 414 | 415 | The node layer modules are separated in the string with a comma (",") 416 | e.g. "OSEK_TP.DLL, OSEKNM.DLL". 417 | """ 418 | return self._get_attribute('NodeLayerModules') 419 | 420 | @node_layer_modules.setter 421 | def node_layer_modules(self, value): 422 | self._set_str_attribute('NodeLayerModules', value) 423 | 424 | @property 425 | def ecu(self): 426 | """ 427 | Specifies the name of the ECU the node belongs to. 428 | 429 | This attribute is only needed if an ECU contains several nodes 430 | (e.g. to define interfaces to multiple buses). 431 | """ 432 | res = self._get_attribute('ECU') 433 | if res is not None: 434 | res = ecu.ECU(self._parent, res) 435 | 436 | return res 437 | 438 | @ecu.setter 439 | def ecu(self, value): 440 | if isinstance(value, ecu.ECU): 441 | value = value.name 442 | 443 | self._set_str_attribute('ECU', value) 444 | 445 | @property 446 | def canoe_start_delay(self): 447 | """ 448 | Time span after the start of measurement 449 | 450 | Which the particular node remains completely passive. 451 | It does not react to external influences, nor does it activate itself. 452 | It does not change its behavior and function like every other node until the time span has elapsed. 453 | """ 454 | return self._get_attribute('CANoeStartDelay') 455 | 456 | @canoe_start_delay.setter 457 | def canoe_start_delay(self, value): 458 | self._set_int_attribute('CANoeStartDelay', 0, 2147483647, value) 459 | 460 | @property 461 | def canoe_drift(self): 462 | """Percentage the timers used in the node are lengthened or shortened.""" 463 | return self._get_attribute('CANoeDrift') 464 | 465 | @canoe_drift.setter 466 | def canoe_drift(self, value): 467 | self._set_int_attribute('CANoeDrift', 0, 2147483647, value) 468 | 469 | @property 470 | def canoe_jitter_min(self): 471 | """ 472 | With CANoeJitterMin and CANOeJitterMax the user specifies the interval within 473 | which the fluctuation of the timers of the node should lie. The fluctuation is 474 | uniformly distributed. 475 | """ 476 | return self._get_attribute('CANoeJitterMin') 477 | 478 | @canoe_jitter_min.setter 479 | def canoe_jitter_min(self, value): 480 | self._set_int_attribute('CANoeJitterMin', 0, 2147483647, value) 481 | 482 | @property 483 | def canoe_jitter_max(self): 484 | """ 485 | With CANoeJitterMin and CANOeJitterMax the user specifies the interval within 486 | which the fluctuation of the timers of the node should lie. The fluctuation is 487 | uniformly distributed. 488 | """ 489 | return self._get_attribute('CANoeJitterMax') 490 | 491 | @canoe_jitter_max.setter 492 | def canoe_jitter_max(self, value): 493 | self._set_int_attribute('CANoeJitterMax', 0, 2147483647, value) 494 | 495 | @property 496 | def il_used(self): 497 | """Set to Yes if the node uses an interaction layer.""" 498 | if 'ILUsed' in self.dbc.attributes: 499 | value = self.dbc.attributes['ILUsed'].value 500 | return bool(value) 501 | 502 | @il_used.setter 503 | def il_used(self, value): 504 | 505 | if 'ILUsed' in self.dbc.attributes: 506 | self.dbc.attributes['ILUsed'].value = value 507 | else: 508 | 509 | if 'ILUsed' in self.dbc.attribute_definitions: 510 | definition = self.dbc.attribute_definitions['ILUsed'] 511 | else: 512 | 513 | definition = attribute_definition.AttributeDefinition( 514 | 'ILUsed', 515 | default_value=0, 516 | kind='BU_', 517 | type_name='ENUM', 518 | choices={0: 'No', 1: 'Yes'} 519 | ) 520 | 521 | self.dbc.attributes['ILUsed'] = attribute.Attribute(int(value), definition) 522 | 523 | @property 524 | def gen_nod_auto_gen_dsp(self): 525 | if 'GenNodAutoGenDsp' in self.dbc.attributes: 526 | value = self.dbc.attributes['GenNodAutoGenDsp'].value 527 | return bool(value) 528 | 529 | @gen_nod_auto_gen_dsp.setter 530 | def gen_nod_auto_gen_dsp(self, value): 531 | if 'GenNodAutoGenDsp' in self.dbc.attributes: 532 | self.dbc.attributes['GenNodAutoGenDsp'].value = value 533 | else: 534 | 535 | if 'GenNodAutoGenDsp' in self.dbc.attribute_definitions: 536 | definition = self.dbc.attribute_definitions['GenNodAutoGenDsp'] 537 | else: 538 | 539 | definition = attribute_definition.AttributeDefinition( 540 | 'GenNodAutoGenDsp', 541 | default_value=0, 542 | kind='BU__', 543 | type_name='ENUM', 544 | choices={0: 'No', 1: 'Yes'} 545 | ) 546 | 547 | self.dbc.attributes['GenNodAutoGenDsp'] = attribute.Attribute(int(value), definition) 548 | 549 | @property 550 | def gen_nod_auto_gen_snd(self): 551 | if 'GenNodAutoGenSnd' in self.dbc.attributes: 552 | value = self.dbc.attributes['GenNodAutoGenSnd'].value 553 | return bool(value) 554 | 555 | @gen_nod_auto_gen_snd.setter 556 | def gen_nod_auto_gen_snd(self, value): 557 | 558 | if 'GenNodAutoGenSnd' in self.dbc.attributes: 559 | self.dbc.attributes['GenNodAutoGenSnd'].value = value 560 | else: 561 | 562 | if 'GenNodAutoGenSnd' in self.dbc.attribute_definitions: 563 | definition = self.dbc.attribute_definitions['GenNodAutoGenSnd'] 564 | else: 565 | 566 | definition = attribute_definition.AttributeDefinition( 567 | 'GenNodAutoGenSnd', 568 | default_value=0, 569 | kind='BU__', 570 | type_name='ENUM', 571 | choices={0: 'No', 1: 'Yes'} 572 | ) 573 | 574 | self.dbc.attributes['GenNodAutoGenSnd'] = attribute.Attribute(int(value), definition) 575 | 576 | @property 577 | def gen_nod_nod_sleep_time(self): 578 | return self._get_attribute('GenNodSleepTime') 579 | 580 | @gen_nod_nod_sleep_time.setter 581 | def gen_nod_nod_sleep_time(self, value): 582 | self._set_int_attribute('GenNodSleepTime', 0, 2147483647, value) 583 | -------------------------------------------------------------------------------- /vector_dbc/sg_mul_val.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def _create_mux_ranges(multiplexer_ids): 4 | """ 5 | Create a list of ranges based on a list of single values. 6 | 7 | Example: 8 | Input: [1, 2, 3, 5, 7, 8, 9] 9 | Output: [[1, 3], [5, 5], [7, 9]] 10 | """ 11 | ordered = sorted(multiplexer_ids) 12 | # Anything but ordered[0] - 1 13 | prev_value = ordered[0] 14 | ranges = [] 15 | 16 | for value in ordered: 17 | if value == prev_value + 1: 18 | ranges[-1][1] = value 19 | else: 20 | ranges.append([value, value]) 21 | 22 | prev_value = value 23 | 24 | return ranges 25 | 26 | 27 | class SG_MUL_VAL(object): 28 | 29 | def __init__(self, parent, multiplexer_signal, multiplexer_ids): 30 | self._parent = parent 31 | 32 | signal = [signal for signal in self._parent.message.signals if signal.name == self._multiplexer_signal] 33 | if len(signal): 34 | self._multiplexer_signal = signal[0] 35 | else: 36 | self._multiplexer_signal = multiplexer_signal 37 | 38 | # for signal in self._parent.message.signals: 39 | # if signal.name == self._multiplexer_signal: 40 | # self._multiplexer_signal = signal 41 | # break 42 | # else: 43 | 44 | self._multiplexer_ids = multiplexer_ids 45 | 46 | def __radd__(self, other): 47 | if isinstance(other, tuple): 48 | other = list(other) 49 | if not isinstance(other, list): 50 | other = [other] 51 | 52 | for item in other: 53 | if item not in self._multiplexer_ids: 54 | self._multiplexer_ids += [item] 55 | 56 | def __contains__(self, item): 57 | return item in self._multiplexer_ids 58 | 59 | def __iter__(self): 60 | return iter(self._multiplexer_ids) 61 | 62 | def __bool__(self): 63 | return len(self) > 0 64 | 65 | def __len__(self): 66 | return len(self._multiplexer_ids) 67 | 68 | def __getitem__(self, item): 69 | return self._multiplexer_ids[item] 70 | 71 | def __setitem__(self, key, value): 72 | self._multiplexer_ids[key] = value 73 | 74 | @property 75 | def multiplexer_signal(self): 76 | if self._multiplexer_signal is None: 77 | return 78 | 79 | if isinstance(self._multiplexer_signal, _signal.Signal): 80 | return self._multiplexer_signal 81 | 82 | res = [signal for signal in self._parent.message.signals if signal.name == self._multiplexer_signal] 83 | if len(res): 84 | self._multiplexer_signal = res[0] 85 | return res[0] 86 | 87 | @multiplexer_signal.setter 88 | def multiplexer_signal(self, value): 89 | if isinstance(value, _signal.Signal): 90 | value = value.name 91 | 92 | try: 93 | signal = self._parent.message.get_signal_by_name(value) 94 | if not signal.is_multiplexer: 95 | raise ValueError('signal is not a multiplexer') 96 | 97 | except KeyError: 98 | raise ValueError('signal not found') 99 | 100 | @property 101 | def is_ok(self): 102 | return self._multiplexer_signal is not None 103 | 104 | @property 105 | def signal(self): 106 | return self._parent 107 | 108 | def __str__(self): 109 | if not self.is_ok: 110 | return '' 111 | 112 | return 'SG_MUL_VAL_ {frame_id} {name} {multiplexer} {ranges};'.format( 113 | frame_id=self._parent.message.dbc_frame_id, 114 | name=self._parent.name, 115 | multiplexer=self.multiplexer_signal.name, 116 | ranges=', '.join( 117 | '{}-{}'.format(minimum, maximum) 118 | for minimum, maximum in _create_mux_ranges(self) 119 | ) 120 | ) 121 | 122 | 123 | from . import signal as _signal 124 | -------------------------------------------------------------------------------- /vector_dbc/signal.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from . import attribute 24 | from . import attribute_definition 25 | 26 | from .errors import EncodeError 27 | from .comment import SignalComment 28 | from . import sg_mul_val 29 | 30 | 31 | class Decimal(object): 32 | """ 33 | Holds the same values as 34 | :attr:`~cantools.database.can.Signal.scale`, 35 | :attr:`~cantools.database.can.Signal.offset`, 36 | :attr:`~cantools.database.can.Signal.minimum` and 37 | :attr:`~cantools.database.can.Signal.maximum`, but as 38 | ``decimal.Decimal`` instead of ``int`` and ``float`` for higher 39 | precision (no rounding errors). 40 | """ 41 | 42 | def __init__(self, scale=None, offset=None, minimum=None, maximum=None): 43 | self._scale = scale 44 | self._offset = offset 45 | self._minimum = minimum 46 | self._maximum = maximum 47 | 48 | @property 49 | def scale(self): 50 | """The scale factor of the signal value as ``decimal.Decimal``.""" 51 | return self._scale 52 | 53 | @scale.setter 54 | def scale(self, value): 55 | self._scale = value 56 | 57 | @property 58 | def offset(self): 59 | """The offset of the signal value as ``decimal.Decimal``.""" 60 | return self._offset 61 | 62 | @offset.setter 63 | def offset(self, value): 64 | self._offset = value 65 | 66 | @property 67 | def minimum(self): 68 | """The minimum value of the signal as ``decimal.Decimal``, or ``None`` if unavailable.""" 69 | return self._minimum 70 | 71 | @minimum.setter 72 | def minimum(self, value): 73 | self._minimum = value 74 | 75 | @property 76 | def maximum(self): 77 | """The maximum value of the signal as ``decimal.Decimal``, or ``None`` if unavailable.""" 78 | return self._maximum 79 | 80 | @maximum.setter 81 | def maximum(self, value): 82 | self._maximum = value 83 | 84 | 85 | class Signal(attribute.AttributeMixin): 86 | """ 87 | A CAN signal with position, size, unit and other information. A 88 | signal is part of a message. 89 | 90 | Signal bit numbering in a message: 91 | 92 | .. code:: text 93 | 94 | Byte: 0 1 2 3 4 5 6 7 95 | +--------+--------+--------+--------+--------+--------+--------+--------+--- - - 96 | | | | | | | | | | 97 | +--------+--------+--------+--------+--------+--------+--------+--------+--- - - 98 | Bit: 7 0 15 8 23 16 31 24 39 32 47 40 55 48 63 56 99 | 100 | Big endian signal with start bit 2 and length 5 (0=LSB, 4=MSB): 101 | 102 | .. code:: text 103 | 104 | Byte: 0 1 2 3 105 | +--------+--------+--------+--- - - 106 | | |432|10| | | 107 | +--------+--------+--------+--- - - 108 | Bit: 7 0 15 8 23 16 31 109 | 110 | Little endian signal with start bit 2 and length 9 (0=LSB, 8=MSB): 111 | 112 | .. code:: text 113 | 114 | Byte: 0 1 2 3 115 | +--------+--------+--------+--- - - 116 | |543210| | |876| | 117 | +--------+--------+--------+--- - - 118 | Bit: 7 0 15 8 23 16 31 119 | """ 120 | _marker = 'SG_' 121 | 122 | def __init__( 123 | self, parent, name, start, length, byte_order='little_endian', is_signed=False, 124 | scale=1, offset=0, minimum=None, maximum=None, unit=None, choices=None, dbc_specifics=None, 125 | comment=None, receivers=None, is_multiplexer=False, 126 | multiplexer_ids=None, multiplexer_signal=None, is_float=False, decimal=None 127 | ): 128 | self._name = name 129 | self._start = start 130 | self._length = length 131 | self._byte_order = byte_order 132 | self._is_signed = is_signed 133 | self._scale = scale 134 | self._offset = offset 135 | self._minimum = minimum 136 | self._maximum = maximum 137 | self._decimal = Decimal() if decimal is None else decimal 138 | self._unit = unit 139 | self._choices = choices 140 | self._dbc = dbc_specifics 141 | self._value = None 142 | 143 | # if the 'comment' argument is a string, we assume that is an 144 | # english comment. this is slightly hacky because the 145 | # function's behavior depends on the type of the passed 146 | # argument, but it is quite convenient... 147 | if isinstance(comment, str): 148 | # use the first comment in the dictionary as "The" comment 149 | self._comments = {None: comment} 150 | elif comment is None: 151 | self._comments = {} 152 | else: 153 | # multi-lingual dictionary 154 | self._comments = comment 155 | 156 | if not multiplexer_ids: 157 | multiplexer_ids = [] 158 | 159 | self._receivers = [] if receivers is None else receivers 160 | self._is_multiplexer = is_multiplexer 161 | self._is_float = is_float 162 | self._parent = parent 163 | self._multiplexer = sg_mul_val.SG_MUL_VAL(self, multiplexer_signal, multiplexer_ids) 164 | 165 | @property 166 | def message(self): 167 | return self._parent 168 | 169 | @property 170 | def name(self): 171 | """The signal name as a string.""" 172 | return self._name 173 | 174 | @name.setter 175 | def name(self, value): 176 | self._name = value 177 | 178 | @property 179 | def start(self): 180 | """The start bit position of the signal within its message.""" 181 | return self._start 182 | 183 | @start.setter 184 | def start(self, value): 185 | self._start = value 186 | 187 | @property 188 | def length(self): 189 | """The length of the signal in bits.""" 190 | return self._length 191 | 192 | @length.setter 193 | def length(self, value): 194 | self._length = value 195 | 196 | @property 197 | def byte_order(self): 198 | """Signal byte order as ``'little_endian'`` or ``'big_endian'``.""" 199 | return self._byte_order 200 | 201 | @byte_order.setter 202 | def byte_order(self, value): 203 | self._byte_order = value 204 | 205 | @property 206 | def is_signed(self): 207 | """ 208 | ``True`` if the signal is signed, ``False`` otherwise. Ignore this 209 | attribute if :data:`~cantools.db.Signal.is_float` is ``True``. 210 | """ 211 | if self.is_float: 212 | return None 213 | 214 | return self._is_signed 215 | 216 | @is_signed.setter 217 | def is_signed(self, value): 218 | if self.is_float: 219 | raise ValueError('This cannot be set when the signal data type is set to float') 220 | self._is_signed = value 221 | 222 | @property 223 | def is_float(self): 224 | """``True`` if the signal is a float, ``False`` otherwise.""" 225 | return self._is_float 226 | 227 | @is_float.setter 228 | def is_float(self, value): 229 | self._is_float = value 230 | 231 | @property 232 | def scale(self): 233 | """The scale factor of the signal value.""" 234 | return self._scale 235 | 236 | @scale.setter 237 | def scale(self, value): 238 | self._scale = value 239 | 240 | @property 241 | def offset(self): 242 | """The offset of the signal value.""" 243 | return self._offset 244 | 245 | @offset.setter 246 | def offset(self, value): 247 | self._offset = value 248 | 249 | @property 250 | def minimum(self): 251 | """The minimum value of the signal, or ``None`` if unavailable.""" 252 | return self._minimum 253 | 254 | @minimum.setter 255 | def minimum(self, value): 256 | self._minimum = value 257 | 258 | @property 259 | def maximum(self): 260 | """The maximum value of the signal, or ``None`` if unavailable.""" 261 | return self._maximum 262 | 263 | @maximum.setter 264 | def maximum(self, value): 265 | self._maximum = value 266 | 267 | @property 268 | def decimal(self): 269 | """ 270 | The high precision values of 271 | :attr:`~cantools.database.can.Signal.scale`, 272 | :attr:`~cantools.database.can.Signal.offset`, 273 | :attr:`~cantools.database.can.Signal.minimum` and 274 | :attr:`~cantools.database.can.Signal.maximum`. 275 | 276 | See :class:`~cantools.database.can.signal.Decimal` for more 277 | details. 278 | """ 279 | return self._decimal 280 | 281 | @property 282 | def unit(self): 283 | """The unit of the signal as a string, or ``None`` if unavailable.""" 284 | return self._unit 285 | 286 | @unit.setter 287 | def unit(self, value): 288 | self._unit = value 289 | 290 | @property 291 | def choices(self): 292 | """A dictionary mapping signal values to enumerated choices, or ``None`` if unavailable.""" 293 | return self._choices 294 | 295 | @property 296 | def dbc(self): 297 | """An object containing dbc specific properties like e.g. attributes.""" 298 | return self._dbc 299 | 300 | @dbc.setter 301 | def dbc(self, value): 302 | self._dbc = value 303 | 304 | @property 305 | def is_updated(self): 306 | return self._value is not None 307 | 308 | @property 309 | def value(self): 310 | val = self._value 311 | self._value = None 312 | return val 313 | 314 | @property 315 | def comments(self): 316 | """The dictionary with the descriptions of the signal in multiple languages. ``None`` if unavailable.""" 317 | 318 | for key, val in list(self._comments.items())[:]: 319 | if val is not None: 320 | if isinstance(val, bytes): 321 | val = val.decode('utf-8') 322 | if not isinstance(val, SignalComment): 323 | val = SignalComment(val) 324 | val.signal = self 325 | 326 | self._comments[key] = val 327 | 328 | return self._comments 329 | 330 | @comments.setter 331 | def comments(self, value): 332 | if value is None: 333 | value = {} 334 | 335 | if not isinstance(value, dict): 336 | raise TypeError('passed value is not a dictionary `dict`') 337 | 338 | for key, val in list(value.items())[:]: 339 | if val is not None: 340 | if isinstance(val, bytes): 341 | val = val.decode('utf-8') 342 | if not isinstance(val, (str, SignalComment)): 343 | val = str(val) 344 | 345 | value[key] = val 346 | 347 | self._comments = value 348 | 349 | @property 350 | def comment(self): 351 | """ 352 | The signal comment, or ``None`` if unavailable. 353 | 354 | Note that we implicitly try to return the comment's language 355 | to be English comment if multiple languages were specified. 356 | """ 357 | comment = self._comments.get(None, None) 358 | 359 | if comment is None: 360 | comment = self._comments.get('EN', None) 361 | 362 | if comment is not None: 363 | if isinstance(comment, bytes): 364 | comment = comment.decode('utf-8') 365 | 366 | if not isinstance(comment, SignalComment): 367 | comment = SignalComment(comment) 368 | comment.signal = self 369 | self._comments['EN'] = comment 370 | 371 | else: 372 | if isinstance(comment, bytes): 373 | comment = comment.decode('utf-8') 374 | 375 | if not isinstance(comment, SignalComment): 376 | comment = SignalComment(comment) 377 | comment.signal = self 378 | self._comments[None] = comment 379 | 380 | return comment 381 | 382 | @comment.setter 383 | def comment(self, value): 384 | if isinstance(value, bytes): 385 | value = value.decode('utf-8') 386 | 387 | if value is not None and not isinstance(value, (str, SignalComment)): 388 | value = str(value) 389 | 390 | self._comments = {None: value} 391 | 392 | @property 393 | def receivers(self): 394 | """A list of all receiver nodes of this signal.""" 395 | receivers = [node for node in self.message.database.nodes if node.name in self._receivers] 396 | return receivers 397 | 398 | @property 399 | def is_multiplexer(self): 400 | """``True`` if this is the multiplexer signal in a message, ``False` otherwise.""" 401 | return self._is_multiplexer 402 | 403 | @is_multiplexer.setter 404 | def is_multiplexer(self, value): 405 | self._is_multiplexer = value 406 | 407 | @property 408 | def multiplexer(self): 409 | """The multiplexer ids list if the signal is part of a multiplexed message, ``None`` otherwise.""" 410 | return self._multiplexer 411 | 412 | def choice_string_to_number(self, string): 413 | for choice_number, choice_string in self.choices.items(): 414 | if choice_string == string: 415 | return choice_number 416 | 417 | def encode(self, data=None, scaling=True, padding=False, strict=True): 418 | """ 419 | Encode a signal directly 420 | 421 | This will encode a message where all other signals in the message have initial 422 | values set and you want to ue those initial values. If there is a multiplexer_signal 423 | for this signal then that will also get set to this signal name 424 | 425 | :param data: the data to set or None if there has been an initial value set and that is what is to be used 426 | :type data: optional, any 427 | """ 428 | 429 | if self._parent is None: 430 | raise EncodeError('This signal is not attached to any database or message') 431 | if data is None: 432 | if self.gen_sig_start_value is not None: 433 | data = {self.name: self.gen_sig_start_value + self.offset} 434 | else: 435 | raise EncodeError( 436 | "You must supply a signal value for signal {0}".format(self.name) 437 | ) 438 | else: 439 | data = {self.name: data} 440 | 441 | if self.multiplexer.is_ok: 442 | m_signal = self.multiplexer.multiplexer_signal 443 | data[m_signal.name] = m_signal.choices[self.multiplexer[0]] 444 | 445 | return self._parent.encode(data, scaling=scaling, padding=padding, strict=strict) 446 | 447 | @property 448 | def gen_sig_send_type(self): 449 | """ 450 | Defines the signal related send type. 451 | 452 | This attribute together with the message related send type and the 453 | send types of the other signals placed on the message define the overall 454 | transmit behavior of the message. 455 | """ 456 | value = self._get_attribute('GenSigSendType') 457 | if value is not None: 458 | return self.dbc.attributes['GenSigSendType'].definition.choices[value] 459 | 460 | @gen_sig_send_type.setter 461 | def gen_sig_send_type(self, value): 462 | if 'GenSigSendType' in self.dbc.attributes: 463 | self.dbc.attributes['GenSigSendType'].value = value 464 | else: 465 | if 'GenSigSendType' in self.dbc.attribute_definitions: 466 | definition = self.dbc.attribute_definitions['GenSigSendType'] 467 | else: 468 | definition = attribute_definition.AttributeDefinition( 469 | 'GenSigSendType', 470 | default_value=0, 471 | kind='SG_', 472 | type_name='ENUM', 473 | choices={ 474 | 0: 'Cyclic', 475 | 1: 'OnWrite', 476 | 2: 'OnWriteWithRepetition', 477 | 3: 'OnChange', 478 | 4: 'OnChangeWithRepetition', 479 | 5: 'IfActive', 480 | 6: 'IfActiveWithRepetition', 481 | 7: 'NoSigSendType' 482 | } 483 | ) 484 | 485 | choices = {v: k for k, v in definition.choices.items()} 486 | 487 | self.dbc.attributes['GenSigSendType'] = attribute.Attribute(choices[value], definition) 488 | 489 | @property 490 | def gen_sig_inactive_value(self): 491 | """ 492 | Defines the inactive value of a signal. 493 | 494 | This value is only used for signal send type IfActive. If the signal 495 | value is unequal to GenSigInactiveValue the message the signal is placed 496 | on will be transmitted periodically with a periodicity of GenMsgCycleTimeFast. 497 | The signals inactive value is given as a signals raw value in this attribute. 498 | """ 499 | return self._get_attribute('GenSigInactiveValue') 500 | 501 | @gen_sig_inactive_value.setter 502 | def gen_sig_inactive_value(self, value): 503 | self._set_int_attribute('GenSigInactiveValue', 0, 2147483647, value) 504 | 505 | @property 506 | def gen_sig_start_value(self): 507 | """ 508 | Defines the start or initial value of the signal. 509 | 510 | This value is send after system start-up until the application 511 | sets the signal value the first time. The signals start value is 512 | given as a signals raw value in this attribute. 513 | """ 514 | return self._get_attribute('GenSigStartValue') 515 | 516 | @gen_sig_start_value.setter 517 | def gen_sig_start_value(self, value): 518 | self._set_int_attribute('GenSigStartValue', 0, 2147483647, value) 519 | 520 | @property 521 | def gen_sig_timeout_time(self): 522 | """ 523 | Defines the time of the signal receive timeout. 524 | 525 | If the message of the signal isn't received for this time interval a 526 | receive timeout will occur. The action performed after the timeout 527 | depends on the interaction layer used. The suffix <_ECU> gives the 528 | name of the receiving ECU if the attribute is defined for signals 529 | instead of Node-mapped Tx-Signal relations. 530 | """ 531 | return self._get_attribute('GenSigTimeoutTime') 532 | 533 | @gen_sig_timeout_time.setter 534 | def gen_sig_timeout_time(self, value): 535 | self._set_int_attribute('GenSigTimeoutTime', 0, 2147483647, value) 536 | 537 | @property 538 | def gen_sig_timeout_msg(self): 539 | """ 540 | Defines the ID of the message the signal is supervised by. 541 | 542 | If the message with the given ID is received by the receiver node, no 543 | signal timeout will occur. The timeout itself is defined in attribute 544 | GenSigTimeout-Time<_ECU>. The suffix <_ECU> gives the name of the 545 | receiving ECU if the attribute is defined for signals instead of 546 | Node-mapped Tx-Signal relations. 547 | """ 548 | return self._get_attribute('GenSigTimeoutMsg') 549 | 550 | @gen_sig_timeout_msg.setter 551 | def gen_sig_timeout_msg(self, value): 552 | self._set_hex_attribute('GenSigTimeoutMsg', 0x0, 0x7FF, value) 553 | 554 | @property 555 | def nwm_wakeup_allowed(self): 556 | """ 557 | This attribute is set to No for signals that have no effect on the NM. 558 | """ 559 | return bool(self._get_attribute('NWM-WakeupAllowed')) 560 | 561 | @nwm_wakeup_allowed.setter 562 | def nwm_wakeup_allowed(self, value): 563 | self._set_yes_no_attribute('NWM-WakeupAllowed', value) 564 | 565 | @property 566 | def sig_type(self): 567 | """ 568 | The valid value range and the interpretation of the signal 569 | value is specified with the attribute SigType. 570 | 571 | By using SigType it is possible to display only valid signal values in the Graphics Window. 572 | If the attribute is not defined the entire value range is valid. 573 | 574 | Valid Values: 575 | Default: normal signal, no protocol-specific interpretation 576 | Range: Signals with the attribute value Range are interpreted according to SAE J1939-71. 577 | The available value range is restricted due to coding additional information. The 578 | attribute value can only be used for signals of value type Unsigned. 579 | 580 | 1 Byte 2 Byte 4 Byte 581 | valid signal 0..FAh 0..FAFFh 0..FAFFFFFFh 582 | parameter specific FBh FB00h..FBFFh FB000000h..FBFFFFFFh 583 | reserved FCh..FDh FC00h..FDFFh FC000000h..FDFFFFFFh 584 | error FEh FE00h..FEFFh FE000000h..FEFFFFFFh 585 | not available FFh FF00h..FFFFh FF000000h..FFFFFFFFh 586 | 587 | 588 | RangeSigned: Signals with the attribute value RangeSigned are interpreted according to 589 | NMEA2000® appendix B4. The available value range is restricted due to coding additional 590 | information. The attribute value can only be used for signals of value type Signed. 591 | 592 | 1 Byte 2 Byte 4 Byte 593 | valid signal 0..7Ch, 80h..FFh 0..7FFCh, 8000h..FFFFh 0..7FFFFFFCh, 80000000h..FFFFFFFFh 594 | parameter specific 7Dh 7FFDh 7FFFFFFDh 595 | error 7Eh 7FFEh 7FFFFFFEh 596 | not available 7Fh 7FFFh 7FFFFFFFh 597 | 598 | 599 | 600 | ASCII: A signal of this type contains a string of fix length. 601 | The length of the string is defined by the size of the signal. 602 | 603 | 1 Byte 604 | valid signal 01h..FEh 605 | error 00h 606 | not available FFh 607 | 608 | 609 | Discrete: 610 | 611 | 2 Bit 612 | not active 0 613 | active 1 614 | error 2 615 | not available 3 616 | 617 | Control: 618 | 619 | 2 Bit 620 | deactivate 0 621 | activate 1 622 | reserved 2 623 | do not change 3 624 | 625 | 626 | 627 | ReferencePGN: The signal contains a PGN, i.E. the RQST (PGN EA00h) Parameter Group contains 628 | a signal with the requested PGN. The Trace Window and Data Window shows the name of the 629 | Parameter Group from the database. 630 | DTC: The signal contains a diagnostic trouble code (32 bit, inclusive SPN, FMI and OC). The 631 | Trace Window and Data Window show the name of the signal depending on the SPN. The 632 | Conversion Method Version 4 or Version 1, depending on the Conversion Method Bit, is used. 633 | StringDelimiter: String of variable length which ends with a delimiter ("*" or "2Ah"). The length 634 | of the signal in the database must be set to 8 bit (minimum length of a string signal). 635 | StringLength: String of variable length which contains the number of characters in the first or 636 | first two bytes (Intel format). The signal size in the database must be set to 8 bit for 1 637 | byte length or 16 bit for 2 byte length (minimum length of a string signal). 638 | StringLengthCtrl: String of variable length which contains the signal length in bytes (including 639 | the size and control bytes) in the first or first two bytes (Intel format) and a control byte 640 | (0=Unicode (UTF16), 1=ASCII) after the length. The signal size in the database must be set to 641 | 16 bit for 1 byte length or 24 bit for 2 byte length (minimum length of a string signal). 642 | MessageCounter: 643 | MessageChecksum: 644 | """ 645 | if 'SigType' in self.dbc.attributes: 646 | value = self.dbc.attributes['SigType'].value 647 | return self.dbc.attributes['SigType'].definition.choices[value] 648 | 649 | @sig_type.setter 650 | def sig_type(self, value): 651 | if 'SigType' in self.dbc.attributes: 652 | self.dbc.attributes['SigType'].value = value 653 | else: 654 | 655 | if 'SigType' in self.dbc.attribute_definitions: 656 | definition = self.dbc.attribute_definitions['SigType'] 657 | else: 658 | 659 | definition = attribute_definition.AttributeDefinition( 660 | 'SigType', 661 | default_value=0, 662 | kind='SG_', 663 | type_name='ENUM', 664 | choices={ 665 | 0: 'Default', 666 | 1: 'Range', 667 | 2: 'RangeSigned', 668 | 3: 'ASCII', 669 | 4: 'Discrete', 670 | 5: 'Control', 671 | 6: 'ReferencePGN', 672 | 7: 'DTC', 673 | 8: 'StringDelimiter', 674 | 9: 'StringLength', 675 | 10: 'StringLengthCtrl', 676 | 11: 'MessageCounter', 677 | 12: 'MessageChecksum' 678 | } 679 | ) 680 | 681 | choices = {v: k for k, v in definition.choices.items()} 682 | 683 | self.dbc.attributes['SigType'] = attribute.Attribute(choices[value], definition) 684 | 685 | @property 686 | def spn(self): 687 | """ 688 | With the attribute SPN, the Suspect Parameter Number is defined 689 | 690 | The SPN is specified in the J1939 specification. This attribute is used, 691 | for example, by the J1939 DTC Monitor. 692 | """ 693 | return self._get_attribute('SPN') 694 | 695 | @spn.setter 696 | def spn(self, value): 697 | self._set_int_attribute('SPN', 0, 524287, value) 698 | 699 | @property 700 | def gen_sig_alt_setting(self): 701 | return self._get_attribute('GenSigAltSetting') 702 | 703 | @gen_sig_alt_setting.setter 704 | def gen_sig_alt_setting(self, value): 705 | self._set_str_attribute('GenSigAltSetting', value) 706 | 707 | @property 708 | def gen_sig_assign_setting(self): 709 | return self._get_attribute('GenSigAssignSetting') 710 | 711 | @gen_sig_assign_setting.setter 712 | def gen_sig_assign_setting(self, value): 713 | self._set_str_attribute('GenSigAssignSetting', value) 714 | 715 | @property 716 | def gen_sig_conditional_send(self): 717 | return self._get_attribute('GenSigConditionalSend') 718 | 719 | @gen_sig_conditional_send.setter 720 | def gen_sig_conditional_send(self, value): 721 | self._set_str_attribute('GenSigConditionalSend', value) 722 | 723 | @property 724 | def gen_sig_ev_name(self): 725 | return self._get_attribute('GenSigEVName') 726 | 727 | @gen_sig_ev_name.setter 728 | def gen_sig_ev_name(self, value): 729 | self._set_str_attribute('GenSigEVName', value) 730 | 731 | @property 732 | def gen_sig_post_if_setting(self): 733 | return self._get_attribute('GenSigPostIfSetting') 734 | 735 | @gen_sig_post_if_setting.setter 736 | def gen_sig_post_if_setting(self, value): 737 | self._set_str_attribute('GenSigPostIfSetting', value) 738 | 739 | @property 740 | def gen_sig_post_setting(self): 741 | return self._get_attribute('GenSigPostSetting') 742 | 743 | @gen_sig_post_setting.setter 744 | def gen_sig_post_setting(self, value): 745 | self._set_str_attribute('GenSigPostSetting', value) 746 | 747 | @property 748 | def gen_sig_pre_if_setting(self): 749 | return self._get_attribute('GenSigPreIfSetting') 750 | 751 | @gen_sig_pre_if_setting.setter 752 | def gen_sig_pre_if_setting(self, value): 753 | self._set_str_attribute('GenSigPreIfSetting', value) 754 | 755 | @property 756 | def gen_sig_pre_setting(self): 757 | return self._get_attribute('GenSigPreSetting') 758 | 759 | @gen_sig_pre_setting.setter 760 | def gen_sig_pre_setting(self, value): 761 | self._set_str_attribute('GenSigPreSetting', value) 762 | 763 | @property 764 | def gen_sig_receive_setting(self): 765 | return self._get_attribute('GenSigReceiveSetting') 766 | 767 | @gen_sig_receive_setting.setter 768 | def gen_sig_receive_setting(self, value): 769 | self._set_str_attribute('GenSigReceiveSetting', value) 770 | 771 | @property 772 | def gen_sig_auto_gen_dsp(self): 773 | return bool(self._get_attribute('GenSigAutoGenDsp')) 774 | 775 | @gen_sig_auto_gen_dsp.setter 776 | def gen_sig_auto_gen_dsp(self, value): 777 | self._set_yes_no_attribute('GenSigAutoGenDsp', value) 778 | 779 | @property 780 | def gen_sig_auto_gen_snd(self): 781 | return bool(self._get_attribute('GenSigAutoGenSnd')) 782 | 783 | @gen_sig_auto_gen_snd.setter 784 | def gen_sig_auto_gen_snd(self, value): 785 | self._set_yes_no_attribute('GenSigAutoGenSnd', value) 786 | 787 | @property 788 | def gen_sig_env_var_type(self): 789 | if 'GenSigEnvVarType' in self.dbc.attributes: 790 | value = self.dbc.attributes['GenSigEnvVarType'].value 791 | return self.dbc.attributes['GenSigEnvVarType'].definition.choices[value] 792 | 793 | @gen_sig_env_var_type.setter 794 | def gen_sig_env_var_type(self, value): 795 | if 'GenSigEnvVarType' in self.dbc.attributes: 796 | self.dbc.attributes['GenSigEnvVarType'].value = value 797 | else: 798 | 799 | if 'GenSigEnvVarType' in self.dbc.attribute_definitions: 800 | definition = self.dbc.attribute_definitions['GenSigEnvVarType'] 801 | else: 802 | 803 | definition = attribute_definition.AttributeDefinition( 804 | 'GenSigEnvVarType', 805 | default_value=2, 806 | kind='SG_', 807 | type_name='ENUM', 808 | choices={ 809 | 0: 'int', 810 | 1: 'float', 811 | 2: 'undef' 812 | } 813 | ) 814 | 815 | choices = {v: k for k, v in definition.choices.items()} 816 | 817 | self.dbc.attributes['GenSigEnvVarType'] = attribute.Attribute(choices[value], definition) 818 | 819 | def __str__(self): 820 | if self.is_multiplexer: 821 | mux = ' M' 822 | elif self.multiplexer.is_ok: 823 | mux = ' m{}'.format(self.multiplexer[0]) 824 | else: 825 | mux = '' 826 | 827 | if self.receivers: 828 | receivers = ' ' + ','.join(node.name for node in self.receivers) 829 | else: 830 | receivers = 'Vector__XXX' 831 | 832 | fmt = ( 833 | ' SG_ {name}{mux} : {start}|{length}@{byte_order}{sign}' 834 | ' ({scale},{offset})' 835 | ' [{minimum}|{maximum}] "{unit}" {receivers}' 836 | ) 837 | 838 | res = fmt.format( 839 | name=self.name, 840 | mux=mux, 841 | start=self.start, 842 | length=self.length, 843 | receivers=receivers, 844 | byte_order=(0 if self.byte_order == 'big_endian' else 1), 845 | sign='-' if self.is_signed else '+', 846 | scale=self.scale, 847 | offset=self.offset, 848 | minimum=0 if self.minimum is None else self.minimum, 849 | maximum=0 if self.maximum is None else self.maximum, 850 | unit='' if self.unit is None else self.unit 851 | ) 852 | return res 853 | -------------------------------------------------------------------------------- /vector_dbc/signal_group.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | class SignalGroup(object): 25 | """ 26 | A CAN signal group. Signal groups are used to define a group of 27 | signals within a message, e.g. to define that the signals of a 28 | group have to be updated in common. 29 | """ 30 | 31 | def __init__(self, parent, name, repetitions=1, signal_names=None): 32 | self._name = name 33 | self._repetitions = repetitions 34 | self._signal_names = signal_names if signal_names else [] 35 | self._parent = parent 36 | 37 | @property 38 | def message(self): 39 | return self._parent 40 | 41 | @property 42 | def name(self): 43 | """The signal group name as a string.""" 44 | return self._name 45 | 46 | @name.setter 47 | def name(self, value): 48 | self._name = value 49 | 50 | @property 51 | def repetitions(self): 52 | """The signal group repetitions.""" 53 | return self._repetitions 54 | 55 | @repetitions.setter 56 | def repetitions(self, value): 57 | self._repetitions = value 58 | 59 | @property 60 | def signals(self): 61 | """The signals in this group""" 62 | return [signal for signal in self._parent.signals if signal.name in self._signal_names] 63 | 64 | @signals.setter 65 | def signals(self, value): 66 | sig_names = [] 67 | 68 | for signal in value: 69 | if signal.message != self._parent: 70 | raise ValueError('signal must be mapped to the message of this signal group') 71 | 72 | if signal.name not in sig_names: 73 | sig_names += [signal.name] 74 | 75 | self._signal_names = sig_names[:] 76 | 77 | def __str__(self): 78 | all_sig_names = list(map(lambda sig: sig.name, self._parent.signals)) 79 | self._signal_names = list(filter( 80 | lambda sig_name: sig_name in all_sig_names, self._signal_names 81 | )) 82 | 83 | return 'SIG_GROUP_ {frame_id} {signal_group_name} {repetitions} : {signal_names};'.format( 84 | frame_id=self._parent.dbc_frame_id, 85 | signal_group_name=self.name, 86 | repetitions=self.repetitions, 87 | signal_names=' '.join(self._signal_names) 88 | ) 89 | 90 | -------------------------------------------------------------------------------- /vector_dbc/signal_value_type.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kdschlosser/vector_dbc/653a60509ad233315f2332cc614239d9ee8f1d22/vector_dbc/signal_value_type.py -------------------------------------------------------------------------------- /vector_dbc/unit_conversion.py: -------------------------------------------------------------------------------- 1 | 2 | from decimal import Decimal 3 | 4 | 5 | # --- Force 6 | def kpa_to_psi(value): 7 | """Kilopascal to Pound-force per square inch""" 8 | return float(Decimal(str(value)) * Decimal('0.145038')) 9 | 10 | 11 | def kpa_to_bar(value): 12 | """Kilopascal to Bar""" 13 | return float(Decimal(str(value)) * Decimal('0.01')) 14 | 15 | 16 | def kpa_to_pa(value): 17 | """Kilopascal to Pascal""" 18 | return float(Decimal(str(value)) * Decimal('1000.0')) 19 | 20 | 21 | def bar_to_psi(value): 22 | """Bar to Pound-force per square inch""" 23 | return float(Decimal(str(value)) * Decimal('14.5038')) 24 | 25 | 26 | def bar_to_kpa(value): 27 | """Bar to Kilopascal""" 28 | return float(Decimal(str(value)) * Decimal('100.0')) 29 | 30 | 31 | def bar_to_pa(value): 32 | """Bar to Pascal""" 33 | return float(Decimal(str(value)) * Decimal('100000.0')) 34 | 35 | 36 | def psi_to_kpa(value): 37 | """Pound-force per square inch to Kilopascal""" 38 | return float(Decimal(str(value)) * Decimal('6.89476')) 39 | 40 | 41 | def psi_to_bar(value): 42 | """Pound-force per square inch to Bar""" 43 | return float(Decimal(str(value)) * Decimal('0.0689476')) 44 | 45 | 46 | def psi_to_pa(value): 47 | """Pound-force per square inch to Pascal""" 48 | return float(Decimal(str(value)) * Decimal('6894.76')) 49 | 50 | 51 | def pa_to_psi(value): 52 | """Pascal to Pound-force per square inch""" 53 | return float(Decimal(str(value)) * Decimal('0.000145038')) 54 | 55 | 56 | def pa_to_kpa(value): 57 | """Pascal to Kilopascal""" 58 | return float(Decimal(str(value)) * Decimal('0.001')) 59 | 60 | 61 | def pa_to_bar(value): 62 | """Pascal to Bar""" 63 | return float(Decimal(str(value)) * Decimal('1e-5')) 64 | 65 | 66 | # --- Speed 67 | def kph_to_mph(value): 68 | """Kilometer per hour to Mile per hour""" 69 | return float(Decimal(str(value)) * Decimal('0.621371')) 70 | 71 | 72 | def kph_to_ftsec(value): 73 | """Kilometer per hour to Foot per second""" 74 | return float(Decimal(str(value)) * Decimal('0.911344')) 75 | 76 | 77 | def kph_to_msec(value): 78 | """Kilometer per hour to Meter per second""" 79 | return float(Decimal(str(value)) * Decimal('0.277778')) 80 | 81 | 82 | def mph_to_kph(value): 83 | """Mile per hour to Kilometer per hour""" 84 | return float(Decimal(str(value)) * Decimal('1.60934')) 85 | 86 | 87 | def mph_to_ftsec(value): 88 | """Mile per hour to Foot per second""" 89 | return float(Decimal(str(value)) * Decimal('1.46667')) 90 | 91 | 92 | def mph_to_msec(value): 93 | """Mile per hour to Meter per second""" 94 | return float(Decimal(str(value)) * Decimal('0.44704')) 95 | 96 | 97 | def ftsec_to_mph(value): 98 | """Foot per second to Mile per hour""" 99 | return float(Decimal(str(value)) * Decimal('0.681818')) 100 | 101 | 102 | def ftsec_to_kph(value): 103 | """Foot per second to Kilometer per hour""" 104 | return float(Decimal(str(value)) * Decimal('1.09728')) 105 | 106 | 107 | def ftsec_to_msec(value): 108 | """Foot per second to Meter per second""" 109 | return float(Decimal(str(value)) * Decimal('0.3048')) 110 | 111 | 112 | def msec_to_kph(value): 113 | """Meter per second to Kilometer per hour""" 114 | return float(Decimal(str(value)) * Decimal('3.6')) 115 | 116 | 117 | def msec_to_mph(value): 118 | """Meter per second to Mile per hour""" 119 | return float(Decimal(str(value)) * Decimal('2.23694')) 120 | 121 | 122 | def msec_to_ftsec(value): 123 | """Meter per second to Foot per second""" 124 | return float(Decimal(str(value)) * Decimal('3.28084')) 125 | 126 | 127 | # --- Temperature 128 | def c_to_f(value): 129 | """Celcius to Farenheit""" 130 | return float((Decimal(str(value)) * Decimal('9.0') / Decimal('5.0')) + Decimal('32.0')) 131 | 132 | 133 | def f_to_c(value): 134 | """Farenheit to Celcius""" 135 | return float((Decimal(str(value)) - Decimal('32.0')) * Decimal('5.0') / Decimal('9.0')) 136 | 137 | 138 | # --- Volume 139 | def lh_to_gh(value): 140 | """Liter per hour to Gallon per hour""" 141 | return float(Decimal(str(value)) * Decimal('0.26')) 142 | 143 | 144 | def gh_to_lh(value): 145 | """Gallon per hour to Liter per hour""" 146 | return float(Decimal(str(value)) * Decimal('3.78541178')) 147 | 148 | 149 | # --- Weight 150 | def gsec_to_lbm(value): 151 | """Gram per second to Pound a minute""" 152 | return float(Decimal(str(value)) * Decimal('0.132277')) 153 | 154 | 155 | def lbm_to_gsec(value): 156 | """Pound a minute to Gram per second""" 157 | return float(Decimal(str(value)) * Decimal('7.5599')) 158 | 159 | 160 | # Volume & Weight 161 | def gsec_to_cfm(value): 162 | """Gram per second to Cubic foot a minute""" 163 | return float((Decimal(str(value)) * Decimal('4') * Decimal('60')) / Decimal('29.92')) 164 | 165 | 166 | def cfm_to_gsec(value): 167 | """Cubic foot a minute to Gram per second""" 168 | return float(((Decimal(str(value)) * Decimal('29.92')) / Decimal('60.0')) / Decimal('4.0')) 169 | 170 | 171 | def cfm_to_lbm(value): 172 | """Cubic foot a minute to Pound a minute""" 173 | return gsec_to_lbm(cfm_to_gsec(value)) 174 | 175 | 176 | def lbm_to_cfm(value): 177 | """Pound a minute to Cubic foot a minute""" 178 | return gsec_to_cfm(lbm_to_gsec(value)) 179 | 180 | 181 | # --- Distance 182 | def km_to_mi(value): 183 | """Kilometer to Mile""" 184 | return float(Decimal(str(value)) * Decimal('0.621371')) 185 | 186 | 187 | def mi_to_km(value): 188 | """Mile to Kilometer""" 189 | return float(Decimal(str(value)) * Decimal('1.60934')) 190 | -------------------------------------------------------------------------------- /vector_dbc/utils.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2021 Kevin Schlosser 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import binascii 24 | from decimal import Decimal 25 | from collections import namedtuple 26 | 27 | try: 28 | import bitstruct.c as bitstruct 29 | except ImportError: 30 | import bitstruct 31 | 32 | 33 | Formats = namedtuple( 34 | 'Formats', 35 | ['big_endian', 'little_endian', 'padding_mask'] 36 | ) 37 | 38 | 39 | def format_or(items): 40 | items = [str(item) for item in items] 41 | 42 | if len(items) == 1: 43 | return items[0] 44 | else: 45 | return '{} or {}'.format( 46 | ', '.join(items[:-1]), items[-1]) 47 | 48 | 49 | def format_and(items): 50 | items = [str(item) for item in items] 51 | 52 | if len(items) == 1: 53 | return items[0] 54 | else: 55 | return '{} and {}'.format( 56 | ', '.join(items[:-1]), items[-1]) 57 | 58 | 59 | def start_bit(data): 60 | if data.byte_order == 'big_endian': 61 | return 8 * (data.start // 8) + (7 - (data.start % 8)) 62 | else: 63 | return data.start 64 | 65 | 66 | def _encode_field(field, data, scaling): 67 | value = data[field.name] 68 | 69 | if isinstance(value, str): 70 | return field.choice_string_to_number(value) 71 | elif scaling: 72 | value = (Decimal(value) - Decimal(field.offset)) / Decimal(field.scale) 73 | 74 | if field.is_float: 75 | return float(value) 76 | else: 77 | return int(value.to_integral()) 78 | else: 79 | return value 80 | 81 | 82 | def _decode_field(field, value, decode_choices, scaling): 83 | if decode_choices: 84 | try: 85 | return field.choices[value] 86 | except (KeyError, TypeError): 87 | pass 88 | 89 | if scaling: 90 | return field.scale * value + field.offset 91 | else: 92 | return value 93 | 94 | 95 | def encode_data(data, fields, formats, scaling): 96 | if len(fields) == 0: 97 | return 0 98 | 99 | unpacked = { 100 | field.name: _encode_field(field, data, scaling) 101 | for field in fields 102 | } 103 | big_packed = formats.big_endian.pack(unpacked) 104 | little_packed = formats.little_endian.pack(unpacked)[::-1] 105 | packed_union = int(binascii.hexlify(big_packed), 16) 106 | packed_union |= int(binascii.hexlify(little_packed), 16) 107 | 108 | return packed_union 109 | 110 | 111 | def decode_data(data, fields, formats, decode_choices, scaling): 112 | unpacked = formats.big_endian.unpack(bytes(data)) 113 | unpacked.update(formats.little_endian.unpack(bytes(data[::-1]))) 114 | 115 | return { 116 | field.name: _decode_field( 117 | field, unpacked[field.name], 118 | decode_choices, scaling) 119 | for field in fields 120 | } 121 | 122 | 123 | def create_encode_decode_formats(datas, number_of_bytes): 124 | format_length = (8 * number_of_bytes) 125 | 126 | def get_format_string_type(data): 127 | if data.is_float: 128 | return 'f' 129 | elif data.is_signed: 130 | return 's' 131 | else: 132 | return 'u' 133 | 134 | def padding_item(length): 135 | fomt = 'p{}'.format(length) 136 | pad_mask = '1' * length 137 | 138 | return fomt, pad_mask, None 139 | 140 | def data_item(data): 141 | fomt = '{}{}'.format( 142 | get_format_string_type(data), 143 | data.length 144 | ) 145 | pad_mask = '0' * data.length 146 | 147 | return fomt, pad_mask, data.name 148 | 149 | def fmt(items): 150 | return ''.join(item[0] for item in items) 151 | 152 | def names(items): 153 | return [item[2] for item in items if item[2] is not None] 154 | 155 | def padding_mask(items): 156 | try: 157 | return int(''.join(item[1] for item in items), 2) 158 | except ValueError: 159 | return 0 160 | 161 | def create_big(): 162 | items = [] 163 | start = 0 164 | 165 | for data in datas: 166 | if data.byte_order == 'little_endian': 167 | continue 168 | 169 | padding_length = (start_bit(data) - start) 170 | 171 | if padding_length > 0: 172 | items.append(padding_item(padding_length)) 173 | 174 | items.append(data_item(data)) 175 | start = (start_bit(data) + data.length) 176 | 177 | if start < format_length: 178 | length = format_length - start 179 | items.append(padding_item(length)) 180 | 181 | return fmt(items), padding_mask(items), names(items) 182 | 183 | def create_little(): 184 | items = [] 185 | end = format_length 186 | 187 | for data in datas[::-1]: 188 | if data.byte_order == 'big_endian': 189 | continue 190 | 191 | padding_length = end - (data.start + data.length) 192 | 193 | if padding_length > 0: 194 | items.append(padding_item(padding_length)) 195 | 196 | items.append(data_item(data)) 197 | end = data.start 198 | 199 | if end > 0: 200 | items.append(padding_item(end)) 201 | 202 | value = padding_mask(items) 203 | 204 | if format_length > 0: 205 | length = len(''.join([item[1] for item in items])) 206 | value = bitstruct.pack('u{}'.format(length), value) 207 | value = int(binascii.hexlify(value[::-1]), 16) 208 | 209 | return fmt(items), value, names(items) 210 | 211 | big_fmt, big_padding_mask, big_names = create_big() 212 | little_fmt, little_padding_mask, little_names = create_little() 213 | 214 | big_compiled = bitstruct.compile(big_fmt, big_names) 215 | little_compiled = bitstruct.compile(little_fmt, little_names) 216 | 217 | return Formats( 218 | big_compiled, little_compiled, 219 | big_padding_mask & little_padding_mask) 220 | --------------------------------------------------------------------------------