├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .flake8 ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── build.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── codecov.yml ├── docs ├── _config.yml ├── commandline.md ├── diagnostics.md ├── frames.md ├── index.md ├── parser.md ├── schedules.md └── signals.md ├── examples ├── communication.py ├── diagnostics.py ├── ldf2json.py ├── lin22.ldf └── read_comments.py ├── ldfparser ├── __init__.py ├── cli.py ├── diagnostics.py ├── encoding.py ├── frame.py ├── grammar.py ├── grammars │ ├── __init__.py │ └── ldf.lark ├── ldf.py ├── lin.py ├── node.py ├── parser.py ├── save.py ├── schedule.py ├── signal.py └── templates │ ├── __init__.py │ └── ldf.jinja2 ├── pytest.ini ├── schemas └── ldf.json ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── ldf ├── iso17987.ldf ├── j2602_1.ldf ├── j2602_1_no_values.ldf ├── ldf_with_sporadic_frames.ldf ├── lin13.ldf ├── lin20.ldf ├── lin21.ldf ├── lin22.ldf ├── lin_diagnostics.ldf ├── lin_encoders.ldf ├── lin_schedules.ldf └── no_signal_subscribers.ldf ├── snapshot_data.py ├── test_cli.py ├── test_comment.py ├── test_diagnostics.py ├── test_encoding.py ├── test_frame.py ├── test_json.py ├── test_lin.py ├── test_node.py ├── test_parser.py ├── test_performance.py ├── test_save.py ├── test_schedule.py ├── test_schema.py └── test_signal.py /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.194.0/containers/python-3/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Python version: 3, 3.9, 3.8, 3.7, 3.6 4 | ARG VARIANT="3.9" 5 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} 6 | 7 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 8 | ARG NODE_VERSION="none" 9 | RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 10 | 11 | # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. 12 | # COPY requirements.txt /tmp/pip-tmp/ 13 | # RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ 14 | # && rm -rf /tmp/pip-tmp 15 | 16 | # [Optional] Uncomment this section to install additional OS packages. 17 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 18 | # && apt-get -y install --no-install-recommends 19 | 20 | # [Optional] Uncomment this line to install global node packages. 21 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.194.0/containers/python-3 3 | { 4 | "name": "Python 3", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "context": "..", 8 | "args": { 9 | // Update 'VARIANT' to pick a Python version: 3, 3.6, 3.7, 3.8, 3.9 10 | "VARIANT": "3.6", 11 | // Options 12 | "NODE_VERSION": "none" 13 | } 14 | }, 15 | 16 | // Set *default* container specific settings.json values on container create. 17 | "settings": { 18 | "python.pythonPath": "/usr/local/bin/python", 19 | "python.languageServer": "Pylance", 20 | "python.linting.enabled": true, 21 | "python.linting.pylintEnabled": true, 22 | "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", 23 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black", 24 | "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", 25 | "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", 26 | "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", 27 | "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", 28 | "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", 29 | "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", 30 | "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" 31 | }, 32 | 33 | // Add the IDs of extensions you want installed when the container is created. 34 | "extensions": [ 35 | "ms-python.python", 36 | "ms-python.vscode-pylance" 37 | ], 38 | 39 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 40 | // "forwardPorts": [], 41 | 42 | // Use 'postCreateCommand' to run commands after the container is created. 43 | "postCreateCommand": "pip3 install --user -r requirements.txt", 44 | 45 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 46 | "remoteUser": "vscode" 47 | } 48 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = W504,W191,E501,E302,E305 3 | exclude = 4 | .git 5 | __pycache__ 6 | .pytest_cache 7 | build 8 | dist 9 | max-complexity = 10 10 | per-file-ignores = 11 | # imported but unused 12 | __init__.py: F401 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | *.pdf filter=lfs diff=lfs merge=lfs -text 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Stacktrace/Code** 25 | If applicable, add stacktrace or code segments to help explain your problem. 26 | 27 | **Environment:** 28 | 29 | + OS: (e.g. Linux) 30 | + Python version: (e.g. 3.8.5) 31 | + ldfparser version: (e.g. 0.2.1) 32 | 33 | ```text 34 | Optionally include the output of 'pipdeptree --warn silence -p ldfparser' 35 | ``` 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Brief 2 | 3 | - Please provide brief information, what this pull request provides or fixes, possibly in a list format 4 | 5 | ### Checklist 6 | 7 | 10 | 11 | - [ ] Add relevant labels to the Pull Request 12 | - [ ] Review test results and code coverage 13 | - [ ] Review snapshot test results for deviations 14 | - [ ] Review code changes 15 | - [ ] Create relevant test scenarios 16 | - [ ] Update examples 17 | - [ ] Update JSON schema 18 | - [ ] Update documentation 19 | - [ ] Update examples in README 20 | - [ ] Update changelog 21 | - [ ] Update version number 22 | 23 | ## Resolves 24 | 25 | 28 | 29 | + Describe the bug or feature and link to relevant issues 30 | 31 | ## Evidence 32 | 33 | 37 | 38 | + Analyze how the change might impact existing code 39 | 40 | + Provide evidence that the feature is tested and covered properly 41 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v* 9 | pull_request: 10 | paths-ignore: 11 | - '*.md' 12 | - '**/*.md' 13 | - 'docs/**.*' 14 | 15 | jobs: 16 | build: 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | python-version: ['3.6', '3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] 21 | runs-on: ubuntu-latest 22 | container: 23 | image: python:${{ matrix.python-version }} 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v4 27 | 28 | - name: Install 29 | run: pip install -e .[dev] 30 | 31 | - name: Test & Coverage 32 | run: pytest -m 'unit or integration' --cov=ldfparser --cov-report xml 33 | 34 | - name: Run Examples 35 | if: always() 36 | run: | 37 | python ./examples/communication.py 38 | python ./examples/ldf2json.py 39 | python ./examples/read_comments.py 40 | 41 | - name: Package 42 | if: always() 43 | run: python setup.py sdist bdist_wheel 44 | 45 | - name: Upload coverage results 46 | uses: codecov/codecov-action@v3 47 | if: always() 48 | with: 49 | token: ${{ secrets.CODECOV_TOKEN }} 50 | directory: ./ 51 | flags: ${{ matrix.python-version }} 52 | name: Python-${{ matrix.python-version }} 53 | fail_ci_if_error: false 54 | 55 | flake8: 56 | runs-on: ubuntu-latest 57 | container: 58 | image: python:3.6 59 | steps: 60 | - name: Checkout 61 | uses: actions/checkout@v4 62 | 63 | - name: Setup flake8 annotations 64 | uses: rbialon/flake8-annotations@v1 65 | 66 | - name: Install 67 | run: pip install -e .[dev] 68 | 69 | - name: Lint using Flake8 70 | run: flake8 --count --show-source --statistics 71 | 72 | pylint: 73 | runs-on: ubuntu-latest 74 | container: 75 | image: python:3.6 76 | steps: 77 | - name: Checkout 78 | uses: actions/checkout@v4 79 | 80 | - name: Install 81 | run: pip install -e .[dev] 82 | 83 | - name: Lint using Pylint 84 | run: pylint ldfparser --fail-under=8.5 85 | 86 | pylint-tests: 87 | runs-on: ubuntu-latest 88 | container: 89 | image: python:3.6 90 | steps: 91 | - name: Checkout 92 | uses: actions/checkout@v4 93 | 94 | - name: Install 95 | run: pip install -e .[dev] 96 | 97 | - name: Lint using Pylint 98 | run: pylint tests --disable="C0114,C0116,R0201" --fail-under=8.0 99 | 100 | snapshot-test: 101 | if: github.event_name == 'pull_request' 102 | runs-on: ubuntu-latest 103 | container: 104 | image: python:3.6 105 | steps: 106 | - name: Checkout (current) 107 | uses: actions/checkout@v4 108 | with: 109 | path: current 110 | 111 | - name: Checkout (base) 112 | uses: actions/checkout@v3 113 | with: 114 | path: base 115 | ref: ${{ github.event.pull_request.base.ref }} 116 | 117 | - name: Setup 118 | run: pip install pytest 119 | 120 | - name: Install base 121 | working-directory: base 122 | run: pip install -e .[dev] 123 | 124 | - name: Generate snapshot data 125 | working-directory: base 126 | run: python tests/snapshot_data.py 127 | 128 | - name: Move snapshot data 129 | run: | 130 | mkdir current/tests/snapshot/ 131 | mv base/tests/snapshot/* current/tests/snapshot/ 132 | 133 | - name: Install 134 | working-directory: current 135 | run: pip install -e .[dev] 136 | 137 | - name: Snapshot test 138 | working-directory: current 139 | run: pytest -m 'snapshot' 140 | 141 | performance-test: 142 | if: github.event_name == 'pull_request' 143 | runs-on: ubuntu-latest 144 | container: 145 | image: python:3.6 146 | steps: 147 | - name: Checkout 148 | uses: actions/checkout@v4 149 | 150 | - name: Install 151 | run: pip install -e .[dev] 152 | 153 | - name: Run performance tests 154 | run: pytest -m 'performance' --benchmark-json output.json 155 | 156 | release: 157 | if: startsWith(github.ref, 'refs/tags/v') 158 | runs-on: ubuntu-latest 159 | container: 160 | image: python:3.6 161 | steps: 162 | - name: Checkout 163 | uses: actions/checkout@v4 164 | 165 | - name: Install 166 | run: pip install -e .[dev] 167 | 168 | - name: Package 169 | run: python setup.py sdist bdist_wheel 170 | 171 | - name: Publish to PyPi 172 | env: 173 | TWINE_USERNAME: __token__ 174 | TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} 175 | run: twine upload dist/* 176 | 177 | - name: Get version from tag 178 | id: tag_name 179 | run: echo "::set-output name=current_version::${GITHUB_REF#refs/tags/v}" 180 | 181 | - name: Read Changelog 182 | id: changelog 183 | uses: mindsers/changelog-reader-action@v2 184 | with: 185 | validation_level: warn 186 | version: ${{ steps.tag_name.outputs.current_version }} 187 | path: CHANGELOG.md 188 | 189 | - name: Create Release 190 | uses: softprops/action-gh-release@v1 191 | with: 192 | name: Release ${{ steps.changelog.outputs.version }} 193 | body: ${{ steps.changelog.outputs.changes }} 194 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | tests/snapshot/ 3 | tests/tmp 4 | 5 | # 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Benchmark results 60 | .benchmarks/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | db.sqlite3-journal 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | .pybuilder/ 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | # For a library or package, you might want to ignore these files since the code is 95 | # intended to run in multiple environments; otherwise, check them in: 96 | # .python-version 97 | 98 | # pipenv 99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 102 | # install all needed dependencies. 103 | #Pipfile.lock 104 | 105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 106 | __pypackages__/ 107 | 108 | # Celery stuff 109 | celerybeat-schedule 110 | celerybeat.pid 111 | 112 | # SageMath parsed files 113 | *.sage.py 114 | 115 | # Environments 116 | .env 117 | .venv 118 | env/ 119 | venv/ 120 | ENV/ 121 | env.bak/ 122 | venv.bak/ 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | 142 | # pytype static type analyzer 143 | .pytype/ 144 | 145 | # Cython debug symbols 146 | cython_debug/ 147 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement by opening an issue 64 | or contacting the repository owner privately. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 127 | at [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to LDF Parser 2 | 3 | The following is a set of guidelines for contributing to ldfparser. 4 | 5 | ## How you can contribute 6 | 7 | --- 8 | 9 | ### Reporting bugs 10 | 11 | Open a new issue using the [Bug report template](.github/ISSUE_TEMPLATE/bug_report.md) 12 | 13 | + Before submitting check whether or not this issue has been reported 14 | 15 | + Use a clear and descriptive title 16 | + Provide as much detail as you can regarding the issue 17 | 18 | ### Suggesting enhancements 19 | 20 | Open a new issue using the [Feature Request template](.github/ISSUE_TEMPLATE/feature_request.md) 21 | 22 | + Before submitting check whether or not this feature has been requested 23 | 24 | + Use a clear and descriptive title 25 | + Provide as much detail as you can regarding the feature 26 | + Describe the current behavior and propose an alternative 27 | + List all potential applications of the proposed feature 28 | 29 | ### Pull requests 30 | 31 | Please follow the process described in [Pull Request template](.github/PULL_REQUEST_TEMPLATE/pull_request_template.md). 32 | 33 | + Verify that status checks are passing. 34 | + Request a review and wait for a maintainer to provide feedback 35 | 36 | ## Styleguide 37 | 38 | --- 39 | 40 | ### Git commit messages 41 | 42 | + First line should be below 50 characters in length 43 | + Second line should be empty 44 | + Further lines should be below 72 characters in length 45 | 46 | ### Python 47 | 48 | Code inside source and test folders are linted using Flake8. 49 | 50 | + Prefer spaces over tabs 51 | + Avoid platform-dependent code 52 | 53 | ### Documentation 54 | 55 | + Non-inline documentation should be in Markdown format located in the `docs/` folder or in root 56 | 57 | + Inline documentation should follow [PEP 257](https://www.python.org/dev/peps/pep-0257/) 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2020 Balázs Eszes 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LDF Parser 2 | 3 | [![Workflow](https://github.com/c4deszes/ldfparser/workflows/CI/badge.svg?branch=master)](https://github.com/c4deszes/ldfparser/actions) 4 | [![Github Pages](https://img.shields.io/static/v1?style=flat&logo=github&label=gh-pages&color=green&message=deployed)](https://c4deszes.github.io/ldfparser/) 5 | [![PyPI version](https://badge.fury.io/py/ldfparser.svg)](https://pypi.org/project/ldfparser/) 6 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ldfparser.svg)](https://pypi.org/project/ldfparser/) 7 | [![codecov.io](https://codecov.io/github/c4deszes/ldfparser/coverage.svg?branch=master)](https://codecov.io/github/c4deszes/ldfparser?branch=master) 8 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/c4deszes/ldfparser.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/c4deszes/ldfparser/alerts/) 9 | [![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/c4deszes/ldfparser.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/c4deszes/ldfparser/context:python) 10 | ![GitHub last commit](https://img.shields.io/github/last-commit/c4deszes/ldfparser) 11 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 12 | 13 | > This tool is able parse LIN Description Files, retrieve signal names and frames from them, as well as encoding messages using frame definitions and decoding them. 14 | 15 | --- 16 | 17 | ## Disclaimers 18 | 19 | The library is still in a pre-release state, therefore features may break between minor versions. 20 | For this reason it's recommended that productive environments pin to the exact version of the 21 | library and do an integration test or review when updating the version. Breaking changes and how to 22 | migrate to the new version will be documented in the 23 | [changelog](https://github.com/c4deszes/ldfparser/blob/master/CHANGELOG.md) and on the 24 | [Github releases page](https://github.com/c4deszes/ldfparser/releases). 25 | 26 | The tool has been written according the LIN standards [1.3](docs/external/LIN_1.3.pdf), 27 | [2.0](docs/external/LIN_2.0.pdf), [2.1](docs/external/LIN_2.1.pdf) and [2.2A](docs/external/LIN_2.2A.pdf), 28 | but due to errors in the documentation there's no guarantee that the library will be able to parse your LDF. 29 | In such cases if possible first verify the LDF with a commercial tool such as Vector LDF Explorer or the 30 | tool that was used to create the LDF. If the LDF seems to be correct then open a new issue. 31 | I also recommend trying the LDF to JSON conversion mechanism, see if that succeeds. 32 | 33 | The LIN standard is now known as [ISO 17987](https://www.iso.org/standard/61222.html) which 34 | clears up some of the confusing parts in the 2.2A specification. Since this new standard is not 35 | freely available **this library won't support the modifications present in ISO 17987**. I don't 36 | think it's going to a huge problem because the LIN 2.2A released in 2010 has overall better adoption. 37 | 38 | The LDF usually contains sensitive information, if you need to open an issue related to the parser 39 | then try to provide either an anonymized version with signals and frames obfuscated or just the 40 | relevant segments in an example LDF when opening issues. 41 | 42 | --- 43 | 44 | ## Installation 45 | 46 | You can install this library from PyPI using pip. 47 | 48 | ```bash 49 | pip install ldfparser 50 | ``` 51 | 52 | --- 53 | 54 | ## Examples 55 | 56 | ```python 57 | import ldfparser 58 | import binascii 59 | 60 | # Load LDF 61 | ldf = ldfparser.parse_ldf(path = "network.ldf") 62 | frame = ldf.get_unconditional_frame('Frame_1') 63 | 64 | # Get baudrate from LDF 65 | print(ldf.get_baudrate()) 66 | 67 | # Encode signal values into frame 68 | message = frame.encode_raw({"Signal_1": 123, "Signal_2": 0}) 69 | print(binascii.hexlify(message)) 70 | >>> 0x7B00 71 | 72 | # Decode message into dictionary of signal names and values 73 | received = bytearray([0x7B, 0x00]) 74 | print(frame.decode(received)) 75 | >>> {"Signal_1": 123, "Signal_2": 0} 76 | 77 | # Encode signal values through converters 78 | message = frame.encode({"MotorRPM": 100, "FanState": "ON"}) 79 | print(binascii.hexlify(message)) 80 | >>> 0xFE01 81 | ``` 82 | 83 | More examples can be found in the [examples directory](./examples). 84 | 85 | --- 86 | 87 | ## Documentation 88 | 89 | Documentation is published to [Github Pages](https://c4deszes.github.io/ldfparser/). 90 | 91 | --- 92 | 93 | ## Features 94 | 95 | + Semantic validation of LDF files 96 | 97 | + Retrieve header information (version, baudrate) 98 | 99 | + Retrieve Signal and Frame information 100 | 101 | + Retrieve Signal encoding types and use them to convert values 102 | 103 | + Retrieve Node attributes 104 | 105 | + Retrieve schedule table information 106 | 107 | + Command Line Interface 108 | 109 | + Capturing comments 110 | 111 | + Encode and decode standard diagnostic frames 112 | 113 | + Saving LDF object as an `.ldf` file (experimental) 114 | 115 | ### Known issues / missing features 116 | 117 | + Certain parsing related errors are unintuitive 118 | 119 | + Checksum calculation for frames 120 | 121 | + Token information is not preserved 122 | 123 | --- 124 | 125 | ## Development 126 | 127 | Install the library locally by running `pip install -e .[dev]` 128 | 129 | [Pytest](https://pytest.org/) is used for testing, to execute all tests run `pytest -m 'not snapshot'` 130 | 131 | [Flake8](https://flake8.pycqa.org/en/latest/) is used for linting, run `flake8` to print out all linting errors. 132 | 133 | --- 134 | 135 | ## Contributors 136 | 137 | [@c4deszes](https://github.com/c4deszes) (Author) 138 | 139 | --- 140 | 141 | ## Credits 142 | 143 | Inspired by [uCAN-LIN LinUSBConverter](https://github.com/uCAN-LIN/LinUSBConverter), specifically the LDF parsing mechanism via [Lark](https://github.com/lark-parser/lark). Previously the library included most of the lark file, parsing code and examples, since 0.5.0 they've been completely rewritten to better accomodate the different LIN standards. 144 | 145 | --- 146 | 147 | ## License 148 | 149 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 150 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: 90% 6 | threshold: 20% 7 | base: auto 8 | if_not_found: failure 9 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /docs/commandline.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: LDF Parser - Command Line Interface 4 | --- 5 | 6 | ## Commons 7 | 8 | ### Information 9 | 10 | The `info` subcommand will print general information about the LDF, such as language 11 | version, baudrate, node and frame counts. When the `details` option is added it will 12 | print a list of node names, frames, etc. instead of counts. 13 | 14 | `ldfparser --ldf info [--details]` 15 | 16 | ### Exporting to JSON 17 | 18 | The `export` subcommand can be used to export the LDF as a JSON file to be used by 19 | other tools. When the `output` option is not specified it will print the contents to `stdout`. 20 | 21 | `ldfparser --ldf export [--output ]` 22 | 23 | --- 24 | 25 | ## Nodes 26 | 27 | The `node` subcommand can be used to access information about the LIN nodes in the LDF. 28 | 29 | ### List of nodes 30 | 31 | `ldfparser --ldf node --list` 32 | 33 | ### Node information per role 34 | 35 | These commands print information about LIN nodes. 36 | 37 | `ldfparser --ldf node --master` 38 | 39 | `ldfparser --ldf node --slave ` 40 | 41 | --- 42 | 43 | ## Frames 44 | 45 | The `frame` subcommand can be used to access information about the LIN frames in the LDF. 46 | 47 | ### List of frames 48 | 49 | `ldfparser --ldf frame --list` 50 | 51 | ### Frame information 52 | 53 | These commands print information about the LIN frames. 54 | 55 | `ldfparser --ldf frame --name ` 56 | 57 | `ldfparser --ldf frame --id ` 58 | 59 | --- 60 | 61 | ## Signals 62 | 63 | The `signal` subcommand can be used to access information about the LIN frames in the LDF. 64 | 65 | ### List of signals 66 | 67 | `ldfparser --ldf signal --list` 68 | 69 | ### Signal information 70 | 71 | `ldfparser --ldf signal --name ` 72 | -------------------------------------------------------------------------------- /docs/diagnostics.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: LDF Parser - Diagnostics 4 | --- 5 | 6 | ## Diagnostics 7 | 8 | ### Encoding diagnostic requests 9 | 10 | The `LinDiagnosticRequest` contains methods that allow encoding and decoding of the standard 11 | node configuration commands. 12 | 13 | ```python 14 | ldf = parse_ldf('network.ldf') 15 | 16 | ldf.master_request_frame.encode_assign_nad(inital_nad=0, 17 | supplier_id=0x7FFF, 18 | function_id=0xFFFF, 19 | new_nad=0x13) 20 | >>> b'\x00\x06\xB0\xFF\x7F\xFF\xFF\x13' 21 | ``` 22 | 23 | ### Decoding diagnostic responses 24 | 25 | ```python 26 | ldf = parse_ldf('network.ldf') 27 | 28 | ldf.slave_response_frame.decode_response(b'\x00\x01\xF0\xFF\xFF\xFF\xFF\xFF') 29 | >>> { 'NAD': 0x00,'PCI': 0x01, 'RSID': 0xF0, 30 | 'D1': 0xFF, 'D2': 0xFF, 'D3': 0xFF, 'D4': 0xFF, 'D5': 0xFF} 31 | ``` 32 | -------------------------------------------------------------------------------- /docs/frames.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: LDF Parser - Frames 4 | --- 5 | 6 | ## Using frames 7 | 8 | ### Retrieving unconditional frame information 9 | 10 | After parsing the LDF into objects the properties of frames defined in the LDF 11 | will be accessible. 12 | 13 | ```python 14 | ldf = parse_ldf('network.ldf') 15 | 16 | lsm_frame1 = ldf.get_unconditional_frame('LSM_Frm1') 17 | print(lsm_frame1.frame_id) 18 | >>> 2 19 | print(lsm_frame1.name) 20 | >>> 'LSM_Frm1' 21 | print(lsm_frame1.length) 22 | >>> 2 23 | print(lsm_frame1.publisher) 24 | >>> LinSlave(..) 25 | ``` 26 | 27 | --- 28 | 29 | ### Encoding frames 30 | 31 | Encoding is the process of the converting the provided LIN Signals into a 32 | complete frame. 33 | 34 | When encoding there are two options, you can encode raw values 35 | and pack them into a frame, or alternatively you can pass the logical values 36 | through the signal encoders before packing. 37 | 38 | ```python 39 | ldf = parse_ldf('network.ldf') 40 | 41 | lsm_frame1 = ldf.get_unconditional_frame('LSM_Frm1') 42 | encoded_frame = lsm_frame1.encode_raw( 43 | {'LeftIntLightsSwitch': 100} 44 | ) 45 | ``` 46 | 47 | When encoding through signal encoders you have the option to pass a list of 48 | value converters, otherwise it will use the default encoders assigned through 49 | signal representations. 50 | 51 | ```python 52 | ldf = parse_ldf('network.ldf') 53 | 54 | lsm_frame1 = ldf.get_unconditional_frame('LSM_Frm1') 55 | encoded_frame = lsm_frame1.encode( 56 | {'LeftIntLightsSwitch': 'Off'} 57 | ) 58 | ``` 59 | 60 | --- 61 | 62 | ### Decoding frames 63 | 64 | Decoding is the process of unpacking a LIN frame into the signal values. 65 | 66 | Similarly to encoding, you can decode frames into raw signal values or decode 67 | them through the signal encoders. 68 | 69 | ```python 70 | ldf = parse_ldf('network.ldf') 71 | 72 | lsm_frame1 = ldf.get_unconditional_frame('LSM_Frm1') 73 | decoded_frame = lsm_frame1.decode_raw(b'\x00') 74 | ``` 75 | 76 | Just like encoding you can also pass custom value converters and there's also 77 | the option to preserve the unit of physical values, in these cases instead of 78 | a floating point value a string will be returned. 79 | 80 | ```python 81 | ldf = parse_ldf('network.ldf') 82 | 83 | lsm_frame1 = ldf.get_unconditional_frame('LSM_Frm1') 84 | decoded_frame = lsm_frame1.decode(b'\x00', keep_unit=True) 85 | ``` 86 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: LDF Parser Documentation 4 | --- 5 | 6 | The `ldfparser` library allows extracting information out of LIN descriptor files 7 | that are used to describe automotive networks. 8 | 9 | ## Project status 10 | 11 | The library is in a *pre-release* state, some of the functionalities are effectively 12 | final such as the intermediate output of the parser. Interfaces, functions, variable 13 | names may change any time with the plan being to first deprecate the function in a 14 | release then remove it later. 15 | 16 | ## Setup 17 | 18 | Releases are automatically published to PyPI, so you can install it using pip. 19 | 20 | ```bash 21 | pip install ldfparser 22 | ``` 23 | 24 | Since the library is still in a pre-release state it's recommended that in 25 | production use cases you pin the version to a minor release in your requirements.txt 26 | 27 | ## Documentation 28 | 29 | [Parsing LDF files](parser.md) 30 | 31 | [Encoding and decoding frames](frames.md) 32 | 33 | [Using signals and encoders](signals.md) 34 | 35 | [Using the command line interface](commandline.md) 36 | 37 | [Using diagnostic frames](diagnostics.md) 38 | 39 | [Accessing schedule tables](schedules.md) 40 | 41 | ## License 42 | 43 | Distributed under the terms of 44 | [MIT license](https://opensource.org/licenses/MITs), 45 | ldfparser is free to use and modify. 46 | -------------------------------------------------------------------------------- /docs/parser.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: LDF Parser - Parsing 4 | --- 5 | 6 | ## About the parser 7 | 8 | ### Process 9 | 10 | The complete parsing procedure has 3 phases. 11 | 12 | 1. The LDF is passed through lark-parser where it's converted into a syntax tree. 13 | 14 | 2. The tree is transformed into a Python dictionary. 15 | 16 | 3. The dictionary is converted into Python objects. 17 | 18 | --- 19 | 20 | ## Using the parser 21 | 22 | ### Parsing into dictionary 23 | 24 | The library allows the conversion of LDF into a Python dictionary. 25 | The dictionary's layout can be interpreted through the LDFTransformer class or 26 | by exporting an LDF into JSON format. The field names and structure try 27 | to be very similar to the ones in the LDF specification. 28 | 29 | ```python 30 | ldf = ldfparser.parse_ldf_to_dict('network.ldf') 31 | 32 | print(ldf['speed']) 33 | >>> 19200 34 | print(ldf['nodes']['slaves']) 35 | >>> ['LSM', 'RSM'] 36 | ``` 37 | 38 | If you're just looking to convert into JSON so that some other tool can interpret 39 | it then have a look at `export` command in the [CLI](commandline.md). 40 | 41 | --- 42 | 43 | ### Parsing into LIN objects 44 | 45 | After converting the LDF into a dictionary the parser maps these values 46 | into Python objects that can be used for easier data access as well as traversal 47 | through the links between objects. 48 | 49 | ```python 50 | ldf = ldfparser.parse_ldf('network.ldf') 51 | print(ldf.get_baudrate()) 52 | >>> 19200 53 | for node in ldf.get_slaves(): 54 | print(node.name) 55 | >>> 'LSM' 56 | >>> 'RSM' 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/schedules.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: LDF Parser - Schedules 4 | --- 5 | 6 | ## Accessing schedules 7 | 8 | ### Retrieving schedule information 9 | 10 | After parsing the LDF into objects the schedule tables defined in the LDF will 11 | be accessible. 12 | 13 | ```python 14 | ldf = parse_ldf('network.ldf') 15 | 16 | configuration_schedule = ldf.get_schedule_table('Configuration_Schedule') 17 | print(configuration_schedule.name) 18 | >>> 'Configuration_Schedule' 19 | for entry in configuration_schedule.schedule: 20 | print(f"{type(entry).__name__} - {entry.delay * 1000} ms") 21 | >>> 'AssignNadEntry - 15 ms' 22 | >>> 'AssignFrameIdRangeEntry - 15 ms' 23 | >>> 'AssignFrameIdEntry - 15 ms' 24 | >>> 'AssignFrameIdEntry - 15 ms' 25 | >>> 'AssignFrameIdEntry - 15 ms' 26 | ``` 27 | 28 | The objects referenced in the table entries are also linked. 29 | 30 | ```python 31 | print(configuration_schedule.schedule[0].node.name) 32 | >>> 'LSM' 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/signals.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: LDF Parser - Signals 4 | --- 5 | 6 | ## Using signals 7 | 8 | ### Retrieving signal information 9 | 10 | After parsing the LDF into objects the properties of signals defined in the LDF 11 | will be accessible. 12 | 13 | ```python 14 | ldf = ldfparser.parse_ldf('network.ldf') 15 | 16 | light_switch_signal = ldf.get_signal('LeftIntLightsSwitch') 17 | print(light_switch_signal.name) 18 | >>> 'LeftIntLightsSwitch' 19 | print(light_switch_signal.width) 20 | >>> 8 21 | print(light_switch_signal.init_value) 22 | >>> 0 23 | ``` 24 | -------------------------------------------------------------------------------- /examples/communication.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import ldfparser 4 | 5 | class LinMaster: 6 | 7 | def send_frame(self, baudrate: int, frame_id: int, data: bytearray): 8 | # LIN Tool specific functionality 9 | pass 10 | 11 | if __name__ == "__main__": 12 | path = os.path.join(os.path.dirname(__file__), 'lin22.ldf') 13 | ldf = ldfparser.parse_ldf(path) 14 | lin_master = LinMaster() 15 | requestFrame = ldf.get_unconditional_frame('CEM_Frm1') 16 | requestData = requestFrame.encode({"InternalLightsRequest": 'on'}, ldf.converters) 17 | 18 | lin_master.send_frame(ldf.baudrate, requestFrame.frame_id, requestData) 19 | -------------------------------------------------------------------------------- /examples/diagnostics.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import ldfparser 4 | 5 | class LinMaster: 6 | 7 | def send_frame(self, baudrate: int, frame_id: int, data: bytearray): 8 | # LIN Tool specific functionality 9 | pass 10 | 11 | def request_frame(self, baudrate: int, frame_id: int) -> bytearray: 12 | # LIN Tool specific functionality 13 | return bytearray() 14 | 15 | if __name__ == "__main__": 16 | path = os.path.join(os.path.dirname(__file__), 'lin22.ldf') 17 | ldf = ldfparser.parse_ldf(path) 18 | lin_master = LinMaster() 19 | 20 | # Send Data dump 21 | requestData = ldf.master_request_frame.encode_data_dump(nad=0x01, data=[0x01, 0xFF, 0xFF, 0xFF, 0xFF]) 22 | lin_master.send_frame(ldf.baudrate, ldf.master_request_frame.frame_id, requestData) 23 | 24 | # Receive data dump response 25 | responseData = lin_master.request_frame(ldf.baudrate, ldf.slave_response_frame.frame_id) 26 | print(ldf.slave_response_frame.decode_response(responseData)) 27 | -------------------------------------------------------------------------------- /examples/ldf2json.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import ldfparser 4 | 5 | if __name__ == "__main__": 6 | path = os.path.join(os.path.dirname(__file__), 'lin22.ldf') 7 | ldf = ldfparser.parse_ldf_to_dict(path) 8 | print(json.dumps(ldf)) 9 | -------------------------------------------------------------------------------- /examples/lin22.ldf: -------------------------------------------------------------------------------- 1 | /*******************************************************/ 2 | /* This is the example LDF from LIN 2.2A specification */ 3 | /*******************************************************/ 4 | 5 | // Source: https://lin-cia.org/fileadmin/microsites/lin-cia.org/resources/documents/LIN_2.2A.pdf 6 | 7 | LIN_description_file; 8 | LIN_protocol_version = "2.2"; 9 | LIN_language_version = "2.2"; 10 | LIN_speed = 19.2 kbps; 11 | Channel_name = "DB"; 12 | 13 | Nodes { 14 | Master: CEM, 5 ms, 0.1 ms; 15 | Slaves: LSM, RSM; 16 | } 17 | 18 | Signals { 19 | InternalLightsRequest: 2, 0, CEM, LSM, RSM; 20 | RightIntLightsSwitch: 8, 0, RSM, CEM; 21 | LeftIntLightsSwitch: 8, 0, LSM, CEM; 22 | LSMerror: 1, 0, LSM, CEM; 23 | RSMerror: 1, 0, RSM, CEM; 24 | IntTest: 2, 0, LSM, CEM; 25 | } 26 | 27 | Frames { 28 | CEM_Frm1: 0x01, CEM, 1 { 29 | InternalLightsRequest, 0; 30 | } 31 | LSM_Frm1: 0x02, LSM, 2 { 32 | LeftIntLightsSwitch, 8; 33 | } 34 | LSM_Frm2: 0x03, LSM, 1 { 35 | LSMerror, 0; 36 | IntTest, 1; 37 | } 38 | RSM_Frm1: 0x04, RSM, 2 { 39 | RightIntLightsSwitch, 8; 40 | } 41 | RSM_Frm2: 0x05, RSM, 1 { 42 | RSMerror, 0; 43 | } 44 | } 45 | 46 | Event_triggered_frames { 47 | Node_Status_Event : Collision_resolver, 0x06, RSM_Frm1, LSM_Frm1; 48 | } 49 | 50 | Node_attributes { 51 | RSM { 52 | LIN_protocol = "2.0"; 53 | configured_NAD = 0x20; 54 | product_id = 0x4E4E, 0x4553, 1; 55 | response_error = RSMerror; 56 | P2_min = 150 ms; 57 | ST_min = 50 ms; 58 | configurable_frames { 59 | Node_Status_Event=0x000; CEM_Frm1 = 0x0001; RSM_Frm1 = 0x0002; 60 | RSM_Frm2 = 0x0003; 61 | } 62 | } 63 | LSM { 64 | LIN_protocol = "2.2"; 65 | configured_NAD = 0x21; 66 | initial_NAD = 0x01; 67 | product_id = 0x4A4F, 0x4841; 68 | response_error = LSMerror; 69 | fault_state_signals = IntTest; 70 | P2_min = 150 ms; 71 | ST_min = 50 ms; 72 | configurable_frames { 73 | Node_Status_Event; 74 | CEM_Frm1; 75 | LSM_Frm1; 76 | LSM_Frm2; 77 | } 78 | } 79 | } 80 | 81 | Schedule_tables { 82 | Configuration_Schedule { 83 | AssignNAD {LSM} delay 15 ms; 84 | AssignFrameIdRange {LSM, 0} delay 15 ms; 85 | AssignFrameId {RSM, CEM_Frm1} delay 15 ms; 86 | AssignFrameId {RSM, RSM_Frm1} delay 15 ms; 87 | AssignFrameId {RSM, RSM_Frm2} delay 15 ms; 88 | } 89 | Normal_Schedule { 90 | CEM_Frm1 delay 15 ms; 91 | LSM_Frm2 delay 15 ms; 92 | RSM_Frm2 delay 15 ms; 93 | Node_Status_Event delay 10 ms; 94 | } 95 | MRF_schedule { 96 | MasterReq delay 10 ms; 97 | } 98 | SRF_schedule { 99 | SlaveResp delay 10 ms; 100 | } 101 | Collision_resolver { // Keep timing of other frames if collision 102 | CEM_Frm1 delay 15 ms; 103 | LSM_Frm2 delay 15 ms; 104 | RSM_Frm2 delay 15 ms; 105 | RSM_Frm1 delay 10 ms; // Poll the RSM node 106 | CEM_Frm1 delay 15 ms; 107 | LSM_Frm2 delay 15 ms; 108 | RSM_Frm2 delay 15 ms; 109 | LSM_Frm1 delay 10 ms; // Poll the LSM node 110 | } 111 | } 112 | 113 | Signal_encoding_types { 114 | Dig2Bit { 115 | logical_value, 0, "off"; 116 | logical_value, 1, "on"; 117 | logical_value, 2, "error"; 118 | logical_value, 3, "void"; 119 | } 120 | ErrorEncoding { 121 | logical_value, 0, "OK"; 122 | logical_value, 1, "error"; 123 | } 124 | FaultStateEncoding { 125 | logical_value, 0, "No test result"; 126 | logical_value, 1, "failed"; 127 | logical_value, 2, "passed"; 128 | logical_value, 3, "not used"; 129 | } 130 | LightEncoding { 131 | logical_value, 0, "Off"; 132 | physical_value, 1, 254, 1, 100, "lux"; 133 | logical_value, 255, "error"; 134 | } 135 | } 136 | Signal_representation { 137 | Dig2Bit: InternalLightsRequest; 138 | ErrorEncoding: RSMerror, LSMerror; 139 | LightEncoding: RightIntLightsSwitch, LeftIntLightsSwitch; 140 | } -------------------------------------------------------------------------------- /examples/read_comments.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ldfparser 3 | 4 | if __name__ == "__main__": 5 | path = os.path.join(os.path.dirname(__file__), 'lin22.ldf') 6 | ldf = ldfparser.parse_ldf(path, capture_comments=True) 7 | print(ldf.comments) 8 | -------------------------------------------------------------------------------- /ldfparser/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ldfparser is a library for parsing LIN Description Files 3 | """ 4 | from .diagnostics import (LinDiagnosticFrame, LinDiagnosticRequest, 5 | LinDiagnosticResponse, LIN_MASTER_REQUEST_FRAME_ID, 6 | LIN_SLAVE_RESPONSE_FRAME_ID, 7 | LIN_NAD_RESERVED, LIN_NAD_SLAVE_NODE_RANGE, 8 | LIN_NAD_FUNCTIONAL_NODE_ADDRESS, LIN_NAD_BROADCAST_ADDRESS, 9 | LIN_NAD_FREE_RANGE, 10 | LIN_SID_RESERVED_RANGE1, LIN_SID_ASSIGN_NAD, LIN_SID_ASSIGN_FRAME_ID, 11 | LIN_SID_READ_BY_ID, LIN_SID_CONDITIONAL_CHANGE_NAD, LIN_SID_DATA_DUMP, 12 | LIN_SID_RESERVED, LIN_SID_SAVE_CONFIGURATION, 13 | LIN_SID_ASSIGN_FRAME_ID_RANGE, LIN_SID_RESERVED_RANGE2, 14 | LIN_SID_READ_BY_ID_PRODUCT_ID, LIN_SID_READ_BY_ID_SERIAL_NUMBER, 15 | LIN_SID_READ_BY_ID_RESERVED_RANGE1, LIN_SID_READ_BY_ID_USER_DEFINED_RANGE, 16 | LIN_SID_READ_BY_ID_RESERVED_RANGE2) 17 | from .encoding import (PhysicalValue, LogicalValue, ASCIIValue, BCDValue, 18 | LinSignalEncodingType) 19 | from .frame import LinEventTriggeredFrame, LinFrame, LinUnconditionalFrame 20 | from .ldf import LDF 21 | from .lin import (LIN_VERSION_1_3, LIN_VERSION_2_0, LIN_VERSION_2_1, 22 | LIN_VERSION_2_2, LinVersion, ISO17987_2015, Iso17987Version) 23 | from .node import (LinMaster, LinProductId, LinSlave, 24 | LinNodeCompositionConfiguration, LinNodeComposition) 25 | from .parser import parse_ldf, parse_ldf_to_dict, parseLDF, parseLDFtoDict 26 | from .save import save_ldf 27 | from .schedule import ScheduleTable, ScheduleTableEntry 28 | from .signal import LinSignal 29 | -------------------------------------------------------------------------------- /ldfparser/cli.py: -------------------------------------------------------------------------------- 1 | """Command Line Interface 2 | """ 3 | import argparse 4 | import json 5 | import os 6 | import sys 7 | 8 | from ldfparser import LDF, LinFrame, LinMaster, LinSignal, LinSlave, parse_ldf 9 | 10 | def auto_int(number: str): 11 | """Converts a string to integer""" 12 | return int(number, 0) 13 | 14 | def exit_with_error(code: int, message: str): 15 | """Exits with the given exit code and message""" 16 | print(message, file=sys.stderr) 17 | sys.exit(code) 18 | 19 | def parse_args(args): 20 | parser = argparse.ArgumentParser() 21 | parser.add_argument('-f', '--ldf', required=True) 22 | parser.add_argument('-e', '--encoding', required=False, default='utf-8') 23 | subparser = parser.add_subparsers(dest="subparser_name") 24 | 25 | infoparser = subparser.add_parser('info') 26 | infoparser.add_argument('--details', action='store_true') 27 | 28 | exportparser = subparser.add_parser('export') 29 | exportparser.add_argument('--output', required=False, default=None) 30 | 31 | nodeparser = subparser.add_parser('node') 32 | nodearggroup = nodeparser.add_mutually_exclusive_group() 33 | nodearggroup.add_argument('--list', action="store_true") 34 | nodearggroup.add_argument('--master', action="store_true") 35 | nodearggroup.add_argument('--slave', type=str) 36 | 37 | frameparser = subparser.add_parser('frame') 38 | framearggroup = frameparser.add_mutually_exclusive_group() 39 | framearggroup.add_argument('--list', action="store_true") 40 | framearggroup.add_argument('--id', type=auto_int) 41 | framearggroup.add_argument('--name', type=str) 42 | 43 | signalparser = subparser.add_parser('signal') 44 | signalarggroup = signalparser.add_mutually_exclusive_group() 45 | signalarggroup.add_argument('--list', action="store_true") 46 | signalarggroup.add_argument('--name', type=str) 47 | 48 | return parser.parse_args(args) 49 | 50 | def main(): 51 | args = parse_args(sys.argv[1:]) 52 | ldf = parse_ldf(args.ldf, encoding=args.encoding) 53 | 54 | if args.subparser_name is None: 55 | print_ldf_info(ldf) 56 | elif args.subparser_name == 'info': 57 | print_ldf_info(ldf, args.details) 58 | elif args.subparser_name == 'export': 59 | export_ldf(ldf, args.output) 60 | elif args.subparser_name == 'node': 61 | handle_node_subcommand(args, ldf) 62 | elif args.subparser_name == 'frame': 63 | handle_frame_subcommand(args, ldf) 64 | elif args.subparser_name == 'signal': 65 | handle_signal_subcommand(args, ldf) 66 | else: 67 | exit_with_error(1, f"Unknown subcommand {args.subparser_name}") 68 | exit(0) 69 | 70 | def handle_node_subcommand(args, ldf: LDF): 71 | if args.list: 72 | print(f"{ldf.master.name} (master)") 73 | for node in ldf.slaves: 74 | print(node.name) 75 | elif args.master: 76 | print_master_info(ldf.master) 77 | else: 78 | if not ldf.slave(args.slave): 79 | exit_with_error(1, f"Slave '{args.slave}' not found.") 80 | print_slave_info(ldf.slave(args.slave)) 81 | 82 | def handle_frame_subcommand(args, ldf: LDF): 83 | if args.list: 84 | for frame in ldf.frames: 85 | print(frame.name) 86 | elif args.id: 87 | if not ldf.frame(args.id): 88 | exit_with_error(1, f"Frame with id '{args.id}' not found.") 89 | print_frame_info(ldf.frame(args.id)) 90 | else: 91 | if not ldf.frame(args.name): 92 | exit_with_error(1, f"Frame with name '{args.name}' not found") 93 | print_frame_info(ldf.frame(args.name)) 94 | 95 | def handle_signal_subcommand(args, ldf: LDF): 96 | if args.list: 97 | for signal in ldf.signals: 98 | print(signal.name) 99 | else: 100 | if not ldf.signal(args.name): 101 | exit_with_error(1, f"Signal with name '{args.name}' not found") 102 | print_signal_info(ldf.signal(args.name)) 103 | 104 | def export_ldf(ldf: LDF, output: str = None): 105 | if output is None: 106 | json.dump(ldf._source, sys.stdout, indent=4) 107 | else: 108 | os.makedirs(os.path.dirname(output), exist_ok=True) 109 | with open(output, 'w+') as file: 110 | json.dump(ldf._source, file, indent=4) 111 | 112 | def print_ldf_info(ldf: LDF, extended: bool = False): 113 | print(f"Protocol Version: {ldf.protocol_version:.01f}") 114 | print(f"Language Version: {ldf.language_version:.01f}") 115 | print(f"Speed: {ldf.baudrate}") 116 | print(f"Channel: {ldf.channel if ldf.channel else '-'}") 117 | 118 | if extended: 119 | print("Nodes:") 120 | print(f"\t{ldf.master.name} (master)") 121 | for slave in ldf.slaves: 122 | print(f"\t{slave.name}") 123 | else: 124 | print(f"Node count: {len(ldf.slaves) + 1}") 125 | 126 | if extended: 127 | print("Frames (id, length, name):") 128 | for frame in ldf.frames: 129 | print(f"\t{frame.frame_id},{frame.length},{frame.name}") 130 | else: 131 | print(f"Frame count: {len(ldf.frames)}") 132 | 133 | if extended: 134 | print("Signals (width, name):") 135 | for signal in ldf.signals: 136 | print(f"\t{signal.width},{signal.name}") 137 | else: 138 | print(f"Signal count: {len(ldf.signals)}") 139 | 140 | def print_slave_info(slave: LinSlave): 141 | print(f"Name: {slave.name}") 142 | print(f"Protocol: {slave.lin_protocol}") 143 | print(f"Configured NAD: 0x{slave.configured_nad:02x}") 144 | print(f"Initial NAD: 0x{slave.initial_nad:02x}") 145 | print("Product Id:") 146 | print(f"\tSupplier Id: 0x{slave.product_id.supplier_id:04x}") 147 | print(f"\tFunction Id: 0x{slave.product_id.function_id:04x}") 148 | print(f"\tVariant: {slave.product_id.variant}") 149 | 150 | def print_master_info(master: LinMaster): 151 | print(f"Name: {master.name}") 152 | print(f"Timebase: {master.timebase * 1000:.02f} ms") 153 | print(f"Jitter: {master.jitter * 1000:.02f} ms") 154 | 155 | def print_frame_info(frame: LinFrame): 156 | print(f"Id: {frame.frame_id}") 157 | print(f"Name: {frame.name}") 158 | print(f"Length: {frame.length} byte(s)") 159 | print(f"Publisher: {frame.publisher.name}") 160 | print("Signals (offset, width, name):") 161 | for signal in frame.signal_map: 162 | print(f"\t{signal[0]},{signal[1].width},{signal[1].name}") 163 | 164 | def print_signal_info(signal: LinSignal): 165 | print(f"Name: {signal.name}") 166 | print(f"Width: {signal.width} bit(s)") 167 | print(f"Initial value: {signal.init_value}") 168 | print(f"Publisher: {signal.publisher.name}") 169 | print("Subscribers:") 170 | for subscriber in signal.subscribers: 171 | print(f"\t{subscriber.name}") 172 | -------------------------------------------------------------------------------- /ldfparser/diagnostics.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Dict 2 | 3 | from .frame import LinUnconditionalFrame 4 | 5 | # LIN Diagnostic Frame IDs 6 | LIN_MASTER_REQUEST_FRAME_ID = 0x3C 7 | LIN_SLAVE_RESPONSE_FRAME_ID = 0x3D 8 | 9 | # NAD values (Specified in 4.2.3.2) 10 | LIN_NAD_RESERVED = 0x00 11 | LIN_NAD_SLAVE_NODE_RANGE = range(0x01, 0x7E) 12 | LIN_NAD_FUNCTIONAL_NODE_ADDRESS = 0x7E 13 | LIN_NAD_BROADCAST_ADDRESS = 0x7F 14 | LIN_NAD_FREE_RANGE = range(0x80, 0x100) 15 | 16 | # Service identifiers (Specified in 4.2.3.4) 17 | LIN_SID_RESERVED_RANGE1 = range(0, 0xB0) 18 | LIN_SID_ASSIGN_NAD = 0xB0 19 | LIN_SID_ASSIGN_FRAME_ID = 0xB1 20 | LIN_SID_READ_BY_ID = 0xB2 21 | LIN_SID_CONDITIONAL_CHANGE_NAD = 0xB3 22 | LIN_SID_DATA_DUMP = 0xB4 23 | LIN_SID_RESERVED = 0xB5 24 | LIN_SID_SAVE_CONFIGURATION = 0xB6 25 | LIN_SID_ASSIGN_FRAME_ID_RANGE = 0xB7 26 | LIN_SID_RESERVED_RANGE2 = range(0xB8, 0x100) 27 | 28 | # Read by identifier request IDs (Specified in 4.2.6.1) 29 | LIN_SID_READ_BY_ID_PRODUCT_ID = 0 30 | LIN_SID_READ_BY_ID_SERIAL_NUMBER = 1 31 | LIN_SID_READ_BY_ID_RESERVED_RANGE1 = range(2, 32) 32 | LIN_SID_READ_BY_ID_USER_DEFINED_RANGE = range(32, 64) 33 | LIN_SID_READ_BY_ID_RESERVED_RANGE2 = range(64, 256) 34 | 35 | def rsid(sid: int) -> int: 36 | """ 37 | Returns the response service identifier for a given service id 38 | 39 | Example: 40 | >>> rsid(LIN_SID_READ_BY_ID) 41 | 0xF2 42 | 43 | :param sid: Service identifier 44 | :type sid: int 45 | :returns: Response Service Identifier (RSID) 46 | :rtype: int 47 | """ 48 | return sid + 0x40 49 | 50 | LIN_PCI_SINGLE_FRAME = 0b0000 51 | LIN_PCI_FIRST_FRAME = 0b0001 52 | LIN_PCI_CONSECUTIVE_FRAME = 0b0010 53 | 54 | def pci_byte(pci_type: int, length: int) -> int: 55 | """ 56 | Returns protocol control information byte 57 | 58 | Example: 59 | >>> pci_byte(LIN_PCI_SINGLE_FRAME, 6) 60 | 0x06 61 | 62 | :param pci_type: PCI Type (Single Frame, First Frame, etc.) 63 | :type pci_type: int 64 | :param length: PCI length \ 65 | For Single Frame type this is the length of the frame \ 66 | For First Frame type this the length of the unit divided by 256 \ 67 | For Consecutive Frame type this is the remaining frame counter 68 | :type length: 69 | :returns: Calculated PCI byte 70 | :rtype: int 71 | """ 72 | return (length & 0x0F) | (pci_type << 4) 73 | 74 | class LinDiagnosticFrame(LinUnconditionalFrame): 75 | """Base class for diagnostic communication""" 76 | pass 77 | 78 | class LinDiagnosticRequest(LinDiagnosticFrame): 79 | """LinDiagnosticRequest is used to encode standard diagnostic messages""" 80 | 81 | _FIELDS = ['NAD', 'PCI', 'SID', 'D1', 'D2', 'D3', 'D4', 'D5'] 82 | 83 | def __init__(self, frame: LinDiagnosticFrame): 84 | super().__init__(frame.frame_id, frame.name, frame.length, dict(frame.signal_map)) 85 | 86 | def encode_request(self, nad: int, pci: int, sid: int, 87 | d1: int, d2: int, d3: int, d4: int, d5: int): 88 | """ 89 | Encodes a diagnostic request into a frame 90 | 91 | Example: 92 | >>> encode_request(0x01, 0x06, LIN_SID_READ_BY_ID, 93 | LIN_SID_READ_BY_ID_PRODUCT_ID, 0xFF, 0x7F, 0xFF, 0xFF) 94 | 95 | :param nad: Node Address 96 | :type nad: int 97 | :param pci: Protocol Control Information 98 | :type pci: int 99 | :param sid: Service Identifier 100 | :type sid: int 101 | :param d1: Data byte 1 102 | :type d1: int 103 | :param d2: Data byte 2 104 | :type d2: int 105 | :param d3: Data byte 3 106 | :type d3: int 107 | :param d4: Data byte 4 108 | :type d4: int 109 | :param d5: Data byte 5 110 | :type d5: int 111 | :returns: Encoded frame 112 | :rtype: bytearray 113 | """ 114 | return self.encode_raw([nad, pci, sid, d1, d2, d3, d4, d5]) 115 | 116 | def encode_assign_nad(self, initial_nad: int, supplier_id: int, function_id: int, 117 | new_nad: int) -> bytearray: 118 | """ 119 | Encodes an AssignNAD diagnostic request into a frame 120 | 121 | Example: 122 | >>> encode_assign_nad(0x00, 0x7FFF, 0xFFFF, 0x01) 123 | 124 | :param initial_nad: Initial Node Address 125 | :type initial_nad: int 126 | :param supplier_id: Supplier ID 127 | :type supplier_id: int 128 | :param function_id: Function ID 129 | :type function_id: int 130 | :param new_nad: New Node Address 131 | :type new_nad: int 132 | :returns: Encoded frame 133 | :rtype: bytearray 134 | """ 135 | return self.encode_request(initial_nad, pci_byte(LIN_PCI_SINGLE_FRAME, 6), 136 | LIN_SID_ASSIGN_NAD, 137 | supplier_id & 0xFF, (supplier_id >> 8) & 0xFF, 138 | function_id & 0xFF, (function_id >> 8) & 0xFF, 139 | new_nad) 140 | 141 | def encode_conditional_change_nad(self, nad: int, identifier: int, byte: int, 142 | mask: int, invert: int, new_nad: int) -> bytearray: 143 | """ 144 | Encodes an ConditionalChangeNAD diagnostic request into a frame 145 | 146 | Example: 147 | >>> encode_conditional_change_nad(0x7F, 0x01, 0x03, 0x01, 0xFF, 0x01) 148 | 149 | :param nad: Node Address 150 | :type nad: int 151 | :param identifier: Identifier 152 | :type identifier: int 153 | :param byte: Byte number 154 | :type byte: int 155 | :param mask: Byte mask 156 | :type mask: int 157 | :param invert: Invert mask 158 | :type invert: int 159 | :param new_nad: New Node Address 160 | :type new_nad: int 161 | :returns: Encoded frame 162 | :rtype: bytearray 163 | """ 164 | return self.encode_request(nad, pci_byte(LIN_PCI_SINGLE_FRAME, 6), 165 | LIN_SID_CONDITIONAL_CHANGE_NAD, 166 | identifier, byte, mask, invert, 167 | new_nad) 168 | 169 | def encode_data_dump(self, nad: int, data: Iterable[int]) -> bytearray: 170 | """ 171 | Encodes a DataDump diagnostic request into a frame 172 | 173 | Example: 174 | >>> encode_data_dump(nad=0x01, data=[0x01, 0x00, 0x00, 0xFF, 0xFF]) 175 | 176 | :param nad: Node Address 177 | :type nad: int 178 | :param data: User defined data of 5 bytes 179 | :type data: Iterable[int] 180 | :returns: Encoded frame 181 | :rtype: bytearray 182 | """ 183 | return self.encode_request(nad, pci_byte(LIN_PCI_SINGLE_FRAME, 6), LIN_SID_DATA_DUMP, 184 | data[0], data[1], data[2], data[3], data[4]) 185 | 186 | def encode_save_configuration(self, nad: int) -> bytearray: 187 | """ 188 | Encodes a SaveConfiguration diagnostic request into a frame 189 | 190 | Example: 191 | >>> encode_save_configuration(nad=0x01) 192 | 193 | :param nad: Node Address 194 | :type nad: int 195 | :returns: Encoded frame 196 | :rtype: bytearray 197 | """ 198 | return self.encode_request(nad, pci_byte(LIN_PCI_SINGLE_FRAME, 1), 199 | LIN_SID_SAVE_CONFIGURATION, 200 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF) 201 | 202 | def encode_assign_frame_id_range(self, nad: int, start_index: int, 203 | pids: Iterable[int]) -> bytearray: 204 | """ 205 | Encodes a AssignFrameIdRange diagnostic request into a frame 206 | 207 | Example: 208 | >>> encode_assign_frame_id_range(nad=0x01, 209 | start_index=0x00, 210 | pids=[0x32, 0x33, 0x34, 0x35]) 211 | 212 | :param nad: Node Address 213 | :type nad: int 214 | :param start_index: First message index to assign 215 | :type start_index: int 216 | :params pids: Protected identifiers to assign 217 | :type pids: Iterable[int] 218 | :returns: Encoded frame 219 | :rtype: bytearray 220 | """ 221 | return self.encode_request(nad, pci_byte(LIN_PCI_SINGLE_FRAME, 6), 222 | LIN_SID_ASSIGN_FRAME_ID_RANGE, 223 | start_index, 224 | pids[0], pids[1], pids[2], pids[3]) 225 | 226 | def encode_read_by_id(self, nad: int, identifier: int, supplier_id: int, 227 | function_id: int) -> bytearray: 228 | """ 229 | Encodes a ReadById diagnostic request into a frame 230 | 231 | Example: 232 | >>> encode_read_by_id(nad=0x01, 233 | identifier=LIN_SID_READ_BY_ID_SERIAL_NUMBER, 234 | supplier_id=0x7FFF, 235 | function_id=0xFFFF) 236 | 237 | :param nad: Node Address 238 | :type nad: int 239 | :param identifier: Identifier to read 240 | :type identifier: int 241 | :param supplier_id: Supplier ID 242 | :type supplier_id: int 243 | :param function_id: Function ID 244 | :type function_id: int 245 | :returns: Encoded frame 246 | :rtype: bytearray 247 | """ 248 | return self.encode_request(nad, pci_byte(LIN_PCI_SINGLE_FRAME, 6), LIN_SID_READ_BY_ID, 249 | identifier, 250 | supplier_id & 0xFF, (supplier_id >> 8) & 0xFF, 251 | function_id & 0xFF, (function_id >> 8) & 0xFF) 252 | 253 | class LinDiagnosticResponse(LinDiagnosticFrame): 254 | """LinDiagnosticResponse is used to decode standard diagnostic responses""" 255 | 256 | _FIELDS = ['NAD', 'PCI', 'RSID', 'D1', 'D2', 'D3', 'D4', 'D5'] 257 | 258 | def __init__(self, frame: LinDiagnosticFrame): 259 | super().__init__(frame.frame_id, frame.name, frame.length, dict(frame.signal_map)) 260 | self._signal_remapper = dict(zip(map(lambda x: x[1].name, frame.signal_map), 261 | LinDiagnosticResponse._FIELDS)) 262 | 263 | def decode_response(self, data: bytearray) -> Dict[str, int]: 264 | """ 265 | Decodes a diagnostic response 266 | 267 | Example: 268 | >>> decode_response(bytearray([0x00,0x01,0xF0,0xFF,0xFF,0xFF,0xFF,0xFF])) 269 | {'NAD': 0x00, 'PCI': 0x01, 'RSID': 0xF0, ... } 270 | 271 | :param data: Frame content 272 | :type data: bytearray 273 | :returns: Dictionary of field keys and values 274 | :rtype: Dict[str, int] 275 | """ 276 | message = self.decode_raw(data) 277 | return {self._signal_remapper[signal_name]: message[signal_name] for signal_name in message} 278 | -------------------------------------------------------------------------------- /ldfparser/encoding.py: -------------------------------------------------------------------------------- 1 | """ 2 | Contains classes that are used to encode and decode Lin Signals. 3 | 4 | Signal encoding is specified in the LIN 2.1 Specification, section 9.2.6.1 5 | """ 6 | from typing import List, Union, Any, TYPE_CHECKING 7 | 8 | if TYPE_CHECKING: 9 | from .signal import LinSignal 10 | 11 | class ValueConverter(): 12 | """ 13 | Value converter is used to convert Lin signal values from and into 14 | their human readable form 15 | """ 16 | 17 | def encode(self, value: Any, signal: 'LinSignal') -> Union[int, List[int]]: 18 | """ 19 | Converts the human readable value into the raw bytes that will be sent 20 | """ 21 | raise NotImplementedError() 22 | 23 | def decode(self, value: Union[int, List[int]], signal: 'LinSignal', keep_unit: bool = False) -> Any: 24 | """ 25 | Converts the received raw bytes into the human readable form 26 | """ 27 | raise NotImplementedError() 28 | 29 | class PhysicalValue(ValueConverter): 30 | """ 31 | Value converter for physical values 32 | 33 | A physical value encoder converts a range of values into a different range of values. 34 | 35 | :Example: 36 | 37 | `PhysicalValue(phy_min=0, phy_max=100, scale=50, offset=400, unit='rpm')` maps signal values 38 | from 0-100 into a range of 400 - 5400 rpm. 39 | """ 40 | 41 | def __init__(self, phy_min: int, phy_max: int, scale: float, offset: float, unit: str = None): 42 | # pylint: disable=too-many-arguments 43 | self.phy_min = phy_min 44 | self.phy_max = phy_max 45 | self.scale = scale 46 | self.offset = offset 47 | self.unit = unit 48 | 49 | def encode(self, value: Union[str, int, float], signal: 'LinSignal') -> int: 50 | if isinstance(value, str) and self.unit is not None and value.endswith(self.unit): 51 | num = float(value[:-len(self.unit)]) 52 | else: 53 | num = float(value) 54 | 55 | raw = self.offset 56 | if self.scale != 0: 57 | raw = round((num - self.offset) / self.scale) 58 | 59 | if raw < self.phy_min or raw > self.phy_max: 60 | raise ValueError(f"value: {raw} out of range ({self.phy_min}, {self.phy_max})") 61 | 62 | return raw 63 | 64 | def decode(self, value: int, signal: 'LinSignal', keep_unit: bool = False) -> float: 65 | if value < self.phy_min or value > self.phy_max: 66 | raise ValueError(f"value: {value} out of range ({self.phy_min}, {self.phy_max})") 67 | 68 | decoded = float(value * self.scale + self.offset) 69 | if keep_unit: 70 | return f"{decoded:.03f} {self.unit}" 71 | return decoded 72 | 73 | class LogicalValue(ValueConverter): 74 | """ 75 | Value converter for logical values 76 | 77 | A logical value encoder converts a particular value into another value. 78 | 79 | :Example: 80 | 81 | `LogicalValue(phy_value=0, unit='off')` maps the signal value `0` into `'off'` 82 | 83 | `LogicalValue(phy_value=1, unit='on')` maps the signal value `1` into `'on'` 84 | """ 85 | 86 | def __init__(self, phy_value: int, info: str = None): 87 | self.phy_value = phy_value 88 | self.info = info 89 | 90 | def encode(self, value: Union[str, int], signal: 'LinSignal') -> int: 91 | if self.info is None and value == self.phy_value: 92 | return self.phy_value 93 | if self.info is not None and value == self.info: 94 | return self.phy_value 95 | raise ValueError(f"value: {value} not equal to signal info") 96 | 97 | def decode(self, value: int, signal: 'LinSignal', keep_unit: bool = False) -> Union[str, int]: 98 | if value == self.phy_value: 99 | return self.info if self.info is not None else self.phy_value 100 | raise ValueError(f"value: {value} not equal to {self.phy_value}") 101 | 102 | class BCDValue(ValueConverter): 103 | """ 104 | Value converter for Binary Coded Decimal values 105 | """ 106 | 107 | def encode(self, value: int, signal: 'LinSignal') -> List[int]: 108 | if value > 10**int(signal.width / 8): 109 | raise ValueError(f"cannot convert value {value} to bcd, out of {signal} bounds") 110 | bcd = [] 111 | for i in range(int(signal.width / 8) - 1, -1, -1): 112 | bcd.append(value // 10**i % 10) 113 | return bcd 114 | 115 | def decode(self, value: List[int], signal: 'LinSignal', keep_unit: bool = False) -> int: 116 | out = 0 117 | length = int(signal.width / 8) 118 | for i in range(length): 119 | if value[i] > 9: 120 | raise ValueError('bcd digit larger than 9') 121 | out += value[i] * 10**(length - i - 1) 122 | return out 123 | 124 | class ASCIIValue(ValueConverter): 125 | """ 126 | Value converter for ASCII values 127 | """ 128 | 129 | def encode(self, value: str, signal: 'LinSignal') -> List[int]: 130 | return list(value.encode()) 131 | 132 | def decode(self, value: List[int], signal: 'LinSignal', keep_unit: bool = False) -> str: 133 | return bytes(value).decode() 134 | 135 | class LinSignalEncodingType(): 136 | """ 137 | LinSignalEncodingType is used to encode and decode LIN signals 138 | 139 | An encoding type contains multiple value converters. 140 | 141 | :param name: Signal encoding type name 142 | :type name: `str` 143 | :param converters: Value converters in the encoding type 144 | :type converters: `List[ValueConverter]` 145 | 146 | :Example: 147 | 148 | ``` 149 | LinSignalEncodingType(name="MotorSpeed", 150 | [ 151 | LogicalValue(phy_value=0, info='off') 152 | PhysicalValue(phy_min=1, phy_max=254, scale=10, offset=100, unit='rpm') 153 | LogicalValue(phy_value=255, info='error') 154 | ]) 155 | ``` 156 | """ 157 | 158 | def __init__(self, name: str, converters: List[ValueConverter]): 159 | self.name: str = name 160 | self._converters: List[ValueConverter] = converters 161 | self._signals: List['LinSignal'] = [] 162 | 163 | def encode(self, value: Union[str, int, float], signal: 'LinSignal') -> int: 164 | """ 165 | Encodes the given value into the physical value 166 | """ 167 | for encoder in self._converters: 168 | try: 169 | return encoder.encode(value, signal) 170 | except ValueError: 171 | pass 172 | raise ValueError(f"cannot encode '{value}' as {self.name}") 173 | 174 | def decode(self, value: int, signal: 'LinSignal', keep_unit: bool = False) -> Union[str, int, float]: 175 | """ 176 | Decodes the given physical value into the signal value 177 | """ 178 | for decoder in self._converters: 179 | try: 180 | return decoder.decode(value, signal, keep_unit) 181 | except ValueError: 182 | pass 183 | raise ValueError(f"cannot decode {value} as {self.name}") 184 | 185 | def get_converters(self) -> List[ValueConverter]: 186 | return self._converters 187 | 188 | def get_signals(self) -> List['LinSignal']: 189 | return self._signals 190 | -------------------------------------------------------------------------------- /ldfparser/grammar.py: -------------------------------------------------------------------------------- 1 | from lark import Transformer 2 | 3 | class LdfTransformer(Transformer): 4 | # pylint: disable=missing-function-docstring,no-self-use,too-many-public-methods,unused-argument 5 | """ 6 | Transforms the LDF grammar into a Python dictionary 7 | """ 8 | 9 | def parse_integer(self, value: str): 10 | try: 11 | return int(value) 12 | except ValueError: 13 | return int(value, 16) 14 | 15 | def parse_real_or_integer(self, value: str): 16 | return float(value) 17 | 18 | def ldf_identifier(self, tree): 19 | return tree[0][0:] 20 | 21 | def ldf_version(self, tree): 22 | return tree[0][0:] 23 | 24 | def ldf_integer(self, tree): 25 | return self.parse_integer(tree[0]) 26 | 27 | def ldf_float(self, tree): 28 | return self.parse_real_or_integer(tree[0]) 29 | 30 | def ldf_channel_name(self, tree): 31 | # This gets rid of quote marks 32 | return tree[0][1:-1] 33 | 34 | def start(self, tree): 35 | return tree[0] 36 | 37 | def ldf(self, tree): 38 | ldf = {} 39 | for k in tree[0:]: 40 | ldf[k[0]] = k[1] 41 | return ldf 42 | 43 | def header_lin_description_file(self, tree): 44 | return ("header", "lin_description_file") 45 | 46 | def header_protocol_version(self, tree): 47 | return ("protocol_version", tree[0]) 48 | 49 | def header_language_version(self, tree): 50 | return ("language_version", tree[0]) 51 | 52 | def header_speed(self, tree): 53 | return ("speed", int(float(tree[0]) * 1000)) 54 | 55 | def header_channel(self, tree): 56 | return ("channel_name", tree[0]) 57 | 58 | def header_file_revision(self, tree): 59 | return ("file_revision", tree[0]) 60 | 61 | def header_sig_byte_order_big_endian(self, tree): 62 | return ("endianness", "big") 63 | 64 | def header_sig_byte_order_little_endian(self, tree): 65 | return ("endianness", "little") 66 | 67 | def nodes(self, tree): 68 | if len(tree) == 0: 69 | return ("nodes", {}) 70 | if len(tree) == 1: 71 | return ("nodes", {'master': tree[0]}) 72 | return ("nodes", {'master': tree[0], 'slaves': tree[1]}) 73 | 74 | def nodes_master(self, tree): 75 | return {"name": tree[0], "timebase": tree[1] * 0.001, "jitter": tree[2] * 0.001, "max_header_length": tree[3] if len(tree) > 3 else None, "response_tolerance": tree[4] * 0.01 if len(tree) > 4 else None} 76 | 77 | def nodes_slaves(self, tree): 78 | return tree 79 | 80 | def node_compositions(self, tree): 81 | return ("node_compositions", tree[0:]) 82 | 83 | def node_compositions_configuration(self, tree): 84 | return {"name": tree[0], "compositions": tree[1]} 85 | 86 | def node_compositions_composite(self, tree): 87 | return {"name": tree[0], "nodes": tree[1:]} 88 | 89 | def signals(self, tree): 90 | return ("signals", tree) 91 | 92 | def signal_definition(self, tree): 93 | return {"name": tree[0], "width": int(tree[1]), "init_value": tree[2], "publisher": tree[3], "subscribers": tree[4:]} 94 | 95 | def signal_default_value(self, tree): 96 | return tree[0] 97 | 98 | def signal_default_value_single(self, tree): 99 | return tree[0] 100 | 101 | def signal_default_value_array(self, tree): 102 | return tree[0:] 103 | 104 | def diagnostic_signals(self, tree): 105 | return ("diagnostic_signals", tree) 106 | 107 | def diagnostic_signal_definition(self, tree): 108 | return {"name": tree[0], "width": int(tree[1]), "init_value": tree[2]} 109 | 110 | def frames(self, tree): 111 | return ("frames", tree) 112 | 113 | def frame_definition(self, tree): 114 | return {"name": tree[0], "frame_id": int(tree[1]), "publisher": tree[2], "length": tree[3] if len(tree) > 4 else None, "signals": tree[4] if len(tree) > 4 else tree[3]} 115 | 116 | def frame_signals(self, tree): 117 | return tree[0:] 118 | 119 | def frame_signal(self, tree): 120 | return {"signal": tree[0], "offset": int(tree[1])} 121 | 122 | def sporadic_frames(self, tree): 123 | return ("sporadic_frames", tree[0:]) 124 | 125 | def sporadic_frame_definition(self, tree): 126 | return {"name": tree[0], "frames": tree[1:]} 127 | 128 | def event_triggered_frames(self, tree): 129 | return ("event_triggered_frames", tree[0:]) 130 | 131 | def event_triggered_frame_definition(self, tree): 132 | return {"name": tree[0], "collision_resolving_schedule_table": tree[1], "frame_id": tree[2], "frames": tree[3]} 133 | 134 | def event_triggered_frame_definition_frames(self, tree): 135 | return tree[0:] 136 | 137 | def diagnostic_frames(self, tree): 138 | return ("diagnostic_frames", tree) 139 | 140 | def diagnostic_frame_definition(self, tree): 141 | return {"name": tree[0], "frame_id": int(tree[1]), "signals": tree[2]} 142 | 143 | def diagnostic_frame_signals(self, tree): 144 | return tree[0:] 145 | 146 | def diagnostic_addresses(self, tree): 147 | return ("diagnostic_addresses", dict(tree)) 148 | 149 | def diagnostic_address(self, tree): 150 | return (tree[0], tree[1]) 151 | 152 | def node_attributes(self, tree): 153 | return ("node_attributes", tree[0:]) 154 | 155 | def node_definition(self, tree): 156 | node = {"name": tree[0]} 157 | for k in tree[1:]: 158 | node[k[0]] = k[1] 159 | return node 160 | 161 | def node_definition_protocol(self, tree): 162 | return ("lin_protocol", tree[0]) 163 | 164 | def node_definition_configured_nad(self, tree): 165 | return ("configured_nad", tree[0]) 166 | 167 | def node_definition_initial_nad(self, tree): 168 | return ("initial_nad", tree[0]) 169 | 170 | def node_definition_product_id(self, tree): 171 | return ("product_id", {"supplier_id": tree[0], "function_id": tree[1], "variant": tree[2] if len(tree) > 2 else 0}) 172 | 173 | def node_definition_response_error(self, tree): 174 | return ("response_error", tree[0]) 175 | 176 | def node_definition_fault_state_signals(self, tree): 177 | return ("fault_state_signals", tree[0:]) 178 | 179 | def node_definition_p2_min(self, tree): 180 | return ("P2_min", tree[0] * 0.001) 181 | 182 | def node_definition_st_min(self, tree): 183 | return ("ST_min", tree[0] * 0.001) 184 | 185 | def node_definition_n_as_timeout(self, tree): 186 | return ("N_As_timeout", tree[0] * 0.001) 187 | 188 | def node_definition_n_cr_timeout(self, tree): 189 | return ("N_Cr_timeout", tree[0] * 0.001) 190 | 191 | def node_definition_configurable_frames(self, tree): 192 | return tree[0] 193 | 194 | def node_definition_configurable_frames_20(self, tree): 195 | frames = {} 196 | value = iter(tree) 197 | for frame, msg_id in zip(value, value): 198 | frames[frame] = msg_id 199 | return ("configurable_frames", frames) 200 | 201 | def node_definition_configurable_frames_21(self, tree): 202 | return ("configurable_frames", tree[0:]) 203 | 204 | def node_definition_response_tolerance(self, tree): 205 | return ("response_tolerance", tree[0] * 0.01) 206 | 207 | def node_definition_wakeup_time(self, tree): 208 | return ("wakeup_time", tree[0] * 0.001) 209 | 210 | def node_definition_poweron_time(self, tree): 211 | return ("poweron_time", tree[0] * 0.001) 212 | 213 | def schedule_tables(self, tree): 214 | return ("schedule_tables", tree) 215 | 216 | def schedule_table_definition(self, tree): 217 | return {"name": tree[0], "schedule": tree[1:]} 218 | 219 | def schedule_table_entry(self, tree): 220 | return {"command": tree[0], "delay": tree[1] * 0.001} 221 | 222 | def schedule_table_command(self, tree): 223 | return tree[0] 224 | 225 | def schedule_table_command_masterreq(self, tree): 226 | return {"type": "master_request"} 227 | 228 | def schedule_table_command_slaveresp(self, tree): 229 | return {"type": "slave_response"} 230 | 231 | def schedule_table_command_assignnad(self, tree): 232 | return {"type": "assign_nad", "node": tree[0]} 233 | 234 | def schedule_table_command_conditionalchangenad(self, tree): 235 | return {"type": "conditional_change_nad", "nad": tree[0], "id": tree[1], "byte": tree[2], "mask": tree[3], "inv": tree[4], "new_nad": tree[5]} 236 | 237 | def schedule_table_command_datadump(self, tree): 238 | return {"type": "data_dump", "node": tree[0], "data": tree[1:]} 239 | 240 | def schedule_table_command_saveconfiguration(self, tree): 241 | return {"type": "save_configuration", "node": tree[0]} 242 | 243 | def schedule_table_command_assignframeidrange(self, tree): 244 | return {"type": "assign_frame_id_range", "node": tree[0], "frame_index": tree[1], "pids": tree[2:]} 245 | 246 | def schedule_table_command_assignframeid(self, tree): 247 | return {"type": "assign_frame_id", "node": tree[0], "frame": tree[1]} 248 | 249 | def schedule_table_command_unassignframeid(self, tree): 250 | return {"type": "unassign_frame_id", "node": tree[0], "frame": tree[1]} 251 | 252 | def schedule_table_command_freeformat(self, tree): 253 | return {"type": "free_format", "data": tree[0:]} 254 | 255 | def schedule_table_command_frame(self, tree): 256 | return {"type": "frame", "frame": tree[0]} 257 | 258 | def signal_groups(self, tree): 259 | return ("signal_groups", tree) 260 | 261 | def signal_group(self, tree): 262 | signals = {} 263 | value = iter(tree[2:]) 264 | for signal, offset in zip(value, value): 265 | signals[signal] = offset 266 | return {"name": tree[0], "size": tree[1], "signals": signals} 267 | 268 | def signal_encoding_types(self, tree): 269 | return ("signal_encoding_types", tree) 270 | 271 | def signal_encoding_type(self, tree): 272 | return {"name": tree[0], "values": tree[1:]} 273 | 274 | def signal_encoding_logical_value(self, tree): 275 | return {"type": "logical", "value": tree[0], "text": tree[1] if len(tree) > 1 else None} 276 | 277 | def signal_encoding_physical_value(self, tree): 278 | return {"type": "physical", "min": tree[0], "max": tree[1], "scale": tree[2], "offset": tree[3], "unit": tree[4] if len(tree) > 4 else None} 279 | 280 | def signal_encoding_bcd_value(self, tree): 281 | return {"type": "bcd"} 282 | 283 | def signal_encoding_ascii_value(self, tree): 284 | return {"type": "ascii"} 285 | 286 | def signal_encoding_text_value(self, tree): 287 | return tree[0][1:-1] 288 | 289 | def signal_representations(self, tree): 290 | return ("signal_representations", tree) 291 | 292 | def signal_representation_node(self, tree): 293 | return {"encoding": tree[0], "signals": tree[1:]} 294 | -------------------------------------------------------------------------------- /ldfparser/grammars/__init__.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=cyclic-import 2 | -------------------------------------------------------------------------------- /ldfparser/grammars/ldf.lark: -------------------------------------------------------------------------------- 1 | start: ldf 2 | 3 | ldf: (header_lin_description_file | header_protocol_version | header_language_version | header_speed | header_channel | header_file_revision | header_sig_byte_order_big_endian | header_sig_byte_order_little_endian | nodes | node_compositions | signals | diagnostic_signals | diagnostic_addresses | frames | sporadic_frames | event_triggered_frames | diagnostic_frames | node_attributes | schedule_tables | signal_groups | signal_encoding_types | signal_representations)* 4 | 5 | ldf_identifier: CNAME 6 | ldf_version: LIN_VERSION | ISO_VERSION | J2602_VERSION 7 | ldf_integer: C_INT 8 | ldf_float: C_FLOAT 9 | ldf_channel_name: ESCAPED_STRING 10 | 11 | // LIN 2.1 Specification, section 9.2.1 12 | header_lin_description_file: "LIN_description_file" ";" 13 | header_protocol_version: "LIN_protocol_version" "=" "\"" ldf_version "\"" ";" 14 | header_language_version: "LIN_language_version" "=" "\"" ldf_version "\"" ";" 15 | header_speed: "LIN_speed" "=" ldf_float "kbps" ";" 16 | header_channel: "Channel_name" "=" ldf_channel_name ";" 17 | 18 | // ISO1798 Specification 19 | header_file_revision: "LDF_file_revision" "=" ESCAPED_STRING ";" 20 | header_sig_byte_order_big_endian: "LIN_sig_byte_order_big_endian" ";" 21 | header_sig_byte_order_little_endian: "LIN_sig_byte_order_little_endian" ";" 22 | 23 | // LIN 2.1 Specification, section 9.2.2 24 | // SAE J2602-3_201001, SECTION 7.2 25 | nodes: "Nodes" "{" nodes_master? nodes_slaves? "}" 26 | nodes_master: "Master" ":" ldf_identifier "," ldf_float "ms" "," ldf_float "ms" ("," ldf_integer "bits" "," ldf_float "%")? ";" 27 | nodes_slaves: "Slaves" ":" ldf_identifier ("," ldf_identifier)* ";" 28 | 29 | // LIN 2.1 Specification, section 9.2.2.3 30 | node_compositions: "composite" "{" (node_compositions_configuration+) "}" 31 | node_compositions_configuration: "configuration" ldf_identifier "{" (node_compositions_composite+) "}" 32 | node_compositions_composite: ldf_identifier "{" ldf_identifier ("," ldf_identifier)* "}" 33 | 34 | // LIN 2.1 Specification, section 9.2.3 35 | signals: "Signals" "{" (signal_definition)+ "}" 36 | signal_definition: ldf_identifier ":" ldf_integer "," signal_default_value "," ldf_identifier ("," ldf_identifier)* ";" 37 | signal_default_value: signal_default_value_array | signal_default_value_single 38 | signal_default_value_single: ldf_integer 39 | signal_default_value_array: "{" ldf_integer ("," ldf_integer)* "}" 40 | 41 | // LIN 2.1 Specification, section 9.2.3.2 42 | diagnostic_signals: "Diagnostic_signals" "{" (diagnostic_signal_definition+) "}" 43 | diagnostic_signal_definition: ldf_identifier ":" ldf_integer "," signal_default_value ";" 44 | 45 | // LIN 2.1 Specification, section 9.2.4 46 | frames: "Frames" "{" [frame_definition]+ "}" 47 | frame_definition : ldf_identifier ":" ldf_integer "," ldf_identifier ("," ldf_integer)? "{" frame_signals "}" 48 | frame_signals: (frame_signal+) 49 | frame_signal: ldf_identifier "," ldf_integer ";" 50 | 51 | // LIN 2.1 Specification, section 9.2.4.2 52 | sporadic_frames: "Sporadic_frames" "{" (sporadic_frame_definition+) "}" 53 | sporadic_frame_definition: ldf_identifier ":" ldf_identifier ("," ldf_identifier)* ";" 54 | 55 | // LIN 2.1 Specification, section 9.2.4.3 56 | event_triggered_frames: "Event_triggered_frames" "{" (event_triggered_frame_definition+) "}" 57 | event_triggered_frame_definition: ldf_identifier ":" (ldf_identifier ",")? ldf_integer event_triggered_frame_definition_frames ";" 58 | event_triggered_frame_definition_frames: ("," ldf_identifier)+ 59 | 60 | // LIN 2.1 Specification, section 9.2.4.4 61 | diagnostic_frames: "Diagnostic_frames" "{" (diagnostic_frame_definition)+ "}" 62 | diagnostic_frame_definition: ldf_identifier ":" ldf_integer "{" diagnostic_frame_signals "}" 63 | diagnostic_frame_signals: (frame_signal+) 64 | 65 | // LIN 1.3 Specification, section 7.5 66 | diagnostic_addresses: "Diagnostic_addresses" "{" (diagnostic_address)* "}" 67 | diagnostic_address: ldf_identifier ":" ldf_integer ";" 68 | 69 | // LIN 2.1 Specification, section 9.2.2.2 70 | // response_tolerance: SAE J2602_1_201001, section 7.2.1 71 | node_attributes: "Node_attributes" "{" (node_definition*) "}" 72 | node_definition: ldf_identifier "{" (node_definition_protocol | node_definition_configured_nad | node_definition_initial_nad | node_definition_product_id | node_definition_response_error | node_definition_fault_state_signals | node_definition_p2_min | node_definition_st_min | node_definition_n_as_timeout | node_definition_n_cr_timeout | node_definition_configurable_frames | node_definition_response_tolerance | node_definition_wakeup_time | node_definition_poweron_time)* "}" 73 | node_definition_protocol: "LIN_protocol" "=" (["\"" ldf_version "\""] | ldf_version) ";" 74 | node_definition_configured_nad: "configured_NAD" "=" ldf_integer ";" 75 | node_definition_initial_nad: "initial_NAD" "=" ldf_integer ";" 76 | node_definition_product_id: "product_id" "=" ldf_integer "," ldf_integer ("," ldf_integer)? ";" 77 | node_definition_response_error: "response_error" "=" ldf_identifier ";" 78 | node_definition_fault_state_signals: "fault_state_signals" "=" ldf_identifier ("," ldf_identifier)* ";" 79 | node_definition_p2_min: "P2_min" "=" ldf_float "ms" ";" 80 | node_definition_st_min: "ST_min" "=" ldf_float "ms" ";" 81 | node_definition_n_as_timeout: "N_As_timeout" "=" ldf_float "ms" ";" 82 | node_definition_n_cr_timeout: "N_Cr_timeout" "=" ldf_float "ms" ";" 83 | node_definition_configurable_frames: node_definition_configurable_frames_20 | node_definition_configurable_frames_21 84 | node_definition_configurable_frames_20: "configurable_frames" "{" (ldf_identifier "=" ldf_integer ";")+ "}" 85 | node_definition_configurable_frames_21: "configurable_frames" "{" (ldf_identifier ";")+ "}" 86 | node_definition_response_tolerance: "response_tolerance" "=" ldf_float "%" ";" 87 | node_definition_wakeup_time: "wakeup_time" "=" ldf_float "ms" ";" 88 | node_definition_poweron_time: "poweron_time" "=" ldf_float "ms" ";" 89 | 90 | // LIN 2.1 Specification, section 9.2.5 91 | schedule_tables: "Schedule_tables" "{" (schedule_table_definition+) "}" 92 | schedule_table_definition: ldf_identifier "{" (schedule_table_entry+) "}" 93 | schedule_table_entry: schedule_table_command "delay" ldf_float "ms" ";" 94 | schedule_table_command: schedule_table_command_masterreq | schedule_table_command_slaveresp | schedule_table_command_assignnad | schedule_table_command_conditionalchangenad | schedule_table_command_datadump | schedule_table_command_saveconfiguration | schedule_table_command_assignframeidrange | schedule_table_command_assignframeid | schedule_table_command_unassignframeid | schedule_table_command_freeformat | schedule_table_command_frame 95 | schedule_table_command_masterreq: "MasterReq" 96 | schedule_table_command_slaveresp: "SlaveResp" 97 | schedule_table_command_assignnad: "AssignNAD" "{" ldf_identifier "}" 98 | schedule_table_command_conditionalchangenad: "ConditionalChangeNAD" "{" ldf_integer "," ldf_integer "," ldf_integer "," ldf_integer "," ldf_integer "," ldf_integer "}" 99 | schedule_table_command_datadump: "DataDump" "{" ldf_identifier "," ldf_integer "," ldf_integer "," ldf_integer "," ldf_integer "," ldf_integer "}" 100 | schedule_table_command_saveconfiguration: "SaveConfiguration" "{" ldf_identifier "}" 101 | schedule_table_command_assignframeidrange: "AssignFrameIdRange" "{" ldf_identifier "," ldf_integer ("," ldf_integer "," ldf_integer "," ldf_integer "," ldf_integer )? "}" 102 | schedule_table_command_assignframeid: "AssignFrameId" "{" ldf_identifier "," ldf_identifier "}" 103 | schedule_table_command_unassignframeid: "UnassignFrameId" "{" ldf_identifier "," ldf_identifier "}" 104 | schedule_table_command_freeformat: "FreeFormat" "{" ldf_integer "," ldf_integer "," ldf_integer "," ldf_integer "," ldf_integer "," ldf_integer "," ldf_integer "," ldf_integer "}" 105 | schedule_table_command_frame: ldf_identifier 106 | 107 | // LIN 2.1 Specification, section 9.2.3.3 108 | signal_groups: "Signal_groups" "{" (signal_group+) "}" 109 | signal_group: ldf_identifier ":" ldf_integer "{" (ldf_identifier "," ldf_integer ";")+ "}" 110 | 111 | // LIN 2.1 Specification, section 9.2.6.1 112 | signal_encoding_types: "Signal_encoding_types" "{" (signal_encoding_type+) "}" 113 | signal_encoding_type: ldf_identifier "{" (signal_encoding_logical_value | signal_encoding_physical_value | signal_encoding_bcd_value | signal_encoding_ascii_value)* "}" 114 | signal_encoding_logical_value: "logical_value" "," ldf_integer ("," signal_encoding_text_value)? ";" 115 | signal_encoding_physical_value: "physical_value" "," ldf_integer "," ldf_integer "," ldf_float "," ldf_float ("," signal_encoding_text_value)? ";" 116 | signal_encoding_bcd_value: "bcd_value" ";" 117 | signal_encoding_ascii_value: "ascii_value" ";" 118 | signal_encoding_text_value: ESCAPED_STRING 119 | 120 | // LIN 2.1 Specification, section 9.2.6.2 121 | signal_representations: "Signal_representation" "{" (signal_representation_node+) "}" 122 | signal_representation_node: ldf_identifier ":" ldf_identifier ("," ldf_identifier)* ";" 123 | 124 | C_INT: ("0x"HEXDIGIT+) | ("-"? INT) 125 | C_FLOAT: ("-"? INT ("." INT)?) (("e" | "E")("+" | "-")? INT)? 126 | LIN_VERSION: INT "." INT 127 | ISO_VERSION: "ISO17987" ":" INT 128 | J2602_VERSION: "J2602" "_" INT "_" INT "." INT 129 | 130 | ANY_SEMICOLON_TERMINATED_LINE: /.*;/ 131 | 132 | // Use common.CPP_COMMENT and common.C_COMMENT once lark-parser v0.10.2 is released 133 | CPP_COMMENT: /\/\/[^\n]*/ 134 | C_COMMENT: "/*" /.*?/s "*/" 135 | 136 | %ignore CPP_COMMENT 137 | %ignore C_COMMENT 138 | %ignore WS 139 | %ignore WS_INLINE 140 | 141 | %import common._STRING_INNER 142 | %import common.HEXDIGIT 143 | %import common.INT 144 | %import common.WORD 145 | %import common.CNAME 146 | %import common.ESCAPED_STRING 147 | %import common.SIGNED_NUMBER 148 | %import common.WS 149 | %import common.WS_INLINE 150 | -------------------------------------------------------------------------------- /ldfparser/lin.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utility classes for LIN objects 3 | """ 4 | from typing import Union 5 | 6 | 7 | class LinVersion: 8 | """ 9 | LinVersion represents the LIN protocol and LDF language versions 10 | """ 11 | 12 | def __init__(self, major: int, minor: int) -> None: 13 | self.major = major 14 | self.minor = minor 15 | 16 | @staticmethod 17 | def from_string(version: str) -> 'LinVersion': 18 | """ 19 | Creates a LinVersion object from the given string 20 | 21 | :Example: 22 | `LinVersion.create('2.1')` will return `LinVersion(major=2, minor=1)` 23 | 24 | :param version: Version 25 | :type version: str 26 | :returns: LIN version 27 | :rtype: LinVersion 28 | """ 29 | (major, minor) = version.split('.') 30 | 31 | return LinVersion(major=int(major), minor=int(minor)) 32 | 33 | def __str__(self) -> str: 34 | return f"{self.major}.{self.minor}" 35 | 36 | def __float__(self) -> float: 37 | # This function shall be removed once the properties on the LDF object 38 | # have been deprecated and removed 39 | return self.major + self.minor * 0.1 40 | 41 | def __eq__(self, o: object) -> bool: 42 | if isinstance(o, LinVersion): 43 | return self.major == o.major and self.minor == o.minor 44 | elif isinstance(o, J2602Version): 45 | return self.major == 2 and self.minor == 0 46 | return False 47 | 48 | def __gt__(self, o) -> bool: 49 | if isinstance(o, LinVersion): 50 | if self.major > o.major: 51 | return True 52 | if self.major == o.major and self.minor > o.minor: 53 | return True 54 | return False 55 | elif isinstance(o, J2602Version): 56 | return (self.major == 2 and self.minor > 0) or self.major > 2 57 | raise TypeError() 58 | 59 | def __lt__(self, o) -> bool: 60 | if isinstance(o, LinVersion): 61 | if self.major < o.major: 62 | return True 63 | if self.major == o.major and self.minor < o.minor: 64 | return True 65 | return False 66 | elif isinstance(o, J2602Version): 67 | return self.major < 2 68 | raise TypeError() 69 | 70 | def __ge__(self, o) -> bool: 71 | return not self.__lt__(o) 72 | 73 | def __le__(self, o) -> bool: 74 | return not self.__gt__(o) 75 | 76 | def __ne__(self, o: object) -> bool: 77 | return not self.__eq__(o) 78 | 79 | LIN_VERSION_1_3 = LinVersion(1, 3) 80 | LIN_VERSION_2_0 = LinVersion(2, 0) 81 | LIN_VERSION_2_1 = LinVersion(2, 1) 82 | LIN_VERSION_2_2 = LinVersion(2, 2) 83 | 84 | class Iso17987Version: 85 | 86 | def __init__(self, revision: int) -> None: 87 | self.revision = revision 88 | 89 | @staticmethod 90 | def from_string(version: str) -> 'Iso17987Version': 91 | (standard, revision) = version.split(':') 92 | 93 | return Iso17987Version(int(revision)) 94 | 95 | def __str__(self) -> str: 96 | return f"ISO17987:{self.revision}" 97 | 98 | def __eq__(self, o: object) -> bool: 99 | if isinstance(o, Iso17987Version): 100 | return self.revision == o.revision 101 | return False 102 | 103 | def __gt__(self, o) -> bool: 104 | if isinstance(o, Iso17987Version): 105 | return (self.revision > o.revision) 106 | if isinstance(o, LinVersion): 107 | return True 108 | raise TypeError() 109 | 110 | def __lt__(self, o) -> bool: 111 | if isinstance(o, Iso17987Version): 112 | return (self.revision < o.revision) 113 | if isinstance(o, LinVersion): 114 | return False 115 | raise TypeError() 116 | 117 | def __ge__(self, o) -> bool: 118 | return not self.__lt__(o) 119 | 120 | def __le__(self, o) -> bool: 121 | return not self.__gt__(o) 122 | 123 | def __ne__(self, o: object) -> bool: 124 | return not self.__eq__(o) 125 | 126 | ISO17987_2015 = Iso17987Version(2015) 127 | 128 | class J2602Version: 129 | def __init__(self, major, minor, part): 130 | """ 131 | Abstract the J2602 version. 132 | """ 133 | self.major = major 134 | self.minor = minor 135 | self.part = part 136 | 137 | @staticmethod 138 | def from_string(version: str) -> 'J2602Version': 139 | """ 140 | Create an instance from the version string. 141 | 142 | The version string J2602_1_2.0 will render: 143 | major=2, minor=0, part=1 144 | 145 | Support for J2602_1_2.0 is not implemented at this time. 146 | """ 147 | if "J2602" not in version: 148 | raise ValueError(f'{version} is not an SAE J2602 version.') 149 | 150 | version = version.replace("J2602_", '') 151 | (part, versions) = version.split('_') 152 | major, minor = [int(value) for value in versions.split('.')] 153 | if major == 1: 154 | return J2602Version(major=major, minor=minor, part=int(part)) 155 | 156 | raise ValueError(f'{version} is not supported yet.') 157 | 158 | def __str__(self) -> str: 159 | return f"J2602_{self.part}_{self.major}.{self.minor}" 160 | 161 | def __eq__(self, o: object) -> bool: 162 | """ 163 | According to J2602-3_202110, section 7.1.3: 164 | “J2602_1_1.0” -> J2602:2012 and earlier -> based on LIN 2.0 165 | “J2602_1_2.0” -> J2602:2021 -> based on ISO 17987:2016 166 | 167 | Therefore, 168 | “J2602_1_1.0” is considered equal to LinVersion(2, 0) 169 | 170 | """ 171 | if isinstance(o, J2602Version): 172 | return ( 173 | self.major == o.major and 174 | self.minor == o.minor and 175 | self.part == o.part 176 | ) 177 | elif isinstance(o, Iso17987Version): 178 | return False 179 | elif isinstance(o, LinVersion): 180 | return o == LIN_VERSION_2_0 181 | return False 182 | 183 | def __gt__(self, o) -> bool: 184 | if isinstance(o, J2602Version): 185 | return ( 186 | self.major > o.major or 187 | ( 188 | self.major == o.major and self.minor > o.minor 189 | ) 190 | ) 191 | if isinstance(o, Iso17987Version): 192 | return False 193 | if isinstance(o, LinVersion): 194 | return o < LIN_VERSION_2_0 195 | raise TypeError() 196 | 197 | def __lt__(self, o) -> bool: 198 | if isinstance(o, J2602Version): 199 | return ( 200 | self.major < o.major or 201 | ( 202 | self.major == o.major and self.minor < o.minor 203 | ) 204 | ) 205 | if isinstance(o, Iso17987Version): 206 | return True 207 | if isinstance(o, LinVersion): 208 | return o > LIN_VERSION_2_0 209 | raise TypeError() 210 | 211 | def __ge__(self, o) -> bool: 212 | return not self.__lt__(o) 213 | 214 | def __le__(self, o) -> bool: 215 | return not self.__gt__(o) 216 | 217 | def __ne__(self, o: object) -> bool: 218 | return not self.__eq__(o) 219 | 220 | def parse_lin_version(version: str) -> Union[LinVersion, Iso17987Version, J2602Version]: 221 | for version_class in [LinVersion, Iso17987Version, J2602Version]: 222 | try: 223 | return version_class.from_string(version) 224 | except (ValueError, IndexError): 225 | pass 226 | 227 | raise ValueError(f'{version} is not a valid LIN version.') 228 | -------------------------------------------------------------------------------- /ldfparser/node.py: -------------------------------------------------------------------------------- 1 | """ 2 | LIN Node utilities 3 | """ 4 | from typing import List, TYPE_CHECKING, Union 5 | 6 | if TYPE_CHECKING: 7 | from .frame import LinFrame 8 | from .signal import LinSignal 9 | from .lin import LinVersion, Iso17987Version, J2602Version 10 | 11 | LIN_SUPPLIER_ID_WILDCARD = 0x7FFF 12 | LIN_FUNCTION_ID_WILDCARD = 0xFFFF 13 | 14 | class LinProductId: 15 | """ 16 | LinProductId identifies a node's manufacturer and product 17 | 18 | :param supplier_id: a number uniquely identifying the manufacturer of the node 19 | :type supplier_id: int 20 | :param function_id: a number assigned by the manufacturer that identifies the product 21 | :type function_id: int 22 | :param variant: an optional number identifying a variant of the product 23 | :type variant: int 24 | """ 25 | 26 | def __init__(self, supplier_id: int, function_id: int, variant: int = 0): 27 | self.supplier_id: int = supplier_id 28 | self.function_id: int = function_id 29 | self.variant: int = variant 30 | 31 | @staticmethod 32 | def create(supplier_id: int, function_id: int, variant: int = 0): 33 | """ 34 | Creates a new LinProductId object and validates it's fields 35 | """ 36 | if supplier_id < 0 or supplier_id > 0x7FFF: 37 | raise ValueError(f"{supplier_id} is invalid, must be 0-32767") 38 | if function_id < 0 or function_id > 0xFFFF: 39 | raise ValueError(f"{function_id} is invalid, must be 0-65535 (16bit)") 40 | if variant < 0 or variant > 0xFF: 41 | raise ValueError(f"{variant} is invalid, must be 0-255 (8bit)") 42 | 43 | return LinProductId(supplier_id, function_id, variant) 44 | 45 | def __str__(self) -> str: 46 | return f"LinProductId(supplier=0x{self.supplier_id:04x},"\ 47 | f"function=0x{self.function_id:04x},variant={self.variant})" 48 | 49 | class LinNode: 50 | """ 51 | Abstract LIN Node class 52 | 53 | Contains the following common attributes: 54 | 55 | :param name: Name of the node 56 | :type name: str 57 | :param subscribes_to: LIN signals that the node is subscribed to 58 | :type subscribes_to: List[LinSignal] 59 | :param publishes: LIN signals that the node is publishing 60 | :type publishes: List[LinSignal] 61 | :param publishes_frames: LIN frames that the node is publishing 62 | :type publishes_frames: List[LinFrame] 63 | """ 64 | 65 | def __init__(self, name: str): 66 | self.name = name 67 | self.subscribes_to: List['LinSignal'] = [] 68 | self.publishes: List['LinSignal'] = [] 69 | self.publishes_frames: List['LinFrame'] = [] 70 | 71 | class LinMaster(LinNode): 72 | """ 73 | LinMaster is a LinNode that controls communication on the network 74 | 75 | :param timebase: LIN network timebase in seconds 76 | :type timebase: float 77 | :param jitter: LIN network jitter in seconds 78 | :type jitter: float 79 | :param max_header_length: The maximum number of bits of the header length 80 | :type max_header_length: int 81 | :param response_tolerance: The value between 0.0 - 1.0 that represents the 82 | percentage of the frame response tolerance. 83 | :type response_tolerance: float 84 | """ 85 | 86 | def __init__( 87 | self, 88 | name: str, 89 | timebase: float, 90 | jitter: float, 91 | max_header_length: int, 92 | response_tolerance: float, 93 | ): 94 | super().__init__(name) 95 | self.timebase: float = timebase 96 | self.jitter: float = jitter 97 | self.max_header_length: int = max_header_length 98 | self.response_tolerance: float = response_tolerance 99 | 100 | 101 | class LinSlave(LinNode): 102 | """ 103 | LinSlave is a LinNode that is listens to frame headers and publishes signals 104 | 105 | :param lin_protocol: LIN protocol version that the node conforms with 106 | :type lin_protocol: LinVersion 107 | :param configured_nad: Network address of the node after network setup 108 | :type configured_nad: int 109 | :param initial_nad: Initial network address of the node 110 | :type intial_nad: int 111 | :param product_id: Product identifier of the node 112 | :type product_id: LinProductId 113 | :param response_error: A signal that the node uses to indicate frame errors 114 | :type response_error: LinSignal 115 | :param fault_state_signals: Signals that the node uses to indicate operating errors 116 | :type fault_state_signals: List[LinSignal] 117 | :param p2_min: 118 | :type p2_min: 119 | :param st_min: 120 | :type st_min: 121 | :param n_as_timeout: 122 | :type n_as_timeout: 123 | :param n_cr_timeout: 124 | :type n_cr_timeout: 125 | :param configurable_frames: 126 | :type configurable_frames: 127 | :param response_tolerance: The value between 0.0 - 1.0 that represents the 128 | percentage of the frame response tolerance. For example, 0.4 for 40%. 129 | :type response_tolerance: float 130 | :param wakeup_time: The time in seconds a responder-node requires to recover from LIN sleep to normal 131 | communication state 132 | :type wakeup_time: float 133 | :param poweron_time: The time in seconds a responder-node requires to recover from power down to LIN 134 | normal communication state 135 | :type poweron_time: float 136 | """ 137 | 138 | def __init__(self, name: str) -> None: 139 | super().__init__(name) 140 | self.lin_protocol: Union[LinVersion, Iso17987Version, J2602Version] = None 141 | self.configured_nad: int = None 142 | self.initial_nad: int = None 143 | self.product_id: LinProductId = None 144 | self.response_error: 'LinSignal' = None 145 | self.fault_state_signals: List['LinSignal'] = [] 146 | self.p2_min: float = 0.05 147 | self.st_min: float = 0 148 | self.n_as_timeout: float = 1 149 | self.n_cr_timeout: float = 1 150 | self.configurable_frames = {} 151 | self.response_tolerance: float = None 152 | self.wakeup_time: float = None 153 | self.poweron_time: float = None 154 | 155 | class LinNodeCompositionConfiguration: 156 | 157 | def __init__(self, name: str) -> None: 158 | self.name: str = name 159 | self.compositions: List[LinNodeComposition] = [] 160 | 161 | class LinNodeComposition: 162 | 163 | def __init__(self, name: str) -> None: 164 | self.name: str = name 165 | self.nodes: List[LinSlave] = [] 166 | -------------------------------------------------------------------------------- /ldfparser/save.py: -------------------------------------------------------------------------------- 1 | """ 2 | Module contains functions for saving LDF objects 3 | """ 4 | import os 5 | import sys 6 | import jinja2 7 | import argparse 8 | from typing import Union 9 | 10 | from ldfparser.ldf import LDF 11 | from ldfparser.parser import parse_ldf 12 | 13 | def save_ldf(ldf: LDF, 14 | output_path: Union[str, bytes, os.PathLike], 15 | template_path: Union[str, bytes, os.PathLike] = None) -> None: 16 | """ 17 | Saves as an LDF object as an `.ldf` file 18 | 19 | :param ldf: LDF object 20 | :type ldf: LDF 21 | :param output_path: Path where the file will be saved 22 | :type output_path: PathLike 23 | :param template_path: Path where the template is located, if not specified then an internal 24 | template will be used 25 | :type template_path: PathLike 26 | """ 27 | if template_path is None: 28 | template_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'templates', 'ldf.jinja2')) 29 | 30 | with open(template_path, 'r') as file: 31 | template = jinja2.Template(file.read()) 32 | ldf_content = template.render(ldf=ldf) 33 | 34 | with open(output_path, 'w+') as ldf_file: 35 | ldf_file.writelines(ldf_content) 36 | 37 | def main(): 38 | parser = argparse.ArgumentParser() 39 | parser.add_argument('-f', '--ldf', required=True) 40 | parser.add_argument('-o', '--output', required=True) 41 | parser.add_argument('-t', '--template', required=False) 42 | args = parser.parse_args(sys.argv[1:]) 43 | 44 | ldf = parse_ldf(args.ldf) 45 | save_ldf(ldf, args.output, args.template) 46 | 47 | if __name__ == '__main__': 48 | main() 49 | -------------------------------------------------------------------------------- /ldfparser/schedule.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import List, TYPE_CHECKING 3 | 4 | if TYPE_CHECKING: 5 | from .frame import LinFrame 6 | from .node import LinNode 7 | 8 | class ScheduleTable(): 9 | 10 | def __init__(self, name: str) -> None: 11 | self.name = name 12 | self.schedule: List[ScheduleTableEntry] = [] 13 | 14 | class ScheduleTableEntry(): 15 | 16 | def __init__(self) -> None: 17 | self.delay: float = 0.0 18 | 19 | class LinFrameEntry(ScheduleTableEntry): 20 | 21 | def __init__(self) -> None: 22 | super().__init__() 23 | self.frame: 'LinFrame' = None 24 | 25 | class MasterRequestEntry(ScheduleTableEntry): 26 | 27 | def __init__(self) -> None: 28 | super().__init__() 29 | 30 | class SlaveResponseEntry(ScheduleTableEntry): 31 | 32 | def __init__(self) -> None: 33 | super().__init__() 34 | 35 | class AssignNadEntry(ScheduleTableEntry): 36 | 37 | def __init__(self) -> None: 38 | super().__init__() 39 | self.node: 'LinNode' = None 40 | 41 | class AssignFrameIdRangeEntry(ScheduleTableEntry): 42 | 43 | def __init__(self) -> None: 44 | super().__init__() 45 | self.node: 'LinNode' = None 46 | self.frame_index: int = 0 47 | self.pids: List[int] = [] 48 | 49 | class ConditionalChangeNadEntry(ScheduleTableEntry): 50 | 51 | def __init__(self) -> None: 52 | super().__init__() 53 | self.nad: int = 0 54 | self.id: int = 0 55 | self.byte: int = 0 56 | self.mask: int = 0 57 | self.inv: int = 0 58 | self.new_nad: int = 0 59 | 60 | class DataDumpEntry(ScheduleTableEntry): 61 | 62 | def __init__(self) -> None: 63 | super().__init__() 64 | self.node: 'LinNode' = None 65 | self.data: List[int] = [] 66 | 67 | class SaveConfigurationEntry(ScheduleTableEntry): 68 | 69 | def __init__(self) -> None: 70 | super().__init__() 71 | self.node: 'LinNode' = None 72 | 73 | class AssignFrameIdEntry(ScheduleTableEntry): 74 | 75 | def __init__(self) -> None: 76 | super().__init__() 77 | self.node: 'LinNode' = None 78 | self.frame: 'LinFrame' = None 79 | 80 | class UnassignFrameIdEntry(ScheduleTableEntry): 81 | 82 | def __init__(self) -> None: 83 | super().__init__() 84 | self.node: 'LinNode' = None 85 | self.frame: 'LinFrame' = None 86 | 87 | class FreeFormatEntry(ScheduleTableEntry): 88 | 89 | def __init__(self) -> None: 90 | super().__init__() 91 | self.data: List[int] = [] 92 | -------------------------------------------------------------------------------- /ldfparser/signal.py: -------------------------------------------------------------------------------- 1 | """ 2 | LIN Signal 3 | """ 4 | from typing import List, Union, TYPE_CHECKING 5 | 6 | if TYPE_CHECKING: 7 | from .node import LinNode 8 | from .encoding import LinSignalEncodingType 9 | from .frame import LinUnconditionalFrame 10 | 11 | class LinSignal: 12 | """ 13 | LinSignal describes values contained inside LinFrames 14 | 15 | :param name: Name of the signal 16 | :type name: str 17 | :param width: Width of the signal in bits 18 | :type width: int 19 | :param init_value: Initial or default value of the signal 20 | :type init_value: int or List[int] in case of array signals 21 | :param publisher: Node that publishes the signal 22 | :type publisher: LinNode 23 | :param subscribers: Nodes that subscribe to the signal 24 | :type subscribers: List[LinNode] 25 | """ 26 | 27 | def __init__(self, name: str, width: int, init_value: Union[int, List[int]]): 28 | self.name: str = name 29 | self.width: int = width 30 | self.init_value: Union[int, List[int]] = init_value 31 | self.publisher: 'LinNode' = None 32 | self.subscribers: List['LinNode'] = [] 33 | self.encoding_type: 'LinSignalEncodingType' = None 34 | self.frame: 'LinUnconditionalFrame' = None # for compatibility reasons this is set 35 | # when the signal is added to only one frame 36 | self.frames: List['LinUnconditionalFrame'] = [] 37 | 38 | def __eq__(self, o: object) -> bool: 39 | if isinstance(o, LinSignal): 40 | return self.name == o.name 41 | return False 42 | 43 | def __ne__(self, o: object) -> bool: 44 | return not self == o 45 | 46 | def __hash__(self) -> int: 47 | return hash((self.name)) 48 | 49 | def is_array(self): 50 | """ 51 | Returns whether the Signal is array type 52 | 53 | :returns: `True` if the signal is an array 54 | :rtype: bool 55 | """ 56 | return isinstance(self.init_value, List) 57 | 58 | @staticmethod 59 | def create(name: str, width: int, init_value: Union[int, List[int]]): 60 | """ 61 | Creates a LinSignal object and validates it's fields 62 | """ 63 | if isinstance(init_value, List): 64 | if width % 8 != 0: 65 | raise ValueError(f"array signal {name}:{width} must have a 8 * n width (8, 16, ..)") 66 | if width < 8 or width > 64: 67 | raise ValueError(f"array signal {name}:{width} must be 8-64bits long") 68 | if len(init_value) != width / 8: 69 | raise ValueError(f"array signal {name}:{width} init value doesn't match the width") 70 | if isinstance(init_value, int) and (width < 1 or width > 16): 71 | raise ValueError(f"scalar signal {name}:{width} must be 1-16 bits long") 72 | return LinSignal(name, width, init_value) 73 | -------------------------------------------------------------------------------- /ldfparser/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c4deszes/ldfparser/2de76f53d1d9c3be536f24d4239f6cd476e3d87a/ldfparser/templates/__init__.py -------------------------------------------------------------------------------- /ldfparser/templates/ldf.jinja2: -------------------------------------------------------------------------------- 1 | /** 2 | * This file was exported using ldfparser Python package 3 | */ 4 | LIN_description_file; 5 | LIN_protocol_version = "{{ ldf.get_protocol_version() }}"; 6 | LIN_language_version = "{{ ldf.get_language_version() }}"; 7 | LIN_speed = {{ (ldf.get_baudrate() / 1000.0) | float }} kbps; 8 | {%- if ldf.get_channel() %} 9 | Channel_name = "{{ ldf.get_channel() }}"; 10 | {%- endif %} 11 | 12 | Nodes { 13 | {%- if ldf.get_master() %} 14 | Master: {{ ldf.get_master().name }}, {{ ldf.get_master().timebase * 1000 | float}} ms, {{ ldf.get_master().jitter * 1000 }} ms{%- if ldf.get_master().max_header_length is not none %}, {{ldf.get_master().max_header_length}} bits{%- endif %}{%- if ldf.get_master().response_tolerance is not none %}, {{ldf.get_master().response_tolerance * 100}} %{%- endif %}; 15 | {%- endif %} 16 | {%- if ldf.get_slaves() %} 17 | Slaves: {%- for slave in ldf.get_slaves() %} {{ slave.name }}{%- if not loop.last %},{% endif -%}{%- endfor -%}; 18 | {%- endif %} 19 | } 20 | 21 | {%- if ldf.get_language_version().major == 2 or "Iso17987Version" in ldf.get_language_version().__class__.__name__ or "J2602" in ldf.get_language_version().__class__.__name__ %} 22 | Node_attributes { 23 | {%- for slave in ldf.get_slaves() %} 24 | {{slave.name}} { 25 | LIN_protocol = "{{slave.lin_protocol}}"; 26 | configured_NAD = {{slave.configured_nad}}; 27 | initial_NAD = {{slave.initial_nad}}; 28 | {%- if slave.product_id %} 29 | product_id = {{slave.product_id.supplier_id}}, {{slave.product_id.function_id -}} 30 | {%- if slave.product_id.variant -%} 31 | , {{slave.product_id.variant -}} 32 | {%- endif %}; 33 | {%- endif %} 34 | {%- if slave.response_error %} 35 | response_error = {{slave.response_error.name}}; 36 | {%- endif %} 37 | P2_min = {{slave.p2_min * 1000}} ms; 38 | ST_min = {{slave.st_min * 1000}} ms; 39 | N_As_timeout = {{slave.n_as_timeout * 1000}} ms; 40 | N_Cr_timeout = {{slave.n_cr_timeout * 1000}} ms; 41 | {%- if slave.configurable_frames.items() | length > 0 %} 42 | configurable_frames { 43 | {%- for (id, frame) in slave.configurable_frames.items() %} 44 | {{frame.name}}{% if slave.lin_protocol == "2.0" %} = {{id}}{% endif -%}; 45 | {%- endfor %} 46 | } 47 | {%- endif %} 48 | {%- if slave.response_tolerance %} 49 | response_tolerance = {{slave.response_tolerance * 100}} %; 50 | {%- endif %} 51 | } 52 | {%- endfor %} 53 | } 54 | {%- endif %} 55 | 56 | Signals { 57 | {%- for signal in ldf.get_signals() %} 58 | {{signal.name}}: {{signal.width}}, 59 | {%- if signal.is_array() %} { 60 | {%- for value in signal.init_value -%} 61 | {{value}} {%- if not loop.last %},{% endif -%} 62 | {%- endfor %} } 63 | {%- else %} {{signal.init_value}} 64 | {%- endif -%}, {{ signal.publisher.name -}} 65 | {%- for subscriber in signal.subscribers %}, {{ subscriber.name }} 66 | {%- endfor -%}; 67 | {%- endfor %} 68 | } 69 | 70 | Frames { 71 | {%- for frame in ldf.get_unconditional_frames() %} 72 | {{frame.name}}: {{frame.frame_id}}, {{frame.publisher.name}}, {{frame.length}} { 73 | {%- for signal in frame.signal_map %} 74 | {{signal[1].name}}, {{signal[0]}}; 75 | {%- endfor %} 76 | } 77 | {%- endfor %} 78 | } 79 | 80 | {%- if ldf.get_event_triggered_frames() | length > 0 %} 81 | Event_triggered_frames { 82 | {%- for frame in ldf.get_event_triggered_frames() %} 83 | {{frame.name}}: {{frame.collision_resolving_schedule_table.name}}, {{frame.frame_id}}, 84 | {%- for unconditional in frame.frames -%} 85 | {{unconditional.name}}{%- if not loop.last %}, {% endif -%} 86 | {%- endfor -%}; 87 | {%- endfor %} 88 | } 89 | {%- endif %} 90 | 91 | {%- if ldf.get_sporadic_frames() | length > 0 %} 92 | Sporadic_frames { 93 | {%- for frame in ldf.get_sporadic_frames() %} 94 | {{frame.name}}: 95 | {%- for unconditional in frame.frames -%} 96 | {{unconditional.name}}{%- if not loop.last %}, {% endif -%} 97 | {%- endfor -%}; 98 | {%- endfor %} 99 | } 100 | {%- endif %} 101 | 102 | Schedule_tables { 103 | {%- for table in ldf.get_schedule_tables() %} 104 | {{table.name}} { 105 | {%- for entry in table.schedule %} 106 | {%- if entry.__class__.__name__ == 'LinFrameEntry' %} 107 | {{ entry.frame.name }} delay {{entry.delay * 1000}} ms; 108 | {%- elif entry.__class__.__name__ == 'MasterRequestEntry' %} 109 | MasterReq delay {{entry.delay * 1000}} ms; 110 | {%- elif entry.__class__.__name__ == 'SlaveResponseEntry' %} 111 | SlaveResp delay {{entry.delay * 1000}} ms; 112 | {%- elif entry.__class__.__name__ == 'AssignNadEntry' %} 113 | AssignNAD { {{ entry.node.name }} } delay {{entry.delay * 1000}} ms; 114 | {%- elif entry.__class__.__name__ == 'AssignFrameIdRangeEntry' %} 115 | AssignFrameIdRange { {{ entry.node.name }}, {{entry.frame_index}} 116 | {%- if entry.pids | length > 0 -%} 117 | , {{entry.pids[0]}}, {{entry.pids[1]}}, {{entry.pids[2]}}, {{entry.pids[3]}} 118 | {%- endif -%} 119 | } delay {{entry.delay * 1000}} ms; 120 | {%- elif entry.__class__.__name__ == 'ConditionalChangeNadEntry' %} 121 | ConditionalChangeNAD { {{entry.nad}}, {{entry.id}}, {{entry.byte}}, {{entry.mask}}, {{entry.inv}}, {{entry.new_nad}} } delay {{entry.delay * 1000}} ms; 122 | {%- elif entry.__class__.__name__ == 'DataDumpEntry' %} 123 | DataDump { {{ entry.node.name }}, {{entry.data[0]}}, {{entry.data[1]}}, {{entry.data[2]}}, {{entry.data[3]}}, {{entry.data[4]}} } delay {{entry.delay * 1000}} ms; 124 | {%- elif entry.__class__.__name__ == 'SaveConfigurationEntry' %} 125 | SaveConfiguration { {{ entry.node.name }} } delay {{entry.delay * 1000}} ms; 126 | {%- elif entry.__class__.__name__ == 'AssignFrameIdEntry' %} 127 | AssignFrameId { {{ entry.node.name }}, {{entry.frame.name}} } delay {{entry.delay * 1000}} ms; 128 | {%- elif entry.__class__.__name__ == 'UnassignFrameIdEntry' %} 129 | UnassignFrameId { {{ entry.node.name }}, {{entry.frame.name}} } delay {{entry.delay * 1000}} ms; 130 | {%- elif entry.__class__.__name__ == 'FreeFormatEntry' %} 131 | FreeFormat { {{entry.data[0]}}, {{entry.data[1]}}, {{entry.data[2]}}, {{entry.data[3]}}, {{entry.data[4]}}, {{entry.data[5]}}, {{entry.data[6]}}, {{entry.data[7]}} } delay {{entry.delay * 1000}} ms; 132 | {%- endif %} 133 | {%- endfor %} 134 | } 135 | {%- endfor %} 136 | } 137 | 138 | {%- if ldf.get_signal_encoding_types() | length > 0 %} 139 | Signal_encoding_types { 140 | {%- for encoder in ldf.get_signal_encoding_types() %} 141 | {{encoder.name}} { 142 | {%- for converter in encoder.get_converters() %} 143 | {%- if converter.__class__.__name__ == 'LogicalValue' %} 144 | logical_value, {{converter.phy_value}} 145 | {%- if converter.info -%} 146 | , "{{converter.info}}" 147 | {%- endif -%} 148 | ; 149 | {%- elif converter.__class__.__name__ == 'PhysicalValue' %} 150 | physical_value, {{converter.phy_min}}, {{converter.phy_max}}, {{converter.scale}}, {{converter.offset}} 151 | {%- if converter.unit -%} 152 | , "{{converter.unit}}" 153 | {%- endif -%} 154 | ; 155 | {%- elif converter.__class__.__name__ == 'BCDValue' %} 156 | bcd_value; 157 | {%- elif converter.__class__.__name__ == 'ASCIIValue' %} 158 | ascii_value; 159 | {%- endif %} 160 | {%- endfor %} 161 | } 162 | {%- endfor %} 163 | } 164 | 165 | Signal_representation { 166 | {%- for encoder in ldf.get_signal_encoding_types() %} 167 | {%- if encoder.get_signals() | length > 0 %} 168 | {{encoder.name}}: {% for signal in encoder.get_signals() -%} 169 | {{signal.name}}{%- if not loop.last %}, {% endif -%} 170 | {%- endfor -%} 171 | ; 172 | {%- endif %} 173 | {%- endfor %} 174 | } 175 | {%- endif %} -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | unit: indicates that the function tests a software unit 4 | integration: indicates that the function tests multiple software units 5 | snapshot: indicates that the test result is compared to previous version 6 | performance: indicates that the test measures performance 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | version = 0.26.0 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | setup( 6 | name='ldfparser', 7 | author="Balazs Eszes", 8 | author_email="c4deszes@gmail.com", 9 | description="LDF Language support for Python", 10 | long_description=long_description, 11 | long_description_content_type="text/markdown", 12 | url="https://github.com/c4deszes/ldfparser", 13 | packages=find_packages(exclude=["tests"]), 14 | package_data={'': ['*.lark']}, 15 | license='MIT', 16 | keywords=['LIN', 'LDF'], 17 | install_requires=['lark>=1,<2', 'bitstruct', 'jinja2'], 18 | extras_require={ 19 | 'dev': [ 20 | # Packaging 21 | "setuptools", 22 | "wheel", 23 | "twine", 24 | # Testing 25 | "pytest", 26 | "pytest-cov", 27 | "pytest-benchmark", 28 | "jsonschema", 29 | # Linting 30 | "pylint", 31 | "flake8" 32 | ] 33 | }, 34 | python_requires='!=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4', 35 | py_modules=['lin', 'parser'], 36 | entry_points={ 37 | 'console_scripts': ['ldfparser=ldfparser.cli:main'] 38 | }, 39 | classifiers=[ 40 | "Programming Language :: Python :: 3.6", 41 | "Programming Language :: Python :: 3.7", 42 | "Programming Language :: Python :: 3.8", 43 | "Programming Language :: Python :: 3.9", 44 | "License :: OSI Approved :: MIT License", 45 | "Operating System :: OS Independent", 46 | ], 47 | project_urls={ 48 | "Documentation": "https://c4deszes.github.io/ldfparser/", 49 | "Source Code": "https://github.com/c4deszes/ldfparser", 50 | } 51 | ) 52 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/c4deszes/ldfparser/2de76f53d1d9c3be536f24d4239f6cd476e3d87a/tests/__init__.py -------------------------------------------------------------------------------- /tests/ldf/iso17987.ldf: -------------------------------------------------------------------------------- 1 | /*************************************************************************************/ 2 | // 3 | // Description: LIN Description file created using Vector's DaVinci Network Designer 4 | // Created: 04 Feb 2009 15:10:34 5 | // Version: 0.2 6 | // 7 | /*************************************************************************************/ 8 | 9 | LIN_description_file; 10 | LIN_protocol_version = "ISO17987:2015"; 11 | LIN_language_version = "ISO17987:2015"; 12 | LIN_speed = 19.2 kbps; 13 | LDF_file_revision = "14.23.01"; 14 | LIN_sig_byte_order_big_endian; 15 | 16 | Nodes { 17 | Master: VectorMasterNode, 1 ms, 0.1 ms ; 18 | Slaves: VectorSlave_ISO, VectorSlave2_0 ; 19 | } 20 | 21 | 22 | Signals { 23 | MotorLinError: 1, 0, VectorSlave_ISO, VectorMasterNode ; 24 | MotorLinError_2: 1, 0, VectorSlave2_0, VectorMasterNode ; 25 | MotorTemp: 8, 0, VectorSlave_ISO, VectorMasterNode ; 26 | MotorTemp_2: 8, 0, VectorSlave2_0, VectorMasterNode ; 27 | sig_MotorQuery1: 40, {5, 4, 3, 2, 1}, VectorMasterNode, VectorSlave_ISO ; 28 | sig_MotorQuery1_2: 8, 5, VectorMasterNode, VectorSlave2_0 ; 29 | sigMotorState1: 8, 0, VectorSlave_ISO, VectorMasterNode ; 30 | sigMotorState1_2: 8, 0, VectorSlave2_0, VectorMasterNode ; 31 | signal1: 16, 16, VectorMasterNode, VectorSlave_ISO ; 32 | signal1_2: 16, 16, VectorMasterNode, VectorSlave2_0 ; 33 | } 34 | 35 | Diagnostic_signals { 36 | MasterReqB0: 8, 0 ; 37 | MasterReqB1: 8, 0 ; 38 | MasterReqB2: 8, 0 ; 39 | MasterReqB3: 8, 0 ; 40 | MasterReqB4: 8, 0 ; 41 | MasterReqB5: 8, 0 ; 42 | MasterReqB6: 8, 0 ; 43 | MasterReqB7: 8, 0 ; 44 | SlaveRespB0: 8, 0 ; 45 | SlaveRespB1: 8, 0 ; 46 | SlaveRespB2: 8, 0 ; 47 | SlaveRespB3: 8, 0 ; 48 | SlaveRespB4: 8, 0 ; 49 | SlaveRespB5: 8, 0 ; 50 | SlaveRespB6: 8, 0 ; 51 | SlaveRespB7: 8, 0 ; 52 | } 53 | 54 | 55 | 56 | Frames { 57 | MotorControl: 4, VectorMasterNode, 2 { 58 | signal1, 0 ; 59 | } 60 | MotorControl_2: 6, VectorMasterNode, 2 { 61 | signal1_2, 0 ; 62 | } 63 | MotorQuery: 5, VectorMasterNode, 5 { 64 | sig_MotorQuery1, 0 ; 65 | } 66 | MotorQuery_2: 7, VectorMasterNode, 1 { 67 | sig_MotorQuery1_2, 0 ; 68 | } 69 | MotorState_Cycl: 0, VectorSlave_ISO, 6 { 70 | MotorTemp, 8 ; 71 | MotorLinError, 40 ; 72 | } 73 | MotorState_Cycl_2: 1, VectorSlave2_0, 6 { 74 | MotorTemp_2, 8 ; 75 | MotorLinError_2, 40 ; 76 | } 77 | MotorState_Event: 2, VectorSlave_ISO, 3 { 78 | sigMotorState1, 8 ; 79 | } 80 | MotorState_Event_2: 3, VectorSlave2_0, 3 { 81 | sigMotorState1_2, 8 ; 82 | } 83 | } 84 | 85 | 86 | Event_triggered_frames { 87 | ETF_MotorState_Cycl: CollisionResolver1, 55, MotorState_Cycl, MotorState_Cycl_2 ; 88 | ETF_MotorState_Event: CollisionResolver2, 56, MotorState_Event, MotorState_Event_2 ; 89 | } 90 | 91 | Diagnostic_frames { 92 | MasterReq: 0x3c { 93 | MasterReqB0, 0 ; 94 | MasterReqB1, 8 ; 95 | MasterReqB2, 16 ; 96 | MasterReqB3, 24 ; 97 | MasterReqB4, 32 ; 98 | MasterReqB5, 40 ; 99 | MasterReqB6, 48 ; 100 | MasterReqB7, 56 ; 101 | } 102 | SlaveResp: 0x3d { 103 | SlaveRespB0, 0 ; 104 | SlaveRespB1, 8 ; 105 | SlaveRespB2, 16 ; 106 | SlaveRespB3, 24 ; 107 | SlaveRespB4, 32 ; 108 | SlaveRespB5, 40 ; 109 | SlaveRespB6, 48 ; 110 | SlaveRespB7, 56 ; 111 | } 112 | } 113 | 114 | Node_attributes { 115 | VectorSlave_ISO{ 116 | LIN_protocol = "ISO17987:2015" ; 117 | configured_NAD = 0x5 ; 118 | initial_NAD = 0x5 ; 119 | product_id = 0x1e, 0x2, 1 ; 120 | response_error = MotorLinError ; 121 | P2_min = 50 ms ; 122 | ST_min = 0 ms ; 123 | configurable_frames { 124 | MotorControl; 125 | MotorQuery; 126 | MotorState_Cycl; 127 | MotorState_Event; 128 | ETF_MotorState_Cycl; 129 | ETF_MotorState_Event; 130 | } 131 | } 132 | VectorSlave2_0{ 133 | LIN_protocol = "2.0" ; 134 | configured_NAD = 0x1 ; 135 | product_id = 0x1e, 0x1, 0 ; 136 | response_error = MotorLinError_2 ; 137 | P2_min = 50 ms ; 138 | ST_min = 0 ms ; 139 | configurable_frames { 140 | MotorControl_2 = 0x1234 ; 141 | MotorQuery_2 = 0x1999 ; 142 | MotorState_Cycl_2 = 0x2222 ; 143 | MotorState_Event_2 = 0x2333 ; 144 | ETF_MotorState_Event = 0x4444 ; 145 | ETF_MotorState_Cycl = 0x4567 ; 146 | } 147 | } 148 | } 149 | 150 | Schedule_tables { 151 | InitTable { 152 | MotorQuery delay 7 ms ; 153 | MotorQuery_2 delay 7 ms ; 154 | MotorControl_2 delay 10 ms ; 155 | MotorControl delay 10 ms ; 156 | MotorState_Cycl delay 10 ms ; 157 | MotorState_Cycl_2 delay 10 ms ; 158 | MotorState_Event delay 6 ms ; 159 | MotorState_Event_2 delay 6 ms ; 160 | } 161 | ETF_Table { 162 | ETF_MotorState_Cycl delay 20 ms ; 163 | ETF_MotorState_Event delay 20 ms ; 164 | } 165 | CollisionResolver1 { 166 | MotorState_Cycl delay 10 ms ; 167 | MotorState_Cycl_2 delay 10 ms ; 168 | } 169 | CollisionResolver2 { 170 | MotorState_Event delay 10 ms ; 171 | MotorState_Event_2 delay 10 ms ; 172 | } 173 | Table4 { 174 | AssignNAD { VectorSlave_ISO } delay 10 ms ; 175 | SlaveResp delay 10 ms ; 176 | } 177 | } 178 | 179 | Signal_encoding_types { 180 | encError { 181 | logical_value, 0, "No Error" ; 182 | logical_value, 1, "Response Error" ; 183 | } 184 | encoding1 { 185 | physical_value, 0, 200, 1.000, 0.000, "temperature" ; 186 | logical_value, 255, "invalid" ; 187 | } 188 | encState { 189 | physical_value, 0, 200, 0.500, -20.000, "" ; 190 | } 191 | encTemperature { 192 | physical_value, 0, 200, 0.500, -20.000, "Degree" ; 193 | } 194 | } 195 | 196 | Signal_representation { 197 | encError: MotorLinError ; 198 | encoding1: signal1 ; 199 | encState: sigMotorState1 ; 200 | encTemperature: MotorTemp ; 201 | } 202 | 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /tests/ldf/j2602_1.ldf: -------------------------------------------------------------------------------- 1 | /* 2 | * Description: The LIN description file for the example program 3 | */ 4 | 5 | LIN_description_file ; 6 | LIN_protocol_version = "J2602_1_1.0"; 7 | LIN_language_version = "J2602_3_1.0"; 8 | LIN_speed = 19.2 kbps; 9 | 10 | Nodes { 11 | Master: CEM, 5 ms, 0.1 ms, 24 bits, 30 % ; 12 | Slaves: LSM; 13 | } 14 | 15 | Signals { 16 | InternalLightsRequest: 2, 0, CEM, LSM; 17 | InternalLightsSwitch: 2, 0, LSM, CEM; 18 | } 19 | 20 | Frames { 21 | VL1_CEM_Frm1: 1, CEM { 22 | InternalLightsRequest, 0; 23 | } 24 | VL1_LSM_Frm1: 2, LSM { 25 | InternalLightsSwitch, 0; 26 | } 27 | } 28 | 29 | Node_attributes { 30 | LSM { 31 | LIN_protocol = 2.0; 32 | configured_NAD = 0x01; 33 | response_tolerance = 38 % ; 34 | wakeup_time = 50 ms; 35 | poweron_time = 60 ms; 36 | } 37 | } 38 | 39 | Schedule_tables { 40 | MySchedule1 { 41 | VL1_CEM_Frm1 delay 15 ms; 42 | VL1_LSM_Frm1 delay 15 ms; 43 | } 44 | } 45 | 46 | Signal_encoding_types { 47 | Dig2Bit { 48 | logical_value, 0, "off"; 49 | logical_value, 1, "on"; 50 | logical_value, 2, "error"; 51 | logical_value, 3, "void"; 52 | } 53 | } 54 | 55 | Signal_representation { 56 | Dig2Bit: InternalLightsRequest, InternalLightsSwitch; 57 | } -------------------------------------------------------------------------------- /tests/ldf/j2602_1_no_values.ldf: -------------------------------------------------------------------------------- 1 | /* 2 | * Description: The LIN description file with J2602 protocol version, without the new attributes 3 | */ 4 | 5 | LIN_description_file ; 6 | LIN_protocol_version = "J2602_1_1.0"; 7 | LIN_language_version = "J2602_3_1.0"; 8 | LIN_speed = 10.417 kbps; 9 | 10 | Nodes { 11 | Master: CEM, 5 ms, 0.1 ms; 12 | Slaves: LSM; 13 | } 14 | 15 | Signals { 16 | InternalLightsRequest: 2, 0, CEM, LSM; 17 | InternalLightsSwitch: 2, 0, LSM, CEM; 18 | } 19 | 20 | Frames { 21 | VL1_CEM_Frm1: 1, CEM { 22 | InternalLightsRequest, 0; 23 | } 24 | VL1_LSM_Frm1: 2, LSM { 25 | InternalLightsSwitch, 0; 26 | } 27 | } 28 | 29 | Node_attributes { 30 | LSM { 31 | LIN_protocol = "J2602_1_1.0"; 32 | configured_NAD = 0x01; 33 | } 34 | } 35 | 36 | Schedule_tables { 37 | MySchedule1 { 38 | VL1_CEM_Frm1 delay 15 ms; 39 | VL1_LSM_Frm1 delay 15 ms; 40 | } 41 | } 42 | 43 | Signal_encoding_types { 44 | Dig2Bit { 45 | logical_value, 0, "off"; 46 | logical_value, 1, "on"; 47 | logical_value, 2, "error"; 48 | logical_value, 3, "void"; 49 | } 50 | } 51 | 52 | Signal_representation { 53 | Dig2Bit: InternalLightsRequest, InternalLightsSwitch; 54 | } -------------------------------------------------------------------------------- /tests/ldf/ldf_with_sporadic_frames.ldf: -------------------------------------------------------------------------------- 1 | LIN_description_file; 2 | LIN_protocol_version = "2.2"; 3 | LIN_language_version = "2.2"; 4 | LIN_speed = 19.2 kbps; 5 | 6 | Nodes { 7 | Master: MASTER, 10 ms, 0 ms ; 8 | Slaves: SLAVE ; 9 | } 10 | 11 | Signals { 12 | REQ_POST_RUN_RPM: 16, 0, MASTER, SLAVE ; 13 | REQ_POST_RUN_DURATION: 12, 0, MASTER, SLAVE ; 14 | CYC_READ_STATUS_LIN_RESPONSE: 1, 0, SLAVE, MASTER; 15 | } 16 | 17 | 18 | Frames { 19 | REQ_POST_RUN: 30, MASTER, 4 { 20 | REQ_POST_RUN_RPM, 0 ; 21 | REQ_POST_RUN_DURATION, 16 ; 22 | } 23 | } 24 | 25 | Sporadic_frames { 26 | SF_REQ_POST_RUN: REQ_POST_RUN ; 27 | } 28 | 29 | Node_attributes { 30 | SLAVE{ 31 | LIN_protocol = "2.2" ; 32 | configured_NAD = 0xD ; 33 | initial_NAD = 0xD ; 34 | product_id = 0x2, 0x0, 255 ; 35 | response_error = CYC_READ_STATUS_LIN_RESPONSE ; 36 | P2_min = 50 ms ; 37 | ST_min = 0 ms ; 38 | N_As_timeout = 1000 ms ; 39 | N_Cr_timeout = 1000 ms ; 40 | configurable_frames { 41 | REQ_POST_RUN ; 42 | } 43 | } 44 | } 45 | 46 | Schedule_tables { 47 | POST_RUN { 48 | SF_REQ_POST_RUN delay 10 ms ; 49 | } 50 | } 51 | 52 | 53 | Signal_encoding_types { 54 | POST_RUN_DURATION_Encoding { 55 | physical_value, 0, 4092, 1, 0, "s" ; 56 | logical_value, 4093, "not avaible" ; 57 | logical_value, 4094, "error" ; 58 | logical_value, 4095, "signal invalid" ; 59 | } 60 | } 61 | 62 | Signal_representation { 63 | POST_RUN_DURATION_Encoding: REQ_POST_RUN_DURATION ; 64 | } 65 | -------------------------------------------------------------------------------- /tests/ldf/lin13.ldf: -------------------------------------------------------------------------------- 1 | // This is a LIN description example file 2 | // Issued by Istvan Horvath 3 | 4 | LIN_description_file ; 5 | LIN_protocol_version = "1.3"; 6 | LIN_language_version = "1.3"; 7 | LIN_speed = 19.2 kbps; 8 | 9 | Nodes { 10 | Master : CEM,5 ms, 0.1 ms; 11 | Slaves : LSM,CPM; 12 | } 13 | 14 | Diagnostic_addresses { 15 | LSM: 1; 16 | CPM: 0x02; 17 | } 18 | 19 | Signals { 20 | RearFogLampInd:1,0,CEM,LSM; 21 | PositionLampInd:1,0,CEM,LSM; 22 | FrontFogLampInd:1,0,CEM,LSM; 23 | IgnitionKeyPos:3,0,CEM,LSM,CPM; 24 | LSMFuncIllum:4,0,CEM,LSM; 25 | LSMSymbolIllum:4,0,CEM,LSM; 26 | StartHeater:3,0,CEM,CPM; 27 | CPMReqB0:8,0,CEM,CPM; 28 | CPMReqB1:8,0,CEM,CPM; 29 | CPMReqB2:8,0,CEM,CPM; 30 | CPMReqB3:8,0,CEM,CPM; 31 | CPMReqB4:8,0,CEM,CPM; 32 | CPMReqB5:8,0,CEM,CPM; 33 | CPMReqB6:8,0,CEM,CPM; 34 | CPMReqB7:8,0,CEM,CPM; 35 | ReostatPos:4,0,LSM,CEM; 36 | HeadLampBeamLev:4,0,LSM,CEM; 37 | FrontFogLampSw:1,0,LSM,CEM; 38 | RearFogLampSw:1,0,LSM,CEM; 39 | MLSOff:1,0,LSM,CEM; 40 | MLSHeadLight:1,0,LSM,CEM; 41 | MLSPosLight:1,0,LSM,CEM; 42 | HBLSortHigh:1,0,LSM,CEM; 43 | HBLShortLow:1,0,LSM,CEM; 44 | ReoShortHigh:1,0,LSM,CEM; 45 | ReoShortLow:1,0,LSM,CEM; 46 | LSMHWPartNoB0:8,0,LSM,CEM; 47 | LSMHWPartNoB1:8,0,LSM,CEM; 48 | LSMHWPartNoB2:8,0,LSM,CEM; 49 | LSMHWPartNoB3:8,0,LSM,CEM; 50 | LSMSWPartNo:8,0,LSM,CEM; 51 | CPMOutputs:10,0,CPM,CEM; 52 | HeaterStatus:4,0,CPM,CEM; 53 | CPMGlowPlug:7,0,CPM,CEM; 54 | CPMFanPWM:8,0,CPM,CEM; 55 | WaterTempLow:8,0,CPM,CEM; 56 | WaterTempHigh:8,0,CPM,CEM; 57 | CPMFuelPump:7,0,CPM,CEM; 58 | CPMRunTime:13,0,CPM,CEM; 59 | FanIdealSpeed:8,0,CPM,CEM; 60 | FanMeasSpeed:8,0,CPM,CEM; 61 | CPMRespB0:1,0,CPM,CEM; 62 | CPMRespB1:1,0,CPM,CEM; 63 | CPMRespB2:1,0,CPM,CEM; 64 | CPMRespB3:1,0,CPM,CEM; 65 | CPMRespB4:1,0,CPM,CEM; 66 | CPMRespB5:1,0,CPM,CEM; 67 | CPMRespB6:1,0,CPM,CEM; 68 | CPMRespB7:1,0,CPM,CEM; 69 | } 70 | 71 | Frames { 72 | VL1_CEM_Frm1:32,CEM,3 { //The length of this frame is redefined to 3 73 | RearFogLampInd,0; 74 | PositionLampInd,1; 75 | FrontFogLampInd,2; 76 | IgnitionKeyPos,3; 77 | LSMFuncIllum,8; 78 | LSMSymbolIllum,12; 79 | StartHeater,16; 80 | } 81 | VL1_CEM_Frm2:48,CEM { 82 | CPMReqB0,0; 83 | CPMReqB1,8; 84 | CPMReqB2,16; 85 | CPMReqB3,24; 86 | CPMReqB4,32; 87 | CPMReqB5,40; 88 | CPMReqB6,48; 89 | CPMReqB7,56; 90 | } 91 | VL1_LSM_Frm1:33,LSM { 92 | ReostatPos,0; 93 | HeadLampBeamLev,4; 94 | FrontFogLampSw,8; 95 | RearFogLampSw,9; 96 | MLSOff,10; 97 | MLSHeadLight,11; 98 | MLSPosLight,12; 99 | HBLSortHigh,16; 100 | HBLShortLow,17; 101 | ReoShortHigh,18; 102 | ReoShortLow,19; 103 | } 104 | VL1_LSM_Frm2:49,LSM,6 { //The length of this frame is redefined to 5 105 | LSMHWPartNoB0,0; 106 | LSMHWPartNoB1,8; 107 | LSMHWPartNoB2,16; 108 | LSMHWPartNoB3,32; 109 | LSMSWPartNo,40; 110 | } 111 | VL1_CPM_Frm1:50,CPM { 112 | CPMOutputs,0; 113 | HeaterStatus,10; 114 | CPMGlowPlug,16; 115 | CPMFanPWM,24; 116 | WaterTempLow,32; 117 | WaterTempHigh,40; 118 | CPMFuelPump,56; 119 | } 120 | VL1_CPM_Frm2:34,CPM { 121 | CPMRunTime,0; 122 | FanIdealSpeed,16; 123 | FanMeasSpeed,24; 124 | } 125 | VL1_CPM_Frm3:51,CPM { 126 | CPMRespB0,0; 127 | CPMRespB1,8; 128 | CPMRespB2,16; 129 | CPMRespB3,24; 130 | CPMRespB4,32; 131 | CPMRespB5,40; 132 | CPMRespB6,48; 133 | CPMRespB7,56; 134 | } 135 | } 136 | Schedule_tables { 137 | VL1_ST1 { 138 | VL1_CEM_Frm1 delay 15 ms; 139 | VL1_LSM_Frm1 delay 15 ms; 140 | VL1_CPM_Frm1 delay 20 ms; 141 | VL1_CPM_Frm2 delay 20 ms; 142 | } 143 | VL1_ST2 { 144 | VL1_CEM_Frm1 delay 15 ms; 145 | VL1_CEM_Frm2 delay 20 ms; 146 | VL1_LSM_Frm1 delay 15 ms; 147 | VL1_LSM_Frm2 delay 20 ms; 148 | VL1_CEM_Frm1 delay 15 ms; 149 | VL1_CPM_Frm1 delay 20 ms; 150 | VL1_CPM_Frm2 delay 20 ms; 151 | VL1_LSM_Frm1 delay 15 ms; 152 | VL1_CPM_Frm3 delay 20 ms; 153 | } 154 | } 155 | 156 | Signal_groups { 157 | CPMReq:64 { 158 | CPMReqB0,0; 159 | CPMReqB1,8; 160 | CPMReqB2,16; 161 | CPMReqB3,24; 162 | CPMReqB4,32; 163 | CPMReqB5,40; 164 | CPMReqB6,48; 165 | CPMReqB7,56; 166 | } 167 | CPMResp:64 { 168 | CPMRespB0,0; 169 | CPMRespB1,8; 170 | CPMRespB2,16; 171 | CPMRespB3,24; 172 | CPMRespB4,32; 173 | CPMRespB5,40; 174 | CPMRespB6,48; 175 | CPMRespB7,56; 176 | } 177 | } 178 | 179 | Signal_encoding_types { 180 | State1 { 181 | logical_value,0,"off"; 182 | logical_value,1,"on"; 183 | } 184 | State2 { 185 | logical_value,0,"off"; 186 | logical_value,1,"on"; 187 | logical_value,2,"error"; 188 | logical_value,3,"void"; 189 | } 190 | Temp { 191 | physical_value,0,250,0.5,-40,"degree"; 192 | physical_value,251,253,1,0,"undefined"; 193 | logical_value,254,"out of range"; 194 | logical_value,255,"error"; 195 | } 196 | Speed { 197 | physical_value,0,65500,0.008,250,"km/h"; 198 | physical_value,65501,65533,1,0,"undefined"; 199 | logical_value,65534,"error"; 200 | logical_value,65535,"void"; 201 | } 202 | } 203 | 204 | Signal_representation { 205 | State1:RearFogLampInd,PositionLampInd,FrontFogLampInd; 206 | Temp:WaterTempLow,WaterTempHigh; 207 | Speed:FanIdealSpeed,FanMeasSpeed; 208 | } -------------------------------------------------------------------------------- /tests/ldf/lin20.ldf: -------------------------------------------------------------------------------- 1 | /* 2 | * File: hello.ldf 3 | * Author: Christian Bondesson 4 | * Description: The LIN description file for the example program 5 | */ 6 | 7 | LIN_description_file ; 8 | LIN_protocol_version = "2.0"; 9 | LIN_language_version = "2.0"; 10 | LIN_speed = 19.2 kbps; 11 | 12 | Nodes { 13 | Master: CEM, 5 ms, 0.1 ms; 14 | Slaves: LSM; 15 | } 16 | 17 | Signals { 18 | InternalLightsRequest: 2, 0, CEM, LSM; 19 | InternalLightsSwitch: 2, 0, LSM, CEM; 20 | } 21 | 22 | Frames { 23 | VL1_CEM_Frm1: 1, CEM { 24 | InternalLightsRequest, 0; 25 | } 26 | VL1_LSM_Frm1: 2, LSM { 27 | InternalLightsSwitch, 0; 28 | } 29 | } 30 | 31 | Node_attributes { 32 | LSM { 33 | LIN_protocol = 2.0; 34 | configured_NAD = 0x01; 35 | } 36 | } 37 | 38 | Schedule_tables { 39 | MySchedule1 { 40 | VL1_CEM_Frm1 delay 15 ms; 41 | VL1_LSM_Frm1 delay 15 ms; 42 | } 43 | } 44 | 45 | Signal_encoding_types { 46 | Dig2Bit { 47 | logical_value, 0, "off"; 48 | logical_value, 1, "on"; 49 | logical_value, 2, "error"; 50 | logical_value, 3, "void"; 51 | } 52 | } 53 | 54 | Signal_representation { 55 | Dig2Bit: InternalLightsRequest, InternalLightsSwitch; 56 | } -------------------------------------------------------------------------------- /tests/ldf/lin21.ldf: -------------------------------------------------------------------------------- 1 | /******************************************************/ 2 | /* This is the example LDF from LIN 2.1 specification */ 3 | /******************************************************/ 4 | 5 | // Source: https://lin-cia.org/fileadmin/microsites/lin-cia.org/resources/documents/LIN-Spec_Pac2_1.pdf 6 | 7 | LIN_description_file; 8 | LIN_protocol_version = "2.1"; 9 | LIN_language_version = "2.1"; 10 | LIN_speed = 19.2 kbps; 11 | Channel_name = "DB"; 12 | 13 | Nodes { 14 | Master: CEM, 5 ms, 0.1 ms; 15 | Slaves: LSM, RSM; 16 | } 17 | 18 | Node_attributes { 19 | LSM { 20 | LIN_protocol = "2.1"; 21 | configured_NAD = 0x20; 22 | initial_NAD = 0x01; 23 | product_id = 0x4A4F, 0x4841; 24 | response_error = LSMerror; 25 | fault_state_signals = IntError; 26 | P2_min = 150 ms; 27 | ST_min = 50 ms; 28 | configurable_frames { 29 | CEM_Frm1; LSM_Frm1; LSM_Frm2; 30 | } 31 | } 32 | RSM { 33 | LIN_protocol = "2.0"; 34 | configured_NAD = 0x20; 35 | product_id = 0x4E4E, 0x4553, 1; 36 | response_error = RSMerror; 37 | P2_min = 150 ms; 38 | ST_min = 50 ms; 39 | N_As_timeout = 1000 ms; 40 | N_Cr_timeout = 1000 ms; 41 | configurable_frames { 42 | CEM_Frm1 = 0x0001; LSM_Frm1 = 0x0002; LSM_Frm2 = 0x0003; 43 | } 44 | } 45 | } 46 | 47 | Signals { 48 | InternalLightsRequest: 2, 0, CEM, LSM, RSM; 49 | RightIntLightsSwitch: 8, 0, RSM, CEM; 50 | LeftIntLightsSwitch: 8, 0, LSM, CEM; 51 | LSMerror: 1, 0, LSM, CEM; 52 | RSMerror: 1, 0, LSM, CEM; 53 | IntError: 2, 0, LSM, CEM; 54 | } 55 | 56 | Frames { 57 | CEM_Frm1: 0x01, CEM, 1 { 58 | InternalLightsRequest, 0; 59 | } 60 | LSM_Frm1: 0x02, LSM, 2 { 61 | LeftIntLightsSwitch, 0; 62 | } 63 | LSM_Frm2: 0x03, LSM, 1 { 64 | LSMerror, 0; 65 | IntError, 1; 66 | } 67 | RSM_Frm1: 0x04, RSM, 2 { 68 | RightIntLightsSwitch, 0; 69 | } 70 | RSM_Frm2: 0x05, RSM, 1 { 71 | RSMerror, 0; 72 | } 73 | } 74 | 75 | Event_triggered_frames { 76 | Node_Status_Event : Collision_resolver, 0x06, RSM_Frm1, LSM_Frm1; 77 | } 78 | 79 | Schedule_tables { 80 | Configuration_Schedule { 81 | AssignNAD {LSM} delay 15 ms; 82 | AssignFrameIdRange {LSM, 0} delay 15 ms; 83 | AssignFrameIdRange {LSM, 0, 1, 2, 3, 4} delay 15 ms; 84 | ConditionalChangeNAD {0x17, 0, 0x20, 0xFF, 0x00, 0x18} delay 15 ms; 85 | DataDump {LSM, 1, 2, 3, 4, 5} delay 15 ms; 86 | SaveConfiguration {LSM} delay 15 ms; 87 | AssignFrameId {RSM, CEM_Frm1} delay 15 ms; 88 | AssignFrameId {RSM, RSM_Frm1} delay 15 ms; 89 | AssignFrameId {RSM, RSM_Frm2} delay 15 ms; 90 | } 91 | Normal_Schedule { 92 | CEM_Frm1 delay 15 ms; 93 | LSM_Frm2 delay 15 ms; 94 | RSM_Frm2 delay 15 ms; 95 | Node_Status_Event delay 10 ms; 96 | } 97 | MRF_schedule { 98 | MasterReq delay 10 ms; 99 | } 100 | SRF_schedule { 101 | SlaveResp delay 10 ms; 102 | } 103 | Collision_resolver { // Keep timing of other frames if collision 104 | CEM_Frm1 delay 15 ms; 105 | LSM_Frm2 delay 15 ms; 106 | RSM_Frm2 delay 15 ms; 107 | RSM_Frm1 delay 10 ms; // Poll the RSM node 108 | CEM_Frm1 delay 15 ms; 109 | LSM_Frm2 delay 15 ms; 110 | RSM_Frm2 delay 15 ms; 111 | LSM_Frm1 delay 10 ms; // Poll the LSM node 112 | } 113 | } 114 | 115 | Signal_encoding_types { 116 | Dig2Bit { 117 | logical_value, 0, "off"; 118 | logical_value, 1, "on"; 119 | logical_value, 2, "error"; 120 | logical_value, 3, "void"; 121 | } 122 | ErrorEncoding { 123 | logical_value, 0, "OK"; 124 | logical_value, 1, "error"; 125 | } 126 | FaultStateEncoding { 127 | logical_value, 0, "No test result"; 128 | logical_value, 1, "failed"; 129 | logical_value, 2, "passed"; 130 | logical_value, 3, "not used"; 131 | } 132 | LightEncoding { 133 | logical_value, 0, "Off"; 134 | physical_value, 1, 254, 1, 100, "lux"; 135 | logical_value, 255, "error"; 136 | } 137 | } 138 | 139 | Signal_representation { 140 | Dig2Bit: InternalLightsRequest; 141 | ErrorEncoding: RSMerror, LSMerror; 142 | FaultStateEncoding: IntError; 143 | LightEncoding: RightIntLightsSwitch, LeftIntLightsSwitch; 144 | } 145 | -------------------------------------------------------------------------------- /tests/ldf/lin22.ldf: -------------------------------------------------------------------------------- 1 | /*******************************************************/ 2 | /* This is the example LDF from LIN 2.2A specification */ 3 | /*******************************************************/ 4 | 5 | // Source: https://lin-cia.org/fileadmin/microsites/lin-cia.org/resources/documents/LIN_2.2A.pdf 6 | 7 | LIN_description_file; 8 | LIN_protocol_version = "2.2"; 9 | LIN_language_version = "2.2"; 10 | LIN_speed = 19.2 kbps; 11 | Channel_name = "DB"; 12 | 13 | Nodes { 14 | Master: CEM, 5 ms, 0.1 ms; 15 | Slaves: LSM, RSM; 16 | } 17 | 18 | Signals { 19 | InternalLightsRequest: 2, 0, CEM, LSM, RSM; 20 | RightIntLightsSwitch: 8, 0, RSM, CEM; 21 | LeftIntLightsSwitch: 8, 0, LSM, CEM; 22 | LSMerror: 1, 0, LSM, CEM; 23 | RSMerror: 1, 0, RSM, CEM; 24 | IntTest: 2, 0, LSM, CEM; 25 | } 26 | 27 | Frames { 28 | CEM_Frm1: 0x01, CEM, 1 { 29 | InternalLightsRequest, 0; 30 | } 31 | LSM_Frm1: 0x02, LSM, 2 { 32 | LeftIntLightsSwitch, 8; 33 | } 34 | LSM_Frm2: 0x03, LSM, 1 { 35 | LSMerror, 0; 36 | IntTest, 1; 37 | } 38 | RSM_Frm1: 0x04, RSM, 2 { 39 | RightIntLightsSwitch, 8; 40 | } 41 | RSM_Frm2: 0x05, RSM, 1 { 42 | RSMerror, 0; 43 | } 44 | } 45 | 46 | Event_triggered_frames { 47 | Node_Status_Event : Collision_resolver, 0x06, RSM_Frm1, LSM_Frm1; 48 | } 49 | 50 | Node_attributes { 51 | RSM { 52 | LIN_protocol = "2.0"; 53 | configured_NAD = 0x20; 54 | product_id = 0x4E4E, 0x4553, 1; 55 | response_error = RSMerror; 56 | P2_min = 150 ms; 57 | ST_min = 50 ms; 58 | configurable_frames { 59 | Node_Status_Event=0x000; CEM_Frm1 = 0x0001; RSM_Frm1 = 0x0002; 60 | RSM_Frm2 = 0x0003; 61 | } 62 | } 63 | LSM { 64 | LIN_protocol = "2.2"; 65 | configured_NAD = 0x21; 66 | initial_NAD = 0x01; 67 | product_id = 0x4A4F, 0x4841; 68 | response_error = LSMerror; 69 | fault_state_signals = IntTest; 70 | P2_min = 150 ms; 71 | ST_min = 50 ms; 72 | N_As_timeout = 1000 ms; 73 | N_Cr_timeout = 1000 ms; 74 | configurable_frames { 75 | Node_Status_Event; 76 | CEM_Frm1; 77 | LSM_Frm1; 78 | LSM_Frm2; 79 | } 80 | } 81 | } 82 | 83 | Schedule_tables { 84 | Configuration_Schedule { 85 | AssignNAD {LSM} delay 15 ms; 86 | AssignFrameIdRange {LSM, 0} delay 15 ms; 87 | AssignFrameIdRange {LSM, 0, 1, 2, 3, 4} delay 15 ms; 88 | ConditionalChangeNAD {0x17, 0, 0x20, 0xFF, 0x00, 0x18} delay 15 ms; 89 | DataDump {LSM, 1, 2, 3, 4, 5} delay 15 ms; 90 | SaveConfiguration {LSM} delay 15 ms; 91 | AssignFrameId {RSM, CEM_Frm1} delay 15 ms; 92 | AssignFrameId {RSM, RSM_Frm1} delay 15 ms; 93 | AssignFrameId {RSM, RSM_Frm2} delay 15 ms; 94 | FreeFormat {1, 2, 3, 4, 5, 6, 7, 8} delay 15 ms; 95 | } 96 | Normal_Schedule { 97 | CEM_Frm1 delay 15 ms; 98 | LSM_Frm2 delay 15 ms; 99 | RSM_Frm2 delay 15 ms; 100 | Node_Status_Event delay 10 ms; 101 | } 102 | MRF_schedule { 103 | MasterReq delay 10 ms; 104 | } 105 | SRF_schedule { 106 | SlaveResp delay 10 ms; 107 | } 108 | Collision_resolver { // Keep timing of other frames if collision 109 | CEM_Frm1 delay 15 ms; 110 | LSM_Frm2 delay 15 ms; 111 | RSM_Frm2 delay 15 ms; 112 | RSM_Frm1 delay 10 ms; // Poll the RSM node 113 | CEM_Frm1 delay 15 ms; 114 | LSM_Frm2 delay 15 ms; 115 | RSM_Frm2 delay 15 ms; 116 | LSM_Frm1 delay 10 ms; // Poll the LSM node 117 | } 118 | } 119 | 120 | Signal_encoding_types { 121 | Dig2Bit { 122 | logical_value, 0, "off"; 123 | logical_value, 1, "on"; 124 | logical_value, 2, "error"; 125 | logical_value, 3, "void"; 126 | } 127 | ErrorEncoding { 128 | logical_value, 0, "OK"; 129 | logical_value, 1, "error"; 130 | } 131 | FaultStateEncoding { 132 | logical_value, 0, "No test result"; 133 | logical_value, 1, "failed"; 134 | logical_value, 2, "passed"; 135 | logical_value, 3, "not used"; 136 | } 137 | LightEncoding { 138 | logical_value, 0, "Off"; 139 | physical_value, 1, 254, 1, 100, "lux"; 140 | logical_value, 255, "error"; 141 | } 142 | } 143 | Signal_representation { 144 | Dig2Bit: InternalLightsRequest; 145 | ErrorEncoding: RSMerror, LSMerror; 146 | LightEncoding: RightIntLightsSwitch, LeftIntLightsSwitch; 147 | } -------------------------------------------------------------------------------- /tests/ldf/lin_diagnostics.ldf: -------------------------------------------------------------------------------- 1 | /*******************************************************/ 2 | /* This is the example LDF from LIN 2.2A specification */ 3 | /*******************************************************/ 4 | 5 | // Source: https://lin-cia.org/fileadmin/microsites/lin-cia.org/resources/documents/LIN_2.2A.pdf 6 | 7 | LIN_description_file; 8 | LIN_protocol_version = "2.2"; 9 | LIN_language_version = "2.2"; 10 | LIN_speed = 19.2 kbps; 11 | Channel_name = "DB"; 12 | 13 | Nodes { 14 | Master: CEM, 5 ms, 0.1 ms; 15 | Slaves: LSM, RSM; 16 | } 17 | 18 | Signals { 19 | InternalLightsRequest: 2, 0, CEM, LSM, RSM; 20 | RightIntLightsSwitch: 8, 0, RSM, CEM; 21 | LeftIntLightsSwitch: 8, 0, LSM, CEM; 22 | LSMerror: 1, 0, LSM, CEM; 23 | RSMerror: 1, 0, RSM, CEM; 24 | IntTest: 2, 0, LSM, CEM; 25 | } 26 | 27 | Frames { 28 | CEM_Frm1: 0x01, CEM, 1 { 29 | InternalLightsRequest, 0; 30 | } 31 | LSM_Frm1: 0x02, LSM, 2 { 32 | LeftIntLightsSwitch, 8; 33 | } 34 | LSM_Frm2: 0x03, LSM, 1 { 35 | LSMerror, 0; 36 | IntTest, 1; 37 | } 38 | RSM_Frm1: 0x04, RSM, 2 { 39 | RightIntLightsSwitch, 8; 40 | } 41 | RSM_Frm2: 0x05, RSM, 1 { 42 | RSMerror, 0; 43 | } 44 | } 45 | 46 | Event_triggered_frames { 47 | Node_Status_Event : Collision_resolver, 0x06, RSM_Frm1, LSM_Frm1; 48 | } 49 | 50 | Diagnostic_signals { 51 | MasterReqB0: 8, 0 ; 52 | MasterReqB1: 8, 0 ; 53 | MasterReqB2: 8, 0 ; 54 | MasterReqB3: 8, 0 ; 55 | MasterReqB4: 8, 0 ; 56 | MasterReqB5: 8, 0 ; 57 | MasterReqB6: 8, 0 ; 58 | MasterReqB7: 8, 0 ; 59 | SlaveRespB0: 8, 0 ; 60 | SlaveRespB1: 8, 0 ; 61 | SlaveRespB2: 8, 0 ; 62 | SlaveRespB3: 8, 0 ; 63 | SlaveRespB4: 8, 0 ; 64 | SlaveRespB5: 8, 0 ; 65 | SlaveRespB6: 8, 0 ; 66 | SlaveRespB7: 8, 0 ; 67 | } 68 | 69 | Diagnostic_frames { 70 | MasterReq: 60 { 71 | MasterReqB0, 0; 72 | MasterReqB1, 8; 73 | MasterReqB2, 16; 74 | MasterReqB3, 24; 75 | MasterReqB4, 32; 76 | MasterReqB5, 40; 77 | MasterReqB6, 48; 78 | MasterReqB7, 56; 79 | } 80 | SlaveResp: 61 { 81 | SlaveRespB0, 0; 82 | SlaveRespB1, 8; 83 | SlaveRespB2, 16; 84 | SlaveRespB3, 24; 85 | SlaveRespB4, 32; 86 | SlaveRespB5, 40; 87 | SlaveRespB6, 48; 88 | SlaveRespB7, 56; 89 | } 90 | } 91 | 92 | Node_attributes { 93 | RSM { 94 | LIN_protocol = "2.0"; 95 | configured_NAD = 0x20; 96 | product_id = 0x4E4E, 0x4553, 1; 97 | response_error = RSMerror; 98 | P2_min = 150 ms; 99 | ST_min = 50 ms; 100 | configurable_frames { 101 | Node_Status_Event=0x000; CEM_Frm1 = 0x0001; RSM_Frm1 = 0x0002; 102 | RSM_Frm2 = 0x0003; 103 | } 104 | } 105 | LSM { 106 | LIN_protocol = "2.2"; 107 | configured_NAD = 0x21; 108 | initial_NAD = 0x01; 109 | product_id = 0x4A4F, 0x4841; 110 | response_error = LSMerror; 111 | fault_state_signals = IntTest; 112 | P2_min = 150 ms; 113 | ST_min = 50 ms; 114 | N_As_timeout = 1000 ms; 115 | N_Cr_timeout = 1000 ms; 116 | configurable_frames { 117 | Node_Status_Event; 118 | CEM_Frm1; 119 | LSM_Frm1; 120 | LSM_Frm2; 121 | } 122 | } 123 | } 124 | 125 | Schedule_tables { 126 | Configuration_Schedule { 127 | AssignNAD {LSM} delay 15 ms; 128 | AssignFrameIdRange {LSM, 0} delay 15 ms; 129 | AssignFrameIdRange {LSM, 0, 1, 2, 3, 4} delay 15 ms; 130 | ConditionalChangeNAD {0x17, 0, 0x20, 0xFF, 0x00, 0x18} delay 15 ms; 131 | DataDump {LSM, 1, 2, 3, 4, 5} delay 15 ms; 132 | SaveConfiguration {LSM} delay 15 ms; 133 | AssignFrameId {RSM, CEM_Frm1} delay 15 ms; 134 | AssignFrameId {RSM, RSM_Frm1} delay 15 ms; 135 | AssignFrameId {RSM, RSM_Frm2} delay 15 ms; 136 | FreeFormat {1, 2, 3, 4, 5, 6, 7, 8} delay 15 ms; 137 | } 138 | Normal_Schedule { 139 | CEM_Frm1 delay 15 ms; 140 | LSM_Frm2 delay 15 ms; 141 | RSM_Frm2 delay 15 ms; 142 | Node_Status_Event delay 10 ms; 143 | } 144 | MRF_schedule { 145 | MasterReq delay 10 ms; 146 | } 147 | SRF_schedule { 148 | SlaveResp delay 10 ms; 149 | } 150 | Collision_resolver { // Keep timing of other frames if collision 151 | CEM_Frm1 delay 15 ms; 152 | LSM_Frm2 delay 15 ms; 153 | RSM_Frm2 delay 15 ms; 154 | RSM_Frm1 delay 10 ms; // Poll the RSM node 155 | CEM_Frm1 delay 15 ms; 156 | LSM_Frm2 delay 15 ms; 157 | RSM_Frm2 delay 15 ms; 158 | LSM_Frm1 delay 10 ms; // Poll the LSM node 159 | } 160 | } 161 | 162 | Signal_encoding_types { 163 | Dig2Bit { 164 | logical_value, 0, "off"; 165 | logical_value, 1, "on"; 166 | logical_value, 2, "error"; 167 | logical_value, 3, "void"; 168 | } 169 | ErrorEncoding { 170 | logical_value, 0, "OK"; 171 | logical_value, 1, "error"; 172 | } 173 | FaultStateEncoding { 174 | logical_value, 0, "No test result"; 175 | logical_value, 1, "failed"; 176 | logical_value, 2, "passed"; 177 | logical_value, 3, "not used"; 178 | } 179 | LightEncoding { 180 | logical_value, 0, "Off"; 181 | physical_value, 1, 254, 1, 100, "lux"; 182 | logical_value, 255, "error"; 183 | } 184 | } 185 | Signal_representation { 186 | Dig2Bit: InternalLightsRequest; 187 | ErrorEncoding: RSMerror, LSMerror; 188 | LightEncoding: RightIntLightsSwitch, LeftIntLightsSwitch; 189 | } -------------------------------------------------------------------------------- /tests/ldf/lin_encoders.ldf: -------------------------------------------------------------------------------- 1 | /*******************************************************/ 2 | /* This is a synthetic example LDF from the LIN 2.2A */ 3 | /* specification that combines all examples of signal */ 4 | /* encodings. */ 5 | /*******************************************************/ 6 | 7 | // Source: https://lin-cia.org/fileadmin/microsites/lin-cia.org/resources/documents/LIN_2.2A.pdf 8 | 9 | LIN_description_file; 10 | LIN_protocol_version = "2.1"; 11 | LIN_language_version = "2.1"; 12 | LIN_speed = 19.2 kbps; 13 | 14 | Nodes { 15 | Master: main_node, 5 ms, 1 ms ; 16 | Slaves: remote_node; 17 | } 18 | 19 | Signals { 20 | bcd_signal: 16, {0x32, 32}, remote_node, main_node ; 21 | ascii_signal: 16, {16, 0x16}, remote_node, main_node ; 22 | } 23 | 24 | Frames { 25 | dummy_frame: 0x25, remote_node, 8 { 26 | bcd_signal, 0; 27 | ascii_signal, 16; 28 | } 29 | } 30 | 31 | Node_attributes { 32 | remote_node { 33 | LIN_protocol = "2.1" ; 34 | configured_NAD = 0x20 ; 35 | product_id = 0x5, 0xA5A5, 0 ; 36 | P2_min = 50 ms ; 37 | ST_min = 0 ms ; 38 | N_As_timeout = 1000 ms ; 39 | N_Cr_timeout = 1000 ms ; 40 | configurable_frames { 41 | dummy_frame ; 42 | } 43 | } 44 | } 45 | 46 | Schedule_tables { 47 | MRF_schedule { 48 | MasterReq delay 10 ms; 49 | } 50 | SRF_schedule { 51 | SlaveResp delay 10 ms; 52 | } 53 | Normal_Schedule { 54 | dummy_frame delay 15 ms ; 55 | } 56 | } 57 | 58 | Signal_encoding_types { 59 | power_state { 60 | logical_value, 0, "off"; 61 | logical_value, 1, "on"; 62 | } 63 | V_battery { 64 | logical_value, 0, "under voltage"; 65 | physical_value, 1, 63, 0.0625, 7.0, "Volt"; 66 | physical_value, 64, 191, 0.0104, 11.0, "Volt"; 67 | physical_value, 192, 253, 0.0625, 13.0, "Volt"; 68 | logical_value, 254, "over voltage"; 69 | logical_value, 255, "invalid"; 70 | } 71 | Dig2Bit { 72 | logical_value, 0, "off"; 73 | logical_value, 1, "on"; 74 | logical_value, 2, "error"; 75 | logical_value, 3, "void"; 76 | } 77 | ErrorEncoding { 78 | logical_value, 0, "OK"; 79 | logical_value, 1, "error"; 80 | } 81 | FaultStateEncoding { 82 | logical_value, 0, "No test result"; 83 | logical_value, 1, "failed"; 84 | logical_value, 2, "passed"; 85 | logical_value, 3, "not used"; 86 | } 87 | LightEncoding { 88 | logical_value, 0, "Off"; 89 | physical_value, 1, 254, 1, 100, "lux"; 90 | logical_value, 255, "error"; 91 | } 92 | AsciiEncoding { 93 | ascii_value; 94 | } 95 | BCDEncoding { 96 | bcd_value; 97 | } 98 | ScientificEncoding { 99 | physical_value, 0, 255, 5.6785558246e-04, 0, "Ohm"; 100 | physical_value, 0, 255, 7.654E-02, 0, "Ohm"; 101 | physical_value, 0, 1024, 3.778e02, 0, "Ohm"; 102 | physical_value, 0, 1024, 1.222E09, 0, "Ohm"; 103 | physical_value, 0, 65535, 3.5E+02, 5, "Ohm"; 104 | } 105 | } 106 | 107 | Signal_representation { 108 | BCDEncoding: bcd_signal; 109 | AsciiEncoding: ascii_signal; 110 | } 111 | -------------------------------------------------------------------------------- /tests/ldf/lin_schedules.ldf: -------------------------------------------------------------------------------- 1 | LIN_description_file; 2 | LIN_protocol_version = "2.1"; 3 | LIN_language_version = "2.1"; 4 | LIN_speed = 19.2 kbps; 5 | 6 | Nodes { 7 | Master: LightController, 5 ms, 1 ms ; 8 | Slaves: LeftLight, RightLight; 9 | } 10 | 11 | Node_attributes { 12 | LeftLight { 13 | LIN_protocol = "2.1"; 14 | configured_NAD = 0x20; 15 | initial_NAD = 0x01; 16 | product_id = 0x4A4F, 0x4841; 17 | configurable_frames { 18 | LeftLightStatus; LeftLightSet; LeftLightError; 19 | } 20 | } 21 | RightLight { 22 | LIN_protocol = "2.1"; 23 | configured_NAD = 0x21; 24 | initial_NAD = 0x02; 25 | product_id = 0x4A4F, 0x4841; 26 | configurable_frames { 27 | RightLightStatus; RightLightSet; RightLightError; 28 | } 29 | } 30 | } 31 | 32 | Signals { 33 | LeftLight_signal: 8, 0xFF, LeftLight, LightController; 34 | SetLeftLight_signal: 8, 0xFF, LightController, LeftLight; 35 | LeftLightError_signal: 1, 0, LeftLight, LightController; 36 | 37 | RightLight_signal: 8, 0xFF, RightLight, LightController; 38 | SetRightLight_signal: 8, 0xFF, LightController, RightLight; 39 | RightLightError_signal: 1, 0, RightLight, LightController; 40 | } 41 | 42 | Frames { 43 | LeftLightStatus: 0x40, LeftLight, 8 { 44 | LeftLight_signal, 0; 45 | } 46 | RightLightStatus: 0x41, RightLight, 8 { 47 | RightLight_signal, 0; 48 | } 49 | LeftLightSet: 0x42, LightController, 8 { 50 | SetLeftLight_signal, 0; 51 | } 52 | RightLightSet: 0x43, LightController, 8 { 53 | SetRightLight_signal, 0; 54 | } 55 | LeftLightError: 0x44, LeftLight, 8 { 56 | LeftLightError_signal, 0; 57 | } 58 | RightLightError: 0x45, RightLight, 8 { 59 | RightLightError_signal, 0; 60 | } 61 | } 62 | 63 | Event_triggered_frames { 64 | LightErrorEvent : Collision_Resolver_Schedule, 0x39, LeftLightError, RightLightError; 65 | } 66 | 67 | Schedule_tables { 68 | AddressConfiguration_Schedule { 69 | AssignNAD { LeftLight } delay 10 ms; 70 | SaveConfiguration { LeftLight } delay 10 ms; 71 | AssignNAD { RightLight } delay 10 ms; 72 | SaveConfiguration { RightLight } delay 10 ms; 73 | 74 | ConditionalChangeNAD { 0x7F, 0x01, 0x03, 0x01, 0xFF, 0x01 } delay 10 ms; 75 | } 76 | FrameConfiguration_Schedule { 77 | AssignFrameIdRange { LeftLight, 0, 0x40, 0x42, 0xFF, 0xFF } delay 10 ms; 78 | SaveConfiguration { LeftLight } delay 10 ms; 79 | AssignFrameIdRange { RightLight, 0, 0x41, 0x43, 0xFF, 0xFF } delay 10 ms; 80 | SaveConfiguration { RightLight } delay 10 ms; 81 | 82 | AssignFrameId { LeftLight, LeftLightStatus } delay 10ms; 83 | UnassignFrameId { LeftLight, LeftLightStatus } delay 10ms; 84 | 85 | AssignFrameId { RightLight, RightLightStatus } delay 10ms; 86 | UnassignFrameId { RightLight, RightLightStatus } delay 10ms; 87 | } 88 | Information_Schedule { 89 | DataDump { LeftLight, 0x10, 0x80, 0x00, 0xFF, 0xFF } delay 10 ms; 90 | DataDump { RightLight, 0x10, 0x80, 0x00, 0xFF, 0xFF } delay 10 ms; 91 | FreeFormat { 0x3C, 0xB2, 0x00, 0x00, 0xFF, 0x7F, 0xFF, 0xFF } delay 10ms; 92 | } 93 | Diagnostic_Schedule { 94 | MasterReq delay 10 ms; 95 | SlaveResp delay 10 ms; 96 | } 97 | Normal_Schedule { 98 | LeftLightSet delay 20 ms; 99 | LeftLightStatus delay 20 ms; 100 | RightLightSet delay 20 ms; 101 | RightLightStatus delay 20 ms; 102 | } 103 | Collision_Resolver_Schedule { 104 | LeftLightError delay 10 ms; 105 | RightLightError delay 10 ms; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/ldf/no_signal_subscribers.ldf: -------------------------------------------------------------------------------- 1 | /* 2 | Here you will find an odd case that is not normally valid in the LIN specification, 3 | However is commonly used by OEMs. 4 | The DummyFrame serves as a "Keep Alive" so slaves don't go to sleep. 5 | */ 6 | 7 | LIN_description_file; 8 | LIN_protocol_version = "2.2"; 9 | LIN_language_version = "2.2"; 10 | LIN_speed = 19.2 kbps; 11 | Channel_name = ""; 12 | 13 | Nodes { 14 | Master: master, 5.0 ms, 0.1 ms; 15 | } 16 | 17 | Signals { 18 | // Signal with no subscribers 19 | DummySignal_0: 8, 255, master; 20 | } 21 | 22 | Frames { 23 | DummyFrame: 59, master, 8 { 24 | DummySignal_0, 0; 25 | } 26 | } 27 | 28 | Node_attributes { 29 | } 30 | 31 | Schedule_tables { 32 | RUN_MAIN { 33 | DummyFrame delay 10.0 ms; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/snapshot_data.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import json 3 | import os 4 | 5 | import ldfparser 6 | 7 | if __name__ == "__main__": 8 | ldf_directory = os.path.join(os.path.dirname(__file__), 'ldf') 9 | snapshot_directory = os.path.join(os.path.dirname(__file__), 'snapshot') 10 | ldf_files = glob.glob(ldf_directory + '/*.ldf') 11 | 12 | if not os.path.exists(snapshot_directory): 13 | os.mkdir(snapshot_directory) 14 | 15 | for ldf in ldf_files: 16 | data = ldfparser.parse_ldf_to_dict(ldf) 17 | output_path = os.path.join(snapshot_directory, os.path.basename(ldf)) + '.json' 18 | with open(output_path, 'w+') as output: 19 | json.dump(data, output) 20 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest.mock import patch 3 | import pytest 4 | 5 | from ldfparser.cli import main 6 | 7 | @pytest.mark.unit 8 | @pytest.mark.parametrize('command', [ 9 | ['ldfparser', '-h'], 10 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf'], 11 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'info'], 12 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'info', '--details'], 13 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'export'], 14 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'export', '--output', './tests/tmp/test_cli_lin22.json'], 15 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'node', '--list'], 16 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'node', '--master'], 17 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'node', '--slave', 'LSM'], 18 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'frame', '--list'], 19 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'frame', '--id', '1'], 20 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'frame', '--id', '0x01'], 21 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'frame', '--name', 'LSM_Frm1'], 22 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'signal', '--list'], 23 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'signal', '--name', 'InternalLightsRequest'] 24 | ]) 25 | def test_valid_commands(command): 26 | with pytest.raises(SystemExit) as exit_ex, patch.object(sys, 'argv', command): 27 | main() 28 | assert exit_ex.value.code == 0 29 | 30 | @pytest.mark.unit 31 | @pytest.mark.parametrize('command', [ 32 | ['ldfparser', '--ldf'], 33 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'node', '--slave', 'ABC'], 34 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'frame', '--name', 'ABC'], 35 | ['ldfparser', '--ldf', './tests/ldf/lin22.ldf', 'signal', '--name', 'ABC'] 36 | ]) 37 | def test_invalid_commands(command): 38 | with pytest.raises(SystemExit) as exit_ex, patch.object(sys, 'argv', command): 39 | main() 40 | assert exit_ex.value.code != 0 41 | -------------------------------------------------------------------------------- /tests/test_comment.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | import ldfparser 4 | 5 | @pytest.mark.integration 6 | def test_comment_collection_lin13(): 7 | path = os.path.join(os.path.dirname(__file__), "ldf", "lin13.ldf") 8 | ldf = ldfparser.parse_ldf(path, capture_comments=True) 9 | assert len(ldf.comments) >= 0 10 | assert '// This is a LIN description example file' in ldf.comments 11 | 12 | @pytest.mark.integration 13 | def test_comment_collection_lin20(): 14 | path = os.path.join(os.path.dirname(__file__), "ldf", "lin20.ldf") 15 | ldf = ldfparser.parse_ldf(path, capture_comments=True) 16 | assert len(ldf.comments) >= 0 17 | 18 | @pytest.mark.integration 19 | def test_comment_collection_lin21(): 20 | path = os.path.join(os.path.dirname(__file__), "ldf", "lin21.ldf") 21 | ldf = ldfparser.parse_ldf(path, capture_comments=True) 22 | assert len(ldf.comments) >= 0 23 | assert "// Source: https://lin-cia.org/fileadmin/microsites/lin-cia.org/resources/documents/LIN-Spec_Pac2_1.pdf" in ldf.comments 24 | 25 | @pytest.mark.integration 26 | def test_comment_collection_lin22(): 27 | path = os.path.join(os.path.dirname(__file__), "ldf", "lin22.ldf") 28 | ldf = ldfparser.parse_ldf(path, capture_comments=True) 29 | assert len(ldf.comments) >= 0 30 | assert "// Source: https://lin-cia.org/fileadmin/microsites/lin-cia.org/resources/documents/LIN_2.2A.pdf" in ldf.comments 31 | -------------------------------------------------------------------------------- /tests/test_diagnostics.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ldfparser.diagnostics import ( 4 | LIN_PCI_CONSECUTIVE_FRAME, LIN_PCI_FIRST_FRAME, LIN_PCI_SINGLE_FRAME, LIN_SID_DATA_DUMP, LIN_SID_READ_BY_ID, 5 | LinDiagnosticFrame, LinDiagnosticRequest, LinDiagnosticResponse, pci_byte, rsid 6 | ) 7 | from ldfparser.signal import LinSignal 8 | 9 | @pytest.mark.parametrize( 10 | ('pci_type', 'length', 'expected'), 11 | [ 12 | (LIN_PCI_SINGLE_FRAME, 1, 0b00000001), 13 | (LIN_PCI_SINGLE_FRAME, 6, 0b00000110), 14 | (LIN_PCI_FIRST_FRAME, 10, 0b00011010), 15 | (LIN_PCI_CONSECUTIVE_FRAME, 7, 0b00100111) 16 | ] 17 | ) 18 | @pytest.mark.unit 19 | def test_pci_calculation(pci_type, length, expected): 20 | assert pci_byte(pci_type, length) == expected 21 | 22 | @pytest.mark.parametrize( 23 | ('sid', 'expected'), 24 | [ 25 | (LIN_SID_READ_BY_ID, 0xF2), 26 | (LIN_SID_DATA_DUMP, 0xF4) 27 | ] 28 | ) 29 | @pytest.mark.unit 30 | def test_rsid_calculation(sid, expected): 31 | assert rsid(sid) == expected 32 | 33 | @pytest.fixture(scope="session") 34 | def diagnostic_request(): 35 | frame = LinDiagnosticFrame(0x3C, 'MasterReq', 8, { 36 | 0: LinSignal('MasterReqB0', 8, 0), 37 | 8: LinSignal('MasterReqB1', 8, 0), 38 | 16: LinSignal('MasterReqB2', 8, 0), 39 | 24: LinSignal('MasterReqB3', 8, 0), 40 | 32: LinSignal('MasterReqB4', 8, 0), 41 | 40: LinSignal('MasterReqB5', 8, 0), 42 | 48: LinSignal('MasterReqB6', 8, 0), 43 | 56: LinSignal('MasterReqB7', 8, 0), 44 | }) 45 | return LinDiagnosticRequest(frame) 46 | 47 | @pytest.mark.unit 48 | def test_encode_assign_nad(diagnostic_request): 49 | data = diagnostic_request.encode_assign_nad(0x00, 0x7FFF, 0xFFFF, 0x01) 50 | assert data == b'\x00\x06\xB0\xFF\x7F\xFF\xFF\x01' 51 | 52 | @pytest.mark.unit 53 | def test_encode_conditional_change_nad(diagnostic_request): 54 | data = diagnostic_request.encode_conditional_change_nad(0x7F, 0x01, 0x03, 0x01, 0xFF, 0x01) 55 | assert data == b'\x7F\x06\xB3\x01\x03\x01\xFF\x01' 56 | 57 | @pytest.mark.unit 58 | def test_encode_data_dump(diagnostic_request): 59 | data = diagnostic_request.encode_data_dump(0x01, [0x00, 0x01, 0x02, 0x03, 0x04]) 60 | assert data == b'\x01\x06\xB4\x00\x01\x02\x03\x04' 61 | 62 | @pytest.mark.unit 63 | def test_encode_save_configuration(diagnostic_request): 64 | data = diagnostic_request.encode_save_configuration(0x01) 65 | assert data == b'\x01\x01\xB6\xFF\xFF\xFF\xFF\xFF' 66 | 67 | @pytest.mark.unit 68 | def test_encode_assign_frame_id_range(diagnostic_request): 69 | data = diagnostic_request.encode_assign_frame_id_range(0x01, 0, [0x32, 0x33, 0x34, 0x35]) 70 | assert data == b'\x01\x06\xB7\x00\x32\x33\x34\x35' 71 | 72 | @pytest.mark.unit 73 | def test_encode_read_by_id(diagnostic_request): 74 | data = diagnostic_request.encode_read_by_id(0x00, 0x05, 0x7FFF, 0xFFFF) 75 | assert data == b'\x00\x06\xB2\x05\xFF\x7F\xFF\xFF' 76 | 77 | @pytest.fixture(scope="session") 78 | def diagnostic_response(): 79 | frame = LinDiagnosticFrame(0x3D, 'SlaveResp', 8, { 80 | 0: LinSignal('SlaveRespB0', 8, 0), 81 | 8: LinSignal('SlaveRespB1', 8, 0), 82 | 16: LinSignal('SlaveRespB2', 8, 0), 83 | 24: LinSignal('SlaveRespB3', 8, 0), 84 | 32: LinSignal('SlaveRespB4', 8, 0), 85 | 40: LinSignal('SlaveRespB5', 8, 0), 86 | 48: LinSignal('SlaveRespB6', 8, 0), 87 | 56: LinSignal('SlaveRespB7', 8, 0), 88 | }) 89 | return LinDiagnosticResponse(frame) 90 | 91 | @pytest.mark.unit 92 | def test_decode_assign_frame_id_response(diagnostic_response): 93 | data = diagnostic_response.decode_response(b'\x00\x01\xF0\xFF\xFF\xFF\xFF\xFF') 94 | assert data == { 95 | 'NAD': 0x00, 96 | 'PCI': 0x01, 97 | 'RSID': 0xF0, 98 | 'D1': 0xFF, 99 | 'D2': 0xFF, 100 | 'D3': 0xFF, 101 | 'D4': 0xFF, 102 | 'D5': 0xFF 103 | } 104 | -------------------------------------------------------------------------------- /tests/test_encoding.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ldfparser.signal import LinSignal 4 | from ldfparser.encoding import ( 5 | ASCIIValue, BCDValue, PhysicalValue, LogicalValue, LinSignalEncodingType 6 | ) 7 | 8 | @pytest.mark.unit 9 | def test_encode_physical_unitless_numeric(): 10 | motor_signal = LinSignal('MotorRPM', 8, 0) 11 | physical_value = PhysicalValue(0, 254, 0.3937, 0, 'rpm') 12 | assert physical_value.encode(0, motor_signal) == 0 13 | assert physical_value.encode(100, motor_signal) == 254 14 | 15 | with pytest.raises(ValueError): 16 | physical_value.encode(-1, motor_signal) 17 | 18 | with pytest.raises(ValueError): 19 | physical_value.encode(101, motor_signal) 20 | 21 | @pytest.mark.unit 22 | def test_encode_physical_unitless_string(): 23 | motor_signal = LinSignal('MotorRPM', 8, 0) 24 | physical_value = PhysicalValue(0, 254, 0.3937, 0, 'rpm') 25 | assert physical_value.encode('0', motor_signal) == 0 26 | assert physical_value.encode('100', motor_signal) == 254 27 | 28 | with pytest.raises(ValueError): 29 | physical_value.encode('-1', motor_signal) 30 | 31 | with pytest.raises(ValueError): 32 | physical_value.encode('101', motor_signal) 33 | 34 | @pytest.mark.unit 35 | def test_encode_physical_string_with_unit(): 36 | motor_signal = LinSignal('MotorRPM', 8, 0) 37 | physical_value = PhysicalValue(0, 254, 0.3937, 0, 'rpm') 38 | assert physical_value.encode('0rpm', motor_signal) == 0 39 | assert physical_value.encode('100rpm', motor_signal) == 254 40 | assert physical_value.encode('100 rpm', motor_signal) == 254 41 | 42 | with pytest.raises(ValueError): 43 | physical_value.encode('-1rpm', motor_signal) 44 | 45 | with pytest.raises(ValueError): 46 | physical_value.encode('101rpm', motor_signal) 47 | 48 | @pytest.mark.unit 49 | def test_encode_physical_string_with_wrong_unit(): 50 | motor_signal = LinSignal('MotorRPM', 8, 0) 51 | physical_value = PhysicalValue(0, 254, 0.3937, 0, 'rpm') 52 | with pytest.raises(ValueError): 53 | physical_value.encode('50deg', motor_signal) 54 | 55 | with pytest.raises(ValueError): 56 | physical_value.encode('101deg', motor_signal) 57 | 58 | @pytest.mark.unit 59 | def test_encode_physical_scale_zero(): 60 | motor_signal = LinSignal('MotorRPM', 8, 0) 61 | value = PhysicalValue(100, 255, 0, 100, 'rpm') 62 | assert value.encode(100, motor_signal) == 100 63 | assert value.encode(120, motor_signal) == 100 64 | 65 | @pytest.mark.unit 66 | def test_decode_physical_valid(): 67 | motor_signal = LinSignal('MotorRPM', 8, 0) 68 | physical_value = PhysicalValue(0, 254, 0.3937, 0, 'rpm') 69 | assert physical_value.decode(0, motor_signal) == 0 70 | assert abs(physical_value.decode(254, motor_signal) - 100.0) < 0.01 71 | 72 | @pytest.mark.unit 73 | def test_decode_physical_invalid(): 74 | motor_signal = LinSignal('MotorRPM', 8, 0) 75 | physical_value = PhysicalValue(0, 254, 0.3937, 0, 'rpm') 76 | with pytest.raises(ValueError): 77 | physical_value.decode(-1, motor_signal) 78 | 79 | @pytest.mark.unit 80 | def test_encode_logical(): 81 | motor_signal = LinSignal('MotorRPM', 8, 0) 82 | logical_value = LogicalValue(1, "on") 83 | assert logical_value.encode("on", motor_signal) == 1 84 | 85 | with pytest.raises(ValueError): 86 | logical_value.encode('off', motor_signal) 87 | 88 | @pytest.mark.unit 89 | def test_decode_logical(): 90 | motor_signal = LinSignal('MotorRPM', 8, 0) 91 | logical_value = LogicalValue(1, "on") 92 | assert logical_value.decode(1, motor_signal) == "on" 93 | 94 | with pytest.raises(ValueError): 95 | logical_value.decode(0, motor_signal) 96 | 97 | @pytest.mark.unit 98 | def test_encode_logical_no_signal_info(): 99 | motor_signal = LinSignal('MotorRPM', 8, 0) 100 | logical_value = LogicalValue(1) 101 | 102 | assert logical_value.encode(1, motor_signal) == 1 103 | 104 | with pytest.raises(ValueError): 105 | logical_value.encode(0, motor_signal) 106 | 107 | @pytest.mark.unit 108 | def test_decode_logical_no_signal_info(): 109 | motor_signal = LinSignal('MotorRPM', 8, 0) 110 | logical_value = LogicalValue(1) 111 | 112 | assert logical_value.decode(1, motor_signal) == 1 113 | 114 | with pytest.raises(ValueError): 115 | logical_value.decode(0, motor_signal) 116 | 117 | @pytest.mark.unit 118 | def test_encode_bcd(): 119 | bcd_value = BCDValue() 120 | 121 | assert bcd_value.encode(1, LinSignal('Counter', 8, [0])) == [1] 122 | assert bcd_value.encode(12, LinSignal('Counter', 16, [0, 0])) == [1, 2] 123 | assert bcd_value.encode(123, LinSignal('Counter', 24, [0, 0, 0])) == [1, 2, 3] 124 | assert bcd_value.encode(1234, LinSignal('Counter', 32, [0, 0, 0, 0])) == [1, 2, 3, 4] 125 | assert bcd_value.encode(12345, LinSignal('Counter', 40, [0, 0, 0, 0, 0])) == [1, 2, 3, 4, 5] 126 | assert bcd_value.encode(123456, LinSignal('Counter', 48, [0, 0, 0, 0, 0, 0])) == [1, 2, 3, 4, 5, 6] 127 | 128 | @pytest.mark.unit 129 | def test_encode_bcd_out_of_bounds(): 130 | bcd_value = BCDValue() 131 | 132 | with pytest.raises(ValueError): 133 | bcd_value.encode(123, LinSignal('Counter', 16, [0, 0])) 134 | 135 | @pytest.mark.unit 136 | def test_decode_bcd(): 137 | bcd_value = BCDValue() 138 | 139 | assert bcd_value.decode([1], LinSignal('Counter', 8, [0])) == 1 140 | assert bcd_value.decode([1, 2], LinSignal('Counter', 16, [0, 0])) == 12 141 | assert bcd_value.decode([1, 2, 3], LinSignal('Counter', 24, [0, 0, 0])) == 123 142 | assert bcd_value.decode([1, 2, 3, 4], LinSignal('Counter', 32, [0, 0, 0, 0])) == 1234 143 | assert bcd_value.decode([1, 2, 3, 4, 5], LinSignal('Counter', 40, [0, 0, 0, 0, 0])) == 12345 144 | assert bcd_value.decode([1, 2, 3, 4, 5, 6], LinSignal('Counter', 48, [0, 0, 0, 0, 0, 0])) == 123456 145 | 146 | @pytest.mark.unit 147 | def test_decode_bcd_invalid_value(): 148 | bcd_value = BCDValue() 149 | 150 | with pytest.raises(ValueError): 151 | bcd_value.decode([0x67, 0x67, 0x67], LinSignal('Counter', 24, [0, 0, 0])) 152 | 153 | @pytest.mark.unit 154 | def test_encode_ascii(): 155 | id_signal = LinSignal('Id', 48, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 156 | ascii_value = ASCIIValue() 157 | 158 | assert ascii_value.encode('ABC123', id_signal) == [65, 66, 67, 49, 50, 51] 159 | 160 | @pytest.mark.unit 161 | def test_decode_ascii(): 162 | id_signal = LinSignal('Id', 48, [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]) 163 | ascii_value = ASCIIValue() 164 | 165 | assert ascii_value.decode([65, 66, 67, 49, 50, 51], id_signal) == 'ABC123' 166 | 167 | @pytest.mark.integration 168 | def test_encode_signal_scalar(): 169 | motor_signal = LinSignal('MotorRPM', 8, 0) 170 | off_value = LogicalValue(0, 'off') 171 | motor_speed = PhysicalValue(1, 99, 1, 0, 'rpm') 172 | overdrive = PhysicalValue(100, 255, 0, 100) 173 | 174 | signal_type = LinSignalEncodingType('MotorType', [motor_speed, overdrive, off_value]) 175 | assert signal_type.encode('off', motor_signal) == 0 176 | assert signal_type.encode(99, motor_signal) == 99 177 | assert signal_type.encode(101, motor_signal) == 100 178 | assert signal_type.encode(120, motor_signal) == 100 179 | 180 | @pytest.mark.integration 181 | def test_encode_signal_bcd(): 182 | counter_signal = LinSignal('Counter', 24, [0, 1, 2]) 183 | counter_value = BCDValue() 184 | 185 | signal_type = LinSignalEncodingType('CounterType', [counter_value]) 186 | assert signal_type.encode(1, counter_signal) == [0, 0, 1] 187 | assert signal_type.encode(12, counter_signal) == [0, 1, 2] 188 | assert signal_type.encode(123, counter_signal) == [1, 2, 3] 189 | 190 | @pytest.mark.integration 191 | def test_encode_signal_ascii(): 192 | text_signal = LinSignal('Text', 24, list("ABC")) 193 | text_value = ASCIIValue() 194 | 195 | signal_type = LinSignalEncodingType('TextType', [text_value]) 196 | assert signal_type.encode("ABC", text_signal) == [65, 66, 67] 197 | -------------------------------------------------------------------------------- /tests/test_frame.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ldfparser.frame import LinUnconditionalFrame 4 | from ldfparser.signal import LinSignal 5 | from ldfparser.encoding import LinSignalEncodingType, LogicalValue, PhysicalValue 6 | 7 | @pytest.mark.unit 8 | def test_frame_raw_encoding(): 9 | signal1 = LinSignal('Signal_1', 8, 0) 10 | signal2 = LinSignal('Signal_2', 4, 0) 11 | signal3 = LinSignal('Signal_3', 1, 0) 12 | 13 | frame = LinUnconditionalFrame(1, 'Frame_1', 2, {0: signal1, 8: signal2, 15: signal3}) 14 | content = frame.raw({ 15 | 'Signal_1': 100, 16 | 'Signal_2': 10, 17 | 'Signal_3': 1 18 | }) 19 | 20 | assert list(content) == [100, 10 | 1 << 7] 21 | 22 | @pytest.mark.unit 23 | def test_frame_raw_encoding_zero(): 24 | signal1 = LinSignal('Signal_1', 8, 255) 25 | signal2 = LinSignal('Signal_2', 4, 15) 26 | signal3 = LinSignal('Signal_3', 1, 1) 27 | 28 | frame = LinUnconditionalFrame(1, 'Frame_1', 2, {0: signal1, 8: signal2, 15: signal3}) 29 | content = frame.raw({ 30 | 'Signal_1': 0, 31 | 'Signal_2': 10, 32 | 'Signal_3': 1 33 | }) 34 | 35 | assert list(content) == [0, 10 | 1 << 7] 36 | 37 | @pytest.mark.unit 38 | def test_frame_raw_encoding_no_signal(): 39 | signal1 = LinSignal('Signal_1', 8, 255) 40 | signal2 = LinSignal('Signal_2', 4, 255) 41 | signal3 = LinSignal('Signal_3', 1, 255) 42 | 43 | frame = LinUnconditionalFrame(1, 'Frame_1', 2, {0: signal1, 8: signal2, 15: signal3}) 44 | content = frame.raw({ 45 | 'Signal_2': 10, 46 | 'Signal_3': 1 47 | }) 48 | 49 | assert list(content) == [255, 10 | 1 << 7] 50 | 51 | @pytest.mark.unit 52 | def test_frame_raw_encoding_array(): 53 | signal1 = LinSignal('Signal_1', 16, [0, 0]) 54 | frame = LinUnconditionalFrame(1, 'Frame_1', 2, {0: signal1}) 55 | content = frame.raw({ 56 | 'Signal_1': [1, 2] 57 | }) 58 | assert list(content) == [1, 2] 59 | 60 | @pytest.mark.unit 61 | def test_frame_raw_encoding_array2(): 62 | signal1 = LinSignal('Signal_1', 16, [0, 0]) 63 | signal2 = LinSignal('Signal_2', 8, 0) 64 | frame = LinUnconditionalFrame(1, 'Frame_1', 3, {0: signal1, 16: signal2}) 65 | content = frame.raw({ 66 | 'Signal_1': [1, 2], 67 | 'Signal_2': 3 68 | }) 69 | assert list(content) == [1, 2, 3] 70 | 71 | @pytest.mark.unit 72 | def test_frame_raw_decoding_array(): 73 | signal1 = LinSignal('Signal_1', 16, [0, 0]) 74 | frame = LinUnconditionalFrame(1, 'Frame_1', 2, {0: signal1}) 75 | assert frame.parse_raw(bytearray([1, 2])) == {"Signal_1": [1, 2]} 76 | 77 | @pytest.mark.unit 78 | def test_frame_raw_decoding_array2(): 79 | signal1 = LinSignal('Signal_1', 16, [0, 0]) 80 | signal2 = LinSignal('Signal_2', 8, 0) 81 | frame = LinUnconditionalFrame(1, 'Frame_1', 3, {0: signal1, 16: signal2}) 82 | assert frame.parse_raw(bytearray([1, 2, 3])) == {"Signal_1": [1, 2], "Signal_2": 3} 83 | 84 | @pytest.mark.unit 85 | def test_frame_raw_encoding_out_of_range(): 86 | signal1 = LinSignal('Signal_1', 8, 0) 87 | signal2 = LinSignal('Signal_2', 4, 0) 88 | signal3 = LinSignal('Signal_3', 1, 0) 89 | 90 | frame = LinUnconditionalFrame(1, 'Frame_1', 2, {0: signal1, 8: signal2, 15: signal3}) 91 | with pytest.raises(Exception): 92 | frame.raw({ 93 | 'Signal_1': 100, 94 | 'Signal_2': 30, 95 | 'Signal_3': 1 96 | }) 97 | 98 | @pytest.mark.unit 99 | def test_frame_signals_overlapping(): 100 | signal1 = LinSignal('Signal_1', 8, 0) 101 | signal2 = LinSignal('Signal_2', 4, 0) 102 | signal3 = LinSignal('Signal_3', 1, 0) 103 | 104 | with pytest.raises(ValueError): 105 | LinUnconditionalFrame(1, 'Frame_1', 2, {0: signal1, 7: signal2, 15: signal3}) 106 | 107 | @pytest.mark.unit 108 | def test_frame_signal_out_of_frame(): 109 | signal1 = LinSignal('Signal_1', 8, 0) 110 | signal2 = LinSignal('Signal_2', 4, 0) 111 | 112 | with pytest.raises(ValueError): 113 | LinUnconditionalFrame(1, 'Frame_1', 2, {0: signal1, 14: signal2}) 114 | 115 | @pytest.mark.unit 116 | def test_frame_encode_data(): 117 | motor_speed = LinSignal('MotorSpeed', 7, 0) 118 | motor_values = [ 119 | LogicalValue(0, 'off'), 120 | PhysicalValue(1, 99, 1, 0, 'rpm'), 121 | PhysicalValue(100, 128, 0, 100)] 122 | 123 | temperature = LinSignal('Temperature', 8, 255) 124 | temperature_values = [ 125 | LogicalValue(0, 'MEASUREMENT_ERROR'), 126 | PhysicalValue(1, 255, 1, -50, 'C')] 127 | 128 | error_state = LinSignal('Error', 1, 0) 129 | error_values = [ 130 | LogicalValue(0, 'NO_ERROR'), 131 | LogicalValue(1, 'ERROR')] 132 | 133 | converters = { 134 | 'MotorSpeed': LinSignalEncodingType('MotorSpeedType', motor_values), 135 | 'Temperature': LinSignalEncodingType('TemperatureType', temperature_values), 136 | 'Error': LinSignalEncodingType('ErrorType', error_values) 137 | } 138 | 139 | frame = LinUnconditionalFrame(1, 'Status', 2, {0: motor_speed, 7: error_state, 8: temperature}) 140 | frame.data( 141 | { 142 | 'Temperature': -30, 143 | 'MotorSpeed': '50rpm', 144 | 'Error': 'NO_ERROR' 145 | }, 146 | converters 147 | ) 148 | 149 | @pytest.mark.unit 150 | def test_frame_encode_data_missing_encoder(): 151 | motor_speed = LinSignal('MotorSpeed', 8, 0) 152 | motor_values = [PhysicalValue(0, 255, 0, 100)] 153 | 154 | converters = { 155 | 'MotorSpeed': LinSignalEncodingType('MotorSpeedType', motor_values) 156 | } 157 | frame = LinUnconditionalFrame(1, 'Status', 1, {0: motor_speed}) 158 | 159 | with pytest.raises(ValueError): 160 | frame.data({'MissingSignal': 0}, converters) 161 | 162 | @pytest.mark.unit 163 | def test_frame_decode_data(): 164 | motor_speed = LinSignal('MotorSpeed', 7, 0) 165 | motor_values = [ 166 | LogicalValue(0, 'off'), 167 | PhysicalValue(1, 99, 1, 0, 'rpm'), 168 | PhysicalValue(100, 128, 0, 100)] 169 | 170 | temperature = LinSignal('Temperature', 8, 255) 171 | temperature_values = [ 172 | LogicalValue(0, 'MEASUREMENT_ERROR'), 173 | PhysicalValue(1, 255, 1, -50, 'C')] 174 | 175 | error_state = LinSignal('Error', 1, 0) 176 | error_values = [ 177 | LogicalValue(0, 'NO_ERROR'), 178 | LogicalValue(1, 'ERROR')] 179 | 180 | converters = { 181 | 'MotorSpeed': LinSignalEncodingType('MotorSpeedType', motor_values), 182 | 'Temperature': LinSignalEncodingType('TemperatureType', temperature_values), 183 | 'Error': LinSignalEncodingType('ErrorType', error_values) 184 | } 185 | 186 | frame = LinUnconditionalFrame(1, 'Status', 2, {0: motor_speed, 7: error_state, 8: temperature}) 187 | frame.parse( 188 | [0x88, 0x88], 189 | converters 190 | ) 191 | 192 | @pytest.mark.unit 193 | def test_frame_decode_data_missing_decoder(): 194 | motor_speed = LinSignal('MotorSpeed', 8, 0) 195 | 196 | converters = {} 197 | frame = LinUnconditionalFrame(1, 'Status', 1, {0: motor_speed}) 198 | 199 | with pytest.raises(ValueError): 200 | frame.parse([0x88], converters) 201 | 202 | # 203 | # 204 | # 205 | 206 | @pytest.fixture(scope="function") 207 | def frame(): 208 | motor_signal = LinSignal('MotorSpeed', 8, 0xFF) 209 | temp_signal = LinSignal('InternalTemperature', 6, 0x3F) 210 | reserved1_signal = LinSignal('Reserved1', 4, 0) 211 | int_error_signal = LinSignal('InternalError', 1, 0) 212 | comm_error_signal = LinSignal('CommError', 1, 0) 213 | return LinUnconditionalFrame(0x20, 'EcuStatus', 3, { 214 | 0: motor_signal, 215 | 8: temp_signal, 216 | 14: reserved1_signal, 217 | 18: int_error_signal, 218 | 19: comm_error_signal 219 | }) 220 | 221 | @pytest.fixture(scope="function") 222 | def range_type(): 223 | return LinSignalEncodingType('MotorSpeedType', [PhysicalValue(0, 255, 50, 0, 'rpm')]) 224 | 225 | @pytest.mark.unit 226 | class TestLinUnconditionalFrameEncodingRaw: 227 | 228 | def test_encode_raw_partial(self, frame): 229 | data = frame.encode_raw({ 230 | 'MotorSpeed': 100, 231 | 'CommError': 1 232 | }) 233 | assert data == b'\x64\x3F\x08' 234 | 235 | @pytest.mark.parametrize( 236 | ['data', 'expected'], 237 | [ 238 | ({ 239 | 'MotorSpeed': 0x64, 240 | 'InternalTemperature': 0x32, 241 | 'Reserved1': 0, 242 | 'InternalError': 0, 243 | 'CommError': 1 244 | }, b'\x64\x32\x08') 245 | ] 246 | ) 247 | def test_encode_raw(self, frame, data, expected): 248 | assert frame.encode_raw(data) == expected 249 | 250 | @pytest.mark.unit 251 | class TestLinUnconditionalFrameEncoding: 252 | 253 | def test_encode_builtin(self, frame, range_type): 254 | frame._get_signal('MotorSpeed').encoding_type = range_type 255 | encoded = frame.encode({'MotorSpeed': '1000rpm'}) 256 | assert encoded == b'\x14\x3F\x00' 257 | 258 | def test_encode_custom(self, frame, range_type): 259 | encoded = frame.encode({'MotorSpeed': '1000rpm'}, {'MotorSpeed': range_type}) 260 | assert encoded == b'\x14\x3F\x00' 261 | 262 | def test_encode_int(self, frame): 263 | encoded = frame.encode({'MotorSpeed': 40}) 264 | assert encoded == b'\x28\x3F\x00' 265 | 266 | @pytest.mark.parametrize( 267 | 'value', ['1000rpm', '1000', 1000.0] 268 | ) 269 | def test_encode_error_type(self, frame, value): 270 | with pytest.raises(ValueError): 271 | frame.encode({'MotorSpeed': value}) 272 | 273 | @pytest.mark.unit 274 | class TestEncodeDecodeArray: 275 | """Test encode/decode of signal with array values""" 276 | @pytest.mark.parametrize("use_converter", [True, False]) 277 | def test_encode_decode_array(self, use_converter): 278 | signal = LinSignal('BattCurr', 24, [0, 0, 2]) 279 | encoding_type = LinSignalEncodingType( 280 | "BattCurrCoding", 281 | [ 282 | PhysicalValue(0, 182272, 0.00390625, -512, "A"), 283 | LogicalValue(16777215, "Invalid") 284 | ] 285 | ) 286 | converters = {} 287 | if use_converter: 288 | converters["BattCurr"] = encoding_type 289 | else: 290 | signal.encoding_type = encoding_type 291 | 292 | frame = LinUnconditionalFrame(0x20, "LinStatus", 3, {0: signal}) 293 | 294 | # Logical value 295 | invalid_value = {"BattCurr": "Invalid"} 296 | encoded = frame.encode(invalid_value, converters) 297 | assert encoded == bytearray([255, 255, 255]) 298 | decoded = frame.decode(encoded, converters) 299 | assert decoded == invalid_value 300 | 301 | # Physical value 302 | valid_value = {"BattCurr": -254.99609375} 303 | encoded = frame.encode(valid_value, converters) 304 | assert encoded == bytearray([1, 1, 1]) 305 | decoded = frame.decode(encoded, converters) 306 | assert decoded == valid_value 307 | 308 | def test_encode_decode_array_no_converter(self): 309 | signal = LinSignal('BattCurr', 24, [0, 0, 2]) 310 | frame = LinUnconditionalFrame(0x20, "LinStatus", 3, {0: signal}) 311 | raw = {"BattCurr": [1, 1, 1]} 312 | encoded_expected = bytearray([1, 1, 1]) 313 | 314 | encoded = frame.encode(raw) 315 | assert encoded == encoded_expected 316 | 317 | decoded = frame.decode(encoded) 318 | assert decoded == raw 319 | 320 | def test_encode_decode_array_default_no_converter(self): 321 | signal = LinSignal('BattCurr', 24, [0, 0, 2]) 322 | frame = LinUnconditionalFrame(0x20, "LinStatus", 3, {0: signal}) 323 | 324 | encoded_expected = bytearray([0, 0, 2]) 325 | decode_expected = {'BattCurr': [0, 0, 2]} 326 | 327 | encoded = frame.encode({}) 328 | assert encoded == encoded_expected 329 | 330 | decoded = frame.decode(encoded) 331 | assert decoded == decode_expected 332 | 333 | 334 | @pytest.mark.unit 335 | class TestLinUnconditionalFrameDecodingRaw: 336 | 337 | @pytest.mark.parametrize( 338 | ['data', 'expected'], 339 | [ 340 | (b'\x64\x32\x08', { 341 | 'MotorSpeed': 0x64, 342 | 'InternalTemperature': 0x32, 343 | 'Reserved1': 0, 344 | 'InternalError': 0, 345 | 'CommError': 1 346 | }), 347 | (b'\x20\x3F\x08', { 348 | 'MotorSpeed': 0x20, 349 | 'InternalTemperature': 0x3F, 350 | 'Reserved1': 0, 351 | 'InternalError': 0, 352 | 'CommError': 1 353 | }) 354 | ] 355 | ) 356 | def test_decode_raw(self, frame, data, expected): 357 | assert frame.decode_raw(data) == expected 358 | 359 | @pytest.mark.unit 360 | class TestLinUnconditionalFrameDecoding: 361 | 362 | def test_decode_builtin(self, frame, range_type): 363 | frame._get_signal('MotorSpeed').encoding_type = range_type 364 | decoded = frame.decode(b'\x20\x3F\x08') 365 | assert decoded['MotorSpeed'] == 1600.0 366 | 367 | def test_decode_custom(self, frame, range_type): 368 | decoded = frame.decode(b'\x20\x3F\x08', {'MotorSpeed': range_type}) 369 | assert decoded['MotorSpeed'] == 1600.0 370 | 371 | def test_decode_int(self, frame): 372 | decoded = frame.decode(b'\x20\x3F\x08') 373 | assert decoded['MotorSpeed'] == 0x20 374 | 375 | def test_decode_with_unit(self, frame, range_type): 376 | decoded = frame.decode(b'\x20\x3F\x08', {'MotorSpeed': range_type}, keep_unit=True) 377 | assert decoded['MotorSpeed'] == '1600.000 rpm' 378 | 379 | @pytest.mark.unit 380 | def test_frame_encoding_with_optional_padding1(): 381 | signal1 = LinSignal('Signal_1', 8, 255) 382 | signal2 = LinSignal('Signal_2', 4, 255) 383 | signal3 = LinSignal('Signal_3', 1, 255) 384 | 385 | frame = LinUnconditionalFrame(1, 'Frame_1', 2, {0: signal1, 8: signal2, 15: signal3}, pad_with_zero=False) 386 | content = frame.encode_raw({ 387 | 'Signal_2': 10, 388 | 'Signal_3': 1 389 | }) 390 | 391 | assert list(content) == [255, 250] # 10 | ( 1 << 7 | 0x70) = 250 392 | -------------------------------------------------------------------------------- /tests/test_json.py: -------------------------------------------------------------------------------- 1 | import difflib 2 | import glob 3 | import json 4 | import os 5 | import warnings 6 | import pytest 7 | 8 | from ldfparser.parser import parse_ldf_to_dict 9 | 10 | ldf_directory = os.path.join(os.path.dirname(__file__), 'ldf') 11 | snapshot_directory = os.path.join(os.path.dirname(__file__), 'snapshot') 12 | ldf_files = glob.glob(ldf_directory + '/*.ldf') 13 | 14 | @pytest.mark.snapshot 15 | @pytest.mark.parametrize( 16 | ('ldf_path'), 17 | ldf_files 18 | ) 19 | def test_compare_json(ldf_path): 20 | snapshot_file = os.path.join(snapshot_directory, os.path.basename(ldf_path)) + '.json' 21 | if not os.path.exists(snapshot_file): 22 | warnings.warn(f'Snapshot for {ldf_path} not found!') 23 | pytest.skip('Snapshot not found.') 24 | with open(snapshot_file, 'r') as snapshot: 25 | snapshot_content = snapshot.read() 26 | current = json.dumps(parse_ldf_to_dict(ldf_path)) 27 | diff = ''.join(difflib.unified_diff(snapshot_content, current)) 28 | assert not bool(diff), f"{ldf_path} not matching snapshot: {diff}" 29 | -------------------------------------------------------------------------------- /tests/test_lin.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=invalid-name 2 | import pytest 3 | from ldfparser.lin import LIN_VERSION_1_3, LIN_VERSION_2_0, LIN_VERSION_2_1, LIN_VERSION_2_2, LinVersion, Iso17987Version, parse_lin_version, J2602Version 4 | 5 | @pytest.mark.unit() 6 | @pytest.mark.parametrize( 7 | ('version', 'expected'), 8 | [ 9 | (LIN_VERSION_1_3, "1.3"), 10 | (LIN_VERSION_2_0, "2.0"), 11 | (LIN_VERSION_2_1, "2.1"), 12 | (LIN_VERSION_2_2, "2.2") 13 | ] 14 | ) 15 | def test_linversion_str(version, expected): 16 | assert str(version) == expected 17 | 18 | @pytest.mark.unit() 19 | @pytest.mark.parametrize( 20 | ('a', 'b'), 21 | [ 22 | (LIN_VERSION_1_3, LIN_VERSION_1_3), 23 | (LIN_VERSION_2_0, LIN_VERSION_2_0), 24 | (LIN_VERSION_2_1, LIN_VERSION_2_1) 25 | ] 26 | ) 27 | def test_linversion_equal_version(a, b): 28 | assert a == b 29 | 30 | @pytest.mark.unit() 31 | @pytest.mark.parametrize( 32 | ('a', 'b'), 33 | [ 34 | (LIN_VERSION_1_3, LIN_VERSION_2_1), 35 | (LIN_VERSION_2_0, LIN_VERSION_2_1), 36 | (LIN_VERSION_2_2, LIN_VERSION_2_1), 37 | (LIN_VERSION_2_2, "2.2") 38 | ] 39 | ) 40 | def test_linversion_not_equal_version(a, b): 41 | assert a != b 42 | 43 | @pytest.mark.unit() 44 | @pytest.mark.parametrize( 45 | ('a', 'b'), 46 | [ 47 | (LIN_VERSION_1_3, LIN_VERSION_2_1), 48 | (LIN_VERSION_2_0, LIN_VERSION_2_1), 49 | (LIN_VERSION_2_1, LIN_VERSION_2_2) 50 | ] 51 | ) 52 | def test_linversion_less_than_version(a, b): 53 | assert a < b 54 | 55 | @pytest.mark.unit() 56 | @pytest.mark.parametrize( 57 | ('a', 'b'), 58 | [ 59 | (LIN_VERSION_2_0, LIN_VERSION_1_3), 60 | (LIN_VERSION_2_1, LIN_VERSION_2_0), 61 | (LIN_VERSION_2_2, LIN_VERSION_2_1) 62 | ] 63 | ) 64 | def test_linversion_greater_than_version(a, b): 65 | assert a > b 66 | 67 | @pytest.mark.unit() 68 | @pytest.mark.parametrize( 69 | ('a', 'b'), 70 | [ 71 | (LIN_VERSION_1_3, LIN_VERSION_1_3), 72 | (LIN_VERSION_1_3, LIN_VERSION_2_0), 73 | (LIN_VERSION_1_3, LIN_VERSION_2_1) 74 | ] 75 | ) 76 | def test_linversion_less_than_equal_version(a, b): 77 | assert a <= b 78 | 79 | @pytest.mark.unit() 80 | @pytest.mark.parametrize( 81 | ('a', 'b'), 82 | [ 83 | (LIN_VERSION_2_0, LIN_VERSION_1_3), 84 | (LIN_VERSION_2_0, LIN_VERSION_2_0), 85 | (LIN_VERSION_2_1, LIN_VERSION_2_0) 86 | ] 87 | ) 88 | def test_linversion_greater_than_equal_version(a, b): 89 | assert a >= b 90 | 91 | @pytest.mark.unit() 92 | @pytest.mark.parametrize( 93 | ('version', 'expected'), 94 | [ 95 | (LIN_VERSION_1_3, 1.3), 96 | (LIN_VERSION_2_0, 2.0), 97 | (LIN_VERSION_2_1, 2.1), 98 | (LIN_VERSION_2_2, 2.2) 99 | ] 100 | ) 101 | def test_linversion_float(version, expected): 102 | assert float(version) == expected 103 | 104 | @pytest.mark.unit() 105 | @pytest.mark.parametrize( 106 | ('func', 'arg'), 107 | [ 108 | (LIN_VERSION_1_3.__gt__, "2.0"), 109 | (LIN_VERSION_1_3.__lt__, "2.0"), 110 | (LIN_VERSION_1_3.__ge__, "2.0"), 111 | (LIN_VERSION_1_3.__le__, "2.0") 112 | ] 113 | ) 114 | def test_linversion_typerror(func, arg): 115 | with pytest.raises(TypeError): 116 | func(arg) 117 | 118 | @pytest.mark.unit() 119 | @pytest.mark.parametrize( 120 | ('a', 'b', 'op', 'result'), 121 | [ 122 | (Iso17987Version(2015), Iso17987Version(2015), Iso17987Version.__eq__, True), 123 | (Iso17987Version(2015), Iso17987Version(2016), Iso17987Version.__eq__, False), 124 | (Iso17987Version(2015), Iso17987Version(2015), Iso17987Version.__ne__, False), 125 | (Iso17987Version(2015), Iso17987Version(2016), Iso17987Version.__ne__, True), 126 | (Iso17987Version(2015), Iso17987Version(2016), Iso17987Version.__gt__, False), 127 | (Iso17987Version(2015), LIN_VERSION_2_0, Iso17987Version.__gt__, True), 128 | (Iso17987Version(2015), Iso17987Version(2015), Iso17987Version.__ge__, True), 129 | (Iso17987Version(2015), Iso17987Version(2016), Iso17987Version.__lt__, True), 130 | (Iso17987Version(2015), LIN_VERSION_2_0, Iso17987Version.__lt__, False), 131 | (Iso17987Version(2015), Iso17987Version(2015), Iso17987Version.__le__, True) 132 | ] 133 | ) 134 | def test_linversion_iso_compare(a, b, op, result): 135 | assert op(a, b) == result 136 | 137 | @pytest.mark.parametrize( 138 | ('a', 'b', 'op', 'exc'), 139 | [ 140 | (Iso17987Version(2015), 2015, Iso17987Version.__gt__, TypeError), 141 | (Iso17987Version(2015), 2015, Iso17987Version.__lt__, TypeError) 142 | ] 143 | ) 144 | def test_linversion_iso_invalid(a, b, op, exc): 145 | with pytest.raises(exc): 146 | op(a, b) 147 | 148 | @pytest.mark.unit() 149 | @pytest.mark.parametrize( 150 | ('value'), 151 | [ 152 | '', 153 | '2.1.2', 154 | 'ISO17987:abc', 155 | 'J2602_1_1_1.0', 156 | ] 157 | ) 158 | def test_linversion_parse_invalid(value): 159 | with pytest.raises(ValueError): 160 | parse_lin_version(value) 161 | 162 | @pytest.mark.unit() 163 | @pytest.mark.parametrize( 164 | ('value', 'major', 'minor', 'part'), 165 | [ 166 | ('J2602_1_1.0', 1, 0, 1), 167 | ('J2602_3_1.0', 1, 0, 3), 168 | ('J2602_1_1.1', 1, 1, 1), 169 | ] 170 | ) 171 | def test_linversion_j2602_valid(value, major, minor, part): 172 | version = J2602Version.from_string(value) 173 | assert version.major == major 174 | assert version.minor == minor 175 | assert version.part == part 176 | assert str(J2602Version(major=major, minor=minor, part=part)) == value 177 | 178 | @pytest.mark.unit() 179 | @pytest.mark.parametrize( 180 | ('value'), 181 | [ 182 | '', 183 | '1.2', 184 | 'ISO17987:2016', 185 | 'J2602_1_2.0' 186 | ] 187 | ) 188 | def test_linversion_unsupported_j2602(value): 189 | with pytest.raises(ValueError): 190 | J2602Version.from_string(value) 191 | 192 | @pytest.mark.unit() 193 | @pytest.mark.parametrize( 194 | ('value', 'expected'), [ 195 | ('2.0', LIN_VERSION_2_0), 196 | ('ISO17987:2015', Iso17987Version(2015)), 197 | ('J2602_3_1.0', J2602Version(1, 0, 3)) 198 | ] 199 | ) 200 | def test_parse_linversion(value, expected): 201 | version = parse_lin_version(value) 202 | assert version == expected 203 | 204 | @pytest.mark.unit() 205 | @pytest.mark.parametrize( 206 | 'a, b, op, result', [ 207 | (J2602Version(1, 0, 1), LIN_VERSION_2_0, J2602Version.__eq__, True), 208 | (J2602Version(1, 0, 1), J2602Version(1, 1, 1), J2602Version.__eq__, False), 209 | (J2602Version(1, 0, 1), J2602Version(1, 0, 1), J2602Version.__eq__, True), 210 | (J2602Version(2, 0, 1), J2602Version(1, 0, 1), J2602Version.__gt__, True), 211 | (J2602Version(1, 0, 1), J2602Version(2, 0, 1), J2602Version.__lt__, True), 212 | (J2602Version(1, 0, 1), "Other type", J2602Version.__eq__, False), 213 | (J2602Version(1, 0, 1), LIN_VERSION_2_2, J2602Version.__eq__, False), 214 | (J2602Version(1, 0, 1), Iso17987Version(2015), J2602Version.__eq__, False), 215 | (J2602Version(1, 0, 1), LIN_VERSION_2_2, J2602Version.__lt__, True), 216 | (J2602Version(1, 0, 1), Iso17987Version(2015), J2602Version.__lt__, True), 217 | (J2602Version(1, 0, 1), LIN_VERSION_1_3, J2602Version.__gt__, True), 218 | (J2602Version(1, 0, 1), LIN_VERSION_1_3, J2602Version.__lt__, False), 219 | (J2602Version(1, 0, 1), Iso17987Version(2015), J2602Version.__gt__, False), 220 | (J2602Version(1, 0, 1), Iso17987Version(2015), J2602Version.__ne__, True), 221 | (J2602Version(1, 0, 1), Iso17987Version(2015), J2602Version.__ge__, False), 222 | (J2602Version(1, 0, 1), Iso17987Version(2015), J2602Version.__le__, True), 223 | (J2602Version(1, 0, 1), LIN_VERSION_2_2, J2602Version.__ne__, True), 224 | (LIN_VERSION_2_0, J2602Version(1, 0, 1), LinVersion.__eq__, True), 225 | (LIN_VERSION_2_2, J2602Version(1, 0, 1), LinVersion.__gt__, True), 226 | (LIN_VERSION_1_3, J2602Version(1, 0, 1), LinVersion.__lt__, True), 227 | ] 228 | ) 229 | def test_linversion_j2602_compare(a, b, op, result): 230 | assert op(a, b) == result 231 | 232 | 233 | @pytest.mark.unit() 234 | @pytest.mark.parametrize( 235 | ('func', 'arg'), 236 | [ 237 | (J2602Version(2, 0, 1).__gt__, "J2602_3_0.1"), 238 | (J2602Version(2, 0, 1).__lt__, "J2602_3_0.1"), 239 | (J2602Version(2, 0, 1).__ge__, "J2602_3_0.1"), 240 | (J2602Version(2, 0, 1).__le__, "J2602_3_0.1") 241 | ] 242 | ) 243 | def test_linversion_j2602_typerror(func, arg): 244 | with pytest.raises(TypeError): 245 | func(arg) 246 | -------------------------------------------------------------------------------- /tests/test_node.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ldfparser.node import LinProductId 3 | 4 | @pytest.mark.unit() 5 | def test_lin_product_id_create_valid(): 6 | LinProductId.create(supplier_id=0x0001, function_id=0x0002, variant=1) 7 | 8 | @pytest.mark.unit() 9 | def test_lin_product_id_create_invalid_supplier(): 10 | with pytest.raises(ValueError): 11 | LinProductId.create(supplier_id=0xFFFF, function_id=0x0002, variant=1) 12 | 13 | @pytest.mark.unit() 14 | def test_lin_product_id_create_invalid_function(): 15 | with pytest.raises(ValueError): 16 | LinProductId.create(supplier_id=0x0001, function_id=0xFFFFFFFF, variant=1) 17 | 18 | @pytest.mark.unit() 19 | def test_lin_product_id_create_invalid_variant(): 20 | with pytest.raises(ValueError): 21 | LinProductId.create(supplier_id=0x0001, function_id=0x0002, variant=-1) 22 | -------------------------------------------------------------------------------- /tests/test_parser.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | from ldfparser.diagnostics import LIN_MASTER_REQUEST_FRAME_ID, LIN_SLAVE_RESPONSE_FRAME_ID 4 | 5 | from ldfparser.parser import parse_ldf 6 | from ldfparser.frame import LinFrame 7 | from ldfparser.signal import LinSignal 8 | from ldfparser.encoding import ASCIIValue, BCDValue, LogicalValue 9 | from ldfparser.lin import Iso17987Version, LIN_VERSION_2_0 10 | 11 | @pytest.mark.unit 12 | def test_load_valid_lin13(): 13 | path = os.path.join(os.path.dirname(__file__), "ldf", "lin13.ldf") 14 | ldf = parse_ldf(path) 15 | 16 | assert ldf.protocol_version == 1.3 17 | assert ldf.language_version == 1.3 18 | assert ldf.baudrate == 19200 19 | 20 | assert ldf.master.timebase == 0.005 21 | assert ldf.master.jitter == 0.0001 22 | 23 | assert ldf.signal('StartHeater') is not None 24 | assert ldf.frame('VL1_CEM_Frm1') is not None 25 | assert ldf.slave('LSM') is not None 26 | 27 | assert ldf.get_slave('CPM').initial_nad == 0x02 28 | 29 | 30 | @pytest.mark.unit 31 | def test_load_valid_lin20(): 32 | path = os.path.join(os.path.dirname(__file__), "ldf", "lin20.ldf") 33 | ldf = parse_ldf(path) 34 | 35 | assert ldf.protocol_version == 2.0 36 | assert ldf.language_version == 2.0 37 | assert ldf.baudrate == 19200 38 | 39 | assert ldf.signal('InternalLightsRequest') is not None 40 | assert ldf.frame('VL1_CEM_Frm1') is not None 41 | assert ldf.slave('LSM') is not None 42 | 43 | with pytest.raises(LookupError): 44 | ldf.get_unconditional_frame('VL1_CEM_Frm1234') 45 | 46 | with pytest.raises(TypeError): 47 | ldf.get_unconditional_frame(['VL1_CEM_Frm1']) 48 | 49 | 50 | @pytest.mark.unit 51 | def test_load_valid_lin21(): 52 | path = os.path.join(os.path.dirname(__file__), "ldf", "lin21.ldf") 53 | ldf = parse_ldf(path) 54 | 55 | assert ldf.protocol_version == 2.1 56 | assert ldf.language_version == 2.1 57 | assert ldf.baudrate == 19200 58 | assert ldf.channel == 'DB' 59 | 60 | internalLightRequest = ldf.signal('InternalLightsRequest') 61 | assert internalLightRequest is not None 62 | assert internalLightRequest.publisher.name == 'CEM' 63 | assert len(internalLightRequest.subscribers) == 2 64 | assert internalLightRequest in ldf.master.publishes 65 | 66 | assert ldf.frame('LSM_Frm2') is not None 67 | assert ldf.frame(0x03) is not None 68 | assert ldf.slave('LSM') is not None 69 | 70 | 71 | @pytest.mark.unit 72 | def test_load_valid_lin22(): 73 | path = os.path.join(os.path.dirname(__file__), "ldf", "lin22.ldf") 74 | ldf = parse_ldf(path) 75 | 76 | assert ldf.protocol_version == 2.2 77 | assert ldf.language_version == 2.2 78 | assert ldf.baudrate == 19200 79 | assert ldf.channel == 'DB' 80 | 81 | internalLightRequest = ldf.signal('InternalLightsRequest') 82 | assert internalLightRequest is not None 83 | assert internalLightRequest.publisher.name == 'CEM' 84 | assert len(internalLightRequest.subscribers) == 2 85 | assert internalLightRequest in ldf.master.publishes 86 | 87 | assert ldf.frame('LSM_Frm2') is not None 88 | assert ldf.frame(0x03) is not None 89 | 90 | LSM = ldf.slave('LSM') 91 | assert LSM is not None 92 | assert isinstance(LSM.subscribes_to[0], LinSignal) 93 | assert isinstance(LSM.publishes[0], LinSignal) 94 | assert isinstance(LSM.publishes_frames[0], LinFrame) 95 | assert LSM.product_id.supplier_id == 0x4A4F 96 | assert LSM.product_id.function_id == 0x4841 97 | assert LSM.fault_state_signals == [ldf.signal('IntTest')] 98 | assert LSM.response_error == ldf.signal('LSMerror') 99 | assert LSM.configurable_frames[0] == ldf.get_frame('Node_Status_Event') 100 | assert LSM.configurable_frames[1] == ldf.get_frame('CEM_Frm1') 101 | assert LSM.configurable_frames[2] == ldf.get_frame('LSM_Frm1') 102 | assert LSM.configurable_frames[3] == ldf.get_frame('LSM_Frm2') 103 | 104 | RSM = ldf.slave('RSM') 105 | assert RSM.configurable_frames[0] == ldf.get_frame('Node_Status_Event') 106 | assert RSM.configurable_frames[1] == ldf.get_frame('CEM_Frm1') 107 | assert RSM.configurable_frames[2] == ldf.get_frame('RSM_Frm1') 108 | assert RSM.configurable_frames[3] == ldf.get_frame('RSM_Frm2') 109 | 110 | converter = ldf.converters['InternalLightsRequest'] 111 | assert converter.name == 'Dig2Bit' 112 | assert isinstance(converter._converters[0], LogicalValue) 113 | 114 | 115 | @pytest.mark.unit 116 | def test_no_signal_subscribers(): 117 | path = os.path.join(os.path.dirname(__file__), "ldf", "no_signal_subscribers.ldf") 118 | ldf = parse_ldf(path) 119 | 120 | assert ldf.protocol_version == 2.2 121 | assert ldf.language_version == 2.2 122 | assert ldf.baudrate == 19200 123 | 124 | assert ldf.signal('DummySignal_0') is not None 125 | assert ldf.frame('DummyFrame') is not None 126 | assert ldf.signal('DummySignal_0').frame.name == 'DummyFrame' 127 | 128 | 129 | @pytest.mark.unit 130 | def test_load_valid_lin_encoders(): 131 | path = os.path.join(os.path.dirname(__file__), "ldf", "lin_encoders.ldf") 132 | ldf = parse_ldf(path) 133 | 134 | assert ldf.protocol_version == 2.1 135 | assert ldf.language_version == 2.1 136 | assert ldf.baudrate == 19200 137 | 138 | bcd_signal = ldf.signal('bcd_signal') 139 | assert bcd_signal is not None 140 | assert bcd_signal.publisher.name == 'remote_node' 141 | assert len(bcd_signal.subscribers) == 1 142 | assert bcd_signal in ldf.slave('remote_node').publishes 143 | assert bcd_signal.init_value == [0x32, 32] 144 | 145 | ascii_signal = ldf.signal('ascii_signal') 146 | assert ascii_signal is not None 147 | assert ascii_signal.publisher.name == 'remote_node' 148 | assert len(ascii_signal.subscribers) == 1 149 | assert ascii_signal in ldf.slave('remote_node').publishes 150 | assert ascii_signal.init_value == [16, 0x16] 151 | 152 | assert ldf.frame('dummy_frame') is not None 153 | assert ldf.frame(0x25) is not None 154 | 155 | remote_node = ldf.slave('remote_node') 156 | assert remote_node is not None 157 | assert remote_node.product_id.supplier_id == 0x5 158 | assert remote_node.product_id.function_id == 0xA5A5 159 | 160 | converter = ldf.converters['bcd_signal'] 161 | assert converter.name == 'BCDEncoding' 162 | assert len(converter._converters) == 1 163 | assert isinstance(converter._converters[0], BCDValue) 164 | 165 | converter = ldf.get_signal_encoding_type('AsciiEncoding') 166 | assert converter.name == 'AsciiEncoding' 167 | assert len(converter._converters) == 1 168 | assert isinstance(converter._converters[0], ASCIIValue) 169 | 170 | with pytest.raises(LookupError): 171 | ldf.get_signal_encoding_type('abc123') 172 | 173 | @pytest.mark.unit 174 | def test_load_valid_diagnostics(): 175 | path = os.path.join(os.path.dirname(__file__), "ldf", "lin_diagnostics.ldf") 176 | ldf = parse_ldf(path) 177 | 178 | assert len(ldf.get_diagnostic_frames()) >= 0 179 | assert ldf.get_diagnostic_frame('MasterReq').frame_id == LIN_MASTER_REQUEST_FRAME_ID 180 | assert ldf.get_diagnostic_frame(LIN_MASTER_REQUEST_FRAME_ID).frame_id == LIN_MASTER_REQUEST_FRAME_ID 181 | assert ldf.master_request_frame.frame_id == LIN_MASTER_REQUEST_FRAME_ID 182 | assert ldf.slave_response_frame.frame_id == LIN_SLAVE_RESPONSE_FRAME_ID 183 | 184 | assert len(ldf.get_diagnostic_signals()) >= 0 185 | assert ldf.get_diagnostic_signal('MasterReqB0').width == 8 186 | 187 | with pytest.raises(LookupError): 188 | ldf.get_diagnostic_signal('MasterReqB9') 189 | 190 | @pytest.mark.unit 191 | def test_load_sporadic_frames(): 192 | path = os.path.join(os.path.dirname(__file__), "ldf", "ldf_with_sporadic_frames.ldf") 193 | ldf = parse_ldf(path) 194 | 195 | assert len(ldf.get_sporadic_frames()) >= 0 196 | 197 | sporadic_frame = ldf.get_frame('SF_REQ_POST_RUN') 198 | assert sporadic_frame.name == 'SF_REQ_POST_RUN' 199 | assert ldf.get_unconditional_frame('REQ_POST_RUN') in sporadic_frame.frames 200 | 201 | with pytest.raises(LookupError): 202 | ldf.get_frame('SF_123') 203 | 204 | @pytest.mark.unit 205 | def test_load_iso17987(): 206 | path = os.path.join(os.path.dirname(__file__), "ldf", "iso17987.ldf") 207 | ldf = parse_ldf(path) 208 | 209 | assert isinstance(ldf.get_protocol_version(), Iso17987Version) 210 | assert ldf.get_protocol_version().revision == 2015 211 | 212 | assert isinstance(ldf.get_language_version(), Iso17987Version) 213 | assert ldf.get_language_version().revision == 2015 214 | 215 | @pytest.mark.unit 216 | def test_load_j2602_attributes(): 217 | path = os.path.join(os.path.dirname(__file__), "ldf", "j2602_1.ldf") 218 | ldf = parse_ldf(path) 219 | 220 | assert ldf.get_protocol_version() == LIN_VERSION_2_0 221 | assert ldf.get_language_version() == LIN_VERSION_2_0 222 | assert ldf.master.max_header_length == 24 223 | assert ldf.master.response_tolerance == 0.3 224 | assert list(ldf.slaves)[0].response_tolerance == 0.38 225 | assert list(ldf.slaves)[0].wakeup_time == 0.05 226 | assert list(ldf.slaves)[0].poweron_time == 0.06 227 | 228 | @pytest.mark.unit 229 | @pytest.mark.parametrize( 230 | 'file, max_header_length, master_response_tolerance, slave_response_tolerance, slave_wakeup_time, slave_poweron_time', 231 | [ 232 | ("lin20.ldf", None, None, None, None, None), 233 | ("j2602_1_no_values.ldf", 48, 0.4, 0.4, 0.1, 0.1) 234 | ] 235 | ) 236 | def test_j2602_attributes_default( 237 | file, max_header_length, master_response_tolerance, slave_response_tolerance, slave_wakeup_time, slave_poweron_time): 238 | """ 239 | Should not set default value for J2602 attributes if protocol is not J2602 240 | """ 241 | path = os.path.join(os.path.dirname(__file__), "ldf", file) 242 | ldf = parse_ldf(path) 243 | 244 | assert ldf.master.max_header_length == max_header_length 245 | assert ldf.master.response_tolerance == master_response_tolerance 246 | assert list(ldf.slaves)[0].response_tolerance == slave_response_tolerance 247 | assert list(ldf.slaves)[0].wakeup_time == slave_wakeup_time 248 | assert list(ldf.slaves)[0].poweron_time == slave_poweron_time 249 | 250 | @pytest.mark.unit 251 | @pytest.mark.parametrize( 252 | "pad_with_zero", [True, False, None] 253 | ) 254 | def test_padding_option(pad_with_zero): 255 | path = os.path.join(os.path.dirname(__file__), "ldf", "lin20.ldf") 256 | if pad_with_zero is None: 257 | ldf = parse_ldf(path) 258 | assert ldf._pad_with_zero is True 259 | else: 260 | ldf = parse_ldf(path, pad_with_zero=pad_with_zero) 261 | assert ldf._pad_with_zero == pad_with_zero 262 | -------------------------------------------------------------------------------- /tests/test_performance.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import pytest 4 | 5 | from ldfparser.parser import parse_ldf 6 | from ldfparser.signal import LinSignal 7 | from ldfparser.encoding import PhysicalValue, LogicalValue 8 | 9 | ldf_directory = os.path.join(os.path.dirname(__file__), 'ldf') 10 | ldf_files = glob.glob(ldf_directory + '/*.ldf') 11 | 12 | @pytest.mark.parametrize( 13 | ('ldf_path'), 14 | ldf_files 15 | ) 16 | @pytest.mark.performance 17 | def test_performance_load(benchmark, ldf_path): 18 | path = os.path.join(os.path.dirname(__file__), "ldf", ldf_path) 19 | benchmark(parse_ldf, path) 20 | 21 | @pytest.mark.performance 22 | def test_performance_physical_encoding(benchmark): 23 | motor_signal = LinSignal('MotorRPM', 8, 0) 24 | physical_value = PhysicalValue(0, 254, 0.3937, 0, 'rpm') 25 | benchmark(physical_value.encode, value=0, signal=motor_signal) 26 | 27 | @pytest.mark.performance 28 | def test_performance_logical_encoding(benchmark): 29 | motor_signal = LinSignal('MotorRPM', 8, 0) 30 | logical_value = LogicalValue(1, "on") 31 | benchmark(logical_value.encode, value='on', signal=motor_signal) 32 | 33 | @pytest.mark.performance 34 | def test_performance_physical_decoding(benchmark): 35 | motor_signal = LinSignal('MotorRPM', 8, 0) 36 | physical_value = PhysicalValue(0, 254, 0.3937, 0, 'rpm') 37 | benchmark(physical_value.decode, value=200, signal=motor_signal) 38 | 39 | @pytest.mark.performance 40 | def test_performance_logical_decoding(benchmark): 41 | motor_signal = LinSignal('MotorRPM', 8, 0) 42 | logical_value = LogicalValue(1, "on") 43 | benchmark(logical_value.decode, value=1, signal=motor_signal) 44 | -------------------------------------------------------------------------------- /tests/test_save.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import glob 3 | import os 4 | import sys 5 | from unittest.mock import patch 6 | 7 | from ldfparser import parse_ldf, save_ldf 8 | from ldfparser.save import main 9 | 10 | ldf_directory = os.path.join(os.path.dirname(__file__), 'ldf') 11 | snapshot_directory = os.path.join(os.path.dirname(__file__), 'snapshot') 12 | ldf_files = glob.glob(ldf_directory + '/*.ldf') 13 | 14 | class TestSave: 15 | 16 | @pytest.mark.unit 17 | @pytest.mark.parametrize(('ldf_path'), ldf_files) 18 | def test_save(self, ldf_path): 19 | """ 20 | Tests whether the loaded LDFs can be saved as is, and then reloaded without any errors 21 | """ 22 | ldf = parse_ldf(ldf_path) 23 | output_path = os.path.join(os.path.dirname(__file__), 24 | 'tmp', 25 | 'test_resave_' + os.path.basename(ldf_path)) 26 | save_ldf(ldf, output_path) 27 | 28 | parse_ldf(output_path) 29 | 30 | @pytest.mark.unit 31 | @pytest.mark.parametrize(('ldf_path'), [ldf_files[0]]) 32 | def test_save_cli(self, ldf_path): 33 | """ 34 | Tests whether LDFs can be saved using the command line interface 35 | """ 36 | output_path = os.path.join(os.path.dirname(__file__), 37 | 'tmp', 38 | 'test_resave_cli_' + os.path.basename(ldf_path)) 39 | command = ['python', '-f', ldf_path, '-o', output_path] 40 | with patch.object(sys, 'argv', command): 41 | main() 42 | -------------------------------------------------------------------------------- /tests/test_schedule.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from ldfparser.parser import parse_ldf 5 | from ldfparser.schedule import (AssignFrameIdEntry, AssignFrameIdRangeEntry, AssignNadEntry, 6 | ConditionalChangeNadEntry, DataDumpEntry, FreeFormatEntry, 7 | LinFrameEntry, MasterRequestEntry, 8 | SaveConfigurationEntry, SlaveResponseEntry, UnassignFrameIdEntry) 9 | 10 | class TestSchedule: 11 | 12 | @pytest.mark.unit 13 | def test_schedule_load(self): 14 | path = os.path.join(os.path.dirname(__file__), "ldf", "lin_schedules.ldf") 15 | ldf = parse_ldf(path) 16 | 17 | assert len(ldf.get_schedule_tables()) == 6 18 | 19 | address_config_table = ldf.get_schedule_table('AddressConfiguration_Schedule') 20 | assert len(address_config_table.schedule) == 5 21 | 22 | entry_1 = address_config_table.schedule[0] 23 | assert isinstance(entry_1, AssignNadEntry) 24 | assert entry_1.delay == 0.010 25 | assert entry_1.node.name == 'LeftLight' 26 | 27 | entry_2 = address_config_table.schedule[1] 28 | assert isinstance(entry_2, SaveConfigurationEntry) 29 | assert entry_2.delay == 0.010 30 | assert entry_2.node.name == 'LeftLight' 31 | 32 | entry_5 = address_config_table.schedule[4] 33 | assert isinstance(entry_5, ConditionalChangeNadEntry) 34 | assert entry_5.delay == 0.010 35 | assert entry_5.nad == 0x7F 36 | assert entry_5.id == 0x01 37 | assert entry_5.byte == 0x03 38 | assert entry_5.mask == 0x01 39 | assert entry_5.inv == 0xFF 40 | assert entry_5.new_nad == 0x01 41 | 42 | frame_config_table = ldf.get_schedule_table('FrameConfiguration_Schedule') 43 | assert len(frame_config_table.schedule) == 8 44 | 45 | entry_1 = frame_config_table.schedule[0] 46 | assert isinstance(entry_1, AssignFrameIdRangeEntry) 47 | assert entry_1.node.name == 'LeftLight' 48 | assert entry_1.frame_index == 0 49 | assert entry_1.pids == [0x40, 0x42, 0xFF, 0xFF] 50 | 51 | entry_5 = frame_config_table.schedule[4] 52 | assert isinstance(entry_5, AssignFrameIdEntry) 53 | assert entry_5.node.name == 'LeftLight' 54 | assert entry_5.frame.name == 'LeftLightStatus' 55 | 56 | entry_6 = frame_config_table.schedule[5] 57 | assert isinstance(entry_6, UnassignFrameIdEntry) 58 | assert entry_6.node.name == 'LeftLight' 59 | assert entry_6.frame.name == 'LeftLightStatus' 60 | 61 | information_schedule = ldf.get_schedule_table('Information_Schedule') 62 | assert len(information_schedule.schedule) == 3 63 | 64 | entry_1 = information_schedule.schedule[0] 65 | assert isinstance(entry_1, DataDumpEntry) 66 | assert entry_1.node.name == 'LeftLight' 67 | assert entry_1.data == [0x10, 0x80, 0x00, 0xFF, 0xFF] 68 | 69 | entry_3 = information_schedule.schedule[2] 70 | assert isinstance(entry_3, FreeFormatEntry) 71 | assert entry_3.data == [0x3C, 0xB2, 0x00, 0x00, 0xFF, 0x7F, 0xFF, 0xFF] 72 | 73 | diagnostic_schedule = ldf.get_schedule_table('Diagnostic_Schedule') 74 | assert len(diagnostic_schedule.schedule) == 2 75 | 76 | entry_1 = diagnostic_schedule.schedule[0] 77 | assert isinstance(entry_1, MasterRequestEntry) 78 | 79 | entry_2 = diagnostic_schedule.schedule[1] 80 | assert isinstance(entry_2, SlaveResponseEntry) 81 | 82 | normal_schedule = ldf.get_schedule_table('Normal_Schedule') 83 | assert len(normal_schedule.schedule) == 4 84 | 85 | entry_1 = normal_schedule.schedule[0] 86 | assert isinstance(entry_1, LinFrameEntry) 87 | assert entry_1.frame.name == 'LeftLightSet' 88 | 89 | event_frame = ldf.get_event_triggered_frame('LightErrorEvent') 90 | assert event_frame.collision_resolving_schedule_table.name == 'Collision_Resolver_Schedule' 91 | 92 | with pytest.raises(LookupError): 93 | ldf.get_schedule_table('NotExistingSchedule') 94 | -------------------------------------------------------------------------------- /tests/test_schema.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import json 3 | import os 4 | 5 | import pytest 6 | from jsonschema import validate 7 | import ldfparser 8 | 9 | ldf_directory = os.path.join(os.path.dirname(__file__), 'ldf') 10 | ldf_files = glob.glob(ldf_directory + '/*.ldf') 11 | 12 | json_schema = json.load(open('schemas/ldf.json',)) 13 | 14 | @pytest.mark.unit 15 | @pytest.mark.parametrize( 16 | ('ldf_path'), 17 | ldf_files 18 | ) 19 | def test_json_schema(ldf_path): 20 | data = ldfparser.parse_ldf_to_dict(ldf_path) 21 | validate(data, schema=json_schema) 22 | -------------------------------------------------------------------------------- /tests/test_signal.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from ldfparser import LinSignal 3 | 4 | @pytest.mark.unit 5 | def test_signal_create_scalar_valid(): 6 | signal = LinSignal.create('LSM', 8, 0) 7 | assert signal.is_array() is False 8 | 9 | @pytest.mark.unit 10 | def test_signal_create_scalar_invalid_size(): 11 | with pytest.raises(ValueError): 12 | LinSignal.create('LSM', 20, 0) 13 | 14 | @pytest.mark.unit 15 | def test_signal_create_array_valid(): 16 | signal = LinSignal.create('LSM', 24, [1, 2, 3]) 17 | assert signal.is_array() is True 18 | 19 | @pytest.mark.unit 20 | def test_signal_create_array_invalid_length(): 21 | with pytest.raises(ValueError): 22 | LinSignal.create('LSM', 13, [0, 1, 2, 3]) 23 | 24 | @pytest.mark.unit 25 | def test_signal_create_array_invalid_width(): 26 | with pytest.raises(ValueError): 27 | LinSignal.create('LSM', 0, [0, 1]) 28 | 29 | @pytest.mark.unit 30 | def test_signal_create_array_invalid_initvalue(): 31 | with pytest.raises(ValueError): 32 | LinSignal.create('LSM', 24, [1, 2, 3, 4, 5]) 33 | --------------------------------------------------------------------------------