diff --git a/.appveyor.yml b/.appveyor.yml
deleted file mode 100644
index ee7b98300..000000000
--- a/.appveyor.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-environment:
- matrix:
-
- # For Python versions available on Appveyor, see
- # http://www.appveyor.com/docs/installed-software#python
- # Python 3.0-3.3 have reached EOL
-
- - PYTHON: "C:\\Python27"
- - PYTHON: "C:\\Python34"
- - PYTHON: "C:\\Python35"
- - PYTHON: "C:\\Python36"
- - PYTHON: "C:\\Python37"
- - PYTHON: "C:\\Python27-x64"
- - PYTHON: "C:\\Python34-x64"
- - PYTHON: "C:\\Python35-x64"
- - PYTHON: "C:\\Python36-x64"
- - PYTHON: "C:\\Python37-x64"
-
-install:
- # Prepend Python installation and scripts (e.g. pytest) to PATH
- - set PATH=%PYTHON_INSTALL%;%PYTHON_INSTALL%\\Scripts;%PATH%
-
- # We need to install the python-can library itself including the dependencies
- - "python -m pip install .[test,neovi]"
-
-build: off
-
-test_script:
- # run tests
- - "pytest"
-
- # uplad coverage reports
- - "codecov"
diff --git a/.codecov.yml b/.codecov.yml
deleted file mode 100644
index d533fd085..000000000
--- a/.codecov.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-codecov:
- archive:
- uploads: no
-
-coverage:
- precision: 2
- round: down
- range: 50...100
- status:
- # pull-requests only
- patch:
- default:
- # coverage may fall by <1.0% and still be considered "passing"
- threshold: 1.0%
-
-comment:
- layout: "header, diff, changes"
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 000000000..a661f6235
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+.git* export-ignore
+.*.yml export-ignore
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..0fe9b647c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,41 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+### Describe the bug
+
+
+
+### To Reproduce
+
+
+
+### Expected behavior
+
+
+
+### Additional context
+
+OS and version:
+Python version:
+python-can version:
+python-can interface/s (if applicable):
+
+
+
+Traceback and logs
+
+
+
+
+
+```python
+def func():
+ return "hello, world!"
+```
+
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 000000000..b3a153a27
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: 'enhancement'
+assignees: ''
+
+---
+
+### Is your feature request related to a problem? Please describe.
+
+
+### Describe the solution you'd like
+
+
+### Describe alternatives you've considered
+
+
+### Additional context
+
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..dbe907783
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,27 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
+
+version: 2
+updates:
+ - package-ecosystem: "pip"
+ # Enable version updates for development dependencies
+ directory: "/"
+ schedule:
+ interval: "monthly"
+ versioning-strategy: "increase-if-necessary"
+ groups:
+ dev-deps:
+ patterns:
+ - "*"
+
+ - package-ecosystem: "github-actions"
+ # Enable version updates for GitHub Actions
+ directory: "/"
+ schedule:
+ interval: "monthly"
+ groups:
+ github-actions:
+ patterns:
+ - "*"
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 000000000..e6ce365d1
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,37 @@
+
+
+## Summary of Changes
+
+
+
+-
+
+## Related Issues / Pull Requests
+
+
+
+- Closes #
+- Related to #
+
+## Type of Change
+
+- [ ] Bug fix
+- [ ] New feature
+- [ ] Documentation update
+- [ ] Refactoring
+- [ ] Other (please describe):
+
+## Checklist
+
+- [ ] I have followed the [contribution guide](https://python-can.readthedocs.io/en/main/development.html).
+- [ ] I have added or updated tests as appropriate.
+- [ ] I have added or updated documentation as appropriate.
+- [ ] I have added a [news fragment](doc/changelog.d/) for towncrier.
+- [ ] All checks and tests pass (`tox`).
+
+## Additional Notes
+
+
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..270e2fd01
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,154 @@
+name: Tests
+
+on:
+ release:
+ types: [ published ]
+ pull_request:
+ push:
+ branches-ignore:
+ - 'dependabot/**'
+
+env:
+ PY_COLORS: "1"
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ env: [
+ "py310",
+ "py311",
+ "py312",
+ "py313",
+ "py314",
+# "py313t",
+# "py314t",
+ "pypy310",
+ "pypy311",
+ ]
+ fail-fast: false
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Install uv
+ uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # 7.1.6
+ - name: Install tox
+ run: uv tool install tox --with tox-uv
+ - name: Setup SocketCAN
+ if: ${{ matrix.os == 'ubuntu-latest' }}
+ run: |
+ sudo apt-get update
+ sudo apt-get -y install linux-modules-extra-$(uname -r)
+ sudo ./test/open_vcan.sh
+ - name: Test with pytest via tox
+ run: |
+ tox -e ${{ matrix.env }}
+ env:
+ # SocketCAN tests currently fail with PyPy because it does not support raw CAN sockets
+ # See: https://github.com/pypy/pypy/issues/3808
+ TEST_SOCKETCAN: "${{ matrix.os == 'ubuntu-latest' && ! startsWith(matrix.env, 'pypy' ) }}"
+ - name: Coveralls Parallel
+ uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # 2.3.7
+ with:
+ github-token: ${{ secrets.github_token }}
+ flag-name: Unittests-${{ matrix.os }}-${{ matrix.env }}
+ parallel: true
+ path-to-lcov: ./coverage.lcov
+
+ coveralls:
+ needs: test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Coveralls Finished
+ uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e # 2.3.7
+ with:
+ github-token: ${{ secrets.github_token }}
+ parallel-finished: true
+
+ static-code-analysis:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Install uv
+ uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # 7.1.6
+ - name: Install tox
+ run: uv tool install tox --with tox-uv
+ - name: Run linters
+ run: |
+ tox -e lint
+ - name: Run type checker
+ run: |
+ tox -e type
+
+ docs:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Install uv
+ uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # 7.1.6
+ - name: Install tox
+ run: uv tool install tox --with tox-uv
+ - name: Build documentation
+ run: |
+ tox -e docs
+
+ build:
+ name: Packaging
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Install uv
+ uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # 7.1.6
+ - name: Build wheel and sdist
+ run: uv build
+ - name: Check build artifacts
+ run: uvx twine check --strict dist/*
+ - name: Save artifacts
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # 6.0.0
+ with:
+ name: release
+ path: ./dist
+
+ upload_pypi:
+ needs: [build]
+ name: Release to PyPi
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: write
+ attestations: write
+
+ # upload to PyPI only on release
+ if: github.event.release && github.event.action == 'published'
+ steps:
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # 7.0.0
+ with:
+ path: dist
+ merge-multiple: true
+
+ - name: Generate artifact attestation
+ uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # 3.1.0
+ with:
+ subject-path: 'dist/*'
+
+ - name: Publish release distributions to PyPI
+ uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # 1.13.0
diff --git a/.gitignore b/.gitignore
index 6b813427e..e4d402ff4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
test/__tempdir__/
.pytest_cache/
+.mypy_cache/
+.dmypy.json
+dmypy.json
# -------------------------
# below: https://github.com/github/gitignore/blob/da00310ccba9de9a988cc973ef5238ad2c1460e9/Python.gitignore
@@ -15,7 +18,8 @@ __pycache__/
# Distribution / packaging
.Python
env/
-venv/
+.venv*/
+venv*/
build/
develop-eggs/
dist/
@@ -49,6 +53,7 @@ htmlcov/
.cache
nosetests.xml
coverage.xml
+coverage.lcov
*,cover
.hypothesis/
test.*
diff --git a/.mergify.yml b/.mergify.yml
new file mode 100644
index 000000000..52b243cd3
--- /dev/null
+++ b/.mergify.yml
@@ -0,0 +1,25 @@
+queue_rules:
+ - name: default
+ conditions:
+ - "status-success=test" # "GitHub Actions works slightly differently [...], only the job name is used."
+ - "status-success=format"
+
+pull_request_rules:
+ - name: Automatic merge passing PR on up to date branch with approving CR
+ conditions:
+ - "base=develop"
+ - "#approved-reviews-by>=1"
+ - "#review-requested=0"
+ - "#changes-requested-reviews-by=0"
+ - "status-success=test"
+ - "status-success=format"
+ - "label!=work-in-progress"
+ actions:
+ queue:
+ name: default
+
+ - name: Delete head branch after merge
+ conditions:
+ - merged
+ actions:
+ delete_head_branch: {}
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 000000000..a8c61d2de
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,33 @@
+# .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Set the version of Python and other tools you might need
+build:
+ os: ubuntu-22.04
+ tools:
+ python: "3.13"
+ jobs:
+ post_install:
+ - pip install --group docs
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+ configuration: doc/conf.py
+
+# If using Sphinx, optionally build your docs in additional formats such as PDF
+formats:
+ - pdf
+ - epub
+
+# Optionally declare the Python requirements required to build your docs
+python:
+ install:
+ - method: pip
+ path: .
+ extra_requirements:
+ - canalystii
+ - gs-usb
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 164953450..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,62 +0,0 @@
-language: python
-
-python:
- # CPython; versions 3.0-3.3 have reached EOL
- - "2.7"
- - "3.4"
- - "3.5"
- - "3.6"
- - "3.7-dev" # TODO: change to "3.7" once it is supported by travis-ci
- - "nightly"
- # PyPy:
- - "pypy" # Python 2.7
- - "pypy3.5" # Python 3.5
-
-os:
- - linux # Linux is officially supported and we test the library under
- # many different Python verions (see "python: ..." above)
-
-# - osx # OSX + Python is not officially supported by Travis CI as of Feb. 2018
- # nevertheless, "nightly" and some "*-dev" versions seem to work, so we
- # include them explicitly below (see "matrix: include: ..." below).
- # They only seem to work with the xcode8.3 image, and not the newer ones.
- # Thus we will leave this in, until it breaks one day, at which point we
- # will probably reomve testing on OSX if it is not supported then.
- # See #385 on Github.
-
-# - windows # Windows is not supported at all by Travis CI as of Feb. 2018
-
-# Linux setup
-dist: trusty
-sudo: required
-
-matrix:
- # see "os: ..." above
- include:
- - os: osx
- osx_image: xcode8.3
- python: "3.6-dev"
- - os: osx
- osx_image: xcode8.3
- python: "3.7-dev"
- - os: osx
- osx_image: xcode8.3
- python: "nightly"
-
- allow_failures:
- # allow all nighly builds to fail, since these python versions might be unstable
- - python: "nightly"
- # we do not allow dev builds to fail, since these builds are considered stable enough
-
-install:
- - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo bash test/open_vcan.sh ; fi
- - travis_retry pip install sphinx
- - travis_retry pip install .[test]
-
-script:
- - pytest
- - codecov
- # Build Docs with Sphinx
- # -a Write all files
- # -n nitpicky
- - if [[ "$TRAVIS_PYTHON_VERSION" == "3.6" ]]; then python -m sphinx -an doc build; fi
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 000000000..75ecab49e
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,852 @@
+# Changelog
+
+
+
+
+
+## Version [v4.6.1](https://github.com/hardbyte/python-can/tree/v4.6.1) - 2025-08-12
+
+### Fixed
+
+- Fix initialisation of an slcan bus, when setting a bitrate. When using CAN 2.0 (not FD), the default setting for `data_bitrate` was invalid, causing an exception. ([#1978](https://github.com/hardbyte/python-can/issues/1978))
+
+
+## Version [v4.6.0](https://github.com/hardbyte/python-can/tree/v4.6.0) - 2025-08-05
+
+### Removed
+
+- Remove support for Python 3.8. ([#1931](https://github.com/hardbyte/python-can/issues/1931))
+- Unknown command line arguments ("extra args") are no longer passed down to `can.Bus()` instantiation. Use the `--bus-kwargs` argument instead. ([#1949](https://github.com/hardbyte/python-can/issues/1949))
+- Remove `can.io.generic.BaseIOHandler` class. Improve `can.io.*` type annotations by using `typing.Generic`. ([#1951](https://github.com/hardbyte/python-can/issues/1951))
+
+### Added
+
+- Support 11-bit identifiers in the `serial` interface. ([#1758](https://github.com/hardbyte/python-can/issues/1758))
+- Keep track of active Notifiers and make Notifier usable as a context manager. Add function `Notifier.find_instances(bus)` to find the active Notifier for a given bus instance. ([#1890](https://github.com/hardbyte/python-can/issues/1890))
+- Add Windows support to `udp_multicast` interface. ([#1914](https://github.com/hardbyte/python-can/issues/1914))
+- Add FD support to `slcan` according to CANable 2.0 implementation. ([#1920](https://github.com/hardbyte/python-can/issues/1920))
+- Add support for error messages to the `socketcand` interface. ([#1941](https://github.com/hardbyte/python-can/issues/1941))
+- Add support for remote and error frames in the `serial` interface. ([#1948](https://github.com/hardbyte/python-can/issues/1948))
+- Add public functions `can.cli.add_bus_arguments` and `can.cli.create_bus_from_namespace` for creating bus command line options. Currently downstream packages need to implement their own logic to configure *python-can* buses. Now *python-can* can create and parse bus options for third party packages. ([#1949](https://github.com/hardbyte/python-can/issues/1949))
+- Add support for remote frames to `TRCReader`. ([#1953](https://github.com/hardbyte/python-can/issues/1953))
+- Mention the `python-can-candle` package in the plugin interface section of the documentation. ([#1954](https://github.com/hardbyte/python-can/issues/1954))
+- Add new CLI tool `python -m can.bridge` (or just `can_bridge`) to create a software bridge between two physical buses. ([#1961](https://github.com/hardbyte/python-can/issues/1961))
+
+### Changed
+
+- Allow sending Classic CAN frames with a DLC value larger than 8 using the `socketcan` interface. ([#1851](https://github.com/hardbyte/python-can/issues/1851))
+- The `gs_usb` extra dependency was renamed to `gs-usb`.
+ The `lint` extra dependency was removed and replaced with new PEP 735 dependency groups `lint`, `docs` and `test`. ([#1945](https://github.com/hardbyte/python-can/issues/1945))
+- Update dependency name from `zlgcan-driver-py` to `zlgcan`. ([#1946](https://github.com/hardbyte/python-can/issues/1946))
+- Use ThreadPoolExecutor in `detect_available_configs()` to reduce runtime and add `timeout` parameter. ([#1947](https://github.com/hardbyte/python-can/issues/1947))
+- Update contribution guide. ([#1960](https://github.com/hardbyte/python-can/issues/1960))
+
+### Fixed
+
+- Fix a bug in `slcanBus.get_version()` and `slcanBus.get_serial_number()`: If any other data was received during the function call, then `None` was returned. ([#1904](https://github.com/hardbyte/python-can/issues/1904))
+- Fix incorrect padding of CAN FD payload in `BlfReader`. ([#1906](https://github.com/hardbyte/python-can/issues/1906))
+- Set correct message direction for messages received with `kvaser` interface and `receive_own_messages=True`. ([#1908](https://github.com/hardbyte/python-can/issues/1908))
+- Fix timestamp rounding error in `BlfWriter`. ([#1921](https://github.com/hardbyte/python-can/issues/1921))
+- Fix timestamp rounding error in `BlfReader`. ([#1927](https://github.com/hardbyte/python-can/issues/1927))
+- Handle timer overflow message and build timestamp according to the epoch in the `ixxat` interface. ([#1934](https://github.com/hardbyte/python-can/issues/1934))
+- Avoid unsupported `ioctl` function call to allow usage of the `udp_multicast` interface on MacOS. ([#1940](https://github.com/hardbyte/python-can/issues/1940))
+- Fix configuration file parsing for the `state` bus parameter. ([#1957](https://github.com/hardbyte/python-can/issues/1957))
+- Mf4Reader: support non-standard `CAN_DataFrame.Dir` values in mf4 files created by [ihedvall/mdflib](https://github.com/ihedvall/mdflib). ([#1967](https://github.com/hardbyte/python-can/issues/1967))
+- PcanBus: Set `Message.channel` attribute in `PcanBus.recv()`. ([#1969](https://github.com/hardbyte/python-can/issues/1969))
+
+
+## Version 4.5.0
+
+### Features
+
+* gs_usb command-line support (and documentation updates and stability fixes) by @BenGardiner in https://github.com/hardbyte/python-can/pull/1790
+* Faster and more general MF4 support by @cssedev in https://github.com/hardbyte/python-can/pull/1892
+* ASCWriter speed improvement by @pierreluctg in https://github.com/hardbyte/python-can/pull/1856
+* Faster Message string representation by @pierreluctg in https://github.com/hardbyte/python-can/pull/1858
+* Added Netronic's CANdo and CANdoISO adapters interface by @belliriccardo in https://github.com/hardbyte/python-can/pull/1887
+* Add autostart option to BusABC.send_periodic() to fix issue #1848 by @SWolfSchunk in https://github.com/hardbyte/python-can/pull/1853
+* Improve TestBusConfig by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1804
+* Improve speed of TRCReader by @lebuni in https://github.com/hardbyte/python-can/pull/1893
+
+### Bug Fixes
+
+* Fix Kvaser timestamp by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1878
+* Set end_time in ThreadBasedCyclicSendTask.start() by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1871
+* Fix regex in _parse_additional_config() by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1868
+* Fix for #1849 (PCAN fails when PCAN_ERROR_ILLDATA is read via ReadFD) by @bures in https://github.com/hardbyte/python-can/pull/1850
+* Period must be >= 1ms for BCM using Win32 API by @pierreluctg in https://github.com/hardbyte/python-can/pull/1847
+* Fix ASCReader Crash on "Start of Measurement" Line by @RitheeshBaradwaj in https://github.com/hardbyte/python-can/pull/1811
+* Resolve AttributeError within NicanError by @vijaysubbiah20 in https://github.com/hardbyte/python-can/pull/1806
+
+
+### Miscellaneous
+
+* Fix CI by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1889
+* Update msgpack dependency by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1875
+* Add tox environment for doctest by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1870
+* Use typing_extensions.TypedDict on python < 3.12 for pydantic support by @NickCao in https://github.com/hardbyte/python-can/pull/1845
+* Replace PyPy3.8 with PyPy3.10 by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1838
+* Fix slcan tests by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1834
+* Test on Python 3.13 by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1833
+* Stop notifier in examples by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1814
+* Use setuptools_scm by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1810
+* Added extra info for Kvaser dongles by @FedericoSpada in https://github.com/hardbyte/python-can/pull/1797
+* Socketcand: show actual response as well as expected in error by @liamkinne in https://github.com/hardbyte/python-can/pull/1807
+* Refactor CLI filter parsing, add tests by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1805
+* Add zlgcan to docs by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1839
+
+
+## Version 4.4.2
+
+### Bug Fixes
+
+* Remove `abstractmethod` decorator from `Listener.stop()` (#1770, #1795)
+* Fix `SizedRotatingLogger` file suffix bug (#1792, #1793)
+* gs_usb: Use `BitTiming` class internally to configure bitrate (#1747, #1748)
+* pcan: Fix unpack error in `PcanBus._detect_available_configs()` (#1767)
+* socketcan: Improve error handling in `SocketcanBus.__init__()` (#1771)
+* socketcan: Do not log exception on non-linux platforms (#1800)
+* vector, kvaser: Activate channels after CAN filters were applied (#1413, #1708, #1796)
+
+### Features
+
+* kvaser: Add support for non-ISO CAN FD (#1752)
+* neovi: Return timestamps relative to epoch (#1789)
+* slcan: Support CANdapter extended length arbitration ID (#1506, #1528)
+* slcan: Add support for `listen_only` mode (#1496)
+* vector: Add support for `listen_only` mode (#1764)
+
+
+## Version 4.4.0
+
+### Features
+
+* TRC 1.3 Support: Added support for .trc log files as generated by PCAN Explorer v5 and other tools, expanding compatibility with common log file formats (#1753).
+* ASCReader refactor: improved the ASCReader code (#1717).
+* SYSTEC Interface Enhancements: Added the ability to pass an explicit DLC value to the send() method when using the SYSTEC interface, enhancing flexibility for message definitions (#1756).
+* Socketcand Beacon Detection: Introduced a feature for detecting socketcand beacons, facilitating easier connection and configuration with socketcand servers (#1687).
+* PCAN Driver Echo Frames: Enabled echo frames in the PCAN driver when receive_own_messages is set, improving feedback for message transmissions (#1723).
+* CAN FD Bus Connection for VectorBus: Enabled connecting to CAN FD buses without specifying bus timings, simplifying the connection process for users (#1716).
+* Neousys Configs Detection: Updated the detection mechanism for available Neousys configurations, ensuring more accurate and comprehensive configuration discovery (#1744).
+
+### Bug Fixes
+
+* Send Periodic Messages: Fixed an issue where fixed-duration periodic messages were sent one extra time beyond their intended count (#1713).
+* Vector Interface on Windows 11: Addressed compatibility issues with the Vector interface on Windows 11, ensuring stable operation across the latest OS version (#1731).
+* ASCWriter Millisecond Handling: Corrected the handling of milliseconds in ASCWriter, ensuring accurate time representation in log files (#1734).
+* Various minor bug fixes: Addressed several minor bugs to improve overall stability and performance.
+
+### Miscellaneous
+
+* Invert default value logic for BusABC._is_shutdown. (#1774)
+* Implemented various logging enhancements to provide more detailed and useful operational insights (#1703).
+* Updated CI to use OIDC for connecting GitHub Actions to PyPi, improving security and access control for CI workflows.
+* Fix CI to work for MacOS (#1772).
+*
+The release also includes various other minor enhancements and bug fixes aimed at improving the reliability and performance of the software.
+
+
+## Version 4.3.1
+
+### Bug Fixes
+
+* Fix socketcand erroneously discarding frames (#1700)
+* Fix initialization order in EtasBus (#1693, #1704)
+
+### Documentation
+
+* Fix install instructions for neovi (#1694, #1697)
+
+
+## Version 4.3.0
+
+### Breaking Changes
+
+* Raise Minimum Python Version to 3.8 (#1597)
+* Do not stop notifier if exception was handled (#1645)
+
+### Bug Fixes
+
+* Vector: channel detection fails, if there is an active flexray channel (#1634)
+* ixxat: Fix exception in 'state' property on bus coupling errors (#1647)
+* NeoVi: Fixed serial number range (#1650)
+* PCAN: Fix timestamp offset due to timezone (#1651)
+* Catch `pywintypes.error` in broadcast manager (#1659)
+* Fix BLFReader error for incomplete or truncated stream (#1662)
+* PCAN: remove Windows registry check to fix 32bit compatibility (#1672)
+* Vector: Skip the `can_op_mode check` if the device reports `can_op_mode=0` (#1678)
+* Vector: using the config from `detect_available_configs` might raise XL_ERR_INVALID_CHANNEL_MASK error (#1681)
+
+### Features
+
+#### API
+
+* Add `modifier_callback` parameter to `BusABC.send_periodic` for auto-modifying cyclic tasks (#703)
+* Add `protocol` property to BusABC to determine active CAN Protocol (#1532)
+* Change Bus constructor implementation and typing (#1557)
+* Add optional `strict` parameter to relax BitTiming & BitTimingFd Validation (#1618)
+* Add `BitTiming.iterate_from_sample_point` static methods (#1671)
+
+#### IO
+
+* Can Player compatibility with interfaces that use additional configuration (#1610)
+
+#### Interface Improvements
+
+* Kvaser: Add BitTiming/BitTimingFd support to KvaserBus (#1510)
+* Ixxat: Implement `detect_available_configs` for the Ixxat bus. (#1607)
+* NeoVi: Enable send and receive on network ID above 255 (#1627)
+* Vector: Send HighPriority Message to flush Tx buffer (#1636)
+* PCAN: Optimize send performance (#1640)
+* PCAN: Support version string of older PCAN basic API (#1644)
+* Kvaser: add parameter exclusive and `override_exclusive` (#1660)
+* socketcand: Add parameter `tcp_tune` to reduce latency (#1683)
+
+#### Miscellaneous
+
+* Distinguish Text/Binary-IO for Reader/Writer classes. (#1585)
+* Convert setup.py to pyproject.toml (#1592)
+* activate ruff pycodestyle checks (#1602)
+* Update linter instructions in development.rst (#1603)
+* remove unnecessary script files (#1604)
+* BigEndian test fixes (#1625)
+* align `ID:` in can.Message string (#1635)
+* Use same configuration file as Linux on macOS (#1657)
+* We do not need to account for drift when we `USE_WINDOWS_EVENTS` (#1666, #1679)
+* Update linters, activate more ruff rules (#1669)
+* Add Python 3.12 Support / Test Python 3.12 (#1673)
+
+
+## Version 4.2.2
+
+### Bug Fixes
+
+* Fix socketcan KeyError (#1598, #1599).
+* Fix IXXAT not properly shutdown message (#1606).
+* Fix Mf4Reader and TRCReader incompatibility with extra CLI args (#1610).
+* Fix decoding error in Kvaser constructor for non-ASCII product name (#1613).
+
+
+## Version 4.2.1
+
+### Bug Fixes
+
+* The ASCWriter now logs the correct channel for error frames (#1578, #1583).
+* Fix PCAN library detection (#1579, #1580).
+* On Windows, the first two periodic frames were sent without delay (#1590).
+
+
+## Version 4.2.0
+
+### Breaking Changes
+
+* The ``can.BitTiming`` class was replaced with the new
+ ``can.BitTiming`` and `can.BitTimingFd` classes (#1468, #1515).
+ Early adopters of ``can.BitTiming`` will need to update their code. Check the
+ [documentation](https://python-can.readthedocs.io/en/develop/bit_timing.html)
+ for more information. Currently, the following interfaces support the new classes:
+ * canalystii (#1468)
+ * cantact (#1468)
+ * nixnet (#1520)
+ * pcan (#1514)
+ * vector (#1470, #1516)
+
+ There are open pull requests for kvaser (#1510), slcan (#1512) and usb2can (#1511). Testing
+ and reviewing of these open PRs would be most appreciated.
+
+### Features
+
+#### IO
+* Add support for MF4 files (#1289).
+* Add support for version 2 TRC files and other TRC file enhancements (#1530).
+
+#### Type Annotations
+* Export symbols to satisfy type checkers (#1547, #1551, #1558, #1568).
+
+#### Interface Improvements
+* Add ``__del__`` method to ``can.BusABC`` to automatically release resources (#1489, #1564).
+* pcan: Update PCAN Basic to 4.6.2.753 (#1481).
+* pcan: Use select instead of polling on Linux (#1410).
+* socketcan: Use ip link JSON output in ``find_available_interfaces`` (#1478).
+* socketcan: Enable SocketCAN interface tests in GitHub CI (#1484).
+* slcan: improve receiving performance (#1490).
+* usb2can: Stop using root logger (#1483).
+* usb2can: Faster channel detection on Windows (#1480).
+* vector: Only check sample point instead of tseg & sjw (#1486).
+* vector: add VN5611 hwtype (#1501).
+
+### Documentation
+
+* Add new section about related tools to documentation. Add a list of
+ plugin interface packages (#1457).
+
+### Bug Fixes
+
+* Automatic type conversion for config values (#1498, #1499).
+* pcan: Fix ``Bus.__new__`` for CAN-FD interfaces (#1458, #1460).
+* pcan: Fix Detection of Library on Windows on ARM (#1463).
+* socketcand: extended ID bug fixes (#1504, #1508).
+* vector: improve robustness against unknown HardwareType values (#1500, #1502).
+
+### Deprecations
+
+* The ``bustype`` parameter of ``can.Bus`` is deprecated and will be
+ removed in version 5.0, use ``interface`` instead. (#1462).
+* The ``context`` parameter of ``can.Bus`` is deprecated and will be
+ removed in version 5.0, use ``config_context`` instead. (#1474).
+* The ``bit_timing`` parameter of ``CantactBus`` is deprecated and will be
+ removed in version 5.0, use ``timing`` instead. (#1468).
+* The ``bit_timing`` parameter of ``CANalystIIBus`` is deprecated and will be
+ removed in version 5.0, use ``timing`` instead. (#1468).
+* The ``brs`` and ``log_errors`` parameters of `` NiXNETcanBus`` are deprecated
+ and will be removed in version 5.0. (#1520).
+
+### Miscellaneous
+
+* Use high resolution timer on Windows to improve
+ timing precision for BroadcastManager (#1449).
+* Improve ThreadBasedCyclicSendTask timing (#1539).
+* Make code examples executable on Linux (#1452).
+* Fix CanFilter type annotation (#1456).
+* Fix ``The entry_points().get`` deprecation warning and improve
+ type annotation of ``can.interfaces.BACKENDS`` (#1465).
+* Add ``ignore_config`` parameter to ``can.Bus`` (#1474).
+* Add deprecation period to utility function ``deprecated_args_alias`` (#1477).
+* Add `ruff` to the CI system (#1551)
+
+## Version 4.1.0
+
+### Breaking Changes
+
+* ``windows-curses`` was moved to optional dependencies (#1395).
+ Use ``pip install python-can[viewer]`` if you are using the ``can.viewer``
+ script on Windows.
+* The attributes of ``can.interfaces.vector.VectorChannelConfig`` were renamed
+ from camelCase to snake_case (#1422).
+
+
+### Features
+
+#### IO
+* The canutils logger preserves message direction (#1244)
+ and uses common interface names (e.g. can0) instead of just
+ channel numbers (#1271).
+* The ``can.logger`` script accepts the ``-a, --append`` option
+ to add new data to an existing log file (#1326, #1327, #1361).
+ Currently only the blf-, canutils- and csv-formats are supported.
+* All CLI ``extra_args`` are passed to the bus, logger
+ and player initialisation (#1366).
+* Initial support for TRC files (#1217)
+
+#### Type Annotations
+* python-can now includes the ``py.typed`` marker to support type checking
+ according to PEP 561 (#1344).
+
+#### Interface Improvements
+* The gs_usb interface can be selected by device index instead
+ of USB bus/address. Loopback frames are now correctly marked
+ with the ``is_rx`` flag (#1270).
+* The PCAN interface can be selected by its device ID instead
+ of just the channel name (#1346).
+* The PCAN Bus implementation supports auto bus-off reset (#1345).
+* SocketCAN: Make ``find_available_interfaces()`` find slcanX interfaces (#1369).
+* Vector: Add xlGetReceiveQueueLevel, xlGenerateSyncPulse and
+ xlFlushReceiveQueue to xldriver (#1387).
+* Vector: Raise a CanInitializationError, if the CAN settings can not
+ be applied according to the arguments of ``VectorBus.__init__`` (#1426).
+* Ixxat bus now implements BusState api and detects errors (#1141)
+
+### Bug Fixes
+
+* Improve robustness of USB2CAN serial number detection (#1129).
+* Fix channel2int conversion (#1268, #1269).
+* Fix BLF timestamp conversion (#1266, #1273).
+* Fix timestamp handling in udp_multicast on macOS (#1275, #1278).
+* Fix failure to initiate the Neousys DLL (#1281).
+* Fix AttributeError in IscanError (#1292, #1293).
+* Add missing vector devices (#1296).
+* Fix error for DLC > 8 in ASCReader (#1299, #1301).
+* Set default mode for FileIOMessageWriter to wt instead of rt (#1303).
+* Fix conversion for port number from config file (#1309).
+* Fix fileno error on Windows (#1312, #1313, #1333).
+* Remove redundant ``writer.stop()`` call that throws error (#1316, #1317).
+* Detect and cast types of CLI ``extra_args`` (#1280, #1328).
+* Fix ASC/CANoe incompatibility due to timestamp format (#1315, #1362).
+* Fix MessageSync timings (#1372, #1374).
+* Fix file name for compressed files in SizedRotatingLogger (#1382, #1683).
+* Fix memory leak in neoVI bus where message_receipts grows with no limit (#1427).
+* Raise ValueError if gzip is used with incompatible log formats (#1429).
+* Allow restarting of transmission tasks for socketcan (#1440)
+
+### Miscellaneous
+
+* Allow ICSApiError to be pickled and un-pickled (#1341)
+* Sort interface names in CLI API to make documentation reproducible (#1342)
+* Exclude repository-configuration from git-archive (#1343)
+* Improve documentation (#1397, #1401, #1405, #1420, #1421, #1434)
+* Officially support Python 3.11 (#1423)
+* Migrate code coverage reporting from Codecov to Coveralls (#1430)
+* Migrate building docs and publishing releases to PyPi from Travis-CI to GitHub Actions (#1433)
+
+## Version 4.0.0
+
+TL;DR: This release includes a ton of improvements from 2.5 years of development! 🎉 Test thoroughly after switching.
+
+For more than two years, there was no major release of *python-can*.
+However, development was very much active over most of this time, and many parts were switched out and improved.
+Over this time, over 530 issues and PRs have been resolved or merged, and discussions took place in even more.
+Statistics of the final diff: About 200 files changed due to ~22k additions and ~7k deletions from more than thirty contributors.
+
+This changelog diligently lists the major changes but does not promise to be the complete list of changes.
+Therefore, users are strongly advised to thoroughly test their programs against this new version.
+Re-reading the documentation for your interfaces might be helpful too as limitations and capabilities might have changed or are more explicit.
+While we did try to avoid breaking changes, in some cases it was not feasible and in particular, many implementation details have changed.
+
+### Major features
+
+* Type hints for the core library and some interfaces (#652 and many others)
+* Support for Python 3.7-3.10+ only (dropped support for Python 2.* and 3.5-3.6) (#528 and many others)
+* [Granular and unified exceptions](https://python-can.readthedocs.io/en/develop/api.html#errors) (#356, #562, #1025; overview in #1046)
+* [Support for automatic configuration detection](https://python-can.readthedocs.io/en/develop/api.html#can.detect_available_configs) in most interfaces (#303, #640, #641, #811, #1077, #1085)
+* Better alignment of interfaces and IO to common conventions and semantics
+
+### New interfaces
+
+* udp_multicast (#644)
+* robotell (#731)
+* cantact (#853)
+* gs_usb (#905)
+* nixnet (#968, #1154)
+* neousys (#980, #1076)
+* socketcand (#1140)
+* etas (#1144)
+
+### Improved interfaces
+
+* socketcan
+ * Support for multiple Cyclic Messages in Tasks (#610)
+ * Socketcan crash when attempting to stop CyclicSendTask with same arbitration ID (#605, #638, #720)
+ * Relax restriction of arbitration ID uniqueness for CyclicSendTask (#721, #785, #930)
+ * Add nanosecond resolution time stamping to socketcan (#938, #1015)
+ * Add support for changing the loopback flag (#960)
+ * Socketcan timestamps are missing sub-second precision (#1021, #1029)
+ * Add parameter to ignore CAN error frames (#1128)
+* socketcan_ctypes
+ * Removed and replaced by socketcan after deprecation period
+* socketcan_native
+ * Removed and replaced by socketcan after deprecation period
+* vector
+ * Add chip state API (#635)
+ * Add methods to handle non message events (#708)
+ * Implement XLbusParams (#718)
+ * Add support for VN8900 xlGetChannelTime function (#732, #733)
+ * Add vector hardware config popup (#774)
+ * Fix Vector CANlib treatment of empty app name (#796, #814)
+ * Make VectorError pickleable (#848)
+ * Add methods get_application_config(), set_application_config() and set_timer_rate() to VectorBus (#849)
+ * Interface arguments are now lowercase (#858)
+ * Fix errors using multiple Vector devices (#898, #971, #977)
+ * Add more interface information to channel config (#917)
+ * Improve timestamp accuracy on Windows (#934, #936)
+ * Fix error with VN8900 (#1184)
+ * Add static typing (#1229)
+* PCAN
+ * Do not incorrectly reset CANMsg.MSGTYPE on remote frame (#659, #681)
+ * Add support for error frames (#711)
+ * Added keycheck for windows platform for better error message (#724)
+ * Added status_string method to return simple status strings (#725)
+ * Fix timestamp timezone offset (#777, #778)
+ * Add [Cygwin](https://www.cygwin.com/) support (#840)
+ * Update PCAN basic Python file to February 7, 2020 (#929)
+ * Fix compatibility with the latest macOS SDK (#947, #948, #957, #976)
+ * Allow numerical channel specifier (#981, #982)
+ * macOS: Try to find libPCBUSB.dylib before loading it (#983, #984)
+ * Disable command PCAN_ALLOW_ERROR_FRAMES on macOS (#985)
+ * Force english error messages (#986, #993, #994)
+ * Add set/get device number (#987)
+ * Timestamps are silently incorrect on Windows without uptime installed (#1053, #1093)
+ * Implement check for minimum version of pcan library (#1065, #1188)
+ * Handle case where uptime is imported successfully but returns None (#1102, #1103)
+* slcan
+ * Fix bitrate setting (#691)
+ * Fix fileno crash on Windows (#924)
+* ics_neovi
+ * Filter out Tx error messages (#854)
+ * Adding support for send timeout (#855)
+ * Raising more precise API error when set bitrate fails (#865)
+ * Avoid flooding the logger with many errors when they are the same (#1125)
+ * Omit the transmit exception cause for brevity (#1086)
+ * Raise ValueError if message data is over max frame length (#1177, #1181)
+ * Setting is_error_frame message property (#1189)
+* ixxat
+ * Raise exception on busoff in recv() (#856)
+ * Add support for 666 kbit/s bitrate (#911)
+ * Add function to list hwids of available devices (#926)
+ * Add CAN FD support (#1119)
+* seeed
+ * Fix fileno crash on Windows (#902)
+* kvaser
+ * Improve timestamp accuracy on Windows (#934, #936)
+* usb2can
+ * Fix "Error 8" on Windows and provide better error messages (#989)
+ * Fix crash on initialization (#1248, #1249)
+ * Pass flags instead of flags_t type upon initialization (#1252)
+* serial
+ * Fix "TypeError: cannot unpack non-iterable NoneType" and more robust error handling (#1000, #1010)
+* canalystii
+ * Fix is_extended_id (#1006)
+ * Fix transmitting onto a busy bus (#1114)
+ * Replace binary library with python driver (#726, #1127)
+
+### Other API changes and improvements
+
+* CAN FD frame support is pretty complete (#963)
+ * ASCWriter (#604) and ASCReader (#741)
+ * Canutils reader and writer (#1042)
+ * Logger, viewer and player tools can handle CAN FD (#632)
+ * Many bugfixes and more testing coverage
+* IO
+ * [Log rotation](https://python-can.readthedocs.io/en/develop/listeners.html#can.SizedRotatingLogger) (#648, #874, #881, #1147)
+ * Transparent (de)compression of [gzip](https://docs.python.org/3/library/gzip.html) files for all formats (#1221)
+ * Add [plugin support to can.io Reader/Writer](https://python-can.readthedocs.io/en/develop/listeners.html#listener) (#783)
+ * ASCReader/Writer enhancements like increased robustness (#820, #1223, #1256, #1257)
+ * Adding absolute timestamps to ASC reader (#761)
+ * Support other base number (radix) at ASCReader (#764)
+ * Add [logconvert script](https://python-can.readthedocs.io/en/develop/scripts.html#can-logconvert) (#1072, #1194)
+ * Adding support for gzipped ASC logging file (.asc.gz) (#1138)
+ * Improve [IO class hierarchy](https://python-can.readthedocs.io/en/develop/internal-api.html#module-can.io.generic) (#1147)
+* An [overview over various "virtual" interfaces](https://python-can.readthedocs.io/en/develop/interfaces/virtual.html#other-virtual-interfaces) (#644)
+* Make ThreadBasedCyclicSendTask event based & improve timing accuracy (#656)
+* Ignore error frames in can.player by default, add --error-frames option (#690)
+* Add an error callback to ThreadBasedCyclicSendTask (#743, #781)
+* Add direction to CAN messages (#773, #779, #780, #852, #966)
+* Notifier no longer raises handled exceptions in rx_thread (#775, #789) but does so if no listener handles them (#1039, #1040)
+* Changes to serial device number decoding (#869)
+* Add a default fileno function to the BusABC (#877)
+* Disallow Messages to simultaneously be "FD" and "remote" (#1049)
+* Speed up interface plugin imports by avoiding pkg_resources (#1110)
+* Allowing for extra config arguments in can.logger (#1142, #1170)
+* Add changed byte highlighting to viewer.py (#1159)
+* Change DLC to DL in Message.\_\_str\_\_() (#1212)
+
+### Other Bugfixes
+
+* BLF PDU padding (#459)
+* stop_all_periodic_tasks skipping every other task (#634, #637, #645)
+* Preserve capitalization when reading config files (#702, #1062)
+* ASCReader: Skip J1939Tp messages (#701)
+* Fix crash in Canutils Log Reader when parsing RTR frames (#713)
+* Various problems with the installation of the library
+* ASCWriter: Fix date format to show correct day of month (#754)
+* Fixes that some BLF files can't be read ( #763, #765)
+* Seek for start of object instead of calculating it (#786, #803, #806)
+* Only import winreg when on Windows (#800, #802)
+* Find the correct Reader/Writer independently of the file extension case (#895)
+* RecursionError when unpickling message object (#804, #885, #904)
+* Move "filelock" to neovi dependencies (#943)
+* Bus() with "fd" parameter as type bool always resolved to fd-enabled configuration (#954, #956)
+* Asyncio code hits error due to deprecated loop parameter (#1005, #1013)
+* Catch time before 1970 in ASCReader (#1034)
+* Fix a bug where error handlers were not called correctly (#1116)
+* Improved user interface of viewer script (#1118)
+* Correct app_name argument in logger (#1151)
+* Calling stop_all_periodic_tasks() in BusABC.shutdown() and all interfaces call it on shutdown (#1174)
+* Timing configurations do not allow int (#1175)
+* Some smaller bugfixes are not listed here since the problems were never part of a proper release
+* ASCReader & ASCWriter using DLC as data length (#1245, #1246)
+
+### Behind the scenes & Quality assurance
+
+* We publish both source distributions (`sdist`) and binary wheels (`bdist_wheel`) (#1059, #1071)
+* Many interfaces were partly rewritten to modernize the code or to better handle errors
+* Performance improvements
+* Dependencies have changed
+* Derive type information in Sphinx docs directly from type hints (#654)
+* Better documentation in many, many places; This includes the examples, README and python-can developer resources
+* Add issue templates (#1008, #1017, #1018, #1178)
+* Many continuous integration (CI) discussions & improvements (for example: #951, #940, #1032)
+ * Use the [mypy](https://github.com/python/mypy) static type checker (#598, #651)
+ * Use [tox](https://tox.wiki/en/latest/) for testing (#582, #833, #870)
+ * Use [Mergify](https://mergify.com/) (#821, #835, #937)
+ * Switch between various CI providers, abandoned [AppVeyor](https://www.appveyor.com/) (#1009) and partly [Travis CI](https://travis-ci.org/), ended up with mostly [GitHub Actions](https://docs.github.com/en/actions) (#827, #1224)
+ * Use the [black](https://black.readthedocs.io/en/stable/) auto-formatter (#950)
+ * [Good test coverage](https://app.codecov.io/gh/hardbyte/python-can/branch/develop) for all but the interfaces
+* Testing: Many of the new features directly added tests, and coverage of existing code was improved too (for example: #1031, #581, #585, #586, #942, #1196, #1198)
+
+## Version 3.3.4
+
+Last call for Python2 support.
+
+* #850 Fix socket.error is a deprecated alias of OSError used on Python versions lower than 3.3.
+
+## Version 3.3.3
+Backported fixes from 4.x development branch which targets Python 3.
+
+* #798 Backport caching msg.data value in neovi interface.
+* #796 Fix Vector CANlib treatment of empty app name.
+* #771 Handle empty CSV file.
+* #741 ASCII reader can now handle FD frames.
+* #740 Exclude test packages from distribution.
+* #713 RTR crash fix in canutils log reader parsing RTR frames.
+* #701 Skip J1939 messages in ASC Reader.
+* #690 Exposes a configuration option to allow the CAN message player to send error frames
+ (and sets the default to not send error frames).
+* #638 Fixes the semantics provided by periodic tasks in SocketCAN interface.
+* #628 Avoid padding CAN_FD_MESSAGE_64 objects to 4 bytes.
+* #617 Fixes the broken CANalyst-II interface.
+* #605 Socketcan BCM status fix.
+
+
+## Version 3.3.2
+
+Minor bug fix release addressing issue in PCAN RTR.
+
+## Version 3.3.1
+
+Minor fix to setup.py to only require pytest-runner when necessary.
+
+## Version 3.3.0
+
+* Adding CAN FD 64 frame support to blf reader
+* Updates to installation instructions
+* Clean up bits generator in PCAN interface #588
+* Minor fix to use latest tools when building wheels on travis.
+
+## Version 3.2.1
+
+* CAN FD 64 frame support to blf reader
+* Minor fix to use latest tools when building wheels on travis.
+* Updates links in documentation.
+
+## Version 3.2.0
+
+### Major features
+
+* FD support added for Pcan by @bmeisels with input from
+ @markuspi, @christiansandberg & @felixdivo in PR #537
+* This is the last version of python-can which will support Python 2.7
+ and Python 3.5. Support has been removed for Python 3.4 in this
+ release in PR #532
+
+### Other notable changes
+
+* #533 BusState is now an enum.
+* #535 This release should automatically be published to PyPi by travis.
+* #577 Travis-ci now uses stages.
+* #548 A guide has been added for new io formats.
+* #550 Finish moving from nose to pytest.
+* #558 Fix installation on Windows.
+* #561 Tests for MessageSync added.
+
+General fixes, cleanup and docs changes can be found on the GitHub milestone
+https://github.com/hardbyte/python-can/milestone/7?closed=1
+
+Pulls: #522, #526, #527, #536, #540, #546, #547, #548, #533, #559, #569, #571, #572, #575
+
+### Backend Specific Changes
+
+#### pcan
+
+* FD
+
+#### slcan
+
+* ability to set custom can speed instead of using predefined speed values. #553
+
+#### socketcan
+
+* Bug fix to properly support 32bit systems. #573
+
+#### usb2can
+
+* slightly better error handling
+* multiple serial devices can be found
+* support for the `_detect_available_configs()` API
+
+Pulls #511, #535
+
+#### vector
+
+* handle `app_name`. #525
+
+## Version 3.1.1
+
+### Major features
+
+Two new interfaces this release:
+
+- SYSTEC contributed by @idaniel86 in PR #466
+- CANalyst-II contributed by @smeng9 in PR #476
+
+### Other notable changes
+
+* #477 The kvaser interface now supports bus statistics via a custom bus method.
+* #434 neovi now supports receiving own messages
+* #490 Adding option to override the neovi library name
+* #488 Allow simultaneous access to IXXAT cards
+* #447 Improvements to serial interface:
+ * to allow receiving partial messages
+ * to fix issue with DLC of remote frames
+ * addition of unit tests
+* #497 Small API changes to `Message` and added unit tests
+* #471 Fix CAN FD issue in kvaser interface
+* #462 Fix `Notifier` issue with asyncio
+* #481 Fix PCAN support on OSX
+* #455 Fix to `Message` initializer
+* Small bugfixes and improvements
+
+## Version 3.1.0
+
+Version 3.1.0 was built with old wheel and/or setuptools
+packages and was replaced with v3.1.1 after an installation
+but was discovered.
+
+## Version 3.0.0
+
+### Major features
+
+* Adds support for developing `asyncio` applications with `python-can` more easily. This can be useful
+ when implementing protocols that handles simultaneous connections to many nodes since you can write
+ synchronous looking code without handling multiple threads and locking mechanisms. #388
+* New can viewer terminal application. (`python -m can.viewer`) #390
+* More formally adds task management responsibility to the `Bus`. By default tasks created with
+ `bus.send_periodic` will have a reference held by the bus - this means in many cases the user
+ doesn't need to keep the task in scope for their periodic messages to continue being sent. If
+ this behavior isn't desired pass `store_task=False` to the `send_periodic` method. Stop all tasks
+ by calling the bus's new `stop_all_periodic_tasks` method. #412
+
+
+### Breaking changes
+
+* Interfaces should no longer override `send_periodic` and instead implement
+ `_send_periodic_internal` to allow the Bus base class to manage tasks. #426
+* writing to closed writers is not supported any more (it was supported only for some)
+* the file in the reader/writer is now always stored in the attribute uniformly called `file`, and not in
+ something like `fp`, `log_file` or `output_file`. Changed the name of the first parameter of the
+ read/writer constructors from `filename` to `file`.
+
+
+### Other notable changes
+
+* can.Message class updated #413
+ - Addition of a `Message.equals` method.
+ - Deprecate id_type in favor of is_extended_id
+ - Initializer parameter extended_id deprecated in favor of is_extended_id
+ - documentation, testing and example updates
+ - Addition of support for various builtins: __repr__, __slots__, __copy__
+* IO module updates to bring consistency to the different CAN message writers and readers. #348
+ - context manager support for all readers and writers
+ - they share a common super class called `BaseIOHandler`
+ - all file handles can now be closed with the `stop()` method
+ - the table name in `SqliteReader`/`SqliteWriter` can be adjusted
+ - append mode added in `CSVWriter` and `CanutilsLogWriter`
+ - [file-like](https://docs.python.org/3/glossary.html#term-file-like-object) and
+ [path-like](https://docs.python.org/3/glossary.html#term-path-like-object) objects can now be passed to
+ the readers and writers (except to the Sqlite handlers)
+ - add a `__ne__()` method to the `Message` class (this was required by the tests)
+ - added a `stop()` method for `BufferedReader`
+ - `SqliteWriter`: this now guarantees that all messages are being written, exposes some previously internal metrics
+ and only buffers messages up to a certain limit before writing/committing to the database.
+ - the unused `header_line` attribute from `CSVReader` has been removed
+ - privatized some attributes that are only to be used internally in the classes
+ - the method `Listener.on_message_received()` is now abstract (using `@abc.abstractmethod`)
+* Start testing against Python 3.7 #380
+* All scripts have been moved into `can/scripts`. #370, #406
+* Added support for additional sections to the config #338
+* Code coverage reports added. #346, #374
+* Bug fix to thread safe bus. #397
+
+General fixes, cleanup and docs changes: (#347, #348, #367, #368, #370, #371, #373, #420, #417, #419, #432)
+
+### Backend Specific Changes
+
+#### 3rd party interfaces
+
+* Deprecated `python_can.interface` entry point instead use `can.interface`. #389
+
+#### neovi
+
+* Added support for CAN-FD #408
+* Fix issues checking if bus is open. #381
+* Adding multiple channels support. #415
+
+#### nican
+
+* implements reset instead of custom `flush_tx_buffer`. #364
+
+#### pcan
+
+* now supported on OSX. #365
+
+#### serial
+
+* Removed TextIOWrapper from serial. #383
+* switch to `serial_for_url` enabling using remote ports via `loop://`, ``socket://` and `rfc2217://` URLs. #393
+* hardware handshake using `rtscts` kwarg #402
+
+#### socketcan
+
+* socketcan tasks now reuse a bcm socket #404, #425, #426,
+* socketcan bugfix to receive error frames #384
+
+#### vector
+
+* Vector interface now implements `_detect_available_configs`. #362
+* Added support to select device by serial number. #387
+
+## Version 2.2.1 (2018-07-12)
+
+* Fix errors and warnings when importing library on Windows
+* Fix Vector backend raising ValueError when hardware is not connected
+
+## Version 2.2.0 (2018-06-30)
+
+* Fallback message filtering implemented in Python for interfaces that don't offer better accelerated mechanism.
+* SocketCAN interfaces have been merged (Now use `socketcan` instead of either `socketcan_native` and `socketcan_ctypes`),
+ this is now completely transparent for the library user.
+* automatic detection of available configs/channels in supported interfaces.
+* Added synchronized (thread-safe) Bus variant.
+* context manager support for the Bus class.
+* Dropped support for Python 3.3 (officially reached end-of-life in Sept. 2017)
+* Deprecated the old `CAN` module, please use the newer `can` entry point (will be removed in an upcoming major version)
+
+## Version 2.1.0 (2018-02-17)
+
+* Support for out of tree can interfaces with pluggy.
+* Initial support for CAN-FD for socketcan_native and kvaser interfaces.
+* Neovi interface now uses Intrepid Control Systems's own interface library.
+* Improvements and new documentation for SQL reader/writer.
+* Fix bug in neovi serial number decoding.
+* Add testing on OSX to TravisCI
+* Fix non english decoding error on pcan
+* Other misc improvements and bug fixes
+
+
+## Version 2.0.0 (2018-01-05)
+
+After an extended baking period we have finally tagged version 2.0.0!
+
+Quite a few major changes from v1.x:
+
+* New interfaces:
+ * Vector
+ * NI-CAN
+ * isCAN
+ * neoVI
+* Simplified periodic send API with initial support for SocketCAN
+* Protocols module including J1939 support removed
+* Logger script moved to module `can.logger`
+* New `can.player` script to replay log files
+* BLF, ASC log file support added in new `can.io` module
+
+You can install from [PyPi](https://pypi.python.org/pypi/python-can/2.0.0) with pip:
+
+```
+pip install python-can==2.0.0
+```
+
+The documentation for v2.0.0 is available at http://python-can.readthedocs.io/en/2.0.0/
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
deleted file mode 100644
index d56a151de..000000000
--- a/CHANGELOG.txt
+++ /dev/null
@@ -1,56 +0,0 @@
-Version 2.2.1 (2018-07-12)
-=====
-
-* Fix errors and warnings when importing library on Windows
-* Fix Vector backend raising ValueError when hardware is not connected
-
-Version 2.2.0 (2018-06-30)
-=====
-
-* Fallback message filtering implemented in Python for interfaces that don't offer better accelerated mechanism.
-* SocketCAN interfaces have been merged (Now use `socketcan` instead of either `socketcan_native` and `socketcan_ctypes`),
- this is now completely transparent for the library user.
-* automatic detection of available configs/channels in supported interfaces.
-* Added synchronized (thread-safe) Bus variant.
-* context manager support for the Bus class.
-* Dropped support for Python 3.3 (officially reached end-of-life in Sept. 2017)
-* Deprecated the old `CAN` module, please use the newer `can` entry point (will be removed in version 2.4)
-
-Version 2.1.0 (2018-02-17)
-=====
-
-* Support for out of tree can interfaces with pluggy.
-* Initial support for CAN-FD for socketcan_native and kvaser interfaces.
-* Neovi interface now uses Intrepid Control Systems's own interface library.
-* Improvements and new documentation for SQL reader/writer.
-* Fix bug in neovi serial number decoding.
-* Add testing on OSX to TravisCI
-* Fix non english decoding error on pcan
-* Other misc improvements and bug fixes
-
-
-Version 2.0.0 (2018-01-05
-=====
-
-After an extended baking period we have finally tagged version 2.0.0!
-
-Quite a few major Changes from v1.x:
-
-* New interfaces:
- * Vector
- * NI-CAN
- * isCAN
- * neoVI
-* Simplified periodic send API with initial support for SocketCAN
-* Protocols module including J1939 support removed
-* Logger script moved to module `can.logger`
-* New `can.player` script to replay log files
-* BLF, ASC log file support added in new `can.io` module
-
-You can install from [PyPi](https://pypi.python.org/pypi/python-can/2.0.0) with pip:
-
-```
-pip install python-can==2.0.0
-```
-
-The documentation for v2.0.0 is available at http://python-can.readthedocs.io/en/2.0.0/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..2f4194b31
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1 @@
+Please read the [Development - Contributing](https://python-can.readthedocs.io/en/main/development.html#contributing) guidelines in the documentation site.
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 3ce2ff730..524908dfc 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -1,24 +1,94 @@
-Ben Powell
-Brian Thorne
-Geert Linders
-Mark Catley
-Phillip Dixon
-Rose Lu
-Karl van Workum
-Albert Bloomfield
-Sam Bristow
-Ethan Zonca
-Robert Kaye
-Andrew Beal
-Jonas Frid
-Tynan McAuley
-Bruno Pennati
-Jack Jester-Weinstein
-Joshua Villyard
-Giuseppe Corbelli
-Christian Sandberg
-Eduard Bröcker
-Boris Wenzlaff
-Pierre-Luc Tessier Gagné
-Felix Divo
-Kristian Sloth Lauszus
+https://github.com/hardbyte/python-can/graphs/contributors
+
+Ben Powell
+Brian Thorne
+Geert Linders
+Mark Catley
+Phillip Dixon
+Rose Lu
+Karl van Workum
+Albert Bloomfield
+Sam Bristow
+Ethan Zonca
+Robert Kaye
+Andrew Beal
+Jonas Frid
+Tynan McAuley
+Bruno Pennati
+Jack Jester-Weinstein
+Joshua Villyard
+Giuseppe Corbelli
+Christian Sandberg
+Eduard Bröcker
+Boris Wenzlaff
+Pierre-Luc Tessier Gagné
+Felix Divo
+Kristian Sloth Lauszus
+Shaoyu Meng
+Alexander Mueller
+Jan Goeteyn
+ykzheng
+Lear Corporation
+Nick Black
+Francisco Javier Burgos Macia
+Felix Nieuwenhuizen
+@marcel-kanter
+@bessman
+@koberbe
+@tamenol
+@deonvdw
+@ptoews
+@chrisoro
+@sou1hacker
+@auden-rovellequartz
+@typecprint
+@tysonite
+@Joey5337
+@Aajn
+@josko7452
+@leventerevesz
+@ericevenchick
+@ixygo
+@Gussy
+@altendky
+@philsc
+@rliebscher
+@jxltom
+@kdschlosser
+@tojoh511
+@s0kr4t3s
+@jaesc
+@NiallDarwin
+@sdorre
+@gordon-epc
+@willson556
+@jjguti
+@wiboticalex
+@illuusio
+@cperkulator
+@simontegelid
+@DawidRosinski
+@fabiocrestani
+@ChrisSweetKT
+@ausserlesh
+@wbarnha
+@projectgus
+@samsmith94
+@alexey
+@MattWoodhead
+@nbusser
+@domologic
+@fjburgos
+@pkess
+@felixn
+@Tbruno25
+@RitheeshBaradwaj
+@vijaysubbiah20
+@liamkinne
+@RitheeshBaradwaj
+@BenGardiner
+@bures
+@NickCao
+@SWolfSchunk
+@belliriccardo
+@cssedev
diff --git a/LICENSE.txt b/LICENSE.txt
index b14ca0a55..65c5ca88a 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,165 +1,165 @@
- GNU LESSER GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
-
- This version of the GNU Lesser General Public License incorporates
-the terms and conditions of version 3 of the GNU General Public
-License, supplemented by the additional permissions listed below.
-
- 0. Additional Definitions.
-
- As used herein, "this License" refers to version 3 of the GNU Lesser
-General Public License, and the "GNU GPL" refers to version 3 of the GNU
-General Public License.
-
- "The Library" refers to a covered work governed by this License,
-other than an Application or a Combined Work as defined below.
-
- An "Application" is any work that makes use of an interface provided
-by the Library, but which is not otherwise based on the Library.
-Defining a subclass of a class defined by the Library is deemed a mode
-of using an interface provided by the Library.
-
- A "Combined Work" is a work produced by combining or linking an
-Application with the Library. The particular version of the Library
-with which the Combined Work was made is also called the "Linked
-Version".
-
- The "Minimal Corresponding Source" for a Combined Work means the
-Corresponding Source for the Combined Work, excluding any source code
-for portions of the Combined Work that, considered in isolation, are
-based on the Application, and not on the Linked Version.
-
- The "Corresponding Application Code" for a Combined Work means the
-object code and/or source code for the Application, including any data
-and utility programs needed for reproducing the Combined Work from the
-Application, but excluding the System Libraries of the Combined Work.
-
- 1. Exception to Section 3 of the GNU GPL.
-
- You may convey a covered work under sections 3 and 4 of this License
-without being bound by section 3 of the GNU GPL.
-
- 2. Conveying Modified Versions.
-
- If you modify a copy of the Library, and, in your modifications, a
-facility refers to a function or data to be supplied by an Application
-that uses the facility (other than as an argument passed when the
-facility is invoked), then you may convey a copy of the modified
-version:
-
- a) under this License, provided that you make a good faith effort to
- ensure that, in the event an Application does not supply the
- function or data, the facility still operates, and performs
- whatever part of its purpose remains meaningful, or
-
- b) under the GNU GPL, with none of the additional permissions of
- this License applicable to that copy.
-
- 3. Object Code Incorporating Material from Library Header Files.
-
- The object code form of an Application may incorporate material from
-a header file that is part of the Library. You may convey such object
-code under terms of your choice, provided that, if the incorporated
-material is not limited to numerical parameters, data structure
-layouts and accessors, or small macros, inline functions and templates
-(ten or fewer lines in length), you do both of the following:
-
- a) Give prominent notice with each copy of the object code that the
- Library is used in it and that the Library and its use are
- covered by this License.
-
- b) Accompany the object code with a copy of the GNU GPL and this license
- document.
-
- 4. Combined Works.
-
- You may convey a Combined Work under terms of your choice that,
-taken together, effectively do not restrict modification of the
-portions of the Library contained in the Combined Work and reverse
-engineering for debugging such modifications, if you also do each of
-the following:
-
- a) Give prominent notice with each copy of the Combined Work that
- the Library is used in it and that the Library and its use are
- covered by this License.
-
- b) Accompany the Combined Work with a copy of the GNU GPL and this license
- document.
-
- c) For a Combined Work that displays copyright notices during
- execution, include the copyright notice for the Library among
- these notices, as well as a reference directing the user to the
- copies of the GNU GPL and this license document.
-
- d) Do one of the following:
-
- 0) Convey the Minimal Corresponding Source under the terms of this
- License, and the Corresponding Application Code in a form
- suitable for, and under terms that permit, the user to
- recombine or relink the Application with a modified version of
- the Linked Version to produce a modified Combined Work, in the
- manner specified by section 6 of the GNU GPL for conveying
- Corresponding Source.
-
- 1) Use a suitable shared library mechanism for linking with the
- Library. A suitable mechanism is one that (a) uses at run time
- a copy of the Library already present on the user's computer
- system, and (b) will operate properly with a modified version
- of the Library that is interface-compatible with the Linked
- Version.
-
- e) Provide Installation Information, but only if you would otherwise
- be required to provide such information under section 6 of the
- GNU GPL, and only to the extent that such information is
- necessary to install and execute a modified version of the
- Combined Work produced by recombining or relinking the
- Application with a modified version of the Linked Version. (If
- you use option 4d0, the Installation Information must accompany
- the Minimal Corresponding Source and Corresponding Application
- Code. If you use option 4d1, you must provide the Installation
- Information in the manner specified by section 6 of the GNU GPL
- for conveying Corresponding Source.)
-
- 5. Combined Libraries.
-
- You may place library facilities that are a work based on the
-Library side by side in a single library together with other library
-facilities that are not Applications and are not covered by this
-License, and convey such a combined library under terms of your
-choice, if you do both of the following:
-
- a) Accompany the combined library with a copy of the same work based
- on the Library, uncombined with any other library facilities,
- conveyed under the terms of this License.
-
- b) Give prominent notice with the combined library that part of it
- is a work based on the Library, and explaining where to find the
- accompanying uncombined form of the same work.
-
- 6. Revised Versions of the GNU Lesser General Public License.
-
- The Free Software Foundation may publish revised and/or new versions
-of the GNU Lesser General Public License from time to time. Such new
-versions will be similar in spirit to the present version, but may
-differ in detail to address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Library as you received it specifies that a certain numbered version
-of the GNU Lesser General Public License "or any later version"
-applies to it, you have the option of following the terms and
-conditions either of that published version or of any later version
-published by the Free Software Foundation. If the Library as you
-received it does not specify a version number of the GNU Lesser
-General Public License, you may choose any version of the GNU Lesser
-General Public License ever published by the Free Software Foundation.
-
- If the Library as you received it specifies that a proxy can decide
-whether future versions of the GNU Lesser General Public License shall
-apply, that proxy's public statement of acceptance of any version is
-permanent authorization for you to choose that version for the
-Library.
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+ This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+ 0. Additional Definitions.
+
+ As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+ "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+ An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+ A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+ The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+ The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+ 1. Exception to Section 3 of the GNU GPL.
+
+ You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+ 2. Conveying Modified Versions.
+
+ If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+ a) under this License, provided that you make a good faith effort to
+ ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+
+ b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+ 3. Object Code Incorporating Material from Library Header Files.
+
+ The object code form of an Application may incorporate material from
+a header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+ a) Give prominent notice with each copy of the object code that the
+ Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the object code with a copy of the GNU GPL and this license
+ document.
+
+ 4. Combined Works.
+
+ You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+ a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+
+ b) Accompany the Combined Work with a copy of the GNU GPL and this license
+ document.
+
+ c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+
+ d) Do one of the following:
+
+ 0) Convey the Minimal Corresponding Source under the terms of this
+ License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+
+ 1) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (a) uses at run time
+ a copy of the Library already present on the user's computer
+ system, and (b) will operate properly with a modified version
+ of the Library that is interface-compatible with the Linked
+ Version.
+
+ e) Provide Installation Information, but only if you would otherwise
+ be required to provide such information under section 6 of the
+ GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the
+ Application with a modified version of the Linked Version. (If
+ you use option 4d0, the Installation Information must accompany
+ the Minimal Corresponding Source and Corresponding Application
+ Code. If you use option 4d1, you must provide the Installation
+ Information in the manner specified by section 6 of the GNU GPL
+ for conveying Corresponding Source.)
+
+ 5. Combined Libraries.
+
+ You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+ a) Accompany the combined library with a copy of the same work based
+ on the Library, uncombined with any other library facilities,
+ conveyed under the terms of this License.
+
+ b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+ 6. Revised Versions of the GNU Lesser General Public License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+ If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/MANIFEST.in b/MANIFEST.in
index 4079706c7..a3d6c7fbd 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,4 @@
include *.txt
+include test/*.py
include test/data/*.*
recursive-include doc *.rst
diff --git a/README.rst b/README.rst
index cf6b45f83..6e75d8d7d 100644
--- a/README.rst
+++ b/README.rst
@@ -1,28 +1,45 @@
python-can
==========
-|release| |docs| |build_travis| |build_appveyor| |coverage|
+|pypi| |conda| |python_implementation| |downloads| |downloads_monthly|
-.. |release| image:: https://img.shields.io/pypi/v/python-can.svg
+|docs| |github-actions| |coverage| |formatter|
+
+.. |pypi| image:: https://img.shields.io/pypi/v/python-can.svg
:target: https://pypi.python.org/pypi/python-can/
:alt: Latest Version on PyPi
+.. |conda| image:: https://img.shields.io/conda/v/conda-forge/python-can
+ :target: https://github.com/conda-forge/python-can-feedstock
+ :alt: Latest Version on conda-forge
+
+.. |python_implementation| image:: https://img.shields.io/pypi/implementation/python-can
+ :target: https://pypi.python.org/pypi/python-can/
+ :alt: Supported Python implementations
+
+.. |downloads| image:: https://static.pepy.tech/badge/python-can
+ :target: https://pepy.tech/project/python-can
+ :alt: Downloads on PePy
+
+.. |downloads_monthly| image:: https://static.pepy.tech/badge/python-can/month
+ :target: https://pepy.tech/project/python-can
+ :alt: Monthly downloads on PePy
+
+.. |formatter| image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/python/black
+ :alt: This project uses the black formatter.
+
.. |docs| image:: https://readthedocs.org/projects/python-can/badge/?version=stable
:target: https://python-can.readthedocs.io/en/stable/
:alt: Documentation
-.. |build_travis| image:: https://travis-ci.org/hardbyte/python-can.svg?branch=develop
- :target: https://travis-ci.org/hardbyte/python-can/branches
- :alt: Travis CI Server for develop branch
-
-.. |build_appveyor| image:: https://ci.appveyor.com/api/projects/status/github/hardbyte/python-can?branch=develop&svg=true
- :target: https://ci.appveyor.com/project/hardbyte/python-can/history
- :alt: AppVeyor CI Server for develop branch
-
-.. |coverage| image:: https://codecov.io/gh/hardbyte/python-can/branch/develop/graph/badge.svg
- :target: https://codecov.io/gh/hardbyte/python-can/branch/develop
- :alt: Test coverage reports on Codecov.io
+.. |github-actions| image:: https://github.com/hardbyte/python-can/actions/workflows/ci.yml/badge.svg
+ :target: https://github.com/hardbyte/python-can/actions/workflows/ci.yml
+ :alt: Github Actions workflow status
+.. |coverage| image:: https://coveralls.io/repos/github/hardbyte/python-can/badge.svg?branch=main
+ :target: https://coveralls.io/github/hardbyte/python-can?branch=main
+ :alt: Test coverage reports on Coveralls.io
The **C**\ ontroller **A**\ rea **N**\ etwork is a bus standard designed
to allow microcontrollers and devices to communicate with each other. It
@@ -34,7 +51,18 @@ Python developers; providing common abstractions to
different hardware devices, and a suite of utilities for sending and receiving
messages on a can bus.
-The library supports Python 2.7, Python 3.4+ as well as PyPy 2 & 3 and runs on Mac, Linux and Windows.
+The library currently supports CPython as well as PyPy and runs on Mac, Linux and Windows.
+
+============================== ===========
+Library Version Python
+------------------------------ -----------
+ 2.x 2.6+, 3.4+
+ 3.x 2.7+, 3.5+
+ 4.0+ 3.7+
+ 4.3+ 3.8+
+ 4.6+ 3.9+
+ main branch 3.10+
+============================== ===========
Features
@@ -44,39 +72,42 @@ Features
- support for many different backends (see the `docs `__)
- receiving, sending, and periodically sending messages
- normal and extended arbitration IDs
-- limited `CAN FD `__ support
-- many different loggers and readers supporting playback: ASC (CANalyzer format), BLF (Binary Logging Format by Vector), CSV, SQLite and Canutils log
+- `CAN FD `__ support
+- many different loggers and readers supporting playback: ASC (CANalyzer format), BLF (Binary Logging Format by Vector), MF4 (Measurement Data Format v4 by ASAM), TRC, CSV, SQLite, and Canutils log
- efficient in-kernel or in-hardware filtering of messages on supported interfaces
-- bus configuration reading from file or environment variables
-- CLI tools for working with CAN busses (see the `docs `__)
+- bus configuration reading from a file or from environment variables
+- command line tools for working with CAN buses (see the `docs `__)
- more
Example usage
-------------
+``pip install python-can``
+
.. code:: python
# import the library
import can
- # create a bus instance
- # many other interfaces are supported as well (see below)
- bus = can.Bus(interface='socketcan',
+ # create a bus instance using 'with' statement,
+ # this will cause bus.shutdown() to be called on the block exit;
+ # many other interfaces are supported as well (see documentation)
+ with can.Bus(interface='socketcan',
channel='vcan0',
- receive_own_messages=True)
+ receive_own_messages=True) as bus:
- # send a message
- message = can.Message(arbitration_id=123, extended_id=True,
- data=[0x11, 0x22, 0x33])
- bus.send(message, timeout=0.2)
+ # send a message
+ message = can.Message(arbitration_id=123, is_extended_id=True,
+ data=[0x11, 0x22, 0x33])
+ bus.send(message, timeout=0.2)
- # iterate over received messages
- for msg in bus:
- print("{X}: {}".format(msg.arbitration_id, msg.data))
+ # iterate over received messages
+ for msg in bus:
+ print(f"{msg.arbitration_id:X}: {msg.data}")
- # or use an asynchronous notifier
- notifier = can.Notifier(bus, [can.Logger("recorded.log"), can.Printer()])
+ # or use an asynchronous notifier
+ notifier = can.Notifier(bus, [can.Logger("recorded.log"), can.Printer()])
You can find more information in the documentation, online at
`python-can.readthedocs.org `__.
@@ -88,9 +119,6 @@ Discussion
If you run into bugs, you can file them in our
`issue tracker `__ on GitHub.
-There is also a `python-can `__
-mailing list for development discussion.
-
`Stackoverflow `__ has several
questions and answers tagged with ``python+can``.
diff --git a/can/CAN.py b/can/CAN.py
deleted file mode 100644
index 7f8469aee..000000000
--- a/can/CAN.py
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-"""
-This module was once the core of python-can, containing
-implementations of all the major classes in the library, now
-however all functionality has been refactored out. This API
-is left intact for version 2.0 to 2.3 to aide with migration.
-
-WARNING:
-This module is deprecated an will get removed in version 2.4.
-Please use ``import can`` instead.
-"""
-
-from __future__ import absolute_import
-
-from can.message import Message
-from can.listener import Listener, BufferedReader, RedirectReader
-from can.util import set_logging_level
-from can.io import *
-
-import logging
-
-log = logging.getLogger('can')
-
-# See #267
-# Version 2.0 - 2.1: Log a Debug message
-# Version 2.2: Log a Warning
-# Version 2.3: Log an Error
-# Version 2.4: Remove the module
-log.warning('Loading python-can via the old "CAN" API is deprecated since v2.0 an will get removed in v2.4. '
- 'Please use `import can` instead.')
diff --git a/can/__init__.py b/can/__init__.py
index d38499cca..b1bd636c1 100644
--- a/can/__init__.py
+++ b/can/__init__.py
@@ -1,53 +1,133 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
-``can`` is an object-orient Controller Area Network (CAN) interface module.
+The ``can`` package provides controller area network support for
+Python developers; providing common abstractions to
+different hardware devices, and a suite of utilities for sending and receiving
+messages on a can bus.
"""
-from __future__ import absolute_import
-
+import contextlib
import logging
+from importlib.metadata import PackageNotFoundError, version
+from typing import Any
-__version__ = "2.3.0-dev"
-
-log = logging.getLogger('can')
-
-rc = dict()
-
-
-class CanError(IOError):
- """Indicates an error with the CAN network.
-
- """
- pass
-
-from .listener import Listener, BufferedReader, RedirectReader
-try:
- from .listener import AsyncBufferedReader
-except ImportError:
- pass
-
-from .io import Logger, Printer, LogReader, MessageSync
-from .io import ASCWriter, ASCReader
-from .io import BLFReader, BLFWriter
-from .io import CanutilsLogReader, CanutilsLogWriter
-from .io import CSVWriter, CSVReader
-from .io import SqliteWriter, SqliteReader
-
-from .util import set_logging_level
+__all__ = [
+ "VALID_INTERFACES",
+ "ASCReader",
+ "ASCWriter",
+ "AsyncBufferedReader",
+ "BLFReader",
+ "BLFWriter",
+ "BitTiming",
+ "BitTimingFd",
+ "BufferedReader",
+ "Bus",
+ "BusABC",
+ "BusState",
+ "CSVReader",
+ "CSVWriter",
+ "CanError",
+ "CanInitializationError",
+ "CanInterfaceNotImplementedError",
+ "CanOperationError",
+ "CanProtocol",
+ "CanTimeoutError",
+ "CanutilsLogReader",
+ "CanutilsLogWriter",
+ "CyclicSendTaskABC",
+ "LimitedDurationCyclicSendTaskABC",
+ "Listener",
+ "LogReader",
+ "Logger",
+ "MF4Reader",
+ "MF4Writer",
+ "Message",
+ "MessageSync",
+ "ModifiableCyclicTaskABC",
+ "Notifier",
+ "Printer",
+ "RedirectReader",
+ "RestartableCyclicTaskABC",
+ "SizedRotatingLogger",
+ "SqliteReader",
+ "SqliteWriter",
+ "TRCFileVersion",
+ "TRCReader",
+ "TRCWriter",
+ "ThreadSafeBus",
+ "bit_timing",
+ "broadcastmanager",
+ "bus",
+ "ctypesutil",
+ "detect_available_configs",
+ "exceptions",
+ "interface",
+ "interfaces",
+ "io",
+ "listener",
+ "log",
+ "logconvert",
+ "logger",
+ "message",
+ "notifier",
+ "player",
+ "set_logging_level",
+ "thread_safe_bus",
+ "typechecking",
+ "util",
+ "viewer",
+]
+from . import typechecking # isort:skip
+from . import util # isort:skip
+from . import broadcastmanager, interface
+from .bit_timing import BitTiming, BitTimingFd
+from .broadcastmanager import (
+ CyclicSendTaskABC,
+ LimitedDurationCyclicSendTaskABC,
+ ModifiableCyclicTaskABC,
+ RestartableCyclicTaskABC,
+)
+from .bus import BusABC, BusState, CanProtocol
+from .exceptions import (
+ CanError,
+ CanInitializationError,
+ CanInterfaceNotImplementedError,
+ CanOperationError,
+ CanTimeoutError,
+)
+from .interface import Bus, detect_available_configs
+from .interfaces import VALID_INTERFACES
+from .io import (
+ ASCReader,
+ ASCWriter,
+ BLFReader,
+ BLFWriter,
+ CanutilsLogReader,
+ CanutilsLogWriter,
+ CSVReader,
+ CSVWriter,
+ Logger,
+ LogReader,
+ MessageSync,
+ MF4Reader,
+ MF4Writer,
+ Printer,
+ SizedRotatingLogger,
+ SqliteReader,
+ SqliteWriter,
+ TRCFileVersion,
+ TRCReader,
+ TRCWriter,
+)
+from .listener import AsyncBufferedReader, BufferedReader, Listener, RedirectReader
from .message import Message
-from .bus import BusABC, BusState
-from .thread_safe_bus import ThreadSafeBus
from .notifier import Notifier
-from .interfaces import VALID_INTERFACES
-from . import interface
-from .interface import Bus, detect_available_configs
+from .thread_safe_bus import ThreadSafeBus
+from .util import set_logging_level
+
+with contextlib.suppress(PackageNotFoundError):
+ __version__ = version("python-can")
+
+log = logging.getLogger("can")
-from .broadcastmanager import send_periodic, \
- CyclicSendTaskABC, \
- LimitedDurationCyclicSendTaskABC, \
- ModifiableCyclicTaskABC, \
- MultiRateCyclicSendTaskABC, \
- RestartableCyclicTaskABC
+rc: dict[str, Any] = {}
diff --git a/can/_entry_points.py b/can/_entry_points.py
new file mode 100644
index 000000000..fd1a62d24
--- /dev/null
+++ b/can/_entry_points.py
@@ -0,0 +1,21 @@
+import importlib
+from dataclasses import dataclass
+from importlib.metadata import entry_points
+from typing import Any
+
+
+@dataclass
+class _EntryPoint:
+ key: str
+ module_name: str
+ class_name: str
+
+ def load(self) -> Any:
+ module = importlib.import_module(self.module_name)
+ return getattr(module, self.class_name)
+
+
+def read_entry_points(group: str) -> list[_EntryPoint]:
+ return [
+ _EntryPoint(ep.name, ep.module, ep.attr) for ep in entry_points(group=group)
+ ]
diff --git a/can/bit_timing.py b/can/bit_timing.py
new file mode 100644
index 000000000..2bb04bfbe
--- /dev/null
+++ b/can/bit_timing.py
@@ -0,0 +1,1204 @@
+# pylint: disable=too-many-lines
+import math
+from collections.abc import Iterator, Mapping
+from typing import TYPE_CHECKING, cast
+
+if TYPE_CHECKING:
+ from can.typechecking import BitTimingDict, BitTimingFdDict
+
+
+class BitTiming(Mapping[str, int]):
+ """Representation of a bit timing configuration for a CAN 2.0 bus.
+
+ The class can be constructed in multiple ways, depending on the information
+ available. The preferred way is using CAN clock frequency, prescaler, tseg1, tseg2 and sjw::
+
+ can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=1, sjw=1)
+
+ Alternatively you can set the bitrate instead of the bit rate prescaler::
+
+ can.BitTiming.from_bitrate_and_segments(
+ f_clock=8_000_000, bitrate=1_000_000, tseg1=5, tseg2=1, sjw=1
+ )
+
+ It is also possible to specify BTR registers::
+
+ can.BitTiming.from_registers(f_clock=8_000_000, btr0=0x00, btr1=0x14)
+
+ or to calculate the timings for a given sample point::
+
+ can.BitTiming.from_sample_point(f_clock=8_000_000, bitrate=1_000_000, sample_point=75.0)
+ """
+
+ def __init__(
+ self,
+ f_clock: int,
+ brp: int,
+ tseg1: int,
+ tseg2: int,
+ sjw: int,
+ nof_samples: int = 1,
+ strict: bool = False,
+ ) -> None:
+ """
+ :param int f_clock:
+ The CAN system clock frequency in Hz.
+ :param int brp:
+ Bit rate prescaler.
+ :param int tseg1:
+ Time segment 1, that is, the number of quanta from (but not including)
+ the Sync Segment to the sampling point.
+ :param int tseg2:
+ Time segment 2, that is, the number of quanta from the sampling
+ point to the end of the bit.
+ :param int sjw:
+ The Synchronization Jump Width. Decides the maximum number of time quanta
+ that the controller can resynchronize every bit.
+ :param int nof_samples:
+ Either 1 or 3. Some CAN controllers can also sample each bit three times.
+ In this case, the bit will be sampled three quanta in a row,
+ with the last sample being taken in the edge between TSEG1 and TSEG2.
+ Three samples should only be used for relatively slow baudrates.
+ :param bool strict:
+ If True, restrict bit timings to the minimum required range as defined in
+ ISO 11898. This can be used to ensure compatibility across a wide variety
+ of CAN hardware.
+ :raises ValueError:
+ if the arguments are invalid.
+ """
+ self._data: BitTimingDict = {
+ "f_clock": f_clock,
+ "brp": brp,
+ "tseg1": tseg1,
+ "tseg2": tseg2,
+ "sjw": sjw,
+ "nof_samples": nof_samples,
+ }
+ self._validate()
+ if strict:
+ self._restrict_to_minimum_range()
+
+ def _validate(self) -> None:
+ if not 1 <= self.brp <= 64:
+ raise ValueError(f"bitrate prescaler (={self.brp}) must be in [1...64].")
+
+ if not 1 <= self.tseg1 <= 16:
+ raise ValueError(f"tseg1 (={self.tseg1}) must be in [1...16].")
+
+ if not 1 <= self.tseg2 <= 8:
+ raise ValueError(f"tseg2 (={self.tseg2}) must be in [1...8].")
+
+ if not 1 <= self.sjw <= 4:
+ raise ValueError(f"sjw (={self.sjw}) must be in [1...4].")
+
+ if self.sjw > self.tseg2:
+ raise ValueError(
+ f"sjw (={self.sjw}) must not be greater than tseg2 (={self.tseg2})."
+ )
+
+ if self.sample_point < 50.0:
+ raise ValueError(
+ f"The sample point must be greater than or equal to 50% "
+ f"(sample_point={self.sample_point:.2f}%)."
+ )
+
+ if self.nof_samples not in (1, 3):
+ raise ValueError("nof_samples must be 1 or 3")
+
+ def _restrict_to_minimum_range(self) -> None:
+ if not 8 <= self.nbt <= 25:
+ raise ValueError(f"nominal bit time (={self.nbt}) must be in [8...25].")
+
+ if not 1 <= self.brp <= 32:
+ raise ValueError(f"bitrate prescaler (={self.brp}) must be in [1...32].")
+
+ if not 5_000 <= self.bitrate <= 1_000_000:
+ raise ValueError(
+ f"bitrate (={self.bitrate}) must be in [5,000...1,000,000]."
+ )
+
+ @classmethod
+ def from_bitrate_and_segments(
+ cls,
+ f_clock: int,
+ bitrate: int,
+ tseg1: int,
+ tseg2: int,
+ sjw: int,
+ nof_samples: int = 1,
+ strict: bool = False,
+ ) -> "BitTiming":
+ """Create a :class:`~can.BitTiming` instance from bitrate and segment lengths.
+
+ :param int f_clock:
+ The CAN system clock frequency in Hz.
+ :param int bitrate:
+ Bitrate in bit/s.
+ :param int tseg1:
+ Time segment 1, that is, the number of quanta from (but not including)
+ the Sync Segment to the sampling point.
+ :param int tseg2:
+ Time segment 2, that is, the number of quanta from the sampling
+ point to the end of the bit.
+ :param int sjw:
+ The Synchronization Jump Width. Decides the maximum number of time quanta
+ that the controller can resynchronize every bit.
+ :param int nof_samples:
+ Either 1 or 3. Some CAN controllers can also sample each bit three times.
+ In this case, the bit will be sampled three quanta in a row,
+ with the last sample being taken in the edge between TSEG1 and TSEG2.
+ Three samples should only be used for relatively slow baudrates.
+ :param bool strict:
+ If True, restrict bit timings to the minimum required range as defined in
+ ISO 11898. This can be used to ensure compatibility across a wide variety
+ of CAN hardware.
+ :raises ValueError:
+ if the arguments are invalid.
+ """
+ try:
+ brp = round(f_clock / (bitrate * (1 + tseg1 + tseg2)))
+ except ZeroDivisionError:
+ raise ValueError("Invalid inputs") from None
+
+ bt = cls(
+ f_clock=f_clock,
+ brp=brp,
+ tseg1=tseg1,
+ tseg2=tseg2,
+ sjw=sjw,
+ nof_samples=nof_samples,
+ strict=strict,
+ )
+ if abs(bt.bitrate - bitrate) > bitrate / 256:
+ raise ValueError(
+ f"the effective bitrate (={bt.bitrate}) diverges "
+ f"from the requested bitrate (={bitrate})"
+ )
+ return bt
+
+ @classmethod
+ def from_registers(
+ cls,
+ f_clock: int,
+ btr0: int,
+ btr1: int,
+ ) -> "BitTiming":
+ """Create a :class:`~can.BitTiming` instance from registers btr0 and btr1.
+
+ :param int f_clock:
+ The CAN system clock frequency in Hz.
+ :param int btr0:
+ The BTR0 register value used by many CAN controllers.
+ :param int btr1:
+ The BTR1 register value used by many CAN controllers.
+ :raises ValueError:
+ if the arguments are invalid.
+ """
+ if not 0 <= btr0 < 2**16:
+ raise ValueError(f"Invalid btr0 value. ({btr0})")
+ if not 0 <= btr1 < 2**16:
+ raise ValueError(f"Invalid btr1 value. ({btr1})")
+
+ brp = (btr0 & 0x3F) + 1
+ sjw = (btr0 >> 6) + 1
+ tseg1 = (btr1 & 0xF) + 1
+ tseg2 = ((btr1 >> 4) & 0x7) + 1
+ nof_samples = 3 if btr1 & 0x80 else 1
+ return cls(
+ brp=brp,
+ f_clock=f_clock,
+ tseg1=tseg1,
+ tseg2=tseg2,
+ sjw=sjw,
+ nof_samples=nof_samples,
+ )
+
+ @classmethod
+ def iterate_from_sample_point(
+ cls, f_clock: int, bitrate: int, sample_point: float = 69.0
+ ) -> Iterator["BitTiming"]:
+ """Create a :class:`~can.BitTiming` iterator with all the solutions for a sample point.
+
+ :param int f_clock:
+ The CAN system clock frequency in Hz.
+ :param int bitrate:
+ Bitrate in bit/s.
+ :param int sample_point:
+ The sample point value in percent.
+ :raises ValueError:
+ if the arguments are invalid.
+ """
+
+ if sample_point < 50.0:
+ raise ValueError(f"sample_point (={sample_point}) must not be below 50%.")
+
+ for brp in range(1, 65):
+ nbt = int(f_clock / (bitrate * brp))
+ if nbt < 8:
+ break
+
+ effective_bitrate = f_clock / (nbt * brp)
+ if abs(effective_bitrate - bitrate) > bitrate / 256:
+ continue
+
+ tseg1 = round(sample_point / 100 * nbt) - 1
+ # limit tseg1, so tseg2 is at least 1 TQ
+ tseg1 = min(tseg1, nbt - 2)
+
+ tseg2 = nbt - tseg1 - 1
+ sjw = min(tseg2, 4)
+
+ try:
+ bt = BitTiming(
+ f_clock=f_clock,
+ brp=brp,
+ tseg1=tseg1,
+ tseg2=tseg2,
+ sjw=sjw,
+ strict=True,
+ )
+ yield bt
+ except ValueError:
+ continue
+
+ @classmethod
+ def from_sample_point(
+ cls, f_clock: int, bitrate: int, sample_point: float = 69.0
+ ) -> "BitTiming":
+ """Create a :class:`~can.BitTiming` instance for a sample point.
+
+ This function tries to find bit timings, which are close to the requested
+ sample point. It does not take physical bus properties into account, so the
+ calculated bus timings might not work properly for you.
+
+ The :func:`oscillator_tolerance` function might be helpful to evaluate the
+ bus timings.
+
+ :param int f_clock:
+ The CAN system clock frequency in Hz.
+ :param int bitrate:
+ Bitrate in bit/s.
+ :param int sample_point:
+ The sample point value in percent.
+ :raises ValueError:
+ if the arguments are invalid.
+ """
+
+ if sample_point < 50.0:
+ raise ValueError(f"sample_point (={sample_point}) must not be below 50%.")
+
+ possible_solutions: list[BitTiming] = list(
+ cls.iterate_from_sample_point(f_clock, bitrate, sample_point)
+ )
+
+ if not possible_solutions:
+ raise ValueError("No suitable bit timings found.")
+
+ # sort solutions
+ for key, reverse in (
+ # prefer low prescaler
+ (lambda x: x.brp, False),
+ # prefer low sample point deviation from requested values
+ (lambda x: abs(x.sample_point - sample_point), False),
+ ):
+ possible_solutions.sort(key=key, reverse=reverse)
+
+ return possible_solutions[0]
+
+ @property
+ def f_clock(self) -> int:
+ """The CAN system clock frequency in Hz."""
+ return self._data["f_clock"]
+
+ @property
+ def bitrate(self) -> int:
+ """Bitrate in bits/s."""
+ return round(self.f_clock / (self.nbt * self.brp))
+
+ @property
+ def brp(self) -> int:
+ """Bit Rate Prescaler."""
+ return self._data["brp"]
+
+ @property
+ def tq(self) -> int:
+ """Time quantum in nanoseconds"""
+ return round(self.brp / self.f_clock * 1e9)
+
+ @property
+ def nbt(self) -> int:
+ """Nominal Bit Time."""
+ return 1 + self.tseg1 + self.tseg2
+
+ @property
+ def tseg1(self) -> int:
+ """Time segment 1.
+
+ The number of quanta from (but not including) the Sync Segment to the sampling point.
+ """
+ return self._data["tseg1"]
+
+ @property
+ def tseg2(self) -> int:
+ """Time segment 2.
+
+ The number of quanta from the sampling point to the end of the bit.
+ """
+ return self._data["tseg2"]
+
+ @property
+ def sjw(self) -> int:
+ """Synchronization Jump Width."""
+ return self._data["sjw"]
+
+ @property
+ def nof_samples(self) -> int:
+ """Number of samples (1 or 3)."""
+ return self._data["nof_samples"]
+
+ @property
+ def sample_point(self) -> float:
+ """Sample point in percent."""
+ return 100.0 * (1 + self.tseg1) / (1 + self.tseg1 + self.tseg2)
+
+ @property
+ def btr0(self) -> int:
+ """Bit timing register 0 for SJA1000."""
+ return (self.sjw - 1) << 6 | self.brp - 1
+
+ @property
+ def btr1(self) -> int:
+ """Bit timing register 1 for SJA1000."""
+ sam = 1 if self.nof_samples == 3 else 0
+ return sam << 7 | (self.tseg2 - 1) << 4 | self.tseg1 - 1
+
+ def oscillator_tolerance(
+ self,
+ node_loop_delay_ns: float = 250.0,
+ bus_length_m: float = 10.0,
+ ) -> float:
+ """Oscillator tolerance in percent according to ISO 11898-1.
+
+ :param float node_loop_delay_ns:
+ Transceiver loop delay in nanoseconds.
+ :param float bus_length_m:
+ Bus length in meters.
+ """
+ delay_per_meter = 5
+ bidirectional_propagation_delay_ns = 2 * (
+ node_loop_delay_ns + delay_per_meter * bus_length_m
+ )
+
+ prop_seg = math.ceil(bidirectional_propagation_delay_ns / self.tq)
+ nom_phase_seg1 = self.tseg1 - prop_seg
+ nom_phase_seg2 = self.tseg2
+ df_clock_list = [
+ _oscillator_tolerance_condition_1(nom_sjw=self.sjw, nbt=self.nbt),
+ _oscillator_tolerance_condition_2(
+ nbt=self.nbt,
+ nom_phase_seg1=nom_phase_seg1,
+ nom_phase_seg2=nom_phase_seg2,
+ ),
+ ]
+ return max(0.0, min(df_clock_list) * 100)
+
+ def recreate_with_f_clock(self, f_clock: int) -> "BitTiming":
+ """Return a new :class:`~can.BitTiming` instance with the given *f_clock* but the same
+ bit rate and sample point.
+
+ :param int f_clock:
+ The CAN system clock frequency in Hz.
+ :raises ValueError:
+ if no suitable bit timings were found.
+ """
+ # try the most simple solution first: another bitrate prescaler
+ try:
+ return BitTiming.from_bitrate_and_segments(
+ f_clock=f_clock,
+ bitrate=self.bitrate,
+ tseg1=self.tseg1,
+ tseg2=self.tseg2,
+ sjw=self.sjw,
+ nof_samples=self.nof_samples,
+ strict=True,
+ )
+ except ValueError:
+ pass
+
+ # create a new timing instance with the same sample point
+ bt = BitTiming.from_sample_point(
+ f_clock=f_clock, bitrate=self.bitrate, sample_point=self.sample_point
+ )
+ if abs(bt.sample_point - self.sample_point) > 1.0:
+ raise ValueError(
+ "f_clock change failed because of sample point discrepancy."
+ )
+ # adapt synchronization jump width, so it has the same size relative to bit time as self
+ sjw = round(self.sjw / self.nbt * bt.nbt)
+ sjw = max(1, min(4, bt.tseg2, sjw))
+ bt._data["sjw"] = sjw # pylint: disable=protected-access
+ bt._data["nof_samples"] = self.nof_samples # pylint: disable=protected-access
+ bt._validate() # pylint: disable=protected-access
+ return bt
+
+ def __str__(self) -> str:
+ segments = [
+ f"BR: {self.bitrate:_} bit/s",
+ f"SP: {self.sample_point:.2f}%",
+ f"BRP: {self.brp}",
+ f"TSEG1: {self.tseg1}",
+ f"TSEG2: {self.tseg2}",
+ f"SJW: {self.sjw}",
+ f"BTR: {self.btr0:02X}{self.btr1:02X}h",
+ f"CLK: {self.f_clock / 1e6:.0f}MHz",
+ ]
+ return ", ".join(segments)
+
+ def __repr__(self) -> str:
+ args = ", ".join(f"{key}={value}" for key, value in self.items())
+ return f"can.{self.__class__.__name__}({args})"
+
+ def __getitem__(self, key: str) -> int:
+ return cast("int", self._data.__getitem__(key))
+
+ def __len__(self) -> int:
+ return self._data.__len__()
+
+ def __iter__(self) -> Iterator[str]:
+ return self._data.__iter__()
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, BitTiming):
+ return False
+
+ return self._data == other._data
+
+ def __hash__(self) -> int:
+ return tuple(self._data.values()).__hash__()
+
+
+class BitTimingFd(Mapping[str, int]):
+ """Representation of a bit timing configuration for a CAN FD bus.
+
+ The class can be constructed in multiple ways, depending on the information
+ available. The preferred way is using CAN clock frequency, bit rate prescaler, tseg1,
+ tseg2 and sjw for both the arbitration (nominal) and data phase::
+
+ can.BitTimingFd(
+ f_clock=80_000_000,
+ nom_brp=1,
+ nom_tseg1=59,
+ nom_tseg2=20,
+ nom_sjw=10,
+ data_brp=1,
+ data_tseg1=6,
+ data_tseg2=3,
+ data_sjw=2,
+ )
+
+ Alternatively you can set the bit rates instead of the bit rate prescalers::
+
+ can.BitTimingFd.from_bitrate_and_segments(
+ f_clock=80_000_000,
+ nom_bitrate=1_000_000,
+ nom_tseg1=59,
+ nom_tseg2=20,
+ nom_sjw=10,
+ data_bitrate=8_000_000,
+ data_tseg1=6,
+ data_tseg2=3,
+ data_sjw=2,
+ )
+
+ It is also possible to calculate the timings for a given
+ pair of arbitration and data sample points::
+
+ can.BitTimingFd.from_sample_point(
+ f_clock=80_000_000,
+ nom_bitrate=1_000_000,
+ nom_sample_point=75.0,
+ data_bitrate=8_000_000,
+ data_sample_point=70.0,
+ )
+ """
+
+ def __init__( # pylint: disable=too-many-arguments
+ self,
+ f_clock: int,
+ nom_brp: int,
+ nom_tseg1: int,
+ nom_tseg2: int,
+ nom_sjw: int,
+ data_brp: int,
+ data_tseg1: int,
+ data_tseg2: int,
+ data_sjw: int,
+ strict: bool = False,
+ ) -> None:
+ """
+ Initialize a BitTimingFd instance with the specified parameters.
+
+ :param int f_clock:
+ The CAN system clock frequency in Hz.
+ :param int nom_brp:
+ Nominal (arbitration) phase bitrate prescaler.
+ :param int nom_tseg1:
+ Nominal phase Time segment 1, that is, the number of quanta from (but not including)
+ the Sync Segment to the sampling point.
+ :param int nom_tseg2:
+ Nominal phase Time segment 2, that is, the number of quanta from the sampling
+ point to the end of the bit.
+ :param int nom_sjw:
+ The Synchronization Jump Width for the nominal phase. This value determines
+ the maximum number of time quanta that the controller can resynchronize every bit.
+ :param int data_brp:
+ Data phase bitrate prescaler.
+ :param int data_tseg1:
+ Data phase Time segment 1, that is, the number of quanta from (but not including)
+ the Sync Segment to the sampling point.
+ :param int data_tseg2:
+ Data phase Time segment 2, that is, the number of quanta from the sampling
+ point to the end of the bit.
+ :param int data_sjw:
+ The Synchronization Jump Width for the data phase. This value determines
+ the maximum number of time quanta that the controller can resynchronize every bit.
+ :param bool strict:
+ If True, restrict bit timings to the minimum required range as defined in
+ ISO 11898. This can be used to ensure compatibility across a wide variety
+ of CAN hardware.
+ :raises ValueError:
+ if the arguments are invalid.
+ """
+ self._data: BitTimingFdDict = {
+ "f_clock": f_clock,
+ "nom_brp": nom_brp,
+ "nom_tseg1": nom_tseg1,
+ "nom_tseg2": nom_tseg2,
+ "nom_sjw": nom_sjw,
+ "data_brp": data_brp,
+ "data_tseg1": data_tseg1,
+ "data_tseg2": data_tseg2,
+ "data_sjw": data_sjw,
+ }
+ self._validate()
+ if strict:
+ self._restrict_to_minimum_range()
+
+ def _validate(self) -> None:
+ for param, value in self._data.items():
+ if value < 0: # type: ignore[operator]
+ err_msg = f"'{param}' (={value}) must not be negative."
+ raise ValueError(err_msg)
+
+ if self.nom_brp < 1:
+ raise ValueError(
+ f"nominal bitrate prescaler (={self.nom_brp}) must be at least 1."
+ )
+
+ if self.data_brp < 1:
+ raise ValueError(
+ f"data bitrate prescaler (={self.data_brp}) must be at least 1."
+ )
+
+ if self.data_bitrate < self.nom_bitrate:
+ raise ValueError(
+ f"data_bitrate (={self.data_bitrate}) must be greater than or "
+ f"equal to nom_bitrate (={self.nom_bitrate})"
+ )
+
+ if self.nom_sjw > self.nom_tseg2:
+ raise ValueError(
+ f"nom_sjw (={self.nom_sjw}) must not be "
+ f"greater than nom_tseg2 (={self.nom_tseg2})."
+ )
+
+ if self.data_sjw > self.data_tseg2:
+ raise ValueError(
+ f"data_sjw (={self.data_sjw}) must not be "
+ f"greater than data_tseg2 (={self.data_tseg2})."
+ )
+
+ if self.nom_sample_point < 50.0:
+ raise ValueError(
+ f"The arbitration sample point must be greater than or equal to 50% "
+ f"(nom_sample_point={self.nom_sample_point:.2f}%)."
+ )
+
+ if self.data_sample_point < 50.0:
+ raise ValueError(
+ f"The data sample point must be greater than or equal to 50% "
+ f"(data_sample_point={self.data_sample_point:.2f}%)."
+ )
+
+ def _restrict_to_minimum_range(self) -> None:
+ # restrict to minimum required range as defined in ISO 11898
+ if not 8 <= self.nbt <= 80:
+ raise ValueError(f"Nominal bit time (={self.nbt}) must be in [8...80]")
+
+ if not 5 <= self.dbt <= 25:
+ raise ValueError(f"Nominal bit time (={self.dbt}) must be in [5...25]")
+
+ if not 1 <= self.data_tseg1 <= 16:
+ raise ValueError(f"data_tseg1 (={self.data_tseg1}) must be in [1...16].")
+
+ if not 2 <= self.data_tseg2 <= 8:
+ raise ValueError(f"data_tseg2 (={self.data_tseg2}) must be in [2...8].")
+
+ if not 1 <= self.data_sjw <= 8:
+ raise ValueError(f"data_sjw (={self.data_sjw}) must be in [1...8].")
+
+ if self.nom_brp == self.data_brp:
+ # shared prescaler
+ if not 2 <= self.nom_tseg1 <= 128:
+ raise ValueError(f"nom_tseg1 (={self.nom_tseg1}) must be in [2...128].")
+
+ if not 2 <= self.nom_tseg2 <= 32:
+ raise ValueError(f"nom_tseg2 (={self.nom_tseg2}) must be in [2...32].")
+
+ if not 1 <= self.nom_sjw <= 32:
+ raise ValueError(f"nom_sjw (={self.nom_sjw}) must be in [1...32].")
+ else:
+ # separate prescaler
+ if not 2 <= self.nom_tseg1 <= 64:
+ raise ValueError(f"nom_tseg1 (={self.nom_tseg1}) must be in [2...64].")
+
+ if not 2 <= self.nom_tseg2 <= 16:
+ raise ValueError(f"nom_tseg2 (={self.nom_tseg2}) must be in [2...16].")
+
+ if not 1 <= self.nom_sjw <= 16:
+ raise ValueError(f"nom_sjw (={self.nom_sjw}) must be in [1...16].")
+
+ @classmethod
+ def from_bitrate_and_segments( # pylint: disable=too-many-arguments
+ cls,
+ f_clock: int,
+ nom_bitrate: int,
+ nom_tseg1: int,
+ nom_tseg2: int,
+ nom_sjw: int,
+ data_bitrate: int,
+ data_tseg1: int,
+ data_tseg2: int,
+ data_sjw: int,
+ strict: bool = False,
+ ) -> "BitTimingFd":
+ """
+ Create a :class:`~can.BitTimingFd` instance with the bitrates and segments lengths.
+
+ :param int f_clock:
+ The CAN system clock frequency in Hz.
+ :param int nom_bitrate:
+ Nominal (arbitration) phase bitrate in bit/s.
+ :param int nom_tseg1:
+ Nominal phase Time segment 1, that is, the number of quanta from (but not including)
+ the Sync Segment to the sampling point.
+ :param int nom_tseg2:
+ Nominal phase Time segment 2, that is, the number of quanta from the sampling
+ point to the end of the bit.
+ :param int nom_sjw:
+ The Synchronization Jump Width for the nominal phase. This value determines
+ the maximum number of time quanta that the controller can resynchronize every bit.
+ :param int data_bitrate:
+ Data phase bitrate in bit/s.
+ :param int data_tseg1:
+ Data phase Time segment 1, that is, the number of quanta from (but not including)
+ the Sync Segment to the sampling point.
+ :param int data_tseg2:
+ Data phase Time segment 2, that is, the number of quanta from the sampling
+ point to the end of the bit.
+ :param int data_sjw:
+ The Synchronization Jump Width for the data phase. This value determines
+ the maximum number of time quanta that the controller can resynchronize every bit.
+ :param bool strict:
+ If True, restrict bit timings to the minimum required range as defined in
+ ISO 11898. This can be used to ensure compatibility across a wide variety
+ of CAN hardware.
+ :raises ValueError:
+ if the arguments are invalid.
+ """
+ try:
+ nom_brp = round(f_clock / (nom_bitrate * (1 + nom_tseg1 + nom_tseg2)))
+ data_brp = round(f_clock / (data_bitrate * (1 + data_tseg1 + data_tseg2)))
+ except ZeroDivisionError:
+ raise ValueError("Invalid inputs.") from None
+
+ bt = cls(
+ f_clock=f_clock,
+ nom_brp=nom_brp,
+ nom_tseg1=nom_tseg1,
+ nom_tseg2=nom_tseg2,
+ nom_sjw=nom_sjw,
+ data_brp=data_brp,
+ data_tseg1=data_tseg1,
+ data_tseg2=data_tseg2,
+ data_sjw=data_sjw,
+ strict=strict,
+ )
+
+ if abs(bt.nom_bitrate - nom_bitrate) > nom_bitrate / 256:
+ raise ValueError(
+ f"the effective nom. bitrate (={bt.nom_bitrate}) diverges "
+ f"from the requested nom. bitrate (={nom_bitrate})"
+ )
+
+ if abs(bt.data_bitrate - data_bitrate) > data_bitrate / 256:
+ raise ValueError(
+ f"the effective data bitrate (={bt.data_bitrate}) diverges "
+ f"from the requested data bitrate (={data_bitrate})"
+ )
+
+ return bt
+
+ @classmethod
+ def iterate_from_sample_point(
+ cls,
+ f_clock: int,
+ nom_bitrate: int,
+ nom_sample_point: float,
+ data_bitrate: int,
+ data_sample_point: float,
+ ) -> Iterator["BitTimingFd"]:
+ """Create an :class:`~can.BitTimingFd` iterator with all the solutions for a sample point.
+
+ :param int f_clock:
+ The CAN system clock frequency in Hz.
+ :param int nom_bitrate:
+ Nominal bitrate in bit/s.
+ :param int nom_sample_point:
+ The sample point value of the arbitration phase in percent.
+ :param int data_bitrate:
+ Data bitrate in bit/s.
+ :param int data_sample_point:
+ The sample point value of the data phase in percent.
+ :raises ValueError:
+ if the arguments are invalid.
+ """
+ if nom_sample_point < 50.0:
+ raise ValueError(
+ f"nom_sample_point (={nom_sample_point}) must not be below 50%."
+ )
+
+ if data_sample_point < 50.0:
+ raise ValueError(
+ f"data_sample_point (={data_sample_point}) must not be below 50%."
+ )
+
+ sync_seg = 1
+
+ for nom_brp in range(1, 257):
+ nbt = int(f_clock / (nom_bitrate * nom_brp))
+ if nbt < 1:
+ break
+
+ effective_nom_bitrate = f_clock / (nbt * nom_brp)
+ if abs(effective_nom_bitrate - nom_bitrate) > nom_bitrate / 256:
+ continue
+
+ nom_tseg1 = round(nom_sample_point / 100 * nbt) - 1
+ # limit tseg1, so tseg2 is at least 2 TQ
+ nom_tseg1 = min(nom_tseg1, nbt - sync_seg - 2)
+ nom_tseg2 = nbt - nom_tseg1 - 1
+
+ nom_sjw = min(nom_tseg2, 128)
+
+ for data_brp in range(1, 257):
+ dbt = round(int(f_clock / (data_bitrate * data_brp)))
+ if dbt < 1:
+ break
+
+ effective_data_bitrate = f_clock / (dbt * data_brp)
+ if abs(effective_data_bitrate - data_bitrate) > data_bitrate / 256:
+ continue
+
+ data_tseg1 = round(data_sample_point / 100 * dbt) - 1
+ # limit tseg1, so tseg2 is at least 2 TQ
+ data_tseg1 = min(data_tseg1, dbt - sync_seg - 2)
+ data_tseg2 = dbt - data_tseg1 - 1
+
+ data_sjw = min(data_tseg2, 16)
+
+ try:
+ bt = BitTimingFd(
+ f_clock=f_clock,
+ nom_brp=nom_brp,
+ nom_tseg1=nom_tseg1,
+ nom_tseg2=nom_tseg2,
+ nom_sjw=nom_sjw,
+ data_brp=data_brp,
+ data_tseg1=data_tseg1,
+ data_tseg2=data_tseg2,
+ data_sjw=data_sjw,
+ strict=True,
+ )
+ yield bt
+ except ValueError:
+ continue
+
+ @classmethod
+ def from_sample_point(
+ cls,
+ f_clock: int,
+ nom_bitrate: int,
+ nom_sample_point: float,
+ data_bitrate: int,
+ data_sample_point: float,
+ ) -> "BitTimingFd":
+ """Create a :class:`~can.BitTimingFd` instance for a sample point.
+
+ This function tries to find bit timings, which are close to the requested
+ sample points. It does not take physical bus properties into account, so the
+ calculated bus timings might not work properly for you.
+
+ The :func:`oscillator_tolerance` function might be helpful to evaluate the
+ bus timings.
+
+ :param int f_clock:
+ The CAN system clock frequency in Hz.
+ :param int nom_bitrate:
+ Nominal bitrate in bit/s.
+ :param int nom_sample_point:
+ The sample point value of the arbitration phase in percent.
+ :param int data_bitrate:
+ Data bitrate in bit/s.
+ :param int data_sample_point:
+ The sample point value of the data phase in percent.
+ :raises ValueError:
+ if the arguments are invalid.
+ """
+ if nom_sample_point < 50.0:
+ raise ValueError(
+ f"nom_sample_point (={nom_sample_point}) must not be below 50%."
+ )
+
+ if data_sample_point < 50.0:
+ raise ValueError(
+ f"data_sample_point (={data_sample_point}) must not be below 50%."
+ )
+
+ possible_solutions: list[BitTimingFd] = list(
+ cls.iterate_from_sample_point(
+ f_clock,
+ nom_bitrate,
+ nom_sample_point,
+ data_bitrate,
+ data_sample_point,
+ )
+ )
+
+ if not possible_solutions:
+ raise ValueError("No suitable bit timings found.")
+
+ # prefer using the same prescaler for arbitration and data phase
+ same_prescaler = list(
+ filter(lambda x: x.nom_brp == x.data_brp, possible_solutions)
+ )
+ if same_prescaler:
+ possible_solutions = same_prescaler
+
+ # sort solutions
+ for key, reverse in (
+ # prefer low prescaler
+ (lambda x: x.nom_brp + x.data_brp, False),
+ # prefer same prescaler for arbitration and data
+ (lambda x: abs(x.nom_brp - x.data_brp), False),
+ # prefer low sample point deviation from requested values
+ (
+ lambda x: (
+ abs(x.nom_sample_point - nom_sample_point)
+ + abs(x.data_sample_point - data_sample_point)
+ ),
+ False,
+ ),
+ ):
+ possible_solutions.sort(key=key, reverse=reverse)
+
+ return possible_solutions[0]
+
+ @property
+ def f_clock(self) -> int:
+ """The CAN system clock frequency in Hz."""
+ return self._data["f_clock"]
+
+ @property
+ def nom_bitrate(self) -> int:
+ """Nominal (arbitration phase) bitrate."""
+ return round(self.f_clock / (self.nbt * self.nom_brp))
+
+ @property
+ def nom_brp(self) -> int:
+ """Prescaler value for the arbitration phase."""
+ return self._data["nom_brp"]
+
+ @property
+ def nom_tq(self) -> int:
+ """Nominal time quantum in nanoseconds"""
+ return round(self.nom_brp / self.f_clock * 1e9)
+
+ @property
+ def nbt(self) -> int:
+ """Number of time quanta in a bit of the arbitration phase."""
+ return 1 + self.nom_tseg1 + self.nom_tseg2
+
+ @property
+ def nom_tseg1(self) -> int:
+ """Time segment 1 value of the arbitration phase.
+
+ This is the sum of the propagation time segment and the phase buffer segment 1.
+ """
+ return self._data["nom_tseg1"]
+
+ @property
+ def nom_tseg2(self) -> int:
+ """Time segment 2 value of the arbitration phase. Also known as phase buffer segment 2."""
+ return self._data["nom_tseg2"]
+
+ @property
+ def nom_sjw(self) -> int:
+ """Synchronization jump width of the arbitration phase.
+
+ The phase buffer segments may be shortened or lengthened by this value.
+ """
+ return self._data["nom_sjw"]
+
+ @property
+ def nom_sample_point(self) -> float:
+ """Sample point of the arbitration phase in percent."""
+ return 100.0 * (1 + self.nom_tseg1) / (1 + self.nom_tseg1 + self.nom_tseg2)
+
+ @property
+ def data_bitrate(self) -> int:
+ """Bitrate of the data phase in bit/s."""
+ return round(self.f_clock / (self.dbt * self.data_brp))
+
+ @property
+ def data_brp(self) -> int:
+ """Prescaler value for the data phase."""
+ return self._data["data_brp"]
+
+ @property
+ def data_tq(self) -> int:
+ """Data time quantum in nanoseconds"""
+ return round(self.data_brp / self.f_clock * 1e9)
+
+ @property
+ def dbt(self) -> int:
+ """Number of time quanta in a bit of the data phase."""
+ return 1 + self.data_tseg1 + self.data_tseg2
+
+ @property
+ def data_tseg1(self) -> int:
+ """TSEG1 value of the data phase.
+
+ This is the sum of the propagation time segment and the phase buffer segment 1.
+ """
+ return self._data["data_tseg1"]
+
+ @property
+ def data_tseg2(self) -> int:
+ """TSEG2 value of the data phase. Also known as phase buffer segment 2."""
+ return self._data["data_tseg2"]
+
+ @property
+ def data_sjw(self) -> int:
+ """Synchronization jump width of the data phase.
+
+ The phase buffer segments may be shortened or lengthened by this value.
+ """
+ return self._data["data_sjw"]
+
+ @property
+ def data_sample_point(self) -> float:
+ """Sample point of the data phase in percent."""
+ return 100.0 * (1 + self.data_tseg1) / (1 + self.data_tseg1 + self.data_tseg2)
+
+ def oscillator_tolerance(
+ self,
+ node_loop_delay_ns: float = 250.0,
+ bus_length_m: float = 10.0,
+ ) -> float:
+ """Oscillator tolerance in percent according to ISO 11898-1.
+
+ :param float node_loop_delay_ns:
+ Transceiver loop delay in nanoseconds.
+ :param float bus_length_m:
+ Bus length in meters.
+ """
+ delay_per_meter = 5
+ bidirectional_propagation_delay_ns = 2 * (
+ node_loop_delay_ns + delay_per_meter * bus_length_m
+ )
+
+ prop_seg = math.ceil(bidirectional_propagation_delay_ns / self.nom_tq)
+ nom_phase_seg1 = self.nom_tseg1 - prop_seg
+ nom_phase_seg2 = self.nom_tseg2
+
+ data_phase_seg2 = self.data_tseg2
+
+ df_clock_list = [
+ _oscillator_tolerance_condition_1(nom_sjw=self.nom_sjw, nbt=self.nbt),
+ _oscillator_tolerance_condition_2(
+ nbt=self.nbt,
+ nom_phase_seg1=nom_phase_seg1,
+ nom_phase_seg2=nom_phase_seg2,
+ ),
+ _oscillator_tolerance_condition_3(data_sjw=self.data_sjw, dbt=self.dbt),
+ _oscillator_tolerance_condition_4(
+ nom_phase_seg1=nom_phase_seg1,
+ nom_phase_seg2=nom_phase_seg2,
+ data_phase_seg2=data_phase_seg2,
+ nbt=self.nbt,
+ dbt=self.dbt,
+ data_brp=self.data_brp,
+ nom_brp=self.nom_brp,
+ ),
+ _oscillator_tolerance_condition_5(
+ data_sjw=self.data_sjw,
+ data_brp=self.data_brp,
+ nom_brp=self.nom_brp,
+ data_phase_seg2=data_phase_seg2,
+ nom_phase_seg2=nom_phase_seg2,
+ nbt=self.nbt,
+ dbt=self.dbt,
+ ),
+ ]
+ return max(0.0, min(df_clock_list) * 100)
+
+ def recreate_with_f_clock(self, f_clock: int) -> "BitTimingFd":
+ """Return a new :class:`~can.BitTimingFd` instance with the given *f_clock* but the same
+ bit rates and sample points.
+
+ :param int f_clock:
+ The CAN system clock frequency in Hz.
+ :raises ValueError:
+ if no suitable bit timings were found.
+ """
+ # try the most simple solution first: another bitrate prescaler
+ try:
+ return BitTimingFd.from_bitrate_and_segments(
+ f_clock=f_clock,
+ nom_bitrate=self.nom_bitrate,
+ nom_tseg1=self.nom_tseg1,
+ nom_tseg2=self.nom_tseg2,
+ nom_sjw=self.nom_sjw,
+ data_bitrate=self.data_bitrate,
+ data_tseg1=self.data_tseg1,
+ data_tseg2=self.data_tseg2,
+ data_sjw=self.data_sjw,
+ strict=True,
+ )
+ except ValueError:
+ pass
+
+ # create a new timing instance with the same sample points
+ bt = BitTimingFd.from_sample_point(
+ f_clock=f_clock,
+ nom_bitrate=self.nom_bitrate,
+ nom_sample_point=self.nom_sample_point,
+ data_bitrate=self.data_bitrate,
+ data_sample_point=self.data_sample_point,
+ )
+ if (
+ abs(bt.nom_sample_point - self.nom_sample_point) > 1.0
+ or abs(bt.data_sample_point - self.data_sample_point) > 1.0
+ ):
+ raise ValueError(
+ "f_clock change failed because of sample point discrepancy."
+ )
+ # adapt synchronization jump width, so it has the same size relative to bit time as self
+ nom_sjw = round(self.nom_sjw / self.nbt * bt.nbt)
+ nom_sjw = max(1, min(bt.nom_tseg2, nom_sjw))
+ bt._data["nom_sjw"] = nom_sjw # pylint: disable=protected-access
+ data_sjw = round(self.data_sjw / self.dbt * bt.dbt)
+ data_sjw = max(1, min(bt.data_tseg2, data_sjw))
+ bt._data["data_sjw"] = data_sjw # pylint: disable=protected-access
+ bt._validate() # pylint: disable=protected-access
+ return bt
+
+ def __str__(self) -> str:
+ segments = [
+ f"NBR: {self.nom_bitrate:_} bit/s",
+ f"NSP: {self.nom_sample_point:.2f}%",
+ f"NBRP: {self.nom_brp}",
+ f"NTSEG1: {self.nom_tseg1}",
+ f"NTSEG2: {self.nom_tseg2}",
+ f"NSJW: {self.nom_sjw}",
+ f"DBR: {self.data_bitrate:_} bit/s",
+ f"DSP: {self.data_sample_point:.2f}%",
+ f"DBRP: {self.data_brp}",
+ f"DTSEG1: {self.data_tseg1}",
+ f"DTSEG2: {self.data_tseg2}",
+ f"DSJW: {self.data_sjw}",
+ f"CLK: {self.f_clock / 1e6:.0f}MHz",
+ ]
+ return ", ".join(segments)
+
+ def __repr__(self) -> str:
+ args = ", ".join(f"{key}={value}" for key, value in self.items())
+ return f"can.{self.__class__.__name__}({args})"
+
+ def __getitem__(self, key: str) -> int:
+ return cast("int", self._data.__getitem__(key))
+
+ def __len__(self) -> int:
+ return self._data.__len__()
+
+ def __iter__(self) -> Iterator[str]:
+ return self._data.__iter__()
+
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, BitTimingFd):
+ return False
+
+ return self._data == other._data
+
+ def __hash__(self) -> int:
+ return tuple(self._data.values()).__hash__()
+
+
+def _oscillator_tolerance_condition_1(nom_sjw: int, nbt: int) -> float:
+ """Arbitration phase - resynchronization"""
+ return nom_sjw / (2 * 10 * nbt)
+
+
+def _oscillator_tolerance_condition_2(
+ nbt: int, nom_phase_seg1: int, nom_phase_seg2: int
+) -> float:
+ """Arbitration phase - sampling of bit after error flag"""
+ return min(nom_phase_seg1, nom_phase_seg2) / (2 * (13 * nbt - nom_phase_seg2))
+
+
+def _oscillator_tolerance_condition_3(data_sjw: int, dbt: int) -> float:
+ """Data phase - resynchronization"""
+ return data_sjw / (2 * 10 * dbt)
+
+
+def _oscillator_tolerance_condition_4(
+ nom_phase_seg1: int,
+ nom_phase_seg2: int,
+ data_phase_seg2: int,
+ nbt: int,
+ dbt: int,
+ data_brp: int,
+ nom_brp: int,
+) -> float:
+ """Data phase - sampling of bit after error flag"""
+ return min(nom_phase_seg1, nom_phase_seg2) / (
+ 2 * ((6 * dbt - data_phase_seg2) * data_brp / nom_brp + 7 * nbt)
+ )
+
+
+def _oscillator_tolerance_condition_5(
+ data_sjw: int,
+ data_brp: int,
+ nom_brp: int,
+ nom_phase_seg2: int,
+ data_phase_seg2: int,
+ nbt: int,
+ dbt: int,
+) -> float:
+ """Data phase - bit rate switch"""
+ max_correctable_phase_shift = data_sjw - max(0.0, nom_brp / data_brp - 1)
+ time_between_resync = 2 * (
+ (2 * nbt - nom_phase_seg2) * nom_brp / data_brp + data_phase_seg2 + 4 * dbt
+ )
+ return max_correctable_phase_shift / time_between_resync
diff --git a/can/bridge.py b/can/bridge.py
new file mode 100644
index 000000000..57ebb368d
--- /dev/null
+++ b/can/bridge.py
@@ -0,0 +1,66 @@
+"""
+Creates a bridge between two CAN buses.
+
+This will connect to two CAN buses. Messages received on one
+bus will be sent to the other bus and vice versa.
+"""
+
+import argparse
+import errno
+import sys
+import time
+from datetime import datetime
+from typing import Final
+
+from can.cli import add_bus_arguments, create_bus_from_namespace
+from can.listener import RedirectReader
+from can.notifier import Notifier
+
+BRIDGE_DESCRIPTION: Final = """\
+Bridge two CAN buses.
+
+Both can buses will be connected so that messages from bus1 will be sent on
+bus2 and messages from bus2 will be sent to bus1.
+"""
+BUS_1_PREFIX: Final = "bus1"
+BUS_2_PREFIX: Final = "bus2"
+
+
+def _parse_bridge_args(args: list[str]) -> argparse.Namespace:
+ """Parse command line arguments for bridge script."""
+
+ parser = argparse.ArgumentParser(description=BRIDGE_DESCRIPTION)
+ add_bus_arguments(parser, prefix=BUS_1_PREFIX, group_title="Bus 1 arguments")
+ add_bus_arguments(parser, prefix=BUS_2_PREFIX, group_title="Bus 2 arguments")
+
+ # print help message when no arguments were given
+ if not args:
+ parser.print_help(sys.stderr)
+ raise SystemExit(errno.EINVAL)
+
+ results, _unknown_args = parser.parse_known_args(args)
+ return results
+
+
+def main() -> None:
+ results = _parse_bridge_args(sys.argv[1:])
+
+ with (
+ create_bus_from_namespace(results, prefix=BUS_1_PREFIX) as bus1,
+ create_bus_from_namespace(results, prefix=BUS_2_PREFIX) as bus2,
+ ):
+ reader1_to_2 = RedirectReader(bus2)
+ reader2_to_1 = RedirectReader(bus1)
+ with Notifier(bus1, [reader1_to_2]), Notifier(bus2, [reader2_to_1]):
+ print(f"CAN Bridge (Started on {datetime.now()})")
+ try:
+ while True:
+ time.sleep(1)
+ except KeyboardInterrupt:
+ pass
+
+ print(f"CAN Bridge (Stopped on {datetime.now()})")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/can/broadcastmanager.py b/can/broadcastmanager.py
index 5e9ff7118..1fea9ac50 100644
--- a/can/broadcastmanager.py
+++ b/can/broadcastmanager.py
@@ -1,6 +1,3 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Exposes several methods for transmitting cyclic messages.
@@ -10,147 +7,382 @@
import abc
import logging
+import platform
+import sys
import threading
import time
+import warnings
+from collections.abc import Callable, Sequence
+from typing import (
+ TYPE_CHECKING,
+ Final,
+ cast,
+)
+
+from can import typechecking
+from can.message import Message
+
+if TYPE_CHECKING:
+ from can.bus import BusABC
+
+
+log = logging.getLogger("can.bcm")
+NANOSECONDS_IN_SECOND: Final[int] = 1_000_000_000
+
+
+class _Pywin32Event:
+ handle: int
+
+
+class _Pywin32:
+ def __init__(self) -> None:
+ import pywintypes # noqa: PLC0415 # pylint: disable=import-outside-toplevel,import-error
+ import win32event # noqa: PLC0415 # pylint: disable=import-outside-toplevel,import-error
+
+ self.pywintypes = pywintypes
+ self.win32event = win32event
+
+ def create_timer(self) -> _Pywin32Event:
+ try:
+ event = self.win32event.CreateWaitableTimerEx(
+ None,
+ None,
+ self.win32event.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
+ self.win32event.TIMER_ALL_ACCESS,
+ )
+ except (
+ AttributeError,
+ OSError,
+ self.pywintypes.error, # pylint: disable=no-member
+ ):
+ event = self.win32event.CreateWaitableTimer(None, False, None)
+ return cast("_Pywin32Event", event)
-log = logging.getLogger('can.bcm')
+ def set_timer(self, event: _Pywin32Event, period_ms: int) -> None:
+ self.win32event.SetWaitableTimer(event.handle, 0, period_ms, None, None, False)
+ def stop_timer(self, event: _Pywin32Event) -> None:
+ self.win32event.SetWaitableTimer(event.handle, 0, 0, None, None, False)
-class CyclicTask(object):
+ def wait_0(self, event: _Pywin32Event) -> None:
+ self.win32event.WaitForSingleObject(event.handle, 0)
+
+ def wait_inf(self, event: _Pywin32Event) -> None:
+ self.win32event.WaitForSingleObject(
+ event.handle,
+ self.win32event.INFINITE,
+ )
+
+
+PYWIN32: _Pywin32 | None = None
+if sys.platform == "win32" and sys.version_info < (3, 11):
+ try:
+ PYWIN32 = _Pywin32()
+ except ImportError:
+ pass
+
+
+class CyclicTask(abc.ABC):
"""
Abstract Base for all cyclic tasks.
"""
@abc.abstractmethod
- def stop(self):
+ def stop(self) -> None:
"""Cancel this periodic task.
+
+ :raises ~can.exceptions.CanError:
+ If stop is called on an already stopped task.
"""
-class CyclicSendTaskABC(CyclicTask):
+class CyclicSendTaskABC(CyclicTask, abc.ABC):
"""
Message send task with defined period
"""
- def __init__(self, message, period):
+ def __init__(self, messages: Sequence[Message] | Message, period: float) -> None:
"""
- :param can.Message message: The message to be sent periodically.
- :param float period: The rate in seconds at which to send the message.
+ :param messages:
+ The messages to be sent periodically.
+ :param period: The rate in seconds at which to send the messages.
+
+ :raises ValueError: If the given messages are invalid
"""
- self.message = message
- self.can_id = message.arbitration_id
- self.arbitration_id = message.arbitration_id
+ messages = self._check_and_convert_messages(messages)
+
+ # Take the Arbitration ID of the first element
+ self.arbitration_id = messages[0].arbitration_id
self.period = period
- super(CyclicSendTaskABC, self).__init__()
+ self.period_ns = round(period * 1e9)
+ self.messages = messages
+
+ @staticmethod
+ def _check_and_convert_messages(
+ messages: Sequence[Message] | Message,
+ ) -> tuple[Message, ...]:
+ """Helper function to convert a Message or Sequence of messages into a
+ tuple, and raises an error when the given value is invalid.
+ Performs error checking to ensure that all Messages have the same
+ arbitration ID and channel.
-class LimitedDurationCyclicSendTaskABC(CyclicSendTaskABC):
+ Should be called when the cyclic task is initialized.
- def __init__(self, message, period, duration):
+ :raises ValueError: If the given messages are invalid
+ """
+ if not isinstance(messages, (list, tuple)):
+ if isinstance(messages, Message):
+ messages = [messages]
+ else:
+ raise ValueError("Must be either a list, tuple, or a Message")
+ if not messages:
+ raise ValueError("Must be at least a list or tuple of length 1")
+ messages = tuple(messages)
+
+ all_same_id = all(
+ message.arbitration_id == messages[0].arbitration_id for message in messages
+ )
+ if not all_same_id:
+ raise ValueError("All Arbitration IDs should be the same")
+
+ all_same_channel = all(
+ message.channel == messages[0].channel for message in messages
+ )
+ if not all_same_channel:
+ raise ValueError("All Channel IDs should be the same")
+
+ return messages
+
+
+class LimitedDurationCyclicSendTaskABC(CyclicSendTaskABC, abc.ABC):
+ def __init__(
+ self,
+ messages: Sequence[Message] | Message,
+ period: float,
+ duration: float | None,
+ ) -> None:
"""Message send task with a defined duration and period.
- :param can.Message message: The message to be sent periodically.
- :param float period: The rate in seconds at which to send the message.
- :param float duration:
- The duration to keep sending this message at given rate.
+ :param messages:
+ The messages to be sent periodically.
+ :param period: The rate in seconds at which to send the messages.
+ :param duration:
+ Approximate duration in seconds to continue sending messages. If
+ no duration is provided, the task will continue indefinitely.
+
+ :raises ValueError: If the given messages are invalid
"""
- super(LimitedDurationCyclicSendTaskABC, self).__init__(message, period)
+ super().__init__(messages, period)
self.duration = duration
+ self.end_time: float | None = None
-class RestartableCyclicTaskABC(CyclicSendTaskABC):
+class RestartableCyclicTaskABC(CyclicSendTaskABC, abc.ABC):
"""Adds support for restarting a stopped cyclic task"""
@abc.abstractmethod
- def start(self):
- """Restart a stopped periodic task.
- """
+ def start(self) -> None:
+ """Restart a stopped periodic task."""
+
+class ModifiableCyclicTaskABC(CyclicSendTaskABC, abc.ABC):
+ def _check_modified_messages(self, messages: tuple[Message, ...]) -> None:
+ """Helper function to perform error checking when modifying the data in
+ the cyclic task.
-class ModifiableCyclicTaskABC(CyclicSendTaskABC):
- """Adds support for modifying a periodic message"""
+ Performs error checking to ensure the arbitration ID and the number of
+ cyclic messages hasn't changed.
- def modify_data(self, message):
- """Update the contents of this periodically sent message without altering
- the timing.
+ Should be called when modify_data is called in the cyclic task.
- :param can.Message message:
- The message with the new :attr:`can.Message.data`.
- Note: The arbitration ID cannot be changed.
+ :raises ValueError: If the given messages are invalid
+ """
+ if len(self.messages) != len(messages):
+ raise ValueError(
+ "The number of new cyclic messages to be sent must be equal to "
+ "the number of messages originally specified for this task"
+ )
+ if self.arbitration_id != messages[0].arbitration_id:
+ raise ValueError(
+ "The arbitration ID of new cyclic messages cannot be changed "
+ "from when the task was created"
+ )
+
+ def modify_data(self, messages: Sequence[Message] | Message) -> None:
+ """Update the contents of the periodically sent messages, without
+ altering the timing.
+
+ :param messages:
+ The messages with the new :attr:`Message.data`.
+
+ Note: The arbitration ID cannot be changed.
+
+ Note: The number of new cyclic messages to be sent must be equal
+ to the original number of messages originally specified for this
+ task.
+
+ :raises ValueError: If the given messages are invalid
"""
- self.message = message
+ messages = self._check_and_convert_messages(messages)
+ self._check_modified_messages(messages)
+ self.messages = messages
-class MultiRateCyclicSendTaskABC(CyclicSendTaskABC):
- """Exposes more of the full power of the TX_SETUP opcode.
- """
- def __init__(self, channel, message, count, initial_period, subsequent_period):
- """
- Transmits a message `count` times at `initial_period` then continues to
- transmit message at `subsequent_period`.
+class MultiRateCyclicSendTaskABC(CyclicSendTaskABC, abc.ABC):
+ """A Cyclic send task that supports switches send frequency after a set time."""
- :param can.interface.Bus channel:
- :param can.Message message:
- :param int count:
- :param float initial_period:
- :param float subsequent_period:
+ def __init__(
+ self,
+ channel: typechecking.Channel,
+ messages: Sequence[Message] | Message,
+ count: int, # pylint: disable=unused-argument
+ initial_period: float, # pylint: disable=unused-argument
+ subsequent_period: float,
+ ) -> None:
"""
- super(MultiRateCyclicSendTaskABC, self).__init__(channel, message, subsequent_period)
-
+ Transmits a message `count` times at `initial_period` then continues to
+ transmit messages at `subsequent_period`.
-class ThreadBasedCyclicSendTask(ModifiableCyclicTaskABC,
- LimitedDurationCyclicSendTaskABC,
- RestartableCyclicTaskABC):
- """Fallback cyclic send task using thread."""
+ :param channel: See interface specific documentation.
+ :param messages:
+ :param count:
+ :param initial_period:
+ :param subsequent_period:
- def __init__(self, bus, lock, message, period, duration=None):
- super(ThreadBasedCyclicSendTask, self).__init__(message, period, duration)
+ :raises ValueError: If the given messages are invalid
+ """
+ super().__init__(messages, subsequent_period)
+ self._channel = channel
+
+
+class ThreadBasedCyclicSendTask(
+ LimitedDurationCyclicSendTaskABC, ModifiableCyclicTaskABC, RestartableCyclicTaskABC
+):
+ """Fallback cyclic send task using daemon thread."""
+
+ def __init__(
+ self,
+ bus: "BusABC",
+ lock: threading.Lock,
+ messages: Sequence[Message] | Message,
+ period: float,
+ duration: float | None = None,
+ on_error: Callable[[Exception], bool] | None = None,
+ autostart: bool = True,
+ modifier_callback: Callable[[Message], None] | None = None,
+ ) -> None:
+ """Transmits `messages` with a `period` seconds for `duration` seconds on a `bus`.
+
+ The `on_error` is called if any error happens on `bus` while sending `messages`.
+ If `on_error` present, and returns ``False`` when invoked, thread is
+ stopped immediately, otherwise, thread continuously tries to send `messages`
+ ignoring errors on a `bus`. Absence of `on_error` means that thread exits immediately
+ on error.
+
+ :param on_error: The callable that accepts an exception if any
+ error happened on a `bus` while sending `messages`,
+ it shall return either ``True`` or ``False`` depending
+ on desired behaviour of `ThreadBasedCyclicSendTask`.
+
+ :raises ValueError: If the given messages are invalid
+ """
+ super().__init__(messages, period, duration)
self.bus = bus
- self.lock = lock
+ self.send_lock = lock
self.stopped = True
- self.thread = None
- self.end_time = time.time() + duration if duration else None
- self.start()
-
- def stop(self):
+ self.thread: threading.Thread | None = None
+ self.on_error = on_error
+ self.modifier_callback = modifier_callback
+
+ self.period_ms = int(round(period * 1000, 0))
+
+ self.event: _Pywin32Event | None = None
+ if PYWIN32:
+ if self.period_ms == 0:
+ # A period of 0 would mean that the timer is signaled only once
+ raise ValueError("The period cannot be smaller than 0.001 (1 ms)")
+ self.event = PYWIN32.create_timer()
+ elif (
+ sys.platform == "win32"
+ and sys.version_info < (3, 11)
+ and platform.python_implementation() == "CPython"
+ ):
+ warnings.warn(
+ f"{self.__class__.__name__} may achieve better timing accuracy "
+ f"if the 'pywin32' package is installed.",
+ RuntimeWarning,
+ stacklevel=1,
+ )
+
+ if autostart:
+ self.start()
+
+ def stop(self) -> None:
self.stopped = True
+ if self.event and PYWIN32:
+ # Reset and signal any pending wait by setting the timer to 0
+ PYWIN32.stop_timer(self.event)
- def start(self):
+ def start(self) -> None:
self.stopped = False
if self.thread is None or not self.thread.is_alive():
- name = "Cyclic send task for 0x%X" % (self.message.arbitration_id)
+ name = f"Cyclic send task for 0x{self.messages[0].arbitration_id:X}"
self.thread = threading.Thread(target=self._run, name=name)
self.thread.daemon = True
+
+ self.end_time: float | None = (
+ time.perf_counter() + self.duration if self.duration else None
+ )
+
+ if self.event and PYWIN32:
+ PYWIN32.set_timer(self.event, self.period_ms)
+
self.thread.start()
- def _run(self):
+ def _run(self) -> None:
+ msg_index = 0
+ msg_due_time_ns = time.perf_counter_ns()
+
+ if self.event and PYWIN32:
+ # Make sure the timer is non-signaled before entering the loop
+ PYWIN32.wait_0(self.event)
+
while not self.stopped:
- # Prevent calling bus.send from multiple threads
- with self.lock:
- started = time.time()
- try:
- self.bus.send(self.message)
- except Exception as exc:
- log.exception(exc)
- break
- if self.end_time is not None and time.time() >= self.end_time:
+ if self.end_time is not None and time.perf_counter() >= self.end_time:
+ self.stop()
break
- # Compensate for the time it takes to send the message
- delay = self.period - (time.time() - started)
- time.sleep(max(0.0, delay))
+ try:
+ if self.modifier_callback is not None:
+ self.modifier_callback(self.messages[msg_index])
+ with self.send_lock:
+ # Prevent calling bus.send from multiple threads
+ self.bus.send(self.messages[msg_index])
+ except Exception as exc: # pylint: disable=broad-except
+ log.exception(exc)
+
+ # stop if `on_error` callback was not given
+ if self.on_error is None:
+ self.stop()
+ raise exc
+
+ # stop if `on_error` returns False
+ if not self.on_error(exc):
+ self.stop()
+ break
-def send_periodic(bus, message, period, *args, **kwargs):
- """
- Send a :class:`~can.Message` every `period` seconds on the given bus.
+ if not self.event:
+ msg_due_time_ns += self.period_ns
- :param can.BusABC bus: A CAN bus which supports sending.
- :param can.Message message: Message to send periodically.
- :param float period: The minimum time between sending messages.
- :return: A started task instance
- """
- log.warning("The function `can.send_periodic` is deprecated and will " +
- "be removed in version 2.3. Please use `can.Bus.send_periodic` instead.")
- return bus.send_periodic(message, period, *args, **kwargs)
+ msg_index = (msg_index + 1) % len(self.messages)
+
+ if self.event and PYWIN32:
+ PYWIN32.wait_inf(self.event)
+ else:
+ # Compensate for the time it takes to send the message
+ delay_ns = msg_due_time_ns - time.perf_counter_ns()
+ if delay_ns > 0:
+ time.sleep(delay_ns / NANOSECONDS_IN_SECOND)
diff --git a/can/bus.py b/can/bus.py
index fd8550cb5..03425caaa 100644
--- a/can/bus.py
+++ b/can/bus.py
@@ -1,40 +1,73 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Contains the ABC bus implementation and its documentation.
"""
-from __future__ import print_function, absolute_import
-
-from abc import ABCMeta, abstractmethod
+import contextlib
import logging
import threading
+from abc import ABC, abstractmethod
+from collections.abc import Callable, Iterator, Sequence
+from enum import Enum, auto
from time import time
-from collections import namedtuple
+from types import TracebackType
+from typing import (
+ cast,
+)
+
+from typing_extensions import Self
-from .broadcastmanager import ThreadBasedCyclicSendTask
+import can.typechecking
+from can.broadcastmanager import CyclicSendTaskABC, ThreadBasedCyclicSendTask
+from can.message import Message
LOG = logging.getLogger(__name__)
-BusState = namedtuple('BusState', 'ACTIVE, PASSIVE, ERROR')
+class BusState(Enum):
+ """The state in which a :class:`can.BusABC` can be."""
+
+ ACTIVE = auto()
+ PASSIVE = auto()
+ ERROR = auto()
+
+
+class CanProtocol(Enum):
+ """The CAN protocol type supported by a :class:`can.BusABC` instance"""
-class BusABC(object):
+ CAN_20 = auto()
+ CAN_FD = auto() # ISO Mode
+ CAN_FD_NON_ISO = auto()
+ CAN_XL = auto()
+
+
+class BusABC(ABC):
"""The CAN Bus Abstract Base Class that serves as the basis
for all concrete interfaces.
- This class may be used as an iterator over the received messages.
+ This class may be used as an iterator over the received messages
+ and as a context manager for auto-closing the bus when done using it.
+
+ Please refer to :ref:`errors` for possible exceptions that may be
+ thrown by certain operations on this bus.
"""
#: a string describing the underlying bus and/or channel
- channel_info = 'unknown'
+ channel_info = "unknown"
#: Log level for received messages
RECV_LOGGING_LEVEL = 9
+ #: Assume that no cleanup is needed until something was initialized
+ _is_shutdown: bool = True
+ _can_protocol: CanProtocol = CanProtocol.CAN_20
+
@abstractmethod
- def __init__(self, channel, can_filters=None, **config):
+ def __init__(
+ self,
+ channel: can.typechecking.Channel,
+ can_filters: can.typechecking.CanFilters | None = None,
+ **kwargs: object,
+ ):
"""Construct and open a CAN bus instance of the specified type.
Subclasses should call though this method with all given parameters
@@ -43,41 +76,50 @@ def __init__(self, channel, can_filters=None, **config):
:param channel:
The can interface identifier. Expected type is backend dependent.
- :param list can_filters:
+ :param can_filters:
See :meth:`~can.BusABC.set_filters` for details.
- :param dict config:
+ :param dict kwargs:
Any backend dependent configurations are passed in this dictionary
+
+ :raises ValueError: If parameters are out of range
+ :raises ~can.exceptions.CanInterfaceNotImplementedError:
+ If the driver cannot be accessed
+ :raises ~can.exceptions.CanInitializationError:
+ If the bus cannot be initialized
"""
+ self._periodic_tasks: list[_SelfRemovingCyclicTask] = []
self.set_filters(can_filters)
+ # Flip the class default value when the constructor finishes. That
+ # usually means the derived class constructor was also successful,
+ # since it calls this parent constructor last.
+ self._is_shutdown: bool = False
- def __str__(self):
+ def __str__(self) -> str:
return self.channel_info
- def recv(self, timeout=None):
+ def recv(self, timeout: float | None = None) -> Message | None:
"""Block waiting for a message from the Bus.
- :type timeout: float or None
:param timeout:
seconds to wait for a message or None to wait indefinitely
- :rtype: can.Message or None
:return:
- None on timeout or a :class:`can.Message` object.
- :raises can.CanError:
- if an error occurred while reading
+ :obj:`None` on timeout or a :class:`~can.Message` object.
+
+ :raises ~can.exceptions.CanOperationError:
+ If an error occurred while reading
"""
start = time()
time_left = timeout
while True:
-
# try to get a message
msg, already_filtered = self._recv_internal(timeout=time_left)
# return it, if it matches
if msg and (already_filtered or self._matches_filters(msg)):
- LOG.log(self.RECV_LOGGING_LEVEL, 'Received: %s', msg)
+ LOG.log(self.RECV_LOGGING_LEVEL, "Received: %s", msg)
return msg
# if not, and timeout is None, try indefinitely
@@ -87,15 +129,14 @@ def recv(self, timeout=None):
# try next one only if there still is time, and with
# reduced timeout
else:
-
time_left = timeout - (time() - start)
if time_left > 0:
continue
- else:
- return None
- def _recv_internal(self, timeout):
+ return None
+
+ def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]:
"""
Read a message from the bus and tell whether it was filtered.
This methods may be called by :meth:`~can.BusABC.recv`
@@ -123,14 +164,13 @@ def _recv_internal(self, timeout):
:param float timeout: seconds to wait for a message,
see :meth:`~can.BusABC.send`
- :rtype: tuple[can.Message, bool] or tuple[None, bool]
:return:
1. a message that was read or None on timeout
2. a bool that is True if message filtering has already
been done and else False
- :raises can.CanError:
- if an error occurred while reading
+ :raises ~can.exceptions.CanOperationError:
+ If an error occurred while reading
:raises NotImplementedError:
if the bus provides it's own :meth:`~can.BusABC.recv`
implementation (legacy implementation)
@@ -139,63 +179,193 @@ def _recv_internal(self, timeout):
raise NotImplementedError("Trying to read from a write only bus?")
@abstractmethod
- def send(self, msg, timeout=None):
+ def send(self, msg: Message, timeout: float | None = None) -> None:
"""Transmit a message to the CAN bus.
Override this method to enable the transmit path.
- :param can.Message msg: A message object.
+ :param Message msg: A message object.
- :type timeout: float or None
:param timeout:
If > 0, wait up to this many seconds for message to be ACK'ed or
for transmit queue to be ready depending on driver implementation.
If timeout is exceeded, an exception will be raised.
Might not be supported by all interfaces.
- None blocks indefinitly.
+ None blocks indefinitely.
- :raises can.CanError:
- if the message could not be sent
+ :raises ~can.exceptions.CanOperationError:
+ If an error occurred while sending
"""
raise NotImplementedError("Trying to write to a readonly bus?")
- def send_periodic(self, msg, period, duration=None):
- """Start sending a message at a given period on this bus.
-
- :param can.Message msg:
- Message to transmit
- :param float period:
+ def send_periodic(
+ self,
+ msgs: Message | Sequence[Message],
+ period: float,
+ duration: float | None = None,
+ store_task: bool = True,
+ autostart: bool = True,
+ modifier_callback: Callable[[Message], None] | None = None,
+ ) -> can.broadcastmanager.CyclicSendTaskABC:
+ """Start sending messages at a given period on this bus.
+
+ The task will be active until one of the following conditions are met:
+
+ - the (optional) duration expires
+ - the Bus instance goes out of scope
+ - the Bus instance is shutdown
+ - :meth:`stop_all_periodic_tasks` is called
+ - the task's :meth:`~can.broadcastmanager.CyclicTask.stop` method is called.
+
+ :param msgs:
+ Message(s) to transmit
+ :param period:
Period in seconds between each message
- :param float duration:
- The duration to keep sending this message at given rate. If
+ :param duration:
+ Approximate duration in seconds to continue sending messages. If
no duration is provided, the task will continue indefinitely.
-
- :return: A started task instance
- :rtype: can.broadcastmanager.CyclicSendTaskABC
+ :param store_task:
+ If True (the default) the task will be attached to this Bus instance.
+ Disable to instead manage tasks manually.
+ :param autostart:
+ If True (the default) the sending task will immediately start after creation.
+ Otherwise, the task has to be started by calling the
+ tasks :meth:`~can.RestartableCyclicTaskABC.start` method on it.
+ :param modifier_callback:
+ Function which should be used to modify each message's data before
+ sending. The callback modifies the :attr:`~can.Message.data` of the
+ message and returns ``None``.
+ :return:
+ A started task instance. Note the task can be stopped (and depending on
+ the backend modified) by calling the task's
+ :meth:`~can.broadcastmanager.CyclicTask.stop` method.
.. note::
- Note the duration before the message stops being sent may not
+ Note the duration before the messages stop being sent may not
be exactly the same as the duration specified by the user. In
general the message will be sent at the given rate until at
least **duration** seconds.
+ .. note::
+
+ For extremely long-running Bus instances with many short-lived
+ tasks the default api with ``store_task==True`` may not be
+ appropriate as the stopped tasks are still taking up memory as they
+ are associated with the Bus instance.
+ """
+ if isinstance(msgs, Message):
+ msgs = [msgs]
+ elif isinstance(msgs, Sequence):
+ # A Sequence does not necessarily provide __bool__ we need to use len()
+ if len(msgs) == 0:
+ raise ValueError("Must be a sequence at least of length 1")
+ else:
+ raise ValueError("Must be either a message or a sequence of messages")
+
+ # Create a backend specific task; will be patched to a _SelfRemovingCyclicTask later
+ task = cast(
+ "_SelfRemovingCyclicTask",
+ self._send_periodic_internal(
+ msgs, period, duration, autostart, modifier_callback
+ ),
+ )
+ # we wrap the task's stop method to also remove it from the Bus's list of tasks
+ periodic_tasks = self._periodic_tasks
+ original_stop_method = task.stop
+
+ def wrapped_stop_method(remove_task: bool = True) -> None:
+ nonlocal task, periodic_tasks, original_stop_method
+ if remove_task:
+ try:
+ periodic_tasks.remove(task)
+ except ValueError:
+ pass # allow the task to be already removed
+ original_stop_method()
+
+ task.stop = wrapped_stop_method # type: ignore[method-assign]
+
+ if store_task:
+ self._periodic_tasks.append(task)
+
+ return task
+
+ def _send_periodic_internal(
+ self,
+ msgs: Sequence[Message] | Message,
+ period: float,
+ duration: float | None = None,
+ autostart: bool = True,
+ modifier_callback: Callable[[Message], None] | None = None,
+ ) -> can.broadcastmanager.CyclicSendTaskABC:
+ """Default implementation of periodic message sending using threading.
+
+ Override this method to enable a more efficient backend specific approach.
+
+ :param msgs:
+ Messages to transmit
+ :param period:
+ Period in seconds between each message
+ :param duration:
+ The duration between sending each message at the given rate. If
+ no duration is provided, the task will continue indefinitely.
+ :param autostart:
+ If True (the default) the sending task will immediately start after creation.
+ Otherwise, the task has to be started by calling the
+ tasks :meth:`~can.RestartableCyclicTaskABC.start` method on it.
+ :return:
+ A started task instance. Note the task can be stopped (and
+ depending on the backend modified) by calling the
+ :meth:`~can.broadcastmanager.CyclicTask.stop` method.
"""
if not hasattr(self, "_lock_send_periodic"):
- # Create a send lock for this bus
- self._lock_send_periodic = threading.Lock()
- return ThreadBasedCyclicSendTask(
- self, self._lock_send_periodic, msg, period, duration)
+ # Create a send lock for this bus, but not for buses which override this method
+ self._lock_send_periodic = ( # pylint: disable=attribute-defined-outside-init
+ threading.Lock()
+ )
+ task = ThreadBasedCyclicSendTask(
+ bus=self,
+ lock=self._lock_send_periodic,
+ messages=msgs,
+ period=period,
+ duration=duration,
+ autostart=autostart,
+ modifier_callback=modifier_callback,
+ )
+ return task
+
+ def stop_all_periodic_tasks(self, remove_tasks: bool = True) -> None:
+ """Stop sending any messages that were started using :meth:`send_periodic`.
- def __iter__(self):
+ .. note::
+ The result is undefined if a single task throws an exception while being stopped.
+
+ :param remove_tasks:
+ Stop tracking the stopped tasks.
+ """
+ if not hasattr(self, "_periodic_tasks"):
+ # avoid AttributeError for partially initialized BusABC instance
+ return
+
+ for task in self._periodic_tasks:
+ # we cannot let `task.stop()` modify `self._periodic_tasks` while we are
+ # iterating over it (#634)
+ task.stop(remove_task=False)
+
+ if remove_tasks:
+ self._periodic_tasks.clear()
+
+ def __iter__(self) -> Iterator[Message]:
"""Allow iteration on messages as they are received.
- >>> for msg in bus:
- ... print(msg)
+ .. code-block:: python
+
+ for msg in bus:
+ print(msg)
:yields:
- :class:`can.Message` msg objects.
+ :class:`Message` msg objects.
"""
while True:
msg = self.recv(timeout=1.0)
@@ -203,7 +373,7 @@ def __iter__(self):
yield msg
@property
- def filters(self):
+ def filters(self) -> can.typechecking.CanFilters | None:
"""
Modify the filters of this bus. See :meth:`~can.BusABC.set_filters`
for details.
@@ -211,10 +381,10 @@ def filters(self):
return self._filters
@filters.setter
- def filters(self, filters):
+ def filters(self, filters: can.typechecking.CanFilters | None) -> None:
self.set_filters(filters)
- def set_filters(self, filters=None):
+ def set_filters(self, filters: can.typechecking.CanFilters | None = None) -> None:
"""Apply filtering to all messages received by this Bus.
All messages that match at least one filter are returned.
@@ -222,13 +392,13 @@ def set_filters(self, filters=None):
messages are matched.
Calling without passing any filters will reset the applied
- filters to `None`.
+ filters to ``None``.
:param filters:
A iterable of dictionaries each containing a "can_id",
- a "can_mask", and an optional "extended" key.
+ a "can_mask", and an optional "extended" key::
- >>> [{"can_id": 0x11, "can_mask": 0x21, "extended": False}]
+ [{"can_id": 0x11, "can_mask": 0x21, "extended": False}]
A filter matches, when
`` & can_mask == can_id & can_mask``.
@@ -237,28 +407,28 @@ def set_filters(self, filters=None):
messages based only on the arbitration ID and mask.
"""
self._filters = filters or None
- self._apply_filters(self._filters)
+ with contextlib.suppress(NotImplementedError):
+ self._apply_filters(self._filters)
- def _apply_filters(self, filters):
+ def _apply_filters(self, filters: can.typechecking.CanFilters | None) -> None:
"""
Hook for applying the filters to the underlying kernel or
hardware if supported/implemented by the interface.
- :param Iterator[dict] filters:
+ :param filters:
See :meth:`~can.BusABC.set_filters` for details.
"""
- pass
+ raise NotImplementedError
- def _matches_filters(self, msg):
+ def _matches_filters(self, msg: Message) -> bool:
"""Checks whether the given message matches at least one of the
current filters. See :meth:`~can.BusABC.set_filters` for details
on how the filters work.
This method should not be overridden.
- :param can.Message msg:
+ :param msg:
the message to check if matching
- :rtype: bool
:return: whether the given message matches at least one filter
"""
@@ -268,13 +438,13 @@ def _matches_filters(self, msg):
for _filter in self._filters:
# check if this filter even applies to the message
- if 'extended' in _filter and \
- _filter['extended'] != msg.is_extended_id:
- continue
+ if "extended" in _filter:
+ if _filter["extended"] != msg.is_extended_id:
+ continue
# then check for the mask and id
- can_id = _filter['can_id']
- can_mask = _filter['can_mask']
+ can_id = _filter["can_id"]
+ can_mask = _filter["can_mask"]
# basically, we compute
# `msg.arbitration_id & can_mask == can_id & can_mask`
@@ -285,43 +455,67 @@ def _matches_filters(self, msg):
# nothing matched
return False
- def flush_tx_buffer(self):
- """Discard every message that may be queued in the output buffer(s).
- """
- pass
+ def flush_tx_buffer(self) -> None:
+ """Discard every message that may be queued in the output buffer(s)."""
+ raise NotImplementedError
- def shutdown(self):
+ def shutdown(self) -> None:
"""
- Called to carry out any interface specific cleanup required
- in shutting down a bus.
+ Called to carry out any interface specific cleanup required in shutting down a bus.
+
+ This method can be safely called multiple times.
"""
- pass
+ if self._is_shutdown:
+ LOG.debug("%s is already shut down", self.__class__)
+ return
- def __enter__(self):
+ self._is_shutdown = True
+ self.stop_all_periodic_tasks()
+
+ def __enter__(self) -> Self:
return self
- def __exit__(self, exc_type, exc_val, exc_tb):
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> None:
self.shutdown()
+ def __del__(self) -> None:
+ if not self._is_shutdown:
+ LOG.warning("%s was not properly shut down", self.__class__.__name__)
+ # We do some best-effort cleanup if the user
+ # forgot to properly close the bus instance
+ with contextlib.suppress(AttributeError):
+ self.shutdown()
+
@property
- def state(self):
+ def state(self) -> BusState:
"""
Return the current state of the hardware
- :return: ACTIVE, PASSIVE or ERROR
- :rtype: NamedTuple
"""
return BusState.ACTIVE
@state.setter
- def state(self, new_state):
+ def state(self, new_state: BusState) -> None:
"""
Set the new state of the hardware
- :param new_state: BusState.ACTIVE, BusState.PASSIVE or BusState.ERROR
"""
raise NotImplementedError("Property is not implemented.")
+ @property
+ def protocol(self) -> CanProtocol:
+ """Return the CAN protocol used by this bus instance.
+
+ This value is set at initialization time and does not change
+ during the lifetime of a bus instance.
+ """
+ return self._can_protocol
+
@staticmethod
- def _detect_available_configs():
+ def _detect_available_configs() -> Sequence[can.typechecking.AutoDetectedConfig]:
"""Detect all configurations/channels that this interface could
currently connect with.
@@ -329,10 +523,20 @@ def _detect_available_configs():
May not to be implemented by every interface on every platform.
- :rtype: Iterator[dict]
:return: an iterable of dicts, each being a configuration suitable
for usage in the interface's bus constructor.
"""
raise NotImplementedError()
- __metaclass__ = ABCMeta
+ def fileno(self) -> int:
+ raise NotImplementedError("fileno is not implemented using current CAN bus")
+
+
+class _SelfRemovingCyclicTask(CyclicSendTaskABC, ABC):
+ """Removes itself from a bus.
+
+ Only needed for typing :meth:`Bus._periodic_tasks`. Do not instantiate.
+ """
+
+ def stop(self, remove_task: bool = True) -> None:
+ raise NotImplementedError()
diff --git a/can/cli.py b/can/cli.py
new file mode 100644
index 000000000..d0ff70126
--- /dev/null
+++ b/can/cli.py
@@ -0,0 +1,320 @@
+import argparse
+import re
+from collections.abc import Sequence
+from typing import Any
+
+import can
+from can.typechecking import CanFilter, TAdditionalCliArgs
+from can.util import _dict2timing, cast_from_string
+
+
+def add_bus_arguments(
+ parser: argparse.ArgumentParser,
+ *,
+ filter_arg: bool = False,
+ prefix: str | None = None,
+ group_title: str | None = None,
+) -> None:
+ """Adds CAN bus configuration options to an argument parser.
+
+ :param parser:
+ The argument parser to which the options will be added.
+ :param filter_arg:
+ Whether to include the filter argument.
+ :param prefix:
+ An optional prefix for the argument names, allowing configuration of multiple buses.
+ :param group_title:
+ The title of the argument group. If not provided, a default title will be generated
+ based on the prefix. For example, "bus arguments (prefix)" if a prefix is specified,
+ or "bus arguments" otherwise.
+ """
+ if group_title is None:
+ group_title = f"bus arguments ({prefix})" if prefix else "bus arguments"
+
+ group = parser.add_argument_group(group_title)
+
+ flags = [f"--{prefix}-channel"] if prefix else ["-c", "--channel"]
+ dest = f"{prefix}_channel" if prefix else "channel"
+ group.add_argument(
+ *flags,
+ dest=dest,
+ default=argparse.SUPPRESS,
+ metavar="CHANNEL",
+ help=r"Most backend interfaces require some sort of channel. For "
+ r"example with the serial interface the channel might be a rfcomm"
+ r' device: "/dev/rfcomm0". With the socketcan interface valid '
+ r'channel examples include: "can0", "vcan0".',
+ )
+
+ flags = [f"--{prefix}-interface"] if prefix else ["-i", "--interface"]
+ dest = f"{prefix}_interface" if prefix else "interface"
+ group.add_argument(
+ *flags,
+ dest=dest,
+ default=argparse.SUPPRESS,
+ choices=sorted(can.VALID_INTERFACES),
+ help="""Specify the backend CAN interface to use. If left blank,
+ fall back to reading from configuration files.""",
+ )
+
+ flags = [f"--{prefix}-bitrate"] if prefix else ["-b", "--bitrate"]
+ dest = f"{prefix}_bitrate" if prefix else "bitrate"
+ group.add_argument(
+ *flags,
+ dest=dest,
+ type=int,
+ default=argparse.SUPPRESS,
+ metavar="BITRATE",
+ help="Bitrate to use for the CAN bus.",
+ )
+
+ flags = [f"--{prefix}-fd"] if prefix else ["--fd"]
+ dest = f"{prefix}_fd" if prefix else "fd"
+ group.add_argument(
+ *flags,
+ dest=dest,
+ default=argparse.SUPPRESS,
+ action="store_true",
+ help="Activate CAN-FD support",
+ )
+
+ flags = [f"--{prefix}-data-bitrate"] if prefix else ["--data-bitrate"]
+ dest = f"{prefix}_data_bitrate" if prefix else "data_bitrate"
+ group.add_argument(
+ *flags,
+ dest=dest,
+ type=int,
+ default=argparse.SUPPRESS,
+ metavar="DATA_BITRATE",
+ help="Bitrate to use for the data phase in case of CAN-FD.",
+ )
+
+ flags = [f"--{prefix}-timing"] if prefix else ["--timing"]
+ dest = f"{prefix}_timing" if prefix else "timing"
+ group.add_argument(
+ *flags,
+ dest=dest,
+ action=_BitTimingAction,
+ nargs=argparse.ONE_OR_MORE,
+ default=argparse.SUPPRESS,
+ metavar="TIMING_ARG",
+ help="Configure bit rate and bit timing. For example, use "
+ "`--timing f_clock=8_000_000 tseg1=5 tseg2=2 sjw=2 brp=2 nof_samples=1` for classical CAN "
+ "or `--timing f_clock=80_000_000 nom_tseg1=119 nom_tseg2=40 nom_sjw=40 nom_brp=1 "
+ "data_tseg1=29 data_tseg2=10 data_sjw=10 data_brp=1` for CAN FD. "
+ "Check the python-can documentation to verify whether your "
+ "CAN interface supports the `timing` argument.",
+ )
+
+ if filter_arg:
+ flags = [f"--{prefix}-filter"] if prefix else ["--filter"]
+ dest = f"{prefix}_can_filters" if prefix else "can_filters"
+ group.add_argument(
+ *flags,
+ dest=dest,
+ nargs=argparse.ONE_OR_MORE,
+ action=_CanFilterAction,
+ default=argparse.SUPPRESS,
+ metavar="{:,~}",
+ help="R|Space separated CAN filters for the given CAN interface:"
+ "\n : (matches when & mask =="
+ " can_id & mask)"
+ "\n ~ (matches when & mask !="
+ " can_id & mask)"
+ "\nFx to show only frames with ID 0x100 to 0x103 and 0x200 to 0x20F:"
+ "\n python -m can.viewer --filter 100:7FC 200:7F0"
+ "\nNote that the ID and mask are always interpreted as hex values",
+ )
+
+ flags = [f"--{prefix}-bus-kwargs"] if prefix else ["--bus-kwargs"]
+ dest = f"{prefix}_bus_kwargs" if prefix else "bus_kwargs"
+ group.add_argument(
+ *flags,
+ dest=dest,
+ action=_BusKwargsAction,
+ nargs=argparse.ONE_OR_MORE,
+ default=argparse.SUPPRESS,
+ metavar="BUS_KWARG",
+ help="Pass keyword arguments down to the instantiation of the bus class. "
+ "For example, `-i vector -c 1 --bus-kwargs app_name=MyCanApp serial=1234` is equivalent "
+ "to opening the bus with `can.Bus('vector', channel=1, app_name='MyCanApp', serial=1234)",
+ )
+
+
+def create_bus_from_namespace(
+ namespace: argparse.Namespace,
+ *,
+ prefix: str | None = None,
+ **kwargs: Any,
+) -> can.BusABC:
+ """Creates and returns a CAN bus instance based on the provided namespace and arguments.
+
+ :param namespace:
+ The namespace containing parsed arguments.
+ :param prefix:
+ An optional prefix for the argument names, enabling support for multiple buses.
+ :param kwargs:
+ Additional keyword arguments to configure the bus.
+ :return:
+ A CAN bus instance.
+ """
+ config: dict[str, Any] = {"single_handle": True, **kwargs}
+
+ for keyword in (
+ "channel",
+ "interface",
+ "bitrate",
+ "fd",
+ "data_bitrate",
+ "can_filters",
+ "timing",
+ "bus_kwargs",
+ ):
+ prefixed_keyword = f"{prefix}_{keyword}" if prefix else keyword
+
+ if prefixed_keyword in namespace:
+ value = getattr(namespace, prefixed_keyword)
+
+ if keyword == "bus_kwargs":
+ config.update(value)
+ else:
+ config[keyword] = value
+
+ try:
+ return can.Bus(**config)
+ except Exception as exc:
+ err_msg = f"Unable to instantiate bus from arguments {vars(namespace)}."
+ raise argparse.ArgumentError(None, err_msg) from exc
+
+
+class _CanFilterAction(argparse.Action):
+ def __call__(
+ self,
+ parser: argparse.ArgumentParser,
+ namespace: argparse.Namespace,
+ values: str | Sequence[Any] | None,
+ option_string: str | None = None,
+ ) -> None:
+ if not isinstance(values, list):
+ raise argparse.ArgumentError(self, "Invalid filter argument")
+
+ print(f"Adding filter(s): {values}")
+ can_filters: list[CanFilter] = []
+
+ for filt in values:
+ if ":" in filt:
+ parts = filt.split(":")
+ can_id = int(parts[0], base=16)
+ can_mask = int(parts[1], base=16)
+ elif "~" in filt:
+ parts = filt.split("~")
+ can_id = int(parts[0], base=16) | 0x20000000 # CAN_INV_FILTER
+ can_mask = int(parts[1], base=16) & 0x20000000 # socket.CAN_ERR_FLAG
+ else:
+ raise argparse.ArgumentError(self, "Invalid filter argument")
+ can_filters.append({"can_id": can_id, "can_mask": can_mask})
+
+ setattr(namespace, self.dest, can_filters)
+
+
+class _BitTimingAction(argparse.Action):
+ def __call__(
+ self,
+ parser: argparse.ArgumentParser,
+ namespace: argparse.Namespace,
+ values: str | Sequence[Any] | None,
+ option_string: str | None = None,
+ ) -> None:
+ if not isinstance(values, list):
+ raise argparse.ArgumentError(self, "Invalid --timing argument")
+
+ timing_dict: dict[str, int] = {}
+ for arg in values:
+ try:
+ key, value_string = arg.split("=")
+ value = int(value_string)
+ timing_dict[key] = value
+ except ValueError:
+ raise argparse.ArgumentError(
+ self, f"Invalid timing argument: {arg}"
+ ) from None
+
+ if not (timing := _dict2timing(timing_dict)):
+ err_msg = "Invalid --timing argument. Incomplete parameters."
+ raise argparse.ArgumentError(self, err_msg)
+
+ setattr(namespace, self.dest, timing)
+ print(timing)
+
+
+class _BusKwargsAction(argparse.Action):
+ def __call__(
+ self,
+ parser: argparse.ArgumentParser,
+ namespace: argparse.Namespace,
+ values: str | Sequence[Any] | None,
+ option_string: str | None = None,
+ ) -> None:
+ if not isinstance(values, list):
+ raise argparse.ArgumentError(self, "Invalid --bus-kwargs argument")
+
+ bus_kwargs: dict[str, str | int | float | bool] = {}
+
+ for arg in values:
+ try:
+ match = re.match(
+ r"^(?P[_a-zA-Z][_a-zA-Z0-9]*)=(?P\S*?)$",
+ arg,
+ )
+ if not match:
+ raise ValueError
+ key = match["name"].replace("-", "_")
+ string_val = match["value"]
+ bus_kwargs[key] = cast_from_string(string_val)
+ except ValueError:
+ raise argparse.ArgumentError(
+ self,
+ f"Unable to parse bus keyword argument '{arg}'",
+ ) from None
+
+ setattr(namespace, self.dest, bus_kwargs)
+
+
+def _add_extra_args(
+ parser: argparse.ArgumentParser | argparse._ArgumentGroup,
+) -> None:
+ parser.add_argument(
+ "extra_args",
+ nargs=argparse.REMAINDER,
+ help="The remaining arguments will be used for logger/player initialisation. "
+ "For example, `can_logger -i virtual -c test -f logfile.blf --compression-level=9` "
+ "passes the keyword argument `compression_level=9` to the BlfWriter.",
+ )
+
+
+def _parse_additional_config(unknown_args: Sequence[str]) -> TAdditionalCliArgs:
+ for arg in unknown_args:
+ if not re.match(r"^--[a-zA-Z][a-zA-Z0-9\-]*=\S*?$", arg):
+ raise ValueError(f"Parsing argument {arg} failed")
+
+ def _split_arg(_arg: str) -> tuple[str, str]:
+ left, right = _arg.split("=", 1)
+ return left.lstrip("-").replace("-", "_"), right
+
+ args: dict[str, str | int | float | bool] = {}
+ for key, string_val in map(_split_arg, unknown_args):
+ args[key] = cast_from_string(string_val)
+ return args
+
+
+def _set_logging_level_from_namespace(namespace: argparse.Namespace) -> None:
+ if "verbosity" in namespace:
+ logging_level_names = [
+ "critical",
+ "error",
+ "warning",
+ "info",
+ "debug",
+ "subdebug",
+ ]
+ can.set_logging_level(logging_level_names[min(5, namespace.verbosity)])
diff --git a/can/ctypesutil.py b/can/ctypesutil.py
index 6dc372268..0798de910 100644
--- a/can/ctypesutil.py
+++ b/can/ctypesutil.py
@@ -1,100 +1,92 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-"""
-This module contains common `ctypes` utils.
-"""
-
-import binascii
-import ctypes
-import logging
-import sys
-
-log = logging.getLogger('can.ctypesutil')
-
-__all__ = ['CLibrary', 'HANDLE', 'PHANDLE', 'HRESULT']
-
-try:
- _LibBase = ctypes.WinDLL
-except AttributeError:
- _LibBase = ctypes.CDLL
-
-
-class LibraryMixin:
- def map_symbol(self, func_name, restype=None, argtypes=(), errcheck=None):
- """
- Map and return a symbol (function) from a C library. A reference to the
- mapped symbol is also held in the instance
-
- :param str func_name:
- symbol_name
- :param ctypes.c_* restype:
- function result type (i.e. ctypes.c_ulong...), defaults to void
- :param tuple(ctypes.c_* ... ) argtypes:
- argument types, defaults to no args
- :param callable errcheck:
- optional error checking function, see ctypes docs for _FuncPtr
- """
- if (argtypes):
- prototype = self.function_type(restype, *argtypes)
- else:
- prototype = self.function_type(restype)
- try:
- symbol = prototype((func_name, self))
- except AttributeError:
- raise ImportError("Could not map function '{}' from library {}".format(func_name, self._name))
-
- setattr(symbol, "_name", func_name)
- log.debug('Wrapped function "{}", result type: {}, error_check {}'.format(func_name, type(restype), errcheck))
-
- if (errcheck):
- symbol.errcheck = errcheck
-
- setattr(self, func_name, symbol)
- return symbol
-
-
-class CLibrary_Win32(_LibBase, LibraryMixin):
- " Basic ctypes.WinDLL derived class + LibraryMixin "
-
- def __init__(self, library_or_path):
- if (isinstance(library_or_path, str)):
- super(CLibrary_Win32, self).__init__(library_or_path)
- else:
- super(CLibrary_Win32, self).__init__(library_or_path._name, library_or_path._handle)
-
- @property
- def function_type(self):
- return ctypes.WINFUNCTYPE
-
-
-class CLibrary_Unix(ctypes.CDLL, LibraryMixin):
- " Basic ctypes.CDLL derived class + LibraryMixin "
-
- def __init__(self, library_or_path):
- if (isinstance(library_or_path, str)):
- super(CLibrary_Unix, self).__init__(library_or_path)
- else:
- super(CLibrary_Unix, self).__init__(library_or_path._name, library_or_path._handle)
-
- @property
- def function_type(self):
- return ctypes.CFUNCTYPE
-
-
-if sys.platform == "win32":
- CLibrary = CLibrary_Win32
- HRESULT = ctypes.HRESULT
-else:
- CLibrary = CLibrary_Unix
- if sys.platform == "cygwin":
- # Define HRESULT for cygwin
- class HRESULT(ctypes.c_long):
- pass
-
-
-# Common win32 definitions
-class HANDLE(ctypes.c_void_p):
- pass
-
-PHANDLE = ctypes.POINTER(HANDLE)
+"""
+This module contains common `ctypes` utils.
+"""
+
+import ctypes
+import logging
+import sys
+from collections.abc import Callable
+from typing import Any
+
+log = logging.getLogger("can.ctypesutil")
+
+__all__ = ["HANDLE", "HRESULT", "PHANDLE", "CLibrary"]
+
+if sys.platform == "win32":
+ _LibBase = ctypes.WinDLL
+ _FUNCTION_TYPE = ctypes.WINFUNCTYPE
+else:
+ _LibBase = ctypes.CDLL
+ _FUNCTION_TYPE = ctypes.CFUNCTYPE
+
+
+class CLibrary(_LibBase):
+ def __init__(self, library_or_path: str | ctypes.CDLL) -> None:
+ self.func_name: Any
+
+ if isinstance(library_or_path, str):
+ super().__init__(library_or_path)
+ else:
+ super().__init__(library_or_path._name, library_or_path._handle)
+
+ def map_symbol(
+ self,
+ func_name: str,
+ restype: Any = None,
+ argtypes: tuple[Any, ...] = (),
+ errcheck: Callable[..., Any] | None = None,
+ ) -> Any:
+ """
+ Map and return a symbol (function) from a C library. A reference to the
+ mapped symbol is also held in the instance
+
+ :param func_name:
+ symbol_name
+ :param ctypes.c_* restype:
+ function result type (i.e. ctypes.c_ulong...), defaults to void
+ :param tuple(ctypes.c_* ... ) argtypes:
+ argument types, defaults to no args
+ :param callable errcheck:
+ optional error checking function, see ctypes docs for _FuncPtr
+ """
+ if argtypes:
+ prototype = _FUNCTION_TYPE(restype, *argtypes)
+ else:
+ prototype = _FUNCTION_TYPE(restype)
+ try:
+ func = prototype((func_name, self))
+ except AttributeError:
+ raise ImportError(
+ f'Could not map function "{func_name}" from library {self._name}'
+ ) from None
+
+ func._name = func_name # type: ignore[attr-defined] # pylint: disable=protected-access
+ log.debug(
+ 'Wrapped function "%s", result type: %s, error_check %s',
+ func_name,
+ type(restype),
+ errcheck,
+ )
+
+ if errcheck is not None:
+ func.errcheck = errcheck
+
+ setattr(self, func_name, func)
+ return func
+
+
+if sys.platform == "win32":
+ HRESULT = ctypes.HRESULT
+
+elif sys.platform == "cygwin":
+
+ class HRESULT(ctypes.c_long):
+ pass
+
+
+# Common win32 definitions
+class HANDLE(ctypes.c_void_p):
+ pass
+
+
+PHANDLE = ctypes.POINTER(HANDLE)
diff --git a/can/exceptions.py b/can/exceptions.py
new file mode 100644
index 000000000..696701399
--- /dev/null
+++ b/can/exceptions.py
@@ -0,0 +1,120 @@
+"""
+There are several specific :class:`Exception` classes to allow user
+code to react to specific scenarios related to CAN buses::
+
+ Exception (Python standard library)
+ +-- ...
+ +-- CanError (python-can)
+ +-- CanInterfaceNotImplementedError
+ +-- CanInitializationError
+ +-- CanOperationError
+ +-- CanTimeoutError
+
+Keep in mind that some functions and methods may raise different exceptions.
+For example, validating typical arguments and parameters might result in a
+:class:`ValueError`. This should always be documented for the function at hand.
+"""
+
+from collections.abc import Generator
+from contextlib import contextmanager
+
+
+class CanError(Exception):
+ """Base class for all CAN related exceptions.
+
+ If specified, the error code is automatically appended to the message:
+
+ .. testsetup:: canerror
+
+ from can import CanError, CanOperationError
+
+ .. doctest:: canerror
+
+ >>> # With an error code (it also works with a specific error):
+ >>> error = CanOperationError(message="Failed to do the thing", error_code=42)
+ >>> str(error)
+ 'Failed to do the thing [Error Code 42]'
+ >>>
+ >>> # Missing the error code:
+ >>> plain_error = CanError(message="Something went wrong ...")
+ >>> str(plain_error)
+ 'Something went wrong ...'
+
+ :param error_code:
+ An optional error code to narrow down the cause of the fault
+
+ :arg error_code:
+ An optional error code to narrow down the cause of the fault
+ """
+
+ def __init__(
+ self,
+ message: str = "",
+ error_code: int | None = None,
+ ) -> None:
+ self.error_code = error_code
+ super().__init__(
+ message if error_code is None else f"{message} [Error Code {error_code}]"
+ )
+
+
+class CanInterfaceNotImplementedError(CanError, NotImplementedError):
+ """Indicates that the interface is not supported on the current platform.
+
+ Example scenarios:
+ - No interface with that name exists
+ - The interface is unsupported on the current operating system or interpreter
+ - The driver could not be found or has the wrong version
+ """
+
+
+class CanInitializationError(CanError):
+ """Indicates an error the occurred while initializing a :class:`can.BusABC`.
+
+ If initialization fails due to a driver or platform missing/being unsupported,
+ a :exc:`~can.exceptions.CanInterfaceNotImplementedError` is raised instead.
+ If initialization fails due to a value being out of range, a :class:`ValueError`
+ is raised.
+
+ Example scenarios:
+ - Try to open a non-existent device and/or channel
+ - Try to use an invalid setting, which is ok by value, but not ok for the interface
+ - The device or other resources are already used
+ """
+
+
+class CanOperationError(CanError):
+ """Indicates an error while in operation.
+
+ Example scenarios:
+ - A call to a library function results in an unexpected return value
+ - An invalid message was received
+ - The driver rejected a message that was meant to be sent
+ - Cyclic redundancy check (CRC) failed
+ - A message remained unacknowledged
+ - A buffer is full
+ """
+
+
+class CanTimeoutError(CanError, TimeoutError):
+ """Indicates the timeout of an operation.
+
+ Example scenarios:
+ - Some message could not be sent after the timeout elapsed
+ - No message was read within the given time
+ """
+
+
+@contextmanager
+def error_check(
+ error_message: str | None = None,
+ exception_type: type[CanError] = CanOperationError,
+) -> Generator[None, None, None]:
+ """Catches any exceptions and turns them into the new type while preserving the stack trace."""
+ try:
+ yield
+ except Exception as error: # pylint: disable=broad-except
+ if error_message is None:
+ raise exception_type(str(error)) from error
+ else:
+ raise exception_type(error_message) from error
diff --git a/can/interface.py b/can/interface.py
index 134c6336a..efde5b214 100644
--- a/can/interface.py
+++ b/can/interface.py
@@ -1,185 +1,233 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
-This module contains the base implementation of `can.Bus` as well
-as a list of all avalibale backends and some implemented
+This module contains the base implementation of :class:`can.BusABC` as well
+as a list of all available backends and some implemented
CyclicSendTasks.
"""
-from __future__ import absolute_import, print_function
-
-import sys
+import concurrent.futures.thread
import importlib
import logging
-import re
+from collections.abc import Callable, Iterable, Sequence
+from typing import Any, cast
-import can
+from . import util
from .bus import BusABC
-from .broadcastmanager import CyclicSendTaskABC, MultiRateCyclicSendTaskABC
-from .util import load_config
+from .exceptions import CanInterfaceNotImplementedError
from .interfaces import BACKENDS
+from .typechecking import AutoDetectedConfig, Channel
-if 'linux' in sys.platform:
- # Deprecated and undocumented access to SocketCAN cyclic tasks
- # Will be removed in version 3.0
- from can.interfaces.socketcan import CyclicSendTask, MultiRateCyclicSendTask
-
-# Required by "detect_available_configs" for argument interpretation
-if sys.version_info.major > 2:
- basestring = str
+log = logging.getLogger("can.interface")
+log_autodetect = log.getChild("detect_available_configs")
-log = logging.getLogger('can.interface')
-log_autodetect = log.getChild('detect_available_configs')
-
-def _get_class_for_interface(interface):
+def _get_class_for_interface(interface: str) -> type[BusABC]:
"""
Returns the main bus class for the given interface.
:raises:
NotImplementedError if the interface is not known
- :raises:
- ImportError if there was a problem while importing the
- interface or the bus class within that
+ :raises CanInterfaceNotImplementedError:
+ if there was a problem while importing the interface or the bus class within that
"""
# Find the correct backend
try:
module_name, class_name = BACKENDS[interface]
except KeyError:
- raise NotImplementedError("CAN interface '{}' not supported".format(interface))
+ raise NotImplementedError(
+ f"CAN interface '{interface}' not supported"
+ ) from None
# Import the correct interface module
try:
module = importlib.import_module(module_name)
except Exception as e:
- raise ImportError(
- "Cannot import module {} for CAN interface '{}': {}".format(module_name, interface, e)
- )
+ raise CanInterfaceNotImplementedError(
+ f"Cannot import module {module_name} for CAN interface '{interface}': {e}"
+ ) from None
# Get the correct class
try:
bus_class = getattr(module, class_name)
except Exception as e:
- raise ImportError(
- "Cannot import class {} from module {} for CAN interface '{}': {}"
- .format(class_name, module_name, interface, e)
- )
+ raise CanInterfaceNotImplementedError(
+ f"Cannot import class {class_name} from module {module_name} for CAN interface "
+ f"'{interface}': {e}"
+ ) from None
+
+ return cast("type[BusABC]", bus_class)
+
+
+@util.deprecated_args_alias(
+ deprecation_start="4.2.0",
+ deprecation_end="5.0.0",
+ bustype="interface",
+ context="config_context",
+)
+def Bus( # noqa: N802
+ channel: Channel | None = None,
+ interface: str | None = None,
+ config_context: str | None = None,
+ ignore_config: bool = False,
+ **kwargs: Any,
+) -> BusABC:
+ """Create a new bus instance with configuration loading.
- return bus_class
+ Instantiates a CAN Bus of the given ``interface``, falls back to reading a
+ configuration file from default locations.
+ .. note::
+ Please note that while the arguments provided to this class take precedence
+ over any existing values from configuration, it is possible that other parameters
+ from the configuration may be added to the bus instantiation.
+ This could potentially have unintended consequences. To prevent this,
+ you may use the *ignore_config* parameter to ignore any existing configurations.
-class Bus(BusABC):
- """Bus wrapper with configuration loading.
+ :param channel:
+ Channel identification. Expected type is backend dependent.
+ Set to ``None`` to let it be resolved automatically from the default
+ :ref:`configuration`.
- Instantiates a CAN Bus of the given ``interface``, falls back to reading a
- configuration file from default locations.
- """
+ :param interface:
+ See :ref:`interface names` for a list of supported interfaces.
+ Set to ``None`` to let it be resolved automatically from the default
+ :ref:`configuration`.
- @staticmethod
- def __new__(cls, channel=None, *args, **config):
- """
- Takes the same arguments as :class:`can.BusABC.__init__`.
- Some might have a special meaning, see below.
+ :param config_context:
+ Extra 'context', that is passed to config sources.
+ This can be used to select a section other than 'default' in the configuration file.
- :param channel:
- Set to ``None`` to let it be reloved automatically from the default
- configuration. That might fail, see below.
+ :param ignore_config:
+ If ``True``, only the given arguments will be used for the bus instantiation. Existing
+ configuration sources will be ignored.
- Expected type is backend dependent.
+ :param kwargs:
+ ``interface`` specific keyword arguments.
- :param dict config:
- Should contain an ``interface`` key with a valid interface name. If not,
- it is completed using :meth:`can.util.load_config`.
+ :raises ~can.exceptions.CanInterfaceNotImplementedError:
+ if the ``interface`` isn't recognized or cannot be loaded
- :raises: NotImplementedError
- if the ``interface`` isn't recognized
+ :raises ~can.exceptions.CanInitializationError:
+ if the bus cannot be instantiated
- :raises: ValueError
- if the ``channel`` could not be determined
- """
+ :raises ValueError:
+ if the ``channel`` could not be determined
+ """
+
+ # figure out the rest of the configuration; this might raise an error
+ if interface is not None:
+ kwargs["interface"] = interface
+ if channel is not None:
+ kwargs["channel"] = channel
- # figure out the rest of the configuration; this might raise an error
- if channel is not None:
- config['channel'] = channel
- if 'context' in config:
- context = config['context']
- del config['context']
- else:
- context = None
- config = load_config(config=config, context=context)
+ if not ignore_config:
+ kwargs = util.load_config(config=kwargs, context=config_context)
- # resolve the bus class to use for that interface
- cls = _get_class_for_interface(config['interface'])
+ # resolve the bus class to use for that interface
+ cls = _get_class_for_interface(kwargs["interface"])
- # remove the 'interface' key so it doesn't get passed to the backend
- del config['interface']
+ # remove the "interface" key, so it doesn't get passed to the backend
+ del kwargs["interface"]
- # make sure the bus can handle this config format
- if 'channel' not in config:
- raise ValueError("'channel' argument missing")
- else:
- channel = config['channel']
- del config['channel']
+ # make sure the bus can handle this config format
+ channel = kwargs.pop("channel", channel)
+ if channel is None:
+ # Use the default channel for the backend
+ bus = cls(**kwargs)
+ else:
+ bus = cls(channel, **kwargs)
- if channel is None:
- # Use the default channel for the backend
- return cls(*args, **config)
- else:
- return cls(channel, *args, **config)
+ return bus
-def detect_available_configs(interfaces=None):
+def detect_available_configs(
+ interfaces: None | str | Iterable[str] = None,
+ timeout: float = 5.0,
+) -> Sequence[AutoDetectedConfig]:
"""Detect all configurations/channels that the interfaces could
currently connect with.
- This might be quite time consuming.
+ This might be quite time-consuming.
Automated configuration detection may not be implemented by
every interface on every platform. This method will not raise
- an error in that case, but with rather return an empty list
+ an error in that case, but will rather return an empty list
for that interface.
:param interfaces: either
- the name of an interface to be searched in as a string,
- an iterable of interface names to search in, or
- `None` to search in all known interfaces.
+ :param timeout: maximum number of seconds to wait for all interface
+ detection tasks to complete. If exceeded, any pending tasks
+ will be cancelled, a warning will be logged, and the method
+ will return results gathered so far.
:rtype: list[dict]
:return: an iterable of dicts, each suitable for usage in
- the constructor of :class:`can.interface.Bus`.
+ the constructor of :class:`can.BusABC`. Interfaces that
+ timed out will be logged as warnings and excluded.
"""
- # Figure out where to search
+ # Determine which interfaces to search
if interfaces is None:
- # use an iterator over the keys so we do not have to copy it
- interfaces = BACKENDS.keys()
- elif isinstance(interfaces, basestring):
- interfaces = [interfaces, ]
- # else it is supposed to be an iterable of strings
-
- result = []
- for interface in interfaces:
-
- try:
- bus_class = _get_class_for_interface(interface)
- except ImportError:
- log_autodetect.debug('interface "%s" can not be loaded for detection of available configurations', interface)
- continue
-
- # get available channels
+ interfaces = BACKENDS
+ elif isinstance(interfaces, str):
+ interfaces = (interfaces,)
+ # otherwise assume iterable of strings
+
+ # Collect detection callbacks
+ callbacks: dict[str, Callable[[], Sequence[AutoDetectedConfig]]] = {}
+ for interface_keyword in interfaces:
try:
- available = list(bus_class._detect_available_configs())
- except NotImplementedError:
- log_autodetect.debug('interface "%s" does not support detection of available configurations', interface)
- else:
- log_autodetect.debug('interface "%s" detected %i available configurations', interface, len(available))
-
- # add the interface name to the configs if it is not already present
- for config in available:
- if 'interface' not in config:
- config['interface'] = interface
-
- # append to result
- result += available
-
+ bus_class = _get_class_for_interface(interface_keyword)
+ callbacks[interface_keyword] = (
+ bus_class._detect_available_configs # pylint: disable=protected-access
+ )
+ except CanInterfaceNotImplementedError:
+ log_autodetect.debug(
+ 'interface "%s" cannot be loaded for detection of available configurations',
+ interface_keyword,
+ )
+
+ result: list[AutoDetectedConfig] = []
+
+ # Use manual executor to allow shutdown without waiting
+ executor = concurrent.futures.ThreadPoolExecutor()
+ try:
+ futures_to_keyword = {
+ executor.submit(func): kw for kw, func in callbacks.items()
+ }
+ done, not_done = concurrent.futures.wait(
+ futures_to_keyword,
+ timeout=timeout,
+ return_when=concurrent.futures.ALL_COMPLETED,
+ )
+ # Log timed-out tasks
+ if not_done:
+ log_autodetect.warning(
+ "Timeout (%.2fs) reached for interfaces: %s",
+ timeout,
+ ", ".join(sorted(futures_to_keyword[fut] for fut in not_done)),
+ )
+ # Process completed futures
+ for future in done:
+ keyword = futures_to_keyword[future]
+ try:
+ available = future.result()
+ except NotImplementedError:
+ log_autodetect.debug(
+ 'interface "%s" does not support detection of available configurations',
+ keyword,
+ )
+ else:
+ log_autodetect.debug(
+ 'interface "%s" detected %i available configurations',
+ keyword,
+ len(available),
+ )
+ for config in available:
+ config.setdefault("interface", keyword)
+ result.extend(available)
+ finally:
+ # shutdown immediately, do not wait for pending threads
+ executor.shutdown(wait=False, cancel_futures=True)
return result
diff --git a/can/interfaces/__init__.py b/can/interfaces/__init__.py
index e1dfc4bf6..1b401639a 100644
--- a/can/interfaces/__init__.py
+++ b/can/interfaces/__init__.py
@@ -1,41 +1,70 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Interfaces contain low level implementations that interact with CAN hardware.
"""
-import logging
-
-from pkg_resources import iter_entry_points
+from can._entry_points import read_entry_points
-logger = logging.getLogger(__name__)
+__all__ = [
+ "BACKENDS",
+ "VALID_INTERFACES",
+ "canalystii",
+ "cantact",
+ "etas",
+ "gs_usb",
+ "ics_neovi",
+ "iscan",
+ "ixxat",
+ "kvaser",
+ "neousys",
+ "nican",
+ "nixnet",
+ "pcan",
+ "robotell",
+ "seeedstudio",
+ "serial",
+ "slcan",
+ "socketcan",
+ "socketcand",
+ "systec",
+ "udp_multicast",
+ "usb2can",
+ "vector",
+ "virtual",
+]
# interface_name => (module, classname)
-BACKENDS = {
- 'kvaser': ('can.interfaces.kvaser', 'KvaserBus'),
- 'socketcan': ('can.interfaces.socketcan', 'SocketcanBus'),
- 'serial': ('can.interfaces.serial.serial_can','SerialBus'),
- 'pcan': ('can.interfaces.pcan', 'PcanBus'),
- 'usb2can': ('can.interfaces.usb2can', 'Usb2canBus'),
- 'ixxat': ('can.interfaces.ixxat', 'IXXATBus'),
- 'nican': ('can.interfaces.nican', 'NicanBus'),
- 'iscan': ('can.interfaces.iscan', 'IscanBus'),
- 'virtual': ('can.interfaces.virtual', 'VirtualBus'),
- 'neovi': ('can.interfaces.ics_neovi', 'NeoViBus'),
- 'vector': ('can.interfaces.vector', 'VectorBus'),
- 'slcan': ('can.interfaces.slcan', 'slcanBus')
+BACKENDS: dict[str, tuple[str, str]] = {
+ "kvaser": ("can.interfaces.kvaser", "KvaserBus"),
+ "socketcan": ("can.interfaces.socketcan", "SocketcanBus"),
+ "serial": ("can.interfaces.serial.serial_can", "SerialBus"),
+ "pcan": ("can.interfaces.pcan", "PcanBus"),
+ "usb2can": ("can.interfaces.usb2can", "Usb2canBus"),
+ "ixxat": ("can.interfaces.ixxat", "IXXATBus"),
+ "nican": ("can.interfaces.nican", "NicanBus"),
+ "iscan": ("can.interfaces.iscan", "IscanBus"),
+ "virtual": ("can.interfaces.virtual", "VirtualBus"),
+ "udp_multicast": ("can.interfaces.udp_multicast", "UdpMulticastBus"),
+ "neovi": ("can.interfaces.ics_neovi", "NeoViBus"),
+ "vector": ("can.interfaces.vector", "VectorBus"),
+ "slcan": ("can.interfaces.slcan", "slcanBus"),
+ "robotell": ("can.interfaces.robotell", "robotellBus"),
+ "canalystii": ("can.interfaces.canalystii", "CANalystIIBus"),
+ "systec": ("can.interfaces.systec", "UcanBus"),
+ "seeedstudio": ("can.interfaces.seeedstudio", "SeeedBus"),
+ "cantact": ("can.interfaces.cantact", "CantactBus"),
+ "gs_usb": ("can.interfaces.gs_usb", "GsUsbBus"),
+ "nixnet": ("can.interfaces.nixnet", "NiXNETcanBus"),
+ "neousys": ("can.interfaces.neousys", "NeousysBus"),
+ "etas": ("can.interfaces.etas", "EtasBus"),
+ "socketcand": ("can.interfaces.socketcand", "SocketCanDaemonBus"),
}
-BACKENDS.update({
- interface.name: (interface.module_name, interface.attrs[0])
- for interface in iter_entry_points('can.interface')
-})
-# Old entry point name. May be removed in 3.0.
-for interface in iter_entry_points('python_can.interface'):
- BACKENDS[interface.name] = (interface.module_name, interface.attrs[0])
- logger.warning('%s is using the deprecated python_can.interface entry point. '
- 'Please change to can.interface instead.', interface.name)
+BACKENDS.update(
+ {
+ interface.key: (interface.module_name, interface.class_name)
+ for interface in read_entry_points(group="can.interface")
+ }
+)
-VALID_INTERFACES = frozenset(list(BACKENDS.keys()) + ['socketcan_native', 'socketcan_ctypes'])
+VALID_INTERFACES = frozenset(sorted(BACKENDS.keys()))
diff --git a/can/interfaces/canalystii.py b/can/interfaces/canalystii.py
new file mode 100644
index 000000000..e2bf7555e
--- /dev/null
+++ b/can/interfaces/canalystii.py
@@ -0,0 +1,218 @@
+import logging
+import time
+from collections import deque
+from collections.abc import Sequence
+from ctypes import c_ubyte
+from typing import Any
+
+import canalystii as driver
+
+from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message
+from can.exceptions import CanTimeoutError
+from can.typechecking import CanFilters
+from can.util import check_or_adjust_timing_clock, deprecated_args_alias
+
+logger = logging.getLogger(__name__)
+
+
+class CANalystIIBus(BusABC):
+ @deprecated_args_alias(
+ deprecation_start="4.2.0", deprecation_end="5.0.0", bit_timing="timing"
+ )
+ def __init__(
+ self,
+ channel: int | Sequence[int] | str = (0, 1),
+ device: int = 0,
+ bitrate: int | None = None,
+ timing: BitTiming | BitTimingFd | None = None,
+ can_filters: CanFilters | None = None,
+ rx_queue_size: int | None = None,
+ **kwargs: dict[str, Any],
+ ):
+ """
+
+ :param channel:
+ Optional channel number, list/tuple of multiple channels, or comma
+ separated string of channels. Default is to configure both
+ channels.
+ :param device:
+ Optional USB device number. Default is 0 (first device found).
+ :param bitrate:
+ CAN bitrate in bits/second. Required unless the bit_timing argument is set.
+ :param timing:
+ Optional :class:`~can.BitTiming` instance to use for custom bit timing setting.
+ If this argument is set then it overrides the bitrate argument. The
+ `f_clock` value of the timing instance must be set to 8_000_000 (8MHz)
+ for standard CAN.
+ CAN FD and the :class:`~can.BitTimingFd` class are not supported.
+ :param can_filters:
+ Optional filters for received CAN messages.
+ :param rx_queue_size:
+ If set, software received message queue can only grow to this many
+ messages (for all channels) before older messages are dropped
+ """
+ if not (bitrate or timing):
+ raise ValueError("Either bitrate or timing argument is required")
+
+ # Do this after the error handling
+ super().__init__(
+ channel=channel,
+ can_filters=can_filters,
+ **kwargs,
+ )
+ if isinstance(channel, str):
+ # Assume comma separated string of channels
+ self.channels = [int(ch.strip()) for ch in channel.split(",")]
+ elif isinstance(channel, int):
+ self.channels = [channel]
+ else: # Sequence[int]
+ self.channels = list(channel)
+
+ self.channel_info = f"CANalyst-II: device {device}, channels {self.channels}"
+ self.rx_queue: deque[tuple[int, driver.Message]] = deque(maxlen=rx_queue_size)
+ self.device = driver.CanalystDevice(device_index=device)
+ self._can_protocol = CanProtocol.CAN_20
+
+ for single_channel in self.channels:
+ if isinstance(timing, BitTiming):
+ timing = check_or_adjust_timing_clock(timing, valid_clocks=[8_000_000])
+ self.device.init(
+ single_channel, timing0=timing.btr0, timing1=timing.btr1
+ )
+ elif isinstance(timing, BitTimingFd):
+ raise NotImplementedError(
+ f"CAN FD is not supported by {self.__class__.__name__}."
+ )
+ else:
+ self.device.init(single_channel, bitrate=bitrate)
+
+ # Delay to use between each poll for new messages
+ #
+ # The timeout is deliberately kept low to avoid the possibility of
+ # a hardware buffer overflow. This value was determined
+ # experimentally, but the ideal value will depend on the specific
+ # system.
+ RX_POLL_DELAY = 0.020
+
+ def send(self, msg: Message, timeout: float | None = None) -> None:
+ """Send a CAN message to the bus
+
+ :param msg: message to send
+ :param timeout: timeout (in seconds) to wait for the TX queue to clear.
+ If set to ``None`` (default) the function returns immediately.
+
+ Note: Due to limitations in the device firmware and protocol, the
+ timeout will not trigger if there are problems with CAN arbitration,
+ but only if the device is overloaded with a backlog of too many
+ messages to send.
+ """
+ raw_message = driver.Message(
+ msg.arbitration_id,
+ 0, # timestamp
+ 1, # time_flag
+ 0, # send_type
+ msg.is_remote_frame,
+ msg.is_extended_id,
+ msg.dlc,
+ (c_ubyte * 8)(*msg.data),
+ )
+
+ if msg.channel is not None:
+ channel = msg.channel
+ elif len(self.channels) == 1:
+ channel = self.channels[0]
+ else:
+ raise ValueError(
+ "Message channel must be set when using multiple channels."
+ )
+
+ send_result = self.device.send(channel, [raw_message], timeout)
+ if timeout is not None and not send_result:
+ raise CanTimeoutError(f"Send timed out after {timeout} seconds")
+
+ def _recv_from_queue(self) -> tuple[Message, bool]:
+ """Return a message from the internal receive queue"""
+ channel, raw_msg = self.rx_queue.popleft()
+
+ # Protocol timestamps are in units of 100us, convert to seconds as
+ # float
+ timestamp = raw_msg.timestamp * 100e-6
+
+ return (
+ Message(
+ channel=channel,
+ timestamp=timestamp,
+ arbitration_id=raw_msg.can_id,
+ is_extended_id=raw_msg.extended,
+ is_remote_frame=raw_msg.remote,
+ dlc=raw_msg.data_len,
+ data=bytes(raw_msg.data),
+ ),
+ False,
+ )
+
+ def poll_received_messages(self) -> None:
+ """Poll new messages from the device into the rx queue but don't
+ return any message to the caller
+
+ Calling this function isn't necessary as polling the device is done
+ automatically when calling recv(). This function is for the situation
+ where an application needs to empty the hardware receive buffer without
+ consuming any message.
+ """
+ for channel in self.channels:
+ self.rx_queue.extend(
+ (channel, raw_msg) for raw_msg in self.device.receive(channel)
+ )
+
+ def _recv_internal(
+ self, timeout: float | None = None
+ ) -> tuple[Message | None, bool]:
+ """
+
+ :param timeout: float in seconds
+ :return:
+ """
+
+ if self.rx_queue:
+ return self._recv_from_queue()
+
+ deadline = None
+ while deadline is None or time.time() < deadline:
+ if deadline is None and timeout is not None:
+ deadline = time.time() + timeout
+
+ self.poll_received_messages()
+
+ if self.rx_queue:
+ return self._recv_from_queue()
+
+ # If blocking on a timeout, add a sleep before we loop again
+ # to reduce CPU usage.
+ if deadline is None or deadline - time.time() > 0.050:
+ time.sleep(self.RX_POLL_DELAY)
+
+ return (None, False)
+
+ def flush_tx_buffer(self, channel: int | None = None) -> None:
+ """Flush the TX buffer of the device.
+
+ :param channel:
+ Optional channel number to flush. If set to None, all initialized
+ channels are flushed.
+
+ Note that because of protocol limitations this function returning
+ doesn't mean that messages have been sent, it may also mean they
+ failed to send.
+ """
+ if channel:
+ self.device.flush_tx_buffer(channel, float("infinity"))
+ else:
+ for ch in self.channels:
+ self.device.flush_tx_buffer(ch, float("infinity"))
+
+ def shutdown(self) -> None:
+ super().shutdown()
+ for channel in self.channels:
+ self.device.stop(channel)
+ self.device = None
diff --git a/can/interfaces/cantact.py b/can/interfaces/cantact.py
new file mode 100644
index 000000000..ee01fbf94
--- /dev/null
+++ b/can/interfaces/cantact.py
@@ -0,0 +1,199 @@
+"""
+Interface for CANtact devices from Linklayer Labs
+"""
+
+import logging
+import time
+from collections.abc import Sequence
+from typing import Any
+from unittest.mock import Mock
+
+from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message
+
+from ..exceptions import (
+ CanInitializationError,
+ CanInterfaceNotImplementedError,
+ error_check,
+)
+from ..typechecking import AutoDetectedConfig
+from ..util import check_or_adjust_timing_clock, deprecated_args_alias
+
+logger = logging.getLogger(__name__)
+
+try:
+ import cantact
+except ImportError:
+ cantact = None
+ logger.warning(
+ "The CANtact module is not installed. Install it using `pip install cantact`"
+ )
+
+
+class CantactBus(BusABC):
+ """CANtact interface"""
+
+ @staticmethod
+ def _detect_available_configs() -> Sequence[AutoDetectedConfig]:
+ try:
+ interface = cantact.Interface()
+ except (NameError, SystemError, AttributeError):
+ logger.debug(
+ "Could not import or instantiate cantact, so no configurations are available"
+ )
+ return []
+
+ channels: list[AutoDetectedConfig] = []
+ for i in range(0, interface.channel_count()):
+ channels.append({"interface": "cantact", "channel": f"ch:{i}"})
+ return channels
+
+ @deprecated_args_alias(
+ deprecation_start="4.2.0", deprecation_end="5.0.0", bit_timing="timing"
+ )
+ def __init__(
+ self,
+ channel: int,
+ bitrate: int = 500_000,
+ poll_interval: float = 0.01,
+ monitor: bool = False,
+ timing: BitTiming | BitTimingFd | None = None,
+ **kwargs: Any,
+ ) -> None:
+ """
+ :param int channel:
+ Channel number (zero indexed, labeled on multi-channel devices)
+ :param int bitrate:
+ Bitrate in bits/s
+ :param bool monitor:
+ If true, operate in listen-only monitoring mode
+ :param timing:
+ Optional :class:`~can.BitTiming` instance to use for custom bit timing setting.
+ If this argument is set then it overrides the bitrate argument. The
+ `f_clock` value of the timing instance must be set to 24_000_000 (24MHz)
+ for standard CAN.
+ CAN FD and the :class:`~can.BitTimingFd` class are not supported.
+ """
+
+ if kwargs.get("_testing", False):
+ self.interface = MockInterface()
+ else:
+ if cantact is None:
+ raise CanInterfaceNotImplementedError(
+ "The CANtact module is not installed. "
+ "Install it using `python -m pip install cantact`"
+ )
+ with error_check(
+ "Cannot create the cantact.Interface", CanInitializationError
+ ):
+ self.interface = cantact.Interface()
+
+ self.channel = int(channel)
+ self.channel_info = f"CANtact: ch:{channel}"
+ self._can_protocol = CanProtocol.CAN_20
+
+ # Configure the interface
+ with error_check("Cannot setup the cantact.Interface", CanInitializationError):
+ if isinstance(timing, BitTiming):
+ timing = check_or_adjust_timing_clock(timing, valid_clocks=[24_000_000])
+
+ # use custom bit timing
+ self.interface.set_bit_timing(
+ int(channel),
+ int(timing.brp),
+ int(timing.tseg1),
+ int(timing.tseg2),
+ int(timing.sjw),
+ )
+ elif isinstance(timing, BitTimingFd):
+ raise NotImplementedError(
+ f"CAN FD is not supported by {self.__class__.__name__}."
+ )
+ else:
+ # use bitrate
+ self.interface.set_bitrate(int(channel), int(bitrate))
+
+ self.interface.set_enabled(int(channel), True)
+ self.interface.set_monitor(int(channel), monitor)
+ self.interface.start()
+
+ super().__init__(
+ channel=channel,
+ bitrate=bitrate,
+ poll_interval=poll_interval,
+ **kwargs,
+ )
+
+ def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]:
+ if timeout is None:
+ raise TypeError(
+ f"{self.__class__.__name__} expects a numeric `timeout` value."
+ )
+
+ with error_check("Cannot receive message"):
+ frame = self.interface.recv(int(timeout * 1000))
+ if frame is None:
+ # timeout occurred
+ return None, False
+
+ msg = Message(
+ arbitration_id=frame["id"],
+ is_extended_id=frame["extended"],
+ timestamp=frame["timestamp"],
+ is_remote_frame=frame["rtr"],
+ dlc=frame["dlc"],
+ data=frame["data"][: frame["dlc"]],
+ channel=frame["channel"],
+ is_rx=(not frame["loopback"]), # received if not loopback frame
+ )
+ return msg, False
+
+ def send(self, msg: Message, timeout: float | None = None) -> None:
+ with error_check("Cannot send message"):
+ self.interface.send(
+ self.channel,
+ msg.arbitration_id,
+ bool(msg.is_extended_id),
+ bool(msg.is_remote_frame),
+ msg.dlc,
+ msg.data,
+ )
+
+ def shutdown(self) -> None:
+ super().shutdown()
+ with error_check("Cannot shutdown interface"):
+ self.interface.stop()
+
+
+def mock_recv(timeout: int) -> dict[str, Any] | None:
+ if timeout > 0:
+ return {
+ "id": 0x123,
+ "extended": False,
+ "timestamp": time.time(),
+ "loopback": False,
+ "rtr": False,
+ "dlc": 8,
+ "data": [1, 2, 3, 4, 5, 6, 7, 8],
+ "channel": 0,
+ }
+ else:
+ # simulate timeout when timeout = 0
+ return None
+
+
+class MockInterface:
+ """
+ Mock interface to replace real interface when testing.
+ This allows for tests to run without actual hardware.
+ """
+
+ start = Mock()
+ set_bitrate = Mock()
+ set_bit_timing = Mock()
+ set_enabled = Mock()
+ set_monitor = Mock()
+ stop = Mock()
+ send = Mock()
+ channel_count = Mock(return_value=1)
+
+ recv = Mock(side_effect=mock_recv)
diff --git a/can/interfaces/etas/__init__.py b/can/interfaces/etas/__init__.py
new file mode 100644
index 000000000..f8364a3fd
--- /dev/null
+++ b/can/interfaces/etas/__init__.py
@@ -0,0 +1,320 @@
+import time
+from typing import Any
+
+import can
+from can.exceptions import CanInitializationError
+
+from .boa import *
+
+
+class EtasBus(can.BusABC):
+ def __init__(
+ self,
+ channel: str,
+ can_filters: can.typechecking.CanFilters | None = None,
+ receive_own_messages: bool = False,
+ bitrate: int = 1000000,
+ fd: bool = True,
+ data_bitrate: int = 2000000,
+ **kwargs: dict[str, Any],
+ ):
+ self.receive_own_messages = receive_own_messages
+ self._can_protocol = can.CanProtocol.CAN_FD if fd else can.CanProtocol.CAN_20
+
+ nodeRange = CSI_NodeRange(CSI_NODE_MIN, CSI_NODE_MAX)
+ self.tree = ctypes.POINTER(CSI_Tree)()
+ CSI_CreateProtocolTree(ctypes.c_char_p(b""), nodeRange, ctypes.byref(self.tree))
+
+ oci_can_v = BOA_Version(1, 4, 0, 0)
+
+ self.ctrl = OCI_ControllerHandle()
+ OCI_CreateCANControllerNoSearch(
+ channel.encode(),
+ ctypes.byref(oci_can_v),
+ self.tree,
+ ctypes.byref(self.ctrl),
+ )
+
+ ctrlConf = OCI_CANConfiguration()
+ ctrlConf.baudrate = bitrate
+ ctrlConf.samplePoint = 80
+ ctrlConf.samplesPerBit = OCI_CAN_THREE_SAMPLES_PER_BIT
+ ctrlConf.BTL_Cycles = 10
+ ctrlConf.SJW = 1
+ ctrlConf.syncEdge = OCI_CAN_SINGLE_SYNC_EDGE
+ ctrlConf.physicalMedia = OCI_CAN_MEDIA_HIGH_SPEED
+ if receive_own_messages:
+ ctrlConf.selfReceptionMode = OCI_SELF_RECEPTION_ON
+ else:
+ ctrlConf.selfReceptionMode = OCI_SELF_RECEPTION_OFF
+ ctrlConf.busParticipationMode = OCI_BUSMODE_ACTIVE
+
+ if fd:
+ ctrlConf.canFDEnabled = True
+ ctrlConf.canFDConfig.dataBitRate = data_bitrate
+ ctrlConf.canFDConfig.dataBTL_Cycles = 10
+ ctrlConf.canFDConfig.dataSamplePoint = 80
+ ctrlConf.canFDConfig.dataSJW = 1
+ ctrlConf.canFDConfig.flags = 0
+ ctrlConf.canFDConfig.canFdTxConfig = OCI_CANFDTX_USE_CAN_AND_CANFD_FRAMES
+ ctrlConf.canFDConfig.canFdRxConfig.canRxMode = (
+ OCI_CAN_RXMODE_CAN_FRAMES_USING_CAN_MESSAGE
+ )
+ ctrlConf.canFDConfig.canFdRxConfig.canFdRxMode = (
+ OCI_CANFDRXMODE_CANFD_FRAMES_USING_CANFD_MESSAGE
+ )
+
+ ctrlProp = OCI_CANControllerProperties()
+ ctrlProp.mode = OCI_CONTROLLER_MODE_RUNNING
+
+ ec = OCI_OpenCANController(
+ self.ctrl, ctypes.byref(ctrlConf), ctypes.byref(ctrlProp)
+ )
+ if ec != 0x0 and ec != 0x40004000: # accept BOA_WARN_PARAM_ADAPTED
+ raise CanInitializationError(
+ f"OCI_OpenCANController failed with error 0x{ec:X}"
+ )
+
+ # RX
+
+ rxQConf = OCI_CANRxQueueConfiguration()
+ rxQConf.onFrame.function = ctypes.cast(None, OCI_CANRxCallbackFunctionSingleMsg)
+ rxQConf.onFrame.userData = None
+ rxQConf.onEvent.function = ctypes.cast(None, OCI_CANRxCallbackFunctionSingleMsg)
+ rxQConf.onEvent.userData = None
+ if receive_own_messages:
+ rxQConf.selfReceptionMode = OCI_SELF_RECEPTION_ON
+ else:
+ rxQConf.selfReceptionMode = OCI_SELF_RECEPTION_OFF
+ self.rxQueue = OCI_QueueHandle()
+ OCI_CreateCANRxQueue(
+ self.ctrl, ctypes.byref(rxQConf), ctypes.byref(self.rxQueue)
+ )
+
+ self._oci_filters = None
+ self.filters = can_filters
+
+ # TX
+
+ txQConf = OCI_CANTxQueueConfiguration()
+ txQConf.reserved = 0
+ self.txQueue = OCI_QueueHandle()
+ OCI_CreateCANTxQueue(
+ self.ctrl, ctypes.byref(txQConf), ctypes.byref(self.txQueue)
+ )
+
+ # Common
+
+ timerCapabilities = OCI_TimerCapabilities()
+ OCI_GetTimerCapabilities(self.ctrl, ctypes.byref(timerCapabilities))
+ self.tickFrequency = timerCapabilities.tickFrequency # clock ticks per second
+
+ # all timestamps are hardware timestamps relative to the CAN device powerup
+ # calculate an offset to make them relative to epoch
+ now = OCI_Time()
+ OCI_GetTimerValue(self.ctrl, ctypes.byref(now))
+ self.timeOffset = time.time() - (float(now.value) / self.tickFrequency)
+
+ self.channel_info = channel
+
+ # Super call must be after child init since super calls set_filters
+ super().__init__(channel=channel, **kwargs)
+
+ def _recv_internal(self, timeout: float | None) -> tuple[can.Message | None, bool]:
+ ociMsgs = (ctypes.POINTER(OCI_CANMessageEx) * 1)()
+ ociMsg = OCI_CANMessageEx()
+ ociMsgs[0] = ctypes.pointer(ociMsg)
+
+ count = ctypes.c_uint32()
+ if timeout is not None: # wait for specified time
+ t = OCI_Time(round(timeout * self.tickFrequency))
+ else: # wait indefinitely
+ t = OCI_NO_TIME
+ OCI_ReadCANDataEx(
+ self.rxQueue,
+ t,
+ ociMsgs,
+ 1,
+ ctypes.byref(count),
+ None,
+ )
+
+ msg = None
+
+ if count.value != 0:
+ if ociMsg.type == OCI_CANFDRX_MESSAGE.value:
+ ociRxMsg = ociMsg.data.canFDRxMessage
+ msg = can.Message(
+ timestamp=float(ociRxMsg.timeStamp) / self.tickFrequency
+ + self.timeOffset,
+ arbitration_id=ociRxMsg.frameID,
+ is_extended_id=bool(ociRxMsg.flags & OCI_CAN_MSG_FLAG_EXTENDED),
+ is_remote_frame=bool(
+ ociRxMsg.flags & OCI_CAN_MSG_FLAG_REMOTE_FRAME
+ ),
+ # is_error_frame=False,
+ # channel=None,
+ dlc=ociRxMsg.size,
+ data=ociRxMsg.data[0 : ociRxMsg.size],
+ is_fd=True,
+ is_rx=not bool(ociRxMsg.flags & OCI_CAN_MSG_FLAG_SELFRECEPTION),
+ bitrate_switch=bool(
+ ociRxMsg.flags & OCI_CAN_MSG_FLAG_FD_DATA_BIT_RATE
+ ),
+ # error_state_indicator=False,
+ # check=False,
+ )
+ elif ociMsg.type == OCI_CAN_RX_MESSAGE.value:
+ ociRxMsg = ociMsg.data.rxMessage
+ msg = can.Message(
+ timestamp=float(ociRxMsg.timeStamp) / self.tickFrequency
+ + self.timeOffset,
+ arbitration_id=ociRxMsg.frameID,
+ is_extended_id=bool(ociRxMsg.flags & OCI_CAN_MSG_FLAG_EXTENDED),
+ is_remote_frame=bool(
+ ociRxMsg.flags & OCI_CAN_MSG_FLAG_REMOTE_FRAME
+ ),
+ # is_error_frame=False,
+ # channel=None,
+ dlc=ociRxMsg.dlc,
+ data=ociRxMsg.data[0 : ociRxMsg.dlc],
+ # is_fd=False,
+ is_rx=not bool(ociRxMsg.flags & OCI_CAN_MSG_FLAG_SELFRECEPTION),
+ # bitrate_switch=False,
+ # error_state_indicator=False,
+ # check=False,
+ )
+
+ return (msg, True)
+
+ def send(self, msg: can.Message, timeout: float | None = None) -> None:
+ ociMsgs = (ctypes.POINTER(OCI_CANMessageEx) * 1)()
+ ociMsg = OCI_CANMessageEx()
+ ociMsgs[0] = ctypes.pointer(ociMsg)
+
+ if msg.is_fd:
+ ociMsg.type = OCI_CANFDTX_MESSAGE
+ ociTxMsg = ociMsg.data.canFDTxMessage
+ ociTxMsg.size = msg.dlc
+ else:
+ ociMsg.type = OCI_CAN_TX_MESSAGE
+ ociTxMsg = ociMsg.data.txMessage
+ ociTxMsg.dlc = msg.dlc
+
+ # set fields common to CAN / CAN-FD
+ ociTxMsg.frameID = msg.arbitration_id
+ ociTxMsg.flags = 0
+ if msg.is_extended_id:
+ ociTxMsg.flags |= OCI_CAN_MSG_FLAG_EXTENDED
+ if msg.is_remote_frame:
+ ociTxMsg.flags |= OCI_CAN_MSG_FLAG_REMOTE_FRAME
+ ociTxMsg.data = tuple(msg.data)
+
+ if msg.is_fd:
+ ociTxMsg.flags |= OCI_CAN_MSG_FLAG_FD_DATA
+ if msg.bitrate_switch:
+ ociTxMsg.flags |= OCI_CAN_MSG_FLAG_FD_DATA_BIT_RATE
+
+ OCI_WriteCANDataEx(self.txQueue, OCI_NO_TIME, ociMsgs, 1, None)
+
+ def _apply_filters(self, filters: can.typechecking.CanFilters | None) -> None:
+ if self._oci_filters:
+ OCI_RemoveCANFrameFilterEx(self.rxQueue, self._oci_filters, 1)
+
+ # "accept all" filter
+ if filters is None:
+ filters = [{"can_id": 0x0, "can_mask": 0x0}]
+
+ self._oci_filters = (ctypes.POINTER(OCI_CANRxFilterEx) * len(filters))()
+
+ for i, filter_ in enumerate(filters):
+ f = OCI_CANRxFilterEx()
+ f.frameIDValue = filter_["can_id"]
+ f.frameIDMask = filter_["can_mask"]
+ f.tag = 0
+ f.flagsValue = 0
+ if self.receive_own_messages:
+ # mask out the SR bit, i.e. ignore the bit -> receive all
+ f.flagsMask = 0
+ else:
+ # enable the SR bit in the mask. since the bit is 0 in flagsValue -> do not self-receive
+ f.flagsMask = OCI_CAN_MSG_FLAG_SELFRECEPTION
+ if filter_.get("extended"):
+ f.flagsValue |= OCI_CAN_MSG_FLAG_EXTENDED
+ f.flagsMask |= OCI_CAN_MSG_FLAG_EXTENDED
+ self._oci_filters[i].contents = f
+
+ OCI_AddCANFrameFilterEx(self.rxQueue, self._oci_filters, len(self._oci_filters))
+
+ def flush_tx_buffer(self) -> None:
+ OCI_ResetQueue(self.txQueue)
+
+ def shutdown(self) -> None:
+ super().shutdown()
+ # Cleanup TX
+ if self.txQueue:
+ OCI_DestroyCANTxQueue(self.txQueue)
+ self.txQueue = None
+
+ # Cleanup RX
+ if self.rxQueue:
+ OCI_DestroyCANRxQueue(self.rxQueue)
+ self.rxQueue = None
+
+ # Cleanup common
+ if self.ctrl:
+ OCI_CloseCANController(self.ctrl)
+ OCI_DestroyCANController(self.ctrl)
+ self.ctrl = None
+
+ if self.tree:
+ CSI_DestroyProtocolTree(self.tree)
+ self.tree = None
+
+ @property
+ def state(self) -> can.BusState:
+ status = OCI_CANControllerStatus()
+ OCI_GetCANControllerStatus(self.ctrl, ctypes.byref(status))
+ if status.stateCode & OCI_CAN_STATE_ACTIVE:
+ return can.BusState.ACTIVE
+ elif status.stateCode & OCI_CAN_STATE_PASSIVE:
+ return can.BusState.PASSIVE
+
+ @state.setter
+ def state(self, new_state: can.BusState) -> None:
+ # disabled, OCI_AdaptCANConfiguration does not allow changing the bus mode
+ # if new_state == can.BusState.ACTIVE:
+ # self.ctrlConf.busParticipationMode = OCI_BUSMODE_ACTIVE
+ # else:
+ # self.ctrlConf.busParticipationMode = OCI_BUSMODE_PASSIVE
+ # ec = OCI_AdaptCANConfiguration(self.ctrl, ctypes.byref(self.ctrlConf))
+ # if ec != 0x0:
+ # raise CanOperationError(f"OCI_AdaptCANConfiguration failed with error 0x{ec:X}")
+ raise NotImplementedError("Setting state is not implemented.")
+
+ @staticmethod
+ def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]:
+ nodeRange = CSI_NodeRange(CSI_NODE_MIN, CSI_NODE_MAX)
+ tree = ctypes.POINTER(CSI_Tree)()
+ CSI_CreateProtocolTree(ctypes.c_char_p(b""), nodeRange, ctypes.byref(tree))
+
+ nodes: list[dict[str, str]] = []
+
+ def _findNodes(tree, prefix):
+ uri = f"{prefix}/{tree.contents.item.uriName.decode()}"
+ if "CAN:" in uri:
+ nodes.append({"interface": "etas", "channel": uri})
+ elif tree.contents.child:
+ _findNodes(
+ tree.contents.child,
+ f"{prefix}/{tree.contents.item.uriName.decode()}",
+ )
+
+ if tree.contents.sibling:
+ _findNodes(tree.contents.sibling, prefix)
+
+ _findNodes(tree, "ETAS:/")
+
+ CSI_DestroyProtocolTree(tree)
+
+ return nodes
diff --git a/can/interfaces/etas/boa.py b/can/interfaces/etas/boa.py
new file mode 100644
index 000000000..3efb20f6c
--- /dev/null
+++ b/can/interfaces/etas/boa.py
@@ -0,0 +1,698 @@
+import ctypes
+
+from ...exceptions import CanInitializationError, CanOperationError
+
+try:
+ # try to load libraries from the system default paths
+ _csi = ctypes.windll.LoadLibrary("dll-csiBind")
+ _oci = ctypes.windll.LoadLibrary("dll-ocdProxy")
+except FileNotFoundError:
+ # try to load libraries with hardcoded paths
+ if ctypes.sizeof(ctypes.c_voidp) == 4:
+ # 32 bit
+ path = "C:/Program Files (x86)/ETAS/BOA_V2/Bin/Win32/Dll/Framework/"
+ elif ctypes.sizeof(ctypes.c_voidp) == 8:
+ # 64 bit
+ path = "C:/Program Files/ETAS/BOA_V2/Bin/x64/Dll/Framework/"
+ _csi = ctypes.windll.LoadLibrary(path + "dll-csiBind")
+ _oci = ctypes.windll.LoadLibrary(path + "dll-ocdProxy")
+
+
+# define helper functions to use with errcheck
+
+
+def errcheck_init(result, func, _arguments):
+ # unfortunately, we can't use OCI_GetError here
+ # because we don't always have a handle to use
+ # text = ctypes.create_string_buffer(500)
+ # OCI_GetError(self.ctrl, ec, text, 500)
+ if result != 0x0:
+ raise CanInitializationError(f"{func.__name__} failed with error 0x{result:X}")
+ return result
+
+
+def errcheck_oper(result, func, _arguments):
+ if result != 0x0:
+ raise CanOperationError(f"{func.__name__} failed with error 0x{result:X}")
+ return result
+
+
+# Common (BOA)
+
+BOA_ResultCode = ctypes.c_uint32
+BOA_Handle = ctypes.c_int32
+BOA_Time = ctypes.c_int64
+
+BOA_NO_VALUE = -1
+BOA_NO_HANDLE = BOA_Handle(BOA_NO_VALUE)
+BOA_NO_TIME = BOA_Time(BOA_NO_VALUE)
+
+
+class BOA_UuidBin(ctypes.Structure):
+ _fields_ = [("data", ctypes.c_uint8 * 16)]
+
+
+class BOA_Version(ctypes.Structure):
+ _fields_ = [
+ ("majorVersion", ctypes.c_uint8),
+ ("minorVersion", ctypes.c_uint8),
+ ("bugfix", ctypes.c_uint8),
+ ("build", ctypes.c_uint8),
+ ]
+
+
+class BOA_UuidVersion(ctypes.Structure):
+ _fields_ = [("uuid", BOA_UuidBin), ("version", BOA_Version)]
+
+
+class BOA_ServiceId(ctypes.Structure):
+ _fields_ = [("api", BOA_UuidVersion), ("access", BOA_UuidVersion)]
+
+
+class BOA_ServiceIdParam(ctypes.Structure):
+ _fields_ = [
+ ("id", BOA_ServiceId),
+ ("count", ctypes.c_uint32),
+ ("accessParam", ctypes.c_char * 128),
+ ]
+
+
+# Connection Service Interface (CSI)
+
+# CSI - Search For Service (SFS)
+
+CSI_NodeType = ctypes.c_uint32
+CSI_NODE_MIN = CSI_NodeType(0)
+CSI_NODE_MAX = CSI_NodeType(0x7FFF)
+
+
+class CSI_NodeRange(ctypes.Structure):
+ _fields_ = [("min", CSI_NodeType), ("max", CSI_NodeType)]
+
+
+class CSI_SubItem(ctypes.Structure):
+ _fields_ = [
+ ("server", BOA_ServiceIdParam),
+ ("nodeType", CSI_NodeType),
+ ("uriName", ctypes.c_char * 128),
+ ("visibleName", ctypes.c_char * 4),
+ ("version", BOA_Version),
+ ("reserved2", ctypes.c_char * 88),
+ ("serverAffinity", BOA_UuidBin),
+ ("requiredAffinity0", BOA_UuidBin),
+ ("reserved", ctypes.c_int32 * 4),
+ ("requiredAffinity1", BOA_UuidBin),
+ ("count", ctypes.c_int32),
+ ("requiredAPI", BOA_ServiceId * 4),
+ ]
+
+
+class CSI_Tree(ctypes.Structure):
+ pass
+
+
+CSI_Tree._fields_ = [
+ ("item", CSI_SubItem),
+ ("sibling", ctypes.POINTER(CSI_Tree)),
+ ("child", ctypes.POINTER(CSI_Tree)),
+ ("childrenProbed", ctypes.c_int),
+]
+
+CSI_CreateProtocolTree = _csi.CSI_CreateProtocolTree
+CSI_CreateProtocolTree.argtypes = [
+ ctypes.c_char_p,
+ CSI_NodeRange,
+ ctypes.POINTER(ctypes.POINTER(CSI_Tree)),
+]
+CSI_CreateProtocolTree.restype = BOA_ResultCode
+CSI_CreateProtocolTree.errcheck = errcheck_init
+
+CSI_DestroyProtocolTree = _csi.CSI_DestroyProtocolTree
+CSI_DestroyProtocolTree.argtypes = [
+ ctypes.POINTER(CSI_Tree),
+]
+CSI_DestroyProtocolTree.restype = BOA_ResultCode
+CSI_DestroyProtocolTree.errcheck = errcheck_oper
+
+# Open Controller Interface (OCI)
+
+# OCI Common - Global Types
+
+OCI_NO_VALUE = BOA_NO_VALUE
+OCI_NO_HANDLE = BOA_NO_HANDLE
+OCI_Handle = BOA_Handle
+OCI_Time = BOA_Time
+
+# OCI Common - Controller Handling
+
+OCI_ControllerHandle = OCI_Handle
+
+OCI_ControllerPropertiesMode = ctypes.c_uint32
+OCI_CONTROLLER_MODE_RUNNING = OCI_ControllerPropertiesMode(0)
+OCI_CONTROLLER_MODE_SUSPENDED = OCI_ControllerPropertiesMode(1)
+
+OCI_SelfReceptionMode = ctypes.c_uint32
+OCI_SELF_RECEPTION_OFF = OCI_SelfReceptionMode(0)
+OCI_SELF_RECEPTION_ON = OCI_SelfReceptionMode(1)
+
+# OCI Common - Event Handling
+
+# OCI Common - Error Management
+
+OCI_ErrorCode = BOA_ResultCode
+
+OCI_InternalErrorEvent = ctypes.c_uint32
+OCI_INTERNAL_GENERAL_ERROR = OCI_InternalErrorEvent(0)
+
+
+class OCI_InternalErrorEventMessage(ctypes.Structure):
+ _fields_ = [
+ ("timeStamp", OCI_Time),
+ ("tag", ctypes.c_uint32),
+ ("eventCode", OCI_InternalErrorEvent),
+ ("errorCode", OCI_ErrorCode),
+ ]
+
+
+OCI_GetError = _oci.OCI_GetError
+OCI_GetError.argtypes = [
+ OCI_Handle,
+ OCI_ErrorCode,
+ ctypes.c_char_p,
+ ctypes.c_uint32,
+]
+OCI_GetError.restype = OCI_ErrorCode
+OCI_GetError.errcheck = errcheck_oper
+
+# OCI Common - Queue Handling
+
+OCI_QueueHandle = OCI_Handle
+
+OCI_QueueEvent = ctypes.c_uint32
+OCI_QUEUE_UNDERRUN = OCI_QueueEvent(0)
+OCI_QUEUE_EMPTY = OCI_QueueEvent(1)
+OCI_QUEUE_NOT_EMPTY = OCI_QueueEvent(2)
+OCI_QUEUE_LOW_WATERMARK = OCI_QueueEvent(3)
+OCI_QUEUE_HIGH_WATERMARK = OCI_QueueEvent(4)
+OCI_QUEUE_FULL = OCI_QueueEvent(5)
+OCI_QUEUE_OVERFLOW = OCI_QueueEvent(6)
+
+
+class OCI_QueueEventMessage(ctypes.Structure):
+ _fields_ = [
+ ("timeStamp", OCI_Time),
+ ("tag", ctypes.c_uint32),
+ ("eventCode", OCI_QueueEvent),
+ ("destination", ctypes.c_uint32),
+ ]
+
+
+OCI_ResetQueue = _oci.OCI_ResetQueue
+OCI_ResetQueue.argtypes = [OCI_QueueHandle]
+OCI_ResetQueue.restype = OCI_ErrorCode
+OCI_ResetQueue.errcheck = errcheck_oper
+
+# OCI Common - Timer Handling
+
+OCI_NO_TIME = BOA_NO_TIME
+
+OCI_TimeReferenceScale = ctypes.c_uint32
+OCI_TimeReferenceScaleUnknown = OCI_TimeReferenceScale(0)
+OCI_TimeReferenceScaleTAI = OCI_TimeReferenceScale(1)
+OCI_TimeReferenceScaleUTC = OCI_TimeReferenceScale(2)
+
+OCI_TimerEvent = ctypes.c_uint32
+OCI_TIMER_EVENT_SYNC_LOCK = OCI_TimerEvent(0)
+OCI_TIMER_EVENT_SYNC_LOSS = OCI_TimerEvent(1)
+OCI_TIMER_EVENT_LEAP_SECOND = OCI_TimerEvent(2)
+
+
+class OCI_TimerCapabilities(ctypes.Structure):
+ _fields_ = [
+ ("localClockID", ctypes.c_char * 40),
+ ("format", ctypes.c_uint32),
+ ("tickFrequency", ctypes.c_uint32),
+ ("ticksPerIncrement", ctypes.c_uint32),
+ ("localStratumLevel", ctypes.c_uint32),
+ ("localReferenceScale", OCI_TimeReferenceScale),
+ ("localTimeOriginIso8601", ctypes.c_char * 40),
+ ("syncSlave", ctypes.c_uint32),
+ ("syncMaster", ctypes.c_uint32),
+ ]
+
+
+class OCI_TimerEventMessage(ctypes.Structure):
+ _fields_ = [
+ ("timeStamp", OCI_Time),
+ ("tag", ctypes.c_uint32),
+ ("eventCode", OCI_TimerEvent),
+ ("destination", ctypes.c_uint32),
+ ]
+
+
+OCI_GetTimerCapabilities = _oci.OCI_GetTimerCapabilities
+OCI_GetTimerCapabilities.argtypes = [
+ OCI_ControllerHandle,
+ ctypes.POINTER(OCI_TimerCapabilities),
+]
+OCI_GetTimerCapabilities.restype = OCI_ErrorCode
+OCI_GetTimerCapabilities.errcheck = errcheck_init
+
+OCI_GetTimerValue = _oci.OCI_GetTimerValue
+OCI_GetTimerValue.argtypes = [
+ OCI_ControllerHandle,
+ ctypes.POINTER(OCI_Time),
+]
+OCI_GetTimerValue.restype = OCI_ErrorCode
+OCI_GetTimerValue.errcheck = errcheck_oper
+
+# OCI CAN
+
+# OCI CAN - CAN-FD
+
+OCI_CANFDRxMode = ctypes.c_uint32
+OCI_CANFDRXMODE_CANFD_FRAMES_IGNORED = OCI_CANFDRxMode(1)
+OCI_CANFDRXMODE_CANFD_FRAMES_USING_CAN_MESSAGE = OCI_CANFDRxMode(2)
+OCI_CANFDRXMODE_CANFD_FRAMES_USING_CANFD_MESSAGE = OCI_CANFDRxMode(4)
+OCI_CANFDRXMODE_CANFD_FRAMES_USING_CANFD_MESSAGE_PADDING = OCI_CANFDRxMode(8)
+
+OCI_CANRxMode = ctypes.c_uint32
+OCI_CAN_RXMODE_CAN_FRAMES_IGNORED = OCI_CANRxMode(1)
+OCI_CAN_RXMODE_CAN_FRAMES_USING_CAN_MESSAGE = OCI_CANRxMode(2)
+
+OCI_CANFDTxConfig = ctypes.c_uint32
+OCI_CANFDTX_USE_CAN_FRAMES_ONLY = OCI_CANFDTxConfig(1)
+OCI_CANFDTX_USE_CANFD_FRAMES_ONLY = OCI_CANFDTxConfig(2)
+OCI_CANFDTX_USE_CAN_AND_CANFD_FRAMES = OCI_CANFDTxConfig(4)
+
+
+class OCI_CANFDRxConfig(ctypes.Structure):
+ _fields_ = [
+ ("canRxMode", OCI_CANRxMode),
+ ("canFdRxMode", OCI_CANFDRxMode),
+ ]
+
+
+class OCI_CANFDConfiguration(ctypes.Structure):
+ _fields_ = [
+ ("dataBitRate", ctypes.c_uint32),
+ ("dataSamplePoint", ctypes.c_uint32),
+ ("dataBTL_Cycles", ctypes.c_uint32),
+ ("dataSJW", ctypes.c_uint32),
+ ("flags", ctypes.c_uint32),
+ ("txSecondarySamplePointOffset", ctypes.c_uint32),
+ ("canFdRxConfig", OCI_CANFDRxConfig),
+ ("canFdTxConfig", OCI_CANFDTxConfig),
+ ("txSecondarySamplePointFilterWindow", ctypes.c_uint16),
+ ("reserved", ctypes.c_uint16),
+ ]
+
+
+class OCI_CANFDRxMessage(ctypes.Structure):
+ _fields_ = [
+ ("timeStamp", OCI_Time),
+ ("tag", ctypes.c_uint32),
+ ("frameID", ctypes.c_uint32),
+ ("flags", ctypes.c_uint16),
+ ("res", ctypes.c_uint8),
+ ("size", ctypes.c_uint8),
+ ("res1", ctypes.c_uint8 * 4),
+ ("data", ctypes.c_uint8 * 64),
+ ]
+
+
+class OCI_CANFDTxMessage(ctypes.Structure):
+ _fields_ = [
+ ("frameID", ctypes.c_uint32),
+ ("flags", ctypes.c_uint16),
+ ("res", ctypes.c_uint8),
+ ("size", ctypes.c_uint8),
+ ("data", ctypes.c_uint8 * 64),
+ ]
+
+
+# OCI CAN - Initialization
+
+OCI_CAN_THREE_SAMPLES_PER_BIT = 2
+OCI_CAN_SINGLE_SYNC_EDGE = 1
+OCI_CAN_MEDIA_HIGH_SPEED = 1
+
+OCI_CAN_STATE_ACTIVE = 0x00000001
+OCI_CAN_STATE_PASSIVE = 0x00000002
+OCI_CAN_STATE_ERRLIMIT = 0x00000004
+OCI_CAN_STATE_BUSOFF = 0x00000008
+
+OCI_CANBusParticipationMode = ctypes.c_uint32
+OCI_BUSMODE_PASSIVE = OCI_CANBusParticipationMode(1)
+OCI_BUSMODE_ACTIVE = OCI_CANBusParticipationMode(2)
+
+OCI_CANBusTransmissionPolicies = ctypes.c_uint32
+OCI_CANTX_UNDEFINED = OCI_CANBusTransmissionPolicies(0)
+OCI_CANTX_DONTCARE = OCI_CANBusTransmissionPolicies(0)
+OCI_CANTX_FIFO = OCI_CANBusTransmissionPolicies(1)
+OCI_CANTX_BESTEFFORT = OCI_CANBusTransmissionPolicies(2)
+
+
+class OCI_CANConfiguration(ctypes.Structure):
+ _fields_ = [
+ ("baudrate", ctypes.c_uint32),
+ ("samplePoint", ctypes.c_uint32),
+ ("samplesPerBit", ctypes.c_uint32),
+ ("BTL_Cycles", ctypes.c_uint32),
+ ("SJW", ctypes.c_uint32),
+ ("syncEdge", ctypes.c_uint32),
+ ("physicalMedia", ctypes.c_uint32),
+ ("selfReceptionMode", OCI_SelfReceptionMode),
+ ("busParticipationMode", OCI_CANBusParticipationMode),
+ ("canFDEnabled", ctypes.c_uint32),
+ ("canFDConfig", OCI_CANFDConfiguration),
+ ("canTxPolicy", OCI_CANBusTransmissionPolicies),
+ ]
+
+
+class OCI_CANControllerProperties(ctypes.Structure):
+ _fields_ = [
+ ("mode", OCI_ControllerPropertiesMode),
+ ]
+
+
+class OCI_CANControllerCapabilities(ctypes.Structure):
+ _fields_ = [
+ ("samplesPerBit", ctypes.c_uint32),
+ ("syncEdge", ctypes.c_uint32),
+ ("physicalMedia", ctypes.c_uint32),
+ ("reserved", ctypes.c_uint32),
+ ("busEvents", ctypes.c_uint32),
+ ("errorFrames", ctypes.c_uint32),
+ ("messageFlags", ctypes.c_uint32),
+ ("canFDSupport", ctypes.c_uint32),
+ ("canFDMaxDataSize", ctypes.c_uint32),
+ ("canFDMaxQualifiedDataRate", ctypes.c_uint32),
+ ("canFDMaxDataRate", ctypes.c_uint32),
+ ("canFDRxConfig_CANMode", ctypes.c_uint32),
+ ("canFDRxConfig_CANFDMode", ctypes.c_uint32),
+ ("canFDTxConfig_Mode", ctypes.c_uint32),
+ ("canBusParticipationMode", ctypes.c_uint32),
+ ("canTxPolicies", ctypes.c_uint32),
+ ]
+
+
+class OCI_CANControllerStatus(ctypes.Structure):
+ _fields_ = [
+ ("reserved", ctypes.c_uint32),
+ ("stateCode", ctypes.c_uint32),
+ ]
+
+
+OCI_CreateCANControllerNoSearch = _oci.OCI_CreateCANControllerNoSearch
+OCI_CreateCANControllerNoSearch.argtypes = [
+ ctypes.c_char_p,
+ ctypes.POINTER(BOA_Version),
+ ctypes.POINTER(CSI_Tree),
+ ctypes.POINTER(OCI_ControllerHandle),
+]
+OCI_CreateCANControllerNoSearch.restype = OCI_ErrorCode
+OCI_CreateCANControllerNoSearch.errcheck = errcheck_init
+
+OCI_OpenCANController = _oci.OCI_OpenCANController
+OCI_OpenCANController.argtypes = [
+ OCI_ControllerHandle,
+ ctypes.POINTER(OCI_CANConfiguration),
+ ctypes.POINTER(OCI_CANControllerProperties),
+]
+OCI_OpenCANController.restype = OCI_ErrorCode
+# no .errcheck, since we tolerate OCI_WARN_PARAM_ADAPTED warning
+# OCI_OpenCANController.errcheck = errcheck_init
+
+OCI_CloseCANController = _oci.OCI_CloseCANController
+OCI_CloseCANController.argtypes = [OCI_ControllerHandle]
+OCI_CloseCANController.restype = OCI_ErrorCode
+OCI_CloseCANController.errcheck = errcheck_oper
+
+OCI_DestroyCANController = _oci.OCI_DestroyCANController
+OCI_DestroyCANController.argtypes = [OCI_ControllerHandle]
+OCI_DestroyCANController.restype = OCI_ErrorCode
+OCI_DestroyCANController.errcheck = errcheck_oper
+
+OCI_AdaptCANConfiguration = _oci.OCI_AdaptCANConfiguration
+OCI_AdaptCANConfiguration.argtypes = [
+ OCI_ControllerHandle,
+ ctypes.POINTER(OCI_CANConfiguration),
+]
+OCI_AdaptCANConfiguration.restype = OCI_ErrorCode
+OCI_AdaptCANConfiguration.errcheck = errcheck_oper
+
+OCI_GetCANControllerCapabilities = _oci.OCI_GetCANControllerCapabilities
+OCI_GetCANControllerCapabilities.argtypes = [
+ OCI_ControllerHandle,
+ ctypes.POINTER(OCI_CANControllerCapabilities),
+]
+OCI_GetCANControllerCapabilities.restype = OCI_ErrorCode
+OCI_GetCANControllerCapabilities.errcheck = errcheck_init
+
+OCI_GetCANControllerStatus = _oci.OCI_GetCANControllerStatus
+OCI_GetCANControllerStatus.argtypes = [
+ OCI_ControllerHandle,
+ ctypes.POINTER(OCI_CANControllerStatus),
+]
+OCI_GetCANControllerStatus.restype = OCI_ErrorCode
+OCI_GetCANControllerStatus.errcheck = errcheck_oper
+
+
+# OCI CAN - Filter
+
+
+class OCI_CANRxFilter(ctypes.Structure):
+ _fields_ = [
+ ("frameIDValue", ctypes.c_uint32),
+ ("frameIDMask", ctypes.c_uint32),
+ ("tag", ctypes.c_uint32),
+ ]
+
+
+class OCI_CANRxFilterEx(ctypes.Structure):
+ _fields_ = [
+ ("frameIDValue", ctypes.c_uint32),
+ ("frameIDMask", ctypes.c_uint32),
+ ("tag", ctypes.c_uint32),
+ ("flagsValue", ctypes.c_uint16),
+ ("flagsMask", ctypes.c_uint16),
+ ]
+
+
+OCI_AddCANFrameFilterEx = _oci.OCI_AddCANFrameFilterEx
+OCI_AddCANFrameFilterEx.argtypes = [
+ OCI_QueueHandle,
+ ctypes.POINTER(ctypes.POINTER(OCI_CANRxFilterEx)),
+ ctypes.c_uint32,
+]
+OCI_AddCANFrameFilterEx.restype = OCI_ErrorCode
+OCI_AddCANFrameFilterEx.errcheck = errcheck_oper
+
+OCI_RemoveCANFrameFilterEx = _oci.OCI_RemoveCANFrameFilterEx
+OCI_RemoveCANFrameFilterEx.argtypes = [
+ OCI_QueueHandle,
+ ctypes.POINTER(ctypes.POINTER(OCI_CANRxFilterEx)),
+ ctypes.c_uint32,
+]
+OCI_RemoveCANFrameFilterEx.restype = OCI_ErrorCode
+OCI_RemoveCANFrameFilterEx.errcheck = errcheck_oper
+
+# OCI CAN - Messages
+
+OCI_CAN_MSG_FLAG_EXTENDED = 0x1
+OCI_CAN_MSG_FLAG_REMOTE_FRAME = 0x2
+OCI_CAN_MSG_FLAG_SELFRECEPTION = 0x4
+OCI_CAN_MSG_FLAG_FD_DATA_BIT_RATE = 0x8
+OCI_CAN_MSG_FLAG_FD_TRUNC_AND_PAD = 0x10
+OCI_CAN_MSG_FLAG_FD_ERROR_PASSIVE = 0x20
+OCI_CAN_MSG_FLAG_FD_DATA = 0x40
+
+OCI_CANMessageDataType = ctypes.c_uint32
+OCI_CAN_RX_MESSAGE = OCI_CANMessageDataType(1)
+OCI_CAN_TX_MESSAGE = OCI_CANMessageDataType(2)
+OCI_CAN_ERROR_FRAME = OCI_CANMessageDataType(3)
+OCI_CAN_BUS_EVENT = OCI_CANMessageDataType(4)
+OCI_CAN_INTERNAL_ERROR_EVENT = OCI_CANMessageDataType(5)
+OCI_CAN_QUEUE_EVENT = OCI_CANMessageDataType(6)
+OCI_CAN_TIMER_EVENT = OCI_CANMessageDataType(7)
+OCI_CANFDRX_MESSAGE = OCI_CANMessageDataType(8)
+OCI_CANFDTX_MESSAGE = OCI_CANMessageDataType(9)
+
+
+class OCI_CANTxMessage(ctypes.Structure):
+ _fields_ = [
+ ("frameID", ctypes.c_uint32),
+ ("flags", ctypes.c_uint16),
+ ("res", ctypes.c_uint8),
+ ("dlc", ctypes.c_uint8),
+ ("data", ctypes.c_uint8 * 8),
+ ]
+
+
+class OCI_CANRxMessage(ctypes.Structure):
+ _fields_ = [
+ ("timeStamp", OCI_Time),
+ ("tag", ctypes.c_uint32),
+ ("frameID", ctypes.c_uint32),
+ ("flags", ctypes.c_uint16),
+ ("res", ctypes.c_uint8),
+ ("dlc", ctypes.c_uint8),
+ ("res1", ctypes.c_uint8 * 4),
+ ("data", ctypes.c_uint8 * 8),
+ ]
+
+
+class OCI_CANErrorFrameMessage(ctypes.Structure):
+ _fields_ = [
+ ("timeStamp", OCI_Time),
+ ("tag", ctypes.c_uint32),
+ ("frameID", ctypes.c_uint32),
+ ("flags", ctypes.c_uint16),
+ ("res", ctypes.c_uint8),
+ ("dlc", ctypes.c_uint8),
+ ("type", ctypes.c_uint32),
+ ("destination", ctypes.c_uint32),
+ ]
+
+
+class OCI_CANEventMessage(ctypes.Structure):
+ _fields_ = [
+ ("timeStamp", OCI_Time),
+ ("tag", ctypes.c_uint32),
+ ("eventCode", ctypes.c_uint32),
+ ("destination", ctypes.c_uint32),
+ ]
+
+
+class OCI_CANMessageData(ctypes.Union):
+ _fields_ = [
+ ("rxMessage", OCI_CANRxMessage),
+ ("txMessage", OCI_CANTxMessage),
+ ("errorFrameMessage", OCI_CANErrorFrameMessage),
+ ("canEventMessage", OCI_CANEventMessage),
+ ("internalErrorEventMessage", OCI_InternalErrorEventMessage),
+ ("timerEventMessage", OCI_TimerEventMessage),
+ ("queueEventMessage", OCI_QueueEventMessage),
+ ]
+
+
+class OCI_CANMessageDataEx(ctypes.Union):
+ _fields_ = [
+ ("rxMessage", OCI_CANRxMessage),
+ ("txMessage", OCI_CANTxMessage),
+ ("errorFrameMessage", OCI_CANErrorFrameMessage),
+ ("canEventMessage", OCI_CANEventMessage),
+ ("internalErrorEventMessage", OCI_InternalErrorEventMessage),
+ ("timerEventMessage", OCI_TimerEventMessage),
+ ("queueEventMessage", OCI_QueueEventMessage),
+ ("canFDRxMessage", OCI_CANFDRxMessage),
+ ("canFDTxMessage", OCI_CANFDTxMessage),
+ ]
+
+
+class OCI_CANMessage(ctypes.Structure):
+ _fields_ = [
+ ("type", OCI_CANMessageDataType),
+ ("reserved", ctypes.c_uint32),
+ ("data", OCI_CANMessageData),
+ ]
+
+
+class OCI_CANMessageEx(ctypes.Structure):
+ _fields_ = [
+ ("type", OCI_CANMessageDataType),
+ ("reserved", ctypes.c_uint32),
+ ("data", OCI_CANMessageDataEx),
+ ]
+
+
+# OCI CAN - Queues
+
+OCI_CANRxCallbackFunctionSingleMsg = ctypes.CFUNCTYPE(
+ None, ctypes.c_void_p, ctypes.POINTER(OCI_CANMessage)
+)
+
+OCI_CANRxCallbackFunctionSingleMsgEx = ctypes.CFUNCTYPE(
+ None, ctypes.c_void_p, ctypes.POINTER(OCI_CANMessageEx)
+)
+
+
+class OCI_CANRxCallbackSingleMsg(ctypes.Structure):
+ class _U(ctypes.Union):
+ _fields_ = [
+ ("function", OCI_CANRxCallbackFunctionSingleMsg),
+ ("functionEx", OCI_CANRxCallbackFunctionSingleMsgEx),
+ ]
+
+ _anonymous_ = ("u",)
+ _fields_ = [
+ ("u", _U),
+ ("userData", ctypes.c_void_p),
+ ]
+
+
+class OCI_CANRxQueueConfiguration(ctypes.Structure):
+ _fields_ = [
+ ("onFrame", OCI_CANRxCallbackSingleMsg),
+ ("onEvent", OCI_CANRxCallbackSingleMsg),
+ ("selfReceptionMode", OCI_SelfReceptionMode),
+ ]
+
+
+class OCI_CANTxQueueConfiguration(ctypes.Structure):
+ _fields_ = [
+ ("reserved", ctypes.c_uint32),
+ ]
+
+
+OCI_CreateCANRxQueue = _oci.OCI_CreateCANRxQueue
+OCI_CreateCANRxQueue.argtypes = [
+ OCI_ControllerHandle,
+ ctypes.POINTER(OCI_CANRxQueueConfiguration),
+ ctypes.POINTER(OCI_QueueHandle),
+]
+OCI_CreateCANRxQueue.restype = OCI_ErrorCode
+OCI_CreateCANRxQueue.errcheck = errcheck_init
+
+OCI_DestroyCANRxQueue = _oci.OCI_DestroyCANRxQueue
+OCI_DestroyCANRxQueue.argtypes = [OCI_QueueHandle]
+OCI_DestroyCANRxQueue.restype = OCI_ErrorCode
+OCI_DestroyCANRxQueue.errcheck = errcheck_oper
+
+OCI_CreateCANTxQueue = _oci.OCI_CreateCANTxQueue
+OCI_CreateCANTxQueue.argtypes = [
+ OCI_ControllerHandle,
+ ctypes.POINTER(OCI_CANTxQueueConfiguration),
+ ctypes.POINTER(OCI_QueueHandle),
+]
+OCI_CreateCANTxQueue.restype = OCI_ErrorCode
+OCI_CreateCANTxQueue.errcheck = errcheck_init
+
+OCI_DestroyCANTxQueue = _oci.OCI_DestroyCANTxQueue
+OCI_DestroyCANTxQueue.argtypes = [OCI_QueueHandle]
+OCI_DestroyCANTxQueue.restype = OCI_ErrorCode
+OCI_DestroyCANTxQueue.errcheck = errcheck_oper
+
+OCI_WriteCANDataEx = _oci.OCI_WriteCANDataEx
+OCI_WriteCANDataEx.argtypes = [
+ OCI_QueueHandle,
+ OCI_Time,
+ ctypes.POINTER(ctypes.POINTER(OCI_CANMessageEx)),
+ ctypes.c_uint32,
+ ctypes.POINTER(ctypes.c_uint32),
+]
+OCI_WriteCANDataEx.restype = OCI_ErrorCode
+OCI_WriteCANDataEx.errcheck = errcheck_oper
+
+OCI_ReadCANDataEx = _oci.OCI_ReadCANDataEx
+OCI_ReadCANDataEx.argtypes = [
+ OCI_QueueHandle,
+ OCI_Time,
+ ctypes.POINTER(ctypes.POINTER(OCI_CANMessageEx)),
+ ctypes.c_uint32,
+ ctypes.POINTER(ctypes.c_uint32),
+ ctypes.POINTER(ctypes.c_uint32),
+]
+OCI_ReadCANDataEx.restype = OCI_ErrorCode
+OCI_ReadCANDataEx.errcheck = errcheck_oper
diff --git a/can/interfaces/gs_usb.py b/can/interfaces/gs_usb.py
new file mode 100644
index 000000000..6297fc1f5
--- /dev/null
+++ b/can/interfaces/gs_usb.py
@@ -0,0 +1,178 @@
+import logging
+
+import usb
+from gs_usb.constants import CAN_EFF_FLAG, CAN_ERR_FLAG, CAN_MAX_DLC, CAN_RTR_FLAG
+from gs_usb.gs_usb import GsUsb
+from gs_usb.gs_usb_frame import GS_USB_NONE_ECHO_ID, GsUsbFrame
+
+import can
+
+from ..exceptions import CanInitializationError, CanOperationError
+
+logger = logging.getLogger(__name__)
+
+
+class GsUsbBus(can.BusABC):
+ def __init__(
+ self,
+ channel,
+ bitrate: int = 500_000,
+ index=None,
+ bus=None,
+ address=None,
+ can_filters=None,
+ **kwargs,
+ ):
+ """
+ :param channel: usb device name
+ :param index: device number if using automatic scan, starting from 0.
+ If specified, bus/address shall not be provided.
+ :param bus: number of the bus that the device is connected to
+ :param address: address of the device on the bus it is connected to
+ :param can_filters: not supported
+ :param bitrate: CAN network bandwidth (bits/s)
+ """
+ self._is_shutdown = False
+ if (index is not None) and ((bus or address) is not None):
+ raise CanInitializationError(
+ "index and bus/address cannot be used simultaneously"
+ )
+
+ if index is None and address is None and bus is None:
+ index = channel
+
+ self._index = None
+ if index is not None:
+ devs = GsUsb.scan()
+ if len(devs) <= index:
+ raise CanInitializationError(
+ f"Cannot find device {index}. Devices found: {len(devs)}"
+ )
+ gs_usb = devs[index]
+ self._index = index
+ else:
+ gs_usb = GsUsb.find(bus=bus, address=address)
+ if not gs_usb:
+ raise CanInitializationError(f"Cannot find device {channel}")
+
+ self.gs_usb = gs_usb
+ self.channel_info = channel
+ self._can_protocol = can.CanProtocol.CAN_20
+
+ bit_timing = can.BitTiming.from_sample_point(
+ f_clock=self.gs_usb.device_capability.fclk_can,
+ bitrate=bitrate,
+ sample_point=87.5,
+ )
+ props_seg = 1
+ self.gs_usb.set_timing(
+ prop_seg=props_seg,
+ phase_seg1=bit_timing.tseg1 - props_seg,
+ phase_seg2=bit_timing.tseg2,
+ sjw=bit_timing.sjw,
+ brp=bit_timing.brp,
+ )
+ self.gs_usb.start()
+ self._bitrate = bitrate
+
+ super().__init__(
+ channel=channel,
+ can_filters=can_filters,
+ **kwargs,
+ )
+
+ def send(self, msg: can.Message, timeout: float | None = None):
+ """Transmit a message to the CAN bus.
+
+ :param Message msg: A message object.
+ :param timeout: timeout is not supported.
+ The function won't return until message is sent or exception is raised.
+
+ :raises CanOperationError:
+ if the message could not be sent
+ """
+ can_id = msg.arbitration_id
+
+ if msg.is_extended_id:
+ can_id = can_id | CAN_EFF_FLAG
+
+ if msg.is_remote_frame:
+ can_id = can_id | CAN_RTR_FLAG
+
+ if msg.is_error_frame:
+ can_id = can_id | CAN_ERR_FLAG
+
+ # Pad message data
+ msg.data.extend([0x00] * (CAN_MAX_DLC - len(msg.data)))
+
+ frame = GsUsbFrame()
+ frame.can_id = can_id
+ frame.can_dlc = msg.dlc
+ frame.timestamp_us = 0 # timestamp frame field is only useful on receive
+ frame.data = list(msg.data)
+
+ try:
+ self.gs_usb.send(frame)
+ except usb.core.USBError as exc:
+ raise CanOperationError("The message could not be sent") from exc
+
+ def _recv_internal(self, timeout: float | None) -> tuple[can.Message | None, bool]:
+ """
+ Read a message from the bus and tell whether it was filtered.
+ This methods may be called by :meth:`~can.BusABC.recv`
+ to read a message multiple times if the filters set by
+ :meth:`~can.BusABC.set_filters` do not match and the call has
+ not yet timed out.
+
+ Never raises an error/exception.
+
+ :param float timeout: seconds to wait for a message,
+ see :meth:`~can.BusABC.send`
+ 0 and None will be converted to minimum value 1ms.
+
+ :return:
+ 1. a message that was read or None on timeout
+ 2. a bool that is True if message filtering has already
+ been done and else False. In this interface it is always False
+ since filtering is not available
+ """
+ frame = GsUsbFrame()
+
+ # Do not set timeout as None or zero here to avoid blocking
+ timeout_ms = round(timeout * 1000) if timeout else 1
+ if not self.gs_usb.read(frame=frame, timeout_ms=timeout_ms):
+ return None, False
+
+ msg = can.Message(
+ timestamp=frame.timestamp,
+ arbitration_id=frame.arbitration_id,
+ is_extended_id=frame.is_extended_id,
+ is_remote_frame=frame.is_remote_frame,
+ is_error_frame=frame.is_error_frame,
+ channel=self.channel_info,
+ dlc=frame.can_dlc,
+ data=bytearray(frame.data)[0 : frame.can_dlc],
+ is_rx=frame.echo_id == GS_USB_NONE_ECHO_ID,
+ )
+
+ return msg, False
+
+ def shutdown(self):
+ if self._is_shutdown:
+ return
+
+ super().shutdown()
+ self.gs_usb.stop()
+ if self._index is not None:
+ # Avoid errors on subsequent __init() by repeating the .scan() and .start() that would otherwise fail
+ # the next time the device is opened in __init__()
+ devs = GsUsb.scan()
+ if self._index < len(devs):
+ gs_usb = devs[self._index]
+ try:
+ gs_usb.set_bitrate(self._bitrate)
+ gs_usb.start()
+ gs_usb.stop()
+ except usb.core.USBError:
+ pass
+ self._is_shutdown = True
diff --git a/can/interfaces/ics_neovi/__init__.py b/can/interfaces/ics_neovi/__init__.py
index 9e9f2b0ba..74cb43af4 100644
--- a/can/interfaces/ics_neovi/__init__.py
+++ b/can/interfaces/ics_neovi/__init__.py
@@ -1,7 +1,11 @@
-#!/usr/bin/env python
-# coding: utf-8
+""" """
-"""
-"""
+__all__ = [
+ "ICSApiError",
+ "ICSInitializationError",
+ "ICSOperationError",
+ "NeoViBus",
+ "neovi_bus",
+]
-from can.interfaces.ics_neovi.neovi_bus import NeoViBus
+from .neovi_bus import ICSApiError, ICSInitializationError, ICSOperationError, NeoViBus
diff --git a/can/interfaces/ics_neovi/neovi_bus.py b/can/interfaces/ics_neovi/neovi_bus.py
index 5928e91d6..815ed6fa0 100644
--- a/can/interfaces/ics_neovi/neovi_bus.py
+++ b/can/interfaces/ics_neovi/neovi_bus.py
@@ -1,20 +1,32 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
-ICS NeoVi interface module.
+Intrepid Control Systems (ICS) neoVI interface module.
python-ics is a Python wrapper around the API provided by Intrepid Control
-Systems for communicating with their NeoVI range of devices.
+Systems for communicating with their neoVI range of devices.
Implementation references:
* https://github.com/intrepidcs/python_ics
"""
+import functools
import logging
-from collections import deque
-
-from can import Message, CanError, BusABC
+import os
+import tempfile
+from collections import Counter, defaultdict, deque
+from datetime import datetime
+from functools import partial
+from itertools import cycle
+from threading import Event
+from warnings import warn
+
+from can import BusABC, CanProtocol, Message
+
+from ...exceptions import (
+ CanError,
+ CanInitializationError,
+ CanOperationError,
+ CanTimeoutError,
+)
logger = logging.getLogger(__name__)
@@ -22,12 +34,45 @@
import ics
except ImportError as ie:
logger.warning(
- "You won't be able to use the ICS NeoVi can backend without the "
- "python-ics module installed!: %s", ie
+ "You won't be able to use the ICS neoVI can backend without the "
+ "python-ics module installed!: %s",
+ ie,
)
ics = None
+try:
+ from filelock import FileLock
+except ImportError as ie:
+ logger.warning(
+ "Using ICS neoVI can backend without the "
+ "filelock module installed may cause some issues!: %s",
+ ie,
+ )
+
+ class FileLock:
+ """Dummy file lock that does not actually do anything"""
+
+ def __init__(self, lock_file, timeout=-1):
+ self._lock_file = lock_file
+ self.timeout = timeout
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ return None
+
+
+# Use inter-process mutex to prevent concurrent device open.
+# When neoVI server is enabled, there is an issue with concurrent device open.
+open_lock = FileLock(os.path.join(tempfile.gettempdir(), "neovi.lock"))
+description_id = cycle(range(1, 0x8000))
+
+ICS_EPOCH = datetime.fromisoformat("2007-01-01")
+ICS_EPOCH_DELTA = (ICS_EPOCH - datetime.fromisoformat("1970-01-01")).total_seconds()
+
+
class ICSApiError(CanError):
"""
Indicates an error with the ICS API.
@@ -43,38 +88,91 @@ class ICSApiError(CanError):
ICS_SPY_ERR_INFORMATION = 0x40
def __init__(
- self, error_number, description_short, description_long,
- severity, restart_needed
+ self,
+ error_code: int,
+ description_short: str,
+ description_long: str,
+ severity: int,
+ restart_needed: int,
):
- super(ICSApiError, self).__init__(description_short)
- self.error_number = error_number
+ super().__init__(f"{description_short}. {description_long}", error_code)
self.description_short = description_short
self.description_long = description_long
self.severity = severity
self.restart_needed = restart_needed == 1
- def __str__(self):
- return "{} {}".format(self.description_short, self.description_long)
+ def __reduce__(self):
+ return type(self), (
+ self.error_code,
+ self.description_short,
+ self.description_long,
+ self.severity,
+ self.restart_needed,
+ )
@property
- def is_critical(self):
+ def error_number(self) -> int:
+ """Deprecated. Renamed to :attr:`can.CanError.error_code`."""
+ warn(
+ "ICSApiError::error_number has been replaced by ICSApiError.error_code in python-can 4.0"
+ "and will be remove in version 5.0.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self.error_code
+
+ @property
+ def is_critical(self) -> bool:
return self.severity == self.ICS_SPY_ERR_CRITICAL
+class ICSInitializationError(ICSApiError, CanInitializationError):
+ pass
+
+
+class ICSOperationError(ICSApiError, CanOperationError):
+ pass
+
+
+def check_if_bus_open(func):
+ """
+ Decorator that checks if the bus is open before executing the function.
+
+ If the bus is not open, it raises a CanOperationError.
+ """
+
+ @functools.wraps(func)
+ def wrapper(self, *args, **kwargs):
+ """
+ Wrapper function that checks if the bus is open before executing the function.
+
+ :raises CanOperationError: If the bus is not open.
+ """
+ if self._is_shutdown:
+ raise CanOperationError("Cannot operate on a closed bus")
+ return func(self, *args, **kwargs)
+
+ return wrapper
+
+
class NeoViBus(BusABC):
"""
The CAN Bus implemented for the python_ics interface
https://github.com/intrepidcs/python_ics
"""
- def __init__(self, channel, can_filters=None, **config):
+ def __init__(self, channel, can_filters=None, **kwargs):
"""
-
- :param int channel:
- The Channel id or name to create this bus with.
+ :param channel:
+ The channel ids to create this bus with.
+ Can also be a single integer, netid name or a comma separated
+ string.
+ :type channel: int or str or list(int) or list(str)
:param list can_filters:
See :meth:`can.BusABC.set_filters` for details.
- :param use_system_timestamp:
+ :param bool receive_own_messages:
+ If transmitted messages should also be received by this bus.
+ :param bool use_system_timestamp:
Use system timestamp for can messages instead of the hardware time
stamp
:param str serial:
@@ -88,52 +186,92 @@ def __init__(self, channel, can_filters=None, **config):
:param int data_bitrate:
Which bitrate to use for data phase in CAN FD.
Defaults to arbitration bitrate.
+ :param override_library_name:
+ Absolute path or relative path to the library including filename.
+
+ :raise ImportError:
+ If *python-ics* is not available
+ :raise CanInitializationError:
+ If the bus could not be set up.
+ May or may not be a :class:`~ICSInitializationError`.
"""
if ics is None:
- raise ImportError('Please install python-ics')
+ raise ImportError("Please install python-ics")
- super(NeoViBus, self).__init__(
- channel=channel, can_filters=can_filters, **config)
+ super().__init__(
+ channel=channel,
+ can_filters=can_filters,
+ **kwargs,
+ )
- logger.info("CAN Filters: {}".format(can_filters))
- logger.info("Got configuration of: {}".format(config))
+ logger.info(f"CAN Filters: {can_filters}")
+ logger.info(f"Got configuration of: {kwargs}")
- self._use_system_timestamp = bool(
- config.get('use_system_timestamp', False)
- )
- try:
- channel = int(channel)
- except ValueError:
- channel = getattr(ics, "NETID_{}".format(channel.upper()), -1)
- if channel == -1:
- raise ValueError(
- 'channel must be an integer or '
- 'a valid ICS channel name'
- )
+ if "override_library_name" in kwargs:
+ ics.override_library_name(kwargs.get("override_library_name"))
- type_filter = config.get('type_filter')
- serial = config.get('serial')
+ if isinstance(channel, (list, tuple)):
+ self.channels = channel
+ elif isinstance(channel, int):
+ self.channels = [channel]
+ else:
+ # Assume comma separated string of channels
+ self.channels = [ch.strip() for ch in channel.split(",")]
+ self.channels = [NeoViBus.channel_to_netid(ch) for ch in self.channels]
+
+ type_filter = kwargs.get("type_filter")
+ serial = kwargs.get("serial")
self.dev = self._find_device(type_filter, serial)
- ics.open_device(self.dev)
- if 'bitrate' in config:
- ics.set_bit_rate(self.dev, config.get('bitrate'), channel)
+ is_fd = kwargs.get("fd", False)
+ self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20
- fd = config.get('fd', False)
- if fd:
- if 'data_bitrate' in config:
- ics.set_fd_bit_rate(
- self.dev, config.get('data_bitrate'), channel)
+ with open_lock:
+ ics.open_device(self.dev)
- self.channel_info = '%s %s CH:%s' % (
- self.dev.Name,
- self.get_serial_number(self.dev),
- channel
+ try:
+ if "bitrate" in kwargs:
+ for channel in self.channels:
+ ics.set_bit_rate(self.dev, kwargs.get("bitrate"), channel)
+
+ if is_fd:
+ if "data_bitrate" in kwargs:
+ for channel in self.channels:
+ ics.set_fd_bit_rate(
+ self.dev, kwargs.get("data_bitrate"), channel
+ )
+ except ics.RuntimeError as re:
+ logger.error(re)
+ err = ICSInitializationError(*ics.get_last_api_error(self.dev))
+ try:
+ self.shutdown()
+ finally:
+ raise err
+
+ self._use_system_timestamp = bool(kwargs.get("use_system_timestamp", False))
+ self._receive_own_messages = kwargs.get("receive_own_messages", True)
+
+ self.channel_info = (
+ f"{self.dev.Name} {self.get_serial_number(self.dev)} CH:{self.channels}"
)
- logger.info("Using device: {}".format(self.channel_info))
+ logger.info(f"Using device: {self.channel_info}")
self.rx_buffer = deque()
- self.network = channel if channel is not None else None
+ self.message_receipts = defaultdict(Event)
+
+ @staticmethod
+ def channel_to_netid(channel_name_or_id):
+ try:
+ channel = int(channel_name_or_id)
+ except ValueError:
+ netid = f"NETID_{channel_name_or_id.upper()}"
+ if hasattr(ics, netid):
+ channel = getattr(ics, netid)
+ else:
+ raise ValueError(
+ "channel must be an integer or a valid ICS channel name"
+ ) from None
+ return channel
@staticmethod
def get_serial_number(device):
@@ -143,13 +281,13 @@ def get_serial_number(device):
:return: ics device serial string
:rtype: str
"""
- a0000 = 604661760
- if device.SerialNumber >= a0000:
+ if int("0A0000", 36) < device.SerialNumber < int("ZZZZZZ", 36):
return ics.base36enc(device.SerialNumber)
- return str(device.SerialNumber)
+ else:
+ return str(device.SerialNumber)
def shutdown(self):
- super(NeoViBus, self).shutdown()
+ super().shutdown()
ics.close_device(self.dev)
@staticmethod
@@ -171,12 +309,17 @@ def _detect_available_configs():
return []
# TODO: add the channel(s)
- return [{
- 'interface': 'neovi',
- 'serial': NeoViBus.get_serial_number(device)
- } for device in devices]
+ return [
+ {"interface": "neovi", "serial": NeoViBus.get_serial_number(device)}
+ for device in devices
+ ]
def _find_device(self, type_filter=None, serial=None):
+ """Returns the first matching device or raises an error.
+
+ :raise CanInitializationError:
+ If not matching device could be found
+ """
if type_filter is not None:
devices = ics.find_devices(type_filter)
else:
@@ -184,34 +327,50 @@ def _find_device(self, type_filter=None, serial=None):
for device in devices:
if serial is None or self.get_serial_number(device) == str(serial):
- dev = device
- break
- else:
- msg = ['No device']
+ return device
- if type_filter is not None:
- msg.append('with type {}'.format(type_filter))
- if serial is not None:
- msg.append('with serial {}'.format(serial))
- msg.append('found.')
- raise Exception(' '.join(msg))
- return dev
+ msg = ["No device"]
+
+ if type_filter is not None:
+ msg.append(f"with type {type_filter}")
+ if serial is not None:
+ msg.append(f"with serial {serial}")
+ msg.append("found.")
+ raise CanInitializationError(" ".join(msg))
+ @check_if_bus_open
def _process_msg_queue(self, timeout=0.1):
try:
messages, errors = ics.get_messages(self.dev, False, timeout)
except ics.RuntimeError:
return
for ics_msg in messages:
- if ics_msg.NetworkID != self.network:
+ channel = ics_msg.NetworkID | (ics_msg.NetworkID2 << 8)
+ if channel not in self.channels:
continue
+
+ is_tx = bool(ics_msg.StatusBitField & ics.SPY_STATUS_TX_MSG)
+
+ if is_tx:
+ if bool(ics_msg.StatusBitField & ics.SPY_STATUS_GLOBAL_ERR):
+ continue
+
+ receipt_key = (ics_msg.ArbIDOrHeader, ics_msg.DescriptionID)
+ if ics_msg.DescriptionID and receipt_key in self.message_receipts:
+ self.message_receipts[receipt_key].set()
+ if not self._receive_own_messages:
+ continue
+
self.rx_buffer.append(ics_msg)
if errors:
- logger.warning("%d error(s) found" % errors)
+ logger.warning("%d error(s) found", errors)
- for msg in ics.get_error_messages(self.dev):
+ for msg, count in Counter(ics.get_error_messages(self.dev)).items():
error = ICSApiError(*msg)
- logger.warning(error)
+ if count > 1:
+ logger.warning(f"{error} (Repeated {count} times)")
+ else:
+ logger.warning(error)
def _get_timestamp_for_msg(self, ics_msg):
if self._use_system_timestamp:
@@ -229,51 +388,42 @@ def _get_timestamp_for_msg(self, ics_msg):
return ics_msg.TimeSystem
else:
# This is the hardware time stamp.
- return ics.get_timestamp_for_msg(self.dev, ics_msg)
+ return ics.get_timestamp_for_msg(self.dev, ics_msg) + ICS_EPOCH_DELTA
def _ics_msg_to_message(self, ics_msg):
is_fd = ics_msg.Protocol == ics.SPY_PROTOCOL_CANFD
+ message_from_ics = partial(
+ Message,
+ timestamp=self._get_timestamp_for_msg(ics_msg),
+ arbitration_id=ics_msg.ArbIDOrHeader,
+ is_extended_id=bool(ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME),
+ is_remote_frame=bool(ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME),
+ is_error_frame=bool(ics_msg.StatusBitField2 & ics.SPY_STATUS2_ERROR_FRAME),
+ channel=ics_msg.NetworkID | (ics_msg.NetworkID2 << 8),
+ dlc=ics_msg.NumberBytesData,
+ is_fd=is_fd,
+ is_rx=not bool(ics_msg.StatusBitField & ics.SPY_STATUS_TX_MSG),
+ )
+
if is_fd:
if ics_msg.ExtraDataPtrEnabled:
- data = ics_msg.ExtraDataPtr[:ics_msg.NumberBytesData]
+ data = ics_msg.ExtraDataPtr[: ics_msg.NumberBytesData]
else:
- data = ics_msg.Data[:ics_msg.NumberBytesData]
+ data = ics_msg.Data[: ics_msg.NumberBytesData]
- return Message(
- timestamp=self._get_timestamp_for_msg(ics_msg),
- arbitration_id=ics_msg.ArbIDOrHeader,
+ return message_from_ics(
data=data,
- dlc=ics_msg.NumberBytesData,
- extended_id=bool(
- ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME
- ),
- is_fd=is_fd,
- is_remote_frame=bool(
- ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME
- ),
error_state_indicator=bool(
ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_ESI
),
bitrate_switch=bool(
ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_BRS
),
- channel=ics_msg.NetworkID
)
else:
- return Message(
- timestamp=self._get_timestamp_for_msg(ics_msg),
- arbitration_id=ics_msg.ArbIDOrHeader,
- data=ics_msg.Data[:ics_msg.NumberBytesData],
- dlc=ics_msg.NumberBytesData,
- extended_id=bool(
- ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME
- ),
- is_fd=is_fd,
- is_remote_frame=bool(
- ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME
- ),
- channel=ics_msg.NetworkID
+ return message_from_ics(
+ data=ics_msg.Data[: ics_msg.NumberBytesData],
)
def _recv_internal(self, timeout=0.1):
@@ -286,9 +436,39 @@ def _recv_internal(self, timeout=0.1):
return None, False
return msg, False
- def send(self, msg, timeout=None):
+ @check_if_bus_open
+ def send(self, msg, timeout=0):
+ """Transmit a message to the CAN bus.
+
+ :param Message msg: A message object.
+
+ :param float timeout:
+ If > 0, wait up to this many seconds for message to be ACK'ed.
+ If timeout is exceeded, an exception will be raised.
+ None blocks indefinitely.
+
+ :raises ValueError:
+ if the message is invalid
+ :raises can.CanTimeoutError:
+ if sending timed out
+ :raises CanOperationError:
+ If the bus is closed or the message could otherwise not be sent.
+ May or may not be a :class:`~ICSOperationError`.
+ """
if not ics.validate_hobject(self.dev):
- raise CanError("bus not open")
+ raise CanOperationError("bus not open")
+
+ # Check for valid DLC to avoid passing extra long data to the driver
+ if msg.is_fd:
+ if msg.dlc > 64:
+ raise ValueError(
+ f"DLC was {msg.dlc} but it should be <= 64 for CAN FD frames"
+ )
+ elif msg.dlc > 8:
+ raise ValueError(
+ f"DLC was {msg.dlc} but it should be <= 8 for normal CAN frames"
+ )
+
message = ics.SpyMessage()
flag0 = 0
@@ -306,17 +486,43 @@ def send(self, msg, timeout=None):
flag3 |= ics.SPY_STATUS3_CANFD_ESI
message.ArbIDOrHeader = msg.arbitration_id
- message.NumberBytesData = len(msg.data)
- message.Data = tuple(msg.data[:8])
- if msg.is_fd and len(msg.data) > 8:
+ msg_data = msg.data[: msg.dlc]
+ message.NumberBytesData = msg.dlc
+ message.Data = tuple(msg_data[:8])
+ if msg.is_fd and len(msg_data) > 8:
message.ExtraDataPtrEnabled = 1
- message.ExtraDataPtr = tuple(msg.data)
+ message.ExtraDataPtr = tuple(msg_data)
message.StatusBitField = flag0
message.StatusBitField2 = 0
message.StatusBitField3 = flag3
- message.NetworkID = self.network
+ if msg.channel is not None:
+ network_id = msg.channel
+ elif len(self.channels) == 1:
+ network_id = self.channels[0]
+ else:
+ raise ValueError("msg.channel must be set when using multiple channels.")
+
+ message.NetworkID, message.NetworkID2 = int(network_id & 0xFF), int(
+ (network_id >> 8) & 0xFF
+ )
+
+ if timeout != 0:
+ msg_desc_id = next(description_id)
+ message.DescriptionID = msg_desc_id
+ receipt_key = (msg.arbitration_id, msg_desc_id)
+ self.message_receipts[receipt_key].clear()
try:
ics.transmit_messages(self.dev, message)
except ics.RuntimeError:
- raise ICSApiError(*ics.get_last_api_error(self.dev))
+ raise ICSOperationError(*ics.get_last_api_error(self.dev)) from None
+
+ # If timeout is set, wait for ACK
+ # This requires a notifier for the bus or
+ # some other thread calling recv periodically
+ if timeout != 0:
+ got_receipt = self.message_receipts[receipt_key].wait(timeout)
+ # We no longer need this receipt, so no point keeping it in memory
+ del self.message_receipts[receipt_key]
+ if not got_receipt:
+ raise CanTimeoutError("Transmit timeout")
diff --git a/can/interfaces/iscan.py b/can/interfaces/iscan.py
index 232447f84..2fa19942a 100644
--- a/can/interfaces/iscan.py
+++ b/can/interfaces/iscan.py
@@ -1,17 +1,20 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
-Interface for isCAN from Thorsis Technologies GmbH, former ifak system GmbH.
+Interface for isCAN from *Thorsis Technologies GmbH*, former *ifak system GmbH*.
"""
-from __future__ import absolute_import, division
-
import ctypes
-import time
import logging
+import time
-from can import CanError, BusABC, Message
+from can import (
+ BusABC,
+ CanError,
+ CanInitializationError,
+ CanInterfaceNotImplementedError,
+ CanOperationError,
+ CanProtocol,
+ Message,
+)
logger = logging.getLogger(__name__)
@@ -28,9 +31,15 @@ class MessageExStruct(ctypes.Structure):
]
-def check_status(result, function, arguments):
+def check_status_initialization(result: int, function, arguments) -> int:
if result > 0:
- raise IscanError(function, result, arguments)
+ raise IscanInitializationError(function, result, arguments)
+ return result
+
+
+def check_status(result: int, function, arguments) -> int:
+ if result > 0:
+ raise IscanOperationError(function, result, arguments)
return result
@@ -41,12 +50,15 @@ def check_status(result, function, arguments):
logger.warning("Failed to load IS-CAN driver: %s", e)
else:
iscan.isCAN_DeviceInitEx.argtypes = [ctypes.c_ubyte, ctypes.c_ubyte]
- iscan.isCAN_DeviceInitEx.errcheck = check_status
+ iscan.isCAN_DeviceInitEx.errcheck = check_status_initialization
iscan.isCAN_DeviceInitEx.restype = ctypes.c_ubyte
+
iscan.isCAN_ReceiveMessageEx.errcheck = check_status
iscan.isCAN_ReceiveMessageEx.restype = ctypes.c_ubyte
+
iscan.isCAN_TransmitMessageEx.errcheck = check_status
iscan.isCAN_TransmitMessageEx.restype = ctypes.c_ubyte
+
iscan.isCAN_CloseDevice.errcheck = check_status
iscan.isCAN_CloseDevice.restype = ctypes.c_ubyte
@@ -64,42 +76,52 @@ class IscanBus(BusABC):
250000: 6,
500000: 7,
800000: 8,
- 1000000: 9
+ 1000000: 9,
}
- def __init__(self, channel, bitrate=500000, poll_interval=0.01, **kwargs):
+ def __init__(
+ self,
+ channel: str | int,
+ bitrate: int = 500000,
+ poll_interval: float = 0.01,
+ **kwargs,
+ ) -> None:
"""
- :param int channel:
+ :param channel:
Device number
- :param int bitrate:
+ :param bitrate:
Bitrate in bits/s
- :param float poll_interval:
+ :param poll_interval:
Poll interval in seconds when reading messages
"""
if iscan is None:
- raise ImportError("Could not load isCAN driver")
+ raise CanInterfaceNotImplementedError("Could not load isCAN driver")
self.channel = ctypes.c_ubyte(int(channel))
- self.channel_info = "IS-CAN: %s" % channel
+ self.channel_info = f"IS-CAN: {self.channel}"
+ self._can_protocol = CanProtocol.CAN_20
if bitrate not in self.BAUDRATES:
- valid_bitrates = ", ".join(str(bitrate) for bitrate in self.BAUDRATES)
- raise ValueError("Invalid bitrate, choose one of " + valid_bitrates)
+ raise ValueError(f"Invalid bitrate, choose one of {set(self.BAUDRATES)}")
self.poll_interval = poll_interval
iscan.isCAN_DeviceInitEx(self.channel, self.BAUDRATES[bitrate])
- super(IscanBus, self).__init__(channel=channel, bitrate=bitrate,
- poll_interval=poll_interval, **kwargs)
+ super().__init__(
+ channel=channel,
+ bitrate=bitrate,
+ poll_interval=poll_interval,
+ **kwargs,
+ )
- def _recv_internal(self, timeout):
+ def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]:
raw_msg = MessageExStruct()
end_time = time.time() + timeout if timeout is not None else None
while True:
try:
iscan.isCAN_ReceiveMessageEx(self.channel, ctypes.byref(raw_msg))
except IscanError as e:
- if e.error_code != 8:
+ if e.error_code != 8: # "No message received"
# An error occurred
raise
if end_time is not None and time.time() > end_time:
@@ -111,31 +133,35 @@ def _recv_internal(self, timeout):
# A message was received
break
- msg = Message(arbitration_id=raw_msg.message_id,
- extended_id=bool(raw_msg.is_extended),
- timestamp=time.time(), # Better than nothing...
- is_remote_frame=bool(raw_msg.remote_req),
- dlc=raw_msg.data_len,
- data=raw_msg.data[:raw_msg.data_len],
- channel=self.channel.value)
+ msg = Message(
+ arbitration_id=raw_msg.message_id,
+ is_extended_id=bool(raw_msg.is_extended),
+ timestamp=time.time(), # Better than nothing...
+ is_remote_frame=bool(raw_msg.remote_req),
+ dlc=raw_msg.data_len,
+ data=raw_msg.data[: raw_msg.data_len],
+ channel=self.channel.value,
+ )
return msg, False
- def send(self, msg, timeout=None):
- raw_msg = MessageExStruct(msg.arbitration_id,
- bool(msg.is_extended_id),
- bool(msg.is_remote_frame),
- msg.dlc,
- CanData(*msg.data))
+ def send(self, msg: Message, timeout: float | None = None) -> None:
+ raw_msg = MessageExStruct(
+ msg.arbitration_id,
+ bool(msg.is_extended_id),
+ bool(msg.is_remote_frame),
+ msg.dlc,
+ CanData(*msg.data),
+ )
iscan.isCAN_TransmitMessageEx(self.channel, ctypes.byref(raw_msg))
- def shutdown(self):
+ def shutdown(self) -> None:
+ super().shutdown()
iscan.isCAN_CloseDevice(self.channel)
class IscanError(CanError):
- # TODO: document
-
ERROR_CODES = {
+ 0: "Success",
1: "No access to device",
2: "Device with ID not found",
3: "Driver operation failed",
@@ -148,6 +174,7 @@ class IscanError(CanError):
10: "Thread already started",
11: "Buffer overrun",
12: "Device not initialized",
+ 15: "Found the device, but it is being used by another process",
16: "Bus error",
17: "Bus off",
18: "Error passive",
@@ -157,19 +184,31 @@ class IscanError(CanError):
31: "Transmission not acknowledged on bus",
32: "Error critical bus",
35: "Callbackthread is blocked, stopping thread failed",
- 40: "Need a licence number under NT4"
+ 40: "Need a licence number under NT4",
}
- def __init__(self, function, error_code, arguments):
- super(IscanError, self).__init__()
- # :Status code
+ def __init__(self, function, error_code: int, arguments) -> None:
+ try:
+ description = ": " + self.ERROR_CODES[error_code]
+ except KeyError:
+ description = ""
+
+ super().__init__(
+ f"Function {function.__name__} failed{description}",
+ error_code=error_code,
+ )
+
+ #: Status code
self.error_code = error_code
- # :Function that failed
+ #: Function that failed
self.function = function
- # :Arguments passed to function
+ #: Arguments passed to function
self.arguments = arguments
- def __str__(self):
- description = self.ERROR_CODES.get(self.error_code,
- "Error code %d" % self.error_code)
- return "Function %s failed: %s" % (self.function.__name__, description)
+
+class IscanOperationError(IscanError, CanOperationError):
+ pass
+
+
+class IscanInitializationError(IscanError, CanInitializationError):
+ pass
diff --git a/can/interfaces/ixxat/__init__.py b/can/interfaces/ixxat/__init__.py
index ab4e1f08c..6fe79adb8 100644
--- a/can/interfaces/ixxat/__init__.py
+++ b/can/interfaces/ixxat/__init__.py
@@ -1,10 +1,21 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
-Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems
+Ctypes wrapper module for IXXAT Virtual CAN Interface V4 on win32 systems
-Copyright (C) 2016 Giuseppe Corbelli
+Copyright (C) 2016-2021 Giuseppe Corbelli
"""
+__all__ = [
+ "IXXATBus",
+ "canlib",
+ "canlib_vcinpl",
+ "canlib_vcinpl2",
+ "constants",
+ "exceptions",
+ "get_ixxat_hwids",
+ "structures",
+]
+
from can.interfaces.ixxat.canlib import IXXATBus
+
+# import this and not the one from vcinpl2 for backward compatibility
+from can.interfaces.ixxat.canlib_vcinpl import get_ixxat_hwids
diff --git a/can/interfaces/ixxat/canlib.py b/can/interfaces/ixxat/canlib.py
index b087fe41e..528e86d5e 100644
--- a/can/interfaces/ixxat/canlib.py
+++ b/can/interfaces/ixxat/canlib.py
@@ -1,576 +1,177 @@
-#!/usr/bin/env python
-# coding: utf-8
+from collections.abc import Callable, Sequence
-"""
-Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems
-
-Copyright (C) 2016 Giuseppe Corbelli
-
-TODO: We could implement this interface such that setting other filters
- could work when the initial filters were set to zero using the
- software fallback. Or could the software filters even be changed
- after the connection was opened? We need to document that bahaviour!
- See also the NICAN interface.
-
-"""
-
-from __future__ import absolute_import, division
-
-import ctypes
-import functools
-import logging
-import sys
-
-from can import CanError, BusABC, Message
-from can.broadcastmanager import (LimitedDurationCyclicSendTaskABC,
- RestartableCyclicTaskABC)
-from can.ctypesutil import CLibrary, HANDLE, PHANDLE, HRESULT as ctypes_HRESULT
-
-from . import constants, structures
-from .exceptions import *
-
-__all__ = ["VCITimeout", "VCIError", "VCIDeviceNotFoundError", "IXXATBus", "vciFormatError"]
-
-log = logging.getLogger('can.ixxat')
-
-try:
- # since Python 3.3
- from time import perf_counter as _timer_function
-except ImportError:
- from time import clock as _timer_function
-
-# Hack to have vciFormatError as a free function, see below
-vciFormatError = None
-
-# main ctypes instance
-_canlib = None
-if sys.platform == "win32":
- try:
- _canlib = CLibrary("vcinpl")
- except Exception as e:
- log.warning("Cannot load IXXAT vcinpl library: %s", e)
-elif sys.platform == "cygwin":
- try:
- _canlib = CLibrary("vcinpl.dll")
- except Exception as e:
- log.warning("Cannot load IXXAT vcinpl library: %s", e)
-else:
- # Will not work on other systems, but have it importable anyway for
- # tests/sphinx
- log.warning("IXXAT VCI library does not work on %s platform", sys.platform)
-
-
-def __vciFormatErrorExtended(library_instance, function, HRESULT, arguments):
- """ Format a VCI error and attach failed function, decoded HRESULT and arguments
- :param CLibrary library_instance:
- Mapped instance of IXXAT vcinpl library
- :param callable function:
- Failed function
- :param HRESULT HRESULT:
- HRESULT returned by vcinpl call
- :param arguments:
- Arbitrary arguments tuple
- :return:
- Formatted string
- """
- #TODO: make sure we don't generate another exception
- return "{} - arguments were {}".format(
- __vciFormatError(library_instance, function, HRESULT),
- arguments
- )
-
-
-def __vciFormatError(library_instance, function, HRESULT):
- """ Format a VCI error and attach failed function and decoded HRESULT
- :param CLibrary library_instance:
- Mapped instance of IXXAT vcinpl library
- :param callable function:
- Failed function
- :param HRESULT HRESULT:
- HRESULT returned by vcinpl call
- :return:
- Formatted string
- """
- buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN)
- ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN)
- library_instance.vciFormatError(HRESULT, buf, constants.VCI_MAX_ERRSTRLEN)
- return "function {} failed ({})".format(function._name, buf.value.decode('utf-8', 'replace'))
-
-
-def __check_status(result, function, arguments):
- """
- Check the result of a vcinpl function call and raise appropriate exception
- in case of an error. Used as errcheck function when mapping C functions
- with ctypes.
- :param result:
- Function call numeric result
- :param callable function:
- Called function
- :param arguments:
- Arbitrary arguments tuple
- :raise:
- :class:VCITimeout
- :class:VCIRxQueueEmptyError
- :class:StopIteration
- :class:VCIError
- """
- if isinstance(result, int):
- # Real return value is an unsigned long
- result = ctypes.c_ulong(result).value
-
- if result == constants.VCI_E_TIMEOUT:
- raise VCITimeout("Function {} timed out".format(function._name))
- elif result == constants.VCI_E_RXQUEUE_EMPTY:
- raise VCIRxQueueEmptyError()
- elif result == constants.VCI_E_NO_MORE_ITEMS:
- raise StopIteration()
- elif result != constants.VCI_OK:
- raise VCIError(vciFormatError(function, result))
-
- return result
-
-try:
- # Map all required symbols and initialize library ---------------------------
- #HRESULT VCIAPI vciInitialize ( void );
- _canlib.map_symbol("vciInitialize", ctypes.c_long, (), __check_status)
-
- #void VCIAPI vciFormatError (HRESULT hrError, PCHAR pszText, UINT32 dwsize);
- _canlib.map_symbol("vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32))
- # Hack to have vciFormatError as a free function
- vciFormatError = functools.partial(__vciFormatError, _canlib)
-
- # HRESULT VCIAPI vciEnumDeviceOpen( OUT PHANDLE hEnum );
- _canlib.map_symbol("vciEnumDeviceOpen", ctypes.c_long, (PHANDLE,), __check_status)
- # HRESULT VCIAPI vciEnumDeviceClose ( IN HANDLE hEnum );
- _canlib.map_symbol("vciEnumDeviceClose", ctypes.c_long, (HANDLE,), __check_status)
- # HRESULT VCIAPI vciEnumDeviceNext( IN HANDLE hEnum, OUT PVCIDEVICEINFO pInfo );
- _canlib.map_symbol("vciEnumDeviceNext", ctypes.c_long, (HANDLE, structures.PVCIDEVICEINFO), __check_status)
-
- # HRESULT VCIAPI vciDeviceOpen( IN REFVCIID rVciid, OUT PHANDLE phDevice );
- _canlib.map_symbol("vciDeviceOpen", ctypes.c_long, (structures.PVCIID, PHANDLE), __check_status)
- # HRESULT vciDeviceClose( HANDLE hDevice )
- _canlib.map_symbol("vciDeviceClose", ctypes.c_long, (HANDLE,), __check_status)
-
- # HRESULT VCIAPI canChannelOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, IN BOOL fExclusive, OUT PHANDLE phCanChn );
- _canlib.map_symbol("canChannelOpen", ctypes.c_long, (HANDLE, ctypes.c_uint32, ctypes.c_long, PHANDLE), __check_status)
- # EXTERN_C HRESULT VCIAPI canChannelInitialize( IN HANDLE hCanChn, IN UINT16 wRxFifoSize, IN UINT16 wRxThreshold, IN UINT16 wTxFifoSize, IN UINT16 wTxThreshold );
- _canlib.map_symbol("canChannelInitialize", ctypes.c_long, (HANDLE, ctypes.c_uint16, ctypes.c_uint16, ctypes.c_uint16, ctypes.c_uint16), __check_status)
- # EXTERN_C HRESULT VCIAPI canChannelActivate( IN HANDLE hCanChn, IN BOOL fEnable );
- _canlib.map_symbol("canChannelActivate", ctypes.c_long, (HANDLE, ctypes.c_long), __check_status)
- # HRESULT canChannelClose( HANDLE hChannel )
- _canlib.map_symbol("canChannelClose", ctypes.c_long, (HANDLE, ), __check_status)
- #EXTERN_C HRESULT VCIAPI canChannelReadMessage( IN HANDLE hCanChn, IN UINT32 dwMsTimeout, OUT PCANMSG pCanMsg );
- _canlib.map_symbol("canChannelReadMessage", ctypes.c_long, (HANDLE, ctypes.c_uint32, structures.PCANMSG), __check_status)
- #HRESULT canChannelPeekMessage(HANDLE hChannel,PCANMSG pCanMsg );
- _canlib.map_symbol("canChannelPeekMessage", ctypes.c_long, (HANDLE, structures.PCANMSG), __check_status)
- #HRESULT canChannelWaitTxEvent (HANDLE hChannel UINT32 dwMsTimeout );
- _canlib.map_symbol("canChannelWaitTxEvent", ctypes.c_long, (HANDLE, ctypes.c_uint32), __check_status)
- #HRESULT canChannelWaitRxEvent (HANDLE hChannel, UINT32 dwMsTimeout );
- _canlib.map_symbol("canChannelWaitRxEvent", ctypes.c_long, (HANDLE, ctypes.c_uint32), __check_status)
- #HRESULT canChannelPostMessage (HANDLE hChannel, PCANMSG pCanMsg );
- _canlib.map_symbol("canChannelPostMessage", ctypes.c_long, (HANDLE, structures.PCANMSG), __check_status)
- #HRESULT canChannelSendMessage (HANDLE hChannel, UINT32 dwMsTimeout, PCANMSG pCanMsg );
- _canlib.map_symbol("canChannelSendMessage", ctypes.c_long, (HANDLE, ctypes.c_uint32, structures.PCANMSG), __check_status)
-
- #EXTERN_C HRESULT VCIAPI canControlOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, OUT PHANDLE phCanCtl );
- _canlib.map_symbol("canControlOpen", ctypes.c_long, (HANDLE, ctypes.c_uint32, PHANDLE), __check_status)
- #EXTERN_C HRESULT VCIAPI canControlInitialize( IN HANDLE hCanCtl, IN UINT8 bMode, IN UINT8 bBtr0, IN UINT8 bBtr1 );
- _canlib.map_symbol("canControlInitialize", ctypes.c_long, (HANDLE, ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint8), __check_status)
- #EXTERN_C HRESULT VCIAPI canControlClose( IN HANDLE hCanCtl );
- _canlib.map_symbol("canControlClose", ctypes.c_long, (HANDLE,), __check_status)
- #EXTERN_C HRESULT VCIAPI canControlReset( IN HANDLE hCanCtl );
- _canlib.map_symbol("canControlReset", ctypes.c_long, (HANDLE,), __check_status)
- #EXTERN_C HRESULT VCIAPI canControlStart( IN HANDLE hCanCtl, IN BOOL fStart );
- _canlib.map_symbol("canControlStart", ctypes.c_long, (HANDLE, ctypes.c_long), __check_status)
- #EXTERN_C HRESULT VCIAPI canControlGetStatus( IN HANDLE hCanCtl, OUT PCANLINESTATUS pStatus );
- _canlib.map_symbol("canControlGetStatus", ctypes.c_long, (HANDLE, structures.PCANLINESTATUS), __check_status)
- #EXTERN_C HRESULT VCIAPI canControlGetCaps( IN HANDLE hCanCtl, OUT PCANCAPABILITIES pCanCaps );
- _canlib.map_symbol("canControlGetCaps", ctypes.c_long, (HANDLE, structures.PCANCAPABILITIES), __check_status)
- #EXTERN_C HRESULT VCIAPI canControlSetAccFilter( IN HANDLE hCanCtl, IN BOOL fExtend, IN UINT32 dwCode, IN UINT32 dwMask );
- _canlib.map_symbol("canControlSetAccFilter", ctypes.c_long, (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), __check_status)
- #EXTERN_C HRESULT canControlAddFilterIds (HANDLE hControl, BOOL fExtended, UINT32 dwCode, UINT32 dwMask);
- _canlib.map_symbol("canControlAddFilterIds", ctypes.c_long, (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), __check_status)
- #EXTERN_C HRESULT canControlRemFilterIds (HANDLE hControl, BOOL fExtendend, UINT32 dwCode, UINT32 dwMask );
- _canlib.map_symbol("canControlRemFilterIds", ctypes.c_long, (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), __check_status)
- #EXTERN_C HRESULT canSchedulerOpen (HANDLE hDevice, UINT32 dwCanNo, PHANDLE phScheduler );
- _canlib.map_symbol("canSchedulerOpen", ctypes.c_long, (HANDLE, ctypes.c_uint32, PHANDLE), __check_status)
- #EXTERN_C HRESULT canSchedulerClose (HANDLE hScheduler );
- _canlib.map_symbol("canSchedulerClose", ctypes.c_long, (HANDLE, ), __check_status)
- #EXTERN_C HRESULT canSchedulerGetCaps (HANDLE hScheduler, PCANCAPABILITIES pCaps );
- _canlib.map_symbol("canSchedulerGetCaps", ctypes.c_long, (HANDLE, structures.PCANCAPABILITIES), __check_status)
- #EXTERN_C HRESULT canSchedulerActivate ( HANDLE hScheduler, BOOL fEnable );
- _canlib.map_symbol("canSchedulerActivate", ctypes.c_long, (HANDLE, ctypes.c_int), __check_status)
- #EXTERN_C HRESULT canSchedulerAddMessage (HANDLE hScheduler, PCANCYCLICTXMSG pMessage, PUINT32 pdwIndex );
- _canlib.map_symbol("canSchedulerAddMessage", ctypes.c_long, (HANDLE, structures.PCANCYCLICTXMSG, ctypes.POINTER(ctypes.c_uint32)), __check_status)
- #EXTERN_C HRESULT canSchedulerRemMessage (HANDLE hScheduler, UINT32 dwIndex );
- _canlib.map_symbol("canSchedulerRemMessage", ctypes.c_long, (HANDLE, ctypes.c_uint32), __check_status)
- #EXTERN_C HRESULT canSchedulerStartMessage (HANDLE hScheduler, UINT32 dwIndex, UINT16 dwCount );
- _canlib.map_symbol("canSchedulerStartMessage", ctypes.c_long, (HANDLE, ctypes.c_uint32, ctypes.c_uint16), __check_status)
- #EXTERN_C HRESULT canSchedulerStopMessage (HANDLE hScheduler, UINT32 dwIndex );
- _canlib.map_symbol("canSchedulerStopMessage", ctypes.c_long, (HANDLE, ctypes.c_uint32), __check_status)
- _canlib.vciInitialize()
-except AttributeError:
- # In case _canlib == None meaning we're not on win32/no lib found
- pass
-except Exception as e:
- log.warning("Could not initialize IXXAT VCI library: %s", e)
-# ---------------------------------------------------------------------------
-
-
-CAN_INFO_MESSAGES = {
- constants.CAN_INFO_START: "CAN started",
- constants.CAN_INFO_STOP: "CAN stopped",
- constants.CAN_INFO_RESET: "CAN reset",
-}
-
-CAN_ERROR_MESSAGES = {
- constants.CAN_ERROR_STUFF: "CAN bit stuff error",
- constants.CAN_ERROR_FORM: "CAN form error",
- constants.CAN_ERROR_ACK: "CAN acknowledgment error",
- constants.CAN_ERROR_BIT: "CAN bit error",
- constants.CAN_ERROR_CRC: "CAN CRC error",
- constants.CAN_ERROR_OTHER: "Other (unknown) CAN error",
-}
-#----------------------------------------------------------------------------
+import can.interfaces.ixxat.canlib_vcinpl as vcinpl
+import can.interfaces.ixxat.canlib_vcinpl2 as vcinpl2
+from can import (
+ BusABC,
+ BusState,
+ CyclicSendTaskABC,
+ Message,
+)
class IXXATBus(BusABC):
"""The CAN Bus implemented for the IXXAT interface.
- .. warning::
-
- This interface does implement efficient filtering of messages, but
- the filters have to be set in :meth:`~can.interfaces.ixxat.IXXATBus.__init__`
- using the ``can_filters`` parameter. Using :meth:`~can.interfaces.ixxat.IXXATBus.set_filters`
- does not work.
+ Based on the C implementation of IXXAT, two different dlls are provided by IXXAT, one to work with CAN,
+ the other with CAN-FD.
+ This class only delegates to related implementation (in calib_vcinpl or canlib_vcinpl2)
+ class depending on fd user option.
"""
- CHANNEL_BITRATES = {
- 0: {
- 10000: constants.CAN_BT0_10KB,
- 20000: constants.CAN_BT0_20KB,
- 50000: constants.CAN_BT0_50KB,
- 100000: constants.CAN_BT0_100KB,
- 125000: constants.CAN_BT0_125KB,
- 250000: constants.CAN_BT0_250KB,
- 500000: constants.CAN_BT0_500KB,
- 800000: constants.CAN_BT0_800KB,
- 1000000: constants.CAN_BT0_1000KB
- },
- 1: {
- 10000: constants.CAN_BT1_10KB,
- 20000: constants.CAN_BT1_20KB,
- 50000: constants.CAN_BT1_50KB,
- 100000: constants.CAN_BT1_100KB,
- 125000: constants.CAN_BT1_125KB,
- 250000: constants.CAN_BT1_250KB,
- 500000: constants.CAN_BT1_500KB,
- 800000: constants.CAN_BT1_800KB,
- 1000000: constants.CAN_BT1_1000KB
- }
- }
-
- def __init__(self, channel, can_filters=None, **config):
+ def __init__(
+ self,
+ channel: int,
+ can_filters=None,
+ receive_own_messages: bool = False,
+ unique_hardware_id: int | None = None,
+ extended: bool = True,
+ fd: bool = False,
+ rx_fifo_size: int | None = None,
+ tx_fifo_size: int | None = None,
+ bitrate: int = 500000,
+ data_bitrate: int = 2000000,
+ sjw_abr: int | None = None,
+ tseg1_abr: int | None = None,
+ tseg2_abr: int | None = None,
+ sjw_dbr: int | None = None,
+ tseg1_dbr: int | None = None,
+ tseg2_dbr: int | None = None,
+ ssp_dbr: int | None = None,
+ **kwargs,
+ ):
"""
- :param int channel:
+ :param channel:
The Channel id to create this bus with.
- :param list can_filters:
+ :param can_filters:
See :meth:`can.BusABC.set_filters`.
- :param bool receive_own_messages:
+ :param receive_own_messages:
Enable self-reception of sent messages.
- :param int UniqueHardwareId:
+ :param unique_hardware_id:
UniqueHardwareId to connect (optional, will use the first found if not supplied)
- :param int bitrate:
- Channel bitrate in bit/s
- """
- if _canlib is None:
- raise ImportError("The IXXAT VCI library has not been initialized. Check the logs for more details.")
- log.info("CAN Filters: %s", can_filters)
- log.info("Got configuration of: %s", config)
- # Configuration options
- bitrate = config.get('bitrate', 500000)
- UniqueHardwareId = config.get('UniqueHardwareId', None)
- rxFifoSize = config.get('rxFifoSize', 16)
- txFifoSize = config.get('txFifoSize', 16)
- self._receive_own_messages = config.get('receive_own_messages', False)
- # Usually comes as a string from the config file
- channel = int(channel)
+ :param extended:
+ Default True, enables the capability to use extended IDs.
- if (bitrate not in self.CHANNEL_BITRATES[0]):
- raise ValueError("Invalid bitrate {}".format(bitrate))
+ :param fd:
+ Default False, enables CAN-FD usage.
- self._device_handle = HANDLE()
- self._device_info = structures.VCIDEVICEINFO()
- self._control_handle = HANDLE()
- self._channel_handle = HANDLE()
- self._channel_capabilities = structures.CANCAPABILITIES()
- self._message = structures.CANMSG()
- self._payload = (ctypes.c_byte * 8)()
+ :param rx_fifo_size:
+ Receive fifo size (default 1024 for fd, else 16)
- # Search for supplied device
- if UniqueHardwareId is None:
- log.info("Searching for first available device")
- else:
- log.info("Searching for unique HW ID %s", UniqueHardwareId)
- _canlib.vciEnumDeviceOpen(ctypes.byref(self._device_handle))
- while True:
- try:
- _canlib.vciEnumDeviceNext(self._device_handle, ctypes.byref(self._device_info))
- except StopIteration:
- if (UniqueHardwareId is None):
- raise VCIDeviceNotFoundError("No IXXAT device(s) connected or device(s) in use by other process(es).")
- else:
- raise VCIDeviceNotFoundError("Unique HW ID {} not connected or not available.".format(UniqueHardwareId))
- else:
- if (UniqueHardwareId is None) or (self._device_info.UniqueHardwareId.AsChar == bytes(UniqueHardwareId, 'ascii')):
- break
- else:
- log.debug("Ignoring IXXAT with hardware id '%s'.", self._device_info.UniqueHardwareId.AsChar.decode("ascii"))
- _canlib.vciEnumDeviceClose(self._device_handle)
- _canlib.vciDeviceOpen(ctypes.byref(self._device_info.VciObjectId), ctypes.byref(self._device_handle))
- log.info("Using unique HW ID %s", self._device_info.UniqueHardwareId.AsChar)
+ :param tx_fifo_size:
+ Transmit fifo size (default 128 for fd, else 16)
- log.info("Initializing channel %d in shared mode, %d rx buffers, %d tx buffers", channel, rxFifoSize, txFifoSize)
- _canlib.canChannelOpen(self._device_handle, channel, constants.FALSE, ctypes.byref(self._channel_handle))
- # Signal TX/RX events when at least one frame has been handled
- _canlib.canChannelInitialize(self._channel_handle, rxFifoSize, 1, txFifoSize, 1)
- _canlib.canChannelActivate(self._channel_handle, constants.TRUE)
+ :param bitrate:
+ Channel bitrate in bit/s
- log.info("Initializing control %d bitrate %d", channel, bitrate)
- _canlib.canControlOpen(self._device_handle, channel, ctypes.byref(self._control_handle))
- _canlib.canControlInitialize(
- self._control_handle,
- constants.CAN_OPMODE_STANDARD|constants.CAN_OPMODE_EXTENDED|constants.CAN_OPMODE_ERRFRAME,
- self.CHANNEL_BITRATES[0][bitrate],
- self.CHANNEL_BITRATES[1][bitrate]
- )
- _canlib.canControlGetCaps(self._control_handle, ctypes.byref(self._channel_capabilities))
+ :param data_bitrate:
+ Channel bitrate in bit/s (only in CAN-Fd if baudrate switch enabled).
- # With receive messages, this field contains the relative reception time of
- # the message in ticks. The resolution of a tick can be calculated from the fields
- # dwClockFreq and dwTscDivisor of the structure CANCAPABILITIES in accordance with the following formula:
- # frequency [1/s] = dwClockFreq / dwTscDivisor
- # We explicitly cast to float for Python 2.x users
- self._tick_resolution = float(self._channel_capabilities.dwClockFreq / self._channel_capabilities.dwTscDivisor)
+ :param sjw_abr:
+ Bus timing value sample jump width (arbitration). Only takes effect with fd enabled.
- # Setup filters before starting the channel
- if can_filters:
- log.info("The IXXAT VCI backend is filtering messages")
- # Disable every message coming in
- for extended in (0, 1):
- _canlib.canControlSetAccFilter(self._control_handle,
- extended,
- constants.CAN_ACC_CODE_NONE,
- constants.CAN_ACC_MASK_NONE)
- for can_filter in can_filters:
- # Whitelist
- code = int(can_filter['can_id'])
- mask = int(can_filter['can_mask'])
- extended = can_filter.get('extended', False)
- _canlib.canControlAddFilterIds(self._control_handle,
- 1 if extended else 0,
- code << 1,
- mask << 1)
- log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask)
+ :param tseg1_abr:
+ Bus timing value tseg1 (arbitration). Only takes effect with fd enabled.
- # Start the CAN controller. Messages will be forwarded to the channel
- _canlib.canControlStart(self._control_handle, constants.TRUE)
+ :param tseg2_abr:
+ Bus timing value tseg2 (arbitration). Only takes effect with fd enabled.
- # For cyclic transmit list. Set when .send_periodic() is first called
- self._scheduler = None
- self._scheduler_resolution = None
- self.channel = channel
+ :param sjw_dbr:
+ Bus timing value sample jump width (data). Only takes effect with fd and baudrate switch enabled.
- # Usually you get back 3 messages like "CAN initialized" ecc...
- # Clear the FIFO by filter them out with low timeout
- for i in range(rxFifoSize):
- try:
- _canlib.canChannelReadMessage(self._channel_handle, 0, ctypes.byref(self._message))
- except (VCITimeout, VCIRxQueueEmptyError):
- break
+ :param tseg1_dbr:
+ Bus timing value tseg1 (data). Only takes effect with fd and bitrate switch enabled.
- super(IXXATBus, self).__init__(channel=channel, can_filters=None, **config)
+ :param tseg2_dbr:
+ Bus timing value tseg2 (data). Only takes effect with fd and bitrate switch enabled.
- def _inWaiting(self):
- try:
- _canlib.canChannelWaitRxEvent(self._channel_handle, 0)
- except VCITimeout:
- return 0
+ :param ssp_dbr:
+ Secondary sample point (data). Only takes effect with fd and bitrate switch enabled.
+
+ """
+ if fd:
+ if rx_fifo_size is None:
+ rx_fifo_size = 1024
+ if tx_fifo_size is None:
+ tx_fifo_size = 128
+ self.bus = vcinpl2.IXXATBus(
+ channel=channel,
+ can_filters=can_filters,
+ receive_own_messages=receive_own_messages,
+ unique_hardware_id=unique_hardware_id,
+ extended=extended,
+ rx_fifo_size=rx_fifo_size,
+ tx_fifo_size=tx_fifo_size,
+ bitrate=bitrate,
+ data_bitrate=data_bitrate,
+ sjw_abr=sjw_abr,
+ tseg1_abr=tseg1_abr,
+ tseg2_abr=tseg2_abr,
+ sjw_dbr=sjw_dbr,
+ tseg1_dbr=tseg1_dbr,
+ tseg2_dbr=tseg2_dbr,
+ ssp_dbr=ssp_dbr,
+ **kwargs,
+ )
else:
- return 1
+ if rx_fifo_size is None:
+ rx_fifo_size = 16
+ if tx_fifo_size is None:
+ tx_fifo_size = 16
+ self.bus = vcinpl.IXXATBus(
+ channel=channel,
+ can_filters=can_filters,
+ receive_own_messages=receive_own_messages,
+ unique_hardware_id=unique_hardware_id,
+ extended=extended,
+ rx_fifo_size=rx_fifo_size,
+ tx_fifo_size=tx_fifo_size,
+ bitrate=bitrate,
+ **kwargs,
+ )
+
+ super().__init__(channel=channel, **kwargs)
+ self._can_protocol = self.bus.protocol
def flush_tx_buffer(self):
- """ Flushes the transmit buffer on the IXXAT """
- # TODO #64: no timeout?
- _canlib.canChannelWaitTxEvent(self._channel_handle, constants.INFINITE)
+ """Flushes the transmit buffer on the IXXAT"""
+ return self.bus.flush_tx_buffer()
def _recv_internal(self, timeout):
- """ Read a message from IXXAT device. """
-
- # TODO: handling CAN error messages?
- data_received = False
-
- if timeout == 0:
- # Peek without waiting
- try:
- _canlib.canChannelPeekMessage(self._channel_handle, ctypes.byref(self._message))
- except (VCITimeout, VCIRxQueueEmptyError):
- return None, True
- else:
- if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA:
- data_received = True
- else:
- # Wait if no message available
- if timeout is None or timeout < 0:
- remaining_ms = constants.INFINITE
- t0 = None
- else:
- timeout_ms = int(timeout * 1000)
- remaining_ms = timeout_ms
- t0 = _timer_function()
-
- while True:
- try:
- _canlib.canChannelReadMessage(self._channel_handle, remaining_ms, ctypes.byref(self._message))
- except (VCITimeout, VCIRxQueueEmptyError):
- # Ignore the 2 errors, the timeout is handled manually with the _timer_function()
- pass
- else:
- # See if we got a data or info/error messages
- if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA:
- data_received = True
- break
-
- elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_INFO:
- log.info(CAN_INFO_MESSAGES.get(self._message.abData[0], "Unknown CAN info message code {}".format(self._message.abData[0])))
-
- elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR:
- log.warning(CAN_ERROR_MESSAGES.get(self._message.abData[0], "Unknown CAN error message code {}".format(self._message.abData[0])))
-
- elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR:
- pass
- else:
- log.warn("Unexpected message info type")
-
- if t0 is not None:
- remaining_ms = timeout_ms - int((_timer_function() - t0) * 1000)
- if remaining_ms < 0:
- break
-
- if not data_received:
- # Timed out / can message type is not DATA
- return None, True
-
- # The _message.dwTime is a 32bit tick value and will overrun,
- # so expect to see the value restarting from 0
- rx_msg = Message(
- timestamp=self._message.dwTime / self._tick_resolution, # Relative time in s
- is_remote_frame=True if self._message.uMsgInfo.Bits.rtr else False,
- extended_id=True if self._message.uMsgInfo.Bits.ext else False,
- arbitration_id=self._message.dwMsgId,
- dlc=self._message.uMsgInfo.Bits.dlc,
- data=self._message.abData[:self._message.uMsgInfo.Bits.dlc],
- channel=self.channel
+ """Read a message from IXXAT device."""
+ return self.bus._recv_internal(timeout)
+
+ def send(self, msg: Message, timeout: float | None = None) -> None:
+ return self.bus.send(msg, timeout)
+
+ def _send_periodic_internal(
+ self,
+ msgs: Sequence[Message] | Message,
+ period: float,
+ duration: float | None = None,
+ autostart: bool = True,
+ modifier_callback: Callable[[Message], None] | None = None,
+ ) -> CyclicSendTaskABC:
+ return self.bus._send_periodic_internal(
+ msgs, period, duration, autostart, modifier_callback
)
- return rx_msg, True
-
- def send(self, msg, timeout=None):
-
- # This system is not designed to be very efficient
- message = structures.CANMSG()
- message.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA
- message.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0
- message.uMsgInfo.Bits.ext = 1 if msg.id_type else 0
- message.uMsgInfo.Bits.srr = 1 if self._receive_own_messages else 0
- message.dwMsgId = msg.arbitration_id
- if msg.dlc:
- message.uMsgInfo.Bits.dlc = msg.dlc
- adapter = (ctypes.c_uint8 * len(msg.data)).from_buffer(msg.data)
- ctypes.memmove(message.abData, adapter, len(msg.data))
-
- if timeout:
- _canlib.canChannelSendMessage(
- self._channel_handle, int(timeout * 1000), message)
- else:
- _canlib.canChannelPostMessage(self._channel_handle, message)
-
- def send_periodic(self, msg, period, duration=None):
- """Send a message using built-in cyclic transmit list functionality."""
- if self._scheduler is None:
- self._scheduler = HANDLE()
- _canlib.canSchedulerOpen(self._device_handle, self.channel,
- self._scheduler)
- caps = structures.CANCAPABILITIES()
- _canlib.canSchedulerGetCaps(self._scheduler, caps)
- self._scheduler_resolution = float(caps.dwClockFreq) / caps.dwCmsDivisor
- _canlib.canSchedulerActivate(self._scheduler, constants.TRUE)
- return CyclicSendTask(self._scheduler, msg, period, duration,
- self._scheduler_resolution)
-
- def shutdown(self):
- if self._scheduler is not None:
- _canlib.canSchedulerClose(self._scheduler)
- _canlib.canChannelClose(self._channel_handle)
- _canlib.canControlStart(self._control_handle, constants.FALSE)
- _canlib.canControlClose(self._control_handle)
- _canlib.vciDeviceClose(self._device_handle)
+ def shutdown(self) -> None:
+ super().shutdown()
+ self.bus.shutdown()
- __set_filters_has_been_called = False
- def set_filters(self, can_filers=None):
- """Unsupported. See note on :class:`~can.interfaces.ixxat.IXXATBus`.
+ @property
+ def state(self) -> BusState:
"""
- if self.__set_filters_has_been_called:
- log.warn("using filters is not supported like this, see note on IXXATBus")
- else:
- # allow the constructor to call this without causing a warning
- self.__set_filters_has_been_called = True
-
-
-class CyclicSendTask(LimitedDurationCyclicSendTaskABC,
- RestartableCyclicTaskABC):
- """A message in the cyclic transmit list."""
-
- def __init__(self, scheduler, msg, period, duration, resolution):
- super(CyclicSendTask, self).__init__(msg, period, duration)
- self._scheduler = scheduler
- self._index = None
- self._count = int(duration / period) if duration else 0
-
- self._msg = structures.CANCYCLICTXMSG()
- self._msg.wCycleTime = int(round(period * resolution))
- self._msg.dwMsgId = msg.arbitration_id
- self._msg.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA
- self._msg.uMsgInfo.Bits.ext = 1 if msg.id_type else 0
- self._msg.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0
- self._msg.uMsgInfo.Bits.dlc = msg.dlc
- for i, b in enumerate(msg.data):
- self._msg.abData[i] = b
- self.start()
-
- def start(self):
- """Start transmitting message (add to list if needed)."""
- if self._index is None:
- self._index = ctypes.c_uint32()
- _canlib.canSchedulerAddMessage(self._scheduler,
- self._msg,
- self._index)
- _canlib.canSchedulerStartMessage(self._scheduler,
- self._index,
- self._count)
-
- def pause(self):
- """Pause transmitting message (keep it in the list)."""
- _canlib.canSchedulerStopMessage(self._scheduler, self._index)
+ Return the current state of the hardware
+ """
+ return self.bus.state
- def stop(self):
- """Stop transmitting message (remove from list)."""
- # Remove it completely instead of just stopping it to avoid filling up
- # the list with permanently stopped messages
- _canlib.canSchedulerRemMessage(self._scheduler, self._index)
- self._index = None
+ @staticmethod
+ def _detect_available_configs() -> Sequence[vcinpl.AutoDetectedIxxatConfig]:
+ return vcinpl._detect_available_configs()
diff --git a/can/interfaces/ixxat/canlib_vcinpl.py b/can/interfaces/ixxat/canlib_vcinpl.py
new file mode 100644
index 000000000..c6c924d8c
--- /dev/null
+++ b/can/interfaces/ixxat/canlib_vcinpl.py
@@ -0,0 +1,1029 @@
+"""
+Ctypes wrapper module for IXXAT Virtual CAN Interface V4 on win32 systems
+
+TODO: We could implement this interface such that setting other filters
+ could work when the initial filters were set to zero using the
+ software fallback. Or could the software filters even be changed
+ after the connection was opened? We need to document that bahaviour!
+ See also the NICAN interface.
+
+"""
+
+import ctypes
+import functools
+import logging
+import sys
+import time
+import warnings
+from collections.abc import Callable, Sequence
+
+from can import (
+ BusABC,
+ BusState,
+ CanProtocol,
+ CyclicSendTaskABC,
+ LimitedDurationCyclicSendTaskABC,
+ Message,
+ RestartableCyclicTaskABC,
+)
+from can.ctypesutil import HANDLE, PHANDLE, CLibrary
+from can.ctypesutil import HRESULT as ctypes_HRESULT
+from can.exceptions import CanInitializationError, CanInterfaceNotImplementedError
+from can.typechecking import AutoDetectedConfig
+from can.util import deprecated_args_alias
+
+from . import constants, structures
+from .exceptions import *
+
+__all__ = [
+ "IXXATBus",
+ "VCIBusOffError",
+ "VCIDeviceNotFoundError",
+ "VCIError",
+ "VCITimeout",
+ "vciFormatError",
+]
+
+log = logging.getLogger("can.ixxat")
+
+
+# Hack to have vciFormatError as a free function, see below
+vciFormatError = None
+
+# main ctypes instance
+_canlib = None
+# TODO: Use ECI driver for linux
+if sys.platform == "win32" or sys.platform == "cygwin":
+ try:
+ _canlib = CLibrary("vcinpl.dll")
+ except Exception as e:
+ log.warning("Cannot load IXXAT vcinpl library: %s", e)
+else:
+ # Will not work on other systems, but have it importable anyway for
+ # tests/sphinx
+ log.warning("IXXAT VCI library does not work on %s platform", sys.platform)
+
+
+def __vciFormatErrorExtended(
+ library_instance: CLibrary, function: Callable, vret: int, args: tuple
+):
+ """Format a VCI error and attach failed function, decoded HRESULT and arguments
+ :param CLibrary library_instance:
+ Mapped instance of IXXAT vcinpl library
+ :param callable function:
+ Failed function
+ :param HRESULT vret:
+ HRESULT returned by vcinpl call
+ :param args:
+ Arbitrary arguments tuple
+ :return:
+ Formatted string
+ """
+ # TODO: make sure we don't generate another exception
+ return (
+ f"{__vciFormatError(library_instance, function, vret)} - arguments were {args}"
+ )
+
+
+def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int):
+ """Format a VCI error and attach failed function and decoded HRESULT
+ :param CLibrary library_instance:
+ Mapped instance of IXXAT vcinpl library
+ :param callable function:
+ Failed function
+ :param HRESULT vret:
+ HRESULT returned by vcinpl call
+ :return:
+ Formatted string
+ """
+ buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN)
+ ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN)
+ library_instance.vciFormatError(vret, buf, constants.VCI_MAX_ERRSTRLEN)
+ return "function {} failed ({})".format(
+ function._name, buf.value.decode("utf-8", "replace")
+ )
+
+
+def __check_status(result, function, args):
+ """
+ Check the result of a vcinpl function call and raise appropriate exception
+ in case of an error. Used as errcheck function when mapping C functions
+ with ctypes.
+ :param result:
+ Function call numeric result
+ :param callable function:
+ Called function
+ :param args:
+ Arbitrary arguments tuple
+ :raise:
+ :class:VCITimeout
+ :class:VCIRxQueueEmptyError
+ :class:StopIteration
+ :class:VCIError
+ """
+ if isinstance(result, int):
+ # Real return value is an unsigned long, the following line converts the number to unsigned
+ result = ctypes.c_ulong(result).value
+
+ if result == constants.VCI_E_TIMEOUT:
+ raise VCITimeout(f"Function {function._name} timed out")
+ elif result == constants.VCI_E_RXQUEUE_EMPTY:
+ raise VCIRxQueueEmptyError()
+ elif result == constants.VCI_E_NO_MORE_ITEMS:
+ raise StopIteration()
+ elif result == constants.VCI_E_ACCESSDENIED:
+ pass # not a real error, might happen if another program has initialized the bus
+ elif result != constants.VCI_OK:
+ raise VCIError(vciFormatError(function, result))
+
+ return result
+
+
+try:
+ # Map all required symbols and initialize library ---------------------------
+ # HRESULT VCIAPI vciInitialize ( void );
+ _canlib.map_symbol("vciInitialize", ctypes.c_long, (), __check_status)
+
+ # void VCIAPI vciFormatError (HRESULT hrError, PCHAR pszText, UINT32 dwsize);
+ _canlib.map_symbol(
+ "vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32)
+ )
+ # Hack to have vciFormatError as a free function
+ vciFormatError = functools.partial(__vciFormatError, _canlib)
+
+ # HRESULT VCIAPI vciEnumDeviceOpen( OUT PHANDLE hEnum );
+ _canlib.map_symbol("vciEnumDeviceOpen", ctypes.c_long, (PHANDLE,), __check_status)
+ # HRESULT VCIAPI vciEnumDeviceClose ( IN HANDLE hEnum );
+ _canlib.map_symbol("vciEnumDeviceClose", ctypes.c_long, (HANDLE,), __check_status)
+ # HRESULT VCIAPI vciEnumDeviceNext( IN HANDLE hEnum, OUT PVCIDEVICEINFO pInfo );
+ _canlib.map_symbol(
+ "vciEnumDeviceNext",
+ ctypes.c_long,
+ (HANDLE, structures.PVCIDEVICEINFO),
+ __check_status,
+ )
+
+ # HRESULT VCIAPI vciDeviceOpen( IN REFVCIID rVciid, OUT PHANDLE phDevice );
+ _canlib.map_symbol(
+ "vciDeviceOpen", ctypes.c_long, (structures.PVCIID, PHANDLE), __check_status
+ )
+ # HRESULT vciDeviceClose( HANDLE hDevice )
+ _canlib.map_symbol("vciDeviceClose", ctypes.c_long, (HANDLE,), __check_status)
+
+ # HRESULT VCIAPI canChannelOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, IN BOOL fExclusive, OUT PHANDLE phCanChn );
+ _canlib.map_symbol(
+ "canChannelOpen",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_uint32, ctypes.c_long, PHANDLE),
+ __check_status,
+ )
+ # EXTERN_C HRESULT VCIAPI canChannelInitialize( IN HANDLE hCanChn, IN UINT16 wRxFifoSize, IN UINT16 wRxThreshold, IN UINT16 wTxFifoSize, IN UINT16 wTxThreshold );
+ _canlib.map_symbol(
+ "canChannelInitialize",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_uint16, ctypes.c_uint16, ctypes.c_uint16, ctypes.c_uint16),
+ __check_status,
+ )
+ # EXTERN_C HRESULT VCIAPI canChannelActivate( IN HANDLE hCanChn, IN BOOL fEnable );
+ _canlib.map_symbol(
+ "canChannelActivate", ctypes.c_long, (HANDLE, ctypes.c_long), __check_status
+ )
+ # HRESULT canChannelClose( HANDLE hChannel )
+ _canlib.map_symbol("canChannelClose", ctypes.c_long, (HANDLE,), __check_status)
+ # EXTERN_C HRESULT VCIAPI canChannelReadMessage( IN HANDLE hCanChn, IN UINT32 dwMsTimeout, OUT PCANMSG pCanMsg );
+ _canlib.map_symbol(
+ "canChannelReadMessage",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_uint32, structures.PCANMSG),
+ __check_status,
+ )
+ # HRESULT canChannelPeekMessage(HANDLE hChannel,PCANMSG pCanMsg );
+ _canlib.map_symbol(
+ "canChannelPeekMessage",
+ ctypes.c_long,
+ (HANDLE, structures.PCANMSG),
+ __check_status,
+ )
+ # HRESULT canChannelWaitTxEvent (HANDLE hChannel UINT32 dwMsTimeout );
+ _canlib.map_symbol(
+ "canChannelWaitTxEvent",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_uint32),
+ __check_status,
+ )
+ # HRESULT canChannelWaitRxEvent (HANDLE hChannel, UINT32 dwMsTimeout );
+ _canlib.map_symbol(
+ "canChannelWaitRxEvent",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_uint32),
+ __check_status,
+ )
+ # HRESULT canChannelPostMessage (HANDLE hChannel, PCANMSG pCanMsg );
+ _canlib.map_symbol(
+ "canChannelPostMessage",
+ ctypes.c_long,
+ (HANDLE, structures.PCANMSG),
+ __check_status,
+ )
+ # HRESULT canChannelSendMessage (HANDLE hChannel, UINT32 dwMsTimeout, PCANMSG pCanMsg );
+ _canlib.map_symbol(
+ "canChannelSendMessage",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_uint32, structures.PCANMSG),
+ __check_status,
+ )
+ # HRESULT canChannelGetStatus (HANDLE hCanChn, PCANCHANSTATUS pStatus );
+ _canlib.map_symbol(
+ "canChannelGetStatus",
+ ctypes.c_long,
+ (HANDLE, structures.PCANCHANSTATUS),
+ __check_status,
+ )
+
+ # EXTERN_C HRESULT VCIAPI canControlOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, OUT PHANDLE phCanCtl );
+ _canlib.map_symbol(
+ "canControlOpen",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_uint32, PHANDLE),
+ __check_status,
+ )
+ # EXTERN_C HRESULT VCIAPI canControlInitialize( IN HANDLE hCanCtl, IN UINT8 bMode, IN UINT8 bBtr0, IN UINT8 bBtr1 );
+ _canlib.map_symbol(
+ "canControlInitialize",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint8),
+ __check_status,
+ )
+ # EXTERN_C HRESULT VCIAPI canControlClose( IN HANDLE hCanCtl );
+ _canlib.map_symbol("canControlClose", ctypes.c_long, (HANDLE,), __check_status)
+ # EXTERN_C HRESULT VCIAPI canControlReset( IN HANDLE hCanCtl );
+ _canlib.map_symbol("canControlReset", ctypes.c_long, (HANDLE,), __check_status)
+ # EXTERN_C HRESULT VCIAPI canControlStart( IN HANDLE hCanCtl, IN BOOL fStart );
+ _canlib.map_symbol(
+ "canControlStart", ctypes.c_long, (HANDLE, ctypes.c_long), __check_status
+ )
+ # EXTERN_C HRESULT VCIAPI canControlGetStatus( IN HANDLE hCanCtl, OUT PCANLINESTATUS pStatus );
+ _canlib.map_symbol(
+ "canControlGetStatus",
+ ctypes.c_long,
+ (HANDLE, structures.PCANLINESTATUS),
+ __check_status,
+ )
+ # EXTERN_C HRESULT VCIAPI canControlGetCaps( IN HANDLE hCanCtl, OUT PCANCAPABILITIES pCanCaps );
+ _canlib.map_symbol(
+ "canControlGetCaps",
+ ctypes.c_long,
+ (HANDLE, structures.PCANCAPABILITIES),
+ __check_status,
+ )
+ # EXTERN_C HRESULT VCIAPI canControlSetAccFilter( IN HANDLE hCanCtl, IN BOOL fExtend, IN UINT32 dwCode, IN UINT32 dwMask );
+ _canlib.map_symbol(
+ "canControlSetAccFilter",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canControlAddFilterIds (HANDLE hControl, BOOL fExtended, UINT32 dwCode, UINT32 dwMask);
+ _canlib.map_symbol(
+ "canControlAddFilterIds",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canControlRemFilterIds (HANDLE hControl, BOOL fExtendend, UINT32 dwCode, UINT32 dwMask );
+ _canlib.map_symbol(
+ "canControlRemFilterIds",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canSchedulerOpen (HANDLE hDevice, UINT32 dwCanNo, PHANDLE phScheduler );
+ _canlib.map_symbol(
+ "canSchedulerOpen",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_uint32, PHANDLE),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canSchedulerClose (HANDLE hScheduler );
+ _canlib.map_symbol("canSchedulerClose", ctypes.c_long, (HANDLE,), __check_status)
+ # EXTERN_C HRESULT canSchedulerGetCaps (HANDLE hScheduler, PCANCAPABILITIES pCaps );
+ _canlib.map_symbol(
+ "canSchedulerGetCaps",
+ ctypes.c_long,
+ (HANDLE, structures.PCANCAPABILITIES),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canSchedulerActivate ( HANDLE hScheduler, BOOL fEnable );
+ _canlib.map_symbol(
+ "canSchedulerActivate", ctypes.c_long, (HANDLE, ctypes.c_int), __check_status
+ )
+ # EXTERN_C HRESULT canSchedulerAddMessage (HANDLE hScheduler, PCANCYCLICTXMSG pMessage, PUINT32 pdwIndex );
+ _canlib.map_symbol(
+ "canSchedulerAddMessage",
+ ctypes.c_long,
+ (HANDLE, structures.PCANCYCLICTXMSG, ctypes.POINTER(ctypes.c_uint32)),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canSchedulerRemMessage (HANDLE hScheduler, UINT32 dwIndex );
+ _canlib.map_symbol(
+ "canSchedulerRemMessage",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_uint32),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canSchedulerStartMessage (HANDLE hScheduler, UINT32 dwIndex, UINT16 dwCount );
+ _canlib.map_symbol(
+ "canSchedulerStartMessage",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_uint32, ctypes.c_uint16),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canSchedulerStopMessage (HANDLE hScheduler, UINT32 dwIndex );
+ _canlib.map_symbol(
+ "canSchedulerStopMessage",
+ ctypes.c_long,
+ (HANDLE, ctypes.c_uint32),
+ __check_status,
+ )
+ _canlib.vciInitialize()
+except AttributeError:
+ # In case _canlib == None meaning we're not on win32/no lib found
+ pass
+except Exception as e:
+ log.warning("Could not initialize IXXAT VCI library: %s", e)
+# ---------------------------------------------------------------------------
+
+
+CAN_INFO_MESSAGES = {
+ constants.CAN_INFO_START: "CAN started",
+ constants.CAN_INFO_STOP: "CAN stopped",
+ constants.CAN_INFO_RESET: "CAN reset",
+}
+
+CAN_ERROR_MESSAGES = {
+ constants.CAN_ERROR_STUFF: "CAN bit stuff error",
+ constants.CAN_ERROR_FORM: "CAN form error",
+ constants.CAN_ERROR_ACK: "CAN acknowledgment error",
+ constants.CAN_ERROR_BIT: "CAN bit error",
+ constants.CAN_ERROR_CRC: "CAN CRC error",
+ constants.CAN_ERROR_OTHER: "Other (unknown) CAN error",
+}
+
+CAN_STATUS_FLAGS = {
+ constants.CAN_STATUS_TXPEND: "transmission pending",
+ constants.CAN_STATUS_OVRRUN: "data overrun occurred",
+ constants.CAN_STATUS_ERRLIM: "error warning limit exceeded",
+ constants.CAN_STATUS_BUSOFF: "bus off",
+ constants.CAN_STATUS_ININIT: "init mode active",
+ constants.CAN_STATUS_BUSCERR: "bus coupling error",
+}
+# ----------------------------------------------------------------------------
+
+
+class IXXATBus(BusABC):
+ """The CAN Bus implemented for the IXXAT interface.
+
+ .. warning::
+
+ This interface does implement efficient filtering of messages, but
+ the filters have to be set in ``__init__`` using the ``can_filters`` parameter.
+ Using :meth:`~can.BusABC.set_filters` does not work.
+ """
+
+ CHANNEL_BITRATES = {
+ 0: {
+ 10000: constants.CAN_BT0_10KB,
+ 20000: constants.CAN_BT0_20KB,
+ 50000: constants.CAN_BT0_50KB,
+ 100000: constants.CAN_BT0_100KB,
+ 125000: constants.CAN_BT0_125KB,
+ 250000: constants.CAN_BT0_250KB,
+ 500000: constants.CAN_BT0_500KB,
+ 666000: constants.CAN_BT0_667KB,
+ 666666: constants.CAN_BT0_667KB,
+ 666667: constants.CAN_BT0_667KB,
+ 667000: constants.CAN_BT0_667KB,
+ 800000: constants.CAN_BT0_800KB,
+ 1000000: constants.CAN_BT0_1000KB,
+ },
+ 1: {
+ 10000: constants.CAN_BT1_10KB,
+ 20000: constants.CAN_BT1_20KB,
+ 50000: constants.CAN_BT1_50KB,
+ 100000: constants.CAN_BT1_100KB,
+ 125000: constants.CAN_BT1_125KB,
+ 250000: constants.CAN_BT1_250KB,
+ 500000: constants.CAN_BT1_500KB,
+ 666000: constants.CAN_BT1_667KB,
+ 666666: constants.CAN_BT1_667KB,
+ 666667: constants.CAN_BT1_667KB,
+ 667000: constants.CAN_BT1_667KB,
+ 800000: constants.CAN_BT1_800KB,
+ 1000000: constants.CAN_BT1_1000KB,
+ },
+ }
+
+ @deprecated_args_alias(
+ deprecation_start="4.0.0",
+ deprecation_end="5.0.0",
+ UniqueHardwareId="unique_hardware_id",
+ rxFifoSize="rx_fifo_size",
+ txFifoSize="tx_fifo_size",
+ )
+ def __init__(
+ self,
+ channel: int,
+ can_filters=None,
+ receive_own_messages: bool = False,
+ unique_hardware_id: int | None = None,
+ extended: bool = True,
+ rx_fifo_size: int = 16,
+ tx_fifo_size: int = 16,
+ bitrate: int = 500000,
+ **kwargs,
+ ):
+ """
+ :param channel:
+ The Channel id to create this bus with.
+
+ :param can_filters:
+ See :meth:`can.BusABC.set_filters`.
+
+ :param receive_own_messages:
+ Enable self-reception of sent messages.
+
+ :param unique_hardware_id:
+ unique_hardware_id to connect (optional, will use the first found if not supplied)
+
+ :param extended:
+ Default True, enables the capability to use extended IDs.
+
+ :param rx_fifo_size:
+ Receive fifo size (default 16)
+
+ :param tx_fifo_size:
+ Transmit fifo size (default 16)
+
+ :param bitrate:
+ Channel bitrate in bit/s
+ """
+ if _canlib is None:
+ raise CanInterfaceNotImplementedError(
+ "The IXXAT VCI library has not been initialized. Check the logs for more details."
+ )
+ log.info("CAN Filters: %s", can_filters)
+ # Configuration options
+ self._receive_own_messages = receive_own_messages
+ # Usually comes as a string from the config file
+ channel = int(channel)
+
+ if bitrate not in self.CHANNEL_BITRATES[0]:
+ raise ValueError(f"Invalid bitrate {bitrate}")
+
+ if rx_fifo_size <= 0:
+ raise ValueError("rx_fifo_size must be > 0")
+
+ if tx_fifo_size <= 0:
+ raise ValueError("tx_fifo_size must be > 0")
+
+ if channel < 0:
+ raise ValueError("channel number must be >= 0")
+
+ self._device_handle = HANDLE()
+ self._device_info = structures.VCIDEVICEINFO()
+ self._control_handle = HANDLE()
+ self._channel_handle = HANDLE()
+ self._channel_capabilities = structures.CANCAPABILITIES()
+ self._message = structures.CANMSG()
+ self._payload = (ctypes.c_byte * 8)()
+ self._can_protocol = CanProtocol.CAN_20
+
+ # Search for supplied device
+ if unique_hardware_id is None:
+ log.info("Searching for first available device")
+ else:
+ log.info("Searching for unique HW ID %s", unique_hardware_id)
+ _canlib.vciEnumDeviceOpen(ctypes.byref(self._device_handle))
+ while True:
+ try:
+ _canlib.vciEnumDeviceNext(
+ self._device_handle, ctypes.byref(self._device_info)
+ )
+ except StopIteration:
+ if unique_hardware_id is None:
+ raise VCIDeviceNotFoundError(
+ "No IXXAT device(s) connected or device(s) in use by other process(es)."
+ ) from None
+ else:
+ raise VCIDeviceNotFoundError(
+ f"Unique HW ID {unique_hardware_id} not connected or not available."
+ ) from None
+ else:
+ if (unique_hardware_id is None) or (
+ self._device_info.UniqueHardwareId.AsChar
+ == bytes(unique_hardware_id, "ascii")
+ ):
+ break
+
+ log.debug(
+ "Ignoring IXXAT with hardware id '%s'.",
+ self._device_info.UniqueHardwareId.AsChar.decode("ascii"),
+ )
+ _canlib.vciEnumDeviceClose(self._device_handle)
+
+ try:
+ _canlib.vciDeviceOpen(
+ ctypes.byref(self._device_info.VciObjectId),
+ ctypes.byref(self._device_handle),
+ )
+ except Exception as exception:
+ raise CanInitializationError(
+ f"Could not open device: {exception}"
+ ) from exception
+
+ log.info("Using unique HW ID %s", self._device_info.UniqueHardwareId.AsChar)
+
+ log.info(
+ "Initializing channel %d in shared mode, %d rx buffers, %d tx buffers",
+ channel,
+ rx_fifo_size,
+ tx_fifo_size,
+ )
+
+ try:
+ _canlib.canChannelOpen(
+ self._device_handle,
+ channel,
+ constants.FALSE,
+ ctypes.byref(self._channel_handle),
+ )
+ except Exception as exception:
+ raise CanInitializationError(
+ f"Could not open and initialize channel: {exception}"
+ ) from exception
+
+ # Signal TX/RX events when at least one frame has been handled
+ _canlib.canChannelInitialize(
+ self._channel_handle, rx_fifo_size, 1, tx_fifo_size, 1
+ )
+ _canlib.canChannelActivate(self._channel_handle, constants.TRUE)
+
+ log.info("Initializing control %d bitrate %d", channel, bitrate)
+ _canlib.canControlOpen(
+ self._device_handle, channel, ctypes.byref(self._control_handle)
+ )
+
+ # compute opmode before control initialize
+ opmode = constants.CAN_OPMODE_STANDARD | constants.CAN_OPMODE_ERRFRAME
+ if extended:
+ opmode |= constants.CAN_OPMODE_EXTENDED
+
+ # control initialize
+ _canlib.canControlInitialize(
+ self._control_handle,
+ opmode,
+ self.CHANNEL_BITRATES[0][bitrate],
+ self.CHANNEL_BITRATES[1][bitrate],
+ )
+ _canlib.canControlGetCaps(
+ self._control_handle, ctypes.byref(self._channel_capabilities)
+ )
+
+ # With receive messages, this field contains the relative reception time of
+ # the message in ticks. The resolution of a tick can be calculated from the fields
+ # dwClockFreq and dwTscDivisor of the structure CANCAPABILITIES in accordance with the following formula:
+ # frequency [1/s] = dwClockFreq / dwTscDivisor
+ self._tick_resolution = (
+ self._channel_capabilities.dwClockFreq
+ / self._channel_capabilities.dwTscDivisor
+ )
+
+ # Setup filters before starting the channel
+ if can_filters:
+ log.info("The IXXAT VCI backend is filtering messages")
+ # Disable every message coming in
+ for extended in (0, 1):
+ _canlib.canControlSetAccFilter(
+ self._control_handle,
+ extended,
+ constants.CAN_ACC_CODE_NONE,
+ constants.CAN_ACC_MASK_NONE,
+ )
+ for can_filter in can_filters:
+ # Filters define what messages are accepted
+ code = int(can_filter["can_id"])
+ mask = int(can_filter["can_mask"])
+ extended = can_filter.get("extended", False)
+ _canlib.canControlAddFilterIds(
+ self._control_handle, 1 if extended else 0, code << 1, mask << 1
+ )
+ log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask)
+
+ # Start the CAN controller. Messages will be forwarded to the channel
+ start_begin = time.time()
+ _canlib.canControlStart(self._control_handle, constants.TRUE)
+ start_end = time.time()
+
+ # Calculate an offset to make them relative to epoch
+ # Assume that the time offset is in the middle of the start command
+ self._timeoffset = start_begin + (start_end - start_begin / 2)
+ self._overrunticks = 0
+ self._starttickoffset = 0
+
+ # For cyclic transmit list. Set when .send_periodic() is first called
+ self._scheduler = None
+ self._scheduler_resolution = None
+ self.channel = channel
+
+ # Usually you get back 3 messages like "CAN initialized" ecc...
+ # Clear the FIFO by filter them out with low timeout
+ for _ in range(rx_fifo_size):
+ try:
+ _canlib.canChannelReadMessage(
+ self._channel_handle, 0, ctypes.byref(self._message)
+ )
+ except (VCITimeout, VCIRxQueueEmptyError):
+ break
+
+ super().__init__(channel=channel, can_filters=None, **kwargs)
+
+ def _inWaiting(self):
+ try:
+ _canlib.canChannelWaitRxEvent(self._channel_handle, 0)
+ except VCITimeout:
+ return 0
+ else:
+ return 1
+
+ def flush_tx_buffer(self):
+ """Flushes the transmit buffer on the IXXAT"""
+ # TODO #64: no timeout?
+ _canlib.canChannelWaitTxEvent(self._channel_handle, constants.INFINITE)
+
+ def _recv_internal(self, timeout):
+ """Read a message from IXXAT device."""
+ data_received = False
+
+ if self._inWaiting() or timeout == 0:
+ # Peek without waiting
+ recv_function = functools.partial(
+ _canlib.canChannelPeekMessage,
+ self._channel_handle,
+ ctypes.byref(self._message),
+ )
+ else:
+ # Wait if no message available
+ timeout = (
+ constants.INFINITE
+ if (timeout is None or timeout < 0)
+ else int(timeout * 1000)
+ )
+ recv_function = functools.partial(
+ _canlib.canChannelReadMessage,
+ self._channel_handle,
+ timeout,
+ ctypes.byref(self._message),
+ )
+
+ try:
+ recv_function()
+ except (VCITimeout, VCIRxQueueEmptyError):
+ # Ignore the 2 errors, overall timeout is handled by BusABC.recv
+ pass
+ else:
+ # See if we got a data or info/error messages
+ if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA:
+ data_received = True
+ elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_INFO:
+ log.info(
+ CAN_INFO_MESSAGES.get(
+ self._message.abData[0],
+ f"Unknown CAN info message code {self._message.abData[0]}",
+ )
+ )
+ # Handle CAN start info message
+ if self._message.abData[0] == constants.CAN_INFO_START:
+ self._starttickoffset = self._message.dwTime
+ elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR:
+ if self._message.uMsgInfo.Bytes.bFlags & constants.CAN_MSGFLAGS_OVR:
+ raise VCIDataOverrunError("Data overrun occurred")
+ else:
+ log.warning(
+ CAN_ERROR_MESSAGES.get(
+ self._message.abData[0],
+ f"Unknown CAN error message code {self._message.abData[0]}",
+ )
+ )
+ log.warning(
+ "CAN message flags bAddFlags/bFlags2 0x%02X bflags 0x%02X",
+ self._message.uMsgInfo.Bytes.bAddFlags,
+ self._message.uMsgInfo.Bytes.bFlags,
+ )
+ elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR:
+ # Add the number of timestamp overruns to the high word
+ self._overrunticks += self._message.dwMsgId << 32
+ else:
+ log.warning(
+ "Unexpected message info type 0x%X",
+ self._message.uMsgInfo.Bits.type,
+ )
+ finally:
+ if not data_received:
+ # Check hard errors
+ status = structures.CANLINESTATUS()
+ _canlib.canControlGetStatus(self._control_handle, ctypes.byref(status))
+ error_byte_1 = status.dwStatus & 0x0F
+ error_byte_2 = status.dwStatus & 0xF0
+ if error_byte_1 > constants.CAN_STATUS_TXPEND:
+ # check CAN_STATUS_BUSOFF first because it is more severe than the other ones
+ if error_byte_1 & constants.CAN_STATUS_BUSOFF:
+ raise VCIBusOffError("Bus off status")
+ elif error_byte_1 & constants.CAN_STATUS_ERRLIM:
+ raise VCIErrorLimitExceededError("Error warning limit exceeded")
+ # Not checking CAN_STATUS_OVRRUN here because it is handled above and would be
+ # raised every time as the flag is never cleared until a reset.
+ elif error_byte_2 > constants.CAN_STATUS_ININIT:
+ # CAN_STATUS_BUSCERR = 0x20 # bus coupling error
+ if error_byte_2 & constants.CAN_STATUS_BUSCERR:
+ raise VCIBusCouplingError("Bus coupling error")
+
+ if not data_received:
+ # Timed out / can message type is not DATA
+ return None, True
+
+ rx_msg = Message(
+ timestamp=(
+ (self._message.dwTime + self._overrunticks - self._starttickoffset)
+ / self._tick_resolution
+ )
+ + self._timeoffset,
+ is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr),
+ is_extended_id=bool(self._message.uMsgInfo.Bits.ext),
+ arbitration_id=self._message.dwMsgId,
+ dlc=self._message.uMsgInfo.Bits.dlc,
+ data=self._message.abData[: self._message.uMsgInfo.Bits.dlc],
+ channel=self.channel,
+ )
+
+ return rx_msg, True
+
+ def send(self, msg: Message, timeout: float | None = None) -> None:
+ """
+ Sends a message on the bus. The interface may buffer the message.
+
+ :param msg:
+ The message to send.
+ :param timeout:
+ Timeout after some time.
+ :raise:
+ :class:CanTimeoutError
+ :class:CanOperationError
+ """
+ # This system is not designed to be very efficient
+ message = structures.CANMSG()
+ message.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA
+ message.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0
+ message.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0
+ message.uMsgInfo.Bits.srr = 1 if self._receive_own_messages else 0
+ message.dwMsgId = msg.arbitration_id
+ if msg.dlc:
+ message.uMsgInfo.Bits.dlc = msg.dlc
+ adapter = (ctypes.c_uint8 * len(msg.data)).from_buffer(msg.data)
+ ctypes.memmove(message.abData, adapter, len(msg.data))
+
+ if timeout:
+ _canlib.canChannelSendMessage(
+ self._channel_handle, int(timeout * 1000), message
+ )
+ else:
+ _canlib.canChannelPostMessage(self._channel_handle, message)
+ # Want to log outgoing messages?
+ # log.log(self.RECV_LOGGING_LEVEL, "Sent: %s", message)
+
+ def _send_periodic_internal(
+ self,
+ msgs: Sequence[Message] | Message,
+ period: float,
+ duration: float | None = None,
+ autostart: bool = True,
+ modifier_callback: Callable[[Message], None] | None = None,
+ ) -> CyclicSendTaskABC:
+ """Send a message using built-in cyclic transmit list functionality."""
+ if modifier_callback is None:
+ if self._scheduler is None:
+ self._scheduler = HANDLE()
+ _canlib.canSchedulerOpen(
+ self._device_handle, self.channel, self._scheduler
+ )
+ caps = structures.CANCAPABILITIES()
+ _canlib.canSchedulerGetCaps(self._scheduler, caps)
+ self._scheduler_resolution = caps.dwClockFreq / caps.dwCmsDivisor
+ _canlib.canSchedulerActivate(self._scheduler, constants.TRUE)
+ return CyclicSendTask(
+ self._scheduler,
+ msgs,
+ period,
+ duration,
+ self._scheduler_resolution,
+ autostart=autostart,
+ )
+
+ # fallback to thread based cyclic task
+ warnings.warn(
+ f"{self.__class__.__name__} falls back to a thread-based cyclic task, "
+ "when the `modifier_callback` argument is given.",
+ stacklevel=3,
+ )
+ return BusABC._send_periodic_internal(
+ self,
+ msgs=msgs,
+ period=period,
+ duration=duration,
+ autostart=autostart,
+ modifier_callback=modifier_callback,
+ )
+
+ def shutdown(self):
+ super().shutdown()
+ if self._scheduler is not None:
+ _canlib.canSchedulerClose(self._scheduler)
+ _canlib.canChannelClose(self._channel_handle)
+ _canlib.canControlStart(self._control_handle, constants.FALSE)
+ _canlib.canControlReset(self._control_handle)
+ _canlib.canControlClose(self._control_handle)
+ _canlib.vciDeviceClose(self._device_handle)
+
+ @property
+ def state(self) -> BusState:
+ """
+ Return the current state of the hardware
+ """
+ status = structures.CANLINESTATUS()
+ _canlib.canControlGetStatus(self._control_handle, ctypes.byref(status))
+ if status.bOpMode == constants.CAN_OPMODE_LISTONLY:
+ return BusState.PASSIVE
+
+ error_byte_1 = status.dwStatus & 0x0F
+ # CAN_STATUS_BUSOFF = 0x08 # bus off status
+ if error_byte_1 & constants.CAN_STATUS_BUSOFF:
+ return BusState.ERROR
+
+ error_byte_2 = status.dwStatus & 0xF0
+ # CAN_STATUS_BUSCERR = 0x20 # bus coupling error
+ if error_byte_2 & constants.CAN_STATUS_BUSCERR:
+ return BusState.ERROR
+
+ return BusState.ACTIVE
+
+
+# ~class IXXATBus(BusABC): ---------------------------------------------------
+
+
+class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC):
+ """A message in the cyclic transmit list."""
+
+ def __init__(
+ self,
+ scheduler,
+ msgs,
+ period,
+ duration,
+ resolution,
+ autostart: bool = True,
+ ):
+ super().__init__(msgs, period, duration)
+ if len(self.messages) != 1:
+ raise ValueError(
+ "IXXAT Interface only supports periodic transmission of 1 element"
+ )
+
+ self._scheduler = scheduler
+ self._index = None
+ self._count = int(duration / period) if duration else 0
+
+ self._msg = structures.CANCYCLICTXMSG()
+ self._msg.wCycleTime = round(period * resolution)
+ self._msg.dwMsgId = self.messages[0].arbitration_id
+ self._msg.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA
+ self._msg.uMsgInfo.Bits.ext = 1 if self.messages[0].is_extended_id else 0
+ self._msg.uMsgInfo.Bits.rtr = 1 if self.messages[0].is_remote_frame else 0
+ self._msg.uMsgInfo.Bits.dlc = self.messages[0].dlc
+ for i, b in enumerate(self.messages[0].data):
+ self._msg.abData[i] = b
+ if autostart:
+ self.start()
+
+ def start(self):
+ """Start transmitting message (add to list if needed)."""
+ if self._index is None:
+ self._index = ctypes.c_uint32()
+ _canlib.canSchedulerAddMessage(self._scheduler, self._msg, self._index)
+ _canlib.canSchedulerStartMessage(self._scheduler, self._index, self._count)
+
+ def pause(self):
+ """Pause transmitting message (keep it in the list)."""
+ _canlib.canSchedulerStopMessage(self._scheduler, self._index)
+
+ def stop(self):
+ """Stop transmitting message (remove from list)."""
+ # Remove it completely instead of just stopping it to avoid filling up
+ # the list with permanently stopped messages
+ _canlib.canSchedulerRemMessage(self._scheduler, self._index)
+ self._index = None
+
+
+def _format_can_status(status_flags: int):
+ """
+ Format a status bitfield found in CAN_MSGTYPE_STATUS messages or in dwStatus
+ field in CANLINESTATUS.
+
+ Valid states are defined in the CAN_STATUS_* constants in cantype.h
+ """
+ states = []
+ for flag, description in CAN_STATUS_FLAGS.items():
+ if status_flags & flag:
+ states.append(description)
+ status_flags &= ~flag
+
+ if status_flags:
+ states.append(f"unknown state 0x{status_flags:02x}")
+
+ if states:
+ return "CAN status message: {}".format(", ".join(states))
+ else:
+ return "Empty CAN status message"
+
+
+def get_ixxat_hwids():
+ """Get a list of hardware ids of all available IXXAT devices."""
+ hwids = []
+ device_handle = HANDLE()
+ device_info = structures.VCIDEVICEINFO()
+
+ _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle))
+ while True:
+ try:
+ _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info))
+ except StopIteration:
+ break
+ else:
+ hwids.append(device_info.UniqueHardwareId.AsChar.decode("ascii"))
+ _canlib.vciEnumDeviceClose(device_handle)
+
+ return hwids
+
+
+def _detect_available_configs() -> Sequence["AutoDetectedIxxatConfig"]:
+ config_list = [] # list in which to store the resulting bus kwargs
+
+ # used to detect HWID
+ device_handle = HANDLE()
+ device_info = structures.VCIDEVICEINFO()
+
+ # used to attempt to open channels
+ channel_handle = HANDLE()
+ device_handle2 = HANDLE()
+
+ try:
+ _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle))
+ while True:
+ try:
+ _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info))
+ except StopIteration:
+ break
+ else:
+ hwid = device_info.UniqueHardwareId.AsChar.decode("ascii")
+ _canlib.vciDeviceOpen(
+ ctypes.byref(device_info.VciObjectId),
+ ctypes.byref(device_handle2),
+ )
+ for channel in range(4):
+ try:
+ _canlib.canChannelOpen(
+ device_handle2,
+ channel,
+ constants.FALSE,
+ ctypes.byref(channel_handle),
+ )
+ except Exception:
+ # Array outside of bounds error == accessing a channel not in the hardware
+ break
+ else:
+ _canlib.canChannelClose(channel_handle)
+ config_list.append(
+ {
+ "interface": "ixxat",
+ "channel": channel,
+ "unique_hardware_id": hwid,
+ }
+ )
+ _canlib.vciDeviceClose(device_handle2)
+ _canlib.vciEnumDeviceClose(device_handle)
+ except AttributeError:
+ pass # _canlib is None in the CI tests -> return a blank list
+
+ return config_list
+
+
+class AutoDetectedIxxatConfig(AutoDetectedConfig):
+ unique_hardware_id: int
diff --git a/can/interfaces/ixxat/canlib_vcinpl2.py b/can/interfaces/ixxat/canlib_vcinpl2.py
new file mode 100644
index 000000000..f74d4cece
--- /dev/null
+++ b/can/interfaces/ixxat/canlib_vcinpl2.py
@@ -0,0 +1,1093 @@
+"""
+Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems
+
+TODO: We could implement this interface such that setting other filters
+ could work when the initial filters were set to zero using the
+ software fallback. Or could the software filters even be changed
+ after the connection was opened? We need to document that bahaviour!
+ See also the NICAN interface.
+
+"""
+
+import ctypes
+import functools
+import logging
+import sys
+import time
+import warnings
+from collections.abc import Callable, Sequence
+
+from can import (
+ BusABC,
+ CanProtocol,
+ CyclicSendTaskABC,
+ LimitedDurationCyclicSendTaskABC,
+ Message,
+ RestartableCyclicTaskABC,
+)
+from can.ctypesutil import HANDLE, PHANDLE, CLibrary
+from can.ctypesutil import HRESULT as ctypes_HRESULT
+from can.exceptions import CanInitializationError, CanInterfaceNotImplementedError
+from can.util import deprecated_args_alias, dlc2len, len2dlc
+
+from . import constants, structures
+from .exceptions import *
+
+__all__ = [
+ "IXXATBus",
+ "VCIBusOffError",
+ "VCIDeviceNotFoundError",
+ "VCIError",
+ "VCITimeout",
+ "vciFormatError",
+]
+
+log = logging.getLogger("can.ixxat")
+
+# Hack to have vciFormatError as a free function, see below
+vciFormatError = None
+
+# main ctypes instance
+_canlib = None
+# TODO: Use ECI driver for linux
+if sys.platform == "win32" or sys.platform == "cygwin":
+ try:
+ _canlib = CLibrary("vcinpl2.dll")
+ except Exception as e:
+ log.warning("Cannot load IXXAT vcinpl library: %s", e)
+else:
+ # Will not work on other systems, but have it importable anyway for
+ # tests/sphinx
+ log.warning("IXXAT VCI library does not work on %s platform", sys.platform)
+
+
+def __vciFormatErrorExtended(
+ library_instance: CLibrary, function: Callable, vret: int, args: tuple
+):
+ """Format a VCI error and attach failed function, decoded HRESULT and arguments
+ :param CLibrary library_instance:
+ Mapped instance of IXXAT vcinpl library
+ :param callable function:
+ Failed function
+ :param HRESULT vret:
+ HRESULT returned by vcinpl call
+ :param args:
+ Arbitrary arguments tuple
+ :return:
+ Formatted string
+ """
+ # TODO: make sure we don't generate another exception
+ return (
+ f"{__vciFormatError(library_instance, function, vret)} - arguments were {args}"
+ )
+
+
+def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int):
+ """Format a VCI error and attach failed function and decoded HRESULT
+ :param CLibrary library_instance:
+ Mapped instance of IXXAT vcinpl library
+ :param callable function:
+ Failed function
+ :param HRESULT vret:
+ HRESULT returned by vcinpl call
+ :return:
+ Formatted string
+ """
+ buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN)
+ ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN)
+ library_instance.vciFormatError(vret, buf, constants.VCI_MAX_ERRSTRLEN)
+ return "function {} failed ({})".format(
+ function._name, buf.value.decode("utf-8", "replace")
+ )
+
+
+def __check_status(result, function, args):
+ """
+ Check the result of a vcinpl function call and raise appropriate exception
+ in case of an error. Used as errcheck function when mapping C functions
+ with ctypes.
+ :param result:
+ Function call numeric result
+ :param callable function:
+ Called function
+ :param args:
+ Arbitrary arguments tuple
+ :raise:
+ :class:VCITimeout
+ :class:VCIRxQueueEmptyError
+ :class:StopIteration
+ :class:VCIError
+ """
+ if result == constants.VCI_E_TIMEOUT:
+ raise VCITimeout(f"Function {function._name} timed out")
+ elif result == constants.VCI_E_RXQUEUE_EMPTY:
+ raise VCIRxQueueEmptyError()
+ elif result == constants.VCI_E_NO_MORE_ITEMS:
+ raise StopIteration()
+ elif result == constants.VCI_E_ACCESSDENIED:
+ pass # not a real error, might happen if another program has initialized the bus
+ elif result != constants.VCI_OK:
+ raise VCIError(vciFormatError(function, result))
+
+ return result
+
+
+try:
+ hresult_type = ctypes.c_ulong
+ # Map all required symbols and initialize library ---------------------------
+ # HRESULT VCIAPI vciInitialize ( void );
+ _canlib.map_symbol("vciInitialize", hresult_type, (), __check_status)
+
+ # void VCIAPI vciFormatError (HRESULT hrError, PCHAR pszText, UINT32 dwsize);
+ try:
+ _canlib.map_symbol(
+ "vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32)
+ )
+ except ImportError:
+ _canlib.map_symbol(
+ "vciFormatErrorA", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32)
+ )
+ _canlib.vciFormatError = _canlib.vciFormatErrorA
+ # Hack to have vciFormatError as a free function
+ vciFormatError = functools.partial(__vciFormatError, _canlib)
+
+ # HRESULT VCIAPI vciEnumDeviceOpen( OUT PHANDLE hEnum );
+ _canlib.map_symbol("vciEnumDeviceOpen", hresult_type, (PHANDLE,), __check_status)
+ # HRESULT VCIAPI vciEnumDeviceClose ( IN HANDLE hEnum );
+ _canlib.map_symbol("vciEnumDeviceClose", hresult_type, (HANDLE,), __check_status)
+ # HRESULT VCIAPI vciEnumDeviceNext( IN HANDLE hEnum, OUT PVCIDEVICEINFO pInfo );
+ _canlib.map_symbol(
+ "vciEnumDeviceNext",
+ hresult_type,
+ (HANDLE, structures.PVCIDEVICEINFO),
+ __check_status,
+ )
+
+ # HRESULT VCIAPI vciDeviceOpen( IN REFVCIID rVciid, OUT PHANDLE phDevice );
+ _canlib.map_symbol(
+ "vciDeviceOpen", hresult_type, (structures.PVCIID, PHANDLE), __check_status
+ )
+ # HRESULT vciDeviceClose( HANDLE hDevice )
+ _canlib.map_symbol("vciDeviceClose", hresult_type, (HANDLE,), __check_status)
+
+ # HRESULT VCIAPI canChannelOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, IN BOOL fExclusive, OUT PHANDLE phCanChn );
+ _canlib.map_symbol(
+ "canChannelOpen",
+ hresult_type,
+ (HANDLE, ctypes.c_uint32, ctypes.c_long, PHANDLE),
+ __check_status,
+ )
+ # EXTERN_C HRESULT VCIAPI
+ # canChannelInitialize( IN HANDLE hCanChn,
+ # IN UINT16 wRxFifoSize,
+ # IN UINT16 wRxThreshold,
+ # IN UINT16 wTxFifoSize,
+ # IN UINT16 wTxThreshold,
+ # IN UINT32 dwFilterSize,
+ # IN UINT8 bFilterMode );
+ _canlib.map_symbol(
+ "canChannelInitialize",
+ hresult_type,
+ (
+ HANDLE,
+ ctypes.c_uint16,
+ ctypes.c_uint16,
+ ctypes.c_uint16,
+ ctypes.c_uint16,
+ ctypes.c_uint32,
+ ctypes.c_uint8,
+ ),
+ __check_status,
+ )
+ # EXTERN_C HRESULT VCIAPI canChannelActivate( IN HANDLE hCanChn, IN BOOL fEnable );
+ _canlib.map_symbol(
+ "canChannelActivate", hresult_type, (HANDLE, ctypes.c_long), __check_status
+ )
+ # HRESULT canChannelClose( HANDLE hChannel )
+ _canlib.map_symbol("canChannelClose", hresult_type, (HANDLE,), __check_status)
+ # EXTERN_C HRESULT VCIAPI canChannelReadMessage( IN HANDLE hCanChn, IN UINT32 dwMsTimeout, OUT PCANMSG2 pCanMsg );
+ _canlib.map_symbol(
+ "canChannelReadMessage",
+ hresult_type,
+ (HANDLE, ctypes.c_uint32, structures.PCANMSG2),
+ __check_status,
+ )
+ # HRESULT canChannelPeekMessage(HANDLE hChannel,PCANMSG2 pCanMsg );
+ _canlib.map_symbol(
+ "canChannelPeekMessage",
+ hresult_type,
+ (HANDLE, structures.PCANMSG2),
+ __check_status,
+ )
+ # HRESULT canChannelWaitTxEvent (HANDLE hChannel UINT32 dwMsTimeout );
+ _canlib.map_symbol(
+ "canChannelWaitTxEvent",
+ hresult_type,
+ (HANDLE, ctypes.c_uint32),
+ __check_status,
+ )
+ # HRESULT canChannelWaitRxEvent (HANDLE hChannel, UINT32 dwMsTimeout );
+ _canlib.map_symbol(
+ "canChannelWaitRxEvent",
+ hresult_type,
+ (HANDLE, ctypes.c_uint32),
+ __check_status,
+ )
+ # HRESULT canChannelPostMessage (HANDLE hChannel, PCANMSG2 pCanMsg );
+ _canlib.map_symbol(
+ "canChannelPostMessage",
+ hresult_type,
+ (HANDLE, structures.PCANMSG2),
+ __check_status,
+ )
+ # HRESULT canChannelSendMessage (HANDLE hChannel, UINT32 dwMsTimeout, PCANMSG2 pCanMsg );
+ _canlib.map_symbol(
+ "canChannelSendMessage",
+ hresult_type,
+ (HANDLE, ctypes.c_uint32, structures.PCANMSG2),
+ __check_status,
+ )
+
+ # EXTERN_C HRESULT VCIAPI canControlOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, OUT PHANDLE phCanCtl );
+ _canlib.map_symbol(
+ "canControlOpen",
+ hresult_type,
+ (HANDLE, ctypes.c_uint32, PHANDLE),
+ __check_status,
+ )
+ # EXTERN_C HRESULT VCIAPI
+ # canControlInitialize( IN HANDLE hCanCtl,
+ # IN UINT8 bOpMode,
+ # IN UINT8 bExMode,
+ # IN UINT8 bSFMode,
+ # IN UINT8 bEFMode,
+ # IN UINT32 dwSFIds,
+ # IN UINT32 dwEFIds,
+ # IN PCANBTP pBtpSDR,
+ # IN PCANBTP pBtpFDR );
+ _canlib.map_symbol(
+ "canControlInitialize",
+ hresult_type,
+ (
+ HANDLE,
+ ctypes.c_uint8,
+ ctypes.c_uint8,
+ ctypes.c_uint8,
+ ctypes.c_uint8,
+ ctypes.c_uint32,
+ ctypes.c_uint32,
+ structures.PCANBTP,
+ structures.PCANBTP,
+ ),
+ __check_status,
+ )
+ # EXTERN_C HRESULT VCIAPI canControlClose( IN HANDLE hCanCtl );
+ _canlib.map_symbol("canControlClose", hresult_type, (HANDLE,), __check_status)
+ # EXTERN_C HRESULT VCIAPI canControlReset( IN HANDLE hCanCtl );
+ _canlib.map_symbol("canControlReset", hresult_type, (HANDLE,), __check_status)
+ # EXTERN_C HRESULT VCIAPI canControlStart( IN HANDLE hCanCtl, IN BOOL fStart );
+ _canlib.map_symbol(
+ "canControlStart", hresult_type, (HANDLE, ctypes.c_long), __check_status
+ )
+ # EXTERN_C HRESULT VCIAPI canControlGetStatus( IN HANDLE hCanCtl, OUT PCANLINESTATUS2 pStatus );
+ _canlib.map_symbol(
+ "canControlGetStatus",
+ hresult_type,
+ (HANDLE, structures.PCANLINESTATUS2),
+ __check_status,
+ )
+ # EXTERN_C HRESULT VCIAPI canControlGetCaps( IN HANDLE hCanCtl, OUT PCANCAPABILITIES2 pCanCaps );
+ _canlib.map_symbol(
+ "canControlGetCaps",
+ hresult_type,
+ (HANDLE, structures.PCANCAPABILITIES2),
+ __check_status,
+ )
+ # EXTERN_C HRESULT VCIAPI canControlSetAccFilter( IN HANDLE hCanCtl, IN BOOL fExtend, IN UINT32 dwCode, IN UINT32 dwMask );
+ _canlib.map_symbol(
+ "canControlSetAccFilter",
+ hresult_type,
+ (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canControlAddFilterIds (HANDLE hControl, BOOL fExtended, UINT32 dwCode, UINT32 dwMask);
+ _canlib.map_symbol(
+ "canControlAddFilterIds",
+ hresult_type,
+ (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canControlRemFilterIds (HANDLE hControl, BOOL fExtendend, UINT32 dwCode, UINT32 dwMask );
+ _canlib.map_symbol(
+ "canControlRemFilterIds",
+ hresult_type,
+ (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canSchedulerOpen (HANDLE hDevice, UINT32 dwCanNo, PHANDLE phScheduler );
+ _canlib.map_symbol(
+ "canSchedulerOpen",
+ hresult_type,
+ (HANDLE, ctypes.c_uint32, PHANDLE),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canSchedulerClose (HANDLE hScheduler );
+ _canlib.map_symbol("canSchedulerClose", hresult_type, (HANDLE,), __check_status)
+ # EXTERN_C HRESULT canSchedulerGetCaps (HANDLE hScheduler, PCANCAPABILITIES2 pCaps );
+ _canlib.map_symbol(
+ "canSchedulerGetCaps",
+ hresult_type,
+ (HANDLE, structures.PCANCAPABILITIES2),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canSchedulerActivate ( HANDLE hScheduler, BOOL fEnable );
+ _canlib.map_symbol(
+ "canSchedulerActivate", hresult_type, (HANDLE, ctypes.c_int), __check_status
+ )
+ # EXTERN_C HRESULT canSchedulerAddMessage (HANDLE hScheduler, PCANCYCLICTXMSG2 pMessage, PUINT32 pdwIndex );
+ _canlib.map_symbol(
+ "canSchedulerAddMessage",
+ hresult_type,
+ (HANDLE, structures.PCANCYCLICTXMSG2, ctypes.POINTER(ctypes.c_uint32)),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canSchedulerRemMessage (HANDLE hScheduler, UINT32 dwIndex );
+ _canlib.map_symbol(
+ "canSchedulerRemMessage",
+ hresult_type,
+ (HANDLE, ctypes.c_uint32),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canSchedulerStartMessage (HANDLE hScheduler, UINT32 dwIndex, UINT16 dwCount );
+ _canlib.map_symbol(
+ "canSchedulerStartMessage",
+ hresult_type,
+ (HANDLE, ctypes.c_uint32, ctypes.c_uint16),
+ __check_status,
+ )
+ # EXTERN_C HRESULT canSchedulerStopMessage (HANDLE hScheduler, UINT32 dwIndex );
+ _canlib.map_symbol(
+ "canSchedulerStopMessage",
+ hresult_type,
+ (HANDLE, ctypes.c_uint32),
+ __check_status,
+ )
+ _canlib.vciInitialize()
+except AttributeError:
+ # In case _canlib == None meaning we're not on win32/no lib found
+ pass
+except Exception as e:
+ log.warning("Could not initialize IXXAT VCI library: %s", e)
+# ---------------------------------------------------------------------------
+
+
+CAN_INFO_MESSAGES = {
+ constants.CAN_INFO_START: "CAN started",
+ constants.CAN_INFO_STOP: "CAN stopped",
+ constants.CAN_INFO_RESET: "CAN reset",
+}
+
+CAN_ERROR_MESSAGES = {
+ constants.CAN_ERROR_STUFF: "CAN bit stuff error",
+ constants.CAN_ERROR_FORM: "CAN form error",
+ constants.CAN_ERROR_ACK: "CAN acknowledgment error",
+ constants.CAN_ERROR_BIT: "CAN bit error",
+ constants.CAN_ERROR_CRC: "CAN CRC error",
+ constants.CAN_ERROR_OTHER: "Other (unknown) CAN error",
+}
+
+CAN_STATUS_FLAGS = {
+ constants.CAN_STATUS_TXPEND: "transmission pending",
+ constants.CAN_STATUS_OVRRUN: "data overrun occurred",
+ constants.CAN_STATUS_ERRLIM: "error warning limit exceeded",
+ constants.CAN_STATUS_BUSOFF: "bus off",
+ constants.CAN_STATUS_ININIT: "init mode active",
+ constants.CAN_STATUS_BUSCERR: "bus coupling error",
+}
+# ----------------------------------------------------------------------------
+
+
+class IXXATBus(BusABC):
+ """The CAN Bus implemented for the IXXAT interface.
+
+ .. warning::
+
+ This interface does implement efficient filtering of messages, but
+ the filters have to be set in ``__init__`` using the ``can_filters`` parameter.
+ Using :meth:`~can.BusABC.set_filters` does not work.
+
+ """
+
+ @deprecated_args_alias(
+ deprecation_start="4.0.0",
+ deprecation_end="5.0.0",
+ UniqueHardwareId="unique_hardware_id",
+ rxFifoSize="rx_fifo_size",
+ txFifoSize="tx_fifo_size",
+ )
+ def __init__(
+ self,
+ channel: int,
+ can_filters=None,
+ receive_own_messages: int = False,
+ unique_hardware_id: int | None = None,
+ extended: bool = True,
+ rx_fifo_size: int = 1024,
+ tx_fifo_size: int = 128,
+ bitrate: int = 500000,
+ data_bitrate: int = 2000000,
+ sjw_abr: int | None = None,
+ tseg1_abr: int | None = None,
+ tseg2_abr: int | None = None,
+ sjw_dbr: int | None = None,
+ tseg1_dbr: int | None = None,
+ tseg2_dbr: int | None = None,
+ ssp_dbr: int | None = None,
+ **kwargs,
+ ):
+ """
+ :param channel:
+ The Channel id to create this bus with.
+
+ :param can_filters:
+ See :meth:`can.BusABC.set_filters`.
+
+ :param receive_own_messages:
+ Enable self-reception of sent messages.
+
+ :param unique_hardware_id:
+ unique_hardware_id to connect (optional, will use the first found if not supplied)
+
+ :param extended:
+ Default True, enables the capability to use extended IDs.
+
+ :param rx_fifo_size:
+ Receive fifo size (default 1024)
+
+ :param tx_fifo_size:
+ Transmit fifo size (default 128)
+
+ :param bitrate:
+ Channel bitrate in bit/s
+
+ :param data_bitrate:
+ Channel bitrate in bit/s (only in CAN-Fd if baudrate switch enabled).
+
+ :param sjw_abr:
+ Bus timing value sample jump width (arbitration).
+
+ :param tseg1_abr:
+ Bus timing value tseg1 (arbitration)
+
+ :param tseg2_abr:
+ Bus timing value tseg2 (arbitration)
+
+ :param sjw_dbr:
+ Bus timing value sample jump width (data)
+
+ :param tseg1_dbr:
+ Bus timing value tseg1 (data). Only takes effect with fd and bitrate switch enabled.
+
+ :param tseg2_dbr:
+ Bus timing value tseg2 (data). Only takes effect with fd and bitrate switch enabled.
+
+ :param ssp_dbr:
+ Secondary sample point (data). Only takes effect with fd and bitrate switch enabled.
+
+ """
+ if _canlib is None:
+ raise CanInterfaceNotImplementedError(
+ "The IXXAT VCI library has not been initialized. Check the logs for more details."
+ )
+ log.info("CAN Filters: %s", can_filters)
+ # Configuration options
+ self._receive_own_messages = receive_own_messages
+ # Usually comes as a string from the config file
+ channel = int(channel)
+
+ if bitrate not in constants.CAN_BITRATE_PRESETS and (
+ tseg1_abr is None or tseg2_abr is None or sjw_abr is None
+ ):
+ raise ValueError(
+ f"To use bitrate {bitrate} (that has not predefined preset) is mandatory "
+ f"to use also parameters tseg1_abr, tseg2_abr and swj_abr"
+ )
+ if data_bitrate not in constants.CAN_DATABITRATE_PRESETS and (
+ tseg1_dbr is None or tseg2_dbr is None or sjw_dbr is None
+ ):
+ raise ValueError(
+ f"To use data_bitrate {data_bitrate} (that has not predefined preset) is mandatory "
+ f"to use also parameters tseg1_dbr, tseg2_dbr and swj_dbr"
+ )
+
+ if rx_fifo_size <= 0:
+ raise ValueError("rx_fifo_size must be > 0")
+
+ if tx_fifo_size <= 0:
+ raise ValueError("tx_fifo_size must be > 0")
+
+ if channel < 0:
+ raise ValueError("channel number must be >= 0")
+
+ self._device_handle = HANDLE()
+ self._device_info = structures.VCIDEVICEINFO()
+ self._control_handle = HANDLE()
+ self._channel_handle = HANDLE()
+ self._channel_capabilities = structures.CANCAPABILITIES2()
+ self._message = structures.CANMSG2()
+ self._payload = (ctypes.c_byte * 64)()
+ self._can_protocol = CanProtocol.CAN_FD
+
+ # Search for supplied device
+ if unique_hardware_id is None:
+ log.info("Searching for first available device")
+ else:
+ log.info("Searching for unique HW ID %s", unique_hardware_id)
+ _canlib.vciEnumDeviceOpen(ctypes.byref(self._device_handle))
+ while True:
+ try:
+ _canlib.vciEnumDeviceNext(
+ self._device_handle, ctypes.byref(self._device_info)
+ )
+ except StopIteration:
+ if unique_hardware_id is None:
+ raise VCIDeviceNotFoundError(
+ "No IXXAT device(s) connected or device(s) in use by other process(es)."
+ ) from None
+ else:
+ raise VCIDeviceNotFoundError(
+ f"Unique HW ID {unique_hardware_id} not connected or not available."
+ ) from None
+ else:
+ if (unique_hardware_id is None) or (
+ self._device_info.UniqueHardwareId.AsChar
+ == bytes(unique_hardware_id, "ascii")
+ ):
+ break
+ else:
+ log.debug(
+ "Ignoring IXXAT with hardware id '%s'.",
+ self._device_info.UniqueHardwareId.AsChar.decode("ascii"),
+ )
+ _canlib.vciEnumDeviceClose(self._device_handle)
+
+ try:
+ _canlib.vciDeviceOpen(
+ ctypes.byref(self._device_info.VciObjectId),
+ ctypes.byref(self._device_handle),
+ )
+ except Exception as exception:
+ raise CanInitializationError(
+ f"Could not open device: {exception}"
+ ) from exception
+
+ log.info("Using unique HW ID %s", self._device_info.UniqueHardwareId.AsChar)
+
+ log.info(
+ "Initializing channel %d in shared mode, %d rx buffers, %d tx buffers",
+ channel,
+ rx_fifo_size,
+ tx_fifo_size,
+ )
+
+ try:
+ _canlib.canChannelOpen(
+ self._device_handle,
+ channel,
+ constants.FALSE,
+ ctypes.byref(self._channel_handle),
+ )
+ except Exception as exception:
+ raise CanInitializationError(
+ f"Could not open and initialize channel: {exception}"
+ ) from exception
+
+ # Signal TX/RX events when at least one frame has been handled
+ _canlib.canChannelInitialize(
+ self._channel_handle,
+ rx_fifo_size,
+ 1,
+ tx_fifo_size,
+ 1,
+ 0,
+ constants.CAN_FILTER_PASS,
+ )
+ _canlib.canChannelActivate(self._channel_handle, constants.TRUE)
+
+ pBtpSDR = IXXATBus._canptb_build(
+ defaults=constants.CAN_BITRATE_PRESETS,
+ bitrate=bitrate,
+ tseg1=tseg1_abr,
+ tseg2=tseg2_abr,
+ sjw=sjw_abr,
+ ssp=0,
+ )
+ pBtpFDR = IXXATBus._canptb_build(
+ defaults=constants.CAN_DATABITRATE_PRESETS,
+ bitrate=data_bitrate,
+ tseg1=tseg1_dbr,
+ tseg2=tseg2_dbr,
+ sjw=sjw_dbr,
+ ssp=ssp_dbr if ssp_dbr is not None else tseg1_dbr,
+ )
+
+ log.info(
+ "Initializing control %d with SDR={%s}, FDR={%s}",
+ channel,
+ pBtpSDR,
+ pBtpFDR,
+ )
+ _canlib.canControlOpen(
+ self._device_handle, channel, ctypes.byref(self._control_handle)
+ )
+
+ _canlib.canControlGetCaps(
+ self._control_handle, ctypes.byref(self._channel_capabilities)
+ )
+
+ # check capabilities
+ bOpMode = constants.CAN_OPMODE_UNDEFINED
+ if (
+ self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT
+ ) != 0:
+ # controller supportes CAN_OPMODE_STANDARD and CAN_OPMODE_EXTENDED at the same time
+ bOpMode |= constants.CAN_OPMODE_STANDARD # enable both 11 bits reception
+ if extended: # parameter from configuration
+ bOpMode |= constants.CAN_OPMODE_EXTENDED # enable 29 bits reception
+ elif (
+ self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT
+ ) != 0:
+ log.warning(
+ "Channel %d capabilities allow either basic or extended IDs, but not both. using %s according to parameter [extended=%s]",
+ channel,
+ "extended" if extended else "basic",
+ "True" if extended else "False",
+ )
+ bOpMode |= (
+ constants.CAN_OPMODE_EXTENDED
+ if extended
+ else constants.CAN_OPMODE_STANDARD
+ )
+
+ if (
+ self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_ERRFRAME
+ ) != 0:
+ bOpMode |= constants.CAN_OPMODE_ERRFRAME
+
+ bExMode = constants.CAN_EXMODE_DISABLED
+ if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_EXTDATA) != 0:
+ bExMode |= constants.CAN_EXMODE_EXTDATALEN
+
+ if (
+ self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_FASTDATA
+ ) != 0:
+ bExMode |= constants.CAN_EXMODE_FASTDATA
+
+ _canlib.canControlInitialize(
+ self._control_handle,
+ bOpMode,
+ bExMode,
+ constants.CAN_FILTER_PASS,
+ constants.CAN_FILTER_PASS,
+ 0,
+ 0,
+ ctypes.byref(pBtpSDR),
+ ctypes.byref(pBtpFDR),
+ )
+
+ # With receive messages, this field contains the relative reception time of
+ # the message in ticks. The resolution of a tick can be calculated from the fields
+ # dwClockFreq and dwTscDivisor of the structure CANCAPABILITIES in accordance with the following formula:
+ # frequency [1/s] = dwClockFreq / dwTscDivisor
+ self._tick_resolution = (
+ self._channel_capabilities.dwTscClkFreq
+ / self._channel_capabilities.dwTscDivisor
+ )
+
+ # Setup filters before starting the channel
+ if can_filters:
+ log.info("The IXXAT VCI backend is filtering messages")
+ # Disable every message coming in
+ for extended in (0, 1):
+ _canlib.canControlSetAccFilter(
+ self._control_handle,
+ extended,
+ constants.CAN_ACC_CODE_NONE,
+ constants.CAN_ACC_MASK_NONE,
+ )
+ for can_filter in can_filters:
+ # Filters define what messages are accepted
+ code = int(can_filter["can_id"])
+ mask = int(can_filter["can_mask"])
+ extended = can_filter.get("extended", False)
+ _canlib.canControlAddFilterIds(
+ self._control_handle, 1 if extended else 0, code << 1, mask << 1
+ )
+ log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask)
+
+ # Start the CAN controller. Messages will be forwarded to the channel
+ start_begin = time.time()
+ _canlib.canControlStart(self._control_handle, constants.TRUE)
+ start_end = time.time()
+
+ # Calculate an offset to make them relative to epoch
+ # Assume that the time offset is in the middle of the start command
+ self._timeoffset = start_begin + (start_end - start_begin / 2)
+ self._overrunticks = 0
+ self._starttickoffset = 0
+
+ # For cyclic transmit list. Set when .send_periodic() is first called
+ self._scheduler = None
+ self._scheduler_resolution = None
+ self.channel = channel
+
+ # Usually you get back 3 messages like "CAN initialized" ecc...
+ # Clear the FIFO by filter them out with low timeout
+ for _ in range(rx_fifo_size):
+ try:
+ _canlib.canChannelReadMessage(
+ self._channel_handle, 0, ctypes.byref(self._message)
+ )
+ except (VCITimeout, VCIRxQueueEmptyError):
+ break
+
+ super().__init__(channel=channel, can_filters=None, **kwargs)
+
+ @staticmethod
+ def _canptb_build(defaults, bitrate, tseg1, tseg2, sjw, ssp):
+ if bitrate in defaults:
+ d = defaults[bitrate]
+ if tseg1 is None:
+ tseg1 = d.wTS1
+ if tseg2 is None:
+ tseg2 = d.wTS2
+ if sjw is None:
+ sjw = d.wSJW
+ if ssp is None:
+ ssp = d.wTDO
+ dw_mode = d.dwMode
+ else:
+ dw_mode = 0
+
+ return structures.CANBTP(
+ dwMode=dw_mode,
+ dwBPS=bitrate,
+ wTS1=tseg1,
+ wTS2=tseg2,
+ wSJW=sjw,
+ wTDO=ssp,
+ )
+
+ def _inWaiting(self):
+ try:
+ _canlib.canChannelWaitRxEvent(self._channel_handle, 0)
+ except VCITimeout:
+ return 0
+ else:
+ return 1
+
+ def flush_tx_buffer(self):
+ """Flushes the transmit buffer on the IXXAT"""
+ # TODO #64: no timeout?
+ _canlib.canChannelWaitTxEvent(self._channel_handle, constants.INFINITE)
+
+ def _recv_internal(self, timeout):
+ """Read a message from IXXAT device."""
+
+ # TODO: handling CAN error messages?
+ data_received = False
+
+ if timeout == 0:
+ # Peek without waiting
+ try:
+ _canlib.canChannelPeekMessage(
+ self._channel_handle, ctypes.byref(self._message)
+ )
+ except (VCITimeout, VCIRxQueueEmptyError, VCIError):
+ # VCIError means no frame available (canChannelPeekMessage returned different from zero)
+ return None, True
+ else:
+ if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA:
+ data_received = True
+ else:
+ # Wait if no message available
+ if timeout is None or timeout < 0:
+ remaining_ms = constants.INFINITE
+ t0 = None
+ else:
+ timeout_ms = int(timeout * 1000)
+ remaining_ms = timeout_ms
+ t0 = time.perf_counter()
+
+ while True:
+ try:
+ _canlib.canChannelReadMessage(
+ self._channel_handle, remaining_ms, ctypes.byref(self._message)
+ )
+ except (VCITimeout, VCIRxQueueEmptyError):
+ # Ignore the 2 errors, the timeout is handled manually with the perf_counter()
+ pass
+ else:
+ # See if we got a data or info/error messages
+ if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA:
+ data_received = True
+ break
+ elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_INFO:
+ log.info(
+ CAN_INFO_MESSAGES.get(
+ self._message.abData[0],
+ f"Unknown CAN info message code {self._message.abData[0]}",
+ )
+ )
+ # Handle CAN start info message
+ elif self._message.abData[0] == constants.CAN_INFO_START:
+ self._starttickoffset = self._message.dwTime
+ elif (
+ self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR
+ ):
+ log.warning(
+ CAN_ERROR_MESSAGES.get(
+ self._message.abData[0],
+ f"Unknown CAN error message code {self._message.abData[0]}",
+ )
+ )
+
+ elif (
+ self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_STATUS
+ ):
+ log.info(_format_can_status(self._message.abData[0]))
+ if self._message.abData[0] & constants.CAN_STATUS_BUSOFF:
+ raise VCIBusOffError("Controller is in BUSOFF state")
+
+ elif (
+ self._message.uMsgInfo.Bits.type
+ == constants.CAN_MSGTYPE_TIMEOVR
+ ):
+ # Add the number of timestamp overruns to the high word
+ self._overrunticks += self._message.dwMsgId << 32
+ else:
+ log.warning("Unexpected message info type")
+
+ if t0 is not None:
+ remaining_ms = timeout_ms - int((time.perf_counter() - t0) * 1000)
+ if remaining_ms < 0:
+ break
+
+ if not data_received:
+ # Timed out / can message type is not DATA
+ return None, True
+
+ data_len = dlc2len(self._message.uMsgInfo.Bits.dlc)
+ rx_msg = Message(
+ timestamp=(
+ (self._message.dwTime + self._overrunticks - self._starttickoffset)
+ / self._tick_resolution
+ )
+ + self._timeoffset,
+ is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr),
+ is_fd=bool(self._message.uMsgInfo.Bits.edl),
+ is_rx=True,
+ is_error_frame=bool(
+ self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR
+ ),
+ bitrate_switch=bool(self._message.uMsgInfo.Bits.fdr),
+ error_state_indicator=bool(self._message.uMsgInfo.Bits.esi),
+ is_extended_id=bool(self._message.uMsgInfo.Bits.ext),
+ arbitration_id=self._message.dwMsgId,
+ dlc=data_len,
+ data=self._message.abData[:data_len],
+ channel=self.channel,
+ )
+
+ return rx_msg, True
+
+ def send(self, msg: Message, timeout: float | None = None) -> None:
+ """
+ Sends a message on the bus. The interface may buffer the message.
+
+ :param msg:
+ The message to send.
+ :param timeout:
+ Timeout after some time.
+ :raise:
+ :class:CanTimeoutError
+ :class:CanOperationError
+ """
+ # This system is not designed to be very efficient
+ message = structures.CANMSG2()
+ message.uMsgInfo.Bits.type = (
+ constants.CAN_MSGTYPE_ERROR
+ if msg.is_error_frame
+ else constants.CAN_MSGTYPE_DATA
+ )
+ message.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0
+ message.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0
+ message.uMsgInfo.Bits.srr = 1 if self._receive_own_messages else 0
+ message.uMsgInfo.Bits.fdr = 1 if msg.bitrate_switch else 0
+ message.uMsgInfo.Bits.esi = 1 if msg.error_state_indicator else 0
+ message.uMsgInfo.Bits.edl = 1 if msg.is_fd else 0
+ message.dwMsgId = msg.arbitration_id
+ if msg.dlc: # this dlc means number of bytes of payload
+ message.uMsgInfo.Bits.dlc = len2dlc(msg.dlc)
+ data_len_dif = msg.dlc - len(msg.data)
+ data = msg.data + bytearray(
+ [0] * data_len_dif
+ ) # pad with zeros until required length
+ adapter = (ctypes.c_uint8 * msg.dlc).from_buffer(data)
+ ctypes.memmove(message.abData, adapter, msg.dlc)
+
+ if timeout:
+ _canlib.canChannelSendMessage(
+ self._channel_handle, int(timeout * 1000), message
+ )
+
+ else:
+ _canlib.canChannelPostMessage(self._channel_handle, message)
+
+ def _send_periodic_internal(
+ self,
+ msgs: Sequence[Message] | Message,
+ period: float,
+ duration: float | None = None,
+ autostart: bool = True,
+ modifier_callback: Callable[[Message], None] | None = None,
+ ) -> CyclicSendTaskABC:
+ """Send a message using built-in cyclic transmit list functionality."""
+ if modifier_callback is None:
+ if self._scheduler is None:
+ self._scheduler = HANDLE()
+ _canlib.canSchedulerOpen(
+ self._device_handle, self.channel, self._scheduler
+ )
+ caps = structures.CANCAPABILITIES2()
+ _canlib.canSchedulerGetCaps(self._scheduler, caps)
+ self._scheduler_resolution = (
+ caps.dwCmsClkFreq / caps.dwCmsDivisor
+ ) # TODO: confirm
+ _canlib.canSchedulerActivate(self._scheduler, constants.TRUE)
+ return CyclicSendTask(
+ self._scheduler,
+ msgs,
+ period,
+ duration,
+ self._scheduler_resolution,
+ autostart=autostart,
+ )
+
+ # fallback to thread based cyclic task
+ warnings.warn(
+ f"{self.__class__.__name__} falls back to a thread-based cyclic task, "
+ "when the `modifier_callback` argument is given.",
+ stacklevel=3,
+ )
+ return BusABC._send_periodic_internal(
+ self,
+ msgs=msgs,
+ period=period,
+ duration=duration,
+ autostart=autostart,
+ modifier_callback=modifier_callback,
+ )
+
+ def shutdown(self):
+ super().shutdown()
+ if self._scheduler is not None:
+ _canlib.canSchedulerClose(self._scheduler)
+ _canlib.canChannelClose(self._channel_handle)
+ _canlib.canControlStart(self._control_handle, constants.FALSE)
+ _canlib.canControlClose(self._control_handle)
+ _canlib.vciDeviceClose(self._device_handle)
+
+
+class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC):
+ """A message in the cyclic transmit list."""
+
+ def __init__(
+ self,
+ scheduler,
+ msgs,
+ period,
+ duration,
+ resolution,
+ autostart: bool = True,
+ ):
+ super().__init__(msgs, period, duration)
+ if len(self.messages) != 1:
+ raise ValueError(
+ "IXXAT Interface only supports periodic transmission of 1 element"
+ )
+
+ self._scheduler = scheduler
+ self._index = None
+ self._count = int(duration / period) if duration else 0
+
+ self._msg = structures.CANCYCLICTXMSG2()
+ self._msg.wCycleTime = round(period * resolution)
+ self._msg.dwMsgId = self.messages[0].arbitration_id
+ self._msg.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA
+ self._msg.uMsgInfo.Bits.ext = 1 if self.messages[0].is_extended_id else 0
+ self._msg.uMsgInfo.Bits.rtr = 1 if self.messages[0].is_remote_frame else 0
+ self._msg.uMsgInfo.Bits.dlc = self.messages[0].dlc
+ for i, b in enumerate(self.messages[0].data):
+ self._msg.abData[i] = b
+ if autostart:
+ self.start()
+
+ def start(self):
+ """Start transmitting message (add to list if needed)."""
+ if self._index is None:
+ self._index = ctypes.c_uint32()
+ _canlib.canSchedulerAddMessage(self._scheduler, self._msg, self._index)
+ _canlib.canSchedulerStartMessage(self._scheduler, self._index, self._count)
+
+ def pause(self):
+ """Pause transmitting message (keep it in the list)."""
+ _canlib.canSchedulerStopMessage(self._scheduler, self._index)
+
+ def stop(self):
+ """Stop transmitting message (remove from list)."""
+ # Remove it completely instead of just stopping it to avoid filling up
+ # the list with permanently stopped messages
+ _canlib.canSchedulerRemMessage(self._scheduler, self._index)
+ self._index = None
+
+
+def _format_can_status(status_flags: int):
+ """
+ Format a status bitfield found in CAN_MSGTYPE_STATUS messages or in dwStatus
+ field in CANLINESTATUS.
+
+ Valid states are defined in the CAN_STATUS_* constants in cantype.h
+ """
+ states = []
+ for flag, description in CAN_STATUS_FLAGS.items():
+ if status_flags & flag:
+ states.append(description)
+ status_flags &= ~flag
+
+ if status_flags:
+ states.append(f"unknown state 0x{status_flags:02x}")
+
+ if states:
+ return "CAN status message: {}".format(", ".join(states))
+ else:
+ return "Empty CAN status message"
+
+
+def get_ixxat_hwids():
+ """Get a list of hardware ids of all available IXXAT devices."""
+ hwids = []
+ device_handle = HANDLE()
+ device_info = structures.VCIDEVICEINFO()
+
+ _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle))
+ while True:
+ try:
+ _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info))
+ except StopIteration:
+ break
+ else:
+ hwids.append(device_info.UniqueHardwareId.AsChar.decode("ascii"))
+ _canlib.vciEnumDeviceClose(device_handle)
+
+ return hwids
diff --git a/can/interfaces/ixxat/constants.py b/can/interfaces/ixxat/constants.py
index 62505dcc5..3bc1aa42e 100644
--- a/can/interfaces/ixxat/constants.py
+++ b/can/interfaces/ixxat/constants.py
@@ -1,149 +1,259 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
-Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems
+Ctypes wrapper module for IXXAT Virtual CAN Interface V4 on win32 systems
Copyright (C) 2016 Giuseppe Corbelli
"""
-FALSE = 0
-TRUE = 1
+from . import structures
+
+FALSE = 0
+TRUE = 1
-INFINITE = 0xFFFFFFFF
+INFINITE = 0xFFFFFFFF
VCI_MAX_ERRSTRLEN = 256
# Bitrates
-CAN_BT0_10KB = 0x31
-CAN_BT1_10KB = 0x1C
-CAN_BT0_20KB = 0x18
-CAN_BT1_20KB = 0x1C
-CAN_BT0_50KB = 0x09
-CAN_BT1_50KB = 0x1C
-CAN_BT0_100KB = 0x04
-CAN_BT1_100KB = 0x1C
-CAN_BT0_125KB = 0x03
-CAN_BT1_125KB = 0x1C
-CAN_BT0_250KB = 0x01
-CAN_BT1_250KB = 0x1C
-CAN_BT0_500KB = 0x00
-CAN_BT1_500KB = 0x1C
-CAN_BT0_800KB = 0x00
-CAN_BT1_800KB = 0x16
-CAN_BT0_1000KB = 0x00
-CAN_BT1_1000KB = 0x14
+CAN_BT0_10KB = 0x31
+CAN_BT1_10KB = 0x1C
+CAN_BT0_20KB = 0x18
+CAN_BT1_20KB = 0x1C
+CAN_BT0_50KB = 0x09
+CAN_BT1_50KB = 0x1C
+CAN_BT0_100KB = 0x04
+CAN_BT1_100KB = 0x1C
+CAN_BT0_125KB = 0x03
+CAN_BT1_125KB = 0x1C
+CAN_BT0_250KB = 0x01
+CAN_BT1_250KB = 0x1C
+CAN_BT0_500KB = 0x00
+CAN_BT1_500KB = 0x1C
+CAN_BT0_667KB = 0x00
+CAN_BT1_667KB = 0x18
+CAN_BT0_800KB = 0x00
+CAN_BT1_800KB = 0x16
+CAN_BT0_1000KB = 0x00
+CAN_BT1_1000KB = 0x14
# Facilities/severities
-SEV_INFO = 0x40000000
-SEV_WARN = 0x80000000
-SEV_ERROR = 0xC0000000
-SEV_MASK = 0xC0000000
-SEV_SUCCESS = 0x00000000
+SEV_INFO = 0x40000000
+SEV_WARN = 0x80000000
+SEV_ERROR = 0xC0000000
+SEV_MASK = 0xC0000000
+SEV_SUCCESS = 0x00000000
-RESERVED_FLAG = 0x10000000
-CUSTOMER_FLAG = 0x20000000
+RESERVED_FLAG = 0x10000000
+CUSTOMER_FLAG = 0x20000000
-STATUS_MASK = 0x0000FFFF
-FACILITY_MASK = 0x0FFF0000
+STATUS_MASK = 0x0000FFFF
+FACILITY_MASK = 0x0FFF0000
# Or so I hope
FACILITY_STD = 0
-SEV_STD_INFO = SEV_INFO |CUSTOMER_FLAG|FACILITY_STD
-SEV_STD_WARN = SEV_WARN |CUSTOMER_FLAG|FACILITY_STD
-SEV_STD_ERROR = SEV_ERROR|CUSTOMER_FLAG|FACILITY_STD
+SEV_STD_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_STD
+SEV_STD_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_STD
+SEV_STD_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_STD
-FACILITY_VCI = 0x00010000
-SEV_VCI_INFO = SEV_INFO |CUSTOMER_FLAG|FACILITY_VCI
-SEV_VCI_WARN = SEV_WARN |CUSTOMER_FLAG|FACILITY_VCI
-SEV_VCI_ERROR = SEV_ERROR|CUSTOMER_FLAG|FACILITY_VCI
+FACILITY_VCI = 0x00010000
+SEV_VCI_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_VCI
+SEV_VCI_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_VCI
+SEV_VCI_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_VCI
-FACILITY_DAL = 0x00020000
-SEV_DAL_INFO = SEV_INFO |CUSTOMER_FLAG|FACILITY_DAL
-SEV_DAL_WARN = SEV_WARN |CUSTOMER_FLAG|FACILITY_DAL
-SEV_DAL_ERROR = SEV_ERROR|CUSTOMER_FLAG|FACILITY_DAL
+FACILITY_DAL = 0x00020000
+SEV_DAL_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_DAL
+SEV_DAL_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_DAL
+SEV_DAL_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_DAL
-FACILITY_CCL = 0x00030000
-SEV_CCL_INFO = SEV_INFO |CUSTOMER_FLAG|FACILITY_CCL
-SEV_CCL_WARN = SEV_WARN |CUSTOMER_FLAG|FACILITY_CCL
-SEV_CCL_ERROR = SEV_ERROR|CUSTOMER_FLAG|FACILITY_CCL
+FACILITY_CCL = 0x00030000
+SEV_CCL_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_CCL
+SEV_CCL_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_CCL
+SEV_CCL_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_CCL
-FACILITY_BAL = 0x00040000
-SEV_BAL_INFO = SEV_INFO |CUSTOMER_FLAG|FACILITY_BAL
-SEV_BAL_WARN = SEV_WARN |CUSTOMER_FLAG|FACILITY_BAL
-SEV_BAL_ERROR = SEV_ERROR|CUSTOMER_FLAG|FACILITY_BAL
+FACILITY_BAL = 0x00040000
+SEV_BAL_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_BAL
+SEV_BAL_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_BAL
+SEV_BAL_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_BAL
# Errors
-VCI_SUCCESS = 0x00
-VCI_OK = 0x00
-VCI_E_UNEXPECTED = SEV_VCI_ERROR | 0x0001
-VCI_E_NOT_IMPLEMENTED = SEV_VCI_ERROR | 0x0002
-VCI_E_OUTOFMEMORY = SEV_VCI_ERROR | 0x0003
-VCI_E_INVALIDARG = SEV_VCI_ERROR | 0x0004
-VCI_E_NOINTERFACE = SEV_VCI_ERROR | 0x0005
-VCI_E_INVPOINTER = SEV_VCI_ERROR | 0x0006
-VCI_E_INVHANDLE = SEV_VCI_ERROR | 0x0007
-VCI_E_ABORT = SEV_VCI_ERROR | 0x0008
-VCI_E_FAIL = SEV_VCI_ERROR | 0x0009
-VCI_E_ACCESSDENIED = SEV_VCI_ERROR | 0x000A
-VCI_E_TIMEOUT = SEV_VCI_ERROR | 0x000B
-VCI_E_BUSY = SEV_VCI_ERROR | 0x000C
-VCI_E_PENDING = SEV_VCI_ERROR | 0x000D
-VCI_E_NO_DATA = SEV_VCI_ERROR | 0x000E
-VCI_E_NO_MORE_ITEMS = SEV_VCI_ERROR | 0x000F
-VCI_E_NOT_INITIALIZED = SEV_VCI_ERROR | 0x0010
+VCI_SUCCESS = 0x00
+VCI_OK = 0x00
+VCI_E_UNEXPECTED = SEV_VCI_ERROR | 0x0001
+VCI_E_NOT_IMPLEMENTED = SEV_VCI_ERROR | 0x0002
+VCI_E_OUTOFMEMORY = SEV_VCI_ERROR | 0x0003
+VCI_E_INVALIDARG = SEV_VCI_ERROR | 0x0004
+VCI_E_NOINTERFACE = SEV_VCI_ERROR | 0x0005
+VCI_E_INVPOINTER = SEV_VCI_ERROR | 0x0006
+VCI_E_INVHANDLE = SEV_VCI_ERROR | 0x0007
+VCI_E_ABORT = SEV_VCI_ERROR | 0x0008
+VCI_E_FAIL = SEV_VCI_ERROR | 0x0009
+VCI_E_ACCESSDENIED = SEV_VCI_ERROR | 0x000A
+VCI_E_TIMEOUT = SEV_VCI_ERROR | 0x000B
+VCI_E_BUSY = SEV_VCI_ERROR | 0x000C
+VCI_E_PENDING = SEV_VCI_ERROR | 0x000D
+VCI_E_NO_DATA = SEV_VCI_ERROR | 0x000E
+VCI_E_NO_MORE_ITEMS = SEV_VCI_ERROR | 0x000F
+VCI_E_NOT_INITIALIZED = SEV_VCI_ERROR | 0x0010
VCI_E_ALREADY_INITIALIZED = SEV_VCI_ERROR | 0x00011
-VCI_E_RXQUEUE_EMPTY = SEV_VCI_ERROR | 0x00012
-VCI_E_TXQUEUE_FULL = SEV_VCI_ERROR | 0x0013
-VCI_E_BUFFER_OVERFLOW = SEV_VCI_ERROR | 0x0014
-VCI_E_INVALID_STATE = SEV_VCI_ERROR | 0x0015
+VCI_E_RXQUEUE_EMPTY = SEV_VCI_ERROR | 0x00012
+VCI_E_TXQUEUE_FULL = SEV_VCI_ERROR | 0x0013
+VCI_E_BUFFER_OVERFLOW = SEV_VCI_ERROR | 0x0014
+VCI_E_INVALID_STATE = SEV_VCI_ERROR | 0x0015
VCI_E_OBJECT_ALREADY_EXISTS = SEV_VCI_ERROR | 0x0016
-VCI_E_INVALID_INDEX = SEV_VCI_ERROR | 0x0017
-VCI_E_END_OF_FILE = SEV_VCI_ERROR | 0x0018
-VCI_E_DISCONNECTED = SEV_VCI_ERROR | 0x0019
+VCI_E_INVALID_INDEX = SEV_VCI_ERROR | 0x0017
+VCI_E_END_OF_FILE = SEV_VCI_ERROR | 0x0018
+VCI_E_DISCONNECTED = SEV_VCI_ERROR | 0x0019
VCI_E_WRONG_FLASHFWVERSION = SEV_VCI_ERROR | 0x001A
# Controller status
-CAN_STATUS_TXPEND = 0x01
-CAN_STATUS_OVRRUN = 0x02
-CAN_STATUS_ERRLIM = 0x04
-CAN_STATUS_BUSOFF = 0x08
-CAN_STATUS_ININIT = 0x10
-CAN_STATUS_BUSCERR = 0x20
+CAN_STATUS_TXPEND = 0x01 # transmission pending
+CAN_STATUS_OVRRUN = 0x02 # data overrun occurred
+CAN_STATUS_ERRLIM = 0x04 # error warning limit exceeded
+CAN_STATUS_BUSOFF = 0x08 # bus off status
+CAN_STATUS_ININIT = 0x10 # init mode active
+CAN_STATUS_BUSCERR = 0x20 # bus coupling error
# Controller operating modes
-CAN_OPMODE_UNDEFINED = 0x00
-CAN_OPMODE_STANDARD = 0x01
-CAN_OPMODE_EXTENDED = 0x02
-CAN_OPMODE_ERRFRAME = 0x04
-CAN_OPMODE_LISTONLY = 0x08
-CAN_OPMODE_LOWSPEED = 0x10
+CAN_OPMODE_UNDEFINED = 0x00 # undefined
+CAN_OPMODE_STANDARD = 0x01 # reception of 11-bit id messages
+CAN_OPMODE_EXTENDED = 0x02 # reception of 29-bit id messages
+CAN_OPMODE_ERRFRAME = 0x04 # reception of error frames
+CAN_OPMODE_LISTONLY = 0x08 # listen only mode (TX passive)
+CAN_OPMODE_LOWSPEED = 0x10 # use low speed bus interface
+CAN_OPMODE_AUTOBAUD = 0x20 # automatic bit rate detection
+
+# Extended operating modes
+CAN_EXMODE_DISABLED = 0x00
+CAN_EXMODE_EXTDATALEN = 0x01
+CAN_EXMODE_FASTDATA = 0x02
+CAN_EXMODE_NONISOCANFD = 0x04
# Message types
-CAN_MSGTYPE_DATA = 0
-CAN_MSGTYPE_INFO = 1
-CAN_MSGTYPE_ERROR = 2
-CAN_MSGTYPE_STATUS = 3
-CAN_MSGTYPE_WAKEUP = 4
+CAN_MSGTYPE_DATA = 0
+CAN_MSGTYPE_INFO = 1
+CAN_MSGTYPE_ERROR = 2
+CAN_MSGTYPE_STATUS = 3
+CAN_MSGTYPE_WAKEUP = 4
CAN_MSGTYPE_TIMEOVR = 5
CAN_MSGTYPE_TIMERST = 6
# Information supplied in the abData[0] field of info frames
# (CANMSGINFO.Bytes.bType = CAN_MSGTYPE_INFO).
-CAN_INFO_START = 1
-CAN_INFO_STOP = 2
-CAN_INFO_RESET = 3
+CAN_INFO_START = 1
+CAN_INFO_STOP = 2
+CAN_INFO_RESET = 3
# Information supplied in the abData[0] field of info frames
# (CANMSGINFO.Bytes.bType = CAN_MSGTYPE_ERROR).
-CAN_ERROR_STUFF = 1 # stuff error
-CAN_ERROR_FORM = 2 # form error
-CAN_ERROR_ACK = 3 # acknowledgment error
-CAN_ERROR_BIT = 4 # bit error
-CAN_ERROR_CRC = 6 # CRC error
-CAN_ERROR_OTHER = 7 # other (unspecified) error
+CAN_ERROR_STUFF = 1 # stuff error
+CAN_ERROR_FORM = 2 # form error
+CAN_ERROR_ACK = 3 # acknowledgment error
+CAN_ERROR_BIT = 4 # bit error
+CAN_ERROR_CRC = 6 # CRC error
+CAN_ERROR_OTHER = 7 # other (unspecified) error
# acceptance code and mask to reject all CAN IDs
-CAN_ACC_MASK_NONE = 0xFFFFFFFF
-CAN_ACC_CODE_NONE = 0x80000000
+CAN_ACC_MASK_NONE = 0xFFFFFFFF
+CAN_ACC_CODE_NONE = 0x80000000
+
+# BTMODEs
+CAN_BTMODE_RAW = 0x00000001 # raw mode
+CAN_BTMODE_TSM = 0x00000002 # triple sampling mode
+
+
+CAN_FILTER_VOID = 0x00 # invalid or unknown filter mode (do not use for initialization)
+CAN_FILTER_LOCK = 0x01 # lock filter (inhibit all IDs)
+CAN_FILTER_PASS = 0x02 # bypass filter (pass all IDs)
+CAN_FILTER_INCL = 0x03 # inclusive filtering (pass registered IDs)
+CAN_FILTER_EXCL = 0x04 # exclusive filtering (inhibit registered IDs)
+
+
+# message information flags (used by )
+CAN_MSGFLAGS_DLC = 0x0F # [bit 0] data length code
+CAN_MSGFLAGS_OVR = 0x10 # [bit 4] data overrun flag
+CAN_MSGFLAGS_SRR = 0x20 # [bit 5] self reception request
+CAN_MSGFLAGS_RTR = 0x40 # [bit 6] remote transmission request
+CAN_MSGFLAGS_EXT = 0x80 # [bit 7] frame format (0=11-bit, 1=29-bit)
+
+# extended message information flags (used by )
+CAN_MSGFLAGS2_SSM = 0x01 # [bit 0] single shot mode
+CAN_MSGFLAGS2_HPM = 0x02 # [bit 1] high priority message
+CAN_MSGFLAGS2_EDL = 0x04 # [bit 2] extended data length
+CAN_MSGFLAGS2_FDR = 0x08 # [bit 3] fast data bit rate
+CAN_MSGFLAGS2_ESI = 0x10 # [bit 4] error state indicator
+CAN_MSGFLAGS2_RES = 0xE0 # [bit 5..7] reserved bits
+
+
+CAN_ACCEPT_REJECT = 0x00 # message not accepted
+CAN_ACCEPT_ALWAYS = 0xFF # message always accepted
+CAN_ACCEPT_FILTER_1 = 0x01 # message accepted by filter 1
+CAN_ACCEPT_FILTER_2 = 0x02 # message accepted by filter 2
+CAN_ACCEPT_PASSEXCL = 0x03 # message passes exclusion filter
+
+
+CAN_FEATURE_STDOREXT = 0x00000001 # 11 OR 29 bit (exclusive)
+CAN_FEATURE_STDANDEXT = 0x00000002 # 11 AND 29 bit (simultaneous)
+CAN_FEATURE_RMTFRAME = 0x00000004 # reception of remote frames
+CAN_FEATURE_ERRFRAME = 0x00000008 # reception of error frames
+CAN_FEATURE_BUSLOAD = 0x00000010 # bus load measurement
+CAN_FEATURE_IDFILTER = 0x00000020 # exact message filter
+CAN_FEATURE_LISTONLY = 0x00000040 # listen only mode
+CAN_FEATURE_SCHEDULER = 0x00000080 # cyclic message scheduler
+CAN_FEATURE_GENERRFRM = 0x00000100 # error frame generation
+CAN_FEATURE_DELAYEDTX = 0x00000200 # delayed message transmitter
+CAN_FEATURE_SINGLESHOT = 0x00000400 # single shot mode
+CAN_FEATURE_HIGHPRIOR = 0x00000800 # high priority message
+CAN_FEATURE_AUTOBAUD = 0x00001000 # automatic bit rate detection
+CAN_FEATURE_EXTDATA = 0x00002000 # extended data length (CANFD)
+CAN_FEATURE_FASTDATA = 0x00004000 # fast data bit rate (CANFD)
+CAN_FEATURE_ISOFRAME = 0x00008000 # ISO conform frame (CANFD)
+CAN_FEATURE_NONISOFRM = (
+ 0x00010000 # non ISO conform frame (CANFD) (different CRC computation)
+)
+CAN_FEATURE_64BITTSC = 0x00020000 # 64-bit time stamp counter
+
+
+CAN_BITRATE_PRESETS = {
+ 250000: structures.CANBTP(
+ dwMode=0, dwBPS=250000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0
+ ), # SP = 80,0%
+ 500000: structures.CANBTP(
+ dwMode=0, dwBPS=500000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0
+ ), # SP = 80,0%
+ 1000000: structures.CANBTP(
+ dwMode=0, dwBPS=1000000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0
+ ), # SP = 80,0%
+}
+
+CAN_DATABITRATE_PRESETS = {
+ 500000: structures.CANBTP(
+ dwMode=0, dwBPS=500000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=6400
+ ), # SP = 80,0%
+ 833333: structures.CANBTP(
+ dwMode=0, dwBPS=833333, wTS1=1600, wTS2=400, wSJW=400, wTDO=1620
+ ), # SP = 80,0%
+ 1000000: structures.CANBTP(
+ dwMode=0, dwBPS=1000000, wTS1=1600, wTS2=400, wSJW=400, wTDO=1600
+ ), # SP = 80,0%
+ 1538461: structures.CANBTP(
+ dwMode=0, dwBPS=1538461, wTS1=1000, wTS2=300, wSJW=300, wTDO=1040
+ ), # SP = 76,9%
+ 2000000: structures.CANBTP(
+ dwMode=0, dwBPS=2000000, wTS1=1600, wTS2=400, wSJW=400, wTDO=1600
+ ), # SP = 80,0%
+ 4000000: structures.CANBTP(
+ dwMode=0, dwBPS=4000000, wTS1=800, wTS2=200, wSJW=200, wTDO=800
+ ), # SP = 80,0%
+ 5000000: structures.CANBTP(
+ dwMode=0, dwBPS=5000000, wTS1=600, wTS2=200, wSJW=200, wTDO=600
+ ), # SP = 75,0%
+ 6666666: structures.CANBTP(
+ dwMode=0, dwBPS=6666666, wTS1=400, wTS2=200, wSJW=200, wTDO=402
+ ), # SP = 66,7%
+ 8000000: structures.CANBTP(
+ dwMode=0, dwBPS=8000000, wTS1=400, wTS2=100, wSJW=100, wTDO=250
+ ), # SP = 80,0%
+ 10000000: structures.CANBTP(
+ dwMode=0, dwBPS=10000000, wTS1=300, wTS2=100, wSJW=100, wTDO=200
+ ), # SP = 75,0%
+}
diff --git a/can/interfaces/ixxat/exceptions.py b/can/interfaces/ixxat/exceptions.py
index 9ac5b8f80..21dea465f 100644
--- a/can/interfaces/ixxat/exceptions.py
+++ b/can/interfaces/ixxat/exceptions.py
@@ -1,32 +1,58 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
-Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems
+Ctypes wrapper module for IXXAT Virtual CAN Interface V4 on win32 systems
Copyright (C) 2016 Giuseppe Corbelli
+Copyright (C) 2019 Marcel Kanter
"""
-from can import CanError
+from can import (
+ CanInitializationError,
+ CanOperationError,
+ CanTimeoutError,
+)
-__all__ = ['VCITimeout', 'VCIError', 'VCIRxQueueEmptyError', 'VCIDeviceNotFoundError']
+__all__ = [
+ "VCIBusCouplingError",
+ "VCIBusOffError",
+ "VCIDataOverrunError",
+ "VCIDeviceNotFoundError",
+ "VCIError",
+ "VCIErrorLimitExceededError",
+ "VCIRxQueueEmptyError",
+ "VCITimeout",
+]
-class VCITimeout(CanError):
- """ Wraps the VCI_E_TIMEOUT error """
- pass
+class VCITimeout(CanTimeoutError):
+ """Wraps the VCI_E_TIMEOUT error"""
-class VCIError(CanError):
- """ Try to display errors that occur within the wrapped C library nicely. """
- pass
+class VCIError(CanOperationError):
+ """Try to display errors that occur within the wrapped C library nicely."""
class VCIRxQueueEmptyError(VCIError):
- """ Wraps the VCI_E_RXQUEUE_EMPTY error """
+ """Wraps the VCI_E_RXQUEUE_EMPTY error"""
+
def __init__(self):
- super(VCIRxQueueEmptyError, self).__init__("Receive queue is empty")
+ super().__init__("Receive queue is empty")
+
+
+class VCIBusOffError(VCIError):
+ """Controller is in BUSOFF state"""
+
+
+class VCIErrorLimitExceededError(VCIError):
+ """overrun of error counter occurred"""
+
+
+class VCIDataOverrunError(VCIError):
+ """data overrun in receive buffer occurred"""
+
+
+class VCIBusCouplingError(VCIError):
+ """Bus coupling error occurred"""
-class VCIDeviceNotFoundError(CanError):
+class VCIDeviceNotFoundError(CanInitializationError):
pass
diff --git a/can/interfaces/ixxat/structures.py b/can/interfaces/ixxat/structures.py
index 72cab99b7..680ed1ac6 100644
--- a/can/interfaces/ixxat/structures.py
+++ b/can/interfaces/ixxat/structures.py
@@ -1,27 +1,23 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
-Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems
+Ctypes wrapper module for IXXAT Virtual CAN Interface V4 on win32 systems
Copyright (C) 2016 Giuseppe Corbelli
"""
import ctypes
+
class LUID(ctypes.Structure):
- _fields_ = [
- ("LowPart", ctypes.c_uint32),
- ("HighPart", ctypes.c_int32),
- ]
+ _fields_ = [("LowPart", ctypes.c_uint32), ("HighPart", ctypes.c_int32)]
+
+
PLUID = ctypes.POINTER(LUID)
class VCIID(ctypes.Union):
- _fields_ = [
- ("AsLuid", LUID),
- ("AsInt64", ctypes.c_int64),
- ]
+ _fields_ = [("AsLuid", LUID), ("AsInt64", ctypes.c_int64)]
+
+
PVCIID = ctypes.POINTER(VCIID)
@@ -36,10 +32,8 @@ class GUID(ctypes.Structure):
class VCIDEVICEINFO(ctypes.Structure):
class UniqueHardwareId(ctypes.Union):
- _fields_ = [
- ("AsChar", ctypes.c_char * 16),
- ("AsGuid", GUID),
- ]
+ _fields_ = [("AsChar", ctypes.c_char * 16), ("AsGuid", GUID)]
+
_fields_ = [
("VciObjectId", VCIID),
("DeviceClass", GUID),
@@ -57,40 +51,48 @@ class UniqueHardwareId(ctypes.Union):
]
def __str__(self):
- return "Mfg: {}, Dev: {} HW: {}.{}.{}.{} Drv: {}.{}.{}.{}".format(
- self.Manufacturer,
- self.Description,
- self.HardwareBranchVersion,
- self.HardwareMajorVersion,
- self.HardwareMinorVersion,
- self.HardwareBuildVersion,
- self.DriverReleaseVersion,
- self.DriverMajorVersion,
- self.DriverMinorVersion,
- self.DriverBuildVersion
+ return (
+ f"Mfg: {self.Manufacturer}, "
+ f"Dev: {self.Description} "
+ f"HW: {self.HardwareBranchVersion}"
+ f".{self.HardwareMajorVersion}"
+ f".{self.HardwareMinorVersion}"
+ f".{self.HardwareBuildVersion} "
+ f"Drv: {self.DriverReleaseVersion}"
+ f".{self.DriverMajorVersion}"
+ f".{self.DriverMinorVersion}"
+ f".{self.DriverBuildVersion}"
)
+
+
PVCIDEVICEINFO = ctypes.POINTER(VCIDEVICEINFO)
class CANLINESTATUS(ctypes.Structure):
_fields_ = [
+ # current CAN operating mode. Value is a logical combination of
+ # one or more CAN_OPMODE_xxx constants
("bOpMode", ctypes.c_uint8),
- ("bBtReg0", ctypes.c_uint8),
- ("bBtReg1", ctypes.c_uint8),
- ("bBusLoad", ctypes.c_uint8),
- ("dwStatus", ctypes.c_uint32)
+ ("bBtReg0", ctypes.c_uint8), # current bus timing register 0 value
+ ("bBtReg1", ctypes.c_uint8), # current bus timing register 1 value
+ ("bBusLoad", ctypes.c_uint8), # average bus load in percent (0..100)
+ ("dwStatus", ctypes.c_uint32), # status of the CAN controller (see CAN_STATUS_)
]
+
+
PCANLINESTATUS = ctypes.POINTER(CANLINESTATUS)
class CANCHANSTATUS(ctypes.Structure):
_fields_ = [
- ("sLineStatus", CANLINESTATUS),
- ("fActivated", ctypes.c_uint32),
- ("fRxOverrun", ctypes.c_uint32),
- ("bRxFifoLoad", ctypes.c_uint8),
- ("bTxFifoLoad", ctypes.c_uint8)
+ ("sLineStatus", CANLINESTATUS), # current CAN line status
+ ("fActivated", ctypes.c_uint32), # TRUE if the channel is activated
+ ("fRxOverrun", ctypes.c_uint32), # TRUE if receive FIFO overrun occurred
+ ("bRxFifoLoad", ctypes.c_uint8), # receive FIFO load in percent (0..100)
+ ("bTxFifoLoad", ctypes.c_uint8), # transmit FIFO load in percent (0..100)
]
+
+
PCANCHANSTATUS = ctypes.POINTER(CANCHANSTATUS)
@@ -104,50 +106,73 @@ class CANCAPABILITIES(ctypes.Structure):
("dwCmsDivisor", ctypes.c_uint32),
("dwCmsMaxTicks", ctypes.c_uint32),
("dwDtxDivisor", ctypes.c_uint32),
- ("dwDtxMaxTicks", ctypes.c_uint32)
+ ("dwDtxMaxTicks", ctypes.c_uint32),
]
+
+
PCANCAPABILITIES = ctypes.POINTER(CANCAPABILITIES)
class CANMSGINFO(ctypes.Union):
class Bytes(ctypes.Structure):
_fields_ = [
- ("bType", ctypes.c_uint8),
- ("bAddFlags", ctypes.c_uint8),
- ("bFlags", ctypes.c_uint8),
- ("bAccept", ctypes.c_uint8),
+ ("bType", ctypes.c_uint8), # type (see CAN_MSGTYPE_ constants)
+ (
+ "bAddFlags",
+ ctypes.c_uint8,
+ ), # extended flags (see CAN_MSGFLAGS2_ constants). AKA bFlags2 in VCI v4
+ ("bFlags", ctypes.c_uint8), # flags (see CAN_MSGFLAGS_ constants)
+ ("bAccept", ctypes.c_uint8), # accept code (see CAN_ACCEPT_ constants)
]
class Bits(ctypes.Structure):
_fields_ = [
- ("type", ctypes.c_uint32, 8),
- ("ssm", ctypes.c_uint32, 1),
- ("hi", ctypes.c_uint32, 2),
- ("res", ctypes.c_uint32, 5),
- ("dlc", ctypes.c_uint32, 4),
- ("ovr", ctypes.c_uint32, 1),
- ("srr", ctypes.c_uint32, 1),
- ("rtr", ctypes.c_uint32, 1),
- ("ext", ctypes.c_uint32, 1),
- ("afc", ctypes.c_uint32, 8)
+ ("type", ctypes.c_uint32, 8), # type (see CAN_MSGTYPE_ constants)
+ ("ssm", ctypes.c_uint32, 1), # single shot mode
+ ("hpm", ctypes.c_uint32, 1), # high priority message
+ ("edl", ctypes.c_uint32, 1), # extended data length
+ ("fdr", ctypes.c_uint32, 1), # fast data bit rate
+ ("esi", ctypes.c_uint32, 1), # error state indicator
+ ("res", ctypes.c_uint32, 3), # reserved set to 0
+ ("dlc", ctypes.c_uint32, 4), # data length code
+ ("ovr", ctypes.c_uint32, 1), # data overrun
+ ("srr", ctypes.c_uint32, 1), # self reception request
+ ("rtr", ctypes.c_uint32, 1), # remote transmission request
+ (
+ "ext",
+ ctypes.c_uint32,
+ 1,
+ ), # extended frame format (0=standard, 1=extended)
+ ("afc", ctypes.c_uint32, 8), # accept code (see CAN_ACCEPT_ constants)
]
- _fields_ = [
- ("Bytes", Bytes),
- ("Bits", Bits)
- ]
+ _fields_ = [("Bytes", Bytes), ("Bits", Bits)]
+
+
PCANMSGINFO = ctypes.POINTER(CANMSGINFO)
class CANMSG(ctypes.Structure):
_fields_ = [
("dwTime", ctypes.c_uint32),
+ # CAN ID of the message in Intel format (aligned right) without RTR bit.
("dwMsgId", ctypes.c_uint32),
("uMsgInfo", CANMSGINFO),
- ("abData", ctypes.c_uint8 * 8)
+ ("abData", ctypes.c_uint8 * 8),
]
+
+ def __str__(self) -> str:
+ return """ID: 0x{:04x}{} DLC: {:02d} DATA: {}""".format(
+ self.dwMsgId,
+ "[RTR]" if self.uMsgInfo.Bits.rtr else "",
+ self.uMsgInfo.Bits.dlc,
+ memoryview(self.abData)[: self.uMsgInfo.Bits.dlc].hex(sep=" "),
+ )
+
+
PCANMSG = ctypes.POINTER(CANMSG)
+
class CANCYCLICTXMSG(ctypes.Structure):
_fields_ = [
("wCycleTime", ctypes.c_uint16),
@@ -155,6 +180,128 @@ class CANCYCLICTXMSG(ctypes.Structure):
("bByteIndex", ctypes.c_uint8),
("dwMsgId", ctypes.c_uint32),
("uMsgInfo", CANMSGINFO),
- ("abData", ctypes.c_uint8 * 8)
+ ("abData", ctypes.c_uint8 * 8),
]
+
+
PCANCYCLICTXMSG = ctypes.POINTER(CANCYCLICTXMSG)
+
+
+class CANBTP(ctypes.Structure):
+ _fields_ = [
+ ("dwMode", ctypes.c_uint32), # timing mode (see CAN_BTMODE_ const)
+ ("dwBPS", ctypes.c_uint32), # bits per second or prescaler (see CAN_BTMODE_RAW)
+ ("wTS1", ctypes.c_uint16), # length of time segment 1 in quanta
+ ("wTS2", ctypes.c_uint16), # length of time segment 2 in quanta
+ ("wSJW", ctypes.c_uint16), # re-synchronization jump width im quanta
+ (
+ "wTDO",
+ ctypes.c_uint16,
+ ), # transceiver delay offset (SSP offset) in quanta (0 = disabled, 0xFFFF = simplified SSP positioning)
+ ]
+
+ def __str__(self):
+ return (
+ f"dwMode={self.dwMode:d}, "
+ f"dwBPS={self.dwBPS:d}, "
+ f"wTS1={self.wTS1:d}, "
+ f"wTS2={self.wTS2:d}, "
+ f"wSJW={self.wSJW:d}, "
+ f"wTDO={self.wTDO:d}"
+ )
+
+
+PCANBTP = ctypes.POINTER(CANBTP)
+
+
+class CANCAPABILITIES2(ctypes.Structure):
+ _fields_ = [
+ ("wCtrlType", ctypes.c_uint16), # Type of CAN controller (see CAN_CTRL_ const)
+ ("wBusCoupling", ctypes.c_uint16), # Type of Bus coupling (see CAN_BUSC_ const)
+ (
+ "dwFeatures",
+ ctypes.c_uint32,
+ ), # supported features (see CAN_FEATURE_ constants)
+ ("dwCanClkFreq", ctypes.c_uint32), # CAN clock frequency [Hz]
+ ("sSdrRangeMin", CANBTP), # minimum bit timing values for standard bit rate
+ ("sSdrRangeMax", CANBTP), # maximum bit timing values for standard bit rate
+ ("sFdrRangeMin", CANBTP), # minimum bit timing values for fast data bit rate
+ ("sFdrRangeMax", CANBTP), # maximum bit timing values for fast data bit rate
+ (
+ "dwTscClkFreq",
+ ctypes.c_uint32,
+ ), # clock frequency of the time stamp counter [Hz]
+ ("dwTscDivisor", ctypes.c_uint32), # divisor for the message time stamp counter
+ (
+ "dwCmsClkFreq",
+ ctypes.c_uint32,
+ ), # clock frequency of cyclic message scheduler [Hz]
+ ("dwCmsDivisor", ctypes.c_uint32), # divisor for the cyclic message scheduler
+ (
+ "dwCmsMaxTicks",
+ ctypes.c_uint32,
+ ), # maximum tick count value of the cyclic message
+ (
+ "dwDtxClkFreq",
+ ctypes.c_uint32,
+ ), # clock frequency of the delayed message transmitter [Hz]
+ (
+ "dwDtxDivisor",
+ ctypes.c_uint32,
+ ), # divisor for the delayed message transmitter
+ (
+ "dwDtxMaxTicks",
+ ctypes.c_uint32,
+ ), # maximum tick count value of the delayed message transmitter
+ ]
+
+
+PCANCAPABILITIES2 = ctypes.POINTER(CANCAPABILITIES2)
+
+
+class CANLINESTATUS2(ctypes.Structure):
+ _fields_ = [
+ ("bOpMode", ctypes.c_uint8), # current CAN operating mode
+ ("bExMode", ctypes.c_uint8), # current CAN extended operating mode
+ ("bBusLoad", ctypes.c_uint8), # average bus load in percent (0..100)
+ ("bReserved", ctypes.c_uint8), # reserved set to 0
+ ("sBtpSdr", ctypes.c_uint8), # standard bit rate timing
+ ("sBtpFdr", ctypes.c_uint8), # fast data bit rate timing
+ ("dwStatus", ctypes.c_uint32), # status of the CAN controller (see CAN_STATUS_)
+ ]
+
+
+PCANLINESTATUS2 = ctypes.POINTER(CANLINESTATUS2)
+
+
+class CANMSG2(ctypes.Structure):
+ _fields_ = [
+ ("dwTime", ctypes.c_uint32), # time stamp for receive message
+ ("rsvd", ctypes.c_uint32), # reserved (set to 0)
+ ("dwMsgId", ctypes.c_uint32), # CAN message identifier (INTEL format)
+ ("uMsgInfo", CANMSGINFO), # message information (bit field)
+ ("abData", ctypes.c_uint8 * 64), # message data
+ ]
+
+
+PCANMSG2 = ctypes.POINTER(CANMSG2)
+
+
+class CANCYCLICTXMSG2(ctypes.Structure):
+ _fields_ = [
+ ("wCycleTime", ctypes.c_uint16), # cycle time for the message in ticks
+ (
+ "bIncrMode",
+ ctypes.c_uint8,
+ ), # auto increment mode (see CAN_CTXMSG_INC_ const)
+ (
+ "bByteIndex",
+ ctypes.c_uint8,
+ ), # index of the byte within abData[] to increment
+ ("dwMsgId", ctypes.c_uint32), # message identifier (INTEL format)
+ ("uMsgInfo", CANMSGINFO), # message information (bit field)
+ ("abData", ctypes.c_uint8 * 64), # message data
+ ]
+
+
+PCANCYCLICTXMSG2 = ctypes.POINTER(CANCYCLICTXMSG2)
diff --git a/can/interfaces/kvaser/__init__.py b/can/interfaces/kvaser/__init__.py
index c55ce39ed..7cfb13d8d 100644
--- a/can/interfaces/kvaser/__init__.py
+++ b/can/interfaces/kvaser/__init__.py
@@ -1,7 +1,13 @@
-#!/usr/bin/env python
-# coding: utf-8
+""" """
-"""
-"""
+__all__ = [
+ "CANLIBInitializationError",
+ "CANLIBOperationError",
+ "KvaserBus",
+ "canlib",
+ "constants",
+ "get_channel_info",
+ "structures",
+]
from can.interfaces.kvaser.canlib import *
diff --git a/can/interfaces/kvaser/canlib.py b/can/interfaces/kvaser/canlib.py
index efe4cb92c..4403b60ca 100644
--- a/can/interfaces/kvaser/canlib.py
+++ b/can/interfaces/kvaser/canlib.py
@@ -1,6 +1,3 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Contains Python equivalents of the function and constant
definitions in CANLIB's canlib.h, with some supporting functionality
@@ -9,18 +6,20 @@
Copyright (C) 2010 Dynamic Controls
"""
-from __future__ import absolute_import
-
+import ctypes
+import logging
import sys
import time
-import logging
-import ctypes
-from can import CanError, BusABC
-from can import Message
+from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message
+from can.exceptions import CanError, CanInitializationError, CanOperationError
+from can.typechecking import CanFilters
+from can.util import check_or_adjust_timing_clock, time_perfcounter_correlation
+
from . import constants as canstat
+from . import structures
-log = logging.getLogger('can.kvaser')
+log = logging.getLogger("can.kvaser")
# Resolution in us
TIMESTAMP_RESOLUTION = 10
@@ -40,21 +39,22 @@
def _unimplemented_function(*args):
- raise NotImplementedError('This function is not implemented in canlib')
+ raise NotImplementedError("This function is not implemented in canlib")
-def __get_canlib_function(func_name, argtypes=[], restype=None, errcheck=None):
- #log.debug('Wrapping function "%s"' % func_name)
+def __get_canlib_function(func_name, argtypes=None, restype=None, errcheck=None):
+ argtypes = [] if argtypes is None else argtypes
+ # log.debug('Wrapping function "%s"' % func_name)
try:
# e.g. canlib.canBusOn
retval = getattr(__canlib, func_name)
- #log.debug('"%s" found in library', func_name)
+ # log.debug('"%s" found in library', func_name)
except AttributeError:
log.warning('"%s" was not found in library', func_name)
return _unimplemented_function
else:
- #log.debug('Result type is: %s' % type(restype))
- #log.debug('Error check function is: %s' % errcheck)
+ # log.debug('Result type is: %s' % type(restype))
+ # log.debug('Error check function is: %s' % errcheck)
retval.argtypes = argtypes
retval.restype = restype
if errcheck:
@@ -63,191 +63,272 @@ def __get_canlib_function(func_name, argtypes=[], restype=None, errcheck=None):
class CANLIBError(CanError):
-
"""
Try to display errors that occur within the wrapped C library nicely.
"""
def __init__(self, function, error_code, arguments):
- super(CANLIBError, self).__init__()
- self.error_code = error_code
+ message = CANLIBError._get_error_message(error_code)
+ super().__init__(f"Function {function.__name__} failed - {message}", error_code)
self.function = function
self.arguments = arguments
- def __str__(self):
- return "Function %s failed - %s" % (self.function.__name__,
- self.__get_error_message())
-
- def __get_error_message(self):
+ @staticmethod
+ def _get_error_message(error_code: int) -> str:
errmsg = ctypes.create_string_buffer(128)
- canGetErrorText(self.error_code, errmsg, len(errmsg))
+ canGetErrorText(error_code, errmsg, len(errmsg))
return errmsg.value.decode("ascii")
+class CANLIBInitializationError(CANLIBError, CanInitializationError):
+ pass
+
+
+class CANLIBOperationError(CANLIBError, CanOperationError):
+ pass
+
+
def __convert_can_status_to_int(result):
- #log.debug("converting can status to int {} ({})".format(result, type(result)))
if isinstance(result, int):
return result
else:
return result.value
-def __check_status(result, function, arguments):
+def __check_status_operation(result, function, arguments):
+ result = __convert_can_status_to_int(result)
+ if not canstat.CANSTATUS_SUCCESS(result):
+ raise CANLIBOperationError(function, result, arguments)
+ return result
+
+
+def __check_status_initialization(result, function, arguments):
result = __convert_can_status_to_int(result)
if not canstat.CANSTATUS_SUCCESS(result):
- #log.debug('Detected error while checking CAN status')
- raise CANLIBError(function, result, arguments)
+ raise CANLIBInitializationError(function, result, arguments)
return result
def __check_status_read(result, function, arguments):
result = __convert_can_status_to_int(result)
if not canstat.CANSTATUS_SUCCESS(result) and result != canstat.canERR_NOMSG:
- #log.debug('Detected error in which checking status read')
- raise CANLIBError(function, result, arguments)
+ raise CANLIBOperationError(function, result, arguments)
return result
class c_canHandle(ctypes.c_int):
pass
+
canINVALID_HANDLE = -1
-def __handle_is_valid(handle):
- return (handle.value > canINVALID_HANDLE)
+def __check_bus_handle_validity(handle, function, arguments):
+ if handle.value > canINVALID_HANDLE:
+ return handle # is valid
+ result = __convert_can_status_to_int(handle)
+ raise CANLIBInitializationError(function, result, arguments)
-def __check_bus_handle_validity(handle, function, arguments):
- if not __handle_is_valid(handle):
- result = __convert_can_status_to_int(handle)
- raise CANLIBError(function, result, arguments)
- else:
- return handle
if __canlib is not None:
canInitializeLibrary = __get_canlib_function("canInitializeLibrary")
- canGetErrorText = __get_canlib_function("canGetErrorText",
- argtypes=[canstat.c_canStatus, ctypes.c_char_p, ctypes.c_uint],
- restype=canstat.c_canStatus,
- errcheck=__check_status)
+ canGetErrorText = __get_canlib_function(
+ "canGetErrorText",
+ argtypes=[canstat.c_canStatus, ctypes.c_char_p, ctypes.c_uint],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_operation,
+ )
# TODO wrap this type of function to provide a more Pythonic API
- canGetNumberOfChannels = __get_canlib_function("canGetNumberOfChannels",
- argtypes=[ctypes.c_void_p],
- restype=canstat.c_canStatus,
- errcheck=__check_status)
-
- kvReadTimer = __get_canlib_function("kvReadTimer",
- argtypes=[c_canHandle,
- ctypes.POINTER(ctypes.c_uint)],
- restype=canstat.c_canStatus,
- errcheck=__check_status)
-
- canBusOff = __get_canlib_function("canBusOff",
- argtypes=[c_canHandle],
- restype=canstat.c_canStatus,
- errcheck=__check_status)
-
- canBusOn = __get_canlib_function("canBusOn",
- argtypes=[c_canHandle],
- restype=canstat.c_canStatus,
- errcheck=__check_status)
-
- canClose = __get_canlib_function("canClose",
- argtypes=[c_canHandle],
- restype=canstat.c_canStatus,
- errcheck=__check_status)
-
- canOpenChannel = __get_canlib_function("canOpenChannel",
- argtypes=[ctypes.c_int, ctypes.c_int],
- restype=c_canHandle,
- errcheck=__check_bus_handle_validity)
-
- canSetBusParams = __get_canlib_function("canSetBusParams",
- argtypes=[c_canHandle, ctypes.c_long,
- ctypes.c_uint, ctypes.c_uint,
- ctypes.c_uint, ctypes.c_uint,
- ctypes.c_uint],
- restype=canstat.c_canStatus,
- errcheck=__check_status)
-
- canSetBusParamsFd = __get_canlib_function("canSetBusParamsFd",
- argtypes=[c_canHandle, ctypes.c_long,
- ctypes.c_uint, ctypes.c_uint,
- ctypes.c_uint],
- restype=canstat.c_canStatus,
- errcheck=__check_status)
-
- canSetBusOutputControl = __get_canlib_function("canSetBusOutputControl",
- argtypes=[c_canHandle,
- ctypes.c_uint],
- restype=canstat.c_canStatus,
- errcheck=__check_status)
-
- canSetAcceptanceFilter = __get_canlib_function("canSetAcceptanceFilter",
- argtypes=[
- c_canHandle,
- ctypes.c_uint,
- ctypes.c_uint,
- ctypes.c_int
- ],
- restype=canstat.c_canStatus,
- errcheck=__check_status)
-
- canReadWait = __get_canlib_function("canReadWait",
- argtypes=[c_canHandle, ctypes.c_void_p,
- ctypes.c_void_p, ctypes.c_void_p,
- ctypes.c_void_p, ctypes.c_void_p,
- ctypes.c_long],
- restype=canstat.c_canStatus,
- errcheck=__check_status_read)
-
- canWrite = __get_canlib_function("canWrite",
- argtypes=[
- c_canHandle,
- ctypes.c_long,
- ctypes.c_void_p,
- ctypes.c_uint,
- ctypes.c_uint],
- restype=canstat.c_canStatus,
- errcheck=__check_status)
-
- canWriteSync = __get_canlib_function("canWriteSync",
- argtypes=[c_canHandle, ctypes.c_ulong],
- restype=canstat.c_canStatus,
- errcheck=__check_status)
-
- canIoCtl = __get_canlib_function("canIoCtl",
- argtypes=[c_canHandle, ctypes.c_uint,
- ctypes.c_void_p, ctypes.c_uint],
- restype=canstat.c_canStatus,
- errcheck=__check_status)
-
- canGetVersion = __get_canlib_function("canGetVersion",
- restype=ctypes.c_short,
- errcheck=__check_status)
-
- kvFlashLeds = __get_canlib_function("kvFlashLeds",
- argtypes=[c_canHandle, ctypes.c_int,
- ctypes.c_int],
- restype=ctypes.c_short,
- errcheck=__check_status)
+ canGetNumberOfChannels = __get_canlib_function(
+ "canGetNumberOfChannels",
+ argtypes=[ctypes.c_void_p],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_initialization,
+ )
+
+ kvReadTimer = __get_canlib_function(
+ "kvReadTimer",
+ argtypes=[c_canHandle, ctypes.POINTER(ctypes.c_uint)],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_initialization,
+ )
+
+ canBusOff = __get_canlib_function(
+ "canBusOff",
+ argtypes=[c_canHandle],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_operation,
+ )
+
+ canBusOn = __get_canlib_function(
+ "canBusOn",
+ argtypes=[c_canHandle],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_initialization,
+ )
+
+ canClose = __get_canlib_function(
+ "canClose",
+ argtypes=[c_canHandle],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_operation,
+ )
+
+ canOpenChannel = __get_canlib_function(
+ "canOpenChannel",
+ argtypes=[ctypes.c_int, ctypes.c_int],
+ restype=c_canHandle,
+ errcheck=__check_bus_handle_validity,
+ )
+
+ canSetBusParams = __get_canlib_function(
+ "canSetBusParams",
+ argtypes=[
+ c_canHandle,
+ ctypes.c_long,
+ ctypes.c_uint,
+ ctypes.c_uint,
+ ctypes.c_uint,
+ ctypes.c_uint,
+ ctypes.c_uint,
+ ],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_initialization,
+ )
+
+ canSetBusParamsC200 = __get_canlib_function(
+ "canSetBusParamsC200",
+ argtypes=[
+ c_canHandle,
+ ctypes.c_byte,
+ ctypes.c_byte,
+ ],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_initialization,
+ )
+
+ canSetBusParamsFd = __get_canlib_function(
+ "canSetBusParamsFd",
+ argtypes=[
+ c_canHandle,
+ ctypes.c_long,
+ ctypes.c_uint,
+ ctypes.c_uint,
+ ctypes.c_uint,
+ ],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_initialization,
+ )
+
+ canSetBusOutputControl = __get_canlib_function(
+ "canSetBusOutputControl",
+ argtypes=[c_canHandle, ctypes.c_uint],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_initialization,
+ )
+
+ canSetAcceptanceFilter = __get_canlib_function(
+ "canSetAcceptanceFilter",
+ argtypes=[c_canHandle, ctypes.c_uint, ctypes.c_uint, ctypes.c_int],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_operation,
+ )
+
+ canReadWait = __get_canlib_function(
+ "canReadWait",
+ argtypes=[
+ c_canHandle,
+ ctypes.c_void_p,
+ ctypes.c_void_p,
+ ctypes.c_void_p,
+ ctypes.c_void_p,
+ ctypes.c_void_p,
+ ctypes.c_long,
+ ],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_read,
+ )
+
+ canWrite = __get_canlib_function(
+ "canWrite",
+ argtypes=[
+ c_canHandle,
+ ctypes.c_long,
+ ctypes.c_void_p,
+ ctypes.c_uint,
+ ctypes.c_uint,
+ ],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_operation,
+ )
+
+ canWriteSync = __get_canlib_function(
+ "canWriteSync",
+ argtypes=[c_canHandle, ctypes.c_ulong],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_operation,
+ )
+
+ canIoCtlInit = __get_canlib_function(
+ "canIoCtl",
+ argtypes=[c_canHandle, ctypes.c_uint, ctypes.c_void_p, ctypes.c_uint],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_initialization,
+ )
+
+ canIoCtl = __get_canlib_function(
+ "canIoCtl",
+ argtypes=[c_canHandle, ctypes.c_uint, ctypes.c_void_p, ctypes.c_uint],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_operation,
+ )
+
+ canGetVersion = __get_canlib_function(
+ "canGetVersion", restype=ctypes.c_short, errcheck=__check_status_operation
+ )
+
+ kvFlashLeds = __get_canlib_function(
+ "kvFlashLeds",
+ argtypes=[c_canHandle, ctypes.c_int, ctypes.c_int],
+ restype=ctypes.c_short,
+ errcheck=__check_status_operation,
+ )
if sys.platform == "win32":
- canGetVersionEx = __get_canlib_function("canGetVersionEx",
- argtypes=[ctypes.c_uint],
- restype=ctypes.c_uint,
- errcheck=__check_status)
+ canGetVersionEx = __get_canlib_function(
+ "canGetVersionEx",
+ argtypes=[ctypes.c_uint],
+ restype=ctypes.c_uint,
+ errcheck=__check_status_operation,
+ )
- canGetChannelData = __get_canlib_function("canGetChannelData",
- argtypes=[ctypes.c_int,
- ctypes.c_int,
- ctypes.c_void_p,
- ctypes.c_size_t],
- restype=canstat.c_canStatus,
- errcheck=__check_status)
+ canGetChannelData = __get_canlib_function(
+ "canGetChannelData",
+ argtypes=[ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_initialization,
+ )
+
+ canRequestBusStatistics = __get_canlib_function(
+ "canRequestBusStatistics",
+ argtypes=[c_canHandle],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_operation,
+ )
+
+ canGetBusStatistics = __get_canlib_function(
+ "canGetBusStatistics",
+ argtypes=[
+ c_canHandle,
+ ctypes.POINTER(structures.BusStatistics),
+ ctypes.c_size_t,
+ ],
+ restype=canstat.c_canStatus,
+ errcheck=__check_status_operation,
+ )
def init_kvaser_library():
@@ -256,7 +337,7 @@ def init_kvaser_library():
log.debug("Initializing Kvaser CAN library")
canInitializeLibrary()
log.debug("CAN library initialized")
- except:
+ except Exception:
log.warning("Kvaser canlib could not be initialized.")
@@ -273,7 +354,7 @@ def init_kvaser_library():
83000: canstat.canBITRATE_83K,
62000: canstat.canBITRATE_62K,
50000: canstat.canBITRATE_50K,
- 10000: canstat.canBITRATE_10K
+ 10000: canstat.canBITRATE_10K,
}
BITRATE_FD = {
@@ -281,7 +362,7 @@ def init_kvaser_library():
1000000: canstat.canFD_BITRATE_1M_80P,
2000000: canstat.canFD_BITRATE_2M_80P,
4000000: canstat.canFD_BITRATE_4M_80P,
- 8000000: canstat.canFD_BITRATE_8M_60P
+ 8000000: canstat.canFD_BITRATE_8M_60P,
}
@@ -290,7 +371,13 @@ class KvaserBus(BusABC):
The CAN Bus implemented for the Kvaser interface.
"""
- def __init__(self, channel, can_filters=None, **config):
+ def __init__(
+ self,
+ channel: int,
+ can_filters: CanFilters | None = None,
+ timing: BitTiming | BitTimingFd | None = None,
+ **kwargs,
+ ):
"""
:param int channel:
The Channel id to create this bus with.
@@ -300,6 +387,12 @@ def __init__(self, channel, can_filters=None, **config):
Backend Configuration
+ :param timing:
+ An instance of :class:`~can.BitTiming` or :class:`~can.BitTimingFd`
+ to specify the bit timing parameters for the Kvaser interface. If provided, it
+ takes precedence over the all other timing-related parameters.
+ Note that the `f_clock` property of the `timing` instance must be 16_000_000 (16MHz)
+ for standard CAN or 80_000_000 (80MHz) for CAN FD.
:param int bitrate:
Bitrate of channel in bit/s
:param bool accept_virtual:
@@ -334,137 +427,225 @@ def __init__(self, channel, can_filters=None, **config):
computer, set this to True or set single_handle to True.
:param bool fd:
If CAN-FD frames should be supported.
+ :param bool fd_non_iso:
+ Open the channel in Non-ISO (Bosch) FD mode. Only applies for FD buses.
+ This changes the handling of the stuff-bit counter and the CRC. Defaults
+ to False (ISO mode)
+ :param bool exclusive:
+ Don't allow sharing of this CANlib channel.
+ :param bool override_exclusive:
+ Open the channel even if it is opened for exclusive access already.
:param int data_bitrate:
Which bitrate to use for data phase in CAN FD.
Defaults to arbitration bitrate.
-
+ :param bool no_init_access:
+ Don't open the handle with init access.
"""
- log.info("CAN Filters: {}".format(can_filters))
- log.info("Got configuration of: {}".format(config))
- bitrate = config.get('bitrate', 500000)
- tseg1 = config.get('tseg1', 0)
- tseg2 = config.get('tseg2', 0)
- sjw = config.get('sjw', 0)
- no_samp = config.get('no_samp', 0)
- driver_mode = config.get('driver_mode', DRIVER_MODE_NORMAL)
- single_handle = config.get('single_handle', False)
- receive_own_messages = config.get('receive_own_messages', False)
- accept_virtual = config.get('accept_virtual', True)
- fd = config.get('fd', False)
- data_bitrate = config.get('data_bitrate', None)
+ log.info(f"CAN Filters: {can_filters}")
+ log.info(f"Got configuration of: {kwargs}")
+ bitrate = kwargs.get("bitrate", 500000)
+ tseg1 = kwargs.get("tseg1", 0)
+ tseg2 = kwargs.get("tseg2", 0)
+ sjw = kwargs.get("sjw", 0)
+ no_samp = kwargs.get("no_samp", 0)
+ driver_mode = kwargs.get("driver_mode", DRIVER_MODE_NORMAL)
+ single_handle = kwargs.get("single_handle", False)
+ receive_own_messages = kwargs.get("receive_own_messages", False)
+ exclusive = kwargs.get("exclusive", False)
+ override_exclusive = kwargs.get("override_exclusive", False)
+ accept_virtual = kwargs.get("accept_virtual", True)
+ no_init_access = kwargs.get("no_init_access", False)
+ fd = isinstance(timing, BitTimingFd) if timing else kwargs.get("fd", False)
+ data_bitrate = kwargs.get("data_bitrate", None)
+ fd_non_iso = kwargs.get("fd_non_iso", False)
try:
channel = int(channel)
except ValueError:
- raise ValueError('channel must be an integer')
- self.channel = channel
+ raise ValueError("channel must be an integer") from None
- log.debug('Initialising bus instance')
+ self.channel = channel
self.single_handle = single_handle
+ self._can_protocol = CanProtocol.CAN_20
+ if fd_non_iso:
+ self._can_protocol = CanProtocol.CAN_FD_NON_ISO
+ elif fd:
+ self._can_protocol = CanProtocol.CAN_FD
+ log.debug("Initialising bus instance")
num_channels = ctypes.c_int(0)
- res = canGetNumberOfChannels(ctypes.byref(num_channels))
- #log.debug("Res: {}".format(res))
+ canGetNumberOfChannels(ctypes.byref(num_channels))
num_channels = int(num_channels.value)
- log.info('Found %d available channels' % num_channels)
+ log.info("Found %d available channels", num_channels)
for idx in range(num_channels):
channel_info = get_channel_info(idx)
- log.info('%d: %s', idx, channel_info)
+ channel_info = f'{channel_info["device_name"]}, S/N {channel_info["serial"]} (#{channel_info["dongle_channel"]})'
+ log.info("%d: %s", idx, channel_info)
if idx == channel:
self.channel_info = channel_info
flags = 0
+ if exclusive:
+ flags |= canstat.canOPEN_EXCLUSIVE
+ if override_exclusive:
+ flags |= canstat.canOPEN_OVERRIDE_EXCLUSIVE
if accept_virtual:
flags |= canstat.canOPEN_ACCEPT_VIRTUAL
+ if no_init_access:
+ flags |= canstat.canOPEN_NO_INIT_ACCESS
if fd:
- flags |= canstat.canOPEN_CAN_FD
+ if fd_non_iso:
+ flags |= canstat.canOPEN_CAN_FD_NONISO
+ else:
+ flags |= canstat.canOPEN_CAN_FD
- log.debug('Creating read handle to bus channel: %s' % channel)
+ log.debug("Creating read handle to bus channel: %s", channel)
self._read_handle = canOpenChannel(channel, flags)
- canIoCtl(self._read_handle,
- canstat.canIOCTL_SET_TIMER_SCALE,
- ctypes.byref(ctypes.c_long(TIMESTAMP_RESOLUTION)),
- 4)
-
- if fd:
- if 'tseg1' not in config and bitrate in BITRATE_FD:
- # Use predefined bitrate for arbitration
- bitrate = BITRATE_FD[bitrate]
- if data_bitrate in BITRATE_FD:
- # Use predefined bitrate for data
- data_bitrate = BITRATE_FD[data_bitrate]
- elif not data_bitrate:
- # Use same bitrate for arbitration and data phase
- data_bitrate = bitrate
- canSetBusParamsFd(self._read_handle, bitrate, tseg1, tseg2, sjw)
+ canIoCtlInit(
+ self._read_handle,
+ canstat.canIOCTL_SET_TIMER_SCALE,
+ ctypes.byref(ctypes.c_long(TIMESTAMP_RESOLUTION)),
+ 4,
+ )
+ if isinstance(timing, BitTimingFd):
+ timing = check_or_adjust_timing_clock(timing, [80_000_000])
+ canSetBusParams(
+ self._read_handle,
+ timing.nom_bitrate,
+ timing.nom_tseg1,
+ timing.nom_tseg2,
+ timing.nom_sjw,
+ 1,
+ 0,
+ )
+ canSetBusParamsFd(
+ self._read_handle,
+ timing.data_bitrate,
+ timing.data_tseg1,
+ timing.data_tseg2,
+ timing.data_sjw,
+ )
+ elif isinstance(timing, BitTiming):
+ timing = check_or_adjust_timing_clock(timing, [16_000_000])
+ canSetBusParamsC200(self._read_handle, timing.btr0, timing.btr1)
else:
- if 'tseg1' not in config and bitrate in BITRATE_OBJS:
- bitrate = BITRATE_OBJS[bitrate]
- canSetBusParams(self._read_handle, bitrate, tseg1, tseg2, sjw, no_samp, 0)
+ if fd:
+ if "tseg1" not in kwargs and bitrate in BITRATE_FD:
+ # Use predefined bitrate for arbitration
+ bitrate = BITRATE_FD[bitrate]
+ if data_bitrate in BITRATE_FD:
+ # Use predefined bitrate for data
+ data_bitrate = BITRATE_FD[data_bitrate]
+ elif not data_bitrate:
+ # Use same bitrate for arbitration and data phase
+ data_bitrate = bitrate
+ canSetBusParamsFd(self._read_handle, data_bitrate, tseg1, tseg2, sjw)
+ else:
+ if "tseg1" not in kwargs and bitrate in BITRATE_OBJS:
+ bitrate = BITRATE_OBJS[bitrate]
+ canSetBusParams(self._read_handle, bitrate, tseg1, tseg2, sjw, no_samp, 0)
# By default, use local echo if single handle is used (see #160)
local_echo = single_handle or receive_own_messages
if receive_own_messages and single_handle:
log.warning("receive_own_messages only works if single_handle is False")
- canIoCtl(self._read_handle,
- canstat.canIOCTL_SET_LOCAL_TXECHO,
- ctypes.byref(ctypes.c_byte(local_echo)),
- 1)
+ canIoCtlInit(
+ self._read_handle,
+ canstat.canIOCTL_SET_LOCAL_TXECHO,
+ ctypes.byref(ctypes.c_byte(local_echo)),
+ 1,
+ )
+
+ # enable canMSG_LOCAL_TXACK flag in received messages
+
+ canIoCtlInit(
+ self._read_handle,
+ canstat.canIOCTL_SET_LOCAL_TXACK,
+ ctypes.byref(ctypes.c_byte(local_echo)),
+ 1,
+ )
if self.single_handle:
log.debug("We don't require separate handles to the bus")
self._write_handle = self._read_handle
else:
- log.debug('Creating separate handle for TX on channel: %s' % channel)
- self._write_handle = canOpenChannel(channel, flags)
- canBusOn(self._read_handle)
+ log.debug("Creating separate handle for TX on channel: %s", channel)
+ if exclusive:
+ flags_ = flags & ~canstat.canOPEN_EXCLUSIVE
+ flags_ |= canstat.canOPEN_OVERRIDE_EXCLUSIVE
+ else:
+ flags_ = flags
+ self._write_handle = canOpenChannel(channel, flags_)
- can_driver_mode = canstat.canDRIVER_SILENT if driver_mode == DRIVER_MODE_SILENT else canstat.canDRIVER_NORMAL
+ can_driver_mode = (
+ canstat.canDRIVER_SILENT
+ if driver_mode == DRIVER_MODE_SILENT
+ else canstat.canDRIVER_NORMAL
+ )
canSetBusOutputControl(self._write_handle, can_driver_mode)
- log.debug('Going bus on TX handle')
+
+ self._is_filtered = False
+ super().__init__(
+ channel=channel,
+ can_filters=can_filters,
+ **kwargs,
+ )
+
+ # activate channel after CAN filters were applied
+ log.debug("Go on bus")
+ if not self.single_handle:
+ canBusOn(self._read_handle)
canBusOn(self._write_handle)
+ # timestamp must be set after bus is online, otherwise kvReadTimer may return erroneous values
+ self._timestamp_offset = self._update_timestamp_offset()
+
+ def _update_timestamp_offset(self) -> float:
timer = ctypes.c_uint(0)
try:
- kvReadTimer(self._read_handle, ctypes.byref(timer))
+ if time.get_clock_info("time").resolution > 1e-5:
+ ts, perfcounter = time_perfcounter_correlation()
+ kvReadTimer(self._read_handle, ctypes.byref(timer))
+ current_perfcounter = time.perf_counter()
+ now = ts + (current_perfcounter - perfcounter)
+ return now - (timer.value * TIMESTAMP_FACTOR)
+ else:
+ kvReadTimer(self._read_handle, ctypes.byref(timer))
+ return time.time() - (timer.value * TIMESTAMP_FACTOR)
+
except Exception as exc:
# timer is usually close to 0
log.info(str(exc))
- self._timestamp_offset = time.time() - (timer.value * TIMESTAMP_FACTOR)
-
- self._is_filtered = False
- super(KvaserBus, self).__init__(channel=channel, can_filters=can_filters, **config)
+ return time.time() - (timer.value * TIMESTAMP_FACTOR)
def _apply_filters(self, filters):
if filters and len(filters) == 1:
- can_id = filters[0]['can_id']
- can_mask = filters[0]['can_mask']
- extended = 1 if filters[0].get('extended') else 0
+ can_id = filters[0]["can_id"]
+ can_mask = filters[0]["can_mask"]
+ extended = 1 if filters[0].get("extended") else 0
try:
for handle in (self._read_handle, self._write_handle):
canSetAcceptanceFilter(handle, can_id, can_mask, extended)
except (NotImplementedError, CANLIBError) as e:
self._is_filtered = False
- log.error('Filtering is not supported - %s', e)
+ log.error("Filtering is not supported - %s", e)
else:
self._is_filtered = True
- log.info('canlib is filtering on ID 0x%X, mask 0x%X', can_id, can_mask)
+ log.info("canlib is filtering on ID 0x%X, mask 0x%X", can_id, can_mask)
else:
self._is_filtered = False
- log.info('Hardware filtering has been disabled')
+ log.info("Hardware filtering has been disabled")
try:
for handle in (self._read_handle, self._write_handle):
for extended in (0, 1):
canSetAcceptanceFilter(handle, 0, 0, extended)
- except (NotImplementedError, CANLIBError):
- # TODO add logging?
- pass
+ except (NotImplementedError, CANLIBError) as e:
+ log.error("An error occurred while disabling filtering: %s", e)
def flush_tx_buffer(self):
- """ Wipeout the transmit buffer on the Kvaser.
- """
+ """Wipeout the transmit buffer on the Kvaser."""
canIoCtl(self._write_handle, canstat.canIOCTL_FLUSH_TX_BUFFER, 0, 0)
def _recv_internal(self, timeout=None):
@@ -484,7 +665,7 @@ def _recv_internal(self, timeout=None):
else:
timeout = int(timeout * 1000)
- #log.log(9, 'Reading for %d ms on handle: %s' % (timeout, self._read_handle))
+ # log.log(9, 'Reading for %d ms on handle: %s' % (timeout, self._read_handle))
status = canReadWait(
self._read_handle,
ctypes.byref(arb_id),
@@ -492,42 +673,43 @@ def _recv_internal(self, timeout=None):
ctypes.byref(dlc),
ctypes.byref(flags),
ctypes.byref(timestamp),
- timeout # This is an X ms blocking read
+ timeout, # This is an X ms blocking read
)
if status == canstat.canOK:
- #log.debug('read complete -> status OK')
data_array = data.raw
flags = flags.value
is_extended = bool(flags & canstat.canMSG_EXT)
is_remote_frame = bool(flags & canstat.canMSG_RTR)
is_error_frame = bool(flags & canstat.canMSG_ERROR_FRAME)
is_fd = bool(flags & canstat.canFDMSG_FDF)
+ is_rx = not bool(flags & canstat.canMSG_LOCAL_TXACK)
bitrate_switch = bool(flags & canstat.canFDMSG_BRS)
error_state_indicator = bool(flags & canstat.canFDMSG_ESI)
msg_timestamp = timestamp.value * TIMESTAMP_FACTOR
- rx_msg = Message(arbitration_id=arb_id.value,
- data=data_array[:dlc.value],
- dlc=dlc.value,
- extended_id=is_extended,
- is_error_frame=is_error_frame,
- is_remote_frame=is_remote_frame,
- is_fd=is_fd,
- bitrate_switch=bitrate_switch,
- error_state_indicator=error_state_indicator,
- channel=self.channel,
- timestamp=msg_timestamp + self._timestamp_offset)
- rx_msg.flags = flags
- rx_msg.raw_timestamp = msg_timestamp
- #log.debug('Got message: %s' % rx_msg)
+ rx_msg = Message(
+ arbitration_id=arb_id.value,
+ data=data_array[: dlc.value],
+ dlc=dlc.value,
+ is_extended_id=is_extended,
+ is_error_frame=is_error_frame,
+ is_remote_frame=is_remote_frame,
+ is_fd=is_fd,
+ is_rx=is_rx,
+ bitrate_switch=bitrate_switch,
+ error_state_indicator=error_state_indicator,
+ channel=self.channel,
+ timestamp=msg_timestamp + self._timestamp_offset,
+ )
+ # log.debug('Got message: %s' % rx_msg)
return rx_msg, self._is_filtered
else:
- #log.debug('read complete -> status not okay')
+ # log.debug('read complete -> status not okay')
return None, self._is_filtered
def send(self, msg, timeout=None):
- #log.debug("Writing a message: {}".format(msg))
- flags = canstat.canMSG_EXT if msg.id_type else canstat.canMSG_STD
+ # log.debug("Writing a message: {}".format(msg))
+ flags = canstat.canMSG_EXT if msg.is_extended_id else canstat.canMSG_STD
if msg.is_remote_frame:
flags |= canstat.canMSG_RTR
if msg.is_error_frame:
@@ -538,11 +720,9 @@ def send(self, msg, timeout=None):
flags |= canstat.canFDMSG_BRS
ArrayConstructor = ctypes.c_byte * msg.dlc
buf = ArrayConstructor(*msg.data)
- canWrite(self._write_handle,
- msg.arbitration_id,
- ctypes.byref(buf),
- msg.dlc,
- flags)
+ canWrite(
+ self._write_handle, msg.arbitration_id, ctypes.byref(buf), msg.dlc, flags
+ )
if timeout:
canWriteSync(self._write_handle, int(timeout * 1000))
@@ -559,9 +739,10 @@ def flash(self, flash=True):
try:
kvFlashLeds(self._read_handle, action, 30000)
except (CANLIBError, NotImplementedError) as e:
- log.error('Could not flash LEDs (%s)', e)
+ log.error("Could not flash LEDs (%s)", e)
def shutdown(self):
+ super().shutdown()
# Wait for transmit queue to be cleared
try:
canWriteSync(self._write_handle, 100)
@@ -575,17 +756,48 @@ def shutdown(self):
canBusOff(self._write_handle)
canClose(self._write_handle)
+ def get_stats(self) -> structures.BusStatistics:
+ """Retrieves the bus statistics.
+
+ Use like so:
+
+ .. testsetup:: kvaser
+
+ from unittest.mock import Mock
+ from can.interfaces.kvaser.structures import BusStatistics
+ bus = Mock()
+ bus.get_stats = Mock(side_effect=lambda: BusStatistics())
+
+ .. doctest:: kvaser
+
+ >>> stats = bus.get_stats()
+ >>> print(stats)
+ std_data: 0, std_remote: 0, ext_data: 0, ext_remote: 0, err_frame: 0, bus_load: 0.0%, overruns: 0
+
+ :returns: bus statistics.
+ """
+ canRequestBusStatistics(self._write_handle)
+ stats = structures.BusStatistics()
+ canGetBusStatistics(
+ self._write_handle, ctypes.pointer(stats), ctypes.sizeof(stats)
+ )
+ return stats
+
@staticmethod
def _detect_available_configs():
- num_channels = ctypes.c_int(0)
+ config_list = []
+
try:
+ num_channels = ctypes.c_int(0)
canGetNumberOfChannels(ctypes.byref(num_channels))
- except Exception:
+
+ for channel in range(0, int(num_channels.value)):
+ info = get_channel_info(channel)
+
+ config_list.append({"interface": "kvaser", "channel": channel, **info})
+ except (CANLIBError, NameError):
pass
- return [
- {'interface': 'kvaser', 'channel': channel}
- for channel in range(num_channels.value)
- ]
+ return config_list
def get_channel_info(channel):
@@ -593,18 +805,30 @@ def get_channel_info(channel):
serial = ctypes.c_uint64()
number = ctypes.c_uint()
- canGetChannelData(channel,
- canstat.canCHANNELDATA_DEVDESCR_ASCII,
- ctypes.byref(name), ctypes.sizeof(name))
- canGetChannelData(channel,
- canstat.canCHANNELDATA_CARD_SERIAL_NO,
- ctypes.byref(serial), ctypes.sizeof(serial))
- canGetChannelData(channel,
- canstat.canCHANNELDATA_CHAN_NO_ON_CARD,
- ctypes.byref(number), ctypes.sizeof(number))
-
- return '%s, S/N %d (#%d)' % (
- name.value.decode("ascii"), serial.value, number.value + 1)
+ canGetChannelData(
+ channel,
+ canstat.canCHANNELDATA_DEVDESCR_ASCII,
+ ctypes.byref(name),
+ ctypes.sizeof(name),
+ )
+ canGetChannelData(
+ channel,
+ canstat.canCHANNELDATA_CARD_SERIAL_NO,
+ ctypes.byref(serial),
+ ctypes.sizeof(serial),
+ )
+ canGetChannelData(
+ channel,
+ canstat.canCHANNELDATA_CHAN_NO_ON_CARD,
+ ctypes.byref(number),
+ ctypes.sizeof(number),
+ )
+
+ return {
+ "device_name": name.value.decode("ascii", errors="replace"),
+ "serial": serial.value,
+ "dongle_channel": number.value + 1,
+ }
init_kvaser_library()
diff --git a/can/interfaces/kvaser/constants.py b/can/interfaces/kvaser/constants.py
index 1c658dce0..dc710648c 100644
--- a/can/interfaces/kvaser/constants.py
+++ b/can/interfaces/kvaser/constants.py
@@ -1,6 +1,3 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Contains Python equivalents of the function and constant
definitions in CANLIB's canstat.h, with some supporting functionality
@@ -15,6 +12,7 @@
class c_canStatus(ctypes.c_int):
pass
+
# TODO better formatting
canOK = 0
canERR_PARAM = -1
@@ -55,7 +53,8 @@ class c_canStatus(ctypes.c_int):
def CANSTATUS_SUCCESS(status):
return status >= canOK
-canMSG_MASK = 0x00ff
+
+canMSG_MASK = 0x00FF
canMSG_RTR = 0x0001
canMSG_STD = 0x0002
canMSG_EXT = 0x0004
@@ -64,12 +63,13 @@ def CANSTATUS_SUCCESS(status):
canMSG_ERROR_FRAME = 0x0020
canMSG_TXACK = 0x0040
canMSG_TXRQ = 0x0080
+canMSG_LOCAL_TXACK = 0x1000_0000
canFDMSG_FDF = 0x010000
canFDMSG_BRS = 0x020000
canFDMSG_ESI = 0x040000
-canMSGERR_MASK = 0xff00
+canMSGERR_MASK = 0xFF00
canMSGERR_HW_OVERRUN = 0x0200
canMSGERR_SW_OVERRUN = 0x0400
canMSGERR_STUFF = 0x0800
@@ -154,7 +154,7 @@ def CANSTATUS_SUCCESS(status):
canTRANSCEIVER_TYPE_LINX_J1708: "LINX_J1708",
canTRANSCEIVER_TYPE_LINX_K: "LINX_K",
canTRANSCEIVER_TYPE_LINX_SWC: "LINX_SWC",
- canTRANSCEIVER_TYPE_LINX_LS: "LINX_LS"
+ canTRANSCEIVER_TYPE_LINX_LS: "LINX_LS",
}
canDRIVER_NORMAL = 4
@@ -162,6 +162,8 @@ def CANSTATUS_SUCCESS(status):
canDRIVER_SELFRECEPTION = 8
canDRIVER_OFF = 0
+canOPEN_EXCLUSIVE = 0x0008
+canOPEN_REQUIRE_EXTENDED = 0x0010
canOPEN_ACCEPT_VIRTUAL = 0x0020
canOPEN_OVERRIDE_EXCLUSIVE = 0x0040
canOPEN_REQUIRE_INIT_ACCESS = 0x0080
@@ -194,6 +196,7 @@ def CANSTATUS_SUCCESS(status):
canIOCTL_GET_USB_THROTTLE = 29
canIOCTL_SET_BUSON_TIME_AUTO_RESET = 30
canIOCTL_SET_LOCAL_TXECHO = 32
+canIOCTL_SET_LOCAL_TXACK = 46
canIOCTL_PREFER_EXT = 1
canIOCTL_PREFER_STD = 2
canIOCTL_CLEAR_ERROR_COUNTERS = 5
diff --git a/can/interfaces/kvaser/structures.py b/can/interfaces/kvaser/structures.py
new file mode 100644
index 000000000..0229cc10a
--- /dev/null
+++ b/can/interfaces/kvaser/structures.py
@@ -0,0 +1,68 @@
+"""
+Contains Python equivalents of the structures in CANLIB's canlib.h,
+with some supporting functionality specific to Python.
+"""
+
+import ctypes
+
+
+class BusStatistics(ctypes.Structure):
+ """This structure is used with the method
+ :meth:`~can.interfaces.kvaser.canlib.KvaserBus.get_stats`.
+ """
+
+ _fields_ = [
+ ("m_stdData", ctypes.c_ulong),
+ ("m_stdRemote", ctypes.c_ulong),
+ ("m_extData", ctypes.c_ulong),
+ ("m_extRemote", ctypes.c_ulong),
+ ("m_errFrame", ctypes.c_ulong),
+ ("m_busLoad", ctypes.c_ulong),
+ ("m_overruns", ctypes.c_ulong),
+ ]
+
+ def __str__(self):
+ return (
+ f"std_data: {self.std_data}, "
+ f"std_remote: {self.std_remote}, "
+ f"ext_data: {self.ext_data}, "
+ f"ext_remote: {self.ext_remote}, "
+ f"err_frame: {self.err_frame}, "
+ f"bus_load: {self.bus_load / 100.0:.1f}%, "
+ f"overruns: {self.overruns}"
+ )
+
+ @property
+ def std_data(self):
+ """Number of received standard (11-bit identifiers) data frames."""
+ return self.m_stdData
+
+ @property
+ def std_remote(self):
+ """Number of received standard (11-bit identifiers) remote frames."""
+ return self.m_stdRemote
+
+ @property
+ def ext_data(self):
+ """Number of received extended (29-bit identifiers) data frames."""
+ return self.m_extData
+
+ @property
+ def ext_remote(self):
+ """Number of received extended (29-bit identifiers) remote frames."""
+ return self.m_extRemote
+
+ @property
+ def err_frame(self):
+ """Number of error frames."""
+ return self.m_errFrame
+
+ @property
+ def bus_load(self):
+ """The bus load, expressed as an integer in the interval 0 - 10000 representing 0.00% - 100.00% bus load."""
+ return self.m_busLoad
+
+ @property
+ def overruns(self):
+ """Number of overruns."""
+ return self.m_overruns
diff --git a/can/interfaces/neousys/__init__.py b/can/interfaces/neousys/__init__.py
new file mode 100644
index 000000000..f3e0cb039
--- /dev/null
+++ b/can/interfaces/neousys/__init__.py
@@ -0,0 +1,8 @@
+"""Neousys CAN bus driver"""
+
+__all__ = [
+ "NeousysBus",
+ "neousys",
+]
+
+from can.interfaces.neousys.neousys import NeousysBus
diff --git a/can/interfaces/neousys/neousys.py b/can/interfaces/neousys/neousys.py
new file mode 100644
index 000000000..7e8c877b4
--- /dev/null
+++ b/can/interfaces/neousys/neousys.py
@@ -0,0 +1,246 @@
+"""Neousys CAN bus driver"""
+
+#
+# This kind of interface can be found for example on Neousys POC-551VTC
+# One needs to have correct drivers and DLL (Share object for Linux) from Neousys
+#
+# https://www.neousys-tech.com/en/support-service/resources/category/299-poc-551vtc-driver
+#
+# Beware this is only tested on Linux kernel higher than v5.3. This should be drop in
+# with Windows but you have to replace with correct named DLL
+#
+
+# pylint: disable=too-few-public-methods
+# pylint: disable=too-many-instance-attributes
+# pylint: disable=wrong-import-position
+
+import logging
+import platform
+import queue
+from ctypes import (
+ CFUNCTYPE,
+ POINTER,
+ Structure,
+ byref,
+ c_ubyte,
+ c_uint,
+ c_ushort,
+ sizeof,
+)
+from time import time
+
+try:
+ from ctypes import WinDLL
+except ImportError:
+ from ctypes import CDLL
+
+from can import (
+ BusABC,
+ CanInitializationError,
+ CanInterfaceNotImplementedError,
+ CanOperationError,
+ CanProtocol,
+ Message,
+)
+
+logger = logging.getLogger(__name__)
+
+
+class NeousysCanSetup(Structure):
+ """C CAN Setup struct"""
+
+ _fields_ = [
+ ("bitRate", c_uint),
+ ("recvConfig", c_uint),
+ ("recvId", c_uint),
+ ("recvMask", c_uint),
+ ]
+
+
+class NeousysCanMsg(Structure):
+ """C CAN Message struct"""
+
+ _fields_ = [
+ ("id", c_uint),
+ ("flags", c_ushort),
+ ("extra", c_ubyte),
+ ("len", c_ubyte),
+ ("data", c_ubyte * 8),
+ ]
+
+
+# valid:2~16, sum of the Synchronization, Propagation, and
+# Phase Buffer 1 segments, measured in time quanta.
+# valid:1~8, the Phase Buffer 2 segment in time quanta.
+# valid:1~4, Resynchronization Jump Width in time quanta
+# valid:1~1023, CAN_CLK divider used to determine time quanta
+class NeousysCanBitClk(Structure):
+ """C CAN BIT Clock struct"""
+
+ _fields_ = [
+ ("syncPropPhase1Seg", c_ushort),
+ ("phase2Seg", c_ushort),
+ ("jumpWidth", c_ushort),
+ ("quantumPrescaler", c_ushort),
+ ]
+
+
+NEOUSYS_CAN_MSG_CALLBACK = CFUNCTYPE(None, POINTER(NeousysCanMsg), c_uint)
+NEOUSYS_CAN_STATUS_CALLBACK = CFUNCTYPE(None, c_uint)
+
+NEOUSYS_CAN_MSG_EXTENDED_ID = 0x0004
+NEOUSYS_CAN_MSG_REMOTE_FRAME = 0x0040
+NEOUSYS_CAN_MSG_DATA_NEW = 0x0080
+NEOUSYS_CAN_MSG_DATA_LOST = 0x0100
+
+NEOUSYS_CAN_MSG_USE_ID_FILTER = 0x00000008
+NEOUSYS_CAN_MSG_USE_DIR_FILTER = (
+ 0x00000010 | NEOUSYS_CAN_MSG_USE_ID_FILTER
+) # only accept the direction specified in the message type
+NEOUSYS_CAN_MSG_USE_EXT_FILTER = (
+ 0x00000020 | NEOUSYS_CAN_MSG_USE_ID_FILTER
+) # filters on only extended identifiers
+
+NEOUSYS_CAN_STATUS_BUS_OFF = 0x00000080
+NEOUSYS_CAN_STATUS_EWARN = (
+ 0x00000040 # can controller error level has reached warning level.
+)
+NEOUSYS_CAN_STATUS_EPASS = (
+ 0x00000020 # can controller error level has reached error passive level.
+)
+NEOUSYS_CAN_STATUS_LEC_STUFF = 0x00000001 # a bit stuffing error has occurred.
+NEOUSYS_CAN_STATUS_LEC_FORM = 0x00000002 # a formatting error has occurred.
+NEOUSYS_CAN_STATUS_LEC_ACK = 0x00000003 # an acknowledge error has occurred.
+NEOUSYS_CAN_STATUS_LEC_BIT1 = (
+ 0x00000004 # the bus remained a bit level of 1 for longer than is allowed.
+)
+NEOUSYS_CAN_STATUS_LEC_BIT0 = (
+ 0x00000005 # the bus remained a bit level of 0 for longer than is allowed.
+)
+NEOUSYS_CAN_STATUS_LEC_CRC = 0x00000006 # a crc error has occurred.
+NEOUSYS_CAN_STATUS_LEC_MASK = (
+ 0x00000007 # this is the mask for the can last error code (lec).
+)
+
+NEOUSYS_CANLIB = None
+
+try:
+ if platform.system() == "Windows":
+ NEOUSYS_CANLIB = WinDLL("./WDT_DIO.dll")
+ else:
+ NEOUSYS_CANLIB = CDLL("libwdt_dio.so")
+ logger.info("Loaded Neousys WDT_DIO Can driver")
+except OSError as error:
+ logger.info("Cannot load Neousys CAN bus dll or shared object: %s", error)
+
+
+class NeousysBus(BusABC):
+ """Neousys CAN bus Class"""
+
+ def __init__(self, channel, device=0, bitrate=500000, **kwargs):
+ """
+ :param channel: channel number
+ :param device: device number
+ :param bitrate: bit rate.
+ """
+ super().__init__(channel, **kwargs)
+
+ if NEOUSYS_CANLIB is None:
+ raise CanInterfaceNotImplementedError("Neousys WDT_DIO Can driver missing")
+
+ self.channel = channel
+ self.device = device
+ self.channel_info = f"Neousys Can: device {self.device}, channel {self.channel}"
+ self._can_protocol = CanProtocol.CAN_20
+
+ self.queue = queue.Queue()
+
+ # Init with accept all and wanted bitrate
+ self.init_config = NeousysCanSetup(bitrate, NEOUSYS_CAN_MSG_USE_ID_FILTER, 0, 0)
+
+ self._neousys_recv_cb = NEOUSYS_CAN_MSG_CALLBACK(self._neousys_recv_cb)
+ self._neousys_status_cb = NEOUSYS_CAN_STATUS_CALLBACK(self._neousys_status_cb)
+
+ if NEOUSYS_CANLIB.CAN_RegisterReceived(0, self._neousys_recv_cb) == 0:
+ raise CanInitializationError("Neousys CAN bus Setup receive callback")
+
+ if NEOUSYS_CANLIB.CAN_RegisterStatus(0, self._neousys_status_cb) == 0:
+ raise CanInitializationError("Neousys CAN bus Setup status callback")
+
+ if (
+ NEOUSYS_CANLIB.CAN_Setup(
+ channel, byref(self.init_config), sizeof(self.init_config)
+ )
+ == 0
+ ):
+ raise CanInitializationError("Neousys CAN bus Setup Error")
+
+ if NEOUSYS_CANLIB.CAN_Start(channel) == 0:
+ raise CanInitializationError("Neousys CAN bus Start Error")
+
+ def send(self, msg, timeout=None) -> None:
+ """
+ :param msg: message to send
+ :param timeout: timeout is not used here
+ """
+
+ tx_msg = NeousysCanMsg(
+ msg.arbitration_id, 0, 0, msg.dlc, (c_ubyte * 8)(*msg.data)
+ )
+
+ if NEOUSYS_CANLIB.CAN_Send(self.channel, byref(tx_msg), sizeof(tx_msg)) == 0:
+ raise CanOperationError("Neousys Can can't send message")
+
+ def _recv_internal(self, timeout):
+ try:
+ return self.queue.get(block=True, timeout=timeout), False
+ except queue.Empty:
+ return None, False
+
+ def _neousys_recv_cb(self, msg, sizeof_msg) -> None:
+ """
+ :param msg: struct CAN_MSG
+ :param sizeof_msg: message number
+ """
+ msg_bytes = bytearray(msg.contents.data)
+ remote_frame = bool(msg.contents.flags & NEOUSYS_CAN_MSG_REMOTE_FRAME)
+ extended_frame = bool(msg.contents.flags & NEOUSYS_CAN_MSG_EXTENDED_ID)
+
+ if msg.contents.flags & NEOUSYS_CAN_MSG_DATA_LOST:
+ logger.error("_neousys_recv_cb flag CAN_MSG_DATA_LOST")
+
+ msg = Message(
+ timestamp=time(),
+ arbitration_id=msg.contents.id,
+ is_remote_frame=remote_frame,
+ is_extended_id=extended_frame,
+ channel=self.channel,
+ dlc=msg.contents.len,
+ data=msg_bytes[: msg.contents.len],
+ )
+
+ # Reading happens in Callback function and
+ # with Python-CAN it happens polling
+ # so cache stuff in array to for poll
+ try:
+ self.queue.put(msg)
+ except queue.Full:
+ raise CanOperationError("Neousys message Queue is full") from None
+
+ def _neousys_status_cb(self, status) -> None:
+ """
+ :param status: BUS Status
+ """
+ logger.info("%s _neousys_status_cb: %d", self.init_config, status)
+
+ def shutdown(self):
+ super().shutdown()
+ NEOUSYS_CANLIB.CAN_Stop(self.channel)
+
+ @staticmethod
+ def _detect_available_configs():
+ if NEOUSYS_CANLIB is None:
+ return []
+ else:
+ # There is only one channel
+ return [{"interface": "neousys", "channel": 0}]
diff --git a/can/interfaces/nican.py b/can/interfaces/nican.py
index 320ef6901..ba5b991c9 100644
--- a/can/interfaces/nican.py
+++ b/can/interfaces/nican.py
@@ -1,6 +1,3 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
NI-CAN interface module.
@@ -11,7 +8,7 @@
TODO: We could implement this interface such that setting other filters
could work when the initial filters were set to zero using the
software fallback. Or could the software filters even be changed
- after the connection was opened? We need to document that bahaviour!
+ after the connection was opened? We need to document that behaviour!
See also the IXXAT interface.
"""
@@ -20,39 +17,48 @@
import logging
import sys
-from can import CanError, BusABC, Message
+import can.typechecking
+from can import (
+ BusABC,
+ CanError,
+ CanInitializationError,
+ CanInterfaceNotImplementedError,
+ CanOperationError,
+ CanProtocol,
+ Message,
+)
logger = logging.getLogger(__name__)
-NC_SUCCESS = 0
-NC_ERR_TIMEOUT = 1
-TIMEOUT_ERROR_CODE = -1074388991
+NC_SUCCESS = 0
+NC_ERR_TIMEOUT = 1
+TIMEOUT_ERROR_CODE = -1074388991
-NC_DURATION_INFINITE = 0xFFFFFFFF
+NC_DURATION_INFINITE = 0xFFFFFFFF
-NC_OP_START = 0x80000001
-NC_OP_STOP = 0x80000002
-NC_OP_RESET = 0x80000003
+NC_OP_START = 0x80000001
+NC_OP_STOP = 0x80000002
+NC_OP_RESET = 0x80000003
-NC_FRMTYPE_REMOTE = 1
-NC_FRMTYPE_COMM_ERR = 2
+NC_FRMTYPE_REMOTE = 1
+NC_FRMTYPE_COMM_ERR = 2
-NC_ST_READ_AVAIL = 0x00000001
-NC_ST_WRITE_SUCCESS = 0x00000002
-NC_ST_ERROR = 0x00000010
-NC_ST_WARNING = 0x00000020
+NC_ST_READ_AVAIL = 0x00000001
+NC_ST_WRITE_SUCCESS = 0x00000002
+NC_ST_ERROR = 0x00000010
+NC_ST_WARNING = 0x00000020
-NC_ATTR_BAUD_RATE = 0x80000007
+NC_ATTR_BAUD_RATE = 0x80000007
NC_ATTR_START_ON_OPEN = 0x80000006
-NC_ATTR_READ_Q_LEN = 0x80000013
-NC_ATTR_WRITE_Q_LEN = 0x80000014
-NC_ATTR_CAN_COMP_STD = 0x80010001
-NC_ATTR_CAN_MASK_STD = 0x80010002
-NC_ATTR_CAN_COMP_XTD = 0x80010003
-NC_ATTR_CAN_MASK_XTD = 0x80010004
+NC_ATTR_READ_Q_LEN = 0x80000013
+NC_ATTR_WRITE_Q_LEN = 0x80000014
+NC_ATTR_CAN_COMP_STD = 0x80010001
+NC_ATTR_CAN_MASK_STD = 0x80010002
+NC_ATTR_CAN_COMP_XTD = 0x80010003
+NC_ATTR_CAN_MASK_XTD = 0x80010004
NC_ATTR_LOG_COMM_ERRS = 0x8001000A
-NC_FL_CAN_ARBID_XTD = 0x20000000
+NC_FL_CAN_ARBID_XTD = 0x20000000
CanData = ctypes.c_ubyte * 8
@@ -67,6 +73,7 @@ class RxMessageStruct(ctypes.Structure):
("data", CanData),
]
+
class TxMessageStruct(ctypes.Structure):
_fields_ = [
("arb_id", ctypes.c_ulong),
@@ -76,15 +83,48 @@ class TxMessageStruct(ctypes.Structure):
]
-def check_status(result, function, arguments):
+class NicanError(CanError):
+ """Error from NI-CAN driver."""
+
+ def __init__(self, function, error_code: int, arguments) -> None:
+ super().__init__(
+ message=f"{function} failed: {get_error_message(error_code)}",
+ error_code=error_code,
+ )
+
+ #: Function that failed
+ self.function = function
+
+ #: Arguments passed to function
+ self.arguments = arguments
+
+
+class NicanInitializationError(NicanError, CanInitializationError):
+ pass
+
+
+class NicanOperationError(NicanError, CanOperationError):
+ pass
+
+
+def check_status(
+ result: int,
+ function,
+ arguments,
+ error_class: type[NicanError] = NicanOperationError,
+) -> int:
if result > 0:
logger.warning(get_error_message(result))
elif result < 0:
- raise NicanError(function, result, arguments)
+ raise error_class(function, result, arguments)
return result
-def get_error_message(status_code):
+def check_status_init(*args, **kwargs) -> int:
+ return check_status(*args, **kwargs, error_class=NicanInitializationError)
+
+
+def get_error_message(status_code: int) -> str:
"""Convert status code to descriptive string."""
errmsg = ctypes.create_string_buffer(1024)
nican.ncStatusToString(status_code, len(errmsg), errmsg)
@@ -99,24 +139,39 @@ def get_error_message(status_code):
logger.error("Failed to load NI-CAN driver: %s", e)
else:
nican.ncConfig.argtypes = [
- ctypes.c_char_p, ctypes.c_ulong, ctypes.c_void_p, ctypes.c_void_p]
- nican.ncConfig.errcheck = check_status
+ ctypes.c_char_p,
+ ctypes.c_ulong,
+ ctypes.c_void_p,
+ ctypes.c_void_p,
+ ]
+ nican.ncConfig.errcheck = check_status_init
+
nican.ncOpenObject.argtypes = [ctypes.c_char_p, ctypes.c_void_p]
- nican.ncOpenObject.errcheck = check_status
+ nican.ncOpenObject.errcheck = check_status_init
+
nican.ncCloseObject.errcheck = check_status
+
nican.ncAction.argtypes = [ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong]
nican.ncAction.errcheck = check_status
+
nican.ncRead.errcheck = check_status
+
nican.ncWrite.errcheck = check_status
+
nican.ncWaitForState.argtypes = [
- ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_void_p]
+ ctypes.c_ulong,
+ ctypes.c_ulong,
+ ctypes.c_ulong,
+ ctypes.c_void_p,
+ ]
nican.ncWaitForState.errcheck = check_status
- nican.ncStatusToString.argtypes = [
- ctypes.c_int, ctypes.c_uint, ctypes.c_char_p]
+
+ nican.ncStatusToString.argtypes = [ctypes.c_int, ctypes.c_uint, ctypes.c_char_p]
else:
nican = None
logger.warning("NI-CAN interface is only available on Windows systems")
+
class NicanBus(BusABC):
"""
The CAN Bus implemented for the NI-CAN interface.
@@ -124,69 +179,80 @@ class NicanBus(BusABC):
.. warning::
This interface does implement efficient filtering of messages, but
- the filters have to be set in :meth:`~can.interfaces.nican.NicanBus.__init__`
- using the ``can_filters`` parameter. Using :meth:`~can.interfaces.nican.NicanBus.set_filters`
- does not work.
-
+ the filters have to be set in ``__init__`` using the ``can_filters`` parameter.
+ Using :meth:`~can.BusABC.set_filters` does not work.
"""
- def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, **kwargs):
+ def __init__(
+ self,
+ channel: str,
+ can_filters: can.typechecking.CanFilters | None = None,
+ bitrate: int | None = None,
+ log_errors: bool = True,
+ **kwargs,
+ ) -> None:
"""
- :param str channel:
- Name of the object to open (e.g. 'CAN0')
+ :param channel:
+ Name of the object to open (e.g. `"CAN0"`)
- :param int bitrate:
- Bitrate in bits/s
+ :param bitrate:
+ Bitrate in bit/s
- :param list can_filters:
+ :param can_filters:
See :meth:`can.BusABC.set_filters`.
- :param bool log_errors:
+ :param log_errors:
If True, communication errors will appear as CAN messages with
``is_error_frame`` set to True and ``arbitration_id`` will identify
the error (default True)
- :raises can.interfaces.nican.NicanError:
- If starting communication fails
-
+ :raise ~can.exceptions.CanInterfaceNotImplementedError:
+ If the current operating system is not supported or the driver could not be loaded.
+ :raise ~can.interfaces.nican.NicanInitializationError:
+ If the bus could not be set up.
"""
if nican is None:
- raise ImportError("The NI-CAN driver could not be loaded. "
- "Check that you are using 32-bit Python on Windows.")
+ raise CanInterfaceNotImplementedError(
+ "The NI-CAN driver could not be loaded. "
+ "Check that you are using 32-bit Python on Windows."
+ )
self.channel = channel
- self.channel_info = "NI-CAN: " + channel
- if not isinstance(channel, bytes):
- channel = channel.encode()
+ self.channel_info = f"NI-CAN: {channel}"
+ self._can_protocol = CanProtocol.CAN_20
+ channel_bytes = channel.encode("ascii")
- config = [
- (NC_ATTR_START_ON_OPEN, True),
- (NC_ATTR_LOG_COMM_ERRS, log_errors)
- ]
+ config = [(NC_ATTR_START_ON_OPEN, True), (NC_ATTR_LOG_COMM_ERRS, log_errors)]
if not can_filters:
logger.info("Filtering has been disabled")
- config.extend([
- (NC_ATTR_CAN_COMP_STD, 0),
- (NC_ATTR_CAN_MASK_STD, 0),
- (NC_ATTR_CAN_COMP_XTD, 0),
- (NC_ATTR_CAN_MASK_XTD, 0)
- ])
+ config.extend(
+ [
+ (NC_ATTR_CAN_COMP_STD, 0),
+ (NC_ATTR_CAN_MASK_STD, 0),
+ (NC_ATTR_CAN_COMP_XTD, 0),
+ (NC_ATTR_CAN_MASK_XTD, 0),
+ ]
+ )
else:
for can_filter in can_filters:
can_id = can_filter["can_id"]
can_mask = can_filter["can_mask"]
logger.info("Filtering on ID 0x%X, mask 0x%X", can_id, can_mask)
if can_filter.get("extended"):
- config.extend([
- (NC_ATTR_CAN_COMP_XTD, can_id | NC_FL_CAN_ARBID_XTD),
- (NC_ATTR_CAN_MASK_XTD, can_mask)
- ])
+ config.extend(
+ [
+ (NC_ATTR_CAN_COMP_XTD, can_id | NC_FL_CAN_ARBID_XTD),
+ (NC_ATTR_CAN_MASK_XTD, can_mask),
+ ]
+ )
else:
- config.extend([
- (NC_ATTR_CAN_COMP_STD, can_id),
- (NC_ATTR_CAN_MASK_STD, can_mask),
- ])
+ config.extend(
+ [
+ (NC_ATTR_CAN_COMP_STD, can_id),
+ (NC_ATTR_CAN_MASK_STD, can_mask),
+ ]
+ )
if bitrate:
config.append((NC_ATTR_BAUD_RATE, bitrate))
@@ -194,26 +260,32 @@ def __init__(self, channel, can_filters=None, bitrate=None, log_errors=True, **k
AttrList = ctypes.c_ulong * len(config)
attr_id_list = AttrList(*(row[0] for row in config))
attr_value_list = AttrList(*(row[1] for row in config))
- nican.ncConfig(channel,
- len(config),
- ctypes.byref(attr_id_list),
- ctypes.byref(attr_value_list))
+ nican.ncConfig(
+ channel_bytes,
+ len(config),
+ ctypes.byref(attr_id_list),
+ ctypes.byref(attr_value_list),
+ )
self.handle = ctypes.c_ulong()
- nican.ncOpenObject(channel, ctypes.byref(self.handle))
+ nican.ncOpenObject(channel_bytes, ctypes.byref(self.handle))
- super(NicanBus, self).__init__(channel=channel,
- can_filters=can_filters, bitrate=bitrate,
- log_errors=log_errors, **kwargs)
+ super().__init__(
+ channel=channel,
+ can_filters=can_filters,
+ bitrate=bitrate,
+ log_errors=log_errors,
+ **kwargs,
+ )
- def _recv_internal(self, timeout):
+ def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]:
"""
Read a message from a NI-CAN bus.
- :param float timeout:
- Max time to wait in seconds or None if infinite
+ :param timeout:
+ Max time to wait in seconds or ``None`` if infinite
- :raises can.interfaces.nican.NicanError:
+ :raises can.interfaces.nican.NicanOperationError:
If reception fails
"""
if timeout is None:
@@ -224,7 +296,8 @@ def _recv_internal(self, timeout):
state = ctypes.c_ulong()
try:
nican.ncWaitForState(
- self.handle, NC_ST_READ_AVAIL, timeout, ctypes.byref(state))
+ self.handle, NC_ST_READ_AVAIL, timeout, ctypes.byref(state)
+ )
except NicanError as e:
if e.error_code == TIMEOUT_ERROR_CODE:
return None, True
@@ -242,81 +315,62 @@ def _recv_internal(self, timeout):
if not is_error_frame:
arb_id &= 0x1FFFFFFF
dlc = raw_msg.dlc
- msg = Message(timestamp=timestamp,
- channel=self.channel,
- is_remote_frame=is_remote_frame,
- is_error_frame=is_error_frame,
- extended_id=is_extended,
- arbitration_id=arb_id,
- dlc=dlc,
- data=raw_msg.data[:dlc])
+ msg = Message(
+ timestamp=timestamp,
+ channel=self.channel,
+ is_remote_frame=is_remote_frame,
+ is_error_frame=is_error_frame,
+ is_extended_id=is_extended,
+ arbitration_id=arb_id,
+ dlc=dlc,
+ data=raw_msg.data[:dlc],
+ )
return msg, True
- def send(self, msg, timeout=None):
+ def send(self, msg: Message, timeout: float | None = None) -> None:
"""
Send a message to NI-CAN.
- :param can.Message msg:
+ :param msg:
Message to send
- :raises can.interfaces.nican.NicanError:
+ :param timeout:
+ The timeout
+
+ .. warning:: This gets ignored.
+
+ :raises can.interfaces.nican.NicanOperationError:
If writing to transmit buffer fails.
It does not wait for message to be ACKed currently.
"""
arb_id = msg.arbitration_id
- if msg.id_type:
+ if msg.is_extended_id:
arb_id |= NC_FL_CAN_ARBID_XTD
- raw_msg = TxMessageStruct(arb_id,
- bool(msg.is_remote_frame),
- msg.dlc,
- CanData(*msg.data))
- nican.ncWrite(
- self.handle, ctypes.sizeof(raw_msg), ctypes.byref(raw_msg))
+ raw_msg = TxMessageStruct(
+ arb_id, bool(msg.is_remote_frame), msg.dlc, CanData(*msg.data)
+ )
+ nican.ncWrite(self.handle, ctypes.sizeof(raw_msg), ctypes.byref(raw_msg))
# TODO:
# ncWaitForState can not be called here if the recv() method is called
# from a different thread, which is a very common use case.
# Maybe it is possible to use ncCreateNotification instead but seems a
# bit overkill at the moment.
- #state = ctypes.c_ulong()
- #nican.ncWaitForState(
- # self.handle, NC_ST_WRITE_SUCCESS, int(timeout * 1000), ctypes.byref(state))
+ # state = ctypes.c_ulong()
+ # nican.ncWaitForState(self.handle, NC_ST_WRITE_SUCCESS, int(timeout * 1000), ctypes.byref(state))
- def reset(self):
+ def reset(self) -> None:
"""
Resets network interface. Stops network interface, then resets the CAN
chip to clear the CAN error counters (clear error passive state).
Resetting includes clearing all entries from read and write queues.
+
+ :raises can.interfaces.nican.NicanOperationError:
+ If resetting fails.
"""
nican.ncAction(self.handle, NC_OP_RESET, 0)
- def shutdown(self):
+ def shutdown(self) -> None:
"""Close object."""
+ super().shutdown()
nican.ncCloseObject(self.handle)
-
- __set_filters_has_been_called = False
- def set_filters(self, can_filers=None):
- """Unsupported. See note on :class:`~can.interfaces.nican.NicanBus`.
- """
- if self.__set_filters_has_been_called:
- logger.warn("using filters is not supported like this, see note on NicanBus")
- else:
- # allow the constructor to call this without causing a warning
- self.__set_filters_has_been_called = True
-
-
-class NicanError(CanError):
- """Error from NI-CAN driver."""
-
- def __init__(self, function, error_code, arguments):
- super(NicanError, self).__init__()
- #: Status code
- self.error_code = error_code
- #: Function that failed
- self.function = function
- #: Arguments passed to function
- self.arguments = arguments
-
- def __str__(self):
- return "Function %s failed:\n%s" % (
- self.function.__name__, get_error_message(self.error_code))
diff --git a/can/interfaces/nixnet.py b/can/interfaces/nixnet.py
new file mode 100644
index 000000000..ec303a364
--- /dev/null
+++ b/can/interfaces/nixnet.py
@@ -0,0 +1,351 @@
+"""
+NI-XNET interface module.
+
+Implementation references:
+ NI-XNET Hardware and Software Manual: https://www.ni.com/pdf/manuals/372840h.pdf
+ NI-XNET Python implementation: https://github.com/ni/nixnet-python
+
+Authors: Javier Rubio Giménez , Jose A. Escobar
+"""
+
+import logging
+import os
+import time
+import warnings
+from queue import SimpleQueue
+from types import ModuleType
+from typing import Any
+
+import can.typechecking
+from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message
+from can.exceptions import (
+ CanInitializationError,
+ CanInterfaceNotImplementedError,
+ CanOperationError,
+)
+from can.util import check_or_adjust_timing_clock, deprecated_args_alias
+
+logger = logging.getLogger(__name__)
+
+nixnet: ModuleType | None = None
+try:
+ import nixnet # type: ignore
+ import nixnet.constants # type: ignore
+ import nixnet.system # type: ignore
+ import nixnet.types # type: ignore
+except Exception as exc:
+ logger.warning("Could not import nixnet: %s", exc)
+
+
+class NiXNETcanBus(BusABC):
+ """
+ The CAN Bus implemented for the NI-XNET interface.
+ """
+
+ @deprecated_args_alias(
+ deprecation_start="4.2.0",
+ deprecation_end="5.0.0",
+ brs=None,
+ log_errors=None,
+ )
+ def __init__(
+ self,
+ channel: str = "CAN1",
+ bitrate: int = 500_000,
+ timing: BitTiming | BitTimingFd | None = None,
+ can_filters: can.typechecking.CanFilters | None = None,
+ receive_own_messages: bool = False,
+ can_termination: bool = False,
+ fd: bool = False,
+ fd_bitrate: int | None = None,
+ poll_interval: float = 0.001,
+ **kwargs: Any,
+ ) -> None:
+ """
+ :param str channel:
+ Name of the object to open (e.g. 'CAN0')
+
+ :param int bitrate:
+ Bitrate in bits/s
+
+ :param timing:
+ Optional :class:`~can.BitTiming` or :class:`~can.BitTimingFd` instance
+ to use for custom bit timing setting. The `f_clock` value of the timing
+ instance must be set to 40_000_000 (40MHz).
+ If this parameter is provided, it takes precedence over all other
+ timing-related parameters like `bitrate`, `fd_bitrate` and `fd`.
+
+ :param list can_filters:
+ See :meth:`can.BusABC.set_filters`.
+
+ :param receive_own_messages:
+ Enable self-reception of sent messages.
+
+ :param poll_interval:
+ Poll interval in seconds.
+
+ :raises ~can.exceptions.CanInitializationError:
+ If starting communication fails
+ """
+ if os.name != "nt" and not kwargs.get("_testing", False):
+ raise CanInterfaceNotImplementedError(
+ f"The NI-XNET interface is only supported on Windows, "
+ f'but you are running "{os.name}"'
+ )
+
+ if nixnet is None:
+ raise CanInterfaceNotImplementedError("The NI-XNET API has not been loaded")
+
+ self.nixnet = nixnet
+
+ self._rx_queue = SimpleQueue() # type: ignore[var-annotated]
+ self.channel = channel
+ self.channel_info = "NI-XNET: " + channel
+
+ self.poll_interval = poll_interval
+
+ is_fd = isinstance(timing, BitTimingFd) if timing else fd
+ self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20
+
+ # Set database for the initialization
+ database_name = ":can_fd_brs:" if is_fd else ":memory:"
+
+ try:
+ # We need two sessions for this application,
+ # one to send frames and another to receive them
+ self._session_send = nixnet.session.FrameOutStreamSession(
+ channel, database_name=database_name
+ )
+ self._session_receive = nixnet.session.FrameInStreamSession(
+ channel, database_name=database_name
+ )
+ self._interface = self._session_send.intf
+
+ # set interface properties
+ self._interface.can_lstn_only = kwargs.get("listen_only", False)
+ self._interface.echo_tx = receive_own_messages
+ self._interface.bus_err_to_in_strm = True
+
+ if isinstance(timing, BitTimingFd):
+ timing = check_or_adjust_timing_clock(timing, [40_000_000])
+ custom_nom_baud_rate = ( # nxPropSession_IntfBaudRate64
+ 0xA0000000
+ + (timing.nom_tq << 32)
+ + (timing.nom_sjw - 1 << 16)
+ + (timing.nom_tseg1 - 1 << 8)
+ + (timing.nom_tseg2 - 1)
+ )
+ custom_data_baud_rate = ( # nxPropSession_IntfCanFdBaudRate64
+ 0xA0000000
+ + (timing.data_tq << 13)
+ + (timing.data_tseg1 - 1 << 8)
+ + (timing.data_tseg2 - 1 << 4)
+ + (timing.data_sjw - 1)
+ )
+ self._interface.baud_rate = custom_nom_baud_rate
+ self._interface.can_fd_baud_rate = custom_data_baud_rate
+ elif isinstance(timing, BitTiming):
+ timing = check_or_adjust_timing_clock(timing, [40_000_000])
+ custom_baud_rate = ( # nxPropSession_IntfBaudRate64
+ 0xA0000000
+ + (timing.tq << 32)
+ + (timing.sjw - 1 << 16)
+ + (timing.tseg1 - 1 << 8)
+ + (timing.tseg2 - 1)
+ )
+ self._interface.baud_rate = custom_baud_rate
+ else:
+ # See page 1017 of NI-XNET Hardware and Software Manual
+ # to set custom can configuration
+ if bitrate:
+ self._interface.baud_rate = bitrate
+
+ if is_fd:
+ # See page 951 of NI-XNET Hardware and Software Manual
+ # to set custom can configuration
+ self._interface.can_fd_baud_rate = fd_bitrate or bitrate
+
+ _can_termination = (
+ nixnet.constants.CanTerm.ON
+ if can_termination
+ else nixnet.constants.CanTerm.OFF
+ )
+ self._interface.can_term = _can_termination
+
+ # self._session_receive.queue_size = 512
+ # Once that all the parameters have been set, we start the sessions
+ self._session_send.start()
+ self._session_receive.start()
+
+ except nixnet.errors.XnetError as error:
+ raise CanInitializationError(
+ f"{error.args[0]} ({error.error_type})", error.error_code
+ ) from None
+
+ self._is_filtered = False
+ super().__init__(
+ channel=channel,
+ can_filters=can_filters,
+ bitrate=bitrate,
+ **kwargs,
+ )
+
+ @property
+ def fd(self) -> bool:
+ class_name = self.__class__.__name__
+ warnings.warn(
+ f"The {class_name}.fd property is deprecated and superseded by "
+ f"{class_name}.protocol. It is scheduled for removal in python-can version 5.0.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self._can_protocol is CanProtocol.CAN_FD
+
+ def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]:
+ end_time = time.perf_counter() + timeout if timeout is not None else None
+
+ while True:
+ # try to read all available frames
+ for frame in self._session_receive.frames.read(1024, timeout=0):
+ self._rx_queue.put_nowait(frame)
+
+ if self._rx_queue.qsize():
+ break
+
+ # check for timeout
+ if end_time is not None and time.perf_counter() > end_time:
+ return None, False
+
+ # Wait a short time until we try to read again
+ time.sleep(self.poll_interval)
+
+ can_frame = self._rx_queue.get_nowait()
+
+ # Timestamp should be converted from raw frame format(100ns increment
+ # from(12:00 a.m. January 1 1601 Coordinated Universal Time (UTC))
+ # to epoch time(number of seconds from January 1, 1970 (midnight UTC/GMT))
+ timestamp = can_frame.timestamp * 1e-7 - 11_644_473_600
+ if can_frame.type is self.nixnet.constants.FrameType.CAN_BUS_ERROR:
+ msg = Message(
+ timestamp=timestamp,
+ channel=self.channel,
+ is_error_frame=True,
+ )
+ else:
+ msg = Message(
+ timestamp=timestamp,
+ channel=self.channel,
+ is_remote_frame=can_frame.type
+ is self.nixnet.constants.FrameType.CAN_REMOTE,
+ is_error_frame=False,
+ is_fd=(
+ can_frame.type is self.nixnet.constants.FrameType.CANFD_DATA
+ or can_frame.type is self.nixnet.constants.FrameType.CANFDBRS_DATA
+ ),
+ bitrate_switch=(
+ can_frame.type is self.nixnet.constants.FrameType.CANFDBRS_DATA
+ ),
+ is_extended_id=can_frame.identifier.extended,
+ # Get identifier from CanIdentifier structure
+ arbitration_id=can_frame.identifier.identifier,
+ dlc=len(can_frame.payload),
+ data=can_frame.payload,
+ is_rx=not can_frame.echo,
+ )
+ return msg, False
+
+ def send(self, msg: Message, timeout: float | None = None) -> None:
+ """
+ Send a message using NI-XNET.
+
+ :param can.Message msg:
+ Message to send
+
+ :param float timeout:
+ Max time to wait for the device to be ready in seconds, None if time is infinite
+
+ :raises can.exceptions.CanOperationError:
+ If writing to transmit buffer fails.
+ It does not wait for message to be ACKed currently.
+ """
+ if timeout is None:
+ timeout = self.nixnet.constants.TIMEOUT_INFINITE
+
+ if msg.is_remote_frame:
+ type_message = self.nixnet.constants.FrameType.CAN_REMOTE
+ elif msg.is_error_frame:
+ type_message = self.nixnet.constants.FrameType.CAN_BUS_ERROR
+ elif msg.is_fd:
+ if msg.bitrate_switch:
+ type_message = self.nixnet.constants.FrameType.CANFDBRS_DATA
+ else:
+ type_message = self.nixnet.constants.FrameType.CANFD_DATA
+ else:
+ type_message = self.nixnet.constants.FrameType.CAN_DATA
+
+ can_frame = self.nixnet.types.CanFrame(
+ self.nixnet.types.CanIdentifier(msg.arbitration_id, msg.is_extended_id),
+ type=type_message,
+ payload=msg.data,
+ )
+
+ try:
+ self._session_send.frames.write([can_frame], timeout)
+ except self.nixnet.errors.XnetError as error:
+ raise CanOperationError(
+ f"{error.args[0]} ({error.error_type})", error.error_code
+ ) from None
+
+ def reset(self) -> None:
+ """
+ Resets network interface. Stops network interface, then resets the CAN
+ chip to clear the CAN error counters (clear error passive state).
+ Resetting includes clearing all entries from read and write queues.
+ """
+ self._session_send.flush()
+ self._session_receive.flush()
+
+ self._session_send.stop()
+ self._session_receive.stop()
+
+ self._session_send.start()
+ self._session_receive.start()
+
+ def shutdown(self) -> None:
+ """Close object."""
+ super().shutdown()
+ if hasattr(self, "_session_send"):
+ self._session_send.flush()
+ self._session_send.stop()
+ self._session_send.close()
+
+ if hasattr(self, "_session_receive"):
+ self._session_receive.flush()
+ self._session_receive.stop()
+ self._session_receive.close()
+
+ @staticmethod
+ def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]:
+ configs = []
+
+ try:
+ with nixnet.system.System() as nixnet_system: # type: ignore[union-attr]
+ for interface in nixnet_system.intf_refs_can:
+ channel = str(interface)
+ logger.debug(
+ "Found channel index %d: %s", interface.port_num, channel
+ )
+ configs.append(
+ {
+ "interface": "nixnet",
+ "channel": channel,
+ "can_term_available": interface.can_term_cap
+ is nixnet.constants.CanTermCap.YES, # type: ignore[union-attr]
+ "supports_fd": interface.can_tcvr_cap
+ is nixnet.constants.CanTcvrCap.HS, # type: ignore[union-attr]
+ }
+ )
+ except Exception as error:
+ logger.debug("An error occured while searching for configs: %s", str(error))
+
+ return configs # type: ignore
diff --git a/can/interfaces/pcan/__init__.py b/can/interfaces/pcan/__init__.py
index 8dbcfd0f9..a3eafcb4d 100644
--- a/can/interfaces/pcan/__init__.py
+++ b/can/interfaces/pcan/__init__.py
@@ -1,7 +1,10 @@
-#!/usr/bin/env python
-# coding: utf-8
+""" """
-"""
-"""
+__all__ = [
+ "PcanBus",
+ "PcanError",
+ "basic",
+ "pcan",
+]
-from can.interfaces.pcan.pcan import PcanBus
+from can.interfaces.pcan.pcan import PcanBus, PcanError
diff --git a/can/interfaces/pcan/basic.py b/can/interfaces/pcan/basic.py
index effbddd28..4d175c645 100644
--- a/can/interfaces/pcan/basic.py
+++ b/can/interfaces/pcan/basic.py
@@ -1,261 +1,422 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-"""
-PCAN-Basic API
-
-Author: Keneth Wagner
-Last change: 13.11.2017 Wagner
-
-Language: Python 2.7, 3.5
-
-Copyright (C) 1999-2017 PEAK-System Technik GmbH, Darmstadt, Germany
-http://www.peak-system.com
-"""
+# PCANBasic.py
+#
+# ~~~~~~~~~~~~
+#
+# PCAN-Basic API
+#
+# ~~~~~~~~~~~~
+#
+# ------------------------------------------------------------------
+# Author : Keneth Wagner
+# Last change: 2022-07-06
+# ------------------------------------------------------------------
+#
+# Copyright (C) 1999-2022 PEAK-System Technik GmbH, Darmstadt
+# more Info at http://www.peak-system.com
-from ctypes import *
-from string import *
-import platform
+# Module Imports
import logging
+import platform
+from ctypes import *
+from ctypes.util import find_library
+
+PLATFORM = platform.system()
+IS_WINDOWS = PLATFORM == "Windows"
+IS_LINUX = PLATFORM == "Linux"
-logger = logging.getLogger('can.pcan')
+logger = logging.getLogger("can.pcan")
-#///////////////////////////////////////////////////////////
+# ///////////////////////////////////////////////////////////
# Type definitions
-#///////////////////////////////////////////////////////////
-
-TPCANHandle = c_ushort # Represents a PCAN hardware channel handle
-TPCANStatus = int # Represents a PCAN status/error code
-TPCANParameter = c_ubyte # Represents a PCAN parameter to be read or set
-TPCANDevice = c_ubyte # Represents a PCAN device
-TPCANMessageType = c_ubyte # Represents the type of a PCAN message
-TPCANType = c_ubyte # Represents the type of PCAN hardware to be initialized
-TPCANMode = c_ubyte # Represents a PCAN filter mode
-TPCANBaudrate = c_ushort # Represents a PCAN Baud rate register value
-TPCANBitrateFD = c_char_p # Represents a PCAN-FD bit rate string
-TPCANTimestampFD = c_ulonglong # Represents a timestamp of a received PCAN FD message
-
-#///////////////////////////////////////////////////////////
+# ///////////////////////////////////////////////////////////
+
+TPCANHandle = c_ushort # Represents a PCAN hardware channel handle
+TPCANStatus = int # Represents a PCAN status/error code
+TPCANParameter = c_ubyte # Represents a PCAN parameter to be read or set
+TPCANDevice = c_ubyte # Represents a PCAN device
+TPCANMessageType = c_ubyte # Represents the type of a PCAN message
+TPCANType = c_ubyte # Represents the type of PCAN hardware to be initialized
+TPCANMode = c_ubyte # Represents a PCAN filter mode
+TPCANBaudrate = c_ushort # Represents a PCAN Baud rate register value
+TPCANBitrateFD = c_char_p # Represents a PCAN-FD bit rate string
+TPCANTimestampFD = c_ulonglong # Represents a timestamp of a received PCAN FD message
+
+# ///////////////////////////////////////////////////////////
# Value definitions
-#///////////////////////////////////////////////////////////
+# ///////////////////////////////////////////////////////////
# Currently defined and supported PCAN channels
-
-PCAN_NONEBUS = TPCANHandle(0x00) # Undefined/default value for a PCAN bus
-
-PCAN_ISABUS1 = TPCANHandle(0x21) # PCAN-ISA interface, channel 1
-PCAN_ISABUS2 = TPCANHandle(0x22) # PCAN-ISA interface, channel 2
-PCAN_ISABUS3 = TPCANHandle(0x23) # PCAN-ISA interface, channel 3
-PCAN_ISABUS4 = TPCANHandle(0x24) # PCAN-ISA interface, channel 4
-PCAN_ISABUS5 = TPCANHandle(0x25) # PCAN-ISA interface, channel 5
-PCAN_ISABUS6 = TPCANHandle(0x26) # PCAN-ISA interface, channel 6
-PCAN_ISABUS7 = TPCANHandle(0x27) # PCAN-ISA interface, channel 7
-PCAN_ISABUS8 = TPCANHandle(0x28) # PCAN-ISA interface, channel 8
-
-PCAN_DNGBUS1 = TPCANHandle(0x31) # PCAN-Dongle/LPT interface, channel 1
-
-PCAN_PCIBUS1 = TPCANHandle(0x41) # PCAN-PCI interface, channel 1
-PCAN_PCIBUS2 = TPCANHandle(0x42) # PCAN-PCI interface, channel 2
-PCAN_PCIBUS3 = TPCANHandle(0x43) # PCAN-PCI interface, channel 3
-PCAN_PCIBUS4 = TPCANHandle(0x44) # PCAN-PCI interface, channel 4
-PCAN_PCIBUS5 = TPCANHandle(0x45) # PCAN-PCI interface, channel 5
-PCAN_PCIBUS6 = TPCANHandle(0x46) # PCAN-PCI interface, channel 6
-PCAN_PCIBUS7 = TPCANHandle(0x47) # PCAN-PCI interface, channel 7
-PCAN_PCIBUS8 = TPCANHandle(0x48) # PCAN-PCI interface, channel 8
-PCAN_PCIBUS9 = TPCANHandle(0x409) # PCAN-PCI interface, channel 9
-PCAN_PCIBUS10 = TPCANHandle(0x40A) # PCAN-PCI interface, channel 10
-PCAN_PCIBUS11 = TPCANHandle(0x40B) # PCAN-PCI interface, channel 11
-PCAN_PCIBUS12 = TPCANHandle(0x40C) # PCAN-PCI interface, channel 12
-PCAN_PCIBUS13 = TPCANHandle(0x40D) # PCAN-PCI interface, channel 13
-PCAN_PCIBUS14 = TPCANHandle(0x40E) # PCAN-PCI interface, channel 14
-PCAN_PCIBUS15 = TPCANHandle(0x40F) # PCAN-PCI interface, channel 15
-PCAN_PCIBUS16 = TPCANHandle(0x410) # PCAN-PCI interface, channel 16
-
-PCAN_USBBUS1 = TPCANHandle(0x51) # PCAN-USB interface, channel 1
-PCAN_USBBUS2 = TPCANHandle(0x52) # PCAN-USB interface, channel 2
-PCAN_USBBUS3 = TPCANHandle(0x53) # PCAN-USB interface, channel 3
-PCAN_USBBUS4 = TPCANHandle(0x54) # PCAN-USB interface, channel 4
-PCAN_USBBUS5 = TPCANHandle(0x55) # PCAN-USB interface, channel 5
-PCAN_USBBUS6 = TPCANHandle(0x56) # PCAN-USB interface, channel 6
-PCAN_USBBUS7 = TPCANHandle(0x57) # PCAN-USB interface, channel 7
-PCAN_USBBUS8 = TPCANHandle(0x58) # PCAN-USB interface, channel 8
-PCAN_USBBUS9 = TPCANHandle(0x509) # PCAN-USB interface, channel 9
-PCAN_USBBUS10 = TPCANHandle(0x50A) # PCAN-USB interface, channel 10
-PCAN_USBBUS11 = TPCANHandle(0x50B) # PCAN-USB interface, channel 11
-PCAN_USBBUS12 = TPCANHandle(0x50C) # PCAN-USB interface, channel 12
-PCAN_USBBUS13 = TPCANHandle(0x50D) # PCAN-USB interface, channel 13
-PCAN_USBBUS14 = TPCANHandle(0x50E) # PCAN-USB interface, channel 14
-PCAN_USBBUS15 = TPCANHandle(0x50F) # PCAN-USB interface, channel 15
-PCAN_USBBUS16 = TPCANHandle(0x510) # PCAN-USB interface, channel 16
-
-PCAN_PCCBUS1 = TPCANHandle(0x61) # PCAN-PC Card interface, channel 1
-PCAN_PCCBUS2 = TPCANHandle(0x62) # PCAN-PC Card interface, channel 2
-
-PCAN_LANBUS1 = TPCANHandle(0x801) # PCAN-LAN interface, channel 1
-PCAN_LANBUS2 = TPCANHandle(0x802) # PCAN-LAN interface, channel 2
-PCAN_LANBUS3 = TPCANHandle(0x803) # PCAN-LAN interface, channel 3
-PCAN_LANBUS4 = TPCANHandle(0x804) # PCAN-LAN interface, channel 4
-PCAN_LANBUS5 = TPCANHandle(0x805) # PCAN-LAN interface, channel 5
-PCAN_LANBUS6 = TPCANHandle(0x806) # PCAN-LAN interface, channel 6
-PCAN_LANBUS7 = TPCANHandle(0x807) # PCAN-LAN interface, channel 7
-PCAN_LANBUS8 = TPCANHandle(0x808) # PCAN-LAN interface, channel 8
-PCAN_LANBUS9 = TPCANHandle(0x809) # PCAN-LAN interface, channel 9
-PCAN_LANBUS10 = TPCANHandle(0x80A) # PCAN-LAN interface, channel 10
-PCAN_LANBUS11 = TPCANHandle(0x80B) # PCAN-LAN interface, channel 11
-PCAN_LANBUS12 = TPCANHandle(0x80C) # PCAN-LAN interface, channel 12
-PCAN_LANBUS13 = TPCANHandle(0x80D) # PCAN-LAN interface, channel 13
-PCAN_LANBUS14 = TPCANHandle(0x80E) # PCAN-LAN interface, channel 14
-PCAN_LANBUS15 = TPCANHandle(0x80F) # PCAN-LAN interface, channel 15
-PCAN_LANBUS16 = TPCANHandle(0x810) # PCAN-LAN interface, channel 16
+#
+PCAN_NONEBUS = TPCANHandle(0x00) # Undefined/default value for a PCAN bus
+
+PCAN_ISABUS1 = TPCANHandle(0x21) # PCAN-ISA interface, channel 1
+PCAN_ISABUS2 = TPCANHandle(0x22) # PCAN-ISA interface, channel 2
+PCAN_ISABUS3 = TPCANHandle(0x23) # PCAN-ISA interface, channel 3
+PCAN_ISABUS4 = TPCANHandle(0x24) # PCAN-ISA interface, channel 4
+PCAN_ISABUS5 = TPCANHandle(0x25) # PCAN-ISA interface, channel 5
+PCAN_ISABUS6 = TPCANHandle(0x26) # PCAN-ISA interface, channel 6
+PCAN_ISABUS7 = TPCANHandle(0x27) # PCAN-ISA interface, channel 7
+PCAN_ISABUS8 = TPCANHandle(0x28) # PCAN-ISA interface, channel 8
+
+PCAN_DNGBUS1 = TPCANHandle(0x31) # PCAN-Dongle/LPT interface, channel 1
+
+PCAN_PCIBUS1 = TPCANHandle(0x41) # PCAN-PCI interface, channel 1
+PCAN_PCIBUS2 = TPCANHandle(0x42) # PCAN-PCI interface, channel 2
+PCAN_PCIBUS3 = TPCANHandle(0x43) # PCAN-PCI interface, channel 3
+PCAN_PCIBUS4 = TPCANHandle(0x44) # PCAN-PCI interface, channel 4
+PCAN_PCIBUS5 = TPCANHandle(0x45) # PCAN-PCI interface, channel 5
+PCAN_PCIBUS6 = TPCANHandle(0x46) # PCAN-PCI interface, channel 6
+PCAN_PCIBUS7 = TPCANHandle(0x47) # PCAN-PCI interface, channel 7
+PCAN_PCIBUS8 = TPCANHandle(0x48) # PCAN-PCI interface, channel 8
+PCAN_PCIBUS9 = TPCANHandle(0x409) # PCAN-PCI interface, channel 9
+PCAN_PCIBUS10 = TPCANHandle(0x40A) # PCAN-PCI interface, channel 10
+PCAN_PCIBUS11 = TPCANHandle(0x40B) # PCAN-PCI interface, channel 11
+PCAN_PCIBUS12 = TPCANHandle(0x40C) # PCAN-PCI interface, channel 12
+PCAN_PCIBUS13 = TPCANHandle(0x40D) # PCAN-PCI interface, channel 13
+PCAN_PCIBUS14 = TPCANHandle(0x40E) # PCAN-PCI interface, channel 14
+PCAN_PCIBUS15 = TPCANHandle(0x40F) # PCAN-PCI interface, channel 15
+PCAN_PCIBUS16 = TPCANHandle(0x410) # PCAN-PCI interface, channel 16
+
+PCAN_USBBUS1 = TPCANHandle(0x51) # PCAN-USB interface, channel 1
+PCAN_USBBUS2 = TPCANHandle(0x52) # PCAN-USB interface, channel 2
+PCAN_USBBUS3 = TPCANHandle(0x53) # PCAN-USB interface, channel 3
+PCAN_USBBUS4 = TPCANHandle(0x54) # PCAN-USB interface, channel 4
+PCAN_USBBUS5 = TPCANHandle(0x55) # PCAN-USB interface, channel 5
+PCAN_USBBUS6 = TPCANHandle(0x56) # PCAN-USB interface, channel 6
+PCAN_USBBUS7 = TPCANHandle(0x57) # PCAN-USB interface, channel 7
+PCAN_USBBUS8 = TPCANHandle(0x58) # PCAN-USB interface, channel 8
+PCAN_USBBUS9 = TPCANHandle(0x509) # PCAN-USB interface, channel 9
+PCAN_USBBUS10 = TPCANHandle(0x50A) # PCAN-USB interface, channel 10
+PCAN_USBBUS11 = TPCANHandle(0x50B) # PCAN-USB interface, channel 11
+PCAN_USBBUS12 = TPCANHandle(0x50C) # PCAN-USB interface, channel 12
+PCAN_USBBUS13 = TPCANHandle(0x50D) # PCAN-USB interface, channel 13
+PCAN_USBBUS14 = TPCANHandle(0x50E) # PCAN-USB interface, channel 14
+PCAN_USBBUS15 = TPCANHandle(0x50F) # PCAN-USB interface, channel 15
+PCAN_USBBUS16 = TPCANHandle(0x510) # PCAN-USB interface, channel 16
+
+PCAN_PCCBUS1 = TPCANHandle(0x61) # PCAN-PC Card interface, channel 1
+PCAN_PCCBUS2 = TPCANHandle(0x62) # PCAN-PC Card interface, channel 2
+
+PCAN_LANBUS1 = TPCANHandle(0x801) # PCAN-LAN interface, channel 1
+PCAN_LANBUS2 = TPCANHandle(0x802) # PCAN-LAN interface, channel 2
+PCAN_LANBUS3 = TPCANHandle(0x803) # PCAN-LAN interface, channel 3
+PCAN_LANBUS4 = TPCANHandle(0x804) # PCAN-LAN interface, channel 4
+PCAN_LANBUS5 = TPCANHandle(0x805) # PCAN-LAN interface, channel 5
+PCAN_LANBUS6 = TPCANHandle(0x806) # PCAN-LAN interface, channel 6
+PCAN_LANBUS7 = TPCANHandle(0x807) # PCAN-LAN interface, channel 7
+PCAN_LANBUS8 = TPCANHandle(0x808) # PCAN-LAN interface, channel 8
+PCAN_LANBUS9 = TPCANHandle(0x809) # PCAN-LAN interface, channel 9
+PCAN_LANBUS10 = TPCANHandle(0x80A) # PCAN-LAN interface, channel 10
+PCAN_LANBUS11 = TPCANHandle(0x80B) # PCAN-LAN interface, channel 11
+PCAN_LANBUS12 = TPCANHandle(0x80C) # PCAN-LAN interface, channel 12
+PCAN_LANBUS13 = TPCANHandle(0x80D) # PCAN-LAN interface, channel 13
+PCAN_LANBUS14 = TPCANHandle(0x80E) # PCAN-LAN interface, channel 14
+PCAN_LANBUS15 = TPCANHandle(0x80F) # PCAN-LAN interface, channel 15
+PCAN_LANBUS16 = TPCANHandle(0x810) # PCAN-LAN interface, channel 16
# Represent the PCAN error and status codes
-PCAN_ERROR_OK = TPCANStatus(0x00000) # No error
-PCAN_ERROR_XMTFULL = TPCANStatus(0x00001) # Transmit buffer in CAN controller is full
-PCAN_ERROR_OVERRUN = TPCANStatus(0x00002) # CAN controller was read too late
-PCAN_ERROR_BUSLIGHT = TPCANStatus(0x00004) # Bus error: an error counter reached the 'light' limit
-PCAN_ERROR_BUSHEAVY = TPCANStatus(0x00008) # Bus error: an error counter reached the 'heavy' limit
-PCAN_ERROR_BUSWARNING = TPCANStatus(PCAN_ERROR_BUSHEAVY) # Bus error: an error counter reached the 'warning' limit
-PCAN_ERROR_BUSPASSIVE = TPCANStatus(0x40000) # Bus error: the CAN controller is error passive
-PCAN_ERROR_BUSOFF = TPCANStatus(0x00010) # Bus error: the CAN controller is in bus-off state
-PCAN_ERROR_ANYBUSERR = TPCANStatus(PCAN_ERROR_BUSWARNING | PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY | PCAN_ERROR_BUSOFF | PCAN_ERROR_BUSPASSIVE) # Mask for all bus errors
-PCAN_ERROR_QRCVEMPTY = TPCANStatus(0x00020) # Receive queue is empty
-PCAN_ERROR_QOVERRUN = TPCANStatus(0x00040) # Receive queue was read too late
-PCAN_ERROR_QXMTFULL = TPCANStatus(0x00080) # Transmit queue is full
-PCAN_ERROR_REGTEST = TPCANStatus(0x00100) # Test of the CAN controller hardware registers failed (no hardware found)
-PCAN_ERROR_NODRIVER = TPCANStatus(0x00200) # Driver not loaded
-PCAN_ERROR_HWINUSE = TPCANStatus(0x00400) # Hardware already in use by a Net
-PCAN_ERROR_NETINUSE = TPCANStatus(0x00800) # A Client is already connected to the Net
-PCAN_ERROR_ILLHW = TPCANStatus(0x01400) # Hardware handle is invalid
-PCAN_ERROR_ILLNET = TPCANStatus(0x01800) # Net handle is invalid
-PCAN_ERROR_ILLCLIENT = TPCANStatus(0x01C00) # Client handle is invalid
-PCAN_ERROR_ILLHANDLE = TPCANStatus(PCAN_ERROR_ILLHW | PCAN_ERROR_ILLNET | PCAN_ERROR_ILLCLIENT) # Mask for all handle errors
-PCAN_ERROR_RESOURCE = TPCANStatus(0x02000) # Resource (FIFO, Client, timeout) cannot be created
-PCAN_ERROR_ILLPARAMTYPE = TPCANStatus(0x04000) # Invalid parameter
-PCAN_ERROR_ILLPARAMVAL = TPCANStatus(0x08000) # Invalid parameter value
-PCAN_ERROR_UNKNOWN = TPCANStatus(0x10000) # Unknown error
-PCAN_ERROR_ILLDATA = TPCANStatus(0x20000) # Invalid data, function, or action
-PCAN_ERROR_CAUTION = TPCANStatus(0x2000000)# An operation was successfully carried out, however, irregularities were registered
-PCAN_ERROR_INITIALIZE = TPCANStatus(0x4000000)# Channel is not initialized [Value was changed from 0x40000 to 0x4000000]
-PCAN_ERROR_ILLOPERATION = TPCANStatus(0x8000000)# Invalid operation [Value was changed from 0x80000 to 0x8000000]
+#
+PCAN_ERROR_OK = TPCANStatus(0x00000) # No error
+PCAN_ERROR_XMTFULL = TPCANStatus(0x00001) # Transmit buffer in CAN controller is full
+PCAN_ERROR_OVERRUN = TPCANStatus(0x00002) # CAN controller was read too late
+PCAN_ERROR_BUSLIGHT = TPCANStatus(
+ 0x00004
+) # Bus error: an error counter reached the 'light' limit
+PCAN_ERROR_BUSHEAVY = TPCANStatus(
+ 0x00008
+) # Bus error: an error counter reached the 'heavy' limit
+PCAN_ERROR_BUSWARNING = TPCANStatus(
+ PCAN_ERROR_BUSHEAVY
+) # Bus error: an error counter reached the 'warning' limit
+PCAN_ERROR_BUSPASSIVE = TPCANStatus(
+ 0x40000
+) # Bus error: the CAN controller is error passive
+PCAN_ERROR_BUSOFF = TPCANStatus(
+ 0x00010
+) # Bus error: the CAN controller is in bus-off state
+PCAN_ERROR_ANYBUSERR = TPCANStatus(
+ PCAN_ERROR_BUSWARNING
+ | PCAN_ERROR_BUSLIGHT
+ | PCAN_ERROR_BUSHEAVY
+ | PCAN_ERROR_BUSOFF
+ | PCAN_ERROR_BUSPASSIVE
+) # Mask for all bus errors
+PCAN_ERROR_QRCVEMPTY = TPCANStatus(0x00020) # Receive queue is empty
+PCAN_ERROR_QOVERRUN = TPCANStatus(0x00040) # Receive queue was read too late
+PCAN_ERROR_QXMTFULL = TPCANStatus(0x00080) # Transmit queue is full
+PCAN_ERROR_REGTEST = TPCANStatus(
+ 0x00100
+) # Test of the CAN controller hardware registers failed (no hardware found)
+PCAN_ERROR_NODRIVER = TPCANStatus(0x00200) # Driver not loaded
+PCAN_ERROR_HWINUSE = TPCANStatus(0x00400) # Hardware already in use by a Net
+PCAN_ERROR_NETINUSE = TPCANStatus(0x00800) # A Client is already connected to the Net
+PCAN_ERROR_ILLHW = TPCANStatus(0x01400) # Hardware handle is invalid
+PCAN_ERROR_ILLNET = TPCANStatus(0x01800) # Net handle is invalid
+PCAN_ERROR_ILLCLIENT = TPCANStatus(0x01C00) # Client handle is invalid
+PCAN_ERROR_ILLHANDLE = TPCANStatus(
+ PCAN_ERROR_ILLHW | PCAN_ERROR_ILLNET | PCAN_ERROR_ILLCLIENT
+) # Mask for all handle errors
+PCAN_ERROR_RESOURCE = TPCANStatus(
+ 0x02000
+) # Resource (FIFO, Client, timeout) cannot be created
+PCAN_ERROR_ILLPARAMTYPE = TPCANStatus(0x04000) # Invalid parameter
+PCAN_ERROR_ILLPARAMVAL = TPCANStatus(0x08000) # Invalid parameter value
+PCAN_ERROR_UNKNOWN = TPCANStatus(0x10000) # Unknown error
+PCAN_ERROR_ILLDATA = TPCANStatus(0x20000) # Invalid data, function, or action
+PCAN_ERROR_ILLMODE = TPCANStatus(
+ 0x80000
+) # Driver object state is wrong for the attempted operation
+PCAN_ERROR_CAUTION = TPCANStatus(
+ 0x2000000
+) # An operation was successfully carried out, however, irregularities were registered
+PCAN_ERROR_INITIALIZE = TPCANStatus(
+ 0x4000000
+) # Channel is not initialized [Value was changed from 0x40000 to 0x4000000]
+PCAN_ERROR_ILLOPERATION = TPCANStatus(
+ 0x8000000
+) # Invalid operation [Value was changed from 0x80000 to 0x8000000]
# PCAN devices
-PCAN_NONE = TPCANDevice(0x00) # Undefined, unknown or not selected PCAN device value
-PCAN_PEAKCAN = TPCANDevice(0x01) # PCAN Non-Plug&Play devices. NOT USED WITHIN PCAN-Basic API
-PCAN_ISA = TPCANDevice(0x02) # PCAN-ISA, PCAN-PC/104, and PCAN-PC/104-Plus
-PCAN_DNG = TPCANDevice(0x03) # PCAN-Dongle
-PCAN_PCI = TPCANDevice(0x04) # PCAN-PCI, PCAN-cPCI, PCAN-miniPCI, and PCAN-PCI Express
-PCAN_USB = TPCANDevice(0x05) # PCAN-USB and PCAN-USB Pro
-PCAN_PCC = TPCANDevice(0x06) # PCAN-PC Card
-PCAN_VIRTUAL = TPCANDevice(0x07) # PCAN Virtual hardware. NOT USED WITHIN PCAN-Basic API
-PCAN_LAN = TPCANDevice(0x08) # PCAN Gateway devices
+#
+PCAN_NONE = TPCANDevice(0x00) # Undefined, unknown or not selected PCAN device value
+PCAN_PEAKCAN = TPCANDevice(0x01) # PCAN Non-PnP devices. NOT USED WITHIN PCAN-Basic API
+PCAN_ISA = TPCANDevice(0x02) # PCAN-ISA, PCAN-PC/104, and PCAN-PC/104-Plus
+PCAN_DNG = TPCANDevice(0x03) # PCAN-Dongle
+PCAN_PCI = TPCANDevice(0x04) # PCAN-PCI, PCAN-cPCI, PCAN-miniPCI, and PCAN-PCI Express
+PCAN_USB = TPCANDevice(0x05) # PCAN-USB and PCAN-USB Pro
+PCAN_PCC = TPCANDevice(0x06) # PCAN-PC Card
+PCAN_VIRTUAL = TPCANDevice(
+ 0x07
+) # PCAN Virtual hardware. NOT USED WITHIN PCAN-Basic API
+PCAN_LAN = TPCANDevice(0x08) # PCAN Gateway devices
# PCAN parameters
-PCAN_DEVICE_NUMBER = TPCANParameter(0x01) # PCAN-USB device number parameter
-PCAN_5VOLTS_POWER = TPCANParameter(0x02) # PCAN-PC Card 5-Volt power parameter
-PCAN_RECEIVE_EVENT = TPCANParameter(0x03) # PCAN receive event handler parameter
-PCAN_MESSAGE_FILTER = TPCANParameter(0x04) # PCAN message filter parameter
-PCAN_API_VERSION = TPCANParameter(0x05) # PCAN-Basic API version parameter
-PCAN_CHANNEL_VERSION = TPCANParameter(0x06) # PCAN device channel version parameter
-PCAN_BUSOFF_AUTORESET = TPCANParameter(0x07) # PCAN Reset-On-Busoff parameter
-PCAN_LISTEN_ONLY = TPCANParameter(0x08) # PCAN Listen-Only parameter
-PCAN_LOG_LOCATION = TPCANParameter(0x09) # Directory path for log files
-PCAN_LOG_STATUS = TPCANParameter(0x0A) # Debug-Log activation status
-PCAN_LOG_CONFIGURE = TPCANParameter(0x0B) # Configuration of the debugged information (LOG_FUNCTION_***)
-PCAN_LOG_TEXT = TPCANParameter(0x0C) # Custom insertion of text into the log file
-PCAN_CHANNEL_CONDITION = TPCANParameter(0x0D) # Availability status of a PCAN-Channel
-PCAN_HARDWARE_NAME = TPCANParameter(0x0E) # PCAN hardware name parameter
-PCAN_RECEIVE_STATUS = TPCANParameter(0x0F) # Message reception status of a PCAN-Channel
-PCAN_CONTROLLER_NUMBER = TPCANParameter(0x10) # CAN-Controller number of a PCAN-Channel
-PCAN_TRACE_LOCATION = TPCANParameter(0x11) # Directory path for PCAN trace files
-PCAN_TRACE_STATUS = TPCANParameter(0x12) # CAN tracing activation status
-PCAN_TRACE_SIZE = TPCANParameter(0x13) # Configuration of the maximum file size of a CAN trace
-PCAN_TRACE_CONFIGURE = TPCANParameter(0x14) # Configuration of the trace file storing mode (TRACE_FILE_***)
-PCAN_CHANNEL_IDENTIFYING = TPCANParameter(0x15) # Physical identification of a USB based PCAN-Channel by blinking its associated LED
-PCAN_CHANNEL_FEATURES = TPCANParameter(0x16) # Capabilities of a PCAN device (FEATURE_***)
-PCAN_BITRATE_ADAPTING = TPCANParameter(0x17) # Using of an existing bit rate (PCAN-View connected to a channel)
-PCAN_BITRATE_INFO = TPCANParameter(0x18) # Configured bit rate as Btr0Btr1 value
-PCAN_BITRATE_INFO_FD = TPCANParameter(0x19) # Configured bit rate as TPCANBitrateFD string
-PCAN_BUSSPEED_NOMINAL = TPCANParameter(0x1A) # Configured nominal CAN Bus speed as Bits per seconds
-PCAN_BUSSPEED_DATA = TPCANParameter(0x1B) # Configured CAN data speed as Bits per seconds
-PCAN_IP_ADDRESS = TPCANParameter(0x1C) # Remote address of a LAN channel as string in IPv4 format
-PCAN_LAN_SERVICE_STATUS = TPCANParameter(0x1D) # Status of the Virtual PCAN-Gateway Service
-PCAN_ALLOW_STATUS_FRAMES = TPCANParameter(0x1E) # Status messages reception status within a PCAN-Channel
-PCAN_ALLOW_RTR_FRAMES = TPCANParameter(0x1F) # RTR messages reception status within a PCAN-Channel
-PCAN_ALLOW_ERROR_FRAMES = TPCANParameter(0x20) # Error messages reception status within a PCAN-Channel
-PCAN_INTERFRAME_DELAY = TPCANParameter(0x21) # Delay, in microseconds, between sending frames
-PCAN_ACCEPTANCE_FILTER_11BIT = TPCANParameter(0x22) # Filter over code and mask patterns for 11-Bit messages
-PCAN_ACCEPTANCE_FILTER_29BIT = TPCANParameter(0x23) # Filter over code and mask patterns for 29-Bit messages
-PCAN_IO_DIGITAL_CONFIGURATION = TPCANParameter(0x24) # Output mode of 32 digital I/O pin of a PCAN-USB Chip. 1: Output-Active 0 : Output Inactive
-PCAN_IO_DIGITAL_VALUE = TPCANParameter(0x25) # Value assigned to a 32 digital I/O pins of a PCAN-USB Chip
-PCAN_IO_DIGITAL_SET = TPCANParameter(0x26) # Value assigned to a 32 digital I/O pins of a PCAN-USB Chip - Multiple digital I/O pins to 1 = High
-PCAN_IO_DIGITAL_CLEAR = TPCANParameter(0x27) # Clear multiple digital I/O pins to 0
-PCAN_IO_ANALOG_VALUE = TPCANParameter(0x28) # Get value of a single analog input pin
+#
+PCAN_DEVICE_ID = TPCANParameter(0x01) # Device identifier parameter
+PCAN_5VOLTS_POWER = TPCANParameter(0x02) # 5-Volt power parameter
+PCAN_RECEIVE_EVENT = TPCANParameter(0x03) # PCAN receive event handler parameter
+PCAN_MESSAGE_FILTER = TPCANParameter(0x04) # PCAN message filter parameter
+PCAN_API_VERSION = TPCANParameter(0x05) # PCAN-Basic API version parameter
+PCAN_CHANNEL_VERSION = TPCANParameter(0x06) # PCAN device channel version parameter
+PCAN_BUSOFF_AUTORESET = TPCANParameter(0x07) # PCAN Reset-On-Busoff parameter
+PCAN_LISTEN_ONLY = TPCANParameter(0x08) # PCAN Listen-Only parameter
+PCAN_LOG_LOCATION = TPCANParameter(0x09) # Directory path for log files
+PCAN_LOG_STATUS = TPCANParameter(0x0A) # Debug-Log activation status
+PCAN_LOG_CONFIGURE = TPCANParameter(
+ 0x0B
+) # Configuration of the debugged information (LOG_FUNCTION_***)
+PCAN_LOG_TEXT = TPCANParameter(0x0C) # Custom insertion of text into the log file
+PCAN_CHANNEL_CONDITION = TPCANParameter(0x0D) # Availability status of a PCAN-Channel
+PCAN_HARDWARE_NAME = TPCANParameter(0x0E) # PCAN hardware name parameter
+PCAN_RECEIVE_STATUS = TPCANParameter(0x0F) # Message reception status of a PCAN-Channel
+PCAN_CONTROLLER_NUMBER = TPCANParameter(0x10) # CAN-Controller number of a PCAN-Channel
+PCAN_TRACE_LOCATION = TPCANParameter(0x11) # Directory path for PCAN trace files
+PCAN_TRACE_STATUS = TPCANParameter(0x12) # CAN tracing activation status
+PCAN_TRACE_SIZE = TPCANParameter(
+ 0x13
+) # Configuration of the maximum file size of a CAN trace
+PCAN_TRACE_CONFIGURE = TPCANParameter(
+ 0x14
+) # Configuration of the trace file storing mode (TRACE_FILE_***)
+PCAN_CHANNEL_IDENTIFYING = TPCANParameter(
+ 0x15
+) # Physical identification of a USB based PCAN-Channel by blinking its associated LED
+PCAN_CHANNEL_FEATURES = TPCANParameter(
+ 0x16
+) # Capabilities of a PCAN device (FEATURE_***)
+PCAN_BITRATE_ADAPTING = TPCANParameter(
+ 0x17
+) # Using of an existing bit rate (PCAN-View connected to a channel)
+PCAN_BITRATE_INFO = TPCANParameter(0x18) # Configured bit rate as Btr0Btr1 value
+PCAN_BITRATE_INFO_FD = TPCANParameter(
+ 0x19
+) # Configured bit rate as TPCANBitrateFD string
+PCAN_BUSSPEED_NOMINAL = TPCANParameter(
+ 0x1A
+) # Configured nominal CAN Bus speed as Bits per seconds
+PCAN_BUSSPEED_DATA = TPCANParameter(
+ 0x1B
+) # Configured CAN data speed as Bits per seconds
+PCAN_IP_ADDRESS = TPCANParameter(
+ 0x1C
+) # Remote address of a LAN channel as string in IPv4 format
+PCAN_LAN_SERVICE_STATUS = TPCANParameter(
+ 0x1D
+) # Status of the Virtual PCAN-Gateway Service
+PCAN_ALLOW_STATUS_FRAMES = TPCANParameter(
+ 0x1E
+) # Status messages reception status within a PCAN-Channel
+PCAN_ALLOW_RTR_FRAMES = TPCANParameter(
+ 0x1F
+) # RTR messages reception status within a PCAN-Channel
+PCAN_ALLOW_ERROR_FRAMES = TPCANParameter(
+ 0x20
+) # Error messages reception status within a PCAN-Channel
+PCAN_INTERFRAME_DELAY = TPCANParameter(
+ 0x21
+) # Delay, in microseconds, between sending frames
+PCAN_ACCEPTANCE_FILTER_11BIT = TPCANParameter(
+ 0x22
+) # Filter over code and mask patterns for 11-Bit messages
+PCAN_ACCEPTANCE_FILTER_29BIT = TPCANParameter(
+ 0x23
+) # Filter over code and mask patterns for 29-Bit messages
+PCAN_IO_DIGITAL_CONFIGURATION = TPCANParameter(
+ 0x24
+) # Output mode of 32 digital I/O pin of a PCAN-USB Chip. 1: Output-Active 0 : Output Inactive
+PCAN_IO_DIGITAL_VALUE = TPCANParameter(
+ 0x25
+) # Value assigned to a 32 digital I/O pins of a PCAN-USB Chip
+PCAN_IO_DIGITAL_SET = TPCANParameter(
+ 0x26
+) # Value assigned to a 32 digital I/O pins of a PCAN-USB Chip - Multiple digital I/O pins to 1 = High
+PCAN_IO_DIGITAL_CLEAR = TPCANParameter(0x27) # Clear multiple digital I/O pins to 0
+PCAN_IO_ANALOG_VALUE = TPCANParameter(0x28) # Get value of a single analog input pin
+PCAN_FIRMWARE_VERSION = TPCANParameter(
+ 0x29
+) # Get the version of the firmware used by the device associated with a PCAN-Channel
+PCAN_ATTACHED_CHANNELS_COUNT = TPCANParameter(
+ 0x2A
+) # Get the amount of PCAN channels attached to a system
+PCAN_ATTACHED_CHANNELS = TPCANParameter(
+ 0x2B
+) # Get information about PCAN channels attached to a system
+PCAN_ALLOW_ECHO_FRAMES = TPCANParameter(
+ 0x2C
+) # Echo messages reception status within a PCAN-Channel
+PCAN_DEVICE_PART_NUMBER = TPCANParameter(
+ 0x2D
+) # Get the part number associated to a device
+
+# DEPRECATED parameters
+#
+PCAN_DEVICE_NUMBER = PCAN_DEVICE_ID # DEPRECATED. Use PCAN_DEVICE_ID instead
# PCAN parameter values
-PCAN_PARAMETER_OFF = int(0x00) # The PCAN parameter is not set (inactive)
-PCAN_PARAMETER_ON = int(0x01) # The PCAN parameter is set (active)
-PCAN_FILTER_CLOSE = int(0x00) # The PCAN filter is closed. No messages will be received
-PCAN_FILTER_OPEN = int(0x01) # The PCAN filter is fully opened. All messages will be received
-PCAN_FILTER_CUSTOM = int(0x02) # The PCAN filter is custom configured. Only registered messages will be received
-PCAN_CHANNEL_UNAVAILABLE = int(0x00) # The PCAN-Channel handle is illegal, or its associated hardware is not available
-PCAN_CHANNEL_AVAILABLE = int(0x01) # The PCAN-Channel handle is available to be connected (Plug&Play Hardware: it means furthermore that the hardware is plugged-in)
-PCAN_CHANNEL_OCCUPIED = int(0x02) # The PCAN-Channel handle is valid, and is already being used
-PCAN_CHANNEL_PCANVIEW = PCAN_CHANNEL_AVAILABLE | PCAN_CHANNEL_OCCUPIED # The PCAN-Channel handle is already being used by a PCAN-View application, but is available to connect
-
-LOG_FUNCTION_DEFAULT = int(0x00) # Logs system exceptions / errors
-LOG_FUNCTION_ENTRY = int(0x01) # Logs the entries to the PCAN-Basic API functions
-LOG_FUNCTION_PARAMETERS = int(0x02) # Logs the parameters passed to the PCAN-Basic API functions
-LOG_FUNCTION_LEAVE = int(0x04) # Logs the exits from the PCAN-Basic API functions
-LOG_FUNCTION_WRITE = int(0x08) # Logs the CAN messages passed to the CAN_Write function
-LOG_FUNCTION_READ = int(0x10) # Logs the CAN messages received within the CAN_Read function
-LOG_FUNCTION_ALL = int(0xFFFF)# Logs all possible information within the PCAN-Basic API functions
-
-TRACE_FILE_SINGLE = int(0x00) # A single file is written until it size reaches PAN_TRACE_SIZE
-TRACE_FILE_SEGMENTED = int(0x01) # Traced data is distributed in several files with size PAN_TRACE_SIZE
-TRACE_FILE_DATE = int(0x02) # Includes the date into the name of the trace file
-TRACE_FILE_TIME = int(0x04) # Includes the start time into the name of the trace file
-TRACE_FILE_OVERWRITE = int(0x80) # Causes the overwriting of available traces (same name)
-
-FEATURE_FD_CAPABLE = int(0x01) # Device supports flexible data-rate (CAN-FD)
-FEATURE_DELAY_CAPABLE = int(0x02) # Device supports a delay between sending frames (FPGA based USB devices)
-FEATURE_IO_CAPABLE = int(0x04) # Device supports I/O functionality for electronic circuits (USB-Chip devices)
-
-SERVICE_STATUS_STOPPED = int(0x01) # The service is not running
-SERVICE_STATUS_RUNNING = int(0x04) # The service is running
+#
+PCAN_PARAMETER_OFF = 0x00 # The PCAN parameter is not set (inactive)
+PCAN_PARAMETER_ON = 0x01 # The PCAN parameter is set (active)
+PCAN_FILTER_CLOSE = 0x00 # The PCAN filter is closed. No messages will be received
+PCAN_FILTER_OPEN = (
+ 0x01 # The PCAN filter is fully opened. All messages will be received
+)
+PCAN_FILTER_CUSTOM = 0x02 # The PCAN filter is custom configured. Only registered messages will be received
+PCAN_CHANNEL_UNAVAILABLE = 0x00 # The PCAN-Channel handle is illegal, or its associated hardware is not available
+PCAN_CHANNEL_AVAILABLE = 0x01 # The PCAN-Channel handle is available to be connected (PnP Hardware: it means furthermore that the hardware is plugged-in)
+PCAN_CHANNEL_OCCUPIED = (
+ 0x02 # The PCAN-Channel handle is valid, and is already being used
+)
+PCAN_CHANNEL_PCANVIEW = (
+ PCAN_CHANNEL_AVAILABLE | PCAN_CHANNEL_OCCUPIED
+) # The PCAN-Channel handle is already being used by a PCAN-View application, but is available to connect
+
+LOG_FUNCTION_DEFAULT = 0x00 # Logs system exceptions / errors
+LOG_FUNCTION_ENTRY = 0x01 # Logs the entries to the PCAN-Basic API functions
+LOG_FUNCTION_PARAMETERS = (
+ 0x02 # Logs the parameters passed to the PCAN-Basic API functions
+)
+LOG_FUNCTION_LEAVE = 0x04 # Logs the exits from the PCAN-Basic API functions
+LOG_FUNCTION_WRITE = 0x08 # Logs the CAN messages passed to the CAN_Write function
+LOG_FUNCTION_READ = 0x10 # Logs the CAN messages received within the CAN_Read function
+LOG_FUNCTION_ALL = (
+ 0xFFFF # Logs all possible information within the PCAN-Basic API functions
+)
+
+TRACE_FILE_SINGLE = (
+ 0x00 # A single file is written until it size reaches PAN_TRACE_SIZE
+)
+TRACE_FILE_SEGMENTED = (
+ 0x01 # Traced data is distributed in several files with size PAN_TRACE_SIZE
+)
+TRACE_FILE_DATE = 0x02 # Includes the date into the name of the trace file
+TRACE_FILE_TIME = 0x04 # Includes the start time into the name of the trace file
+TRACE_FILE_OVERWRITE = 0x80 # Causes the overwriting of available traces (same name)
+
+FEATURE_FD_CAPABLE = 0x01 # Device supports flexible data-rate (CAN-FD)
+FEATURE_DELAY_CAPABLE = (
+ 0x02 # Device supports a delay between sending frames (FPGA based USB devices)
+)
+FEATURE_IO_CAPABLE = (
+ 0x04 # Device supports I/O functionality for electronic circuits (USB-Chip devices)
+)
+
+SERVICE_STATUS_STOPPED = 0x01 # The service is not running
+SERVICE_STATUS_RUNNING = 0x04 # The service is running
+
+# Other constants
+#
+MAX_LENGTH_HARDWARE_NAME = (
+ 33 # Maximum length of the name of a device: 32 characters + terminator
+)
+MAX_LENGTH_VERSION_STRING = (
+ 256 # Maximum length of a version string: 255 characters + terminator
+)
# PCAN message types
-PCAN_MESSAGE_STANDARD = TPCANMessageType(0x00) # The PCAN message is a CAN Standard Frame (11-bit identifier)
-PCAN_MESSAGE_RTR = TPCANMessageType(0x01) # The PCAN message is a CAN Remote-Transfer-Request Frame
-PCAN_MESSAGE_EXTENDED = TPCANMessageType(0x02) # The PCAN message is a CAN Extended Frame (29-bit identifier)
-PCAN_MESSAGE_FD = TPCANMessageType(0x04) # The PCAN message represents a FD frame in terms of CiA Specs
-PCAN_MESSAGE_BRS = TPCANMessageType(0x08) # The PCAN message represents a FD bit rate switch (CAN data at a higher bit rate)
-PCAN_MESSAGE_ESI = TPCANMessageType(0x10) # The PCAN message represents a FD error state indicator(CAN FD transmitter was error active)
-PCAN_MESSAGE_ERRFRAME = TPCANMessageType(0x40) # The PCAN message represents an error frame
-PCAN_MESSAGE_STATUS = TPCANMessageType(0x80) # The PCAN message represents a PCAN status message
+#
+PCAN_MESSAGE_STANDARD = TPCANMessageType(
+ 0x00
+) # The PCAN message is a CAN Standard Frame (11-bit identifier)
+PCAN_MESSAGE_RTR = TPCANMessageType(
+ 0x01
+) # The PCAN message is a CAN Remote-Transfer-Request Frame
+PCAN_MESSAGE_EXTENDED = TPCANMessageType(
+ 0x02
+) # The PCAN message is a CAN Extended Frame (29-bit identifier)
+PCAN_MESSAGE_FD = TPCANMessageType(
+ 0x04
+) # The PCAN message represents a FD frame in terms of CiA Specs
+PCAN_MESSAGE_BRS = TPCANMessageType(
+ 0x08
+) # The PCAN message represents a FD bit rate switch (CAN data at a higher bit rate)
+PCAN_MESSAGE_ESI = TPCANMessageType(
+ 0x10
+) # The PCAN message represents a FD error state indicator(CAN FD transmitter was error active)
+PCAN_MESSAGE_ECHO = TPCANMessageType(
+ 0x20
+) # The PCAN message represents an echo CAN Frame
+PCAN_MESSAGE_ERRFRAME = TPCANMessageType(
+ 0x40
+) # The PCAN message represents an error frame
+PCAN_MESSAGE_STATUS = TPCANMessageType(
+ 0x80
+) # The PCAN message represents a PCAN status message
+
+# LookUp Parameters
+#
+LOOKUP_DEVICE_TYPE = (
+ b"devicetype" # Lookup channel by Device type (see PCAN devices e.g. PCAN_USB)
+)
+LOOKUP_DEVICE_ID = b"deviceid" # Lookup channel by device id
+LOOKUP_CONTROLLER_NUMBER = (
+ b"controllernumber" # Lookup channel by CAN controller 0-based index
+)
+LOOKUP_IP_ADDRESS = b"ipaddress" # Lookup channel by IP address (LAN channels only)
# Frame Type / Initialization Mode
-PCAN_MODE_STANDARD = PCAN_MESSAGE_STANDARD
-PCAN_MODE_EXTENDED = PCAN_MESSAGE_EXTENDED
+#
+PCAN_MODE_STANDARD = PCAN_MESSAGE_STANDARD
+PCAN_MODE_EXTENDED = PCAN_MESSAGE_EXTENDED
# Baud rate codes = BTR0/BTR1 register values for the CAN controller.
# You can define your own Baud rate with the BTROBTR1 register.
# Take a look at www.peak-system.com for our free software "BAUDTOOL"
# to calculate the BTROBTR1 register for every bit rate and sample point.
-
-PCAN_BAUD_1M = TPCANBaudrate(0x0014) # 1 MBit/s
-PCAN_BAUD_800K = TPCANBaudrate(0x0016) # 800 kBit/s
-PCAN_BAUD_500K = TPCANBaudrate(0x001C) # 500 kBit/s
-PCAN_BAUD_250K = TPCANBaudrate(0x011C) # 250 kBit/s
-PCAN_BAUD_125K = TPCANBaudrate(0x031C) # 125 kBit/s
-PCAN_BAUD_100K = TPCANBaudrate(0x432F) # 100 kBit/s
-PCAN_BAUD_95K = TPCANBaudrate(0xC34E) # 95,238 kBit/s
-PCAN_BAUD_83K = TPCANBaudrate(0x852B) # 83,333 kBit/s
-PCAN_BAUD_50K = TPCANBaudrate(0x472F) # 50 kBit/s
-PCAN_BAUD_47K = TPCANBaudrate(0x1414) # 47,619 kBit/s
-PCAN_BAUD_33K = TPCANBaudrate(0x8B2F) # 33,333 kBit/s
-PCAN_BAUD_20K = TPCANBaudrate(0x532F) # 20 kBit/s
-PCAN_BAUD_10K = TPCANBaudrate(0x672F) # 10 kBit/s
-PCAN_BAUD_5K = TPCANBaudrate(0x7F7F) # 5 kBit/s
+#
+PCAN_BAUD_1M = TPCANBaudrate(0x0014) # 1 MBit/s
+PCAN_BAUD_800K = TPCANBaudrate(0x0016) # 800 kBit/s
+PCAN_BAUD_500K = TPCANBaudrate(0x001C) # 500 kBit/s
+PCAN_BAUD_250K = TPCANBaudrate(0x011C) # 250 kBit/s
+PCAN_BAUD_125K = TPCANBaudrate(0x031C) # 125 kBit/s
+PCAN_BAUD_100K = TPCANBaudrate(0x432F) # 100 kBit/s
+PCAN_BAUD_95K = TPCANBaudrate(0xC34E) # 95,238 kBit/s
+PCAN_BAUD_83K = TPCANBaudrate(0x852B) # 83,333 kBit/s
+PCAN_BAUD_50K = TPCANBaudrate(0x472F) # 50 kBit/s
+PCAN_BAUD_47K = TPCANBaudrate(0x1414) # 47,619 kBit/s
+PCAN_BAUD_33K = TPCANBaudrate(0x8B2F) # 33,333 kBit/s
+PCAN_BAUD_20K = TPCANBaudrate(0x532F) # 20 kBit/s
+PCAN_BAUD_10K = TPCANBaudrate(0x672F) # 10 kBit/s
+PCAN_BAUD_5K = TPCANBaudrate(0x7F7F) # 5 kBit/s
# Represents the configuration for a CAN bit rate
# Note:
@@ -265,124 +426,315 @@
# Example:
# f_clock=80000000,nom_brp=10,nom_tseg1=5,nom_tseg2=2,nom_sjw=1,data_brp=4,data_tseg1=7,data_tseg2=2,data_sjw=1
#
-PCAN_BR_CLOCK = TPCANBitrateFD(b"f_clock")
-PCAN_BR_CLOCK_MHZ = TPCANBitrateFD(b"f_clock_mhz")
-PCAN_BR_NOM_BRP = TPCANBitrateFD(b"nom_brp")
-PCAN_BR_NOM_TSEG1 = TPCANBitrateFD(b"nom_tseg1")
-PCAN_BR_NOM_TSEG2 = TPCANBitrateFD(b"nom_tseg2")
-PCAN_BR_NOM_SJW = TPCANBitrateFD(b"nom_sjw")
-PCAN_BR_NOM_SAMPLE = TPCANBitrateFD(b"nom_sam")
-PCAN_BR_DATA_BRP = TPCANBitrateFD(b"data_brp")
-PCAN_BR_DATA_TSEG1 = TPCANBitrateFD(b"data_tseg1")
-PCAN_BR_DATA_TSEG2 = TPCANBitrateFD(b"data_tseg2")
-PCAN_BR_DATA_SJW = TPCANBitrateFD(b"data_sjw")
-PCAN_BR_DATA_SAMPLE = TPCANBitrateFD(b"data_ssp_offset")
-
-# Supported No-Plug-And-Play Hardware types
-PCAN_TYPE_ISA = TPCANType(0x01) # PCAN-ISA 82C200
-PCAN_TYPE_ISA_SJA = TPCANType(0x09) # PCAN-ISA SJA1000
-PCAN_TYPE_ISA_PHYTEC = TPCANType(0x04) # PHYTEC ISA
-PCAN_TYPE_DNG = TPCANType(0x02) # PCAN-Dongle 82C200
-PCAN_TYPE_DNG_EPP = TPCANType(0x03) # PCAN-Dongle EPP 82C200
-PCAN_TYPE_DNG_SJA = TPCANType(0x05) # PCAN-Dongle SJA1000
-PCAN_TYPE_DNG_SJA_EPP = TPCANType(0x06) # PCAN-Dongle EPP SJA1000
-
-
-class TPCANMsg (Structure):
+PCAN_BR_CLOCK = TPCANBitrateFD(b"f_clock")
+PCAN_BR_CLOCK_MHZ = TPCANBitrateFD(b"f_clock_mhz")
+PCAN_BR_NOM_BRP = TPCANBitrateFD(b"nom_brp")
+PCAN_BR_NOM_TSEG1 = TPCANBitrateFD(b"nom_tseg1")
+PCAN_BR_NOM_TSEG2 = TPCANBitrateFD(b"nom_tseg2")
+PCAN_BR_NOM_SJW = TPCANBitrateFD(b"nom_sjw")
+PCAN_BR_NOM_SAMPLE = TPCANBitrateFD(b"nom_sam")
+PCAN_BR_DATA_BRP = TPCANBitrateFD(b"data_brp")
+PCAN_BR_DATA_TSEG1 = TPCANBitrateFD(b"data_tseg1")
+PCAN_BR_DATA_TSEG2 = TPCANBitrateFD(b"data_tseg2")
+PCAN_BR_DATA_SJW = TPCANBitrateFD(b"data_sjw")
+PCAN_BR_DATA_SAMPLE = TPCANBitrateFD(b"data_ssp_offset")
+
+# Supported Non-PnP Hardware types
+#
+PCAN_TYPE_ISA = TPCANType(0x01) # PCAN-ISA 82C200
+PCAN_TYPE_ISA_SJA = TPCANType(0x09) # PCAN-ISA SJA1000
+PCAN_TYPE_ISA_PHYTEC = TPCANType(0x04) # PHYTEC ISA
+PCAN_TYPE_DNG = TPCANType(0x02) # PCAN-Dongle 82C200
+PCAN_TYPE_DNG_EPP = TPCANType(0x03) # PCAN-Dongle EPP 82C200
+PCAN_TYPE_DNG_SJA = TPCANType(0x05) # PCAN-Dongle SJA1000
+PCAN_TYPE_DNG_SJA_EPP = TPCANType(0x06) # PCAN-Dongle EPP SJA1000
+
+# string description of the error codes
+PCAN_DICT_STATUS = {
+ PCAN_ERROR_OK: "OK",
+ PCAN_ERROR_XMTFULL: "XMTFULL",
+ PCAN_ERROR_OVERRUN: "OVERRUN",
+ PCAN_ERROR_BUSLIGHT: "BUSLIGHT",
+ PCAN_ERROR_BUSHEAVY: "BUSHEAVY",
+ PCAN_ERROR_BUSWARNING: "BUSWARNING",
+ PCAN_ERROR_BUSPASSIVE: "BUSPASSIVE",
+ PCAN_ERROR_BUSOFF: "BUSOFF",
+ PCAN_ERROR_ANYBUSERR: "ANYBUSERR",
+ PCAN_ERROR_QRCVEMPTY: "QRCVEMPTY",
+ PCAN_ERROR_QOVERRUN: "QOVERRUN",
+ PCAN_ERROR_QXMTFULL: "QXMTFULL",
+ PCAN_ERROR_REGTEST: "ERR_REGTEST",
+ PCAN_ERROR_NODRIVER: "NODRIVER",
+ PCAN_ERROR_HWINUSE: "HWINUSE",
+ PCAN_ERROR_NETINUSE: "NETINUSE",
+ PCAN_ERROR_ILLHW: "ILLHW",
+ PCAN_ERROR_ILLNET: "ILLNET",
+ PCAN_ERROR_ILLCLIENT: "ILLCLIENT",
+ PCAN_ERROR_ILLHANDLE: "ILLHANDLE",
+ PCAN_ERROR_RESOURCE: "ERR_RESOURCE",
+ PCAN_ERROR_ILLPARAMTYPE: "ILLPARAMTYPE",
+ PCAN_ERROR_ILLPARAMVAL: "ILLPARAMVAL",
+ PCAN_ERROR_UNKNOWN: "UNKNOWN",
+ PCAN_ERROR_ILLDATA: "ILLDATA",
+ PCAN_ERROR_CAUTION: "CAUTION",
+ PCAN_ERROR_INITIALIZE: "ERR_INITIALIZE",
+ PCAN_ERROR_ILLOPERATION: "ILLOPERATION",
+}
+
+
+# Represents a PCAN message
+#
+class TPCANMsg(Structure):
"""
Represents a PCAN message
"""
- _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS
- ("MSGTYPE", TPCANMessageType), # Type of the message
- ("LEN", c_ubyte), # Data Length Code of the message (0..8)
- ("DATA", c_ubyte * 8) ] # Data of the message (DATA[0]..DATA[7])
+ _fields_ = [
+ ("ID", c_uint), # 11/29-bit message identifier
+ ("MSGTYPE", TPCANMessageType), # Type of the message
+ ("LEN", c_ubyte), # Data Length Code of the message (0..8)
+ ("DATA", c_ubyte * 8),
+ ] # Data of the message (DATA[0]..DATA[7])
-class TPCANTimestamp (Structure):
+
+# Represents a timestamp of a received PCAN message
+# Total Microseconds = micros + 1000 * millis + 0x100000000 * 1000 * millis_overflow
+#
+class TPCANTimestamp(Structure):
"""
Represents a timestamp of a received PCAN message
Total Microseconds = micros + 1000 * millis + 0x100000000 * 1000 * millis_overflow
"""
- _fields_ = [ ("millis", c_ulong), # Base-value: milliseconds: 0.. 2^32-1 - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS
- ("millis_overflow", c_ushort), # Roll-arounds of millis
- ("micros", c_ushort) ] # Microseconds: 0..999
+ _fields_ = [
+ ("millis", c_uint), # Base-value: milliseconds: 0.. 2^32-1
+ ("millis_overflow", c_ushort), # Roll-arounds of millis
+ ("micros", c_ushort),
+ ] # Microseconds: 0..999
-class TPCANMsgFD (Structure):
+
+# Represents a PCAN message from a FD capable hardware
+#
+class TPCANMsgFD(Structure):
"""
Represents a PCAN message
"""
- _fields_ = [ ("ID", c_ulong), # 11/29-bit message identifier - was changed from u_uint to c_ulong, so it is compatible with the PCAN-USB Driver for macOS
- ("MSGTYPE", TPCANMessageType), # Type of the message
- ("DLC", c_ubyte), # Data Length Code of the message (0..15)
- ("DATA", c_ubyte * 64) ] # Data of the message (DATA[0]..DATA[63])
-#///////////////////////////////////////////////////////////
+ _fields_ = [
+ ("ID", c_uint), # 11/29-bit message identifier
+ ("MSGTYPE", TPCANMessageType), # Type of the message
+ ("DLC", c_ubyte), # Data Length Code of the message (0..15)
+ ("DATA", c_ubyte * 64),
+ ] # Data of the message (DATA[0]..DATA[63])
+
+
+# Describes an available PCAN channel
+#
+class TPCANChannelInformation(Structure):
+ """
+ Describes an available PCAN channel
+ """
+
+ _fields_ = [
+ ("channel_handle", TPCANHandle), # PCAN channel handle
+ ("device_type", TPCANDevice), # Kind of PCAN device
+ ("controller_number", c_ubyte), # CAN-Controller number
+ ("device_features", c_uint), # Device capabilities flag (see FEATURE_*)
+ ("device_name", c_char * MAX_LENGTH_HARDWARE_NAME), # Device name
+ ("device_id", c_uint), # Device number
+ ("channel_condition", c_uint),
+ ] # Availability status of a PCAN-Channel
+
+
+# ///////////////////////////////////////////////////////////
+# Additional objects
+# ///////////////////////////////////////////////////////////
+
+PCAN_BITRATES = {
+ 1000000: PCAN_BAUD_1M,
+ 800000: PCAN_BAUD_800K,
+ 500000: PCAN_BAUD_500K,
+ 250000: PCAN_BAUD_250K,
+ 125000: PCAN_BAUD_125K,
+ 100000: PCAN_BAUD_100K,
+ 95000: PCAN_BAUD_95K,
+ 83000: PCAN_BAUD_83K,
+ 50000: PCAN_BAUD_50K,
+ 47000: PCAN_BAUD_47K,
+ 33000: PCAN_BAUD_33K,
+ 20000: PCAN_BAUD_20K,
+ 10000: PCAN_BAUD_10K,
+ 5000: PCAN_BAUD_5K,
+}
+
+PCAN_FD_PARAMETER_LIST = (
+ "nom_brp",
+ "nom_tseg1",
+ "nom_tseg2",
+ "nom_sjw",
+ "data_brp",
+ "data_tseg1",
+ "data_tseg2",
+ "data_sjw",
+)
+
+PCAN_CHANNEL_NAMES = {
+ "PCAN_NONEBUS": PCAN_NONEBUS,
+ "PCAN_ISABUS1": PCAN_ISABUS1,
+ "PCAN_ISABUS2": PCAN_ISABUS2,
+ "PCAN_ISABUS3": PCAN_ISABUS3,
+ "PCAN_ISABUS4": PCAN_ISABUS4,
+ "PCAN_ISABUS5": PCAN_ISABUS5,
+ "PCAN_ISABUS6": PCAN_ISABUS6,
+ "PCAN_ISABUS7": PCAN_ISABUS7,
+ "PCAN_ISABUS8": PCAN_ISABUS8,
+ "PCAN_DNGBUS1": PCAN_DNGBUS1,
+ "PCAN_PCIBUS1": PCAN_PCIBUS1,
+ "PCAN_PCIBUS2": PCAN_PCIBUS2,
+ "PCAN_PCIBUS3": PCAN_PCIBUS3,
+ "PCAN_PCIBUS4": PCAN_PCIBUS4,
+ "PCAN_PCIBUS5": PCAN_PCIBUS5,
+ "PCAN_PCIBUS6": PCAN_PCIBUS6,
+ "PCAN_PCIBUS7": PCAN_PCIBUS7,
+ "PCAN_PCIBUS8": PCAN_PCIBUS8,
+ "PCAN_PCIBUS9": PCAN_PCIBUS9,
+ "PCAN_PCIBUS10": PCAN_PCIBUS10,
+ "PCAN_PCIBUS11": PCAN_PCIBUS11,
+ "PCAN_PCIBUS12": PCAN_PCIBUS12,
+ "PCAN_PCIBUS13": PCAN_PCIBUS13,
+ "PCAN_PCIBUS14": PCAN_PCIBUS14,
+ "PCAN_PCIBUS15": PCAN_PCIBUS15,
+ "PCAN_PCIBUS16": PCAN_PCIBUS16,
+ "PCAN_USBBUS1": PCAN_USBBUS1,
+ "PCAN_USBBUS2": PCAN_USBBUS2,
+ "PCAN_USBBUS3": PCAN_USBBUS3,
+ "PCAN_USBBUS4": PCAN_USBBUS4,
+ "PCAN_USBBUS5": PCAN_USBBUS5,
+ "PCAN_USBBUS6": PCAN_USBBUS6,
+ "PCAN_USBBUS7": PCAN_USBBUS7,
+ "PCAN_USBBUS8": PCAN_USBBUS8,
+ "PCAN_USBBUS9": PCAN_USBBUS9,
+ "PCAN_USBBUS10": PCAN_USBBUS10,
+ "PCAN_USBBUS11": PCAN_USBBUS11,
+ "PCAN_USBBUS12": PCAN_USBBUS12,
+ "PCAN_USBBUS13": PCAN_USBBUS13,
+ "PCAN_USBBUS14": PCAN_USBBUS14,
+ "PCAN_USBBUS15": PCAN_USBBUS15,
+ "PCAN_USBBUS16": PCAN_USBBUS16,
+ "PCAN_PCCBUS1": PCAN_PCCBUS1,
+ "PCAN_PCCBUS2": PCAN_PCCBUS2,
+ "PCAN_LANBUS1": PCAN_LANBUS1,
+ "PCAN_LANBUS2": PCAN_LANBUS2,
+ "PCAN_LANBUS3": PCAN_LANBUS3,
+ "PCAN_LANBUS4": PCAN_LANBUS4,
+ "PCAN_LANBUS5": PCAN_LANBUS5,
+ "PCAN_LANBUS6": PCAN_LANBUS6,
+ "PCAN_LANBUS7": PCAN_LANBUS7,
+ "PCAN_LANBUS8": PCAN_LANBUS8,
+ "PCAN_LANBUS9": PCAN_LANBUS9,
+ "PCAN_LANBUS10": PCAN_LANBUS10,
+ "PCAN_LANBUS11": PCAN_LANBUS11,
+ "PCAN_LANBUS12": PCAN_LANBUS12,
+ "PCAN_LANBUS13": PCAN_LANBUS13,
+ "PCAN_LANBUS14": PCAN_LANBUS14,
+ "PCAN_LANBUS15": PCAN_LANBUS15,
+ "PCAN_LANBUS16": PCAN_LANBUS16,
+}
+
+VALID_PCAN_CAN_CLOCKS = [8_000_000]
+
+VALID_PCAN_FD_CLOCKS = [
+ 20_000_000,
+ 24_000_000,
+ 30_000_000,
+ 40_000_000,
+ 60_000_000,
+ 80_000_000,
+]
+
+# ///////////////////////////////////////////////////////////
# PCAN-Basic API function declarations
-#///////////////////////////////////////////////////////////
+# ///////////////////////////////////////////////////////////
+
+# PCAN-Basic API class implementation
+#
class PCANBasic:
- """PCAN-Basic API class implementation
- """
+ """PCAN-Basic API class implementation"""
def __init__(self):
- # Loads the PCANBasic.dll
- if platform.system() == 'Windows':
- self.__m_dllBasic = windll.LoadLibrary("PCANBasic")
- elif platform.system() == 'Darwin':
- self.__m_dllBasic = cdll.LoadLibrary('libPCBUSB.dylib')
+ if platform.system() == "Windows":
+ load_library_func = windll.LoadLibrary
+ else:
+ load_library_func = cdll.LoadLibrary
+
+ if platform.system() == "Windows" or "CYGWIN" in platform.system():
+ lib_name = "PCANBasic"
+ elif platform.system() == "Darwin":
+ # PCBUSB library is a third-party software created
+ # and maintained by the MacCAN project
+ lib_name = "PCBUSB"
else:
- self.__m_dllBasic = cdll.LoadLibrary("libpcanbasic.so")
- if self.__m_dllBasic == None:
- logger.error("Exception: The PCAN-Basic DLL couldn't be loaded!")
+ lib_name = "pcanbasic"
+ lib_path = find_library(lib_name)
+ if not lib_path:
+ raise OSError(f"{lib_name} library not found.")
+
+ try:
+ self.__m_dllBasic = load_library_func(lib_path)
+ except OSError:
+ raise OSError(
+ f"The PCAN-Basic API could not be loaded. ({lib_path})"
+ ) from None
+
+ # Initializes a PCAN Channel
+ #
def Initialize(
self,
Channel,
Btr0Btr1,
- HwType = TPCANType(0),
- IOPort = c_uint(0),
- Interrupt = c_ushort(0)):
-
- """
- Initializes a PCAN Channel
+ HwType=TPCANType(0), # noqa: B008
+ IOPort=c_uint(0), # noqa: B008
+ Interrupt=c_ushort(0), # noqa: B008
+ ):
+ """Initializes a PCAN Channel
Parameters:
Channel : A TPCANHandle representing a PCAN Channel
Btr0Btr1 : The speed for the communication (BTR0BTR1 code)
- HwType : NON PLUG&PLAY: The type of hardware and operation mode
- IOPort : NON PLUG&PLAY: The I/O address for the parallel port
- Interrupt: NON PLUG&PLAY: Interrupt number of the parallel port
+ HwType : Non-PnP: The type of hardware and operation mode
+ IOPort : Non-PnP: The I/O address for the parallel port
+ Interrupt: Non-PnP: Interrupt number of the parallel port
Returns:
A TPCANStatus error code
"""
try:
- res = self.__m_dllBasic.CAN_Initialize(Channel,Btr0Btr1,HwType,IOPort,Interrupt)
+ res = self.__m_dllBasic.CAN_Initialize(
+ Channel, Btr0Btr1, HwType, IOPort, Interrupt
+ )
return TPCANStatus(res)
except:
logger.error("Exception on PCANBasic.Initialize")
raise
- def InitializeFD(
- self,
- Channel,
- BitrateFD):
-
- """
- Initializes a FD capable PCAN Channel
+ # Initializes a FD capable PCAN Channel
+ #
+ def InitializeFD(self, Channel, BitrateFD):
+ """Initializes a FD capable PCAN Channel
Parameters:
Channel : The handle of a FD capable PCAN Channel
BitrateFD : The speed for the communication (FD bit rate string)
- Remarks:
- See PCAN_BR_* values.
+ Remarks:
+ * See PCAN_BR_* values.
* parameter and values must be separated by '='
* Couples of Parameter/value must be separated by ','
* Following Parameter must be filled out: f_clock, data_brp, data_sjw, data_tseg1, data_tseg2,
nom_brp, nom_sjw, nom_tseg1, nom_tseg2.
- * Following Parameters are optional (not used yet): data_ssp_offset, nom_samp
+ * Following Parameters are optional (not used yet): data_ssp_offset, nom_sam
Example:
f_clock=80000000,nom_brp=10,nom_tseg1=5,nom_tseg2=2,nom_sjw=1,data_brp=4,data_tseg1=7,data_tseg2=2,data_sjw=1
@@ -391,18 +743,16 @@ def InitializeFD(
A TPCANStatus error code
"""
try:
- res = self.__m_dllBasic.CAN_InitializeFD(Channel,BitrateFD)
+ res = self.__m_dllBasic.CAN_InitializeFD(Channel, BitrateFD)
return TPCANStatus(res)
except:
logger.error("Exception on PCANBasic.InitializeFD")
raise
- def Uninitialize(
- self,
- Channel):
-
- """
- Uninitializes one or all PCAN Channels initialized by CAN_Initialize
+ # Uninitializes one or all PCAN Channels initialized by CAN_Initialize
+ #
+ def Uninitialize(self, Channel):
+ """Uninitializes one or all PCAN Channels initialized by CAN_Initialize
Remarks:
Giving the TPCANHandle value "PCAN_NONEBUS", uninitialize all initialized channels
@@ -420,12 +770,10 @@ def Uninitialize(
logger.error("Exception on PCANBasic.Uninitialize")
raise
- def Reset(
- self,
- Channel):
-
- """
- Resets the receive and transmit queues of the PCAN Channel
+ # Resets the receive and transmit queues of the PCAN Channel
+ #
+ def Reset(self, Channel):
+ """Resets the receive and transmit queues of the PCAN Channel
Remarks:
A reset of the CAN controller is not performed
@@ -443,12 +791,10 @@ def Reset(
logger.error("Exception on PCANBasic.Reset")
raise
- def GetStatus(
- self,
- Channel):
-
- """
- Gets the current status of a PCAN Channel
+ # Gets the current status of a PCAN Channel
+ #
+ def GetStatus(self, Channel):
+ """Gets the current status of a PCAN Channel
Parameters:
Channel : A TPCANHandle representing a PCAN Channel
@@ -463,15 +809,13 @@ def GetStatus(
logger.error("Exception on PCANBasic.GetStatus")
raise
- def Read(
- self,
- Channel):
-
- """
- Reads a CAN message from the receive queue of a PCAN Channel
+ # Reads a CAN message from the receive queue of a PCAN Channel
+ #
+ def Read(self, Channel):
+ """Reads a CAN message from the receive queue of a PCAN Channel
Remarks:
- The return value of this method is a 3-touple, where
+ The return value of this method is a 3-tuple, where
the first value is the result (TPCANStatus) of the method.
The order of the values are:
[0]: A TPCANStatus error code
@@ -482,26 +826,24 @@ def Read(
Channel : A TPCANHandle representing a PCAN Channel
Returns:
- A touple with three values
+ A tuple with three values
"""
try:
msg = TPCANMsg()
timestamp = TPCANTimestamp()
- res = self.__m_dllBasic.CAN_Read(Channel,byref(msg),byref(timestamp))
- return TPCANStatus(res),msg,timestamp
+ res = self.__m_dllBasic.CAN_Read(Channel, byref(msg), byref(timestamp))
+ return TPCANStatus(res), msg, timestamp
except:
logger.error("Exception on PCANBasic.Read")
raise
- def ReadFD(
- self,
- Channel):
-
- """
- Reads a CAN message from the receive queue of a FD capable PCAN Channel
+ # Reads a CAN message from the receive queue of a FD capable PCAN Channel
+ #
+ def ReadFD(self, Channel):
+ """Reads a CAN message from the receive queue of a FD capable PCAN Channel
Remarks:
- The return value of this method is a 3-touple, where
+ The return value of this method is a 3-tuple, where
the first value is the result (TPCANStatus) of the method.
The order of the values are:
[0]: A TPCANStatus error code
@@ -512,24 +854,21 @@ def ReadFD(
Channel : The handle of a FD capable PCAN Channel
Returns:
- A touple with three values
+ A tuple with three values
"""
try:
msg = TPCANMsgFD()
timestamp = TPCANTimestampFD()
- res = self.__m_dllBasic.CAN_ReadFD(Channel,byref(msg),byref(timestamp))
- return TPCANStatus(res),msg,timestamp
+ res = self.__m_dllBasic.CAN_ReadFD(Channel, byref(msg), byref(timestamp))
+ return TPCANStatus(res), msg, timestamp
except:
logger.error("Exception on PCANBasic.ReadFD")
raise
- def Write(
- self,
- Channel,
- MessageBuffer):
-
- """
- Transmits a CAN message
+ # Transmits a CAN message
+ #
+ def Write(self, Channel, MessageBuffer):
+ """Transmits a CAN message
Parameters:
Channel : A TPCANHandle representing a PCAN Channel
@@ -539,19 +878,16 @@ def Write(
A TPCANStatus error code
"""
try:
- res = self.__m_dllBasic.CAN_Write(Channel,byref(MessageBuffer))
+ res = self.__m_dllBasic.CAN_Write(Channel, byref(MessageBuffer))
return TPCANStatus(res)
except:
logger.error("Exception on PCANBasic.Write")
raise
- def WriteFD(
- self,
- Channel,
- MessageBuffer):
-
- """
- Transmits a CAN message over a FD capable PCAN Channel
+ # Transmits a CAN message over a FD capable PCAN Channel
+ #
+ def WriteFD(self, Channel, MessageBuffer):
+ """Transmits a CAN message over a FD capable PCAN Channel
Parameters:
Channel : The handle of a FD capable PCAN Channel
@@ -561,21 +897,16 @@ def WriteFD(
A TPCANStatus error code
"""
try:
- res = self.__m_dllBasic.CAN_WriteFD(Channel,byref(MessageBuffer))
+ res = self.__m_dllBasic.CAN_WriteFD(Channel, byref(MessageBuffer))
return TPCANStatus(res)
except:
logger.error("Exception on PCANBasic.WriteFD")
raise
- def FilterMessages(
- self,
- Channel,
- FromID,
- ToID,
- Mode):
-
- """
- Configures the reception filter
+ # Configures the reception filter
+ #
+ def FilterMessages(self, Channel, FromID, ToID, Mode):
+ """Configures the reception filter
Remarks:
The message filter will be expanded with every call to this function.
@@ -592,26 +923,23 @@ def FilterMessages(
A TPCANStatus error code
"""
try:
- res = self.__m_dllBasic.CAN_FilterMessages(Channel,FromID,ToID,Mode)
+ res = self.__m_dllBasic.CAN_FilterMessages(Channel, FromID, ToID, Mode)
return TPCANStatus(res)
except:
logger.error("Exception on PCANBasic.FilterMessages")
raise
- def GetValue(
- self,
- Channel,
- Parameter):
-
- """
- Retrieves a PCAN Channel value
+ # Retrieves a PCAN Channel value
+ #
+ def GetValue(self, Channel, Parameter):
+ """Retrieves a PCAN Channel value
Remarks:
Parameters can be present or not according with the kind
of Hardware (PCAN Channel) being used. If a parameter is not available,
a PCAN_ERROR_ILLPARAMTYPE error will be returned.
- The return value of this method is a 2-touple, where
+ The return value of this method is a 2-tuple, where
the first value is the result (TPCANStatus) of the method and
the second one, the asked value
@@ -620,29 +948,54 @@ def GetValue(
Parameter : The TPCANParameter parameter to get
Returns:
- A touple with 2 values
+ A tuple with 2 values
"""
try:
- if Parameter == PCAN_API_VERSION or Parameter == PCAN_HARDWARE_NAME or Parameter == PCAN_CHANNEL_VERSION or Parameter == PCAN_LOG_LOCATION or Parameter == PCAN_TRACE_LOCATION or Parameter == PCAN_BITRATE_INFO_FD or Parameter == PCAN_IP_ADDRESS:
+ if (
+ Parameter == PCAN_API_VERSION
+ or Parameter == PCAN_HARDWARE_NAME
+ or Parameter == PCAN_CHANNEL_VERSION
+ or Parameter == PCAN_LOG_LOCATION
+ or Parameter == PCAN_TRACE_LOCATION
+ or Parameter == PCAN_BITRATE_INFO_FD
+ or Parameter == PCAN_IP_ADDRESS
+ or Parameter == PCAN_FIRMWARE_VERSION
+ or Parameter == PCAN_DEVICE_PART_NUMBER
+ ):
mybuffer = create_string_buffer(256)
+
+ elif Parameter == PCAN_ATTACHED_CHANNELS:
+ res = self.GetValue(Channel, PCAN_ATTACHED_CHANNELS_COUNT)
+ if TPCANStatus(res[0]) != PCAN_ERROR_OK:
+ return TPCANStatus(res[0]), ()
+ mybuffer = (TPCANChannelInformation * res[1])()
+
+ elif (
+ Parameter == PCAN_ACCEPTANCE_FILTER_11BIT
+ or PCAN_ACCEPTANCE_FILTER_29BIT
+ ):
+ mybuffer = c_int64(0)
+
else:
mybuffer = c_int(0)
- res = self.__m_dllBasic.CAN_GetValue(Channel,Parameter,byref(mybuffer),sizeof(mybuffer))
- return TPCANStatus(res),mybuffer.value
+ res = self.__m_dllBasic.CAN_GetValue(
+ Channel, Parameter, byref(mybuffer), sizeof(mybuffer)
+ )
+ if Parameter == PCAN_ATTACHED_CHANNELS:
+ return TPCANStatus(res), mybuffer
+ else:
+ return TPCANStatus(res), mybuffer.value
except:
logger.error("Exception on PCANBasic.GetValue")
raise
- def SetValue(
- self,
- Channel,
- Parameter,
- Buffer):
-
- """
- Returns a descriptive text of a given TPCANStatus error
- code, in any desired language
+ # Returns a descriptive text of a given TPCANStatus
+ # error code, in any desired language
+ #
+ def SetValue(self, Channel, Parameter, Buffer):
+ """Returns a descriptive text of a given TPCANStatus error
+ code, in any desired language
Remarks:
Parameters can be present or not according with the kind
@@ -659,25 +1012,31 @@ def SetValue(
A TPCANStatus error code
"""
try:
- if Parameter == PCAN_LOG_LOCATION or Parameter == PCAN_LOG_TEXT or Parameter == PCAN_TRACE_LOCATION:
+ if (
+ Parameter == PCAN_LOG_LOCATION
+ or Parameter == PCAN_LOG_TEXT
+ or Parameter == PCAN_TRACE_LOCATION
+ ):
mybuffer = create_string_buffer(256)
+ elif (
+ Parameter == PCAN_ACCEPTANCE_FILTER_11BIT
+ or PCAN_ACCEPTANCE_FILTER_29BIT
+ ):
+ mybuffer = c_int64(0)
else:
mybuffer = c_int(0)
mybuffer.value = Buffer
- res = self.__m_dllBasic.CAN_SetValue(Channel,Parameter,byref(mybuffer),sizeof(mybuffer))
+ res = self.__m_dllBasic.CAN_SetValue(
+ Channel, Parameter, byref(mybuffer), sizeof(mybuffer)
+ )
return TPCANStatus(res)
except:
logger.error("Exception on PCANBasic.SetValue")
raise
- def GetErrorText(
- self,
- Error,
- Language = 0):
-
- """
- Configures or sets a PCAN Channel value
+ def GetErrorText(self, Error, Language=0):
+ """Configures or sets a PCAN Channel value
Remarks:
@@ -685,7 +1044,7 @@ def GetErrorText(
Neutral (0x00), German (0x07), English (0x09), Spanish (0x0A),
Italian (0x10) and French (0x0C)
- The return value of this method is a 2-touple, where
+ The return value of this method is a 2-tuple, where
the first value is the result (TPCANStatus) of the method and
the second one, the error text
@@ -694,12 +1053,36 @@ def GetErrorText(
Language : Indicates a 'Primary language ID' (Default is Neutral(0))
Returns:
- A touple with 2 values
+ A tuple with 2 values
"""
try:
mybuffer = create_string_buffer(256)
- res = self.__m_dllBasic.CAN_GetErrorText(Error,Language,byref(mybuffer))
- return TPCANStatus(res),mybuffer.value
+ res = self.__m_dllBasic.CAN_GetErrorText(Error, Language, byref(mybuffer))
+ return TPCANStatus(res), mybuffer.value
except:
logger.error("Exception on PCANBasic.GetErrorText")
raise
+
+ def LookUpChannel(self, Parameters):
+ """Finds a PCAN-Basic channel that matches with the given parameters
+
+ Remarks:
+
+ The return value of this method is a 2-tuple, where
+ the first value is the result (TPCANStatus) of the method and
+ the second one a TPCANHandle value
+
+ Parameters:
+ Parameters : A comma separated string contained pairs of parameter-name/value
+ to be matched within a PCAN-Basic channel
+
+ Returns:
+ A tuple with 2 values
+ """
+ try:
+ mybuffer = TPCANHandle(0)
+ res = self.__m_dllBasic.CAN_LookUpChannel(Parameters, byref(mybuffer))
+ return TPCANStatus(res), mybuffer
+ except:
+ logger.error("Exception on PCANBasic.LookUpChannel")
+ raise
diff --git a/can/interfaces/pcan/pcan.py b/can/interfaces/pcan/pcan.py
index 05aa533ff..a2f5f361f 100644
--- a/can/interfaces/pcan/pcan.py
+++ b/can/interfaces/pcan/pcan.py
@@ -1,121 +1,387 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Enable basic CAN over a PCAN USB device.
"""
-from __future__ import absolute_import, print_function, division
-
import logging
-import sys
+import platform
import time
+import warnings
+from typing import Any
+
+from packaging import version
+
+from can import (
+ BitTiming,
+ BitTimingFd,
+ BusABC,
+ BusState,
+ CanError,
+ CanInitializationError,
+ CanOperationError,
+ CanProtocol,
+ Message,
+)
+from can.util import check_or_adjust_timing_clock, dlc2len, len2dlc
+
+from .basic import (
+ FEATURE_FD_CAPABLE,
+ IS_LINUX,
+ IS_WINDOWS,
+ PCAN_ALLOW_ECHO_FRAMES,
+ PCAN_ALLOW_ERROR_FRAMES,
+ PCAN_API_VERSION,
+ PCAN_ATTACHED_CHANNELS,
+ PCAN_BAUD_500K,
+ PCAN_BITRATES,
+ PCAN_BUSOFF_AUTORESET,
+ PCAN_CHANNEL_AVAILABLE,
+ PCAN_CHANNEL_CONDITION,
+ PCAN_CHANNEL_FEATURES,
+ PCAN_CHANNEL_IDENTIFYING,
+ PCAN_CHANNEL_NAMES,
+ PCAN_DEVICE_NUMBER,
+ PCAN_DICT_STATUS,
+ PCAN_ERROR_BUSHEAVY,
+ PCAN_ERROR_BUSLIGHT,
+ PCAN_ERROR_ILLDATA,
+ PCAN_ERROR_OK,
+ PCAN_ERROR_QRCVEMPTY,
+ PCAN_FD_PARAMETER_LIST,
+ PCAN_LANBUS1,
+ PCAN_LISTEN_ONLY,
+ PCAN_MESSAGE_BRS,
+ PCAN_MESSAGE_ECHO,
+ PCAN_MESSAGE_ERRFRAME,
+ PCAN_MESSAGE_ESI,
+ PCAN_MESSAGE_EXTENDED,
+ PCAN_MESSAGE_FD,
+ PCAN_MESSAGE_RTR,
+ PCAN_MESSAGE_STANDARD,
+ PCAN_NONEBUS,
+ PCAN_PARAMETER_OFF,
+ PCAN_PARAMETER_ON,
+ PCAN_PCCBUS1,
+ PCAN_PCIBUS1,
+ PCAN_RECEIVE_EVENT,
+ PCAN_TYPE_ISA,
+ PCAN_USBBUS1,
+ VALID_PCAN_CAN_CLOCKS,
+ VALID_PCAN_FD_CLOCKS,
+ PCANBasic,
+ TPCANBaudrate,
+ TPCANChannelInformation,
+ TPCANHandle,
+ TPCANMsg,
+ TPCANMsgFD,
+)
+
+# Set up logging
+log = logging.getLogger("can.pcan")
-import can
-from can import CanError, Message, BusABC
-from can.bus import BusState
-from .basic import *
+MIN_PCAN_API_VERSION = version.parse("4.2.0")
-boottimeEpoch = 0
try:
+ # use the "uptime" library if available
import uptime
- import datetime
- boottimeEpoch = (uptime.boottime() - datetime.datetime.utcfromtimestamp(0)).total_seconds()
-except:
- boottimeEpoch = 0
-try:
- # Try builtin Python 3 Windows API
- from _overlapped import CreateEvent
- from _winapi import WaitForSingleObject, WAIT_OBJECT_0, INFINITE
- HAS_EVENTS = True
+ # boottime() and fromtimestamp() are timezone offset, so the difference is not.
+ if uptime.boottime() is None:
+ boottimeEpoch = 0
+ else:
+ boottimeEpoch = uptime.boottime().timestamp()
except ImportError:
+ log.warning(
+ "uptime library not available, timestamps are relative to boot time and not to Epoch UTC",
+ )
+ boottimeEpoch = 0
+
+HAS_EVENTS = False
+
+if IS_WINDOWS:
try:
- # Try pywin32 package
- from win32event import CreateEvent
- from win32event import WaitForSingleObject, WAIT_OBJECT_0, INFINITE
+ # Try builtin Python 3 Windows API
+ from _overlapped import CreateEvent
+ from _winapi import INFINITE, WAIT_OBJECT_0, WaitForSingleObject
+
HAS_EVENTS = True
except ImportError:
- # Use polling instead
- HAS_EVENTS = False
-
-try:
- # new in 3.3
- timeout_clock = time.perf_counter
-except AttributeError:
- # deprecated in 3.3
- timeout_clock = time.clock
-
-# Set up logging
-log = logging.getLogger('can.pcan')
+ pass
+elif IS_LINUX:
+ try:
+ import select
-pcan_bitrate_objs = {1000000 : PCAN_BAUD_1M,
- 800000 : PCAN_BAUD_800K,
- 500000 : PCAN_BAUD_500K,
- 250000 : PCAN_BAUD_250K,
- 125000 : PCAN_BAUD_125K,
- 100000 : PCAN_BAUD_100K,
- 95000 : PCAN_BAUD_95K,
- 83000 : PCAN_BAUD_83K,
- 50000 : PCAN_BAUD_50K,
- 47000 : PCAN_BAUD_47K,
- 33000 : PCAN_BAUD_33K,
- 20000 : PCAN_BAUD_20K,
- 10000 : PCAN_BAUD_10K,
- 5000 : PCAN_BAUD_5K}
+ HAS_EVENTS = True
+ except Exception:
+ pass
class PcanBus(BusABC):
-
- def __init__(self, channel='PCAN_USBBUS1', state=BusState.ACTIVE, bitrate=500000, *args, **kwargs):
+ def __init__(
+ self,
+ channel: str = "PCAN_USBBUS1",
+ device_id: int | None = None,
+ state: BusState = BusState.ACTIVE,
+ timing: BitTiming | BitTimingFd | None = None,
+ bitrate: int = 500000,
+ receive_own_messages: bool = False,
+ **kwargs: Any,
+ ):
"""A PCAN USB interface to CAN.
On top of the usual :class:`~can.Bus` methods provided,
- the PCAN interface includes the :meth:`~can.interface.pcan.PcanBus.flash`
- and :meth:`~can.interface.pcan.PcanBus.status` methods.
+ the PCAN interface includes the :meth:`flash`
+ and :meth:`status` methods.
:param str channel:
- The can interface name. An example would be 'PCAN_USBBUS1'
+ The can interface name. An example would be 'PCAN_USBBUS1'.
+ Alternatively the value can be an int with the numerical value.
Default is 'PCAN_USBBUS1'
+ :param int device_id:
+ Select the PCAN interface based on its ID. The device ID is a 8/32bit
+ value that can be configured for each PCAN device. If you set the
+ device_id parameter, it takes precedence over the channel parameter.
+ The constructor searches all connected interfaces and initializes the
+ first one that matches the parameter value. If no device is found,
+ an exception is raised.
+
:param can.bus.BusState state:
BusState of the channel.
Default is ACTIVE
+ :param timing:
+ An instance of :class:`~can.BitTiming` or :class:`~can.BitTimingFd`
+ to specify the bit timing parameters for the PCAN interface. If this parameter
+ is provided, it takes precedence over all other timing-related parameters.
+ If this parameter is not provided, the bit timing parameters can be specified
+ using the `bitrate` parameter for standard CAN or the `fd`, `f_clock`,
+ `f_clock_mhz`, `nom_brp`, `nom_tseg1`, `nom_tseg2`, `nom_sjw`, `data_brp`,
+ `data_tseg1`, `data_tseg2`, and `data_sjw` parameters for CAN FD.
+ Note that the `f_clock` value of the `timing` instance must be 8_000_000
+ for standard CAN or any of the following values for CAN FD: 20_000_000,
+ 24_000_000, 30_000_000, 40_000_000, 60_000_000, 80_000_000.
+
:param int bitrate:
Bitrate of channel in bit/s.
Default is 500 kbit/s.
-
+ Ignored if using CanFD.
+
+ :param receive_own_messages:
+ Enable self-reception of sent messages.
+
+ :param bool fd:
+ Should the Bus be initialized in CAN-FD mode.
+
+ :param int f_clock:
+ Clock rate in Hz.
+ Any of the following:
+ 20000000, 24000000, 30000000, 40000000, 60000000, 80000000.
+ Ignored if not using CAN-FD.
+ Pass either f_clock or f_clock_mhz.
+
+ :param int f_clock_mhz:
+ Clock rate in MHz.
+ Any of the following:
+ 20, 24, 30, 40, 60, 80.
+ Ignored if not using CAN-FD.
+ Pass either f_clock or f_clock_mhz.
+
+ :param int nom_brp:
+ Clock prescaler for nominal time quantum.
+ In the range (1..1024)
+ Ignored if not using CAN-FD.
+
+ :param int nom_tseg1:
+ Time segment 1 for nominal bit rate,
+ that is, the number of quanta from (but not including)
+ the Sync Segment to the sampling point.
+ In the range (1..256).
+ Ignored if not using CAN-FD.
+
+ :param int nom_tseg2:
+ Time segment 2 for nominal bit rate,
+ that is, the number of quanta from the sampling
+ point to the end of the bit.
+ In the range (1..128).
+ Ignored if not using CAN-FD.
+
+ :param int nom_sjw:
+ Synchronization Jump Width for nominal bit rate.
+ Decides the maximum number of time quanta
+ that the controller can resynchronize every bit.
+ In the range (1..128).
+ Ignored if not using CAN-FD.
+
+ :param int data_brp:
+ Clock prescaler for fast data time quantum.
+ In the range (1..1024)
+ Ignored if not using CAN-FD.
+
+ :param int data_tseg1:
+ Time segment 1 for fast data bit rate,
+ that is, the number of quanta from (but not including)
+ the Sync Segment to the sampling point.
+ In the range (1..32).
+ Ignored if not using CAN-FD.
+
+ :param int data_tseg2:
+ Time segment 2 for fast data bit rate,
+ that is, the number of quanta from the sampling
+ point to the end of the bit.
+ In the range (1..16).
+ Ignored if not using CAN-FD.
+
+ :param int data_sjw:
+ Synchronization Jump Width for fast data bit rate.
+ Decides the maximum number of time quanta
+ that the controller can resynchronize every bit.
+ In the range (1..16).
+ Ignored if not using CAN-FD.
+
+ :param bool auto_reset:
+ Enable automatic recovery in bus off scenario.
+ Resetting the driver takes ~500ms during which
+ it will not be responsive.
"""
- self.channel_info = channel
- pcan_bitrate = pcan_bitrate_objs.get(bitrate, PCAN_BAUD_500K)
+ self.m_objPCANBasic = PCANBasic()
+
+ if device_id is not None:
+ channel = self._find_channel_by_dev_id(device_id)
+
+ if channel is None:
+ err_msg = f"Cannot find a channel with ID {device_id:08x}"
+ raise ValueError(err_msg)
+
+ is_fd = isinstance(timing, BitTimingFd) if timing else kwargs.get("fd", False)
+ self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20
+ self.channel_info = str(channel)
hwtype = PCAN_TYPE_ISA
ioport = 0x02A0
interrupt = 11
- self.m_objPCANBasic = PCANBasic()
- self.m_PcanHandle = globals()[channel]
+ if not isinstance(channel, int):
+ channel = PCAN_CHANNEL_NAMES[channel]
- if state is BusState.ACTIVE or BusState.PASSIVE:
- self._state = state
+ self.m_PcanHandle = channel
+
+ self.check_api_version()
+
+ if state in [BusState.ACTIVE, BusState.PASSIVE]:
+ self.state = state
else:
- raise ArgumentError("BusState must be Active or Passive")
+ raise ValueError("BusState must be Active or Passive")
+
+ if isinstance(timing, BitTiming):
+ timing = check_or_adjust_timing_clock(timing, VALID_PCAN_CAN_CLOCKS)
+ pcan_bitrate = TPCANBaudrate(timing.btr0 << 8 | timing.btr1)
+ result = self.m_objPCANBasic.Initialize(
+ self.m_PcanHandle, pcan_bitrate, hwtype, ioport, interrupt
+ )
+ elif is_fd:
+ if isinstance(timing, BitTimingFd):
+ timing = check_or_adjust_timing_clock(
+ timing, sorted(VALID_PCAN_FD_CLOCKS, reverse=True)
+ )
+ # We dump the timing parameters into the kwargs because they have equal names
+ # as the kwargs parameters and this saves us one additional code path
+ kwargs.update(timing)
+
+ clock_param = "f_clock" if "f_clock" in kwargs else "f_clock_mhz"
+ fd_parameters_values = [
+ f"{key}={kwargs[key]}"
+ for key in (clock_param, *PCAN_FD_PARAMETER_LIST)
+ if key in kwargs
+ ]
+
+ self.fd_bitrate = ", ".join(fd_parameters_values).encode("ascii")
+
+ result = self.m_objPCANBasic.InitializeFD(
+ self.m_PcanHandle, self.fd_bitrate
+ )
- result = self.m_objPCANBasic.Initialize(self.m_PcanHandle, pcan_bitrate, hwtype, ioport, interrupt)
+ else:
+ pcan_bitrate = PCAN_BITRATES.get(bitrate, PCAN_BAUD_500K)
+ result = self.m_objPCANBasic.Initialize(
+ self.m_PcanHandle, pcan_bitrate, hwtype, ioport, interrupt
+ )
if result != PCAN_ERROR_OK:
- raise PcanError(self._get_formatted_error(result))
+ raise PcanCanInitializationError(self._get_formatted_error(result))
- if HAS_EVENTS:
- self._recv_event = CreateEvent(None, 0, 0, None)
+ result = self.m_objPCANBasic.SetValue(
+ self.m_PcanHandle, PCAN_ALLOW_ERROR_FRAMES, PCAN_PARAMETER_ON
+ )
+
+ if result != PCAN_ERROR_OK:
+ if platform.system() != "Darwin":
+ raise PcanCanInitializationError(self._get_formatted_error(result))
+ else:
+ # TODO Remove Filter when MACCan actually supports it:
+ # https://github.com/mac-can/PCBUSB-Library/
+ log.debug(
+ "Ignoring error. PCAN_ALLOW_ERROR_FRAMES is still unsupported by OSX Library PCANUSB v0.11.2"
+ )
+
+ if receive_own_messages:
result = self.m_objPCANBasic.SetValue(
- self.m_PcanHandle, PCAN_RECEIVE_EVENT, self._recv_event)
+ self.m_PcanHandle, PCAN_ALLOW_ECHO_FRAMES, PCAN_PARAMETER_ON
+ )
+
if result != PCAN_ERROR_OK:
- raise PcanError(self._get_formatted_error(result))
+ raise PcanCanInitializationError(self._get_formatted_error(result))
- super(PcanBus, self).__init__(channel=channel, state=state, bitrate=bitrate, *args, **kwargs)
+ if kwargs.get("auto_reset", False):
+ result = self.m_objPCANBasic.SetValue(
+ self.m_PcanHandle, PCAN_BUSOFF_AUTORESET, PCAN_PARAMETER_ON
+ )
+
+ if result != PCAN_ERROR_OK:
+ raise PcanCanInitializationError(self._get_formatted_error(result))
+
+ if HAS_EVENTS:
+ if IS_WINDOWS:
+ self._recv_event = CreateEvent(None, 0, 0, None)
+ result = self.m_objPCANBasic.SetValue(
+ self.m_PcanHandle, PCAN_RECEIVE_EVENT, self._recv_event
+ )
+ elif IS_LINUX:
+ result, self._recv_event = self.m_objPCANBasic.GetValue(
+ self.m_PcanHandle, PCAN_RECEIVE_EVENT
+ )
+
+ if result != PCAN_ERROR_OK:
+ raise PcanCanInitializationError(self._get_formatted_error(result))
+
+ super().__init__(
+ channel=channel,
+ state=state,
+ bitrate=bitrate,
+ **kwargs,
+ )
+
+ def _find_channel_by_dev_id(self, device_id):
+ """
+ Iterate over all possible channels to find a channel that matches the device
+ ID. This method is somewhat brute force, but the Basic API only offers a
+ suitable API call since V4.4.0.
+
+ :param device_id: The device_id for which to search for
+ :return: The name of a PCAN channel that matches the device ID, or None if
+ no channel can be found.
+ """
+ for ch_name, ch_handle in PCAN_CHANNEL_NAMES.items():
+ err, cur_dev_id = self.m_objPCANBasic.GetValue(
+ ch_handle, PCAN_DEVICE_NUMBER
+ )
+ if err != PCAN_ERROR_OK:
+ continue
+
+ if cur_dev_id == device_id:
+ return ch_name
+
+ return None
def _get_formatted_error(self, error):
"""
@@ -127,31 +393,55 @@ def _get_formatted_error(self, error):
"""
def bits(n):
- """TODO: document"""
+ """
+ Iterate over all the set bits in `n`, returning the masked bits at
+ the set indices
+ """
while n:
- b = n & (~n+1)
- yield b
- n ^= b
-
- stsReturn = self.m_objPCANBasic.GetErrorText(error, 0)
+ # Create a mask to mask the lowest set bit in n
+ mask = ~n + 1
+ masked_value = n & mask
+ yield masked_value
+ # Toggle the lowest set bit
+ n ^= masked_value
+
+ stsReturn = self.m_objPCANBasic.GetErrorText(error, 0x9)
if stsReturn[0] != PCAN_ERROR_OK:
strings = []
for b in bits(error):
- stsReturn = self.m_objPCANBasic.GetErrorText(b, 0)
+ stsReturn = self.m_objPCANBasic.GetErrorText(b, 0x9)
if stsReturn[0] != PCAN_ERROR_OK:
- text = "An error occurred. Error-code's text ({0:X}h) couldn't be retrieved".format(error)
+ text = f"An error occurred. Error-code's text ({error:X}h) couldn't be retrieved"
else:
- text = stsReturn[1].decode('utf-8', errors='replace')
+ text = stsReturn[1].decode("utf-8", errors="replace")
strings.append(text)
- complete_text = '\n'.join(strings)
+ complete_text = "\n".join(strings)
else:
- complete_text = stsReturn[1].decode('utf-8', errors='replace')
+ complete_text = stsReturn[1].decode("utf-8", errors="replace")
return complete_text
+ def get_api_version(self):
+ error, value = self.m_objPCANBasic.GetValue(PCAN_NONEBUS, PCAN_API_VERSION)
+ if error != PCAN_ERROR_OK:
+ raise CanInitializationError("Failed to read pcan basic api version")
+
+ # fix https://github.com/hardbyte/python-can/issues/1642
+ version_string = value.decode("ascii").replace(",", ".").replace(" ", "")
+
+ return version.parse(version_string)
+
+ def check_api_version(self):
+ apv = self.get_api_version()
+ if apv < MIN_PCAN_API_VERSION:
+ log.warning(
+ f"Minimum version of pcan api is {MIN_PCAN_API_VERSION}."
+ f" Installed version is {apv}. Consider upgrade of pcan basic package"
+ )
+
def status(self):
"""
Query the PCAN bus status.
@@ -175,118 +465,351 @@ def reset(self):
status = self.m_objPCANBasic.Reset(self.m_PcanHandle)
return status == PCAN_ERROR_OK
- def _recv_internal(self, timeout):
+ def get_device_number(self):
+ """
+ Return the PCAN device number.
- if HAS_EVENTS:
- # We will utilize events for the timeout handling
- timeout_ms = int(timeout * 1000) if timeout is not None else INFINITE
- elif timeout is not None:
- # Calculate max time
- end_time = timeout_clock() + timeout
-
- #log.debug("Trying to read a msg")
-
- result = None
- while result is None:
- result = self.m_objPCANBasic.Read(self.m_PcanHandle)
- if result[0] == PCAN_ERROR_QRCVEMPTY:
- if HAS_EVENTS:
- result = None
- val = WaitForSingleObject(self._recv_event, timeout_ms)
- if val != WAIT_OBJECT_0:
- return None, False
- elif timeout is not None and timeout_clock() >= end_time:
- return None, False
+ :rtype: int
+ :return: PCAN device number
+ """
+ error, value = self.m_objPCANBasic.GetValue(
+ self.m_PcanHandle, PCAN_DEVICE_NUMBER
+ )
+ if error != PCAN_ERROR_OK:
+ return None
+ return value
+
+ def set_device_number(self, device_number):
+ """
+ Set the PCAN device number.
+
+ :param device_number: new PCAN device number
+ :rtype: bool
+ :return: True if device number set successfully
+ """
+ try:
+ if (
+ self.m_objPCANBasic.SetValue(
+ self.m_PcanHandle, PCAN_DEVICE_NUMBER, int(device_number)
+ )
+ != PCAN_ERROR_OK
+ ):
+ raise ValueError()
+ except ValueError:
+ log.error("Invalid value '%s' for device number.", device_number)
+ return False
+ return True
+
+ def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]:
+ end_time = time.time() + timeout if timeout is not None else None
+
+ while True:
+ if self._can_protocol is CanProtocol.CAN_FD:
+ result, pcan_msg, pcan_timestamp = self.m_objPCANBasic.ReadFD(
+ self.m_PcanHandle
+ )
+ else:
+ result, pcan_msg, pcan_timestamp = self.m_objPCANBasic.Read(
+ self.m_PcanHandle
+ )
+
+ if result == PCAN_ERROR_OK:
+ # message received
+ break
+
+ if result == PCAN_ERROR_QRCVEMPTY:
+ # receive queue is empty, wait or return on timeout
+
+ if end_time is None:
+ time_left: float | None = None
+ timed_out = False
else:
- result = None
- time.sleep(0.001)
- elif result[0] & (PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY):
- log.warning(self._get_formatted_error(result[0]))
- return None, False
- elif result[0] != PCAN_ERROR_OK:
- raise PcanError(self._get_formatted_error(result[0]))
+ time_left = max(0.0, end_time - time.time())
+ timed_out = time_left == 0.0
+
+ if timed_out:
+ return None, False
- theMsg = result[1]
- itsTimeStamp = result[2]
+ if not HAS_EVENTS:
+ # polling mode
+ time.sleep(0.001)
+ continue
+
+ if IS_WINDOWS:
+ # Windows with event
+ if time_left is None:
+ time_left_ms = INFINITE
+ else:
+ time_left_ms = int(time_left * 1000)
+ _ret = WaitForSingleObject(self._recv_event, time_left_ms)
+ if _ret == WAIT_OBJECT_0:
+ continue
+
+ elif IS_LINUX:
+ # Linux with event
+ recv, _, _ = select.select([self._recv_event], [], [], time_left)
+ if self._recv_event in recv:
+ continue
+
+ elif result & (PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY):
+ log.warning(self._get_formatted_error(result))
+
+ elif result == PCAN_ERROR_ILLDATA:
+ # When there is an invalid frame on CAN bus (in our case CAN FD), PCAN first reports result PCAN_ERROR_ILLDATA
+ # and then it sends the error frame. If the PCAN_ERROR_ILLDATA is not ignored, python-can throws an exception.
+ # So we ignore any PCAN_ERROR_ILLDATA results here.
+ pass
+
+ else:
+ raise PcanCanOperationError(self._get_formatted_error(result))
+
+ return None, False
+
+ is_extended_id = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_EXTENDED.value)
+ is_remote_frame = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_RTR.value)
+ is_fd = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_FD.value)
+ is_rx = not bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_ECHO.value)
+ bitrate_switch = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_BRS.value)
+ error_state_indicator = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_ESI.value)
+ is_error_frame = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_ERRFRAME.value)
+
+ if self._can_protocol is CanProtocol.CAN_FD:
+ dlc = dlc2len(pcan_msg.DLC)
+ timestamp = boottimeEpoch + (pcan_timestamp.value / (1000.0 * 1000.0))
+ else:
+ dlc = pcan_msg.LEN
+ timestamp = boottimeEpoch + (
+ (
+ pcan_timestamp.micros
+ + 1000 * pcan_timestamp.millis
+ + 0x100000000 * 1000 * pcan_timestamp.millis_overflow
+ )
+ / (1000.0 * 1000.0)
+ )
+
+ rx_msg = Message(
+ channel=self.channel_info,
+ timestamp=timestamp,
+ arbitration_id=pcan_msg.ID,
+ is_extended_id=is_extended_id,
+ is_remote_frame=is_remote_frame,
+ is_error_frame=is_error_frame,
+ dlc=dlc,
+ data=pcan_msg.DATA[:dlc],
+ is_fd=is_fd,
+ is_rx=is_rx,
+ bitrate_switch=bitrate_switch,
+ error_state_indicator=error_state_indicator,
+ )
- #log.debug("Received a message")
+ return rx_msg, False
- bIsRTR = (theMsg.MSGTYPE & PCAN_MESSAGE_RTR.value) == PCAN_MESSAGE_RTR.value
- bIsExt = (theMsg.MSGTYPE & PCAN_MESSAGE_EXTENDED.value) == PCAN_MESSAGE_EXTENDED.value
+ def send(self, msg, timeout=None):
+ msgType = (
+ PCAN_MESSAGE_EXTENDED.value
+ if msg.is_extended_id
+ else PCAN_MESSAGE_STANDARD.value
+ )
+ if msg.is_remote_frame:
+ msgType |= PCAN_MESSAGE_RTR.value
+ if msg.is_error_frame:
+ msgType |= PCAN_MESSAGE_ERRFRAME.value
+ if msg.is_fd:
+ msgType |= PCAN_MESSAGE_FD.value
+ if msg.bitrate_switch:
+ msgType |= PCAN_MESSAGE_BRS.value
+ if msg.error_state_indicator:
+ msgType |= PCAN_MESSAGE_ESI.value
+
+ if self._can_protocol is CanProtocol.CAN_FD:
+ # create a TPCANMsg message structure
+ CANMsg = TPCANMsgFD()
+
+ # configure the message. ID, Length of data, message type and data
+ CANMsg.ID = msg.arbitration_id
+ CANMsg.DLC = len2dlc(msg.dlc)
+ CANMsg.MSGTYPE = msgType
- dlc = theMsg.LEN
- timestamp = boottimeEpoch + ((itsTimeStamp.micros + 1000 * itsTimeStamp.millis + 0x100000000 * 1000 * itsTimeStamp.millis_overflow) / (1000.0 * 1000.0))
+ # copy data
+ CANMsg.DATA[: msg.dlc] = msg.data[: msg.dlc]
- rx_msg = Message(timestamp=timestamp,
- arbitration_id=theMsg.ID,
- extended_id=bIsExt,
- is_remote_frame=bIsRTR,
- dlc=dlc,
- data=theMsg.DATA[:dlc])
+ log.debug("Data: %s", msg.data)
+ log.debug("Type: %s", type(msg.data))
- return rx_msg, False
+ result = self.m_objPCANBasic.WriteFD(self.m_PcanHandle, CANMsg)
- def send(self, msg, timeout=None):
- if msg.id_type:
- msgType = PCAN_MESSAGE_EXTENDED
else:
- msgType = PCAN_MESSAGE_STANDARD
+ # create a TPCANMsg message structure
+ CANMsg = TPCANMsg()
- # create a TPCANMsg message structure
- CANMsg = TPCANMsg()
+ # configure the message. ID, Length of data, message type and data
+ CANMsg.ID = msg.arbitration_id
+ CANMsg.LEN = msg.dlc
+ CANMsg.MSGTYPE = msgType
- # configure the message. ID, Length of data, message type and data
- CANMsg.ID = msg.arbitration_id
- CANMsg.LEN = msg.dlc
- CANMsg.MSGTYPE = msgType
+ # if a remote frame will be sent, data bytes are not important.
+ if not msg.is_remote_frame:
+ # copy data
+ CANMsg.DATA[: CANMsg.LEN] = msg.data[: CANMsg.LEN]
- # if a remote frame will be sent, data bytes are not important.
- if msg.is_remote_frame:
- CANMsg.MSGTYPE = msgType.value | PCAN_MESSAGE_RTR.value
- else:
- # copy data
- for i in range(CANMsg.LEN):
- CANMsg.DATA[i] = msg.data[i]
+ log.debug("Data: %s", msg.data)
+ log.debug("Type: %s", type(msg.data))
- log.debug("Data: %s", msg.data)
- log.debug("Type: %s", type(msg.data))
+ result = self.m_objPCANBasic.Write(self.m_PcanHandle, CANMsg)
- result = self.m_objPCANBasic.Write(self.m_PcanHandle, CANMsg)
if result != PCAN_ERROR_OK:
- raise PcanError("Failed to send: " + self._get_formatted_error(result))
+ raise PcanCanOperationError(
+ "Failed to send: " + self._get_formatted_error(result)
+ )
def flash(self, flash):
"""
Turn on or off flashing of the device's LED for physical
identification purposes.
"""
- self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_CHANNEL_IDENTIFYING, bool(flash))
+ self.m_objPCANBasic.SetValue(
+ self.m_PcanHandle, PCAN_CHANNEL_IDENTIFYING, bool(flash)
+ )
def shutdown(self):
- super(PcanBus, self).shutdown()
+ super().shutdown()
+ if HAS_EVENTS and IS_LINUX:
+ self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_RECEIVE_EVENT, 0)
+
self.m_objPCANBasic.Uninitialize(self.m_PcanHandle)
+ @property
+ def fd(self) -> bool:
+ class_name = self.__class__.__name__
+ warnings.warn(
+ f"The {class_name}.fd property is deprecated and superseded by {class_name}.protocol. "
+ "It is scheduled for removal in python-can version 5.0.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self._can_protocol is CanProtocol.CAN_FD
+
@property
def state(self):
return self._state
@state.setter
def state(self, new_state):
-
- self._state = new_state
+ # declare here, which is called by __init__()
+ self._state = new_state # pylint: disable=attribute-defined-outside-init
if new_state is BusState.ACTIVE:
- self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_OFF)
+ self.m_objPCANBasic.SetValue(
+ self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_OFF
+ )
- if new_state is BusState.PASSIVE:
+ elif new_state is BusState.PASSIVE:
# When this mode is set, the CAN controller does not take part on active events (eg. transmit CAN messages)
# but stays in a passive mode (CAN monitor), in which it can analyse the traffic on the CAN bus used by a
# PCAN channel. See also the Philips Data Sheet "SJA1000 Stand-alone CAN controller".
- self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_ON)
+ self.m_objPCANBasic.SetValue(
+ self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_ON
+ )
+
+ @staticmethod
+ def _detect_available_configs():
+ channels = []
+ try:
+ library_handle = PCANBasic()
+ except OSError:
+ return channels
+
+ interfaces = []
+
+ if platform.system() != "Darwin":
+ res, value = library_handle.GetValue(PCAN_NONEBUS, PCAN_ATTACHED_CHANNELS)
+ if res != PCAN_ERROR_OK:
+ return interfaces
+ channel_information: list[TPCANChannelInformation] = list(value)
+ for channel in channel_information:
+ # find channel name in PCAN_CHANNEL_NAMES by value
+ channel_name = next(
+ _channel_name
+ for _channel_name, channel_id in PCAN_CHANNEL_NAMES.items()
+ if channel_id.value == channel.channel_handle
+ )
+ channel_config = {
+ "interface": "pcan",
+ "channel": channel_name,
+ "supports_fd": bool(channel.device_features & FEATURE_FD_CAPABLE),
+ "controller_number": channel.controller_number,
+ "device_features": channel.device_features,
+ "device_id": channel.device_id,
+ "device_name": channel.device_name.decode("latin-1"),
+ "device_type": channel.device_type,
+ "channel_condition": channel.channel_condition,
+ }
+ interfaces.append(channel_config)
+ return interfaces
+
+ for i in range(16):
+ interfaces.append(
+ {
+ "id": TPCANHandle(PCAN_PCIBUS1.value + i),
+ "name": "PCAN_PCIBUS" + str(i + 1),
+ }
+ )
+ for i in range(16):
+ interfaces.append(
+ {
+ "id": TPCANHandle(PCAN_USBBUS1.value + i),
+ "name": "PCAN_USBBUS" + str(i + 1),
+ }
+ )
+ for i in range(2):
+ interfaces.append(
+ {
+ "id": TPCANHandle(PCAN_PCCBUS1.value + i),
+ "name": "PCAN_PCCBUS" + str(i + 1),
+ }
+ )
+ for i in range(16):
+ interfaces.append(
+ {
+ "id": TPCANHandle(PCAN_LANBUS1.value + i),
+ "name": "PCAN_LANBUS" + str(i + 1),
+ }
+ )
+ for i in interfaces:
+ try:
+ error, value = library_handle.GetValue(i["id"], PCAN_CHANNEL_CONDITION)
+ if error != PCAN_ERROR_OK or value != PCAN_CHANNEL_AVAILABLE:
+ continue
+ has_fd = False
+ error, value = library_handle.GetValue(i["id"], PCAN_CHANNEL_FEATURES)
+ if error == PCAN_ERROR_OK:
+ has_fd = bool(value & FEATURE_FD_CAPABLE)
+ channels.append(
+ {"interface": "pcan", "channel": i["name"], "supports_fd": has_fd}
+ )
+ except AttributeError: # Ignore if this fails for some interfaces
+ pass
+ return channels
+
+ def status_string(self) -> str | None:
+ """
+ Query the PCAN bus status.
+
+ :return: The status description, if any was found.
+ """
+ try:
+ return PCAN_DICT_STATUS[self.status()]
+ except KeyError:
+ return None
class PcanError(CanError):
- """
- TODO: add docs
- """
- pass
+ """A generic error on a PCAN bus."""
+
+
+class PcanCanOperationError(CanOperationError, PcanError):
+ """Like :class:`can.exceptions.CanOperationError`, but specific to Pcan."""
+
+
+class PcanCanInitializationError(CanInitializationError, PcanError):
+ """Like :class:`can.exceptions.CanInitializationError`, but specific to Pcan."""
diff --git a/can/interfaces/robotell.py b/can/interfaces/robotell.py
new file mode 100644
index 000000000..b24543856
--- /dev/null
+++ b/can/interfaces/robotell.py
@@ -0,0 +1,403 @@
+"""
+Interface for Chinese Robotell compatible interfaces (win32/linux).
+"""
+
+import io
+import logging
+import time
+
+from can import BusABC, CanProtocol, Message
+
+from ..exceptions import CanInterfaceNotImplementedError, CanOperationError
+
+logger = logging.getLogger(__name__)
+
+try:
+ import serial
+except ImportError:
+ logger.warning(
+ "You won't be able to use the Robotell can backend without "
+ "the serial module installed!"
+ )
+ serial = None
+
+
+class robotellBus(BusABC):
+ """
+ robotell interface
+ """
+
+ _PACKET_HEAD = 0xAA # Frame starts with 2x FRAME_HEAD bytes
+ _PACKET_TAIL = 0x55 # Frame ends with 2x FRAME_END bytes
+ _PACKET_ESC = (
+ 0xA5 # Escape char before any HEAD, TAIL or ESC chat (including in checksum)
+ )
+
+ _CAN_CONFIG_CHANNEL = 0xFF # Configuration channel of CAN
+ _CAN_SERIALBPS_ID = 0x01FFFE90 # USB Serial port speed
+ _CAN_ART_ID = 0x01FFFEA0 # Automatic retransmission
+ _CAN_ABOM_ID = 0x01FFFEB0 # Automatic bus management
+ _CAN_RESET_ID = 0x01FFFEC0 # ID for initialization
+ _CAN_BAUD_ID = 0x01FFFED0 # CAN baud rate
+ _CAN_FILTER_BASE_ID = 0x01FFFEE0 # ID for first filter (filter0)
+ _CAN_FILTER_MAX_ID = 0x01FFFEE0 + 13 # ID for the last filter (filter13)
+ _CAN_INIT_FLASH_ID = 0x01FFFEFF # Restore factory settings
+ _CAN_READ_SERIAL1 = 0x01FFFFF0 # Read first part of device serial number
+ _CAN_READ_SERIAL2 = 0x01FFFFF1 # Read first part of device serial number
+ _MAX_CAN_BAUD = 1000000 # Maximum supported CAN baud rate
+ _FILTER_ID_MASK = 0x0000000F # Filter ID mask
+ _CAN_FILTER_EXTENDED = 0x40000000 # Enable mask
+ _CAN_FILTER_ENABLE = 0x80000000 # Enable filter
+
+ _CAN_STANDARD_FMT = 0 # Standard message ID
+ _CAN_EXTENDED_FMT = 1 # 29 Bit extended format ID
+ _CAN_DATA_FRAME = 0 # Send data frame
+ _CAN_REMOTE_FRAME = 1 # Request remote frame
+
+ def __init__(
+ self, channel, ttyBaudrate=115200, bitrate=None, rtscts=False, **kwargs
+ ):
+ """
+ :param str channel:
+ port of underlying serial or usb device (e.g. ``/dev/ttyUSB0``, ``COM8``, ...)
+ Must not be empty. Can also end with ``@115200`` (or similarly) to specify the baudrate.
+ :param int ttyBaudrate:
+ baudrate of underlying serial or usb device (Ignored if set via the ``channel`` parameter)
+ :param int bitrate:
+ CAN Bitrate in bit/s. Value is stored in the adapter and will be used as default if no bitrate is specified
+ :param bool rtscts:
+ turn hardware handshake (RTS/CTS) on and off
+ """
+ if serial is None:
+ raise CanInterfaceNotImplementedError("The serial module is not installed")
+
+ if not channel: # if None or empty
+ raise TypeError("Must specify a serial port.")
+ if "@" in channel:
+ (channel, ttyBaudrate) = channel.split("@")
+ self.serialPortOrig = serial.serial_for_url(
+ channel, baudrate=ttyBaudrate, rtscts=rtscts
+ )
+
+ # Disable flushing queued config ACKs on lookup channel (for unit tests)
+ self._loopback_test = channel == "loop://"
+
+ self._rxbuffer = bytearray() # raw bytes from the serial port
+ self._rxmsg = [] # extracted CAN messages waiting to be read
+ self._configmsg = [] # extracted config channel messages
+
+ self._writeconfig(self._CAN_RESET_ID, 0) # Not sure if this is really necessary
+
+ if bitrate is not None:
+ self.set_bitrate(bitrate)
+
+ self._can_protocol = CanProtocol.CAN_20
+ self.channel_info = (
+ f"Robotell USB-CAN s/n {self.get_serial_number(1)} on {channel}"
+ )
+ logger.info("Using device: %s", self.channel_info)
+
+ super().__init__(channel=channel, **kwargs)
+
+ def set_bitrate(self, bitrate):
+ """
+ :raise ValueError: if *bitrate* is greater than 1000000
+ :param int bitrate:
+ Bitrate in bit/s
+ """
+ if bitrate <= self._MAX_CAN_BAUD:
+ self._writeconfig(self._CAN_BAUD_ID, bitrate)
+ else:
+ raise ValueError(f"Invalid bitrate, must be less than {self._MAX_CAN_BAUD}")
+
+ def set_auto_retransmit(self, retrans_flag):
+ """
+ :param bool retrans_flag:
+ Enable/disable automatic retransmission of unacknowledged CAN frames
+ """
+ self._writeconfig(self._CAN_ART_ID, 1 if retrans_flag else 0)
+
+ def set_auto_bus_management(self, auto_man):
+ """
+ :param bool auto_man:
+ Enable/disable automatic bus management
+ """
+ # Not sure what "automatic bus management" does. Does not seem to control
+ # automatic ACK of CAN frames (listen only mode)
+ self._writeconfig(self._CAN_ABOM_ID, 1 if auto_man else 0)
+
+ def set_serial_rate(self, serial_bps):
+ """
+ :param int serial_bps:
+ Set the baud rate of the serial port (not CAN) interface
+ """
+ self._writeconfig(self._CAN_SERIALBPS_ID, serial_bps)
+
+ def set_hw_filter(self, filterid, enabled, msgid_value, msgid_mask, extended_msg):
+ """
+ :raise ValueError: if *filterid* is not between 1 and 14
+ :param int filterid:
+ ID of filter (1-14)
+ :param bool enabled:
+ This filter is enabled
+ :param int msgid_value:
+ CAN message ID to filter on. The test unit does not accept an extented message ID unless bit 31 of the ID was set.
+ :param int msgid_mask:
+ Mask to apply to CAN messagge ID
+ :param bool extended_msg:
+ Filter operates on extended format messages
+ """
+ if filterid < 1 or filterid > 14:
+ raise ValueError("Invalid filter ID. ID must be between 0 and 13")
+ else:
+ configid = self._CAN_FILTER_BASE_ID + (filterid - 1)
+ msgid_value += self._CAN_FILTER_ENABLE if enabled else 0
+ msgid_value += self._CAN_FILTER_EXTENDED if extended_msg else 0
+ self._writeconfig(configid, msgid_value, msgid_mask)
+
+ def _getconfigsize(self, configid):
+ if configid == self._CAN_ART_ID or configid == self._CAN_ABOM_ID:
+ return 1
+ if configid == self._CAN_BAUD_ID or configid == self._CAN_INIT_FLASH_ID:
+ return 4
+ if configid == self._CAN_SERIALBPS_ID:
+ return 4
+ if configid == self._CAN_READ_SERIAL1 or configid <= self._CAN_READ_SERIAL2:
+ return 8
+ if self._CAN_FILTER_BASE_ID <= configid <= self._CAN_FILTER_MAX_ID:
+ return 8
+ return 0
+
+ def _readconfig(self, configid, timeout):
+ self._writemessage(
+ msgid=configid,
+ msgdata=bytearray(8),
+ datalen=self._getconfigsize(configid),
+ msgchan=self._CAN_CONFIG_CHANNEL,
+ msgformat=self._CAN_EXTENDED_FMT,
+ msgtype=self._CAN_REMOTE_FRAME,
+ )
+ # Read message from config channel with result. Flush any previously pending config messages
+ newmsg = self._readmessage(not self._loopback_test, True, timeout)
+ if newmsg is None:
+ logger.warning(
+ f"Timeout waiting for response when reading config value {configid:04X}."
+ )
+ return None
+ return newmsg[4:12]
+
+ def _writeconfig(self, configid, value, value2=0):
+ configsize = self._getconfigsize(configid)
+ configdata = bytearray(configsize)
+ if configsize >= 1:
+ configdata[0] = value & 0xFF
+ if configsize >= 4:
+ configdata[1] = (value >> 8) & 0xFF
+ configdata[2] = (value >> 16) & 0xFF
+ configdata[3] = (value >> 24) & 0xFF
+ if configsize >= 8:
+ configdata[4] = value2 & 0xFF
+ configdata[5] = (value2 >> 8) & 0xFF
+ configdata[6] = (value2 >> 16) & 0xFF
+ configdata[7] = (value2 >> 24) & 0xFF
+ self._writemessage(
+ msgid=configid,
+ msgdata=configdata,
+ datalen=configsize,
+ msgchan=self._CAN_CONFIG_CHANNEL,
+ msgformat=self._CAN_EXTENDED_FMT,
+ msgtype=self._CAN_DATA_FRAME,
+ )
+ # Read message from config channel to verify. Flush any previously pending config messages
+ newmsg = self._readmessage(not self._loopback_test, True, 1)
+ if newmsg is None:
+ logger.warning(
+ "Timeout waiting for response when writing config value %d", configid
+ )
+
+ def _readmessage(self, flushold, cfgchannel, timeout):
+ header = bytearray([self._PACKET_HEAD, self._PACKET_HEAD])
+ terminator = bytearray([self._PACKET_TAIL, self._PACKET_TAIL])
+
+ msgqueue = self._configmsg if cfgchannel else self._rxmsg
+ if flushold:
+ del msgqueue[:]
+
+ # read what is already in serial port receive buffer - unless we are doing loopback testing
+ if not self._loopback_test:
+ while self.serialPortOrig.in_waiting:
+ self._rxbuffer += self.serialPortOrig.read()
+
+ # loop until we have read an appropriate message
+ start = time.time()
+ time_left = timeout
+ while True:
+ # make sure first bytes in RX buffer is a new packet header
+ headpos = self._rxbuffer.find(header)
+ if headpos > 0:
+ # data does not start with expected header bytes. Log error and ignore garbage
+ logger.warning("Ignoring extra " + str(headpos) + " garbage bytes")
+ del self._rxbuffer[:headpos]
+ headpos = self._rxbuffer.find(header) # should now be at index 0!
+
+ # check to see if we have a complete packet in the RX buffer
+ termpos = self._rxbuffer.find(terminator)
+ if headpos == 0 and termpos > headpos:
+ # copy packet into message structure and un-escape bytes
+ newmsg = bytearray()
+ idx = headpos + len(header)
+ while idx < termpos:
+ if self._rxbuffer[idx] == self._PACKET_ESC:
+ idx += 1
+ newmsg.append(self._rxbuffer[idx])
+ idx += 1
+ del self._rxbuffer[: termpos + len(terminator)]
+
+ # Check one - make sure message structure is the correct length
+ if len(newmsg) == 17:
+ # Check two - verify the checksum
+ cs = 0
+ for idx in range(16):
+ cs = (cs + newmsg[idx]) & 0xFF
+ if newmsg[16] == cs:
+ # OK, valid message - place it in the correct queue
+ if newmsg[13] == 0xFF: # Check for config channel
+ self._configmsg.append(newmsg)
+ else:
+ self._rxmsg.append(newmsg)
+ else:
+ logger.warning("Incorrect message checksum, discarded message")
+ else:
+ logger.warning(
+ "Invalid message structure length %d, ignoring message",
+ len(newmsg),
+ )
+
+ # Check if we have a message in the desired queue - if so copy and return
+ if len(msgqueue) > 0:
+ newmsg = msgqueue[0]
+ del msgqueue[:1]
+ return newmsg
+
+ # if we still don't have a complete message, do a blocking read
+ self.serialPortOrig.timeout = time_left
+ byte = self.serialPortOrig.read()
+ if byte:
+ self._rxbuffer += byte
+ # If there is time left, try next one with reduced timeout
+ if timeout is not None:
+ time_left = timeout - (time.time() - start)
+ if time_left <= 0:
+ return None
+
+ def _writemessage(self, msgid, msgdata, datalen, msgchan, msgformat, msgtype):
+ msgbuf = bytearray(17) # Message structure plus checksum byte
+
+ msgbuf[0] = msgid & 0xFF
+ msgbuf[1] = (msgid >> 8) & 0xFF
+ msgbuf[2] = (msgid >> 16) & 0xFF
+ msgbuf[3] = (msgid >> 24) & 0xFF
+
+ if msgtype == self._CAN_DATA_FRAME:
+ for idx in range(datalen):
+ msgbuf[idx + 4] = msgdata[idx]
+
+ msgbuf[12] = datalen
+ msgbuf[13] = msgchan
+ msgbuf[14] = msgformat
+ msgbuf[15] = msgtype
+
+ cs = 0
+ for idx in range(16):
+ cs = (cs + msgbuf[idx]) & 0xFF
+ msgbuf[16] = cs
+
+ packet = bytearray()
+ packet.append(self._PACKET_HEAD)
+ packet.append(self._PACKET_HEAD)
+ for msgbyte in msgbuf:
+ if (
+ msgbyte == self._PACKET_ESC
+ or msgbyte == self._PACKET_HEAD
+ or msgbyte == self._PACKET_TAIL
+ ):
+ packet.append(self._PACKET_ESC)
+ packet.append(msgbyte)
+ packet.append(self._PACKET_TAIL)
+ packet.append(self._PACKET_TAIL)
+
+ self.serialPortOrig.write(packet)
+ self.serialPortOrig.flush()
+
+ def flush(self):
+ del self._rxbuffer[:]
+ del self._rxmsg[:]
+ del self._configmsg[:]
+ while self.serialPortOrig.in_waiting:
+ self.serialPortOrig.read()
+
+ def _recv_internal(self, timeout):
+ msgbuf = self._readmessage(False, False, timeout)
+ if msgbuf is not None:
+ msg = Message(
+ arbitration_id=msgbuf[0]
+ + (msgbuf[1] << 8)
+ + (msgbuf[2] << 16)
+ + (msgbuf[3] << 24),
+ is_extended_id=(msgbuf[14] == self._CAN_EXTENDED_FMT),
+ timestamp=time.time(), # Better than nothing...
+ is_remote_frame=(msgbuf[15] == self._CAN_REMOTE_FRAME),
+ dlc=msgbuf[12],
+ data=msgbuf[4 : 4 + msgbuf[12]],
+ )
+ return msg, False
+ return None, False
+
+ def send(self, msg, timeout=None):
+ if timeout != self.serialPortOrig.write_timeout:
+ self.serialPortOrig.write_timeout = timeout
+ self._writemessage(
+ msg.arbitration_id,
+ msg.data,
+ msg.dlc,
+ 0,
+ self._CAN_EXTENDED_FMT if msg.is_extended_id else self._CAN_STANDARD_FMT,
+ self._CAN_REMOTE_FRAME if msg.is_remote_frame else self._CAN_DATA_FRAME,
+ )
+
+ def shutdown(self):
+ super().shutdown()
+ self.serialPortOrig.close()
+
+ def fileno(self):
+ try:
+ return self.serialPortOrig.fileno()
+ except io.UnsupportedOperation:
+ raise NotImplementedError(
+ "fileno is not implemented using current CAN bus on this platform"
+ ) from None
+ except Exception as exception:
+ raise CanOperationError("Cannot fetch fileno") from exception
+
+ def get_serial_number(self, timeout: int | None) -> str | None:
+ """Get serial number of the slcan interface.
+
+ :param timeout:
+ seconds to wait for serial number or None to wait indefinitely
+ :return:
+ None on timeout or a str object.
+ """
+
+ sn1 = self._readconfig(self._CAN_READ_SERIAL1, timeout)
+ if sn1 is None:
+ return None
+ sn2 = self._readconfig(self._CAN_READ_SERIAL2, timeout)
+ if sn2 is None:
+ return None
+
+ serial = ""
+ for idx in range(0, 8, 2):
+ serial += f"{sn1[idx]:02X}{sn1[idx + 1]:02X}-"
+ for idx in range(0, 4, 2):
+ serial += f"{sn2[idx]:02X}{sn2[idx + 1]:02X}-"
+ return serial[:-1]
diff --git a/can/interfaces/seeedstudio/__init__.py b/can/interfaces/seeedstudio/__init__.py
new file mode 100644
index 000000000..9466bde43
--- /dev/null
+++ b/can/interfaces/seeedstudio/__init__.py
@@ -0,0 +1,6 @@
+__all__ = [
+ "SeeedBus",
+ "seeedstudio",
+]
+
+from can.interfaces.seeedstudio.seeedstudio import SeeedBus
diff --git a/can/interfaces/seeedstudio/seeedstudio.py b/can/interfaces/seeedstudio/seeedstudio.py
new file mode 100644
index 000000000..26339616c
--- /dev/null
+++ b/can/interfaces/seeedstudio/seeedstudio.py
@@ -0,0 +1,329 @@
+"""
+To Support the Seeed USB-Can analyzer interface. The device will appear
+as a serial port, for example "/dev/ttyUSB0" on Linux machines
+or "COM1" on Windows.
+https://www.seeedstudio.com/USB-CAN-Analyzer-p-2888.html
+SKU 114991193
+"""
+
+import io
+import logging
+import struct
+from time import time
+
+import can
+from can import BusABC, CanProtocol, Message
+
+logger = logging.getLogger("seeedbus")
+
+try:
+ import serial
+except ImportError:
+ logger.warning(
+ "You won't be able to use the serial can backend without "
+ "the serial module installed!"
+ )
+ serial = None
+
+
+class SeeedBus(BusABC):
+ """
+ Enable basic can communication over a USB-CAN-Analyzer device.
+ """
+
+ BITRATE = {
+ 1000000: 0x01,
+ 800000: 0x02,
+ 500000: 0x03,
+ 400000: 0x04,
+ 250000: 0x05,
+ 200000: 0x06,
+ 125000: 0x07,
+ 100000: 0x08,
+ 50000: 0x09,
+ 20000: 0x0A,
+ 10000: 0x0B,
+ 5000: 0x0C,
+ }
+
+ FRAMETYPE = {"STD": 0x01, "EXT": 0x02}
+
+ OPERATIONMODE = {
+ "normal": 0x00,
+ "loopback": 0x01,
+ "silent": 0x02,
+ "loopback_and_silent": 0x03,
+ }
+
+ def __init__(
+ self,
+ channel,
+ baudrate=2000000,
+ timeout=0.1,
+ frame_type="STD",
+ operation_mode="normal",
+ bitrate=500000,
+ can_filters=None,
+ **kwargs,
+ ):
+ """
+ :param str channel:
+ The serial device to open. For example "/dev/ttyS1" or
+ "/dev/ttyUSB0" on Linux or "COM1" on Windows systems.
+
+ :param baudrate:
+ The default matches required baudrate
+
+ :param float timeout:
+ Timeout for the serial device in seconds (default 0.1).
+
+ :param str frame_type:
+ STD or EXT, to select standard or extended messages
+
+ :param operation_mode
+ normal, loopback, silent or loopback_and_silent.
+
+ :param bitrate
+ CAN bus bit rate, selected from available list.
+
+ :param can_filters:
+ A list of CAN filter dictionaries. If one filter is provided,
+ it will be used by the high-performance hardware filter. If
+ zero or more than one filter is provided, software-based
+ filtering will be used. Defaults to None (no filtering).
+
+ :raises can.CanInitializationError: If the given parameters are invalid.
+ :raises can.CanInterfaceNotImplementedError: If the serial module is not installed.
+ """
+
+ if serial is None:
+ raise can.CanInterfaceNotImplementedError(
+ "the serial module is not installed"
+ )
+
+ can_id = 0x00
+ can_mask = 0x00
+ self._is_filtered = False
+
+ if can_filters and len(can_filters) == 1:
+ self._is_filtered = True
+ hw_filter = can_filters[0]
+ can_id = hw_filter["can_id"]
+ can_mask = hw_filter["can_mask"]
+
+ self.bit_rate = bitrate
+ self.frame_type = frame_type
+ self.op_mode = operation_mode
+ self.filter_id = struct.pack(" None:
+ try:
+ self.ser.write(byte_msg)
+ except serial.PortNotOpenError as error:
+ raise can.CanOperationError("writing to closed port") from error
+ except serial.SerialTimeoutException as error:
+ raise can.CanTimeoutError() from error
+
+ def _recv_internal(self, timeout):
+ """
+ Read a message from the serial device.
+
+ :param timeout:
+
+ .. warning::
+ This parameter will be ignored. The timeout value of the
+ channel is used.
+
+ :return:
+ 1. a message that was read or None on timeout
+ 2. a bool that is True if hw_filter is enabled, else False
+
+ :rtype:
+ can.Message, bool
+ """
+ try:
+ # ser.read can return an empty string
+ # or raise a SerialException
+ rx_byte_1 = self.ser.read()
+
+ except serial.PortNotOpenError as error:
+ raise can.CanOperationError("reading from closed port") from error
+ except serial.SerialException:
+ return None, self._is_filtered
+
+ if rx_byte_1 and ord(rx_byte_1) == 0xAA:
+ try:
+ rx_byte_2 = ord(self.ser.read())
+
+ time_stamp = time()
+ if rx_byte_2 == 0x55:
+ status = bytearray([0xAA, 0x55])
+ status += bytearray(self.ser.read(18))
+ logger.debug("status resp:\t%s", status.hex())
+
+ else:
+ length = int(rx_byte_2 & 0x0F)
+ is_extended = bool(rx_byte_2 & 0x20)
+ is_remote = bool(rx_byte_2 & 0x10)
+ if is_extended:
+ s_3_4_5_6 = bytearray(self.ser.read(4))
+ arb_id = (struct.unpack(" None:
"""
- :param str channel:
+ :param channel:
The serial device to open. For example "/dev/ttyS1" or
"/dev/ttyUSB0" on Linux or "COM1" on Windows systems.
- :param int baudrate:
+ :param baudrate:
Baud rate of the serial device in bit/s (default 115200).
.. warning::
Some serial port implementations don't care about the baudrate.
- :param float timeout:
+ :param timeout:
Timeout for the serial device in seconds (default 0.1).
- :param bool rtscts:
+ :param rtscts:
turn hardware handshake (RTS/CTS) on and off
+ :raises ~can.exceptions.CanInitializationError:
+ If the given parameters are invalid.
+ :raises ~can.exceptions.CanInterfaceNotImplementedError:
+ If the serial module is not installed.
"""
+
+ if not serial:
+ raise CanInterfaceNotImplementedError("the serial module is not installed")
+
if not channel:
- raise ValueError("Must specify a serial port.")
+ raise TypeError("Must specify a serial port.")
- self.channel_info = "Serial interface: " + channel
- self.ser = serial.serial_for_url(
- channel, baudrate=baudrate, timeout=timeout, rtscts=rtscts)
+ self.channel_info = f"Serial interface: {channel}"
+ self._can_protocol = CanProtocol.CAN_20
+
+ try:
+ self._ser = serial.serial_for_url(
+ channel, baudrate=baudrate, timeout=timeout, rtscts=rtscts
+ )
+ except ValueError as error:
+ raise CanInitializationError(
+ "could not create the serial device"
+ ) from error
- super(SerialBus, self).__init__(channel=channel, *args, **kwargs)
+ super().__init__(channel, **kwargs)
- def shutdown(self):
+ def shutdown(self) -> None:
"""
Close the serial interface.
"""
- self.ser.close()
+ super().shutdown()
+ self._ser.close()
- def send(self, msg, timeout=None):
+ def send(self, msg: Message, timeout: float | None = None) -> None:
"""
Send a message over the serial device.
- :param can.Message msg:
+ :param msg:
Message to send.
- .. note:: Flags like ``extended_id``, ``is_remote_frame`` and
- ``is_error_frame`` will be ignored.
-
.. note:: If the timestamp is a float value it will be converted
to an integer.
@@ -87,27 +124,44 @@ def send(self, msg, timeout=None):
used instead.
"""
+ # Pack timestamp
try:
- timestamp = struct.pack(' tuple[Message | None, bool]:
"""
Read a message from the serial device.
@@ -117,47 +171,72 @@ def _recv_internal(self, timeout):
This parameter will be ignored. The timeout value of the channel is used.
:returns:
- Received message and False (because not filtering as taken place).
-
- .. warning::
- Flags like extended_id, is_remote_frame and is_error_frame
- will not be set over this function, the flags in the return
- message are the default values.
-
- :rtype:
- can.Message, bool
+ Received message and :obj:`False` (because no filtering as taken place).
"""
try:
- # ser.read can return an empty string
- # or raise a SerialException
- rx_byte = self.ser.read()
- except serial.SerialException:
- return None, False
-
- if rx_byte and ord(rx_byte) == 0xAA:
- s = bytearray(self.ser.read(4))
- timestamp = (struct.unpack(' 8:
+ raise ValueError("received DLC may not exceed 8 bytes")
+
+ s = self._ser.read(4)
+ arbitration_id = struct.unpack(" int:
+ try:
+ return cast("int", self._ser.fileno())
+ except io.UnsupportedOperation:
+ raise NotImplementedError(
+ "fileno is not implemented using current CAN bus on this platform"
+ ) from None
+ except Exception as exception:
+ raise CanOperationError("Cannot fetch fileno") from exception
+
+ @staticmethod
+ def _detect_available_configs() -> Sequence[AutoDetectedConfig]:
+ configs: list[AutoDetectedConfig] = []
+ if serial is None:
+ return configs
+
+ for port in serial.tools.list_ports.comports():
+ configs.append({"interface": "serial", "channel": port.device})
+ return configs
diff --git a/can/interfaces/slcan.py b/can/interfaces/slcan.py
old mode 100755
new mode 100644
index d2a2fe82f..086d9ed32
--- a/can/interfaces/slcan.py
+++ b/can/interfaces/slcan.py
@@ -1,29 +1,37 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Interface for slcan compatible interfaces (win32/linux).
-
-.. note::
-
- Linux users can use slcand or socketcan as well.
-
"""
-from __future__ import absolute_import
-
-import time
+import io
import logging
-
-from can import BusABC, Message
+import time
+import warnings
+from queue import SimpleQueue
+from typing import Any, cast
+
+from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message, typechecking
+from can.exceptions import (
+ CanInitializationError,
+ CanInterfaceNotImplementedError,
+ CanOperationError,
+ error_check,
+)
+from can.util import (
+ CAN_FD_DLC,
+ check_or_adjust_timing_clock,
+ deprecated_args_alias,
+ len2dlc,
+)
logger = logging.getLogger(__name__)
try:
import serial
except ImportError:
- logger.warning("You won't be able to use the slcan can backend without "
- "the serial module installed!")
+ logger.warning(
+ "You won't be able to use the slcan can backend without "
+ "the serial module installed!"
+ )
serial = None
@@ -34,144 +42,391 @@ class slcanBus(BusABC):
# the supported bitrates and their commands
_BITRATES = {
- 10000: 'S0',
- 20000: 'S1',
- 50000: 'S2',
- 100000: 'S3',
- 125000: 'S4',
- 250000: 'S5',
- 500000: 'S6',
- 750000: 'S7',
- 1000000: 'S8',
- 83300: 'S9'
+ 10000: "S0",
+ 20000: "S1",
+ 50000: "S2",
+ 100000: "S3",
+ 125000: "S4",
+ 250000: "S5",
+ 500000: "S6",
+ 750000: "S7",
+ 1000000: "S8",
+ 83300: "S9",
+ }
+ _DATA_BITRATES = {
+ 0: "",
+ 2000000: "Y2",
+ 5000000: "Y5",
}
_SLEEP_AFTER_SERIAL_OPEN = 2 # in seconds
- def __init__(self, channel, ttyBaudrate=115200, bitrate=None,
- rtscts=False, **kwargs):
+ _OK = b"\r"
+ _ERROR = b"\a"
+
+ LINE_TERMINATOR = b"\r"
+
+ @deprecated_args_alias(
+ deprecation_start="4.5.0",
+ deprecation_end="5.0.0",
+ ttyBaudrate="tty_baudrate",
+ )
+ def __init__(
+ self,
+ channel: typechecking.ChannelStr,
+ tty_baudrate: int = 115200,
+ bitrate: int | None = None,
+ timing: BitTiming | BitTimingFd | None = None,
+ sleep_after_open: float = _SLEEP_AFTER_SERIAL_OPEN,
+ rtscts: bool = False,
+ listen_only: bool = False,
+ timeout: float = 0.001,
+ **kwargs: Any,
+ ) -> None:
"""
:param str channel:
- port of underlying serial or usb device (e.g. /dev/ttyUSB0, COM8, ...)
- Must not be empty.
- :param int ttyBaudrate:
- baudrate of underlying serial or usb device
- :param int bitrate:
+ port of underlying serial or usb device (e.g. ``/dev/ttyUSB0``, ``COM8``, ...)
+ Must not be empty. Can also end with ``@115200`` (or similarly) to specify the baudrate.
+ :param int tty_baudrate:
+ baudrate of underlying serial or usb device (Ignored if set via the ``channel`` parameter)
+ :param bitrate:
Bitrate in bit/s
- :param float poll_interval:
+ :param timing:
+ Optional :class:`~can.BitTiming` instance to use for custom bit timing setting.
+ If this argument is set then it overrides the bitrate and btr arguments. The
+ `f_clock` value of the timing instance must be set to 8_000_000 (8MHz)
+ for standard CAN.
+ CAN FD and the :class:`~can.BitTimingFd` class have partial support according to the non-standard
+ slcan protocol implementation in the CANABLE 2.0 firmware: currently only data rates of 2M and 5M.
+ :param poll_interval:
Poll interval in seconds when reading messages
- :param bool rtscts:
+ :param sleep_after_open:
+ Time to wait in seconds after opening serial connection
+ :param rtscts:
turn hardware handshake (RTS/CTS) on and off
+ :param listen_only:
+ If True, open interface/channel in listen mode with ``L`` command.
+ Otherwise, the (default) ``O`` command is still used. See ``open`` method.
+ :param timeout:
+ Timeout for the serial or usb device in seconds (default 0.001)
+
+ :raise ValueError: if both ``bitrate`` and ``btr`` are set or the channel is invalid
+ :raise CanInterfaceNotImplementedError: if the serial module is missing
+ :raise CanInitializationError: if the underlying serial connection could not be established
"""
+ self._listen_only = listen_only
- if not channel: # if None or empty
- raise TypeError("Must specify a serial port.")
+ if serial is None:
+ raise CanInterfaceNotImplementedError("The serial module is not installed")
- if '@' in channel:
- (channel, ttyBaudrate) = channel.split('@')
+ btr: str | None = kwargs.get("btr", None)
+ if btr is not None:
+ warnings.warn(
+ "The 'btr' argument is deprecated since python-can v4.5.0 "
+ "and scheduled for removal in v5.0.0. "
+ "Use the 'timing' argument instead.",
+ DeprecationWarning,
+ stacklevel=1,
+ )
- self.serialPortOrig = serial.serial_for_url(
- channel, baudrate=ttyBaudrate, rtscts=rtscts)
+ if not channel: # if None or empty
+ raise ValueError("Must specify a serial port.")
+ if "@" in channel:
+ (channel, baudrate) = channel.split("@")
+ tty_baudrate = int(baudrate)
+
+ with error_check(exception_type=CanInitializationError):
+ self.serialPortOrig = serial.serial_for_url(
+ channel,
+ baudrate=tty_baudrate,
+ rtscts=rtscts,
+ timeout=timeout,
+ )
+
+ self._queue: SimpleQueue[str] = SimpleQueue()
+ self._buffer = bytearray()
+ self._can_protocol = CanProtocol.CAN_20
+
+ time.sleep(sleep_after_open)
+
+ with error_check(exception_type=CanInitializationError):
+ if isinstance(timing, BitTiming):
+ timing = check_or_adjust_timing_clock(timing, valid_clocks=[8_000_000])
+ self.set_bitrate_reg(f"{timing.btr0:02X}{timing.btr1:02X}")
+ elif isinstance(timing, BitTimingFd):
+ self.set_bitrate(timing.nom_bitrate, timing.data_bitrate)
+ else:
+ if bitrate is not None and btr is not None:
+ raise ValueError("Bitrate and btr mutually exclusive.")
+ if bitrate is not None:
+ self.set_bitrate(bitrate)
+ if btr is not None:
+ self.set_bitrate_reg(btr)
+ self.open()
- time.sleep(self._SLEEP_AFTER_SERIAL_OPEN)
+ super().__init__(channel, **kwargs)
- if bitrate is not None:
- self.close()
- if bitrate in self._BITRATES:
- self.write(self._BITRATES[bitrate])
- else:
- raise ValueError("Invalid bitrate, choose one of " + (', '.join(self._BITRATES)) + '.')
+ def set_bitrate(self, bitrate: int, data_bitrate: int | None = None) -> None:
+ """
+ :param bitrate:
+ Bitrate in bit/s
+ :param data_bitrate:
+ Data Bitrate in bit/s for FD frames
- self.open()
+ :raise ValueError: if ``bitrate`` is not among the possible values
+ """
+ if bitrate in self._BITRATES:
+ bitrate_code = self._BITRATES[bitrate]
+ else:
+ bitrates = ", ".join(str(k) for k in self._BITRATES.keys())
+ raise ValueError(f"Invalid bitrate, choose one of {bitrates}.")
- super(slcanBus, self).__init__(channel, ttyBaudrate=115200,
- bitrate=None, rtscts=False, **kwargs)
+ # If data_bitrate is None, we set it to 0 which means no data bitrate
+ if data_bitrate is None:
+ data_bitrate = 0
- def write(self, string):
- if not string.endswith('\r'):
- string += '\r'
- self.serialPortOrig.write(string.encode())
- self.serialPortOrig.flush()
+ if data_bitrate in self._DATA_BITRATES:
+ dbitrate_code = self._DATA_BITRATES[data_bitrate]
+ else:
+ dbitrates = ", ".join(str(k) for k in self._DATA_BITRATES.keys())
+ raise ValueError(f"Invalid data bitrate, choose one of {dbitrates}.")
- def open(self):
- self.write('O')
+ self.close()
+ self._write(bitrate_code)
+ self._write(dbitrate_code)
+ self.open()
- def close(self):
- self.write('C')
+ def set_bitrate_reg(self, btr: str) -> None:
+ """
+ :param btr:
+ BTR register value to set custom can speed as a string `xxyy` where
+ xx is the BTR0 value in hex and yy is the BTR1 value in hex.
+ """
+ self.close()
+ self._write("s" + btr)
+ self.open()
+
+ def _write(self, string: str) -> None:
+ with error_check("Could not write to serial device"):
+ self.serialPortOrig.write(string.encode() + self.LINE_TERMINATOR)
+ self.serialPortOrig.flush()
+
+ def _read(self, timeout: float | None) -> str | None:
+ _timeout = serial.Timeout(timeout)
+
+ with error_check("Could not read from serial device"):
+ while True:
+ # Due to accessing `serialPortOrig.in_waiting` too often will reduce the performance.
+ # We read the `serialPortOrig.in_waiting` only once here.
+ in_waiting = self.serialPortOrig.in_waiting
+ for _ in range(max(1, in_waiting)):
+ new_byte = self.serialPortOrig.read(1)
+ if new_byte:
+ self._buffer.extend(new_byte)
+ else:
+ break
+
+ if new_byte in (self._ERROR, self._OK):
+ string = self._buffer.decode()
+ self._buffer.clear()
+ return string
+
+ if _timeout.expired():
+ break
+
+ return None
+
+ def flush(self) -> None:
+ self._buffer.clear()
+ with error_check("Could not flush"):
+ self.serialPortOrig.reset_input_buffer()
+
+ def open(self) -> None:
+ if self._listen_only:
+ self._write("L")
+ else:
+ self._write("O")
- def _recv_internal(self, timeout):
- if timeout != self.serialPortOrig.timeout:
- self.serialPortOrig.timeout = timeout
+ def close(self) -> None:
+ self._write("C")
+ def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]:
canId = None
remote = False
extended = False
- frame = []
+ data = None
+ isFd = False
+ fdBrs = False
- readStr = self.serialPortOrig.read_until(b'\r')
-
- if not readStr:
- return None, False
+ if self._queue.qsize():
+ string: str | None = self._queue.get_nowait()
else:
- readStr = readStr.decode()
- if readStr[0] == 'T':
- # extended frame
- canId = int(readStr[1:9], 16)
- dlc = int(readStr[9])
- extended = True
- for i in range(0, dlc):
- frame.append(int(readStr[10 + i * 2:12 + i * 2], 16))
- elif readStr[0] == 't':
- # normal frame
- canId = int(readStr[1:4], 16)
- dlc = int(readStr[4])
- for i in range(0, dlc):
- frame.append(int(readStr[5 + i * 2:7 + i * 2], 16))
- elif readStr[0] == 'r':
- # remote frame
- canId = int(readStr[1:4], 16)
- remote = True
- elif readStr[0] == 'R':
- # remote extended frame
- canId = int(readStr[1:9], 16)
- extended = True
- remote = True
-
- if canId is not None:
- msg = Message(arbitration_id=canId,
- extended_id=extended,
- timestamp=time.time(), # Better than nothing...
- is_remote_frame=remote,
- dlc=dlc,
- data=frame)
- return msg, False
- else:
- return None, False
-
- def send(self, msg, timeout=0):
+ string = self._read(timeout)
+
+ if not string:
+ pass
+ elif string[0] in (
+ "T",
+ "x", # x is an alternative extended message identifier for CANDapter
+ ):
+ # extended frame
+ canId = int(string[1:9], 16)
+ dlc = int(string[9])
+ extended = True
+ data = bytearray.fromhex(string[10 : 10 + dlc * 2])
+ elif string[0] == "t":
+ # normal frame
+ canId = int(string[1:4], 16)
+ dlc = int(string[4])
+ data = bytearray.fromhex(string[5 : 5 + dlc * 2])
+ elif string[0] == "r":
+ # remote frame
+ canId = int(string[1:4], 16)
+ dlc = int(string[4])
+ remote = True
+ elif string[0] == "R":
+ # remote extended frame
+ canId = int(string[1:9], 16)
+ dlc = int(string[9])
+ extended = True
+ remote = True
+ elif string[0] == "d":
+ # FD standard frame
+ canId = int(string[1:4], 16)
+ dlc = int(string[4], 16)
+ isFd = True
+ data = bytearray.fromhex(string[5 : 5 + CAN_FD_DLC[dlc] * 2])
+ elif string[0] == "D":
+ # FD extended frame
+ canId = int(string[1:9], 16)
+ dlc = int(string[9], 16)
+ extended = True
+ isFd = True
+ data = bytearray.fromhex(string[10 : 10 + CAN_FD_DLC[dlc] * 2])
+ elif string[0] == "b":
+ # FD with bitrate switch
+ canId = int(string[1:4], 16)
+ dlc = int(string[4], 16)
+ isFd = True
+ fdBrs = True
+ data = bytearray.fromhex(string[5 : 5 + CAN_FD_DLC[dlc] * 2])
+ elif string[0] == "B":
+ # FD extended with bitrate switch
+ canId = int(string[1:9], 16)
+ dlc = int(string[9], 16)
+ extended = True
+ isFd = True
+ fdBrs = True
+ data = bytearray.fromhex(string[10 : 10 + CAN_FD_DLC[dlc] * 2])
+
+ if canId is not None:
+ msg = Message(
+ arbitration_id=canId,
+ is_extended_id=extended,
+ timestamp=time.time(), # Better than nothing...
+ is_remote_frame=remote,
+ is_fd=isFd,
+ bitrate_switch=fdBrs,
+ dlc=CAN_FD_DLC[dlc],
+ data=data,
+ )
+ return msg, False
+ return None, False
+
+ def send(self, msg: Message, timeout: float | None = None) -> None:
if timeout != self.serialPortOrig.write_timeout:
self.serialPortOrig.write_timeout = timeout
-
if msg.is_remote_frame:
if msg.is_extended_id:
- sendStr = "R%08X0" % (msg.arbitration_id)
+ sendStr = f"R{msg.arbitration_id:08X}{msg.dlc:d}"
+ else:
+ sendStr = f"r{msg.arbitration_id:03X}{msg.dlc:d}"
+ elif msg.is_fd:
+ fd_dlc = len2dlc(msg.dlc)
+ if msg.bitrate_switch:
+ if msg.is_extended_id:
+ sendStr = f"B{msg.arbitration_id:08X}{fd_dlc:X}"
+ else:
+ sendStr = f"b{msg.arbitration_id:03X}{fd_dlc:X}"
+ sendStr += msg.data.hex().upper()
else:
- sendStr = "r%03X0" % (msg.arbitration_id)
+ if msg.is_extended_id:
+ sendStr = f"D{msg.arbitration_id:08X}{fd_dlc:X}"
+ else:
+ sendStr = f"d{msg.arbitration_id:03X}{fd_dlc:X}"
+ sendStr += msg.data.hex().upper()
else:
if msg.is_extended_id:
- sendStr = "T%08X%d" % (msg.arbitration_id, msg.dlc)
+ sendStr = f"T{msg.arbitration_id:08X}{msg.dlc:d}"
else:
- sendStr = "t%03X%d" % (msg.arbitration_id, msg.dlc)
+ sendStr = f"t{msg.arbitration_id:03X}{msg.dlc:d}"
+ sendStr += msg.data.hex().upper()
+ self._write(sendStr)
- for i in range(0, msg.dlc):
- sendStr += "%02X" % msg.data[i]
- self.write(sendStr)
-
- def shutdown(self):
+ def shutdown(self) -> None:
+ super().shutdown()
self.close()
-
- def fileno(self):
- if hasattr(self.serialPortOrig, 'fileno'):
- return self.serialPortOrig.fileno()
- # Return an invalid file descriptor on Windows
- return -1
+ with error_check("Could not close serial socket"):
+ self.serialPortOrig.close()
+
+ def fileno(self) -> int:
+ try:
+ return cast("int", self.serialPortOrig.fileno())
+ except io.UnsupportedOperation:
+ raise NotImplementedError(
+ "fileno is not implemented using current CAN bus on this platform"
+ ) from None
+ except Exception as exception:
+ raise CanOperationError("Cannot fetch fileno") from exception
+
+ def get_version(self, timeout: float | None) -> tuple[int | None, int | None]:
+ """Get HW and SW version of the slcan interface.
+
+ :param timeout:
+ seconds to wait for version or None to wait indefinitely
+
+ :returns: tuple (hw_version, sw_version)
+ WHERE
+ int hw_version is the hardware version or None on timeout
+ int sw_version is the software version or None on timeout
+ """
+ _timeout = serial.Timeout(timeout)
+ cmd = "V"
+ self._write(cmd)
+
+ while True:
+ if string := self._read(_timeout.time_left()):
+ if string[0] == cmd:
+ # convert ASCII coded version
+ hw_version = int(string[1:3])
+ sw_version = int(string[3:5])
+ return hw_version, sw_version
+ else:
+ self._queue.put_nowait(string)
+ if _timeout.expired():
+ break
+ return None, None
+
+ def get_serial_number(self, timeout: float | None) -> str | None:
+ """Get serial number of the slcan interface.
+
+ :param timeout:
+ seconds to wait for serial number or :obj:`None` to wait indefinitely
+
+ :return:
+ :obj:`None` on timeout or a :class:`str` object.
+ """
+ _timeout = serial.Timeout(timeout)
+ cmd = "N"
+ self._write(cmd)
+
+ while True:
+ if string := self._read(_timeout.time_left()):
+ if string[0] == cmd:
+ serial_number = string[1:-1]
+ return serial_number
+ else:
+ self._queue.put_nowait(string)
+ if _timeout.expired():
+ break
+ return None
diff --git a/can/interfaces/socketcan/__init__.py b/can/interfaces/socketcan/__init__.py
index 338946136..6e279c2fe 100644
--- a/can/interfaces/socketcan/__init__.py
+++ b/can/interfaces/socketcan/__init__.py
@@ -1,8 +1,14 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
See: https://www.kernel.org/doc/Documentation/networking/can.txt
"""
-from can.interfaces.socketcan.socketcan import SocketcanBus, CyclicSendTask, MultiRateCyclicSendTask
+__all__ = [
+ "CyclicSendTask",
+ "MultiRateCyclicSendTask",
+ "SocketcanBus",
+ "constants",
+ "socketcan",
+ "utils",
+]
+
+from .socketcan import CyclicSendTask, MultiRateCyclicSendTask, SocketcanBus
diff --git a/can/interfaces/socketcan/constants.py b/can/interfaces/socketcan/constants.py
index dc1b85ec3..941d52573 100644
--- a/can/interfaces/socketcan/constants.py
+++ b/can/interfaces/socketcan/constants.py
@@ -1,91 +1,75 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Defines shared CAN constants.
"""
-canMSG_EXT = 0x0004
+# Generic socket constants
+SO_TIMESTAMPNS = 35
-CAN_ERR_FLAG = 0x20000000
-CAN_RTR_FLAG = 0x40000000
-CAN_EFF_FLAG = 0x80000000
+CAN_ERR_FLAG = 0x20000000
+CAN_RTR_FLAG = 0x40000000
+CAN_EFF_FLAG = 0x80000000
# BCM opcodes
-CAN_BCM_TX_SETUP = 1
-CAN_BCM_TX_DELETE = 2
-
-CAN_BCM_TX_EXPIRED = 9
-
-CAN_BCM_RX_TIMEOUT = 11
+CAN_BCM_TX_SETUP = 1
+CAN_BCM_TX_DELETE = 2
+CAN_BCM_TX_READ = 3
# BCM flags
-SETTIMER = 0x0001
-STARTTIMER = 0x0002
-TX_COUNTEVT = 0x0004
-TX_ANNOUNCE = 0x0008
-TX_CP_CAN_ID = 0x0010
-RX_FILTER_ID = 0x0020
-RX_CHECK_DLC = 0x0040
-RX_NO_AUTOTIMER = 0x0080
-RX_ANNOUNCE_RESUME = 0x0100
-TX_RESET_MULTI_IDX = 0x0200
-RX_RTR_FRAME = 0x0400
-CAN_FD_FRAME = 0x0800
-
-CAN_RAW = 1
-CAN_BCM = 2
-
-SOL_CAN_BASE = 100
-SOL_CAN_RAW = SOL_CAN_BASE + CAN_RAW
-
-CAN_RAW_FILTER = 1
-CAN_RAW_ERR_FILTER = 2
-CAN_RAW_LOOPBACK = 3
+SETTIMER = 0x0001
+STARTTIMER = 0x0002
+TX_COUNTEVT = 0x0004
+TX_ANNOUNCE = 0x0008
+TX_CP_CAN_ID = 0x0010
+RX_FILTER_ID = 0x0020
+RX_CHECK_DLC = 0x0040
+RX_NO_AUTOTIMER = 0x0080
+RX_ANNOUNCE_RESUME = 0x0100
+TX_RESET_MULTI_IDX = 0x0200
+RX_RTR_FRAME = 0x0400
+CAN_FD_FRAME = 0x0800
+
+CAN_RAW = 1
+CAN_BCM = 2
+
+SOL_CAN_BASE = 100
+SOL_CAN_RAW = SOL_CAN_BASE + CAN_RAW
+
+CAN_RAW_FILTER = 1
+CAN_RAW_ERR_FILTER = 2
+CAN_RAW_LOOPBACK = 3
CAN_RAW_RECV_OWN_MSGS = 4
-CAN_RAW_FD_FRAMES = 5
+CAN_RAW_FD_FRAMES = 5
-MSK_ARBID = 0x1FFFFFFF
-MSK_FLAGS = 0xE0000000
+MSK_ARBID = 0x1FFFFFFF
+MSK_FLAGS = 0xE0000000
-PF_CAN = 29
-SOCK_RAW = 3
-SOCK_DGRAM = 2
-AF_CAN = PF_CAN
+PF_CAN = 29
+SOCK_RAW = 3
+SOCK_DGRAM = 2
+AF_CAN = PF_CAN
-SIOCGIFNAME = 0x8910
-SIOCGIFINDEX = 0x8933
-SIOCGSTAMP = 0x8906
-EXTFLG = 0x0004
+SIOCGIFNAME = 0x8910
+SIOCGIFINDEX = 0x8933
+SIOCGSTAMP = 0x8906
+EXTFLG = 0x0004
-SKT_ERRFLG = 0x0001
-SKT_RTRFLG = 0x0002
+CANFD_BRS = 0x01 # bit rate switch (second bitrate for payload data)
+CANFD_ESI = 0x02 # error state indicator of the transmitting node
+CANFD_FDF = 0x04 # mark CAN FD for dual use of struct canfd_frame
-CANFD_BRS = 0x01
-CANFD_ESI = 0x02
-
-CANFD_MTU = 72
+# CAN payload length and DLC definitions according to ISO 11898-1
+CAN_MAX_DLC = 8
+CAN_MAX_RAW_DLC = 15
+CAN_MAX_DLEN = 8
-PYCAN_ERRFLG = 0x0020
-PYCAN_STDFLG = 0x0002
-PYCAN_RTRFLG = 0x0001
+# CAN FD payload length and DLC definitions according to ISO 11898-7
+CANFD_MAX_DLC = 15
+CANFD_MAX_DLEN = 64
-ID_TYPE_EXTENDED = True
-ID_TYPE_STANDARD = False
-
-ID_TYPE_29_BIT = ID_TYPE_EXTENDED
-ID_TYPE_11_BIT = ID_TYPE_STANDARD
-
-REMOTE_FRAME = True
-DATA_FRAME = False
-WAKEUP_MSG = True
-ERROR_FRAME = True
-
-DRIVER_MODE_SILENT = False
-DRIVER_MODE_NORMAL = (not DRIVER_MODE_SILENT)
+CANFD_MTU = 72
-STD_ACCEPTANCE_MASK_ALL_BITS = (2**11 - 1)
+STD_ACCEPTANCE_MASK_ALL_BITS = 2**11 - 1
MAX_11_BIT_ID = STD_ACCEPTANCE_MASK_ALL_BITS
-EXT_ACCEPTANCE_MASK_ALL_BITS = (2**29 - 1)
+EXT_ACCEPTANCE_MASK_ALL_BITS = 2**29 - 1
MAX_29_BIT_ID = EXT_ACCEPTANCE_MASK_ALL_BITS
diff --git a/can/interfaces/socketcan/socketcan.py b/can/interfaces/socketcan/socketcan.py
index 1e3e64ae6..6dc856cbf 100644
--- a/can/interfaces/socketcan/socketcan.py
+++ b/can/interfaces/socketcan/socketcan.py
@@ -1,71 +1,135 @@
-#!/usr/bin/env python
-# coding: utf-8
-import logging
+"""
+The main module of the socketcan interface containing most user-facing classes and methods
+along some internal methods.
+
+At the end of the file the usage of the internal methods is shown.
+"""
import ctypes
import ctypes.util
-import os
+import errno
+import logging
import select
import socket
import struct
+import threading
import time
-import errno
+import warnings
+from collections.abc import Callable, Sequence
+
+import can
+from can import BusABC, CanProtocol, Message
+from can.broadcastmanager import (
+ LimitedDurationCyclicSendTaskABC,
+ ModifiableCyclicTaskABC,
+ RestartableCyclicTaskABC,
+)
+from can.interfaces.socketcan import constants
+from can.interfaces.socketcan.utils import find_available_interfaces, pack_filters
+from can.typechecking import CanFilters
log = logging.getLogger(__name__)
log_tx = log.getChild("tx")
log_rx = log.getChild("rx")
-log.debug("Loading socketcan native backend")
-
-try:
- import fcntl
-except ImportError:
- log.error("fcntl not available on this platform")
-
-
-import can
-from can import Message, BusABC
-from can.broadcastmanager import ModifiableCyclicTaskABC, \
- RestartableCyclicTaskABC, LimitedDurationCyclicSendTaskABC
-from can.interfaces.socketcan.constants import * # CAN_RAW, CAN_*_FLAG
-from can.interfaces.socketcan.utils import \
- pack_filters, find_available_interfaces, error_code_to_str
-
-
try:
- socket.CAN_BCM
-except AttributeError:
- HAS_NATIVE_SUPPORT = False
-else:
- HAS_NATIVE_SUPPORT = True
-
-
-if not HAS_NATIVE_SUPPORT:
- def check_status(result, function, arguments):
- if result < 0:
- raise can.CanError(error_code_to_str(ctypes.get_errno()))
- return result
+ from socket import CMSG_SPACE
- try:
- libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
- libc.bind.errcheck = check_status
- libc.connect.errcheck = check_status
- libc.sendto.errcheck = check_status
- libc.recvfrom.errcheck = check_status
- except:
- log.warning("libc is unavailable")
- libc = None
-
- def get_addr(sock, channel):
- """Get sockaddr for a channel."""
- if channel:
- data = struct.pack("16si", channel.encode(), 0)
- res = fcntl.ioctl(sock, SIOCGIFINDEX, data)
- idx, = struct.unpack("16xi", res)
- else:
- # All channels
- idx = 0
- return struct.pack("HiLL", AF_CAN, idx, 0, 0)
+ CMSG_SPACE_available = True
+except ImportError:
+ CMSG_SPACE_available = False
+ log.error("socket.CMSG_SPACE not available on this platform")
+
+
+# Constants needed for precise handling of timestamps
+RECEIVED_TIMESTAMP_STRUCT = struct.Struct("@ll")
+RECEIVED_ANCILLARY_BUFFER_SIZE = (
+ CMSG_SPACE(RECEIVED_TIMESTAMP_STRUCT.size) if CMSG_SPACE_available else 0
+)
+
+
+# Setup BCM struct
+def bcm_header_factory(
+ fields: list[tuple[str, type[ctypes.c_uint32] | type[ctypes.c_long]]],
+ alignment: int = 8,
+):
+ curr_stride = 0
+ results: list[
+ tuple[str, type[ctypes.c_uint8] | type[ctypes.c_uint32] | type[ctypes.c_long]]
+ ] = []
+ pad_index = 0
+ for field in fields:
+ field_alignment = ctypes.alignment(field[1])
+ field_size = ctypes.sizeof(field[1])
+
+ # If the current stride index isn't a multiple of the alignment
+ # requirements of this field, then we must add padding bytes until we
+ # are aligned
+ while curr_stride % field_alignment != 0:
+ results.append((f"pad_{pad_index}", ctypes.c_uint8))
+ pad_index += 1
+ curr_stride += 1
+
+ # Now can it fit?
+ # Example: If this is 8 bytes and the type requires 4 bytes alignment
+ # then we can only fit when we're starting at 0. Otherwise, we will
+ # split across 2 strides.
+ #
+ # | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+ results.append(field)
+ curr_stride += field_size
+
+ # Add trailing padding to align to a multiple of the largest scalar member
+ # in the structure
+ while curr_stride % alignment != 0:
+ results.append((f"pad_{pad_index}", ctypes.c_uint8))
+ pad_index += 1
+ curr_stride += 1
+
+ return type("BcmMsgHead", (ctypes.Structure,), {"_fields_": results})
+
+
+# The fields definition is taken from the C struct definitions in
+#
+#
+# struct bcm_timeval {
+# long tv_sec;
+# long tv_usec;
+# };
+#
+# /**
+# * struct bcm_msg_head - head of messages to/from the broadcast manager
+# * @opcode: opcode, see enum below.
+# * @flags: special flags, see below.
+# * @count: number of frames to send before changing interval.
+# * @ival1: interval for the first @count frames.
+# * @ival2: interval for the following frames.
+# * @can_id: CAN ID of frames to be sent or received.
+# * @nframes: number of frames appended to the message head.
+# * @frames: array of CAN frames.
+# */
+# struct bcm_msg_head {
+# __u32 opcode;
+# __u32 flags;
+# __u32 count;
+# struct bcm_timeval ival1, ival2;
+# canid_t can_id;
+# __u32 nframes;
+# struct can_frame frames[0];
+# };
+BcmMsgHead = bcm_header_factory(
+ fields=[
+ ("opcode", ctypes.c_uint32),
+ ("flags", ctypes.c_uint32),
+ ("count", ctypes.c_uint32),
+ ("ival1_tv_sec", ctypes.c_long),
+ ("ival1_tv_usec", ctypes.c_long),
+ ("ival2_tv_sec", ctypes.c_long),
+ ("ival2_tv_usec", ctypes.c_long),
+ ("can_id", ctypes.c_uint32),
+ ("nframes", ctypes.c_uint32),
+ ]
+)
# struct module defines a binary packing format:
@@ -73,23 +137,68 @@ def get_addr(sock, channel):
# The 32bit can id is directly followed by the 8bit data link count
# The data field is aligned on an 8 byte boundary, hence we add padding
# which aligns the data field to an 8 byte boundary.
-CAN_FRAME_HEADER_STRUCT = struct.Struct("=IBB2x")
+CAN_FRAME_HEADER_STRUCT = struct.Struct("=IBB1xB")
-def build_can_frame(msg):
- """ CAN frame packing/unpacking (see 'struct can_frame' in )
+def build_can_frame(msg: Message) -> bytes:
+ """CAN frame packing/unpacking (see 'struct can_frame' in )
/**
- * struct can_frame - basic CAN frame structure
- * @can_id: the CAN ID of the frame and CAN_*_FLAG flags, see above.
- * @can_dlc: the data length field of the CAN frame
- * @data: the CAN frame payload.
- */
+ * struct can_frame - Classical CAN frame structure (aka CAN 2.0B)
+ * @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition
+ * @len: CAN frame payload length in byte (0 .. 8)
+ * @can_dlc: deprecated name for CAN frame payload length in byte (0 .. 8)
+ * @__pad: padding
+ * @__res0: reserved / padding
+ * @len8_dlc: optional DLC value (9 .. 15) at 8 byte payload length
+ * len8_dlc contains values from 9 .. 15 when the payload length is
+ * 8 bytes but the DLC value (see ISO 11898-1) is greater then 8.
+ * CAN_CTRLMODE_CC_LEN8_DLC flag has to be enabled in CAN driver.
+ * @data: CAN frame payload (up to 8 byte)
+ */
struct can_frame {
canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */
- __u8 can_dlc; /* data length code: 0 .. 8 */
- __u8 data[8] __attribute__((aligned(8)));
+ union {
+ /* CAN frame payload length in byte (0 .. CAN_MAX_DLEN)
+ * was previously named can_dlc so we need to carry that
+ * name for legacy support
+ */
+ __u8 len;
+ __u8 can_dlc; /* deprecated */
+ } __attribute__((packed)); /* disable padding added in some ABIs */
+ __u8 __pad; /* padding */
+ __u8 __res0; /* reserved / padding */
+ __u8 len8_dlc; /* optional DLC for 8 byte payload length (9 .. 15) */
+ __u8 data[CAN_MAX_DLEN] __attribute__((aligned(8)));
};
+ /*
+ * defined bits for canfd_frame.flags
+ *
+ * The use of struct canfd_frame implies the FD Frame (FDF) bit to
+ * be set in the CAN frame bitstream on the wire. The FDF bit switch turns
+ * the CAN controllers bitstream processor into the CAN FD mode which creates
+ * two new options within the CAN FD frame specification:
+ *
+ * Bit Rate Switch - to indicate a second bitrate is/was used for the payload
+ * Error State Indicator - represents the error state of the transmitting node
+ *
+ * As the CANFD_ESI bit is internally generated by the transmitting CAN
+ * controller only the CANFD_BRS bit is relevant for real CAN controllers when
+ * building a CAN FD frame for transmission. Setting the CANFD_ESI bit can make
+ * sense for virtual CAN interfaces to test applications with echoed frames.
+ *
+ * The struct can_frame and struct canfd_frame intentionally share the same
+ * layout to be able to write CAN frame content into a CAN FD frame structure.
+ * When this is done the former differentiation via CAN_MTU / CANFD_MTU gets
+ * lost. CANFD_FDF allows programmers to mark CAN FD frames in the case of
+ * using struct canfd_frame for mixed CAN / CAN FD content (dual use).
+ * Since the introduction of CAN XL the CANFD_FDF flag is set in all CAN FD
+ * frame structures provided by the CAN subsystem of the Linux kernel.
+ */
+ #define CANFD_BRS 0x01 /* bit rate switch (second bitrate for payload data) */
+ #define CANFD_ESI 0x02 /* error state indicator of the transmitting node */
+ #define CANFD_FDF 0x04 /* mark CAN FD for dual use of struct canfd_frame */
+
/**
* struct canfd_frame - CAN flexible data rate frame structure
* @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition
@@ -108,56 +217,80 @@ def build_can_frame(msg):
__u8 data[CANFD_MAX_DLEN] __attribute__((aligned(8)));
};
"""
- can_id = _add_flags_to_can_id(msg)
+ can_id = _compose_arbitration_id(msg)
+
flags = 0
+
+ # The socketcan code identify the received FD frame by the packet length.
+ # So, padding to the data length is performed according to the message type (Classic / FD)
+ if msg.is_fd:
+ flags |= constants.CANFD_FDF
+ max_len = constants.CANFD_MAX_DLEN
+ else:
+ max_len = constants.CAN_MAX_DLEN
+
if msg.bitrate_switch:
- flags |= CANFD_BRS
+ flags |= constants.CANFD_BRS
if msg.error_state_indicator:
- flags |= CANFD_ESI
- max_len = 64 if msg.is_fd else 8
- data = bytes(msg.data).ljust(max_len, b'\x00')
- return CAN_FRAME_HEADER_STRUCT.pack(can_id, msg.dlc, flags) + data
-
-
-def build_bcm_header(opcode, flags, count, ival1_seconds, ival1_usec, ival2_seconds, ival2_usec, can_id, nframes):
- # == Must use native not standard types for packing ==
- # struct bcm_msg_head {
- # __u32 opcode; -> I
- # __u32 flags; -> I
- # __u32 count; -> I
- # struct timeval ival1, ival2; -> llll ...
- # canid_t can_id; -> I
- # __u32 nframes; -> I
- bcm_cmd_msg_fmt = "@3I4l2I0q"
-
- return struct.pack(bcm_cmd_msg_fmt,
- opcode,
- flags,
- count,
- ival1_seconds,
- ival1_usec,
- ival2_seconds,
- ival2_usec,
- can_id,
- nframes)
-
-
-def build_bcm_tx_delete_header(can_id, flags):
- opcode = CAN_BCM_TX_DELETE
+ flags |= constants.CANFD_ESI
+
+ data = bytes(msg.data).ljust(max_len, b"\x00")
+
+ if msg.is_remote_frame:
+ data_len = msg.dlc
+ else:
+ data_len = min(i for i in can.util.CAN_FD_DLC if i >= len(msg.data))
+ header = CAN_FRAME_HEADER_STRUCT.pack(can_id, data_len, flags, msg.dlc)
+ return header + data
+
+
+def build_bcm_header(
+ opcode: int,
+ flags: int,
+ count: int,
+ ival1_seconds: int,
+ ival1_usec: int,
+ ival2_seconds: int,
+ ival2_usec: int,
+ can_id: int,
+ nframes: int,
+) -> bytes:
+ result = BcmMsgHead(
+ opcode=opcode,
+ flags=flags,
+ count=count,
+ ival1_tv_sec=ival1_seconds,
+ ival1_tv_usec=ival1_usec,
+ ival2_tv_sec=ival2_seconds,
+ ival2_tv_usec=ival2_usec,
+ can_id=can_id,
+ nframes=nframes,
+ )
+ return ctypes.string_at(ctypes.addressof(result), ctypes.sizeof(result))
+
+
+def build_bcm_tx_delete_header(can_id: int, flags: int) -> bytes:
+ opcode = constants.CAN_BCM_TX_DELETE
return build_bcm_header(opcode, flags, 0, 0, 0, 0, 0, can_id, 1)
-def build_bcm_transmit_header(can_id, count, initial_period, subsequent_period,
- msg_flags):
- opcode = CAN_BCM_TX_SETUP
+def build_bcm_transmit_header(
+ can_id: int,
+ count: int,
+ initial_period: float,
+ subsequent_period: float,
+ msg_flags: int,
+ nframes: int = 1,
+) -> bytes:
+ opcode = constants.CAN_BCM_TX_SETUP
- flags = msg_flags | SETTIMER | STARTTIMER
+ flags = msg_flags | constants.SETTIMER | constants.STARTTIMER
if initial_period > 0:
# Note `TX_COUNTEVT` creates the message TX_EXPIRED when count expires
- flags |= TX_COUNTEVT
+ flags |= constants.TX_COUNTEVT
- def split_time(value):
+ def split_time(value: float) -> tuple[int, int]:
"""Given seconds as a float, return whole seconds and microseconds"""
seconds = int(value)
microseconds = int(1e6 * (value - seconds))
@@ -165,361 +298,552 @@ def split_time(value):
ival1_seconds, ival1_usec = split_time(initial_period)
ival2_seconds, ival2_usec = split_time(subsequent_period)
- nframes = 1
- return build_bcm_header(opcode, flags, count, ival1_seconds, ival1_usec, ival2_seconds, ival2_usec, can_id, nframes)
+ return build_bcm_header(
+ opcode,
+ flags,
+ count,
+ ival1_seconds,
+ ival1_usec,
+ ival2_seconds,
+ ival2_usec,
+ can_id,
+ nframes,
+ )
+
+
+def build_bcm_update_header(can_id: int, msg_flags: int, nframes: int = 1) -> bytes:
+ return build_bcm_header(
+ constants.CAN_BCM_TX_SETUP, msg_flags, 0, 0, 0, 0, 0, can_id, nframes
+ )
+
+def is_frame_fd(frame: bytes):
+ # According to the SocketCAN implementation the frame length
+ # should indicate if the message is FD or not (not the flag value)
+ return len(frame) == constants.CANFD_MTU
-def build_bcm_update_header(can_id, msg_flags):
- return build_bcm_header(CAN_BCM_TX_SETUP, msg_flags, 0, 0, 0, 0, 0, can_id, 1)
+def dissect_can_frame(frame: bytes) -> tuple[int, int, int, bytes]:
+ can_id, data_len, flags, len8_dlc = CAN_FRAME_HEADER_STRUCT.unpack_from(frame)
-def dissect_can_frame(frame):
- can_id, can_dlc, flags = CAN_FRAME_HEADER_STRUCT.unpack_from(frame)
- if len(frame) != CANFD_MTU:
+ if data_len not in can.util.CAN_FD_DLC:
+ data_len = min(i for i in can.util.CAN_FD_DLC if i >= data_len)
+
+ can_dlc = data_len
+
+ if not is_frame_fd(frame):
# Flags not valid in non-FD frames
flags = 0
- return can_id, can_dlc, flags, frame[8:8+can_dlc]
+ if (
+ data_len == constants.CAN_MAX_DLEN
+ and constants.CAN_MAX_DLEN < len8_dlc <= constants.CAN_MAX_RAW_DLC
+ ):
+ can_dlc = len8_dlc
+
+ return can_id, can_dlc, flags, frame[8 : 8 + data_len]
-def create_bcm_socket(channel):
+
+def create_bcm_socket(channel: str) -> socket.socket:
"""create a broadcast manager socket and connect to the given interface"""
- s = socket.socket(PF_CAN, socket.SOCK_DGRAM, CAN_BCM)
- if HAS_NATIVE_SUPPORT:
- s.connect((channel,))
- else:
- addr = get_addr(s, channel)
- libc.connect(s.fileno(), addr, len(addr))
+ s = socket.socket(constants.PF_CAN, socket.SOCK_DGRAM, constants.CAN_BCM)
+ s.connect((channel,))
return s
-def send_bcm(bcm_socket, data):
+def send_bcm(bcm_socket: socket.socket, data: bytes) -> int:
"""
Send raw frame to a BCM socket and handle errors.
"""
try:
return bcm_socket.send(data)
- except OSError as e:
- base = "Couldn't send CAN BCM frame. OS Error {}: {}\n".format(e.errno, e.strerror)
-
- if e.errno == errno.EINVAL:
- raise can.CanError(base + "You are probably referring to a non-existing frame.")
+ except OSError as error:
+ base = f"Couldn't send CAN BCM frame due to OS Error: {error.strerror}"
+
+ if error.errno == errno.EINVAL:
+ specific_message = " You are probably referring to a non-existing frame."
+ elif error.errno == errno.ENETDOWN:
+ specific_message = " The CAN interface appears to be down."
+ elif error.errno == errno.EBADF:
+ specific_message = " The CAN socket appears to be closed."
+ else:
+ specific_message = ""
- elif e.errno == errno.ENETDOWN:
- raise can.CanError(base + "The CAN interface appears to be down.")
+ raise can.CanOperationError(base + specific_message, error.errno) from error
- elif e.errno == errno.EBADF:
- raise can.CanError(base + "The CAN socket appears to be closed.")
- else:
- raise e
-
-def _add_flags_to_can_id(message):
+def _compose_arbitration_id(message: Message) -> int:
can_id = message.arbitration_id
if message.is_extended_id:
log.debug("sending an extended id type message")
- can_id |= CAN_EFF_FLAG
+ can_id |= constants.CAN_EFF_FLAG
if message.is_remote_frame:
log.debug("requesting a remote frame")
- can_id |= CAN_RTR_FLAG
+ can_id |= constants.CAN_RTR_FLAG
if message.is_error_frame:
log.debug("sending error frame")
- can_id |= CAN_ERR_FLAG
-
+ can_id |= constants.CAN_ERR_FLAG
return can_id
-class CyclicSendTask(LimitedDurationCyclicSendTaskABC,
- ModifiableCyclicTaskABC, RestartableCyclicTaskABC):
+class CyclicSendTask(
+ LimitedDurationCyclicSendTaskABC, ModifiableCyclicTaskABC, RestartableCyclicTaskABC
+):
"""
- A socketcan cyclic send task supports:
+ A SocketCAN cyclic send task supports:
- setting of a task duration
- modifying the data
- stopping then subsequent restarting of the task
-
"""
- def __init__(self, channel, message, period, duration=None):
- """
- :param str channel: The name of the CAN channel to connect to.
- :param can.Message message: The message to be sent periodically.
- :param float period: The rate in seconds at which to send the message.
- :param float duration: Approximate duration in seconds to send the message.
+ def __init__(
+ self,
+ bcm_socket: socket.socket,
+ task_id: int,
+ messages: Sequence[Message] | Message,
+ period: float,
+ duration: float | None = None,
+ autostart: bool = True,
+ ) -> None:
+ """Construct and :meth:`~start` a task.
+
+ :param bcm_socket: An open BCM socket on the desired CAN channel.
+ :param task_id:
+ The identifier used to uniquely reference particular cyclic send task
+ within Linux BCM.
+ :param messages:
+ The messages to be sent periodically.
+ :param period:
+ The rate in seconds at which to send the messages.
+ :param duration:
+ Approximate duration in seconds to send the messages for.
"""
- super(CyclicSendTask, self).__init__(message, period, duration)
- self.channel = channel
- self.duration = duration
- self._tx_setup(message)
- self.message = message
-
- def _tx_setup(self, message):
- self.bcm_socket = create_bcm_socket(self.channel)
+ # The following are assigned by LimitedDurationCyclicSendTaskABC:
+ # - self.messages
+ # - self.period
+ # - self.duration
+ super().__init__(messages, period, duration)
+
+ self.bcm_socket = bcm_socket
+ self.task_id = task_id
+ if autostart:
+ self._tx_setup(self.messages)
+
+ def _tx_setup(
+ self,
+ messages: Sequence[Message],
+ raise_if_task_exists: bool = True,
+ ) -> None:
# Create a low level packed frame to pass to the kernel
- self.can_id_with_flags = _add_flags_to_can_id(message)
- self.flags = CAN_FD_FRAME if message.is_fd else 0
+ body = bytearray()
+ self.flags = constants.CAN_FD_FRAME if messages[0].is_fd else 0
+
if self.duration:
count = int(self.duration / self.period)
ival1 = self.period
- ival2 = 0
+ ival2 = 0.0
else:
count = 0
- ival1 = 0
+ ival1 = 0.0
ival2 = self.period
- header = build_bcm_transmit_header(self.can_id_with_flags, count, ival1,
- ival2, self.flags)
- frame = build_can_frame(message)
+
+ if raise_if_task_exists:
+ self._check_bcm_task()
+
+ header = build_bcm_transmit_header(
+ self.task_id, count, ival1, ival2, self.flags, nframes=len(messages)
+ )
+ for message in messages:
+ body += build_can_frame(message)
log.debug("Sending BCM command")
- send_bcm(self.bcm_socket, header + frame)
+ send_bcm(self.bcm_socket, header + body)
+
+ def _check_bcm_task(self) -> None:
+ # Do a TX_READ on a task ID, and check if we get EINVAL. If so,
+ # then we are referring to a CAN message with an existing ID
+ check_header = build_bcm_header(
+ opcode=constants.CAN_BCM_TX_READ,
+ flags=0,
+ count=0,
+ ival1_seconds=0,
+ ival1_usec=0,
+ ival2_seconds=0,
+ ival2_usec=0,
+ can_id=self.task_id,
+ nframes=0,
+ )
+ log.debug(
+ "Reading properties of (cyclic) transmission task id=%d", self.task_id
+ )
+ try:
+ self.bcm_socket.send(check_header)
+ except OSError as error:
+ if error.errno != errno.EINVAL:
+ raise can.CanOperationError("failed to check", error.errno) from error
+ else:
+ log.debug("Invalid argument - transmission task not known to kernel")
+ else:
+ # No exception raised - transmission task with this ID exists in kernel.
+ # Existence of an existing transmission task might not be a problem!
+ raise can.CanOperationError(
+ f"A periodic task for task ID {self.task_id} is already in progress "
+ "by the SocketCAN Linux layer"
+ )
- def stop(self):
- """Send a TX_DELETE message to cancel this task.
+ def stop(self) -> None:
+ """Stop a task by sending TX_DELETE message to Linux kernel.
This will delete the entry for the transmission of the CAN-message
- with the specified can_id CAN identifier. The message length for the command
- TX_DELETE is {[bcm_msg_head]} (only the header).
+ with the specified ``task_id`` identifier. The message length
+ for the command TX_DELETE is {[bcm_msg_head]} (only the header).
"""
log.debug("Stopping periodic task")
- stopframe = build_bcm_tx_delete_header(self.can_id_with_flags, self.flags)
+ stopframe = build_bcm_tx_delete_header(self.task_id, self.flags)
send_bcm(self.bcm_socket, stopframe)
- self.bcm_socket.close()
- def modify_data(self, message):
- """Update the contents of this periodically sent message.
+ def modify_data(self, messages: Sequence[Message] | Message) -> None:
+ """Update the contents of the periodically sent CAN messages by
+ sending TX_SETUP message to Linux kernel.
- Note the Message must have the same :attr:`~can.Message.arbitration_id`
- like the first message.
+ The number of new cyclic messages to be sent must be equal to the
+ original number of messages originally specified for this task.
+
+ .. note:: The messages must all have the same
+ :attr:`~can.Message.arbitration_id` like the first message.
+
+ :param messages:
+ The messages with the new :attr:`can.Message.data`.
"""
- assert message.arbitration_id == self.can_id, "You cannot modify the can identifier"
- self.message = message
- header = build_bcm_update_header(self.can_id_with_flags, self.flags)
- frame = build_can_frame(message)
- send_bcm(self.bcm_socket, header + frame)
+ messages = self._check_and_convert_messages(messages)
+ self._check_modified_messages(messages)
- def start(self):
- self._tx_setup(self.message)
+ self.messages = messages
+ body = bytearray()
+ header = build_bcm_update_header(
+ can_id=self.task_id, msg_flags=self.flags, nframes=len(messages)
+ )
+ for message in messages:
+ body += build_can_frame(message)
+ log.debug("Sending BCM command")
+ send_bcm(self.bcm_socket, header + body)
-class MultiRateCyclicSendTask(CyclicSendTask):
- """Exposes more of the full power of the TX_SETUP opcode.
+ def start(self) -> None:
+ """Restart a periodic task by sending TX_SETUP message to Linux kernel.
- Transmits a message `count` times at `initial_period` then
- continues to transmit message at `subsequent_period`.
- """
+ It verifies presence of the particular BCM task through sending TX_READ
+ message to Linux kernel prior to scheduling.
- def __init__(self, channel, message, count, initial_period, subsequent_period):
- super(MultiRateCyclicSendTask, self).__init__(channel, message, subsequent_period)
+ :raises ValueError:
+ If the task referenced by ``task_id`` is already running.
+ """
+ self._tx_setup(self.messages, raise_if_task_exists=False)
+
+
+class MultiRateCyclicSendTask(CyclicSendTask):
+ """Exposes more of the full power of the TX_SETUP opcode."""
+
+ def __init__(
+ self,
+ channel: socket.socket,
+ task_id: int,
+ messages: Sequence[Message],
+ count: int,
+ initial_period: float,
+ subsequent_period: float,
+ ):
+ super().__init__(channel, task_id, messages, subsequent_period)
# Create a low level packed frame to pass to the kernel
- frame = build_can_frame(message)
header = build_bcm_transmit_header(
- self.can_id_with_flags,
+ self.task_id,
count,
initial_period,
subsequent_period,
- self.flags)
+ self.flags,
+ nframes=len(messages),
+ )
+
+ body = bytearray()
+ for message in messages:
+ body += build_can_frame(message)
log.info("Sending BCM TX_SETUP command")
- send_bcm(self.bcm_socket, header + frame)
+ send_bcm(self.bcm_socket, header + body)
-def create_socket():
+def create_socket() -> socket.socket:
"""Creates a raw CAN socket. The socket will
be returned unbound to any interface.
"""
- sock = socket.socket(PF_CAN, socket.SOCK_RAW, CAN_RAW)
+ sock = socket.socket(constants.PF_CAN, socket.SOCK_RAW, constants.CAN_RAW)
- log.info('Created a socket')
+ log.info("Created a socket")
return sock
-def bind_socket(sock, channel='can0'):
+def bind_socket(sock: socket.socket, channel: str = "can0") -> None:
"""
Binds the given socket to the given interface.
- :param socket.socket sock:
+ :param sock:
The socket to be bound
+ :param channel:
+ The channel / interface to bind to
:raises OSError:
If the specified interface isn't found.
"""
- log.debug('Binding socket to channel=%s', channel)
- if HAS_NATIVE_SUPPORT:
- sock.bind((channel,))
- else:
- # For Python 2.7
- addr = get_addr(sock, channel)
- libc.bind(sock.fileno(), addr, len(addr))
- log.debug('Bound socket.')
+ log.debug("Binding socket to channel=%s", channel)
+ sock.bind((channel,))
+ log.debug("Bound socket.")
-def capture_message(sock, get_channel=False):
+def capture_message(sock: socket.socket, get_channel: bool = False) -> Message | None:
"""
Captures a message from given socket.
- :param socket.socket sock:
+ :param sock:
The socket to read a message from.
- :param bool get_channel:
+ :param get_channel:
Find out which channel the message comes from.
:return: The received message, or None on failure.
"""
# Fetching the Arb ID, DLC and Data
try:
+ cf, ancillary_data, msg_flags, addr = sock.recvmsg(
+ constants.CANFD_MTU, RECEIVED_ANCILLARY_BUFFER_SIZE
+ )
if get_channel:
- if HAS_NATIVE_SUPPORT:
- cf, addr = sock.recvfrom(CANFD_MTU)
- channel = addr[0] if isinstance(addr, tuple) else addr
- else:
- data = ctypes.create_string_buffer(CANFD_MTU)
- addr = ctypes.create_string_buffer(32)
- addrlen = ctypes.c_int(len(addr))
- received = libc.recvfrom(sock.fileno(), data, len(data), 0,
- addr, ctypes.byref(addrlen))
- cf = data.raw[:received]
- # Figure out the channel name
- family, ifindex = struct.unpack_from("Hi", addr.raw)
- assert family == AF_CAN
- data = struct.pack("16xi", ifindex)
- res = fcntl.ioctl(sock, SIOCGIFNAME, data)
- channel = ctypes.create_string_buffer(res).value.decode()
+ channel = addr[0] if isinstance(addr, tuple) else addr
else:
- cf = sock.recv(CANFD_MTU)
channel = None
- except socket.error as exc:
- raise can.CanError("Error receiving: %s" % exc)
+ except OSError as error:
+ raise can.CanOperationError(
+ f"Error receiving: {error.strerror}", error.errno
+ ) from error
can_id, can_dlc, flags, data = dissect_can_frame(cf)
- #log.debug('Received: can_id=%x, can_dlc=%x, data=%s', can_id, can_dlc, data)
# Fetching the timestamp
- binary_structure = "@LL"
- res = fcntl.ioctl(sock, SIOCGSTAMP, struct.pack(binary_structure, 0, 0))
-
- seconds, microseconds = struct.unpack(binary_structure, res)
- timestamp = seconds + microseconds * 1e-6
+ assert len(ancillary_data) == 1, "only requested a single extra field"
+ cmsg_level, cmsg_type, cmsg_data = ancillary_data[0]
+ assert (
+ cmsg_level == socket.SOL_SOCKET and cmsg_type == constants.SO_TIMESTAMPNS
+ ), "received control message type that was not requested"
+ # see https://man7.org/linux/man-pages/man3/timespec.3.html -> struct timespec for details
+ seconds, nanoseconds = RECEIVED_TIMESTAMP_STRUCT.unpack_from(cmsg_data)
+ if nanoseconds >= 1e9:
+ raise can.CanOperationError(
+ f"Timestamp nanoseconds field was out of range: {nanoseconds} not less than 1e9"
+ )
+ timestamp = seconds + nanoseconds * 1e-9
# EXT, RTR, ERR flags -> boolean attributes
# /* special address description flags for the CAN_ID */
# #define CAN_EFF_FLAG 0x80000000U /* EFF/SFF is set in the MSB */
# #define CAN_RTR_FLAG 0x40000000U /* remote transmission request */
# #define CAN_ERR_FLAG 0x20000000U /* error frame */
- is_extended_frame_format = bool(can_id & CAN_EFF_FLAG)
- is_remote_transmission_request = bool(can_id & CAN_RTR_FLAG)
- is_error_frame = bool(can_id & CAN_ERR_FLAG)
- is_fd = len(cf) == CANFD_MTU
- bitrate_switch = bool(flags & CANFD_BRS)
- error_state_indicator = bool(flags & CANFD_ESI)
+ is_extended_frame_format = bool(can_id & constants.CAN_EFF_FLAG)
+ is_remote_transmission_request = bool(can_id & constants.CAN_RTR_FLAG)
+ is_error_frame = bool(can_id & constants.CAN_ERR_FLAG)
+ is_fd = len(cf) == constants.CANFD_MTU
+ bitrate_switch = bool(flags & constants.CANFD_BRS)
+ error_state_indicator = bool(flags & constants.CANFD_ESI)
+
+ # Section 4.7.1: MSG_DONTROUTE: set when the received frame was created on the local host.
+ is_rx = not bool(msg_flags & socket.MSG_DONTROUTE)
if is_extended_frame_format:
- #log.debug("CAN: Extended")
+ # log.debug("CAN: Extended")
# TODO does this depend on SFF or EFF?
arbitration_id = can_id & 0x1FFFFFFF
else:
- #log.debug("CAN: Standard")
+ # log.debug("CAN: Standard")
arbitration_id = can_id & 0x000007FF
- msg = Message(timestamp=timestamp,
- channel=channel,
- arbitration_id=arbitration_id,
- extended_id=is_extended_frame_format,
- is_remote_frame=is_remote_transmission_request,
- is_error_frame=is_error_frame,
- is_fd=is_fd,
- bitrate_switch=bitrate_switch,
- error_state_indicator=error_state_indicator,
- dlc=can_dlc,
- data=data)
-
- #log_rx.debug('Received: %s', msg)
+ msg = Message(
+ timestamp=timestamp,
+ channel=channel,
+ arbitration_id=arbitration_id,
+ is_extended_id=is_extended_frame_format,
+ is_remote_frame=is_remote_transmission_request,
+ is_error_frame=is_error_frame,
+ is_fd=is_fd,
+ is_rx=is_rx,
+ bitrate_switch=bitrate_switch,
+ error_state_indicator=error_state_indicator,
+ dlc=can_dlc,
+ data=data,
+ )
return msg
-class SocketcanBus(BusABC):
- """
- Implements :meth:`can.BusABC._detect_available_configs`.
+class SocketcanBus(BusABC): # pylint: disable=abstract-method
+ """A SocketCAN interface to CAN.
+
+ It implements :meth:`can.BusABC._detect_available_configs` to search for
+ available interfaces.
"""
- def __init__(self, channel="", receive_own_messages=False, fd=False, **kwargs):
- """
- :param str channel:
- The can interface name with which to create this bus. An example channel
- would be 'vcan0' or 'can0'.
+ def __init__(
+ self,
+ channel: str = "",
+ receive_own_messages: bool = False,
+ local_loopback: bool = True,
+ fd: bool = False,
+ can_filters: CanFilters | None = None,
+ ignore_rx_error_frames=False,
+ **kwargs,
+ ) -> None:
+ """Creates a new socketcan bus.
+
+ If setting some socket options fails, an error will be printed
+ but no exception will be thrown. This includes enabling:
+
+ - that own messages should be received,
+ - CAN-FD frames and
+ - error frames.
+
+ :param channel:
+ The can interface name with which to create this bus.
+ An example channel would be 'vcan0' or 'can0'.
An empty string '' will receive messages from all channels.
In that case any sent messages must be explicitly addressed to a
channel using :attr:`can.Message.channel`.
- :param bool receive_own_messages:
+ :param receive_own_messages:
If transmitted messages should also be received by this bus.
- :param bool fd:
+ :param local_loopback:
+ If local loopback should be enabled on this bus.
+ Please note that local loopback does not mean that messages sent
+ on a socket will be readable on the same socket, they will only
+ be readable on other open sockets on the same machine. More info
+ can be read on the socketcan documentation:
+ See https://www.kernel.org/doc/html/latest/networking/can.html#socketcan-local-loopback1
+ :param fd:
If CAN-FD frames should be supported.
- :param list can_filters:
+ :param can_filters:
See :meth:`can.BusABC.set_filters`.
+ :param ignore_rx_error_frames:
+ If incoming error frames should be discarded.
"""
self.socket = create_socket()
self.channel = channel
- self.channel_info = "socketcan channel '%s'" % channel
-
- # set the receive_own_messages paramater
+ self.channel_info = f"socketcan channel '{channel}'"
+ self._bcm_sockets: dict[str, socket.socket] = {}
+ self._is_filtered = False
+ self._task_id = 0
+ self._task_id_guard = threading.Lock()
+ self._can_protocol = CanProtocol.CAN_FD if fd else CanProtocol.CAN_20
+
+ # set the local_loopback parameter
try:
- self.socket.setsockopt(SOL_CAN_RAW,
- CAN_RAW_RECV_OWN_MSGS,
- 1 if receive_own_messages else 0)
- except socket.error as e:
- log.error("Could not receive own messages (%s)", e)
-
+ self.socket.setsockopt(
+ constants.SOL_CAN_RAW,
+ constants.CAN_RAW_LOOPBACK,
+ 1 if local_loopback else 0,
+ )
+ except OSError as error:
+ log.error("Could not set local loopback flag(%s)", error)
+
+ # set the receive_own_messages parameter
+ try:
+ self.socket.setsockopt(
+ constants.SOL_CAN_RAW,
+ constants.CAN_RAW_RECV_OWN_MSGS,
+ 1 if receive_own_messages else 0,
+ )
+ except OSError as error:
+ log.error("Could not receive own messages (%s)", error)
+
+ # enable CAN-FD frames if desired
if fd:
- # TODO handle errors
- self.socket.setsockopt(SOL_CAN_RAW,
- CAN_RAW_FD_FRAMES,
- 1)
-
- # Enable error frames
- self.socket.setsockopt(SOL_CAN_RAW,
- CAN_RAW_ERR_FILTER,
- 0x1FFFFFFF)
+ try:
+ self.socket.setsockopt(
+ constants.SOL_CAN_RAW, constants.CAN_RAW_FD_FRAMES, 1
+ )
+ except OSError as error:
+ log.error("Could not enable CAN-FD frames (%s)", error)
+
+ if not ignore_rx_error_frames:
+ # enable error frames
+ try:
+ self.socket.setsockopt(
+ constants.SOL_CAN_RAW, constants.CAN_RAW_ERR_FILTER, 0x1FFFFFFF
+ )
+ except OSError as error:
+ log.error("Could not enable error frames (%s)", error)
+
+ # enable nanosecond resolution timestamping
+ # we can always do this since
+ # 1) it is guaranteed to be at least as precise as without
+ # 2) it is available since Linux 2.6.22, and CAN support was only added afterward
+ # so this is always supported by the kernel
+ self.socket.setsockopt(socket.SOL_SOCKET, constants.SO_TIMESTAMPNS, 1)
- bind_socket(self.socket, channel)
-
- kwargs.update({'receive_own_messages': receive_own_messages, 'fd': fd})
- super(SocketcanBus, self).__init__(channel=channel, **kwargs)
-
- def shutdown(self):
- """Closes the socket."""
+ try:
+ bind_socket(self.socket, channel)
+ kwargs.update(
+ {
+ "receive_own_messages": receive_own_messages,
+ "fd": fd,
+ "local_loopback": local_loopback,
+ }
+ )
+ except OSError as error:
+ log.error("Could not access SocketCAN device %s (%s)", channel, error)
+ raise
+ super().__init__(
+ channel=channel,
+ can_filters=can_filters,
+ **kwargs,
+ )
+
+ def shutdown(self) -> None:
+ """Stops all active periodic tasks and closes the socket."""
+ super().shutdown()
+ for channel, bcm_socket in self._bcm_sockets.items():
+ log.debug("Closing bcm socket for channel %s", channel)
+ bcm_socket.close()
+ log.debug("Closing raw can socket")
self.socket.close()
- def _recv_internal(self, timeout):
- # get all sockets that are ready (can be a list with a single value
- # being self.socket or an empty list if self.socket is not ready)
+ def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]:
try:
# get all sockets that are ready (can be a list with a single value
# being self.socket or an empty list if self.socket is not ready)
ready_receive_sockets, _, _ = select.select([self.socket], [], [], timeout)
- except socket.error as exc:
+ except OSError as error:
# something bad happened (e.g. the interface went down)
- raise can.CanError("Failed to receive: %s" % exc)
+ raise can.CanOperationError(
+ f"Failed to receive: {error.strerror}", error.errno
+ ) from error
- if ready_receive_sockets: # not empty or True
+ if ready_receive_sockets: # not empty
get_channel = self.channel == ""
msg = capture_message(self.socket, get_channel)
- if not msg.channel and self.channel:
+ if msg and not msg.channel and self.channel:
# Default to our own channel
msg.channel = self.channel
return msg, self._is_filtered
- else:
- # socket wasn't readable or timeout occurred
- return None, self._is_filtered
- def send(self, msg, timeout=None):
+ # socket wasn't readable or timeout occurred
+ return None, self._is_filtered
+
+ def send(self, msg: Message, timeout: float | None = None) -> None:
"""Transmit a message to the CAN bus.
- :param can.Message msg: A message object.
- :param float timeout:
+ :param msg: A message object.
+ :param timeout:
Wait up to this many seconds for the transmit queue to be ready.
If not given, the call may fail immediately.
- :raises can.CanError:
+ :raises ~can.exceptions.CanError:
if the message could not be written.
"""
log.debug("We've been asked to write a message to the bus")
@@ -539,109 +863,131 @@ def send(self, msg, timeout=None):
if not ready:
# Timeout
break
- sent = self._send_once(data, msg.channel)
+ channel = str(msg.channel) if msg.channel else None
+ sent = self._send_once(data, channel)
if sent == len(data):
return
# Not all data were sent, try again with remaining data
data = data[sent:]
time_left = timeout - (time.time() - started)
- raise can.CanError("Transmit buffer full")
+ raise can.CanOperationError("Transmit buffer full")
- def _send_once(self, data, channel=None):
+ def _send_once(self, data: bytes, channel: str | None = None) -> int:
try:
if self.channel == "" and channel:
# Message must be addressed to a specific channel
- if HAS_NATIVE_SUPPORT:
- sent = self.socket.sendto(data, (channel, ))
- else:
- addr = get_addr(self.socket, channel)
- sent = libc.sendto(self.socket.fileno(),
- data, len(data), 0,
- addr, len(addr))
+ sent = self.socket.sendto(data, (channel,))
else:
sent = self.socket.send(data)
- except socket.error as exc:
- raise can.CanError("Failed to transmit: %s" % exc)
+ except OSError as error:
+ raise can.CanOperationError(
+ f"Failed to transmit: {error.strerror}", error.errno
+ ) from error
return sent
- def send_periodic(self, msg, period, duration=None):
- """Start sending a message at a given period on this bus.
-
- The kernel's broadcast manager will be used.
-
- :param can.Message msg:
- Message to transmit
- :param float period:
- Period in seconds between each message
- :param float duration:
- The duration to keep sending this message at given rate. If
+ def _send_periodic_internal(
+ self,
+ msgs: Sequence[Message] | Message,
+ period: float,
+ duration: float | None = None,
+ autostart: bool = True,
+ modifier_callback: Callable[[Message], None] | None = None,
+ ) -> can.broadcastmanager.CyclicSendTaskABC:
+ """Start sending messages at a given period on this bus.
+
+ The Linux kernel's Broadcast Manager SocketCAN API is used to schedule
+ periodic sending of CAN messages. The wrapping 32-bit counter (see
+ :meth:`~_get_next_task_id()`) designated to distinguish different
+ :class:`CyclicSendTask` within BCM provides flexibility to schedule
+ CAN messages sending with the same CAN ID, but different CAN data.
+
+ :param msgs:
+ The message(s) to be sent periodically.
+ :param period:
+ The rate in seconds at which to send the messages.
+ :param duration:
+ Approximate duration in seconds to continue sending messages. If
no duration is provided, the task will continue indefinitely.
+ :param autostart:
+ If True (the default) the sending task will immediately start after creation.
+ Otherwise, the task has to be started by calling the
+ tasks :meth:`~can.RestartableCyclicTaskABC.start` method on it.
+
+ :raises ValueError:
+ If task identifier passed to :class:`CyclicSendTask` can't be used
+ to schedule new task in Linux BCM.
- :return: A started task instance
- :rtype: can.interfaces.socketcan.CyclicSendTask
+ :return:
+ A :class:`CyclicSendTask` task instance. This can be used to modify the data,
+ pause/resume the transmission and to stop the transmission.
.. note::
- Note the duration before the message stops being sent may not
+ Note the duration before the messages stop being sent may not
be exactly the same as the duration specified by the user. In
general the message will be sent at the given rate until at
least *duration* seconds.
-
"""
- return CyclicSendTask(msg.channel or self.channel, msg, period, duration)
-
- def _apply_filters(self, filters):
+ if modifier_callback is None:
+ msgs = LimitedDurationCyclicSendTaskABC._check_and_convert_messages( # pylint: disable=protected-access
+ msgs
+ )
+
+ msgs_channel = str(msgs[0].channel) if msgs[0].channel else None
+ bcm_socket = self._get_bcm_socket(msgs_channel or self.channel)
+ task_id = self._get_next_task_id()
+ task = CyclicSendTask(
+ bcm_socket, task_id, msgs, period, duration, autostart=autostart
+ )
+ return task
+
+ # fallback to thread based cyclic task
+ warnings.warn(
+ f"{self.__class__.__name__} falls back to a thread-based cyclic task, "
+ "when the `modifier_callback` argument is given.",
+ stacklevel=3,
+ )
+ return BusABC._send_periodic_internal(
+ self,
+ msgs=msgs,
+ period=period,
+ duration=duration,
+ autostart=autostart,
+ modifier_callback=modifier_callback,
+ )
+
+ def _get_next_task_id(self) -> int:
+ with self._task_id_guard:
+ self._task_id = (self._task_id + 1) % (2**32 - 1)
+ return self._task_id
+
+ def _get_bcm_socket(self, channel: str) -> socket.socket:
+ if channel not in self._bcm_sockets:
+ self._bcm_sockets[channel] = create_bcm_socket(self.channel)
+ return self._bcm_sockets[channel]
+
+ def _apply_filters(self, filters: can.typechecking.CanFilters | None) -> None:
try:
- self.socket.setsockopt(SOL_CAN_RAW,
- CAN_RAW_FILTER,
- pack_filters(filters))
- except socket.error as err:
+ self.socket.setsockopt(
+ constants.SOL_CAN_RAW, constants.CAN_RAW_FILTER, pack_filters(filters)
+ )
+ except OSError as error:
# fall back to "software filtering" (= not in kernel)
self._is_filtered = False
- # TODO Is this serious enough to raise a CanError exception?
- log.error('Setting filters failed; falling back to software filtering (not in kernel): %s', err)
+ log.error(
+ "Setting filters failed; falling back to software filtering (not in kernel): %s",
+ error,
+ )
else:
self._is_filtered = True
- def fileno(self):
+ def fileno(self) -> int:
return self.socket.fileno()
@staticmethod
- def _detect_available_configs():
- return [{'interface': 'socketcan', 'channel': channel}
- for channel in find_available_interfaces()]
-
-
-if __name__ == "__main__":
- # TODO move below to examples?
-
- # Create two sockets on vcan0 to test send and receive
- #
- # If you want to try it out you can do the following (possibly using sudo):
- #
- # modprobe vcan
- # ip link add dev vcan0 type vcan
- # ifconfig vcan0 up
- #
- log.setLevel(logging.DEBUG)
-
- def receiver(event):
- receiver_socket = create_socket()
- bind_socket(receiver_socket, 'vcan0')
- print("Receiver is waiting for a message...")
- event.set()
- print("Receiver got: ", capture_message(receiver_socket))
-
- def sender(event):
- event.wait()
- sender_socket = create_socket()
- bind_socket(sender_socket, 'vcan0')
- msg = Message(arbitration_id=0x01, data=b'\x01\x02\x03')
- sender_socket.send(build_can_frame(msg))
- print("Sender sent a message.")
-
- import threading
- e = threading.Event()
- threading.Thread(target=receiver, args=(e,)).start()
- threading.Thread(target=sender, args=(e,)).start()
+ def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]:
+ return [
+ {"interface": "socketcan", "channel": channel}
+ for channel in find_available_interfaces()
+ ]
diff --git a/can/interfaces/socketcan/utils.py b/can/interfaces/socketcan/utils.py
index ef522e408..0740f769d 100644
--- a/can/interfaces/socketcan/utils.py
+++ b/can/interfaces/socketcan/utils.py
@@ -1,39 +1,35 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Defines common socketcan functions.
"""
+import errno
+import json
import logging
import os
-import errno
import struct
-import sys
import subprocess
-import re
+import sys
+from can import typechecking
from can.interfaces.socketcan.constants import CAN_EFF_FLAG
log = logging.getLogger(__name__)
-def pack_filters(can_filters=None):
+
+def pack_filters(can_filters: typechecking.CanFilters | None = None) -> bytes:
if can_filters is None:
# Pass all messages
- can_filters = [{
- 'can_id': 0,
- 'can_mask': 0
- }]
+ can_filters = [{"can_id": 0, "can_mask": 0}]
- can_filter_fmt = "={}I".format(2 * len(can_filters))
+ can_filter_fmt = f"={2 * len(can_filters)}I"
filter_data = []
for can_filter in can_filters:
- can_id = can_filter['can_id']
- can_mask = can_filter['can_mask']
- if 'extended' in can_filter:
+ can_id = can_filter["can_id"]
+ can_mask = can_filter["can_mask"]
+ if "extended" in can_filter:
# Match on either 11-bit OR 29-bit messages instead of both
can_mask |= CAN_EFF_FLAG
- if can_filter['extended']:
+ if can_filter["extended"]:
can_id |= CAN_EFF_FLAG
filter_data.append(can_id)
filter_data.append(can_mask)
@@ -41,51 +37,49 @@ def pack_filters(can_filters=None):
return struct.pack(can_filter_fmt, *filter_data)
-_PATTERN_CAN_INTERFACE = re.compile(r"v?can\d+")
+def find_available_interfaces() -> list[str]:
+ """Returns the names of all open can/vcan interfaces
-def find_available_interfaces():
- """Returns the names of all open can/vcan interfaces using
- the ``ip link list`` command. If the lookup fails, an error
+ The function calls the ``ip link list`` command. If the lookup fails, an error
is logged to the console and an empty list is returned.
- :rtype: an iterable of :class:`str`
+ :return: The list of available and active CAN interfaces or an empty list of the command failed
"""
+ if sys.platform != "linux":
+ return []
try:
- # it might be good to add "type vcan", but that might (?) exclude physical can devices
- command = ["ip", "-o", "link", "list", "up"]
- output = subprocess.check_output(command, universal_newlines=True)
+ command = ["ip", "-json", "link", "list", "up"]
+ output_str = subprocess.check_output(command, text=True)
+ except Exception: # pylint: disable=broad-except
+ # subprocess.CalledProcessError is too specific
+ log.exception("failed to fetch opened can devices from ip link")
+ return []
- except Exception as e: # subprocess.CalledProcessError was too specific
- log.error("failed to fetch opened can devices: %s", e)
+ try:
+ output_json = json.loads(output_str)
+ except json.JSONDecodeError:
+ log.exception("Failed to parse ip link JSON output: %s", output_str)
return []
- else:
- #log.debug("find_available_interfaces(): output=\n%s", output)
- # output contains some lines like "1: vcan42: ..."
- # extract the "vcan42" of each line
- interface_names = [line.split(": ", 3)[1] for line in output.splitlines()]
- log.debug("find_available_interfaces(): detected: %s", interface_names)
- return filter(_PATTERN_CAN_INTERFACE.match, interface_names)
+ log.debug(
+ "find_available_interfaces(): detected these interfaces (before filtering): %s",
+ output_json,
+ )
+
+ interfaces = [i["ifname"] for i in output_json if i.get("link_type") == "can"]
+ return interfaces
-def error_code_to_str(code):
+
+def error_code_to_str(code: int | None) -> str:
"""
Converts a given error code (errno) to a useful and human readable string.
- :param int code: a possibly invalid/unknown error code
- :rtype: str
+ :param code: a possibly invalid/unknown error code
:returns: a string explaining and containing the given error code, or a string
explaining that the errorcode is unknown if that is the case
"""
+ name = errno.errorcode.get(code, "UNKNOWN") # type: ignore
+ description = os.strerror(code) if code is not None else "NO DESCRIPTION AVAILABLE"
- try:
- name = errno.errorcode[code]
- except KeyError:
- name = "UNKNOWN"
-
- try:
- description = os.strerror(code)
- except ValueError:
- description = "no description available"
-
- return "{} (errno {}): {}".format(name, code, description)
+ return f"{name} (errno {code}): {description}"
diff --git a/can/interfaces/socketcand/__init__.py b/can/interfaces/socketcand/__init__.py
new file mode 100644
index 000000000..64950f7f4
--- /dev/null
+++ b/can/interfaces/socketcand/__init__.py
@@ -0,0 +1,15 @@
+"""
+Interface to socketcand
+see https://github.com/linux-can/socketcand
+
+Copyright (C) 2021 DOMOLOGIC GmbH
+http://www.domologic.de
+"""
+
+__all__ = [
+ "SocketCanDaemonBus",
+ "detect_beacon",
+ "socketcand",
+]
+
+from .socketcand import SocketCanDaemonBus, detect_beacon
diff --git a/can/interfaces/socketcand/socketcand.py b/can/interfaces/socketcand/socketcand.py
new file mode 100644
index 000000000..d401102f7
--- /dev/null
+++ b/can/interfaces/socketcand/socketcand.py
@@ -0,0 +1,373 @@
+"""
+Interface to socketcand
+see https://github.com/linux-can/socketcand
+
+Authors: Marvin Seiler, Gerrit Telkamp
+
+Copyright (C) 2021 DOMOLOGIC GmbH
+http://www.domologic.de
+"""
+
+import logging
+import os
+import select
+import socket
+import time
+import traceback
+import urllib.parse as urlparselib
+import xml.etree.ElementTree as ET
+from collections import deque
+
+import can
+
+log = logging.getLogger(__name__)
+
+DEFAULT_SOCKETCAND_DISCOVERY_ADDRESS = ""
+DEFAULT_SOCKETCAND_DISCOVERY_PORT = 42000
+
+
+def detect_beacon(timeout_ms: int = 3100) -> list[can.typechecking.AutoDetectedConfig]:
+ """
+ Detects socketcand servers
+
+ This is what :meth:`can.detect_available_configs` ends up calling to search
+ for available socketcand servers with a default timeout of 3100ms
+ (socketcand sends a beacon packet every 3000ms).
+
+ Using this method directly allows for adjusting the timeout. Extending
+ the timeout beyond the default time period could be useful if UDP
+ packet loss is a concern.
+
+ :param timeout_ms:
+ Timeout in milliseconds to wait for socketcand beacon packets
+
+ :return:
+ See :meth:`~can.detect_available_configs`
+ """
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
+ sock.bind(
+ (DEFAULT_SOCKETCAND_DISCOVERY_ADDRESS, DEFAULT_SOCKETCAND_DISCOVERY_PORT)
+ )
+ log.info(
+ "Listening on for socketcand UDP advertisement on %s:%s",
+ DEFAULT_SOCKETCAND_DISCOVERY_ADDRESS,
+ DEFAULT_SOCKETCAND_DISCOVERY_PORT,
+ )
+
+ now = time.time() * 1000
+ end_time = now + timeout_ms
+ while (time.time() * 1000) < end_time:
+ try:
+ # get all sockets that are ready (can be a list with a single value
+ # being self.socket or an empty list if self.socket is not ready)
+ ready_receive_sockets, _, _ = select.select([sock], [], [], 1)
+
+ if not ready_receive_sockets:
+ log.debug("No advertisement received")
+ continue
+
+ msg = sock.recv(1024).decode("utf-8")
+ root = ET.fromstring(msg)
+ if root.tag != "CANBeacon":
+ log.debug("Unexpected message received over UDP")
+ continue
+
+ det_devs = []
+ det_host = None
+ det_port = None
+ for child in root:
+ if child.tag == "Bus":
+ bus_name = child.attrib["name"]
+ det_devs.append(bus_name)
+ elif child.tag == "URL":
+ url = urlparselib.urlparse(child.text)
+ det_host = url.hostname
+ det_port = url.port
+
+ if not det_devs:
+ log.debug(
+ "Got advertisement, but no SocketCAN devices advertised by socketcand"
+ )
+ continue
+
+ if (det_host is None) or (det_port is None):
+ det_host = None
+ det_port = None
+ log.debug(
+ "Got advertisement, but no SocketCAN URL advertised by socketcand"
+ )
+ continue
+
+ log.info(f"Found SocketCAN devices: {det_devs}")
+ return [
+ {
+ "interface": "socketcand",
+ "host": det_host,
+ "port": det_port,
+ "channel": channel,
+ }
+ for channel in det_devs
+ ]
+
+ except ET.ParseError:
+ log.debug("Unexpected message received over UDP")
+ continue
+
+ except Exception as exc:
+ # something bad happened (e.g. the interface went down)
+ log.error(f"Failed to detect beacon: {exc} {traceback.format_exc()}")
+ raise OSError(
+ f"Failed to detect beacon: {exc} {traceback.format_exc()}"
+ ) from exc
+
+ return []
+
+
+def convert_ascii_message_to_can_message(ascii_msg: str) -> can.Message:
+ if not ascii_msg.endswith(" >"):
+ log.warning(f"Missing ending character in ascii message: {ascii_msg}")
+ return None
+
+ if ascii_msg.startswith("< frame "):
+ # frame_string = ascii_msg.removeprefix("< frame ").removesuffix(" >")
+ frame_string = ascii_msg[8:-2]
+ parts = frame_string.split(" ", 3)
+ can_id, timestamp = int(parts[0], 16), float(parts[1])
+ is_ext = len(parts[0]) != 3
+
+ data = bytearray.fromhex(parts[2])
+ can_dlc = len(data)
+ can_message = can.Message(
+ timestamp=timestamp,
+ arbitration_id=can_id,
+ data=data,
+ dlc=can_dlc,
+ is_extended_id=is_ext,
+ is_rx=True,
+ )
+ return can_message
+
+ if ascii_msg.startswith("< error "):
+ frame_string = ascii_msg[8:-2]
+ parts = frame_string.split(" ", 3)
+ can_id, timestamp = int(parts[0], 16), float(parts[1])
+ is_ext = len(parts[0]) != 3
+
+ # socketcand sends no data in the error message so we don't have information
+ # about the error details, therefore the can frame is created with one
+ # data byte set to zero
+ data = bytearray([0])
+ can_dlc = len(data)
+ can_message = can.Message(
+ timestamp=timestamp,
+ arbitration_id=can_id & 0x1FFFFFFF,
+ is_error_frame=True,
+ data=data,
+ dlc=can_dlc,
+ is_extended_id=True,
+ is_rx=True,
+ )
+ return can_message
+
+ log.warning(f"Could not parse ascii message: {ascii_msg}")
+ return None
+
+
+def convert_can_message_to_ascii_message(can_message: can.Message) -> str:
+ # Note: socketcan bus adds extended flag, remote_frame_flag & error_flag to id
+ # not sure if that is necessary here
+ can_id = can_message.arbitration_id
+ if can_message.is_extended_id:
+ can_id_string = f"{(can_id&0x1FFFFFFF):08X}"
+ else:
+ can_id_string = f"{(can_id&0x7FF):03X}"
+ # Note: seems like we cannot add CANFD_BRS (bitrate_switch) and CANFD_ESI (error_state_indicator) flags
+ data = can_message.data
+ length = can_message.dlc
+ bytes_string = " ".join(f"{x:x}" for x in data[0:length])
+ return f"< send {can_id_string} {length:X} {bytes_string} >"
+
+
+def connect_to_server(s, host, port):
+ timeout_ms = 10000
+ now = time.time() * 1000
+ end_time = now + timeout_ms
+ while now < end_time:
+ try:
+ s.connect((host, port))
+ return
+ except Exception as e:
+ log.warning(f"Failed to connect to server: {type(e)} Message: {e}")
+ now = time.time() * 1000
+ raise TimeoutError(
+ f"connect_to_server: Failed to connect server for {timeout_ms} ms"
+ )
+
+
+class SocketCanDaemonBus(can.BusABC):
+ def __init__(self, channel, host, port, tcp_tune=False, can_filters=None, **kwargs):
+ """Connects to a CAN bus served by socketcand.
+
+ It implements :meth:`can.BusABC._detect_available_configs` to search for
+ available interfaces.
+
+ It will attempt to connect to the server for up to 10s, after which a
+ TimeoutError exception will be thrown.
+
+ If the handshake with the socketcand server fails, a CanError exception
+ is thrown.
+
+ :param channel:
+ The can interface name served by socketcand.
+ An example channel would be 'vcan0' or 'can0'.
+ :param host:
+ The host address of the socketcand server.
+ :param port:
+ The port of the socketcand server.
+ :param tcp_tune:
+ This tunes the TCP socket for low latency (TCP_NODELAY, and
+ TCP_QUICKACK).
+ This option is not available under windows.
+ :param can_filters:
+ See :meth:`can.BusABC.set_filters`.
+ """
+ self.__host = host
+ self.__port = port
+
+ self.__tcp_tune = tcp_tune
+ self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+
+ if self.__tcp_tune:
+ if os.name == "nt":
+ self.__tcp_tune = False
+ log.warning("'tcp_tune' not available in Windows. Setting to False")
+ else:
+ self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+
+ self.__message_buffer = deque()
+ self.__receive_buffer = "" # i know string is not the most efficient here
+ self.channel = channel
+ self.channel_info = f"socketcand on {channel}@{host}:{port}"
+ connect_to_server(self.__socket, self.__host, self.__port)
+ self._expect_msg("< hi >")
+
+ log.info(
+ f"SocketCanDaemonBus: connected with address {self.__socket.getsockname()}"
+ )
+ self._tcp_send(f"< open {channel} >")
+ self._expect_msg("< ok >")
+ self._tcp_send("< rawmode >")
+ self._expect_msg("< ok >")
+ super().__init__(channel=channel, can_filters=can_filters, **kwargs)
+
+ def _recv_internal(self, timeout):
+ if len(self.__message_buffer) != 0:
+ can_message = self.__message_buffer.popleft()
+ return can_message, False
+
+ try:
+ # get all sockets that are ready (can be a list with a single value
+ # being self.socket or an empty list if self.socket is not ready)
+ ready_receive_sockets, _, _ = select.select(
+ [self.__socket], [], [], timeout
+ )
+ except OSError as exc:
+ # something bad happened (e.g. the interface went down)
+ log.error(f"Failed to receive: {exc}")
+ raise can.CanError(f"Failed to receive: {exc}") from exc
+
+ try:
+ if not ready_receive_sockets:
+ # socket wasn't readable or timeout occurred
+ log.debug("Socket not ready")
+ return None, False
+
+ ascii_msg = self.__socket.recv(1024).decode(
+ "ascii"
+ ) # may contain multiple messages
+ if self.__tcp_tune:
+ self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)
+ self.__receive_buffer += ascii_msg
+ log.debug(f"Received Ascii Message: {ascii_msg}")
+ buffer_view = self.__receive_buffer
+ chars_processed_successfully = 0
+ while True:
+ if len(buffer_view) == 0:
+ break
+
+ start = buffer_view.find("<")
+ if start == -1:
+ log.warning(
+ f"Bad data: No opening < found => discarding entire buffer '{buffer_view}'"
+ )
+ chars_processed_successfully = len(self.__receive_buffer)
+ break
+ end = buffer_view.find(">")
+ if end == -1:
+ log.warning("Got incomplete message => waiting for more data")
+ if len(buffer_view) > 200:
+ log.warning(
+ "Incomplete message exceeds 200 chars => Discarding"
+ )
+ chars_processed_successfully = len(self.__receive_buffer)
+ break
+ chars_processed_successfully += end + 1
+ single_message = buffer_view[start : end + 1]
+ parsed_can_message = convert_ascii_message_to_can_message(
+ single_message
+ )
+ if parsed_can_message is None:
+ log.warning(f"Invalid Frame: {single_message}")
+ else:
+ parsed_can_message.channel = self.channel
+ self.__message_buffer.append(parsed_can_message)
+ buffer_view = buffer_view[end + 1 :]
+
+ self.__receive_buffer = self.__receive_buffer[chars_processed_successfully:]
+ can_message = (
+ None
+ if len(self.__message_buffer) == 0
+ else self.__message_buffer.popleft()
+ )
+ return can_message, False
+
+ except Exception as exc:
+ log.error(f"Failed to receive: {exc} {traceback.format_exc()}")
+ raise can.CanError(
+ f"Failed to receive: {exc} {traceback.format_exc()}"
+ ) from exc
+
+ def _tcp_send(self, msg: str):
+ log.debug(f"Sending TCP Message: '{msg}'")
+ self.__socket.sendall(msg.encode("ascii"))
+ if self.__tcp_tune:
+ self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)
+
+ def _expect_msg(self, msg):
+ ascii_msg = self.__socket.recv(256).decode("ascii")
+ if self.__tcp_tune:
+ self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1)
+ if not ascii_msg == msg:
+ raise can.CanError(f"Expected '{msg}' got: '{ascii_msg}'")
+
+ def send(self, msg, timeout=None):
+ """Transmit a message to the CAN bus.
+
+ :param msg: A message object.
+ :param timeout: Ignored
+ """
+ ascii_msg = convert_can_message_to_ascii_message(msg)
+ self._tcp_send(ascii_msg)
+
+ def shutdown(self):
+ """Stops all active periodic tasks and closes the socket."""
+ super().shutdown()
+ self.__socket.close()
+
+ @staticmethod
+ def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]:
+ try:
+ return detect_beacon()
+ except Exception as e:
+ log.warning(f"Could not detect socketcand beacon: {e}")
+ return []
diff --git a/can/interfaces/systec/__init__.py b/can/interfaces/systec/__init__.py
new file mode 100644
index 000000000..e38a52fb1
--- /dev/null
+++ b/can/interfaces/systec/__init__.py
@@ -0,0 +1,10 @@
+__all__ = [
+ "UcanBus",
+ "constants",
+ "exceptions",
+ "structures",
+ "ucan",
+ "ucanbus",
+]
+
+from can.interfaces.systec.ucanbus import UcanBus
diff --git a/can/interfaces/systec/constants.py b/can/interfaces/systec/constants.py
new file mode 100644
index 000000000..8caf9eab4
--- /dev/null
+++ b/can/interfaces/systec/constants.py
@@ -0,0 +1,657 @@
+from ctypes import c_ubyte as BYTE
+from ctypes import c_ulong as DWORD
+from ctypes import c_ushort as WORD
+
+#: Maximum number of modules that are supported.
+MAX_MODULES = 64
+
+#: Maximum number of applications that can use the USB-CAN-library.
+MAX_INSTANCES = 64
+
+#: With the method :meth:`UcanServer.init_can` the module is used, which is detected at first.
+#: This value only should be used in case only one module is connected to the computer.
+ANY_MODULE = 255
+
+#: No valid USB-CAN Handle (only used internally).
+INVALID_HANDLE = 0xFF
+
+
+class Baudrate(WORD):
+ """
+ Specifies pre-defined baud rate values for GW-001, GW-002 and all systec USB-CANmoduls.
+
+ .. seealso::
+
+ :meth:`UcanServer.init_can`
+
+ :meth:`UcanServer.set_baudrate`
+
+ :meth:`UcanServer.get_baudrate_message`
+
+ :class:`BaudrateEx`
+ """
+
+ #: 1000 kBit/sec
+ BAUD_1MBit = 0x14
+ #: 800 kBit/sec
+ BAUD_800kBit = 0x16
+ #: 500 kBit/sec
+ BAUD_500kBit = 0x1C
+ #: 250 kBit/sec
+ BAUD_250kBit = 0x11C
+ #: 125 kBit/sec
+ BAUD_125kBit = 0x31C
+ #: 100 kBit/sec
+ BAUD_100kBit = 0x432F
+ #: 50 kBit/sec
+ BAUD_50kBit = 0x472F
+ #: 20 kBit/sec
+ BAUD_20kBit = 0x532F
+ #: 10 kBit/sec
+ BAUD_10kBit = 0x672F
+ #: Uses pre-defined extended values of baudrate for all systec USB-CANmoduls.
+ BAUD_USE_BTREX = 0x0
+ #: Automatic baud rate detection (not implemented in this version).
+ BAUD_AUTO = -1
+
+
+class BaudrateEx(DWORD):
+ """
+ Specifies pre-defined baud rate values for all systec USB-CANmoduls.
+
+ These values cannot be used for GW-001 and GW-002! Use values from enum :class:`Baudrate` instead.
+
+ .. seealso::
+
+ :meth:`UcanServer.init_can`
+
+ :meth:`UcanServer.set_baudrate`
+
+ :meth:`UcanServer.get_baudrate_ex_message`
+
+ :class:`Baudrate`
+ """
+
+ #: G3: 1000 kBit/sec
+ BAUDEX_1MBit = 0x20354
+ #: G3: 800 kBit/sec
+ BAUDEX_800kBit = 0x30254
+ #: G3: 500 kBit/sec
+ BAUDEX_500kBit = 0x50354
+ #: G3: 250 kBit/sec
+ BAUDEX_250kBit = 0xB0354
+ #: G3: 125 kBit/sec
+ BAUDEX_125kBit = 0x170354
+ #: G3: 100 kBit/sec
+ BAUDEX_100kBit = 0x170466
+ #: G3: 50 kBit/sec
+ BAUDEX_50kBit = 0x2F0466
+ #: G3: 20 kBit/sec
+ BAUDEX_20kBit = 0x770466
+ #: G3: 10 kBit/sec (half CPU clock)
+ BAUDEX_10kBit = 0x80770466
+ #: G3: 1000 kBit/sec Sample Point: 87,50%
+ BAUDEX_SP2_1MBit = 0x20741
+ #: G3: 800 kBit/sec Sample Point: 86,67%
+ BAUDEX_SP2_800kBit = 0x30731
+ #: G3: 500 kBit/sec Sample Point: 87,50%
+ BAUDEX_SP2_500kBit = 0x50741
+ #: G3: 250 kBit/sec Sample Point: 87,50%
+ BAUDEX_SP2_250kBit = 0xB0741
+ #: G3: 125 kBit/sec Sample Point: 87,50%
+ BAUDEX_SP2_125kBit = 0x170741
+ #: G3: 100 kBit/sec Sample Point: 87,50%
+ BAUDEX_SP2_100kBit = 0x1D1741
+ #: G3: 50 kBit/sec Sample Point: 87,50%
+ BAUDEX_SP2_50kBit = 0x3B1741
+ #: G3: 20 kBit/sec Sample Point: 85,00%
+ BAUDEX_SP2_20kBit = 0x771772
+ #: G3: 10 kBit/sec Sample Point: 85,00% (half CPU clock)
+ BAUDEX_SP2_10kBit = 0x80771772
+
+ #: G4: 1000 kBit/sec Sample Point: 83,33%
+ BAUDEX_G4_1MBit = 0x406F0000
+ #: G4: 800 kBit/sec Sample Point: 80,00%
+ BAUDEX_G4_800kBit = 0x402A0001
+ #: G4: 500 kBit/sec Sample Point: 83,33%
+ BAUDEX_G4_500kBit = 0x406F0001
+ #: G4: 250 kBit/sec Sample Point: 83,33%
+ BAUDEX_G4_250kBit = 0x406F0003
+ #: G4: 125 kBit/sec Sample Point: 83,33%
+ BAUDEX_G4_125kBit = 0x406F0007
+ #: G4: 100 kBit/sec Sample Point: 83,33%
+ BAUDEX_G4_100kBit = 0x416F0009
+ #: G4: 50 kBit/sec Sample Point: 83,33%
+ BAUDEX_G4_50kBit = 0x416F0013
+ #: G4: 20 kBit/sec Sample Point: 84,00%
+ BAUDEX_G4_20kBit = 0x417F002F
+ #: G4: 10 kBit/sec Sample Point: 84,00% (half CPU clock)
+ BAUDEX_G4_10kBit = 0x417F005F
+ #: Uses pre-defined values of baud rates of :class:`Baudrate`.
+ BAUDEX_USE_BTR01 = 0x0
+ #: Automatic baud rate detection (not implemented in this version).
+ BAUDEX_AUTO = 0xFFFFFFFF
+
+
+class MsgFrameFormat(BYTE):
+ """
+ Specifies values for the frame format of CAN messages for member :attr:`CanMsg.m_bFF` in structure
+ :class:`CanMsg`. These values can be combined.
+
+ .. seealso:: :class:`CanMsg`
+ """
+
+ #: standard CAN data frame with 11 bit ID (CAN2.0A spec.)
+ MSG_FF_STD = 0x0
+ #: transmit echo
+ MSG_FF_ECHO = 0x20
+ #: CAN remote request frame with
+ MSG_FF_RTR = 0x40
+ #: extended CAN data frame with 29 bit ID (CAN2.0B spec.)
+ MSG_FF_EXT = 0x80
+
+
+class ReturnCode(BYTE):
+ """
+ Specifies all return codes of all methods of this class.
+ """
+
+ #: no error
+ SUCCESSFUL = 0x0
+ # start of error codes coming from USB-CAN-library
+ ERR = 0x1
+ # start of error codes coming from command interface between host and USB-CANmodul
+ ERRCMD = 0x40
+ # start of warning codes
+ WARNING = 0x80
+ # start of reserved codes which are only used internally
+ RESERVED = 0xC0
+
+ #: could not created a resource (memory, handle, ...)
+ ERR_RESOURCE = 0x1
+ #: the maximum number of opened modules is reached
+ ERR_MAXMODULES = 0x2
+ #: the specified module is already in use
+ ERR_HWINUSE = 0x3
+ #: the software versions of the module and library are incompatible
+ ERR_ILLVERSION = 0x4
+ #: the module with the specified device number is not connected (or used by an other application)
+ ERR_ILLHW = 0x5
+ #: wrong USB-CAN-Handle handed over to the function
+ ERR_ILLHANDLE = 0x6
+ #: wrong parameter handed over to the function
+ ERR_ILLPARAM = 0x7
+ #: instruction can not be processed at this time
+ ERR_BUSY = 0x8
+ #: no answer from module
+ ERR_TIMEOUT = 0x9
+ #: a request to the driver failed
+ ERR_IOFAILED = 0xA
+ #: a CAN message did not fit into the transmit buffer
+ ERR_DLL_TXFULL = 0xB
+ #: maximum number of applications is reached
+ ERR_MAXINSTANCES = 0xC
+ #: CAN interface is not yet initialized
+ ERR_CANNOTINIT = 0xD
+ #: USB-CANmodul was disconnected
+ ERR_DISCONECT = 0xE
+ #: the needed device class does not exist
+ ERR_NOHWCLASS = 0xF
+ #: illegal CAN channel
+ ERR_ILLCHANNEL = 0x10
+ #: reserved
+ ERR_RESERVED1 = 0x11
+ #: the API function can not be used with this hardware
+ ERR_ILLHWTYPE = 0x12
+
+ #: the received response does not match to the transmitted command
+ ERRCMD_NOTEQU = 0x40
+ #: no access to the CAN controller
+ ERRCMD_REGTST = 0x41
+ #: the module could not interpret the command
+ ERRCMD_ILLCMD = 0x42
+ #: error while reading the EEPROM
+ ERRCMD_EEPROM = 0x43
+ #: reserved
+ ERRCMD_RESERVED1 = 0x44
+ #: reserved
+ ERRCMD_RESERVED2 = 0x45
+ #: reserved
+ ERRCMD_RESERVED3 = 0x46
+ #: illegal baud rate value specified in BTR0/BTR1 for systec USB-CANmoduls
+ ERRCMD_ILLBDR = 0x47
+ #: CAN channel is not initialized
+ ERRCMD_NOTINIT = 0x48
+ #: CAN channel is already initialized
+ ERRCMD_ALREADYINIT = 0x49
+ #: illegal sub-command specified
+ ERRCMD_ILLSUBCMD = 0x4A
+ #: illegal index specified (e.g. index for cyclic CAN messages)
+ ERRCMD_ILLIDX = 0x4B
+ #: cyclic CAN message(s) can not be defined because transmission of cyclic CAN messages is already running
+ ERRCMD_RUNNING = 0x4C
+
+ #: no CAN messages received
+ WARN_NODATA = 0x80
+ #: overrun in receive buffer of the kernel driver
+ WARN_SYS_RXOVERRUN = 0x81
+ #: overrun in receive buffer of the USB-CAN-library
+ WARN_DLL_RXOVERRUN = 0x82
+ #: reserved
+ WARN_RESERVED1 = 0x83
+ #: reserved
+ WARN_RESERVED2 = 0x84
+ #: overrun in transmit buffer of the firmware (but this CAN message was successfully stored in buffer of the
+ #: library)
+ WARN_FW_TXOVERRUN = 0x85
+ #: overrun in receive buffer of the firmware (but this CAN message was successfully read)
+ WARN_FW_RXOVERRUN = 0x86
+ #: reserved
+ WARN_FW_TXMSGLOST = 0x87
+ #: pointer is NULL
+ WARN_NULL_PTR = 0x90
+ #: not all CAN messages could be stored to the transmit buffer in USB-CAN-library (check output of parameter
+ #: pdwCount_p)
+ WARN_TXLIMIT = 0x91
+ #: reserved
+ WARN_BUSY = 0x92
+
+
+class CbEvent(BYTE):
+ """
+ This enum defines events for the callback functions of the library.
+
+ .. seealso:: :meth:`UcanServer.get_status`
+ """
+
+ #: The USB-CANmodul has been initialized.
+ EVENT_INITHW = 0
+ #: The CAN interface has been initialized.
+ EVENT_init_can = 1
+ #: A new CAN message has been received.
+ EVENT_RECEIVE = 2
+ #: The error state in the module has changed.
+ EVENT_STATUS = 3
+ #: The CAN interface has been deinitialized.
+ EVENT_DEINIT_CAN = 4
+ #: The USB-CANmodul has been deinitialized.
+ EVENT_DEINITHW = 5
+ #: A new USB-CANmodul has been connected.
+ EVENT_CONNECT = 6
+ #: Any USB-CANmodul has been disconnected.
+ EVENT_DISCONNECT = 7
+ #: A USB-CANmodul has been disconnected during operation.
+ EVENT_FATALDISCON = 8
+ #: Reserved
+ EVENT_RESERVED1 = 0x80
+
+
+class CanStatus(WORD):
+ """
+ CAN error status bits. These bit values occurs in combination with the method :meth:`UcanServer.get_status`.
+
+ .. seealso::
+
+ :meth:`UcanServer.get_status`
+
+ :meth:`UcanServer.get_can_status_message`
+ """
+
+ #: No error.
+ CANERR_OK = 0x0
+ #: Transmit buffer of the CAN controller is full.
+ CANERR_XMTFULL = 0x1
+ #: Receive buffer of the CAN controller is full.
+ CANERR_OVERRUN = 0x2
+ #: Bus error: Error Limit 1 exceeded (Warning Limit reached)
+ CANERR_BUSLIGHT = 0x4
+ #: Bus error: Error Limit 2 exceeded (Error Passive)
+ CANERR_BUSHEAVY = 0x8
+ #: Bus error: CAN controller has gone into Bus-Off state.
+ #: Method :meth:`UcanServer.reset_can` has to be called.
+ CANERR_BUSOFF = 0x10
+ #: No CAN message is within the receive buffer.
+ CANERR_QRCVEMPTY = 0x20
+ #: Receive buffer is full. CAN messages has been lost.
+ CANERR_QOVERRUN = 0x40
+ #: Transmit buffer is full.
+ CANERR_QXMTFULL = 0x80
+ #: Register test of the CAN controller failed.
+ CANERR_REGTEST = 0x100
+ #: Memory test on hardware failed.
+ CANERR_MEMTEST = 0x200
+ #: Transmit CAN message(s) was/were automatically deleted by firmware (transmit timeout).
+ CANERR_TXMSGLOST = 0x400
+
+
+class UsbStatus(WORD):
+ """
+ USB error status bits. These bit values occurs in combination with the method :meth:`UcanServer.get_status`.
+
+ .. seealso:: :meth:`UcanServer.get_status`
+ """
+
+ #: No error.
+ USBERR_OK = 0x0
+
+
+#: Specifies the acceptance mask for receiving all CAN messages.
+#:
+#: .. seealso::
+#:
+#: :const:`ACR_ALL`
+#:
+#: :meth:`UcanServer.init_can`
+#:
+#: :meth:`UcanServer.set_acceptance`
+AMR_ALL = 0xFFFFFFFF
+
+#: Specifies the acceptance code for receiving all CAN messages.
+#:
+#: .. seealso::
+#:
+#: :const:`AMR_ALL`
+#:
+#: :meth:`UcanServer.init_can`
+#:
+#: :meth:`UcanServer.set_acceptance`
+ACR_ALL = 0x0
+
+
+class OutputControl(BYTE):
+ """
+ Specifies pre-defined values for the Output Control Register of SJA1000 on GW-001 and GW-002.
+ These values are only important for GW-001 and GW-002.
+ They does not have an effect on systec USB-CANmoduls.
+ """
+
+ #: default OCR value for the standard USB-CANmodul GW-001/GW-002
+ OCR_DEFAULT = 0x1A
+ #: OCR value for RS485 interface and galvanic isolation
+ OCR_RS485_ISOLATED = 0x1E
+ #: OCR value for RS485 interface but without galvanic isolation
+ OCR_RS485_NOT_ISOLATED = 0xA
+
+
+#: Specifies the default value for the maximum number of entries in the receive and transmit buffer.
+DEFAULT_BUFFER_ENTRIES = 4096
+
+
+class Channel(BYTE):
+ """
+ Specifies values for the CAN channel to be used on multi-channel USB-CANmoduls.
+ """
+
+ #: Specifies the first CAN channel (GW-001/GW-002 and USB-CANmodul1 only can be used with this channel).
+ CHANNEL_CH0 = 0
+ #: Specifies the second CAN channel (this channel cannot be used with GW-001/GW-002 and USB-CANmodul1).
+ CHANNEL_CH1 = 1
+ #: Specifies all CAN channels (can only be used with the method :meth:`UcanServer.shutdown`).
+ CHANNEL_ALL = 254
+ #: Specifies the use of any channel (can only be used with the method :meth:`UcanServer.read_can_msg`).
+ CHANNEL_ANY = 255
+ #: Specifies the first CAN channel (equivalent to :data:`CHANNEL_CH0`).
+ CHANNEL_CAN1 = CHANNEL_CH0
+ #: Specifies the second CAN channel (equivalent to :data:`CHANNEL_CH1`).
+ CHANNEL_CAN2 = CHANNEL_CH1
+ #: Specifies the LIN channel (currently not supported by the software).
+ CHANNEL_LIN = CHANNEL_CH1
+
+
+class ResetFlags(DWORD):
+ """
+ Specifies flags for resetting USB-CANmodul with method :meth:`UcanServer.reset_can`.
+ These flags can be used in combination.
+
+ .. seealso:: :meth:`UcanServer.reset_can`
+ """
+
+ #: reset everything
+ RESET_ALL = 0x0
+ #: no CAN status reset (only supported for systec USB-CANmoduls)
+ RESET_NO_STATUS = 0x1
+ #: no CAN controller reset
+ RESET_NO_CANCTRL = 0x2
+ #: no transmit message counter reset
+ RESET_NO_TXCOUNTER = 0x4
+ #: no receive message counter reset
+ RESET_NO_RXCOUNTER = 0x8
+ #: no transmit message buffer reset at channel level
+ RESET_NO_TXBUFFER_CH = 0x10
+ #: no transmit message buffer reset at USB-CAN-library level
+ RESET_NO_TXBUFFER_DLL = 0x20
+ #: no transmit message buffer reset at firmware level
+ RESET_NO_TXBUFFER_FW = 0x80
+ #: no receive message buffer reset at channel level
+ RESET_NO_RXBUFFER_CH = 0x100
+ #: no receive message buffer reset at USB-CAN-library level
+ RESET_NO_RXBUFFER_DLL = 0x200
+ #: no receive message buffer reset at kernel driver level
+ RESET_NO_RXBUFFER_SYS = 0x400
+ #: no receive message buffer reset at firmware level
+ RESET_NO_RXBUFFER_FW = 0x800
+ #: complete firmware reset (module will automatically reconnect at USB port in 500msec)
+ RESET_FIRMWARE = 0xFFFFFFFF
+
+ #: no reset of all message counters
+ RESET_NO_COUNTER_ALL = RESET_NO_TXCOUNTER | RESET_NO_RXCOUNTER
+ #: no reset of transmit message buffers at communication level (firmware, kernel and library)
+ RESET_NO_TXBUFFER_COMM = RESET_NO_TXBUFFER_DLL | 0x40 | RESET_NO_TXBUFFER_FW
+ #: no reset of receive message buffers at communication level (firmware, kernel and library)
+ RESET_NO_RXBUFFER_COMM = (
+ RESET_NO_RXBUFFER_DLL | RESET_NO_RXBUFFER_SYS | RESET_NO_RXBUFFER_FW
+ )
+ #: no reset of all transmit message buffers
+ RESET_NO_TXBUFFER_ALL = RESET_NO_TXBUFFER_CH | RESET_NO_TXBUFFER_COMM
+ #: no reset of all receive message buffers
+ RESET_NO_RXBUFFER_ALL = RESET_NO_RXBUFFER_CH | RESET_NO_RXBUFFER_COMM
+ #: no reset of all message buffers at communication level (firmware, kernel and library)
+ RESET_NO_BUFFER_COMM = RESET_NO_TXBUFFER_COMM | RESET_NO_RXBUFFER_COMM
+ #: no reset of all message buffers
+ RESET_NO_BUFFER_ALL = RESET_NO_TXBUFFER_ALL | RESET_NO_RXBUFFER_ALL
+ #: reset of the CAN status only
+ RESET_ONLY_STATUS = 0xFFFF & ~RESET_NO_STATUS
+ #: reset of the CAN controller only
+ RESET_ONLY_CANCTRL = 0xFFFF & ~RESET_NO_CANCTRL
+ #: reset of the transmit buffer in firmware only
+ RESET_ONLY_TXBUFFER_FW = 0xFFFF & ~RESET_NO_TXBUFFER_FW
+ #: reset of the receive buffer in firmware only
+ RESET_ONLY_RXBUFFER_FW = 0xFFFF & ~RESET_NO_RXBUFFER_FW
+ #: reset of the specified channel of the receive buffer only
+ RESET_ONLY_RXCHANNEL_BUFF = 0xFFFF & ~RESET_NO_RXBUFFER_CH
+ #: reset of the specified channel of the transmit buffer only
+ RESET_ONLY_TXCHANNEL_BUFF = 0xFFFF & ~RESET_NO_TXBUFFER_CH
+ #: reset of the receive buffer and receive message counter only
+ RESET_ONLY_RX_BUFF = 0xFFFF & ~(RESET_NO_RXBUFFER_ALL | RESET_NO_RXCOUNTER)
+ #: reset of the receive buffer and receive message counter (for GW-002) only
+ RESET_ONLY_RX_BUFF_GW002 = 0xFFFF & ~(
+ RESET_NO_RXBUFFER_ALL | RESET_NO_RXCOUNTER | RESET_NO_TXBUFFER_FW
+ )
+ #: reset of the transmit buffer and transmit message counter only
+ RESET_ONLY_TX_BUFF = 0xFFFF & ~(RESET_NO_TXBUFFER_ALL | RESET_NO_TXCOUNTER)
+ #: reset of all buffers and all message counters only
+ RESET_ONLY_ALL_BUFF = RESET_ONLY_RX_BUFF & RESET_ONLY_TX_BUFF
+ #: reset of all message counters only
+ RESET_ONLY_ALL_COUNTER = 0xFFFF & ~RESET_NO_COUNTER_ALL
+
+
+PRODCODE_PID_TWO_CHA = 0x1
+PRODCODE_PID_TERM = 0x1
+PRODCODE_PID_RBUSER = 0x1
+PRODCODE_PID_RBCAN = 0x1
+PRODCODE_PID_G4 = 0x20
+PRODCODE_PID_RESVD = 0x40
+
+PRODCODE_MASK_DID = 0xFFFF0000
+PRODCODE_MASK_PID = 0xFFFF
+PRODCODE_MASK_PIDG3 = PRODCODE_MASK_PID & 0xFFFFFFBF
+
+
+class ProductCode(WORD):
+ """
+ These values defines product codes for all known USB-CANmodul derivatives received in member
+ :attr:`HardwareInfoEx.m_dwProductCode` of structure :class:`HardwareInfoEx`
+ with method :meth:`UcanServer.get_hardware_info`.
+
+ .. seealso::
+
+ :meth:`UcanServer.get_hardware_info`
+
+ :class:`HardwareInfoEx`
+ """
+
+ #: Product code for GW-001 (outdated).
+ PRODCODE_PID_GW001 = 0x1100
+ #: Product code for GW-002 (outdated).
+ PRODCODE_PID_GW002 = 0x1102
+ #: Product code for Multiport CAN-to-USB G3.
+ PRODCODE_PID_MULTIPORT = 0x1103
+ #: Product code for USB-CANmodul1 G3.
+ PRODCODE_PID_BASIC = 0x1104
+ #: Product code for USB-CANmodul2 G3.
+ PRODCODE_PID_ADVANCED = 0x1105
+ #: Product code for USB-CANmodul8 G3.
+ PRODCODE_PID_USBCAN8 = 0x1107
+ #: Product code for USB-CANmodul16 G3.
+ PRODCODE_PID_USBCAN16 = 0x1109
+ #: Reserved.
+ PRODCODE_PID_RESERVED3 = 0x1110
+ #: Product code for USB-CANmodul2 G4.
+ PRODCODE_PID_ADVANCED_G4 = 0x1121
+ #: Product code for USB-CANmodul1 G4.
+ PRODCODE_PID_BASIC_G4 = 0x1122
+ #: Reserved.
+ PRODCODE_PID_RESERVED1 = 0x1144
+ #: Reserved.
+ PRODCODE_PID_RESERVED2 = 0x1145
+
+
+#: Definitions for cyclic CAN messages.
+MAX_CYCLIC_CAN_MSG = 16
+
+
+class CyclicFlags(DWORD):
+ """
+ Specifies flags for cyclical CAN messages.
+ These flags can be used in combinations with method :meth:`UcanServer.enable_cyclic_can_msg`.
+
+ .. seealso:: :meth:`UcanServer.enable_cyclic_can_msg`
+ """
+
+ #: Stops the transmission of cyclic CAN messages.
+ CYCLIC_FLAG_STOPP = 0x0
+ #: Global enable of transmission of cyclic CAN messages.
+ CYCLIC_FLAG_START = 0x80000000
+ #: List of cyclic CAN messages will be processed in sequential mode (otherwise in parallel mode).
+ CYCLIC_FLAG_SEQUMODE = 0x40000000
+ #: No echo will be sent back if echo mode is enabled with method :meth:`UcanServer.init_can`.
+ CYCLIC_FLAG_NOECHO = 0x10000
+ #: CAN message with index 0 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_0 = 0x1
+ #: CAN message with index 1 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_1 = 0x2
+ #: CAN message with index 2 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_2 = 0x4
+ #: CAN message with index 3 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_3 = 0x8
+ #: CAN message with index 4 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_4 = 0x10
+ #: CAN message with index 5 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_5 = 0x20
+ #: CAN message with index 6 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_6 = 0x40
+ #: CAN message with index 7 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_7 = 0x80
+ #: CAN message with index 8 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_8 = 0x100
+ #: CAN message with index 9 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_9 = 0x200
+ #: CAN message with index 10 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_10 = 0x400
+ #: CAN message with index 11 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_11 = 0x800
+ #: CAN message with index 12 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_12 = 0x1000
+ #: CAN message with index 13 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_13 = 0x2000
+ #: CAN message with index 14 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_14 = 0x4000
+ #: CAN message with index 15 of the list will not be sent.
+ CYCLIC_FLAG_LOCK_15 = 0x8000
+
+
+class PendingFlags(BYTE):
+ """
+ Specifies flags for method :meth:`UcanServer.get_msg_pending`.
+ These flags can be uses in combinations.
+
+ .. seealso:: :meth:`UcanServer.get_msg_pending`
+ """
+
+ #: number of pending CAN messages in receive buffer of USB-CAN-library
+ PENDING_FLAG_RX_DLL = 0x1
+ #: reserved
+ PENDING_FLAG_RX_SYS = 0x2
+ #: number of pending CAN messages in receive buffer of firmware
+ PENDING_FLAG_RX_FW = 0x4
+ #: number of pending CAN messages in transmit buffer of USB-CAN-library
+ PENDING_FLAG_TX_DLL = 0x10
+ #: reserved
+ PENDING_FLAG_TX_SYS = 0x20
+ #: number of pending CAN messages in transmit buffer of firmware
+ PENDING_FLAG_TX_FW = 0x40
+ #: number of pending CAN messages in all receive buffers
+ PENDING_FLAG_RX_ALL = PENDING_FLAG_RX_DLL | PENDING_FLAG_RX_SYS | PENDING_FLAG_RX_FW
+ #: number of pending CAN messages in all transmit buffers
+ PENDING_FLAG_TX_ALL = PENDING_FLAG_TX_DLL | PENDING_FLAG_TX_SYS | PENDING_FLAG_TX_FW
+ #: number of pending CAN messages in all buffers
+ PENDING_FLAG_ALL = PENDING_FLAG_RX_ALL | PENDING_FLAG_TX_ALL
+
+
+class Mode(BYTE):
+ """
+ Specifies values for operation mode of a CAN channel.
+ These values can be combined by OR operation with the method :meth:`UcanServer.init_can`.
+ """
+
+ #: normal operation mode (transmitting and receiving)
+ MODE_NORMAL = 0
+ #: listen only mode (receiving only, no ACK at CAN bus)
+ MODE_LISTEN_ONLY = 1
+ #: CAN messages which was sent will be received back with method :meth:`UcanServer.read_can_msg`
+ MODE_TX_ECHO = 2
+ #: reserved (not implemented in this version)
+ MODE_RX_ORDER_CH = 4
+ #: high resolution time stamps in received CAN messages (only available with STM derivatives)
+ MODE_HIGH_RES_TIMER = 8
+
+
+class VersionType(BYTE):
+ """
+ Specifies values for receiving the version information of several driver files.
+
+ .. note:: This structure is only used internally.
+ """
+
+ #: version of the USB-CAN-library
+ VER_TYPE_USER_LIB = 1
+ #: equivalent to :attr:`VER_TYPE_USER_LIB`
+ VER_TYPE_USER_DLL = 1
+ #: version of USBCAN.SYS (not supported in this version)
+ VER_TYPE_SYS_DRV = 2
+ #: version of firmware in hardware (not supported, use method :meth:`UcanServer.get_fw_version`)
+ VER_TYPE_FIRMWARE = 3
+ #: version of UCANNET.SYS
+ VER_TYPE_NET_DRV = 4
+ #: version of USBCANLD.SYS
+ VER_TYPE_SYS_LD = 5
+ #: version of USBCANL2.SYS
+ VER_TYPE_SYS_L2 = 6
+ #: version of USBCANL3.SYS
+ VER_TYPE_SYS_L3 = 7
+ #: version of USBCANL4.SYS
+ VER_TYPE_SYS_L4 = 8
+ #: version of USBCANL5.SYS
+ VER_TYPE_SYS_L5 = 9
+ #: version of USBCANCP.CPL
+ VER_TYPE_CPL = 10
diff --git a/can/interfaces/systec/exceptions.py b/can/interfaces/systec/exceptions.py
new file mode 100644
index 000000000..8768b412a
--- /dev/null
+++ b/can/interfaces/systec/exceptions.py
@@ -0,0 +1,105 @@
+from abc import ABC, abstractmethod
+
+from can import CanError
+
+from .constants import ReturnCode
+
+
+class UcanException(CanError, ABC):
+ """Base class for USB can errors."""
+
+ def __init__(self, result, func, arguments):
+ self.result = result
+ self.func = func
+ self.arguments = arguments
+
+ message = self._error_message_mapping.get(result, "unknown")
+ super().__init__(
+ message=f"Function {func.__name__} (called with {arguments}): {message}",
+ error_code=result.value,
+ )
+
+ @property
+ @abstractmethod
+ def _error_message_mapping(self) -> dict[ReturnCode, str]: ...
+
+
+class UcanError(UcanException):
+ """Exception class for errors from USB-CAN-library."""
+
+ _ERROR_MESSAGES = {
+ ReturnCode.ERR_RESOURCE: "could not created a resource (memory, handle, ...)",
+ ReturnCode.ERR_MAXMODULES: "the maximum number of opened modules is reached",
+ ReturnCode.ERR_HWINUSE: "the specified module is already in use",
+ ReturnCode.ERR_ILLVERSION: "the software versions of the module and library are incompatible",
+ ReturnCode.ERR_ILLHW: "the module with the specified device number is not connected "
+ "(or used by an other application)",
+ ReturnCode.ERR_ILLHANDLE: "wrong USB-CAN-Handle handed over to the function",
+ ReturnCode.ERR_ILLPARAM: "wrong parameter handed over to the function",
+ ReturnCode.ERR_BUSY: "instruction can not be processed at this time",
+ ReturnCode.ERR_TIMEOUT: "no answer from module",
+ ReturnCode.ERR_IOFAILED: "a request to the driver failed",
+ ReturnCode.ERR_DLL_TXFULL: "a CAN message did not fit into the transmit buffer",
+ ReturnCode.ERR_MAXINSTANCES: "maximum number of applications is reached",
+ ReturnCode.ERR_CANNOTINIT: "CAN interface is not yet initialized",
+ ReturnCode.ERR_DISCONECT: "USB-CANmodul was disconnected",
+ ReturnCode.ERR_NOHWCLASS: "the needed device class does not exist",
+ ReturnCode.ERR_ILLCHANNEL: "illegal CAN channel",
+ ReturnCode.ERR_RESERVED1: "reserved",
+ ReturnCode.ERR_ILLHWTYPE: "the API function can not be used with this hardware",
+ }
+
+ @property
+ def _error_message_mapping(self) -> dict[ReturnCode, str]:
+ return UcanError._ERROR_MESSAGES
+
+
+class UcanCmdError(UcanException):
+ """Exception class for errors from firmware in USB-CANmodul."""
+
+ _ERROR_MESSAGES = {
+ ReturnCode.ERRCMD_NOTEQU: "the received response does not match to the transmitted command",
+ ReturnCode.ERRCMD_REGTST: "no access to the CAN controller",
+ ReturnCode.ERRCMD_ILLCMD: "the module could not interpret the command",
+ ReturnCode.ERRCMD_EEPROM: "error while reading the EEPROM",
+ ReturnCode.ERRCMD_RESERVED1: "reserved",
+ ReturnCode.ERRCMD_RESERVED2: "reserved",
+ ReturnCode.ERRCMD_RESERVED3: "reserved",
+ ReturnCode.ERRCMD_ILLBDR: "illegal baud rate value specified in BTR0/BTR1 for systec "
+ "USB-CANmoduls",
+ ReturnCode.ERRCMD_NOTINIT: "CAN channel is not initialized",
+ ReturnCode.ERRCMD_ALREADYINIT: "CAN channel is already initialized",
+ ReturnCode.ERRCMD_ILLSUBCMD: "illegal sub-command specified",
+ ReturnCode.ERRCMD_ILLIDX: "illegal index specified (e.g. index for cyclic CAN messages)",
+ ReturnCode.ERRCMD_RUNNING: "cyclic CAN message(s) can not be defined because transmission of "
+ "cyclic CAN messages is already running",
+ }
+
+ @property
+ def _error_message_mapping(self) -> dict[ReturnCode, str]:
+ return UcanCmdError._ERROR_MESSAGES
+
+
+class UcanWarning(UcanException):
+ """Exception class for warnings, the function has been executed anyway."""
+
+ _ERROR_MESSAGES = {
+ ReturnCode.WARN_NODATA: "no CAN messages received",
+ ReturnCode.WARN_SYS_RXOVERRUN: "overrun in receive buffer of the kernel driver",
+ ReturnCode.WARN_DLL_RXOVERRUN: "overrun in receive buffer of the USB-CAN-library",
+ ReturnCode.WARN_RESERVED1: "reserved",
+ ReturnCode.WARN_RESERVED2: "reserved",
+ ReturnCode.WARN_FW_TXOVERRUN: "overrun in transmit buffer of the firmware (but this CAN message "
+ "was successfully stored in buffer of the ibrary)",
+ ReturnCode.WARN_FW_RXOVERRUN: "overrun in receive buffer of the firmware (but this CAN message "
+ "was successfully read)",
+ ReturnCode.WARN_FW_TXMSGLOST: "reserved",
+ ReturnCode.WARN_NULL_PTR: "pointer is NULL",
+ ReturnCode.WARN_TXLIMIT: "not all CAN messages could be stored to the transmit buffer in "
+ "USB-CAN-library",
+ ReturnCode.WARN_BUSY: "reserved",
+ }
+
+ @property
+ def _error_message_mapping(self) -> dict[ReturnCode, str]:
+ return UcanWarning._ERROR_MESSAGES
diff --git a/can/interfaces/systec/structures.py b/can/interfaces/systec/structures.py
new file mode 100644
index 000000000..a50ac4c26
--- /dev/null
+++ b/can/interfaces/systec/structures.py
@@ -0,0 +1,480 @@
+import os
+from ctypes import POINTER, Structure, sizeof
+from ctypes import (
+ c_long as BOOL,
+)
+from ctypes import (
+ c_ubyte as BYTE,
+)
+from ctypes import (
+ c_ulong as DWORD,
+)
+from ctypes import (
+ c_ushort as WORD,
+)
+from ctypes import (
+ c_void_p as LPVOID,
+)
+
+# Workaround for Unix based platforms to be able to load structures for testing, etc...
+if os.name == "nt":
+ from ctypes import WINFUNCTYPE as FUNCTYPE
+else:
+ from ctypes import CFUNCTYPE as FUNCTYPE
+
+from .constants import MsgFrameFormat
+
+
+class CanMsg(Structure):
+ """
+ Structure of a CAN message.
+
+ .. seealso::
+
+ :meth:`UcanServer.read_can_msg`
+
+ :meth:`UcanServer.write_can_msg`
+
+ :meth:`UcanServer.define_cyclic_can_msg`
+
+ :meth:`UcanServer.read_cyclic_can_msg`
+ """
+
+ _pack_ = 1
+ _fields_ = [
+ ("m_dwID", DWORD), # CAN Identifier
+ ("m_bFF", BYTE), # CAN Frame Format (see enum :class:`MsgFrameFormat`)
+ ("m_bDLC", BYTE), # CAN Data Length Code
+ ("m_bData", BYTE * 8), # CAN Data (array of 8 bytes)
+ (
+ "m_dwTime",
+ DWORD,
+ ), # Receive time stamp in ms (for transmit messages no meaning)
+ ]
+ __hash__ = Structure.__hash__
+
+ def __init__(
+ self, id_=0, frame_format=MsgFrameFormat.MSG_FF_STD, data=None, dlc=None
+ ):
+ data = [] if data is None else data
+ dlc = len(data) if dlc is None else dlc
+ super().__init__(id_, frame_format, dlc, (BYTE * 8)(*data), 0)
+
+ def __eq__(self, other):
+ if not isinstance(other, CanMsg):
+ return False
+
+ return (
+ self.id == other.id
+ and self.frame_format == other.frame_format
+ and self.data == other.data
+ )
+
+ @property
+ def id(self):
+ return self.m_dwID
+
+ @id.setter
+ def id(self, value):
+ self.m_dwID = value
+
+ @property
+ def frame_format(self):
+ return self.m_bFF
+
+ @frame_format.setter
+ def frame_format(self, frame_format):
+ self.m_bFF = frame_format
+
+ @property
+ def data(self):
+ return self.m_bData[: self.m_bDLC]
+
+ @data.setter
+ def data(self, data):
+ self.m_bDLC = len(data)
+ self.m_bData((BYTE * 8)(*data))
+
+ @property
+ def time(self):
+ return self.m_dwTime
+
+
+class Status(Structure):
+ """
+ Structure with the error status of CAN and USB.
+ Use this structure with the method :meth:`UcanServer.get_status`
+
+ .. seealso::
+
+ :meth:`UcanServer.get_status`
+
+ :meth:`UcanServer.get_can_status_message`
+ """
+
+ _pack_ = 1
+ _fields_ = [
+ ("m_wCanStatus", WORD), # CAN error status (see enum :class:`CanStatus`)
+ ("m_wUsbStatus", WORD), # USB error status (see enum :class:`UsbStatus`)
+ ]
+ __hash__ = Structure.__hash__
+
+ def __eq__(self, other):
+ if not isinstance(other, Status):
+ return False
+
+ return (
+ self.can_status == other.can_status and self.usb_status == other.usb_status
+ )
+
+ @property
+ def can_status(self):
+ return self.m_wCanStatus
+
+ @property
+ def usb_status(self):
+ return self.m_wUsbStatus
+
+
+class InitCanParam(Structure):
+ """
+ Structure including initialisation parameters used internally in :meth:`UcanServer.init_can`.
+
+ .. note:: This structure is only used internally.
+ """
+
+ _pack_ = 1
+ _fields_ = [
+ ("m_dwSize", DWORD), # size of this structure (only used internally)
+ (
+ "m_bMode",
+ BYTE,
+ ), # selects the mode of CAN controller (see enum :class:`Mode`)
+ # Baudrate Registers for GW-001 or GW-002
+ ("m_bBTR0", BYTE), # Bus Timing Register 0 (see enum :class:`Baudrate`)
+ ("m_bBTR1", BYTE), # Bus Timing Register 1 (see enum :class:`Baudrate`)
+ ("m_bOCR", BYTE), # Output Control Register (see enum :class:`OutputControl`)
+ (
+ "m_dwAMR",
+ DWORD,
+ ), # Acceptance Mask Register (see method :meth:`UcanServer.set_acceptance`)
+ (
+ "m_dwACR",
+ DWORD,
+ ), # Acceptance Code Register (see method :meth:`UcanServer.set_acceptance`)
+ ("m_dwBaudrate", DWORD), # Baudrate Register for all systec USB-CANmoduls
+ # (see enum :class:`BaudrateEx`)
+ (
+ "m_wNrOfRxBufferEntries",
+ WORD,
+ ), # number of receive buffer entries (default is 4096)
+ (
+ "m_wNrOfTxBufferEntries",
+ WORD,
+ ), # number of transmit buffer entries (default is 4096)
+ ]
+ __hash__ = Structure.__hash__
+
+ def __init__(
+ self, mode, BTR, OCR, AMR, ACR, baudrate, rx_buffer_entries, tx_buffer_entries
+ ):
+ super().__init__(
+ sizeof(InitCanParam),
+ mode,
+ BTR >> 8,
+ BTR,
+ OCR,
+ AMR,
+ ACR,
+ baudrate,
+ rx_buffer_entries,
+ tx_buffer_entries,
+ )
+
+ def __eq__(self, other):
+ if not isinstance(other, InitCanParam):
+ return False
+
+ return (
+ self.mode == other.mode
+ and self.BTR == other.BTR
+ and self.OCR == other.OCR
+ and self.baudrate == other.baudrate
+ and self.rx_buffer_entries == other.rx_buffer_entries
+ and self.tx_buffer_entries == other.tx_buffer_entries
+ )
+
+ @property
+ def mode(self):
+ return self.m_bMode
+
+ @mode.setter
+ def mode(self, mode):
+ self.m_bMode = mode
+
+ @property
+ def BTR(self):
+ return self.m_bBTR0 << 8 | self.m_bBTR1
+
+ @BTR.setter
+ def BTR(self, BTR):
+ self.m_bBTR0, self.m_bBTR1 = BTR >> 8, BTR
+
+ @property
+ def OCR(self):
+ return self.m_bOCR
+
+ @OCR.setter
+ def OCR(self, OCR):
+ self.m_bOCR = OCR
+
+ @property
+ def baudrate(self):
+ return self.m_dwBaudrate
+
+ @baudrate.setter
+ def baudrate(self, baudrate):
+ self.m_dwBaudrate = baudrate
+
+ @property
+ def rx_buffer_entries(self):
+ return self.m_wNrOfRxBufferEntries
+
+ @rx_buffer_entries.setter
+ def rx_buffer_entries(self, rx_buffer_entries):
+ self.m_wNrOfRxBufferEntries = rx_buffer_entries
+
+ @property
+ def tx_buffer_entries(self):
+ return self.m_wNrOfTxBufferEntries
+
+ @tx_buffer_entries.setter
+ def tx_buffer_entries(self, tx_buffer_entries):
+ self.m_wNrOfTxBufferEntries = tx_buffer_entries
+
+
+class Handle(BYTE):
+ pass
+
+
+class HardwareInfoEx(Structure):
+ """
+ Structure including hardware information about the USB-CANmodul.
+ This structure is used with the method :meth:`UcanServer.get_hardware_info`.
+
+ .. seealso:: :meth:`UcanServer.get_hardware_info`
+ """
+
+ _pack_ = 1
+ _fields_ = [
+ ("m_dwSize", DWORD), # size of this structure (only used internally)
+ ("m_UcanHandle", Handle), # USB-CAN-Handle assigned by the DLL
+ ("m_bDeviceNr", BYTE), # device number of the USB-CANmodul
+ ("m_dwSerialNr", DWORD), # serial number from USB-CANmodul
+ ("m_dwFwVersionEx", DWORD), # version of firmware
+ ("m_dwProductCode", DWORD), # product code (see enum :class:`ProductCode`)
+ # unique ID (available since V5.01) !!! m_dwSize must be >= HWINFO_SIZE_V2
+ ("m_dwUniqueId0", DWORD),
+ ("m_dwUniqueId1", DWORD),
+ ("m_dwUniqueId2", DWORD),
+ ("m_dwUniqueId3", DWORD),
+ ("m_dwFlags", DWORD), # additional flags
+ ]
+ __hash__ = Structure.__hash__
+
+ def __init__(self):
+ super().__init__(sizeof(HardwareInfoEx))
+
+ def __eq__(self, other):
+ if not isinstance(other, HardwareInfoEx):
+ return False
+
+ return (
+ self.device_number == other.device_number
+ and self.serial == other.serial
+ and self.fw_version == other.fw_version
+ and self.product_code == other.product_code
+ and self.unique_id == other.unique_id
+ and self.flags == other.flags
+ )
+
+ @property
+ def device_number(self):
+ return self.m_bDeviceNr
+
+ @property
+ def serial(self):
+ return self.m_dwSerialNr
+
+ @property
+ def fw_version(self):
+ return self.m_dwFwVersionEx
+
+ @property
+ def product_code(self):
+ return self.m_dwProductCode
+
+ @property
+ def unique_id(self):
+ return (
+ self.m_dwUniqueId0,
+ self.m_dwUniqueId1,
+ self.m_dwUniqueId2,
+ self.m_dwUniqueId3,
+ )
+
+ @property
+ def flags(self):
+ return self.m_dwFlags
+
+
+# void PUBLIC UcanCallbackFktEx (Handle UcanHandle_p, DWORD dwEvent_p,
+# BYTE bChannel_p, void* pArg_p);
+CallbackFktEx = FUNCTYPE(None, Handle, DWORD, BYTE, LPVOID)
+
+
+class HardwareInitInfo(Structure):
+ """
+ Structure including information about the enumeration of USB-CANmoduls.
+
+ .. seealso:: :meth:`UcanServer.enumerate_hardware`
+
+ .. note:: This structure is only used internally.
+ """
+
+ _pack_ = 1
+ _fields_ = [
+ ("m_dwSize", DWORD), # size of this structure
+ (
+ "m_fDoInitialize",
+ BOOL,
+ ), # specifies if the found module should be initialized by the DLL
+ ("m_pUcanHandle", Handle), # pointer to variable receiving the USB-CAN-Handle
+ ("m_fpCallbackFktEx", CallbackFktEx), # pointer to callback function
+ (
+ "m_pCallbackArg",
+ LPVOID,
+ ), # pointer to user defined parameter for callback function
+ ("m_fTryNext", BOOL), # specifies if a further module should be found
+ ]
+
+
+class ChannelInfo(Structure):
+ """
+ Structure including CAN channel information.
+ This structure is used with the method :meth:`UcanServer.get_hardware_info`.
+
+ .. seealso:: :meth:`UcanServer.get_hardware_info`
+ """
+
+ _pack_ = 1
+ _fields_ = [
+ ("m_dwSize", DWORD), # size of this structure
+ ("m_bMode", BYTE), # operation mode of CAN controller (see enum :class:`Mode`)
+ ("m_bBTR0", BYTE), # Bus Timing Register 0 (see enum :class:`Baudrate`)
+ ("m_bBTR1", BYTE), # Bus Timing Register 1 (see enum :class:`Baudrate`)
+ ("m_bOCR", BYTE), # Output Control Register (see enum :class:`OutputControl`)
+ (
+ "m_dwAMR",
+ DWORD,
+ ), # Acceptance Mask Register (see method :meth:`UcanServer.set_acceptance`)
+ (
+ "m_dwACR",
+ DWORD,
+ ), # Acceptance Code Register (see method :meth:`UcanServer.set_acceptance`)
+ ("m_dwBaudrate", DWORD), # Baudrate Register for all systec USB-CANmoduls
+ # (see enum :class:`BaudrateEx`)
+ (
+ "m_fCanIsInit",
+ BOOL,
+ ), # True if the CAN interface is initialized, otherwise false
+ (
+ "m_wCanStatus",
+ WORD,
+ ), # CAN status (same as received by method :meth:`UcanServer.get_status`)
+ ]
+ __hash__ = Structure.__hash__
+
+ def __init__(self):
+ super().__init__(sizeof(ChannelInfo))
+
+ def __eq__(self, other):
+ if not isinstance(other, ChannelInfo):
+ return False
+
+ return (
+ self.mode == other.mode
+ and self.BTR == other.BTR
+ and self.OCR == other.OCR
+ and self.AMR == other.AMR
+ and self.ACR == other.ACR
+ and self.baudrate == other.baudrate
+ and self.can_is_init == other.can_is_init
+ and self.can_status == other.can_status
+ )
+
+ @property
+ def mode(self):
+ return self.m_bMode
+
+ @property
+ def BTR(self):
+ return self.m_bBTR0 << 8 | self.m_bBTR1
+
+ @property
+ def OCR(self):
+ return self.m_bOCR
+
+ @property
+ def AMR(self):
+ return self.m_dwAMR
+
+ @property
+ def ACR(self):
+ return self.m_dwACR
+
+ @property
+ def baudrate(self):
+ return self.m_dwBaudrate
+
+ @property
+ def can_is_init(self):
+ return self.m_fCanIsInit
+
+ @property
+ def can_status(self):
+ return self.m_wCanStatus
+
+
+class MsgCountInfo(Structure):
+ """
+ Structure including the number of sent and received CAN messages.
+ This structure is used with the method :meth:`UcanServer.get_msg_count_info`.
+
+ .. seealso:: :meth:`UcanServer.get_msg_count_info`
+
+ .. note:: This structure is only used internally.
+ """
+
+ _fields_ = [
+ ("m_wSentMsgCount", WORD), # number of sent CAN messages
+ ("m_wRecvdMsgCount", WORD), # number of received CAN messages
+ ]
+
+ @property
+ def sent_msg_count(self):
+ return self.m_wSentMsgCount
+
+ @property
+ def recv_msg_count(self):
+ return self.m_wRecvdMsgCount
+
+
+# void (PUBLIC *ConnectControlFktEx) (DWORD dwEvent_p, DWORD dwParam_p, void* pArg_p);
+ConnectControlFktEx = FUNCTYPE(None, DWORD, DWORD, LPVOID)
+
+# typedef void (PUBLIC *EnumCallback) (DWORD dwIndex_p, BOOL fIsUsed_p,
+# HardwareInfoEx* pHwInfoEx_p, HardwareInitInfo* pInitInfo_p, void* pArg_p);
+EnumCallback = FUNCTYPE(
+ None, DWORD, BOOL, POINTER(HardwareInfoEx), POINTER(HardwareInitInfo), LPVOID
+)
diff --git a/can/interfaces/systec/ucan.py b/can/interfaces/systec/ucan.py
new file mode 100644
index 000000000..f969532d7
--- /dev/null
+++ b/can/interfaces/systec/ucan.py
@@ -0,0 +1,1168 @@
+import logging
+import sys
+from ctypes import byref
+from ctypes import c_wchar_p as LPWSTR
+
+from ...exceptions import CanInterfaceNotImplementedError
+from .constants import *
+from .exceptions import *
+from .structures import *
+
+log = logging.getLogger("can.systec")
+
+
+def check_valid_rx_can_msg(result):
+ """
+ Checks if function :meth:`UcanServer.read_can_msg` returns a valid CAN message.
+
+ :param ReturnCode result: Error code of the function.
+ :return: True if a valid CAN messages was received, otherwise False.
+ :rtype: bool
+ """
+ return (result.value == ReturnCode.SUCCESSFUL) or (
+ result.value > ReturnCode.WARNING
+ )
+
+
+def check_tx_ok(result):
+ """
+ Checks if function :meth:`UcanServer.write_can_msg` successfully wrote CAN message(s).
+
+ While using :meth:`UcanServer.write_can_msg_ex` the number of sent CAN messages can be less than
+ the number of CAN messages which should be sent.
+
+ :param ReturnCode result: Error code of the function.
+ :return: True if CAN message(s) was(were) written successfully, otherwise False.
+ :rtype: bool
+
+ .. :seealso: :const:`ReturnCode.WARN_TXLIMIT`
+ """
+ return (result.value == ReturnCode.SUCCESSFUL) or (
+ result.value > ReturnCode.WARNING
+ )
+
+
+def check_tx_success(result):
+ """
+ Checks if function :meth:`UcanServer.write_can_msg_ex` successfully wrote all CAN message(s).
+
+ :param ReturnCode result: Error code of the function.
+ :return: True if CAN message(s) was(were) written successfully, otherwise False.
+ :rtype: bool
+ """
+ return result.value == ReturnCode.SUCCESSFUL
+
+
+def check_tx_not_all(result):
+ """
+ Checks if function :meth:`UcanServer.write_can_msg_ex` did not sent all CAN messages.
+
+ :param ReturnCode result: Error code of the function.
+ :return: True if not all CAN messages were written, otherwise False.
+ :rtype: bool
+ """
+ return result.value == ReturnCode.WARN_TXLIMIT
+
+
+def check_warning(result):
+ """
+ Checks if any function returns a warning.
+
+ :param ReturnCode result: Error code of the function.
+ :return: True if a function returned warning, otherwise False.
+ :rtype: bool
+ """
+ return result.value >= ReturnCode.WARNING
+
+
+def check_error(result):
+ """
+ Checks if any function returns an error from USB-CAN-library.
+
+ :param ReturnCode result: Error code of the function.
+ :return: True if a function returned error, otherwise False.
+ :rtype: bool
+ """
+ return (result.value != ReturnCode.SUCCESSFUL) and (
+ result.value < ReturnCode.WARNING
+ )
+
+
+def check_error_cmd(result):
+ """
+ Checks if any function returns an error from firmware in USB-CANmodul.
+
+ :param ReturnCode result: Error code of the function.
+ :return: True if a function returned error from firmware, otherwise False.
+ :rtype: bool
+ """
+ return (result.value >= ReturnCode.ERRCMD) and (result.value < ReturnCode.WARNING)
+
+
+def check_result(result, func, arguments):
+ if check_warning(result) and (result.value != ReturnCode.WARN_NODATA):
+ log.warning(UcanWarning(result, func, arguments))
+ elif check_error(result):
+ if check_error_cmd(result):
+ raise UcanCmdError(result, func, arguments)
+ else:
+ raise UcanError(result, func, arguments)
+ return result
+
+
+_UCAN_INITIALIZED = False
+if os.name != "nt":
+ log.warning("SYSTEC ucan library does not work on %s platform.", sys.platform)
+else:
+ from ctypes import WinDLL
+
+ try:
+ # Select the proper dll architecture
+ lib = WinDLL("usbcan64.dll" if sys.maxsize > 2**32 else "usbcan32.dll")
+
+ # BOOL PUBLIC UcanSetDebugMode (DWORD dwDbgLevel_p, _TCHAR* pszFilePathName_p, DWORD dwFlags_p);
+ UcanSetDebugMode = lib.UcanSetDebugMode
+ UcanSetDebugMode.restype = BOOL
+ UcanSetDebugMode.argtypes = [DWORD, LPWSTR, DWORD]
+
+ # DWORD PUBLIC UcanGetVersionEx (VersionType VerType_p);
+ UcanGetVersionEx = lib.UcanGetVersionEx
+ UcanGetVersionEx.restype = DWORD
+ UcanGetVersionEx.argtypes = [VersionType]
+
+ # DWORD PUBLIC UcanGetFwVersion (Handle UcanHandle_p);
+ UcanGetFwVersion = lib.UcanGetFwVersion
+ UcanGetFwVersion.restype = DWORD
+ UcanGetFwVersion.argtypes = [Handle]
+
+ # BYTE PUBLIC UcanInitHwConnectControlEx (ConnectControlFktEx fpConnectControlFktEx_p, void* pCallbackArg_p);
+ UcanInitHwConnectControlEx = lib.UcanInitHwConnectControlEx
+ UcanInitHwConnectControlEx.restype = ReturnCode
+ UcanInitHwConnectControlEx.argtypes = [ConnectControlFktEx, LPVOID]
+ UcanInitHwConnectControlEx.errcheck = check_result
+
+ # BYTE PUBLIC UcanDeinitHwConnectControl (void)
+ UcanDeinitHwConnectControl = lib.UcanDeinitHwConnectControl
+ UcanDeinitHwConnectControl.restype = ReturnCode
+ UcanDeinitHwConnectControl.argtypes = []
+ UcanDeinitHwConnectControl.errcheck = check_result
+
+ # DWORD PUBLIC UcanEnumerateHardware (EnumCallback fpCallback_p, void* pCallbackArg_p,
+ # BOOL fEnumUsedDevs_p,
+ # BYTE bDeviceNrLow_p, BYTE bDeviceNrHigh_p,
+ # DWORD dwSerialNrLow_p, DWORD dwSerialNrHigh_p,
+ # DWORD dwProductCodeLow_p, DWORD dwProductCodeHigh_p);
+ UcanEnumerateHardware = lib.UcanEnumerateHardware
+ UcanEnumerateHardware.restype = DWORD
+ UcanEnumerateHardware.argtypes = [
+ EnumCallback,
+ LPVOID,
+ BOOL,
+ BYTE,
+ BYTE,
+ DWORD,
+ DWORD,
+ DWORD,
+ DWORD,
+ ]
+
+ # BYTE PUBLIC UcanInitHardwareEx (Handle* pUcanHandle_p, BYTE bDeviceNr_p,
+ # CallbackFktEx fpCallbackFktEx_p, void* pCallbackArg_p);
+ UcanInitHardwareEx = lib.UcanInitHardwareEx
+ UcanInitHardwareEx.restype = ReturnCode
+ UcanInitHardwareEx.argtypes = [POINTER(Handle), BYTE, CallbackFktEx, LPVOID]
+ UcanInitHardwareEx.errcheck = check_result
+
+ # BYTE PUBLIC UcanInitHardwareEx2 (Handle* pUcanHandle_p, DWORD dwSerialNr_p,
+ # CallbackFktEx fpCallbackFktEx_p, void* pCallbackArg_p);
+ UcanInitHardwareEx2 = lib.UcanInitHardwareEx2
+ UcanInitHardwareEx2.restype = ReturnCode
+ UcanInitHardwareEx2.argtypes = [POINTER(Handle), DWORD, CallbackFktEx, LPVOID]
+ UcanInitHardwareEx2.errcheck = check_result
+
+ # BYTE PUBLIC UcanGetModuleTime (Handle UcanHandle_p, DWORD* pdwTime_p);
+ UcanGetModuleTime = lib.UcanGetModuleTime
+ UcanGetModuleTime.restype = ReturnCode
+ UcanGetModuleTime.argtypes = [Handle, POINTER(DWORD)]
+ UcanGetModuleTime.errcheck = check_result
+
+ # BYTE PUBLIC UcanGetHardwareInfoEx2 (Handle UcanHandle_p,
+ # HardwareInfoEx* pHwInfo_p,
+ # ChannelInfo* pCanInfoCh0_p, ChannelInfo* pCanInfoCh1_p);
+ UcanGetHardwareInfoEx2 = lib.UcanGetHardwareInfoEx2
+ UcanGetHardwareInfoEx2.restype = ReturnCode
+ UcanGetHardwareInfoEx2.argtypes = [
+ Handle,
+ POINTER(HardwareInfoEx),
+ POINTER(ChannelInfo),
+ POINTER(ChannelInfo),
+ ]
+ UcanGetHardwareInfoEx2.errcheck = check_result
+
+ # BYTE PUBLIC UcanInitCanEx2 (Handle UcanHandle_p, BYTE bChannel_p, tUcaninit_canParam* pinit_canParam_p);
+ UcanInitCanEx2 = lib.UcanInitCanEx2
+ UcanInitCanEx2.restype = ReturnCode
+ UcanInitCanEx2.argtypes = [Handle, BYTE, POINTER(InitCanParam)]
+ UcanInitCanEx2.errcheck = check_result
+
+ # BYTE PUBLIC UcanSetBaudrateEx (Handle UcanHandle_p,
+ # BYTE bChannel_p, BYTE bBTR0_p, BYTE bBTR1_p, DWORD dwBaudrate_p);
+ UcanSetBaudrateEx = lib.UcanSetBaudrateEx
+ UcanSetBaudrateEx.restype = ReturnCode
+ UcanSetBaudrateEx.argtypes = [Handle, BYTE, BYTE, BYTE, DWORD]
+ UcanSetBaudrateEx.errcheck = check_result
+
+ # BYTE PUBLIC UcanSetAcceptanceEx (Handle UcanHandle_p, BYTE bChannel_p,
+ # DWORD dwAMR_p, DWORD dwACR_p);
+ UcanSetAcceptanceEx = lib.UcanSetAcceptanceEx
+ UcanSetAcceptanceEx.restype = ReturnCode
+ UcanSetAcceptanceEx.argtypes = [Handle, BYTE, DWORD, DWORD]
+ UcanSetAcceptanceEx.errcheck = check_result
+
+ # BYTE PUBLIC UcanResetCanEx (Handle UcanHandle_p, BYTE bChannel_p, DWORD dwResetFlags_p);
+ UcanResetCanEx = lib.UcanResetCanEx
+ UcanResetCanEx.restype = ReturnCode
+ UcanResetCanEx.argtypes = [Handle, BYTE, DWORD]
+ UcanResetCanEx.errcheck = check_result
+
+ # BYTE PUBLIC UcanReadCanMsgEx (Handle UcanHandle_p, BYTE* pbChannel_p,
+ # CanMsg* pCanMsg_p, DWORD* pdwCount_p);
+ UcanReadCanMsgEx = lib.UcanReadCanMsgEx
+ UcanReadCanMsgEx.restype = ReturnCode
+ UcanReadCanMsgEx.argtypes = [
+ Handle,
+ POINTER(BYTE),
+ POINTER(CanMsg),
+ POINTER(DWORD),
+ ]
+ UcanReadCanMsgEx.errcheck = check_result
+
+ # BYTE PUBLIC UcanWriteCanMsgEx (Handle UcanHandle_p, BYTE bChannel_p,
+ # CanMsg* pCanMsg_p, DWORD* pdwCount_p);
+ UcanWriteCanMsgEx = lib.UcanWriteCanMsgEx
+ UcanWriteCanMsgEx.restype = ReturnCode
+ UcanWriteCanMsgEx.argtypes = [Handle, BYTE, POINTER(CanMsg), POINTER(DWORD)]
+ UcanWriteCanMsgEx.errcheck = check_result
+
+ # BYTE PUBLIC UcanGetStatusEx (Handle UcanHandle_p, BYTE bChannel_p, Status* pStatus_p);
+ UcanGetStatusEx = lib.UcanGetStatusEx
+ UcanGetStatusEx.restype = ReturnCode
+ UcanGetStatusEx.argtypes = [Handle, BYTE, POINTER(Status)]
+ UcanGetStatusEx.errcheck = check_result
+
+ # BYTE PUBLIC UcanGetMsgCountInfoEx (Handle UcanHandle_p, BYTE bChannel_p,
+ # MsgCountInfo* pMsgCountInfo_p);
+ UcanGetMsgCountInfoEx = lib.UcanGetMsgCountInfoEx
+ UcanGetMsgCountInfoEx.restype = ReturnCode
+ UcanGetMsgCountInfoEx.argtypes = [Handle, BYTE, POINTER(MsgCountInfo)]
+ UcanGetMsgCountInfoEx.errcheck = check_result
+
+ # BYTE PUBLIC UcanGetMsgPending (Handle UcanHandle_p,
+ # BYTE bChannel_p, DWORD dwFlags_p, DWORD* pdwPendingCount_p);
+ UcanGetMsgPending = lib.UcanGetMsgPending
+ UcanGetMsgPending.restype = ReturnCode
+ UcanGetMsgPending.argtypes = [Handle, BYTE, DWORD, POINTER(DWORD)]
+ UcanGetMsgPending.errcheck = check_result
+
+ # BYTE PUBLIC UcanGetCanErrorCounter (Handle UcanHandle_p,
+ # BYTE bChannel_p, DWORD* pdwTxErrorCounter_p, DWORD* pdwRxErrorCounter_p);
+ UcanGetCanErrorCounter = lib.UcanGetCanErrorCounter
+ UcanGetCanErrorCounter.restype = ReturnCode
+ UcanGetCanErrorCounter.argtypes = [Handle, BYTE, POINTER(DWORD), POINTER(DWORD)]
+ UcanGetCanErrorCounter.errcheck = check_result
+
+ # BYTE PUBLIC UcanSetTxTimeout (Handle UcanHandle_p,
+ # BYTE bChannel_p, DWORD dwTxTimeout_p);
+ UcanSetTxTimeout = lib.UcanSetTxTimeout
+ UcanSetTxTimeout.restype = ReturnCode
+ UcanSetTxTimeout.argtypes = [Handle, BYTE, DWORD]
+ UcanSetTxTimeout.errcheck = check_result
+
+ # BYTE PUBLIC UcanDeinitCanEx (Handle UcanHandle_p, BYTE bChannel_p);
+ UcanDeinitCanEx = lib.UcanDeinitCanEx
+ UcanDeinitCanEx.restype = ReturnCode
+ UcanDeinitCanEx.argtypes = [Handle, BYTE]
+ UcanDeinitCanEx.errcheck = check_result
+
+ # BYTE PUBLIC UcanDeinitHardware (Handle UcanHandle_p);
+ UcanDeinitHardware = lib.UcanDeinitHardware
+ UcanDeinitHardware.restype = ReturnCode
+ UcanDeinitHardware.argtypes = [Handle]
+ UcanDeinitHardware.errcheck = check_result
+
+ # BYTE PUBLIC UcanDefineCyclicCanMsg (Handle UcanHandle_p,
+ # BYTE bChannel_p, CanMsg* pCanMsgList_p, DWORD dwCount_p);
+ UcanDefineCyclicCanMsg = lib.UcanDefineCyclicCanMsg
+ UcanDefineCyclicCanMsg.restype = ReturnCode
+ UcanDefineCyclicCanMsg.argtypes = [Handle, BYTE, POINTER(CanMsg), DWORD]
+ UcanDefineCyclicCanMsg.errcheck = check_result
+
+ # BYTE PUBLIC UcanReadCyclicCanMsg (Handle UcanHandle_p,
+ # BYTE bChannel_p, CanMsg* pCanMsgList_p, DWORD* pdwCount_p);
+ UcanReadCyclicCanMsg = lib.UcanReadCyclicCanMsg
+ UcanReadCyclicCanMsg.restype = ReturnCode
+ UcanReadCyclicCanMsg.argtypes = [Handle, BYTE, POINTER(CanMsg), POINTER(DWORD)]
+ UcanReadCyclicCanMsg.errcheck = check_result
+
+ # BYTE PUBLIC UcanEnableCyclicCanMsg (Handle UcanHandle_p,
+ # BYTE bChannel_p, DWORD dwFlags_p);
+ UcanEnableCyclicCanMsg = lib.UcanEnableCyclicCanMsg
+ UcanEnableCyclicCanMsg.restype = ReturnCode
+ UcanEnableCyclicCanMsg.argtypes = [Handle, BYTE, DWORD]
+ UcanEnableCyclicCanMsg.errcheck = check_result
+
+ _UCAN_INITIALIZED = True
+
+ except Exception as ex:
+ log.warning("Cannot load SYSTEC ucan library: %s.", ex)
+
+
+class UcanServer:
+ """
+ UcanServer is a Python wrapper class for using the usbcan32.dll / usbcan64.dll.
+ """
+
+ _modules_found = []
+ _connect_control_ref = None
+
+ def __init__(self):
+ if not _UCAN_INITIALIZED:
+ raise CanInterfaceNotImplementedError(
+ "The interface could not be loaded on the current platform"
+ )
+
+ self._handle = Handle(INVALID_HANDLE)
+ self._is_initialized = False
+ self._hw_is_initialized = False
+ self._ch_is_initialized = {
+ Channel.CHANNEL_CH0: False,
+ Channel.CHANNEL_CH1: False,
+ }
+ self._callback_ref = CallbackFktEx(self._callback)
+ if self._connect_control_ref is None:
+ self._connect_control_ref = ConnectControlFktEx(self._connect_control)
+ UcanInitHwConnectControlEx(self._connect_control_ref, None)
+
+ @property
+ def is_initialized(self):
+ """
+ Returns whether hardware interface is initialized.
+
+ :return: True if initialized, otherwise False.
+ :rtype: bool
+ """
+ return self._is_initialized
+
+ @property
+ def is_can0_initialized(self):
+ """
+ Returns whether CAN interface for channel 0 is initialized.
+
+ :return: True if initialized, otherwise False.
+ :rtype: bool
+ """
+ return self._ch_is_initialized[Channel.CHANNEL_CH0]
+
+ @property
+ def is_can1_initialized(self):
+ """
+ Returns whether CAN interface for channel 1 is initialized.
+
+ :return: True if initialized, otherwise False.
+ :rtype: bool
+ """
+ return self._ch_is_initialized[Channel.CHANNEL_CH1]
+
+ @classmethod
+ def _enum_callback(cls, index, is_used, hw_info_ex, init_info, arg):
+ cls._modules_found.append(
+ (index, bool(is_used), hw_info_ex.contents, init_info.contents)
+ )
+
+ @classmethod
+ def enumerate_hardware(
+ cls,
+ device_number_low=0,
+ device_number_high=-1,
+ serial_low=0,
+ serial_high=-1,
+ product_code_low=0,
+ product_code_high=-1,
+ enum_used_devices=False,
+ ):
+ cls._modules_found = []
+ UcanEnumerateHardware(
+ cls._enum_callback_ref,
+ None,
+ enum_used_devices,
+ device_number_low,
+ device_number_high,
+ serial_low,
+ serial_high,
+ product_code_low,
+ product_code_high,
+ )
+ return cls._modules_found
+
+ def init_hardware(self, serial=None, device_number=ANY_MODULE):
+ """
+ Initializes the device with the corresponding serial or device number.
+
+ :param int or None serial: Serial number of the USB-CANmodul.
+ :param int device_number: Device number (0 - 254, or :const:`ANY_MODULE` for the first device).
+ """
+ if not self._hw_is_initialized:
+ # initialize hardware either by device number or serial
+ if serial is None:
+ UcanInitHardwareEx(
+ byref(self._handle), device_number, self._callback_ref, None
+ )
+ else:
+ UcanInitHardwareEx2(
+ byref(self._handle), serial, self._callback_ref, None
+ )
+ self._hw_is_initialized = True
+
+ def init_can(
+ self,
+ channel=Channel.CHANNEL_CH0,
+ BTR=Baudrate.BAUD_1MBit,
+ baudrate=BaudrateEx.BAUDEX_USE_BTR01,
+ AMR=AMR_ALL,
+ ACR=ACR_ALL,
+ mode=Mode.MODE_NORMAL,
+ OCR=OutputControl.OCR_DEFAULT,
+ rx_buffer_entries=DEFAULT_BUFFER_ENTRIES,
+ tx_buffer_entries=DEFAULT_BUFFER_ENTRIES,
+ ):
+ """
+ Initializes a specific CAN channel of a device.
+
+ :param int channel: CAN channel to be initialized (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`).
+ :param int BTR:
+ Baud rate register BTR0 as high byte, baud rate register BTR1 as low byte (see enum :class:`Baudrate`).
+ :param int baudrate: Baud rate register for all systec USB-CANmoduls (see enum :class:`BaudrateEx`).
+ :param int AMR: Acceptance filter mask (see method :meth:`set_acceptance`).
+ :param int ACR: Acceptance filter code (see method :meth:`set_acceptance`).
+ :param int mode: Transmission mode of CAN channel (see enum :class:`Mode`).
+ :param int OCR: Output Control Register (see enum :class:`OutputControl`).
+ :param int rx_buffer_entries: The number of maximum entries in the receive buffer.
+ :param int tx_buffer_entries: The number of maximum entries in the transmit buffer.
+ """
+ if not self._ch_is_initialized.get(channel, False):
+ init_param = InitCanParam(
+ mode, BTR, OCR, AMR, ACR, baudrate, rx_buffer_entries, tx_buffer_entries
+ )
+ UcanInitCanEx2(self._handle, channel, init_param)
+ self._ch_is_initialized[channel] = True
+
+ def read_can_msg(self, channel, count):
+ """
+ Reads one or more CAN-messages from the buffer of the specified CAN channel.
+
+ :param int channel:
+ CAN channel to read from (:data:`Channel.CHANNEL_CH0`, :data:`Channel.CHANNEL_CH1`,
+ :data:`Channel.CHANNEL_ANY`).
+ :param int count: The number of CAN messages to be received.
+ :return: Tuple with list of CAN message/s received and the CAN channel where the read CAN messages came from.
+ :rtype: tuple(list(CanMsg), int)
+ """
+ c_channel = BYTE(channel)
+ c_can_msg = (CanMsg * count)()
+ c_count = DWORD(count)
+ UcanReadCanMsgEx(self._handle, byref(c_channel), c_can_msg, byref(c_count))
+ return c_can_msg[: c_count.value], c_channel.value
+
+ def write_can_msg(self, channel, can_msg):
+ """
+ Transmits one ore more CAN messages through the specified CAN channel of the device.
+
+ :param int channel:
+ CAN channel, which is to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`).
+ :param list(CanMsg) can_msg: List of CAN message structure (see structure :class:`CanMsg`).
+ :return: The number of successfully transmitted CAN messages.
+ :rtype: int
+ """
+ c_can_msg = (CanMsg * len(can_msg))(*can_msg)
+ c_count = DWORD(len(can_msg))
+ UcanWriteCanMsgEx(self._handle, channel, c_can_msg, c_count)
+ return c_count
+
+ def set_baudrate(self, channel, BTR, baudarate):
+ """
+ This function is used to configure the baud rate of specific CAN channel of a device.
+
+ :param int channel:
+ CAN channel, which is to be configured (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`).
+ :param int BTR:
+ Baud rate register BTR0 as high byte, baud rate register BTR1 as low byte (see enum :class:`Baudrate`).
+ :param int baudarate: Baud rate register for all systec USB-CANmoduls (see enum :class:`BaudrateEx`>).
+ """
+ UcanSetBaudrateEx(self._handle, channel, BTR >> 8, BTR, baudarate)
+
+ def set_acceptance(self, channel=Channel.CHANNEL_CH0, AMR=AMR_ALL, ACR=ACR_ALL):
+ """
+ This function is used to change the acceptance filter values for a specific CAN channel on a device.
+
+ :param int channel:
+ CAN channel, which is to be configured (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`).
+ :param int AMR: Acceptance filter mask (AMR).
+ :param int ACR: Acceptance filter code (ACR).
+ """
+ UcanSetAcceptanceEx(self._handle, channel, AMR, ACR)
+
+ def get_status(self, channel=Channel.CHANNEL_CH0):
+ """
+ Returns the error status of a specific CAN channel.
+
+ :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`).
+ :return: Tuple with CAN and USB status (see structure :class:`Status`).
+ :rtype: tuple(int, int)
+ """
+ status = Status()
+ UcanGetStatusEx(self._handle, channel, byref(status))
+ return status.can_status, status.usb_status
+
+ def get_msg_count_info(self, channel=Channel.CHANNEL_CH0):
+ """
+ Reads the message counters of the specified CAN channel.
+
+ :param int channel:
+ CAN channel, which is to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`).
+ :return: Tuple with number of CAN messages sent and received.
+ :rtype: tuple(int, int)
+ """
+ msg_count_info = MsgCountInfo()
+ UcanGetMsgCountInfoEx(self._handle, channel, byref(msg_count_info))
+ return msg_count_info.sent_msg_count, msg_count_info.recv_msg_count
+
+ def reset_can(self, channel=Channel.CHANNEL_CH0, flags=ResetFlags.RESET_ALL):
+ """
+ Resets a CAN channel of a device (hardware reset, empty buffer, and so on).
+
+ :param int channel: CAN channel, to be reset (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`).
+ :param int flags: Flags defines what should be reset (see enum :class:`ResetFlags`).
+ """
+ UcanResetCanEx(self._handle, channel, flags)
+
+ def get_hardware_info(self):
+ """
+ Returns the extended hardware information of a device. With multi-channel USB-CANmoduls the information for
+ both CAN channels are returned separately.
+
+ :return:
+ Tuple with extended hardware information structure (see structure :class:`HardwareInfoEx`) and
+ structures with information of CAN channel 0 and 1 (see structure :class:`ChannelInfo`).
+ :rtype: tuple(HardwareInfoEx, ChannelInfo, ChannelInfo)
+ """
+ hw_info_ex = HardwareInfoEx()
+ can_info_ch0, can_info_ch1 = ChannelInfo(), ChannelInfo()
+ UcanGetHardwareInfoEx2(
+ self._handle, byref(hw_info_ex), byref(can_info_ch0), byref(can_info_ch1)
+ )
+ return hw_info_ex, can_info_ch0, can_info_ch1
+
+ def get_fw_version(self):
+ """
+ Returns the firmware version number of the device.
+
+ :return: Firmware version number.
+ :rtype: int
+ """
+ return UcanGetFwVersion(self._handle)
+
+ def define_cyclic_can_msg(self, channel, can_msg=None):
+ """
+ Defines a list of CAN messages for automatic transmission.
+
+ :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`).
+ :param list(CanMsg) can_msg:
+ List of CAN messages (up to 16, see structure :class:`CanMsg`), or None to delete an older list.
+ """
+ if can_msg is not None:
+ c_can_msg = (CanMsg * len(can_msg))(*can_msg)
+ c_count = DWORD(len(can_msg))
+ else:
+ c_can_msg = CanMsg()
+ c_count = 0
+ UcanDefineCyclicCanMsg(self._handle, channel, c_can_msg, c_count)
+
+ def read_cyclic_can_msg(self, channel, count):
+ """
+ Reads back the list of CAN messages for automatically sending.
+
+ :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`).
+ :param int count: The number of cyclic CAN messages to be received.
+ :return: List of received CAN messages (up to 16, see structure :class:`CanMsg`).
+ :rtype: list(CanMsg)
+ """
+ c_channel = BYTE(channel)
+ c_can_msg = (CanMsg * count)()
+ c_count = DWORD(count)
+ UcanReadCyclicCanMsg(self._handle, byref(c_channel), c_can_msg, c_count)
+ return c_can_msg[: c_count.value]
+
+ def enable_cyclic_can_msg(self, channel, flags):
+ """
+ Enables or disables the automatically sending.
+
+ :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`).
+ :param int flags: Flags for enabling or disabling (see enum :class:`CyclicFlags`).
+ """
+ UcanEnableCyclicCanMsg(self._handle, channel, flags)
+
+ def get_msg_pending(self, channel, flags):
+ """
+ Returns the number of pending CAN messages.
+
+ :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`).
+ :param int flags: Flags specifies which buffers should be checked (see enum :class:`PendingFlags`).
+ :return: The number of pending messages.
+ :rtype: int
+ """
+ count = DWORD(0)
+ UcanGetMsgPending(self._handle, channel, flags, byref(count))
+ return count.value
+
+ def get_can_error_counter(self, channel):
+ """
+ Reads the current value of the error counters within the CAN controller.
+
+ :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`).
+ :return: Tuple with the TX and RX error counter.
+ :rtype: tuple(int, int)
+
+ .. note:: Only available for systec USB-CANmoduls (NOT for GW-001 and GW-002 !!!).
+ """
+ tx_error_counter = DWORD(0)
+ rx_error_counter = DWORD(0)
+ UcanGetCanErrorCounter(
+ self._handle, channel, byref(tx_error_counter), byref(rx_error_counter)
+ )
+ return tx_error_counter, rx_error_counter
+
+ def set_tx_timeout(self, channel, timeout):
+ """
+ Sets the transmission timeout.
+
+ :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`).
+ :param float timeout: Transmit timeout in seconds (value 0 disables this feature).
+ """
+ UcanSetTxTimeout(self._handle, channel, int(timeout * 1000))
+
+ def shutdown(self, channel=Channel.CHANNEL_ALL, shutdown_hardware=True):
+ """
+ Shuts down all CAN interfaces and/or the hardware interface.
+
+ :param int channel:
+ CAN channel, to be used (:data:`Channel.CHANNEL_CH0`, :data:`Channel.CHANNEL_CH1` or
+ :data:`Channel.CHANNEL_ALL`)
+ :param bool shutdown_hardware: If true then the hardware interface will be closed too.
+ """
+ # shutdown each channel if it's initialized
+ for _channel, is_initialized in self._ch_is_initialized.items():
+ if is_initialized and (
+ _channel == channel
+ or channel == Channel.CHANNEL_ALL
+ or shutdown_hardware
+ ):
+ UcanDeinitCanEx(self._handle, _channel)
+ self._ch_is_initialized[_channel] = False
+
+ # shutdown hardware
+ if self._hw_is_initialized and shutdown_hardware:
+ UcanDeinitHardware(self._handle)
+ self._hw_is_initialized = False
+ self._handle = Handle(INVALID_HANDLE)
+
+ @staticmethod
+ def get_user_dll_version():
+ """
+ Returns the version number of the USBCAN-library.
+
+ :return: Software version number.
+ :rtype: int
+ """
+ return UcanGetVersionEx(VersionType.VER_TYPE_USER_DLL)
+
+ @staticmethod
+ def set_debug_mode(level, filename, flags=0):
+ """
+ This function enables the creation of a debug log file out of the USBCAN-library. If this
+ feature has already been activated via the USB-CANmodul Control, the content of the
+ “old” log file will be copied to the new file. Further debug information will be appended to
+ the new file.
+
+ :param int level: Debug level (bit format).
+ :param str filename: File path to debug log file.
+ :param int flags: Additional flags (bit0: file append mode).
+ :return: False if logfile not created otherwise True.
+ :rtype: bool
+ """
+ return UcanSetDebugMode(level, filename, flags)
+
+ @staticmethod
+ def get_can_status_message(can_status):
+ """
+ Converts a given CAN status value to the appropriate message string.
+
+ :param can_status: CAN status value from method :meth:`get_status` (see enum :class:`CanStatus`)
+ :return: Status message string.
+ :rtype: str
+ """
+ status_msgs = {
+ CanStatus.CANERR_TXMSGLOST: "Transmit message lost",
+ CanStatus.CANERR_MEMTEST: "Memory test failed",
+ CanStatus.CANERR_REGTEST: "Register test failed",
+ CanStatus.CANERR_QXMTFULL: "Transmit queue is full",
+ CanStatus.CANERR_QOVERRUN: "Receive queue overrun",
+ CanStatus.CANERR_QRCVEMPTY: "Receive queue is empty",
+ CanStatus.CANERR_BUSOFF: "Bus Off",
+ CanStatus.CANERR_BUSHEAVY: "Error Passive",
+ CanStatus.CANERR_BUSLIGHT: "Warning Limit",
+ CanStatus.CANERR_OVERRUN: "Rx-buffer is full",
+ CanStatus.CANERR_XMTFULL: "Tx-buffer is full",
+ }
+ return (
+ "OK"
+ if can_status == CanStatus.CANERR_OK
+ else ", ".join(
+ msg for status, msg in status_msgs.items() if can_status & status
+ )
+ )
+
+ @staticmethod
+ def get_baudrate_message(baudrate):
+ """
+ Converts a given baud rate value for GW-001/GW-002 to the appropriate message string.
+
+ :param Baudrate baudrate:
+ Bus Timing Registers, BTR0 in high order byte and BTR1 in low order byte
+ (see enum :class:`Baudrate`)
+ :return: Baud rate message string.
+ :rtype: str
+ """
+ baudrate_msgs = {
+ Baudrate.BAUD_AUTO: "auto baudrate",
+ Baudrate.BAUD_10kBit: "10 kBit/sec",
+ Baudrate.BAUD_20kBit: "20 kBit/sec",
+ Baudrate.BAUD_50kBit: "50 kBit/sec",
+ Baudrate.BAUD_100kBit: "100 kBit/sec",
+ Baudrate.BAUD_125kBit: "125 kBit/sec",
+ Baudrate.BAUD_250kBit: "250 kBit/sec",
+ Baudrate.BAUD_500kBit: "500 kBit/sec",
+ Baudrate.BAUD_800kBit: "800 kBit/sec",
+ Baudrate.BAUD_1MBit: "1 MBit/s",
+ Baudrate.BAUD_USE_BTREX: "BTR Ext is used",
+ }
+ return baudrate_msgs.get(baudrate, "BTR is unknown (user specific)")
+
+ @staticmethod
+ def get_baudrate_ex_message(baudrate_ex):
+ """
+ Converts a given baud rate value for systec USB-CANmoduls to the appropriate message string.
+
+ :param BaudrateEx baudrate_ex: Bus Timing Registers (see enum :class:`BaudrateEx`)
+ :return: Baud rate message string.
+ :rtype: str
+ """
+ baudrate_ex_msgs = {
+ Baudrate.BAUDEX_AUTO: "auto baudrate",
+ Baudrate.BAUDEX_10kBit: "10 kBit/sec",
+ Baudrate.BAUDEX_SP2_10kBit: "10 kBit/sec",
+ Baudrate.BAUDEX_20kBit: "20 kBit/sec",
+ Baudrate.BAUDEX_SP2_20kBit: "20 kBit/sec",
+ Baudrate.BAUDEX_50kBit: "50 kBit/sec",
+ Baudrate.BAUDEX_SP2_50kBit: "50 kBit/sec",
+ Baudrate.BAUDEX_100kBit: "100 kBit/sec",
+ Baudrate.BAUDEX_SP2_100kBit: "100 kBit/sec",
+ Baudrate.BAUDEX_125kBit: "125 kBit/sec",
+ Baudrate.BAUDEX_SP2_125kBit: "125 kBit/sec",
+ Baudrate.BAUDEX_250kBit: "250 kBit/sec",
+ Baudrate.BAUDEX_SP2_250kBit: "250 kBit/sec",
+ Baudrate.BAUDEX_500kBit: "500 kBit/sec",
+ Baudrate.BAUDEX_SP2_500kBit: "500 kBit/sec",
+ Baudrate.BAUDEX_800kBit: "800 kBit/sec",
+ Baudrate.BAUDEX_SP2_800kBit: "800 kBit/sec",
+ Baudrate.BAUDEX_1MBit: "1 MBit/s",
+ Baudrate.BAUDEX_SP2_1MBit: "1 MBit/s",
+ Baudrate.BAUDEX_USE_BTR01: "BTR0/BTR1 is used",
+ }
+ return baudrate_ex_msgs.get(baudrate_ex, "BTR is unknown (user specific)")
+
+ @staticmethod
+ def get_product_code_message(product_code):
+ product_code_msgs = {
+ ProductCode.PRODCODE_PID_GW001: "GW-001",
+ ProductCode.PRODCODE_PID_GW002: "GW-002",
+ ProductCode.PRODCODE_PID_MULTIPORT: "Multiport CAN-to-USB G3",
+ ProductCode.PRODCODE_PID_BASIC: "USB-CANmodul1 G3",
+ ProductCode.PRODCODE_PID_ADVANCED: "USB-CANmodul2 G3",
+ ProductCode.PRODCODE_PID_USBCAN8: "USB-CANmodul8 G3",
+ ProductCode.PRODCODE_PID_USBCAN16: "USB-CANmodul16 G3",
+ ProductCode.PRODCODE_PID_RESERVED3: "Reserved",
+ ProductCode.PRODCODE_PID_ADVANCED_G4: "USB-CANmodul2 G4",
+ ProductCode.PRODCODE_PID_BASIC_G4: "USB-CANmodul1 G4",
+ ProductCode.PRODCODE_PID_RESERVED1: "Reserved",
+ ProductCode.PRODCODE_PID_RESERVED2: "Reserved",
+ }
+ return product_code_msgs.get(
+ product_code & PRODCODE_MASK_PID, "Product code is unknown"
+ )
+
+ @classmethod
+ def convert_to_major_ver(cls, version):
+ """
+ Converts the a version number into the major version.
+
+ :param int version: Version number to be converted.
+ :return: Major version.
+ :rtype: int
+ """
+ return version & 0xFF
+
+ @classmethod
+ def convert_to_minor_ver(cls, version):
+ """
+ Converts the a version number into the minor version.
+
+ :param int version: Version number to be converted.
+ :return: Minor version.
+ :rtype: int
+ """
+ return (version & 0xFF00) >> 8
+
+ @classmethod
+ def convert_to_release_ver(cls, version):
+ """
+ Converts the a version number into the release version.
+
+ :param int version: Version number to be converted.
+ :return: Release version.
+ :rtype: int
+ """
+ return (version & 0xFFFF0000) >> 16
+
+ @classmethod
+ def check_version_is_equal_or_higher(cls, version, cmp_major, cmp_minor):
+ """
+ Checks if the version is equal or higher than a specified value.
+
+ :param int version: Version number to be checked.
+ :param int cmp_major: Major version to be compared with.
+ :param int cmp_minor: Minor version to be compared with.
+ :return: True if equal or higher, otherwise False.
+ :rtype: bool
+ """
+ return (cls.convert_to_major_ver(version) > cmp_major) or (
+ cls.convert_to_major_ver(version) == cmp_major
+ and cls.convert_to_minor_ver(version) >= cmp_minor
+ )
+
+ @classmethod
+ def check_is_systec(cls, hw_info_ex):
+ """
+ Checks whether the module is a systec USB-CANmodul.
+
+ :param HardwareInfoEx hw_info_ex:
+ Extended hardware information structure (see method :meth:`get_hardware_info`).
+ :return: True when the module is a systec USB-CANmodul, otherwise False.
+ :rtype: bool
+ """
+ return (
+ hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID
+ ) >= ProductCode.PRODCODE_PID_MULTIPORT
+
+ @classmethod
+ def check_is_G4(cls, hw_info_ex):
+ """
+ Checks whether the module is an USB-CANmodul of fourth generation (G4).
+
+ :param HardwareInfoEx hw_info_ex:
+ Extended hardware information structure (see method :meth:`get_hardware_info`).
+ :return: True when the module is an USB-CANmodul G4, otherwise False.
+ :rtype: bool
+ """
+ return hw_info_ex.m_dwProductCode & PRODCODE_PID_G4
+
+ @classmethod
+ def check_is_G3(cls, hw_info_ex):
+ """
+ Checks whether the module is an USB-CANmodul of third generation (G3).
+
+ :param HardwareInfoEx hw_info_ex:
+ Extended hardware information structure (see method :meth:`get_hardware_info`).
+ :return: True when the module is an USB-CANmodul G3, otherwise False.
+ :rtype: bool
+ """
+ return cls.check_is_systec(hw_info_ex) and not cls.check_is_G4(hw_info_ex)
+
+ @classmethod
+ def check_support_cyclic_msg(cls, hw_info_ex):
+ """
+ Checks whether the module supports automatically transmission of cyclic CAN messages.
+
+ :param HardwareInfoEx hw_info_ex:
+ Extended hardware information structure (see method :meth:`get_hardware_info`).
+ :return: True when the module does support cyclic CAN messages, otherwise False.
+ :rtype: bool
+ """
+ return cls.check_is_systec(hw_info_ex) and cls.check_version_is_equal_or_higher(
+ hw_info_ex.m_dwFwVersionEx, 3, 6
+ )
+
+ @classmethod
+ def check_support_two_channel(cls, hw_info_ex):
+ """
+ Checks whether the module supports two CAN channels (at logical device).
+
+ :param HardwareInfoEx hw_info_ex:
+ Extended hardware information structure (see method :meth:`get_hardware_info`).
+ :return: True when the module (logical device) does support two CAN channels, otherwise False.
+ :rtype: bool
+ """
+ return cls.check_is_systec(hw_info_ex) and (
+ hw_info_ex.m_dwProductCode & PRODCODE_PID_TWO_CHA
+ )
+
+ @classmethod
+ def check_support_term_resistor(cls, hw_info_ex):
+ """
+ Checks whether the module supports a termination resistor at the CAN bus.
+
+ :param HardwareInfoEx hw_info_ex:
+ Extended hardware information structure (see method :meth:`get_hardware_info`).
+ :return: True when the module does support a termination resistor.
+ :rtype: bool
+ """
+ return hw_info_ex.m_dwProductCode & PRODCODE_PID_TERM
+
+ @classmethod
+ def check_support_user_port(cls, hw_info_ex):
+ """
+ Checks whether the module supports a user I/O port.
+
+ :param HardwareInfoEx hw_info_ex:
+ Extended hardware information structure (see method :meth:`get_hardware_info`).
+ :return: True when the module supports a user I/O port, otherwise False.
+ :rtype: bool
+ """
+ return (
+ (
+ (hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID)
+ != ProductCode.PRODCODE_PID_BASIC
+ )
+ and (
+ (hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID)
+ != ProductCode.PRODCODE_PID_RESERVED1
+ )
+ and cls.check_version_is_equal_or_higher(hw_info_ex.m_dwFwVersionEx, 2, 16)
+ )
+
+ @classmethod
+ def check_support_rb_user_port(cls, hw_info_ex):
+ """
+ Checks whether the module supports a user I/O port including read back feature.
+
+ :param HardwareInfoEx hw_info_ex:
+ Extended hardware information structure (see method :meth:`get_hardware_info`).
+ :return: True when the module does support a user I/O port including the read back feature, otherwise False.
+ :rtype: bool
+ """
+ return hw_info_ex.m_dwProductCode & PRODCODE_PID_RBUSER
+
+ @classmethod
+ def check_support_rb_can_port(cls, hw_info_ex):
+ """
+ Checks whether the module supports a CAN I/O port including read back feature.
+
+ :param HardwareInfoEx hw_info_ex:
+ Extended hardware information structure (see method :meth:`get_hardware_info`).
+ :return: True when the module does support a CAN I/O port including the read back feature, otherwise False.
+ :rtype: bool
+ """
+ return hw_info_ex.m_dwProductCode & PRODCODE_PID_RBCAN
+
+ @classmethod
+ def check_support_ucannet(cls, hw_info_ex):
+ """
+ Checks whether the module supports the usage of USB-CANnetwork driver.
+
+ :param HardwareInfoEx hw_info_ex:
+ Extended hardware information structure (see method :meth:`get_hardware_info`).
+ :return: True when the module does support the usage of the USB-CANnetwork driver, otherwise False.
+ :rtype: bool
+ """
+ return cls.check_is_systec(hw_info_ex) and cls.check_version_is_equal_or_higher(
+ hw_info_ex.m_dwFwVersionEx, 3, 8
+ )
+
+ @classmethod
+ def calculate_amr(cls, is_extended, from_id, to_id, rtr_only=False, rtr_too=True):
+ """
+ Calculates AMR using CAN-ID range as parameter.
+
+ :param bool is_extended: If True parameters from_id and to_id contains 29-bit CAN-ID.
+ :param int from_id: First CAN-ID which should be received.
+ :param int to_id: Last CAN-ID which should be received.
+ :param bool rtr_only: If True only RTR-Messages should be received, and rtr_too will be ignored.
+ :param bool rtr_too: If True CAN data frames and RTR-Messages should be received.
+ :return: Value for AMR.
+ :rtype: int
+ """
+ return (
+ (((from_id ^ to_id) << 3) | (0x7 if rtr_too and not rtr_only else 0x3))
+ if is_extended
+ else (
+ ((from_id ^ to_id) << 21)
+ | (0x1FFFFF if rtr_too and not rtr_only else 0xFFFFF)
+ )
+ )
+
+ @classmethod
+ def calculate_acr(cls, is_extended, from_id, to_id, rtr_only=False, rtr_too=True):
+ """
+ Calculates ACR using CAN-ID range as parameter.
+
+ :param bool is_extended: If True parameters from_id and to_id contains 29-bit CAN-ID.
+ :param int from_id: First CAN-ID which should be received.
+ :param int to_id: Last CAN-ID which should be received.
+ :param bool rtr_only: If True only RTR-Messages should be received, and rtr_too will be ignored.
+ :param bool rtr_too: If True CAN data frames and RTR-Messages should be received.
+ :return: Value for ACR.
+ :rtype: int
+ """
+ return (
+ (((from_id & to_id) << 3) | (0x04 if rtr_only else 0))
+ if is_extended
+ else (((from_id & to_id) << 21) | (0x100000 if rtr_only else 0))
+ )
+
+ def _connect_control(self, event, param, arg):
+ """
+ Is the actual callback function for :meth:`init_hw_connect_control_ex`.
+
+ :param event:
+ Event (:data:`CbEvent.EVENT_CONNECT`, :data:`CbEvent.EVENT_DISCONNECT` or
+ :data:`CbEvent.EVENT_FATALDISCON`).
+ :param param: Additional parameter depending on the event.
+ - CbEvent.EVENT_CONNECT: always 0
+ - CbEvent.EVENT_DISCONNECT: always 0
+ - CbEvent.EVENT_FATALDISCON: USB-CAN-Handle of the disconnected module
+ :param arg: Additional parameter defined with :meth:`init_hardware_ex` (not used in this wrapper class).
+ """
+ log.debug("Event: %s, Param: %s", event, param)
+
+ if event == CbEvent.EVENT_FATALDISCON:
+ self.fatal_disconnect_event(param)
+ elif event == CbEvent.EVENT_CONNECT:
+ self.connect_event()
+ elif event == CbEvent.EVENT_DISCONNECT:
+ self.disconnect_event()
+
+ def _callback(self, handle, event, channel, arg):
+ """
+ Is called if a working event occurred.
+
+ :param int handle: USB-CAN-Handle returned by the function :meth:`init_hardware`.
+ :param int event: Event type.
+ :param int channel:
+ CAN channel (:data:`Channel.CHANNEL_CH0`, :data:`Channel.CHANNEL_CH1` or :data:`Channel.CHANNEL_ANY`).
+ :param arg: Additional parameter defined with :meth:`init_hardware_ex`.
+ """
+ log.debug("Handle: %s, Event: %s, Channel: %s", handle, event, channel)
+
+ if event == CbEvent.EVENT_INITHW:
+ self.init_hw_event()
+ elif event == CbEvent.EVENT_init_can:
+ self.init_can_event(channel)
+ elif event == CbEvent.EVENT_RECEIVE:
+ self.can_msg_received_event(channel)
+ elif event == CbEvent.EVENT_STATUS:
+ self.status_event(channel)
+ elif event == CbEvent.EVENT_DEINIT_CAN:
+ self.deinit_can_event(channel)
+ elif event == CbEvent.EVENT_DEINITHW:
+ self.deinit_hw_event()
+
+ def init_hw_event(self):
+ """
+ Event occurs when an USB-CANmodul has been initialized (see method :meth:`init_hardware`).
+
+ .. note:: To be overridden by subclassing.
+ """
+
+ def init_can_event(self, channel):
+ """
+ Event occurs when a CAN interface of an USB-CANmodul has been initialized.
+
+ :param int channel: Specifies the CAN channel which was initialized (see method :meth:`init_can`).
+
+ .. note:: To be overridden by subclassing.
+ """
+
+ def can_msg_received_event(self, channel):
+ """
+ Event occurs when at leas one CAN message has been received.
+
+ Call the method :meth:`read_can_msg` to receive the CAN messages.
+
+ :param int channel: Specifies the CAN channel which received CAN messages.
+
+ .. note:: To be overridden by subclassing.
+ """
+
+ def status_event(self, channel):
+ """
+ Event occurs when the error status of a module has been changed.
+
+ Call the method :meth:`get_status` to receive the error status.
+
+ :param int channel: Specifies the CAN channel which status has been changed.
+
+ .. note:: To be overridden by subclassing.
+ """
+
+ def deinit_can_event(self, channel):
+ """
+ Event occurs when a CAN interface has been deinitialized (see method :meth:`shutdown`).
+
+ :param int channel: Specifies the CAN channel which status has been changed.
+
+ .. note:: To be overridden by subclassing.
+ """
+
+ def deinit_hw_event(self):
+ """
+ Event occurs when an USB-CANmodul has been deinitialized (see method :meth:`shutdown`).
+
+ .. note:: To be overridden by subclassing.
+ """
+
+ def connect_event(self):
+ """
+ Event occurs when a new USB-CANmodul has been connected to the host.
+
+ .. note:: To be overridden by subclassing.
+ """
+
+ def disconnect_event(self):
+ """
+ Event occurs when an USB-CANmodul has been disconnected from the host.
+
+ .. note:: To be overridden by subclassing.
+ """
+
+ def fatal_disconnect_event(self, device_number):
+ """
+ Event occurs when an USB-CANmodul has been disconnected from the host which was currently initialized.
+
+ No method can be called for this module.
+
+ :param int device_number: The device number which was disconnected.
+
+ .. note:: To be overridden by subclassing.
+ """
+
+
+UcanServer._enum_callback_ref = EnumCallback(UcanServer._enum_callback)
diff --git a/can/interfaces/systec/ucanbus.py b/can/interfaces/systec/ucanbus.py
new file mode 100644
index 000000000..00f101e4e
--- /dev/null
+++ b/can/interfaces/systec/ucanbus.py
@@ -0,0 +1,333 @@
+import logging
+from threading import Event
+
+from can import (
+ BusABC,
+ BusState,
+ CanError,
+ CanInitializationError,
+ CanOperationError,
+ CanProtocol,
+ Message,
+)
+
+from .constants import *
+from .exceptions import UcanException
+from .structures import *
+from .ucan import UcanServer
+
+log = logging.getLogger("can.systec")
+
+
+class Ucan(UcanServer):
+ """
+ Wrapper around UcanServer to read messages with timeout using events.
+ """
+
+ def __init__(self):
+ super().__init__()
+ self._msg_received_event = Event()
+
+ def can_msg_received_event(self, channel):
+ self._msg_received_event.set()
+
+ def read_can_msg(self, channel, count, timeout):
+ self._msg_received_event.clear()
+ if self.get_msg_pending(channel, PendingFlags.PENDING_FLAG_RX_DLL) == 0:
+ if not self._msg_received_event.wait(timeout):
+ return None, False
+ return super().read_can_msg(channel, 1)
+
+
+class UcanBus(BusABC):
+ """
+ The CAN Bus implemented for the SYSTEC interface.
+ """
+
+ BITRATES = {
+ 10000: Baudrate.BAUD_10kBit,
+ 20000: Baudrate.BAUD_20kBit,
+ 50000: Baudrate.BAUD_50kBit,
+ 100000: Baudrate.BAUD_100kBit,
+ 125000: Baudrate.BAUD_125kBit,
+ 250000: Baudrate.BAUD_250kBit,
+ 500000: Baudrate.BAUD_500kBit,
+ 800000: Baudrate.BAUD_800kBit,
+ 1000000: Baudrate.BAUD_1MBit,
+ }
+
+ def __init__(self, channel, can_filters=None, **kwargs):
+ """
+ :param int channel:
+ The Channel id to create this bus with.
+
+ :param list can_filters:
+ See :meth:`can.BusABC.set_filters`.
+
+ Backend Configuration
+
+ :param int bitrate:
+ Channel bitrate in bit/s.
+ Default is 500000.
+
+ :param int device_number:
+ The device number of the USB-CAN.
+ Valid values: 0 through 254. Special value 255 is reserved to detect the first connected device (should only
+ be used, in case only one module is connected to the computer).
+ Default is 255.
+
+ :param can.bus.BusState state:
+ BusState of the channel.
+ Default is ACTIVE.
+
+ :param bool receive_own_messages:
+ If messages transmitted should also be received back.
+ Default is False.
+
+ :param int rx_buffer_entries:
+ The maximum number of entries in the receive buffer.
+ Default is 4096.
+
+ :param int tx_buffer_entries:
+ The maximum number of entries in the transmit buffer.
+ Default is 4096.
+
+ :raises ValueError:
+ If invalid input parameter were passed.
+
+ :raises ~can.exceptions.CanInterfaceNotImplementedError:
+ If the platform is not supported.
+
+ :raises ~can.exceptions.CanInitializationError:
+ If hardware or CAN interface initialization failed.
+ """
+ try:
+ self._ucan = Ucan()
+ except CanError as error:
+ raise error
+ except Exception as exception:
+ raise CanInitializationError(
+ "The SYSTEC ucan library has not been initialized."
+ ) from exception
+
+ self.channel = int(channel)
+ self._can_protocol = CanProtocol.CAN_20
+
+ device_number = int(kwargs.get("device_number", ANY_MODULE))
+
+ # configuration options
+ bitrate = kwargs.get("bitrate", 500000)
+ if bitrate not in self.BITRATES:
+ raise ValueError(f"Invalid bitrate {bitrate}")
+
+ state = kwargs.get("state", BusState.ACTIVE)
+ if state is BusState.ACTIVE or state is BusState.PASSIVE:
+ self._state = state
+ else:
+ raise ValueError("BusState must be Active or Passive")
+
+ # get parameters
+ self._params = {
+ "mode": Mode.MODE_NORMAL
+ | (Mode.MODE_TX_ECHO if kwargs.get("receive_own_messages") else 0)
+ | (Mode.MODE_LISTEN_ONLY if state is BusState.PASSIVE else 0),
+ "BTR": self.BITRATES[bitrate],
+ }
+ # get extra parameters
+ if kwargs.get("rx_buffer_entries"):
+ self._params["rx_buffer_entries"] = int(kwargs.get("rx_buffer_entries"))
+ if kwargs.get("tx_buffer_entries"):
+ self._params["tx_buffer_entries"] = int(kwargs.get("tx_buffer_entries"))
+
+ try:
+ self._ucan.init_hardware(device_number=device_number)
+ self._ucan.init_can(self.channel, **self._params)
+ hw_info_ex, _, _ = self._ucan.get_hardware_info()
+ self.channel_info = (
+ f"{self._ucan.get_product_code_message(hw_info_ex.product_code)}, "
+ f"S/N {hw_info_ex.serial}, "
+ f"CH {self.channel}, "
+ f"BTR {self._ucan.get_baudrate_message(self.BITRATES[bitrate])}"
+ )
+ except UcanException as exception:
+ raise CanInitializationError() from exception
+
+ self._is_filtered = False
+
+ super().__init__(
+ channel=channel,
+ can_filters=can_filters,
+ **kwargs,
+ )
+
+ def _recv_internal(self, timeout):
+ try:
+ message, _ = self._ucan.read_can_msg(self.channel, 1, timeout)
+ except UcanException as exception:
+ raise CanOperationError() from exception
+
+ if not message:
+ return None, False
+
+ msg = Message(
+ timestamp=float(message[0].time) / 1000.0,
+ is_remote_frame=bool(message[0].frame_format & MsgFrameFormat.MSG_FF_RTR),
+ is_extended_id=bool(message[0].frame_format & MsgFrameFormat.MSG_FF_EXT),
+ arbitration_id=message[0].id,
+ dlc=len(message[0].data),
+ data=message[0].data,
+ )
+ return msg, self._is_filtered
+
+ def send(self, msg, timeout=None):
+ """
+ Sends one CAN message.
+
+ When a transmission timeout is set the firmware tries to send
+ a message within this timeout. If it could not be sent the firmware sets
+ the "auto delete" state. Within this state all transmit CAN messages for
+ this channel will be deleted automatically for not blocking the other channel.
+
+ :param can.Message msg:
+ The CAN message.
+
+ :param float timeout:
+ Transmit timeout in seconds (value 0 switches off the "auto delete")
+
+ :raises ~can.exceptions.CanOperationError:
+ If the message could not be sent.
+ """
+ try:
+ if timeout is not None and timeout >= 0:
+ self._ucan.set_tx_timeout(self.channel, int(timeout * 1000))
+
+ message = CanMsg(
+ msg.arbitration_id,
+ MsgFrameFormat.MSG_FF_STD
+ | (MsgFrameFormat.MSG_FF_EXT if msg.is_extended_id else 0)
+ | (MsgFrameFormat.MSG_FF_RTR if msg.is_remote_frame else 0),
+ msg.data,
+ msg.dlc,
+ )
+ self._ucan.write_can_msg(self.channel, [message])
+ except UcanException as exception:
+ raise CanOperationError() from exception
+
+ @staticmethod
+ def _detect_available_configs():
+ configs = []
+ try:
+ # index, is_used, hw_info_ex, init_info
+ for _, _, hw_info_ex, _ in Ucan.enumerate_hardware():
+ configs.append(
+ {
+ "interface": "systec",
+ "channel": Channel.CHANNEL_CH0,
+ "device_number": hw_info_ex.device_number,
+ }
+ )
+ if Ucan.check_support_two_channel(hw_info_ex):
+ configs.append(
+ {
+ "interface": "systec",
+ "channel": Channel.CHANNEL_CH1,
+ "device_number": hw_info_ex.device_number,
+ }
+ )
+ except Exception:
+ log.warning("The SYSTEC ucan library has not been initialized.")
+ return configs
+
+ def _apply_filters(self, filters):
+ try:
+ if filters and len(filters) == 1:
+ can_id = filters[0]["can_id"]
+ can_mask = filters[0]["can_mask"]
+ self._ucan.set_acceptance(self.channel, can_mask, can_id)
+ self._is_filtered = True
+ log.info("Hardware filtering on ID 0x%X, mask 0x%X", can_id, can_mask)
+ else:
+ self._ucan.set_acceptance(self.channel)
+ self._is_filtered = False
+ log.info("Hardware filtering has been disabled")
+ except UcanException as exception:
+ raise CanOperationError() from exception
+
+ def flush_tx_buffer(self):
+ """
+ Flushes the transmit buffer.
+
+ :raises ~can.exceptions.CanError:
+ If flushing of the transmit buffer failed.
+ """
+ log.info("Flushing transmit buffer")
+ try:
+ self._ucan.reset_can(self.channel, ResetFlags.RESET_ONLY_TX_BUFF)
+ except UcanException as exception:
+ raise CanOperationError() from exception
+
+ @staticmethod
+ def create_filter(extended, from_id, to_id, rtr_only, rtr_too):
+ """
+ Calculates AMR and ACR using CAN-ID as parameter.
+
+ :param bool extended:
+ if True parameters from_id and to_id contains 29-bit CAN-ID
+
+ :param int from_id:
+ first CAN-ID which should be received
+
+ :param int to_id:
+ last CAN-ID which should be received
+
+ :param bool rtr_only:
+ if True only RTR-Messages should be received, and rtr_too will be ignored
+
+ :param bool rtr_too:
+ if True CAN data frames and RTR-Messages should be received
+
+ :return: Returns list with one filter containing a "can_id", a "can_mask" and "extended" key.
+ """
+ return [
+ {
+ "can_id": Ucan.calculate_acr(
+ extended, from_id, to_id, rtr_only, rtr_too
+ ),
+ "can_mask": Ucan.calculate_amr(
+ extended, from_id, to_id, rtr_only, rtr_too
+ ),
+ "extended": extended,
+ }
+ ]
+
+ @property
+ def state(self):
+ return self._state
+
+ @state.setter
+ def state(self, new_state):
+ if self._state is not BusState.ERROR and (
+ new_state is BusState.ACTIVE or new_state is BusState.PASSIVE
+ ):
+ try:
+ # close the CAN channel
+ self._ucan.shutdown(self.channel, False)
+ # set mode
+ if new_state is BusState.ACTIVE:
+ self._params["mode"] &= ~Mode.MODE_LISTEN_ONLY
+ else:
+ self._params["mode"] |= Mode.MODE_LISTEN_ONLY
+ # reinitialize CAN channel
+ self._ucan.init_can(self.channel, **self._params)
+ except UcanException as exception:
+ raise CanOperationError() from exception
+
+ def shutdown(self):
+ """
+ Shuts down all CAN interfaces and hardware interface.
+ """
+ super().shutdown()
+ try:
+ self._ucan.shutdown()
+ except Exception as exception:
+ log.error(exception)
diff --git a/can/interfaces/udp_multicast/__init__.py b/can/interfaces/udp_multicast/__init__.py
new file mode 100644
index 000000000..d52c028f0
--- /dev/null
+++ b/can/interfaces/udp_multicast/__init__.py
@@ -0,0 +1,9 @@
+"""A module to allow CAN over UDP on IPv4/IPv6 multicast."""
+
+__all__ = [
+ "UdpMulticastBus",
+ "bus",
+ "utils",
+]
+
+from .bus import UdpMulticastBus
diff --git a/can/interfaces/udp_multicast/bus.py b/can/interfaces/udp_multicast/bus.py
new file mode 100644
index 000000000..87a0800fa
--- /dev/null
+++ b/can/interfaces/udp_multicast/bus.py
@@ -0,0 +1,445 @@
+import errno
+import logging
+import platform
+import select
+import socket
+import struct
+import time
+import warnings
+from typing import Any
+
+import can
+from can import BusABC, CanProtocol, Message
+from can.typechecking import AutoDetectedConfig
+
+from .utils import is_msgpack_installed, pack_message, unpack_message
+
+is_linux = platform.system() == "Linux"
+if is_linux:
+ from fcntl import ioctl
+
+log = logging.getLogger(__name__)
+
+
+# see socket.getaddrinfo()
+IPv4_ADDRESS_INFO = tuple[str, int] # address, port
+IPv6_ADDRESS_INFO = tuple[str, int, int, int] # address, port, flowinfo, scope_id
+IP_ADDRESS_INFO = IPv4_ADDRESS_INFO | IPv6_ADDRESS_INFO
+
+# Additional constants for the interaction with Unix kernels
+SO_TIMESTAMPNS = 35
+SIOCGSTAMP = 0x8906
+
+# Additional constants for the interaction with the Winsock API
+WSAEINVAL = 10022
+
+
+class UdpMulticastBus(BusABC):
+ """A virtual interface for CAN communications between multiple processes using UDP over Multicast IP.
+
+ It supports IPv4 and IPv6, specified via the channel (which really is just a multicast IP address as a
+ string). You can also specify the port and the IPv6 *hop limit*/the IPv4 *time to live* (TTL).
+
+ This bus does not support filtering based on message IDs on the kernel level but instead provides it in
+ user space (in Python) as a fallback.
+
+ Both default addresses should allow for multi-host CAN networks in a normal local area network (LAN) where
+ multicast is enabled.
+
+ .. note::
+ The auto-detection of available interfaces (see) is implemented using heuristic that checks if the
+ required socket operations are available. It then returns two configurations, one based on
+ the :attr:`~UdpMulticastBus.DEFAULT_GROUP_IPv6` address and another one based on
+ the :attr:`~UdpMulticastBus.DEFAULT_GROUP_IPv4` address.
+
+ .. warning::
+ The parameter `receive_own_messages` is currently unsupported and setting it to `True` will raise an
+ exception.
+
+ .. warning::
+ This interface does not make guarantees on reliable delivery and message ordering, and also does not
+ implement rate limiting or ID arbitration/prioritization under high loads. Please refer to the section
+ :ref:`virtual_interfaces_doc` for more information on this and a comparison to alternatives.
+
+ :param channel: A multicast IPv4 address (in `224.0.0.0/4`) or an IPv6 address (in `ff00::/8`).
+ This defines which version of IP is used. See
+ `Wikipedia ("Multicast address") `__
+ for more details on the addressing schemes.
+ Defaults to :attr:`~UdpMulticastBus.DEFAULT_GROUP_IPv6`.
+ :param port: The IP port to read from and write to.
+ :param hop_limit: The hop limit in IPv6 or in IPv4 the time to live (TTL).
+ :param receive_own_messages: If transmitted messages should also be received by this bus.
+ CURRENTLY UNSUPPORTED.
+ :param fd:
+ If CAN-FD frames should be supported. If set to false, an error will be raised upon sending such a
+ frame and such received frames will be ignored.
+ :param can_filters: See :meth:`~can.BusABC.set_filters`.
+
+ :raises RuntimeError: If the *msgpack*-dependency is not available. It should be installed on all
+ non Windows platforms via the `setup.py` requirements.
+ :raises NotImplementedError: If the `receive_own_messages` is passed as `True`.
+ """
+
+ #: An arbitrary IPv6 multicast address with "site-local" scope, i.e. only to be routed within the local
+ #: physical network and not beyond it. It should allow for multi-host CAN networks in a normal IPv6 LAN.
+ #: This is the default channel and should work with most modern routers if multicast is allowed.
+ DEFAULT_GROUP_IPv6 = "ff15:7079:7468:6f6e:6465:6d6f:6d63:6173"
+
+ #: An arbitrary IPv4 multicast address with "administrative" scope, i.e. only to be routed within
+ #: administrative organizational boundaries and not beyond it.
+ #: It should allow for multi-host CAN networks in a normal IPv4 LAN.
+ #: This is provided as a default fallback channel if IPv6 is (still) not supported.
+ DEFAULT_GROUP_IPv4 = "239.74.163.2"
+
+ def __init__(
+ self,
+ channel: str = DEFAULT_GROUP_IPv6,
+ port: int = 43113,
+ hop_limit: int = 1,
+ receive_own_messages: bool = False,
+ fd: bool = True,
+ **kwargs: Any,
+ ) -> None:
+ is_msgpack_installed()
+
+ if receive_own_messages:
+ raise can.CanInterfaceNotImplementedError(
+ "receiving own messages is not yet implemented"
+ )
+
+ super().__init__(
+ channel,
+ **kwargs,
+ )
+
+ self._multicast = GeneralPurposeUdpMulticastBus(channel, port, hop_limit)
+ self._can_protocol = CanProtocol.CAN_FD if fd else CanProtocol.CAN_20
+
+ @property
+ def is_fd(self) -> bool:
+ class_name = self.__class__.__name__
+ warnings.warn(
+ f"The {class_name}.is_fd property is deprecated and superseded by "
+ f"{class_name}.protocol. It is scheduled for removal in python-can version 5.0.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self._can_protocol is CanProtocol.CAN_FD
+
+ def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]:
+ result = self._multicast.recv(timeout)
+ if not result:
+ return None, False
+
+ data, _, timestamp = result
+ try:
+ can_message = unpack_message(
+ data, replace={"timestamp": timestamp}, check=True
+ )
+ except Exception as exception:
+ raise can.CanOperationError(
+ "could not unpack received message"
+ ) from exception
+
+ if self._can_protocol is not CanProtocol.CAN_FD and can_message.is_fd:
+ return None, False
+
+ return can_message, False
+
+ def send(self, msg: can.Message, timeout: float | None = None) -> None:
+ if self._can_protocol is not CanProtocol.CAN_FD and msg.is_fd:
+ raise can.CanOperationError(
+ "cannot send FD message over bus with CAN FD disabled"
+ )
+
+ data = pack_message(msg)
+ self._multicast.send(data, timeout)
+
+ def fileno(self) -> int:
+ """Provides the internally used file descriptor of the socket or `-1` if not available."""
+ return self._multicast.fileno()
+
+ def shutdown(self) -> None:
+ """Close all sockets and free up any resources.
+
+ Never throws errors and only logs them.
+ """
+ super().shutdown()
+ self._multicast.shutdown()
+
+ @staticmethod
+ def _detect_available_configs() -> list[AutoDetectedConfig]:
+ if hasattr(socket, "CMSG_SPACE"):
+ return [
+ {
+ "interface": "udp_multicast",
+ "channel": UdpMulticastBus.DEFAULT_GROUP_IPv6,
+ },
+ {
+ "interface": "udp_multicast",
+ "channel": UdpMulticastBus.DEFAULT_GROUP_IPv4,
+ },
+ ]
+
+ # else, this interface cannot be used
+ return []
+
+
+class GeneralPurposeUdpMulticastBus:
+ """A general purpose send and receive handler for multicast over IP/UDP.
+
+ However, it raises CAN-specific exceptions for convenience.
+ """
+
+ def __init__(
+ self, group: str, port: int, hop_limit: int, max_buffer: int = 4096
+ ) -> None:
+ self.group = group
+ self.port = port
+ self.hop_limit = hop_limit
+ self.max_buffer = max_buffer
+
+ # `False` will always work, no matter the setup. This might be changed by _create_socket().
+ self.timestamp_nanosecond = False
+
+ # Look up multicast group address in name server and find out IP version of the first suitable target
+ # and then get the address family of it (socket.AF_INET or socket.AF_INET6)
+ connection_candidates = socket.getaddrinfo(
+ group, self.port, type=socket.SOCK_DGRAM
+ )
+ sock = None
+ for connection_candidate in connection_candidates:
+ address_family: socket.AddressFamily = connection_candidate[0]
+ self.ip_version = 4 if address_family == socket.AF_INET else 6
+ try:
+ sock = self._create_socket(address_family)
+ except OSError as error:
+ log.info(
+ "could not connect to the multicast IP network of candidate %s; reason: %s",
+ connection_candidates,
+ error,
+ )
+ if sock is not None:
+ self._socket = sock
+ else:
+ raise can.CanInitializationError(
+ "could not connect to a multicast IP network"
+ )
+
+ # used in recv()
+ self.received_timestamp_struct = "@ll"
+ self.received_timestamp_struct_size = struct.calcsize(
+ self.received_timestamp_struct
+ )
+ if self.timestamp_nanosecond:
+ self.received_ancillary_buffer_size = socket.CMSG_SPACE(
+ self.received_timestamp_struct_size
+ )
+ else:
+ self.received_ancillary_buffer_size = 0
+
+ # used by send()
+ self._send_destination = (self.group, self.port)
+ self._last_send_timeout: float | None = None
+
+ def _create_socket(self, address_family: socket.AddressFamily) -> socket.socket:
+ """Creates a new socket. This might fail and raise an exception!
+
+ :param address_family: whether this is of type `socket.AF_INET` or `socket.AF_INET6`
+
+ :raises can.CanInitializationError:
+ if the socket could not be opened or configured correctly; in this case, it is
+ guaranteed to be closed/cleaned up
+ """
+ # create the UDP socket
+ # this might already fail but then there is nothing to clean up
+ sock = socket.socket(address_family, socket.SOCK_DGRAM)
+
+ # configure the socket
+ try:
+ # set hop limit / TTL
+ ttl_as_binary = struct.pack("@I", self.hop_limit)
+ if self.ip_version == 4:
+ sock.setsockopt(
+ socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl_as_binary
+ )
+ else:
+ sock.setsockopt(
+ socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, ttl_as_binary
+ )
+
+ # Allow multiple programs to access that address + port
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+ # Option not supported on Windows.
+ if hasattr(socket, "SO_REUSEPORT"):
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+
+ # set how to receive timestamps
+ try:
+ sock.setsockopt(socket.SOL_SOCKET, SO_TIMESTAMPNS, 1)
+ except OSError as error:
+ if (
+ error.errno == errno.ENOPROTOOPT
+ or error.errno == errno.EINVAL
+ or error.errno == WSAEINVAL
+ ): # It is unavailable on macOS (ENOPROTOOPT) or windows(EINVAL/WSAEINVAL)
+ self.timestamp_nanosecond = False
+ else:
+ raise error
+ else:
+ self.timestamp_nanosecond = True
+
+ # Bind it to the port (on any interface)
+ sock.bind(("", self.port))
+
+ # Join the multicast group
+ group_as_binary = socket.inet_pton(address_family, self.group)
+ if self.ip_version == 4:
+ request = group_as_binary + struct.pack("@I", socket.INADDR_ANY)
+ sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, request)
+ else:
+ request = group_as_binary + struct.pack("@I", 0)
+ sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, request)
+
+ return sock
+
+ except OSError as error:
+ # clean up the incompletely configured but opened socket
+ try:
+ sock.close()
+ except OSError as close_error:
+ # ignore but log any failures in here
+ log.warning("Could not close partly configured socket: %s", close_error)
+
+ # still raise the error
+ raise can.CanInitializationError(
+ "could not create or configure socket"
+ ) from error
+
+ def send(self, data: bytes, timeout: float | None = None) -> None:
+ """Send data to all group members. This call blocks.
+
+ :param timeout: the timeout in seconds after which an Exception is raised is sending has failed
+ :param data: the data to be sent
+ :raises can.CanOperationError: if an error occurred while writing to the underlying socket
+ :raises can.CanTimeoutError: if the timeout ran out before sending was completed
+ """
+ if timeout != self._last_send_timeout:
+ self._last_send_timeout = timeout
+ # this applies to all blocking calls on the socket, but sending is the only one that is blocking
+ self._socket.settimeout(timeout)
+
+ try:
+ bytes_sent = self._socket.sendto(data, self._send_destination)
+ if bytes_sent < len(data):
+ raise TimeoutError()
+ except TimeoutError:
+ raise can.CanTimeoutError() from None
+ except OSError as error:
+ raise can.CanOperationError("failed to send via socket") from error
+
+ def recv(
+ self, timeout: float | None = None
+ ) -> tuple[bytes, IP_ADDRESS_INFO, float] | None:
+ """
+ Receive up to **max_buffer** bytes.
+
+ :param timeout: the timeout in seconds after which `None` is returned if no data arrived
+ :returns: `None` on timeout, or a 3-tuple comprised of:
+ - received data,
+ - the sender of the data, and
+ - a timestamp in seconds
+ """
+ # get all sockets that are ready (can be a list with a single value
+ # being self.socket or an empty list if self.socket is not ready)
+ try:
+ # get all sockets that are ready (can be a list with a single value
+ # being self.socket or an empty list if self.socket is not ready)
+ ready_receive_sockets, _, _ = select.select([self._socket], [], [], timeout)
+ except OSError as exc:
+ # something bad (not a timeout) happened (e.g. the interface went down)
+ raise can.CanOperationError(
+ f"Failed to wait for IP/UDP socket: {exc}"
+ ) from exc
+
+ if ready_receive_sockets: # not empty
+ # fetch timestamp; this is configured in _create_socket()
+ if self.timestamp_nanosecond:
+ # fetch data, timestamp & source address
+ (
+ raw_message_data,
+ ancillary_data,
+ _, # flags
+ sender_address,
+ ) = self._socket.recvmsg(
+ self.max_buffer, self.received_ancillary_buffer_size
+ )
+
+ # Very similar to timestamp handling in can/interfaces/socketcan/socketcan.py -> capture_message()
+ if len(ancillary_data) != 1:
+ raise can.CanOperationError(
+ "Only requested a single extra field but got a different amount"
+ )
+ cmsg_level, cmsg_type, cmsg_data = ancillary_data[0]
+ if cmsg_level != socket.SOL_SOCKET or cmsg_type != SO_TIMESTAMPNS:
+ raise can.CanOperationError(
+ "received control message type that was not requested"
+ )
+ # see https://man7.org/linux/man-pages/man3/timespec.3.html -> struct timespec for details
+ seconds, nanoseconds = struct.unpack(
+ self.received_timestamp_struct, cmsg_data
+ )
+ if nanoseconds >= 1e9:
+ raise can.CanOperationError(
+ f"Timestamp nanoseconds field was out of range: {nanoseconds} not less than 1e9"
+ )
+ timestamp = seconds + nanoseconds * 1.0e-9
+ else:
+ # fetch data & source address
+ (raw_message_data, sender_address) = self._socket.recvfrom(
+ self.max_buffer
+ )
+
+ if is_linux:
+ # This ioctl isn't supported on Darwin & Windows.
+ result_buffer = ioctl(
+ self._socket.fileno(),
+ SIOCGSTAMP,
+ bytes(self.received_timestamp_struct_size),
+ )
+ seconds, microseconds = struct.unpack(
+ self.received_timestamp_struct, result_buffer
+ )
+ else:
+ # fallback to time.time_ns
+ now = time.time()
+
+ # Extract seconds and microseconds
+ seconds = int(now)
+ microseconds = int((now - seconds) * 1000000)
+
+ if microseconds >= 1e6:
+ raise can.CanOperationError(
+ f"Timestamp microseconds field was out of range: {microseconds} not less than 1e6"
+ )
+ timestamp = seconds + microseconds * 1e-6
+
+ return raw_message_data, sender_address, timestamp
+
+ # socket wasn't readable or timeout occurred
+ return None
+
+ def fileno(self) -> int:
+ """Provides the internally used file descriptor of the socket or `-1` if not available."""
+ return self._socket.fileno()
+
+ def shutdown(self) -> None:
+ """Close all sockets and free up any resources.
+
+ Never throws errors and only logs them.
+ """
+ try:
+ self._socket.close()
+ except OSError as exception:
+ log.error("could not close IP socket: %s", exception)
diff --git a/can/interfaces/udp_multicast/utils.py b/can/interfaces/udp_multicast/utils.py
new file mode 100644
index 000000000..1e1d62c23
--- /dev/null
+++ b/can/interfaces/udp_multicast/utils.py
@@ -0,0 +1,77 @@
+"""
+Defines common functions.
+"""
+
+from typing import Any, cast
+
+from can import CanInterfaceNotImplementedError, Message
+from can.typechecking import ReadableBytesLike
+
+try:
+ import msgpack
+except ImportError:
+ msgpack = None
+
+
+def is_msgpack_installed(raise_exception: bool = True) -> bool:
+ """Check whether the ``msgpack`` module is installed.
+
+ :param raise_exception:
+ If True, raise a :class:`can.CanInterfaceNotImplementedError` when ``msgpack`` is not installed.
+ If False, return False instead.
+ :return:
+ True if ``msgpack`` is installed, False otherwise.
+ :raises can.CanInterfaceNotImplementedError:
+ If ``msgpack`` is not installed and ``raise_exception`` is True.
+ """
+ if msgpack is None:
+ if raise_exception:
+ raise CanInterfaceNotImplementedError("msgpack not installed")
+ return False
+ return True
+
+
+def pack_message(message: Message) -> bytes:
+ """
+ Pack a can.Message into a msgpack byte blob.
+
+ :param message: the message to be packed
+ """
+ is_msgpack_installed()
+ as_dict = {
+ "timestamp": message.timestamp,
+ "arbitration_id": message.arbitration_id,
+ "is_extended_id": message.is_extended_id,
+ "is_remote_frame": message.is_remote_frame,
+ "is_error_frame": message.is_error_frame,
+ "channel": message.channel,
+ "dlc": message.dlc,
+ "data": message.data,
+ "is_fd": message.is_fd,
+ "bitrate_switch": message.bitrate_switch,
+ "error_state_indicator": message.error_state_indicator,
+ }
+ return cast("bytes", msgpack.packb(as_dict, use_bin_type=True))
+
+
+def unpack_message(
+ data: ReadableBytesLike,
+ replace: dict[str, Any] | None = None,
+ check: bool = False,
+) -> Message:
+ """Unpack a can.Message from a msgpack byte blob.
+
+ :param data: the raw data
+ :param replace: a mapping from field names to values to be replaced after decoding the new message, or
+ `None` to disable this feature
+ :param check: this is passed to :meth:`can.Message.__init__` to specify whether to validate the message
+
+ :raise TypeError: if the data contains key that are not valid arguments for :meth:`can.Message.__init__`
+ :raise ValueError: if `check` is true and the message metadata is invalid in some way
+ :raise Exception: if there was another problem while unpacking
+ """
+ is_msgpack_installed()
+ as_dict = msgpack.unpackb(data, raw=False)
+ if replace is not None:
+ as_dict.update(replace)
+ return Message(check=check, **as_dict)
diff --git a/can/interfaces/usb2can/__init__.py b/can/interfaces/usb2can/__init__.py
index 8262dc47b..f818130ee 100644
--- a/can/interfaces/usb2can/__init__.py
+++ b/can/interfaces/usb2can/__init__.py
@@ -1,10 +1,12 @@
-#!/usr/bin/env python
-# coding: utf-8
+""" """
-"""
-"""
+__all__ = [
+ "Usb2CanAbstractionLayer",
+ "Usb2canBus",
+ "serial_selector",
+ "usb2canInterface",
+ "usb2canabstractionlayer",
+]
-from __future__ import absolute_import
-
-from .usb2canInterface import Usb2canBus
from .usb2canabstractionlayer import Usb2CanAbstractionLayer
+from .usb2canInterface import Usb2canBus
diff --git a/can/interfaces/usb2can/serial_selector.py b/can/interfaces/usb2can/serial_selector.py
index 0e1ffb56a..18ad3f873 100644
--- a/can/interfaces/usb2can/serial_selector.py
+++ b/can/interfaces/usb2can/serial_selector.py
@@ -1,41 +1,63 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-"""
-"""
+""" """
import logging
+log = logging.getLogger("can.usb2can")
+
try:
+ import pythoncom
import win32com.client
except ImportError:
- logging.warning("win32com.client module required for usb2can")
+ log.warning(
+ "win32com.client module required for usb2can. Install the 'pywin32' package."
+ )
raise
-def WMIDateStringToDate(dtmDate):
- if (dtmDate[4] == 0):
- strDateTime = dtmDate[5] + '/'
+def WMIDateStringToDate(dtmDate) -> str:
+ if dtmDate[4] == 0:
+ strDateTime = dtmDate[5] + "/"
else:
- strDateTime = dtmDate[4] + dtmDate[5] + '/'
+ strDateTime = dtmDate[4] + dtmDate[5] + "/"
- if (dtmDate[6] == 0):
- strDateTime = strDateTime + dtmDate[7] + '/'
+ if dtmDate[6] == 0:
+ strDateTime = strDateTime + dtmDate[7] + "/"
else:
- strDateTime = strDateTime + dtmDate[6] + dtmDate[7] + '/'
- strDateTime = strDateTime + dtmDate[0] + dtmDate[1] + dtmDate[2] + dtmDate[3] + ' ' + dtmDate[8] + dtmDate[9] \
- + ':' + dtmDate[10] + dtmDate[11] + ':' + dtmDate[12] + dtmDate[13]
+ strDateTime = strDateTime + dtmDate[6] + dtmDate[7] + "/"
+ strDateTime = (
+ strDateTime
+ + dtmDate[0]
+ + dtmDate[1]
+ + dtmDate[2]
+ + dtmDate[3]
+ + " "
+ + dtmDate[8]
+ + dtmDate[9]
+ + ":"
+ + dtmDate[10]
+ + dtmDate[11]
+ + ":"
+ + dtmDate[12]
+ + dtmDate[13]
+ )
return strDateTime
-def serial():
- strComputer = '.'
- objWMIService = win32com.client.Dispatch("WbemScripting.SWbemLocator")
- objSWbemServices = objWMIService.ConnectServer(strComputer, "root\cimv2")
- colItems = objSWbemServices.ExecQuery("SELECT * FROM Win32_USBControllerDevice")
-
- for objItem in colItems:
- string = objItem.Dependent
- # find based on beginning of serial
- if 'ED' in string:
- return string[len(string) - 9:len(string) - 1]
+def find_serial_devices(serial_matcher: str = "") -> list[str]:
+ """
+ Finds a list of USB devices where the serial number (partially) matches the given string.
+
+ :param serial_matcher:
+ only device IDs starting with this string are returned
+ """
+ serial_numbers = []
+ pythoncom.CoInitialize()
+ wmi = win32com.client.GetObject("winmgmts:")
+ for usb_controller in wmi.InstancesOf("Win32_USBControllerDevice"):
+ usb_device = wmi.Get(usb_controller.Dependent)
+ if "USB2CAN" in usb_device.Name:
+ serial_numbers.append(usb_device.DeviceID.split("\\")[-1])
+
+ if serial_matcher:
+ return [sn for sn in serial_numbers if serial_matcher in sn]
+ return serial_numbers
diff --git a/can/interfaces/usb2can/usb2canInterface.py b/can/interfaces/usb2can/usb2canInterface.py
index 46bbda20e..66c171f4d 100644
--- a/can/interfaces/usb2can/usb2canInterface.py
+++ b/can/interfaces/usb2can/usb2canInterface.py
@@ -1,134 +1,172 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
-This interface is for windows only, otherwise use socketCAN.
+This interface is for Windows only, otherwise use SocketCAN.
"""
-from __future__ import absolute_import, division
-
import logging
-
-from can import BusABC, Message
-from .usb2canabstractionlayer import *
-
-bootTimeEpoch = 0
-try:
- import uptime
- import datetime
-
- bootTimeEpoch = (uptime.boottime() - datetime.datetime.utcfromtimestamp(0)).total_seconds()
-except:
- bootTimeEpoch = 0
+from ctypes import byref
+
+from can import (
+ BitTiming,
+ BitTimingFd,
+ BusABC,
+ CanInitializationError,
+ CanOperationError,
+ CanProtocol,
+ Message,
+)
+from can.util import check_or_adjust_timing_clock
+
+from .serial_selector import find_serial_devices
+from .usb2canabstractionlayer import (
+ IS_ERROR_FRAME,
+ IS_ID_TYPE,
+ IS_REMOTE_FRAME,
+ CanalError,
+ CanalMsg,
+ Usb2CanAbstractionLayer,
+)
# Set up logging
-log = logging.getLogger('can.usb2can')
-
-
-def format_connection_string(deviceID, baudrate='500'):
- """setup the string for the device
-
- config = deviceID + '; ' + baudrate
- """
- return "%s; %s" % (deviceID, baudrate)
+log = logging.getLogger("can.usb2can")
def message_convert_tx(msg):
- messagetx = CanalMsg()
+ message_tx = CanalMsg()
- length = len(msg.data)
- messagetx.sizeData = length
+ length = msg.dlc
+ message_tx.sizeData = length
- messagetx.id = msg.arbitration_id
+ message_tx.id = msg.arbitration_id
for i in range(length):
- messagetx.data[i] = msg.data[i]
+ message_tx.data[i] = msg.data[i]
- messagetx.flags = 0x80000000
+ message_tx.flags = 0x80000000
if msg.is_error_frame:
- messagetx.flags |= IS_ERROR_FRAME
+ message_tx.flags |= IS_ERROR_FRAME
if msg.is_remote_frame:
- messagetx.flags |= IS_REMOTE_FRAME
+ message_tx.flags |= IS_REMOTE_FRAME
- if msg.id_type:
- messagetx.flags |= IS_ID_TYPE
+ if msg.is_extended_id:
+ message_tx.flags |= IS_ID_TYPE
- return messagetx
+ return message_tx
-def message_convert_rx(messagerx):
+def message_convert_rx(message_rx):
"""convert the message from the CANAL type to pythoncan type"""
- ID_TYPE = bool(messagerx.flags & IS_ID_TYPE)
- REMOTE_FRAME = bool(messagerx.flags & IS_REMOTE_FRAME)
- ERROR_FRAME = bool(messagerx.flags & IS_ERROR_FRAME)
+ is_extended_id = bool(message_rx.flags & IS_ID_TYPE)
+ is_remote_frame = bool(message_rx.flags & IS_REMOTE_FRAME)
+ is_error_frame = bool(message_rx.flags & IS_ERROR_FRAME)
- msgrx = Message(timestamp=messagerx.timestamp,
- is_remote_frame=REMOTE_FRAME,
- extended_id=ID_TYPE,
- is_error_frame=ERROR_FRAME,
- arbitration_id=messagerx.id,
- dlc=messagerx.sizeData,
- data=messagerx.data[:messagerx.sizeData]
- )
-
- return msgrx
+ return Message(
+ timestamp=message_rx.timestamp,
+ is_remote_frame=is_remote_frame,
+ is_extended_id=is_extended_id,
+ is_error_frame=is_error_frame,
+ arbitration_id=message_rx.id,
+ dlc=message_rx.sizeData,
+ data=message_rx.data[: message_rx.sizeData],
+ )
class Usb2canBus(BusABC):
"""Interface to a USB2CAN Bus.
- :param str channel:
+ This interface only works on Windows.
+ Please use socketcan on Linux.
+
+ :param channel:
The device's serial number. If not provided, Windows Management Instrumentation
- will be used to identify the first such device. The *kwarg* `serial` may also be
- used.
+ will be used to identify the first such device.
- :param int bitrate:
+ :param bitrate:
Bitrate of channel in bit/s. Values will be limited to a maximum of 1000 Kb/s.
Default is 500 Kbs
- :param int flags:
+ :param timing:
+ Optional :class:`~can.BitTiming` instance to use for custom bit timing setting.
+ If this argument is set then it overrides the bitrate argument. The
+ `f_clock` value of the timing instance must be set to 32_000_000 (32MHz)
+ for standard CAN.
+ CAN FD and the :class:`~can.BitTimingFd` class are not supported.
+
+ :param flags:
Flags to directly pass to open function of the usb2can abstraction layer.
- """
- def __init__(self, channel, *args, **kwargs):
+ :param dll:
+ Path to the DLL with the CANAL API to load
+ Defaults to 'usb2can.dll'
- self.can = Usb2CanAbstractionLayer()
+ :param serial:
+ Alias for `channel` that is provided for legacy reasons.
+ If both `serial` and `channel` are set, `serial` will be used and
+ channel will be ignored.
- # set flags on the connection
- if 'flags' in kwargs:
- enable_flags = kwargs["flags"]
- else:
- enable_flags = 0x00000008
+ """
- # code to get the serial number of the device
- if 'serial' in kwargs:
- deviceID = kwargs["serial"]
- elif channel is not None:
- deviceID = channel
+ def __init__(
+ self,
+ channel: str | None = None,
+ dll: str = "usb2can.dll",
+ flags: int = 0x00000008,
+ bitrate: int = 500000,
+ timing: BitTiming | BitTimingFd | None = None,
+ serial: str | None = None,
+ **kwargs,
+ ):
+ self.can = Usb2CanAbstractionLayer(dll)
+
+ # get the serial number of the device
+ device_id = serial or channel
+
+ # search for a serial number if the device_id is None or empty
+ if not device_id:
+ devices = find_serial_devices()
+ if not devices:
+ raise CanInitializationError("could not automatically find any device")
+ device_id = devices[0]
+
+ self.channel_info = f"USB2CAN device {device_id}"
+
+ if isinstance(timing, BitTiming):
+ timing = check_or_adjust_timing_clock(timing, valid_clocks=[32_000_000])
+ connector = (
+ f"{device_id};"
+ "0;"
+ f"{timing.tseg1};"
+ f"{timing.tseg2};"
+ f"{timing.sjw};"
+ f"{timing.brp}"
+ )
+ elif isinstance(timing, BitTimingFd):
+ raise NotImplementedError(
+ f"CAN FD is not supported by {self.__class__.__name__}."
+ )
else:
- from can.interfaces.usb2can.serial_selector import serial
- deviceID = serial()
-
- # get baudrate in b/s from bitrate or use default
- bitrate = kwargs.get("bitrate", 500000)
- # convert to kb/s (eg:500000 bitrate must be 500), max rate is 1000 kb/s
- baudrate = min(1000, int(bitrate/1000))
+ # convert to kb/s and cap: max rate is 1000 kb/s
+ baudrate = min(int(bitrate // 1000), 1000)
+ connector = f"{device_id};{baudrate}"
- connector = format_connection_string(deviceID, baudrate)
+ self._can_protocol = CanProtocol.CAN_20
+ self.handle = self.can.open(connector, flags)
- self.handle = self.can.open(connector.encode('utf-8'), enable_flags)
+ super().__init__(channel=channel, **kwargs)
def send(self, msg, timeout=None):
tx = message_convert_tx(msg)
+
if timeout:
- self.can.blocking_send(self.handle, byref(tx), int(timeout * 1000))
+ status = self.can.blocking_send(self.handle, byref(tx), int(timeout * 1000))
else:
- self.can.send(self.handle, byref(tx))
+ status = self.can.send(self.handle, byref(tx))
- def _recv_internal(self, timeout):
+ if status != CanalError.SUCCESS:
+ raise CanOperationError("could not send message", error_code=status)
+ def _recv_internal(self, timeout):
messagerx = CanalMsg()
if timeout == 0:
@@ -138,18 +176,46 @@ def _recv_internal(self, timeout):
time = 0 if timeout is None else int(timeout * 1000)
status = self.can.blocking_receive(self.handle, byref(messagerx), time)
- if status == 0:
+ if status == CanalError.SUCCESS:
rx = message_convert_rx(messagerx)
- elif status == 19 or status == 32:
- # CANAL_ERROR_RCV_EMPTY or CANAL_ERROR_TIMEOUT
+ elif status in (
+ CanalError.RCV_EMPTY,
+ CanalError.TIMEOUT,
+ CanalError.FIFO_EMPTY,
+ ):
rx = None
else:
- log.error('Canal Error %s', status)
- rx = None
+ raise CanOperationError("could not receive message", error_code=status)
return rx, False
def shutdown(self):
- """Shut down the device safely"""
- # TODO handle error
+ """
+ Shuts down connection to the device safely.
+
+ :raise cam.CanOperationError: is closing the connection did not work
+ """
+ super().shutdown()
status = self.can.close(self.handle)
+
+ if status != CanalError.SUCCESS:
+ raise CanOperationError("could not shut down bus", error_code=status)
+
+ @staticmethod
+ def _detect_available_configs():
+ return Usb2canBus.detect_available_configs()
+
+ @staticmethod
+ def detect_available_configs(serial_matcher: str | None = None):
+ """
+ Uses the *Windows Management Instrumentation* to identify serial devices.
+
+ :param serial_matcher:
+ search string for automatic detection of the device serial
+ """
+ if serial_matcher is None:
+ channels = find_serial_devices()
+ else:
+ channels = find_serial_devices(serial_matcher)
+
+ return [{"interface": "usb2can", "channel": c} for c in channels]
diff --git a/can/interfaces/usb2can/usb2canabstractionlayer.py b/can/interfaces/usb2can/usb2canabstractionlayer.py
index 608c1dca8..9fbf5c15c 100644
--- a/can/interfaces/usb2can/usb2canabstractionlayer.py
+++ b/can/interfaces/usb2can/usb2canabstractionlayer.py
@@ -1,24 +1,25 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
This wrapper is for windows or direct access via CANAL API.
Socket CAN is recommended under Unix/Linux systems.
"""
-import can
-from ctypes import *
-from struct import *
import logging
+from ctypes import *
+from enum import IntEnum
+
+import can
+
+from ...exceptions import error_check
+from ...typechecking import StringPathLike
-log = logging.getLogger('can.usb2can')
+log = logging.getLogger("can.usb2can")
# type definitions
-flags = c_ulong
+flags_t = c_ulong
pConfigureStr = c_char_p
-handle = c_long
-timeout = c_ulong
-filter = c_ulong
+handle_t = c_long
+timeout_t = c_ulong
+filter_t = c_ulong
# flags mappings
IS_ERROR_FRAME = 4
@@ -26,132 +27,180 @@
IS_ID_TYPE = 1
+class CanalError(IntEnum):
+ SUCCESS = 0
+ BAUDRATE = 1
+ BUS_OFF = 2
+ BUS_PASSIVE = 3
+ BUS_WARNING = 4
+ CAN_ID = 5
+ CAN_MESSAGE = 6
+ CHANNEL = 7
+ FIFO_EMPTY = 8
+ FIFO_FULL = 9
+ FIFO_SIZE = 10
+ FIFO_WAIT = 11
+ GENERIC = 12
+ HARDWARE = 13
+ INIT_FAIL = 14
+ INIT_MISSING = 15
+ INIT_READY = 16
+ NOT_SUPPORTED = 17
+ OVERRUN = 18
+ RCV_EMPTY = 19
+ REGISTER = 20
+ TRM_FULL = 21
+ ERRFRM_STUFF = 22
+ ERRFRM_FORM = 23
+ ERRFRM_ACK = 24
+ ERRFRM_BIT1 = 25
+ ERRFRM_BIT0 = 26
+ ERRFRM_CRC = 27
+ LIBRARY = 28
+ PROCADDRESS = 29
+ ONLY_ONE_INSTANCE = 30
+ SUB_DRIVER = 31
+ TIMEOUT = 32
+ NOT_OPEN = 33
+ PARAMETER = 34
+ MEMORY = 35
+ INTERNAL = 36
+ COMMUNICATION = 37
+
+
class CanalStatistics(Structure):
- _fields_ = [('ReceiveFrams', c_ulong),
- ('TransmistFrams', c_ulong),
- ('ReceiveData', c_ulong),
- ('TransmitData', c_ulong),
- ('Overruns', c_ulong),
- ('BusWarnings', c_ulong),
- ('BusOff', c_ulong)]
+ _fields_ = [
+ ("ReceiveFrams", c_ulong),
+ ("TransmistFrams", c_ulong),
+ ("ReceiveData", c_ulong),
+ ("TransmitData", c_ulong),
+ ("Overruns", c_ulong),
+ ("BusWarnings", c_ulong),
+ ("BusOff", c_ulong),
+ ]
stat = CanalStatistics
class CanalStatus(Structure):
- _fields_ = [('channel_status', c_ulong),
- ('lasterrorcode', c_ulong),
- ('lasterrorsubcode', c_ulong),
- ('lasterrorstr', c_byte * 80)]
+ _fields_ = [
+ ("channel_status", c_ulong),
+ ("lasterrorcode", c_ulong),
+ ("lasterrorsubcode", c_ulong),
+ ("lasterrorstr", c_byte * 80),
+ ]
# data type for the CAN Message
class CanalMsg(Structure):
- _fields_ = [('flags', c_ulong),
- ('obid', c_ulong),
- ('id', c_ulong),
- ('sizeData', c_ubyte),
- ('data', c_ubyte * 8),
- ('timestamp', c_ulong)]
+ _fields_ = [
+ ("flags", c_ulong),
+ ("obid", c_ulong),
+ ("id", c_ulong),
+ ("sizeData", c_ubyte),
+ ("data", c_ubyte * 8),
+ ("timestamp", c_ulong),
+ ]
class Usb2CanAbstractionLayer:
"""A low level wrapper around the usb2can library.
Documentation: http://www.8devices.com/media/products/usb2can/downloads/CANAL_API.pdf
-
"""
- def __init__(self):
- self.__m_dllBasic = windll.LoadLibrary("usb2can.dll")
-
- if self.__m_dllBasic is None:
- log.warning('DLL failed to load')
-
- def open(self, pConfigureStr, flags):
- try:
- res = self.__m_dllBasic.CanalOpen(pConfigureStr, flags)
- return res
- except:
- log.warning('Failed to open')
- raise
- def close(self, handle):
- try:
- res = self.__m_dllBasic.CanalClose(handle)
- return res
- except:
- log.warning('Failed to close')
- raise
-
- def send(self, handle, msg):
- try:
- res = self.__m_dllBasic.CanalSend(handle, msg)
- return res
- except:
- log.warning('Sending error')
- raise can.CanError("Failed to transmit frame")
+ def __init__(self, dll: StringPathLike = "usb2can.dll") -> None:
+ """
+ :param dll:
+ the path to the usb2can DLL to load
- def receive(self, handle, msg):
+ :raises ~can.exceptions.CanInterfaceNotImplementedError:
+ if the DLL could not be loaded
+ """
try:
- res = self.__m_dllBasic.CanalReceive(handle, msg)
- return res
- except:
- log.warning('Receive error')
- raise
-
- def blocking_send(self, handle, msg, timeout):
+ self.__m_dllBasic = windll.LoadLibrary(dll)
+ if self.__m_dllBasic is None:
+ raise Exception("__m_dllBasic is None")
+
+ except Exception as error:
+ message = f"DLL failed to load at path: {dll}"
+ raise can.CanInterfaceNotImplementedError(message) from error
+
+ def open(self, configuration: str, flags: int):
+ """
+ Opens a CAN connection using `CanalOpen()`.
+
+ :param configuration:
+ the configuration: "device_id; baudrate"
+ :param flags:
+ the flags to be set
+ :returns:
+ Valid handle for CANAL API functions on success
+
+ :raises ~can.exceptions.CanInterfaceNotImplementedError:
+ if any error occurred
+ """
try:
- res = self.__m_dllBasic.CanalBlockingSend(handle, msg, timeout)
- return res
- except:
- log.warning('Blocking send error')
- raise
-
- def blocking_receive(self, handle, msg, timeout):
- try:
- res = self.__m_dllBasic.CanalBlockingReceive(handle, msg, timeout)
- return res
- except:
- log.warning('Blocking Receive Failed')
- raise
-
- def get_status(self, handle, CanalStatus):
- try:
- res = self.__m_dllBasic.CanalGetStatus(handle, CanalStatus)
- return res
- except:
- log.warning('Get status failed')
- raise
-
- def get_statistics(self, handle, CanalStatistics):
- try:
- res = self.__m_dllBasic.CanalGetStatistics(handle, CanalStatistics)
- return res
- except:
- log.warning('Get Statistics failed')
- raise
+ # we need to convert this into bytes, since the underlying DLL cannot
+ # handle non-ASCII configuration strings
+ config_ascii = configuration.encode("ascii", "ignore")
+ result = self.__m_dllBasic.CanalOpen(config_ascii, flags)
+ except Exception as ex:
+ # catch any errors thrown by this call and re-raise
+ raise can.CanInitializationError(
+ f'CanalOpen() failed, configuration: "{configuration}", error: {ex}'
+ ) from ex
+ else:
+ # any greater-than-zero return value indicates a success
+ # (see https://grodansparadis.gitbooks.io/the-vscp-daemon/canal_interface_specification.html)
+ # raise an error if the return code is <= 0
+ if result <= 0:
+ raise can.CanInitializationError(
+ f'CanalOpen() failed, configuration: "{configuration}"',
+ error_code=result,
+ )
+ else:
+ return result
+
+ def close(self, handle) -> CanalError:
+ with error_check("Failed to close"):
+ return CanalError(self.__m_dllBasic.CanalClose(handle))
+
+ def send(self, handle, msg) -> CanalError:
+ with error_check("Failed to transmit frame"):
+ return CanalError(self.__m_dllBasic.CanalSend(handle, msg))
+
+ def receive(self, handle, msg) -> CanalError:
+ with error_check("Receive error"):
+ return CanalError(self.__m_dllBasic.CanalReceive(handle, msg))
+
+ def blocking_send(self, handle, msg, timeout) -> CanalError:
+ with error_check("Blocking send error"):
+ return CanalError(self.__m_dllBasic.CanalBlockingSend(handle, msg, timeout))
+
+ def blocking_receive(self, handle, msg, timeout) -> CanalError:
+ with error_check("Blocking Receive Failed"):
+ return CanalError(
+ self.__m_dllBasic.CanalBlockingReceive(handle, msg, timeout)
+ )
+
+ def get_status(self, handle, status) -> CanalError:
+ with error_check("Get status failed"):
+ return CanalError(self.__m_dllBasic.CanalGetStatus(handle, status))
+
+ def get_statistics(self, handle, statistics) -> CanalError:
+ with error_check("Get Statistics failed"):
+ return CanalError(self.__m_dllBasic.CanalGetStatistics(handle, statistics))
def get_version(self):
- try:
- res = self.__m_dllBasic.CanalGetVersion()
- return res
- except:
- log.warning('Failed to get version info')
- raise
+ with error_check("Failed to get version info"):
+ return self.__m_dllBasic.CanalGetVersion()
def get_library_version(self):
- try:
- res = self.__m_dllBasic.CanalGetDllVersion()
- return res
- except:
- log.warning('Failed to get DLL version')
- raise
+ with error_check("Failed to get DLL version"):
+ return self.__m_dllBasic.CanalGetDllVersion()
def get_vendor_string(self):
- try:
- res = self.__m_dllBasic.CanalGetVendorString()
- return res
- except:
- log.warning('Failed to get vendor string')
- raise
+ with error_check("Failed to get vendor string"):
+ return self.__m_dllBasic.CanalGetVendorString()
diff --git a/can/interfaces/vector/__init__.py b/can/interfaces/vector/__init__.py
index 9342e6d60..e78783f1f 100644
--- a/can/interfaces/vector/__init__.py
+++ b/can/interfaces/vector/__init__.py
@@ -1,8 +1,28 @@
-#!/usr/bin/env python
-# coding: utf-8
+""" """
-"""
-"""
+__all__ = [
+ "VectorBus",
+ "VectorBusParams",
+ "VectorCanFdParams",
+ "VectorCanParams",
+ "VectorChannelConfig",
+ "VectorError",
+ "VectorInitializationError",
+ "VectorOperationError",
+ "canlib",
+ "exceptions",
+ "get_channel_configs",
+ "xlclass",
+ "xldefine",
+ "xldriver",
+]
-from .canlib import VectorBus
-from .exceptions import VectorError
+from .canlib import (
+ VectorBus,
+ VectorBusParams,
+ VectorCanFdParams,
+ VectorCanParams,
+ VectorChannelConfig,
+ get_channel_configs,
+)
+from .exceptions import VectorError, VectorInitializationError, VectorOperationError
diff --git a/can/interfaces/vector/canlib.py b/can/interfaces/vector/canlib.py
index bace816fc..8bdd77b83 100644
--- a/can/interfaces/vector/canlib.py
+++ b/can/interfaces/vector/canlib.py
@@ -1,229 +1,680 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Ctypes wrapper module for Vector CAN Interface on win32/win64 systems.
Authors: Julien Grave , Christian Sandberg
"""
-# Import Standard Python Modules
-# ==============================
+import contextlib
import ctypes
import logging
-import sys
+import os
import time
+import warnings
+from collections.abc import Callable, Iterator, Sequence
+from types import ModuleType
+from typing import (
+ Any,
+ NamedTuple,
+ cast,
+)
+
+from can import (
+ BitTiming,
+ BitTimingFd,
+ BusABC,
+ CanInitializationError,
+ CanInterfaceNotImplementedError,
+ CanProtocol,
+ Message,
+)
+from can.typechecking import AutoDetectedConfig, CanFilters
+from can.util import (
+ check_or_adjust_timing_clock,
+ deprecated_args_alias,
+ dlc2len,
+ len2dlc,
+ time_perfcounter_correlation,
+)
+
+from . import xlclass, xldefine
+from .exceptions import VectorError, VectorInitializationError, VectorOperationError
-try:
- # Try builtin Python 3 Windows API
- from _winapi import WaitForSingleObject, INFINITE
- HAS_EVENTS = True
-except ImportError:
- try:
- # Try pywin32 package
- from win32event import WaitForSingleObject, INFINITE
- HAS_EVENTS = True
- except ImportError:
- # Use polling instead
- HAS_EVENTS = False
-
-# Import Modules
-# ==============
-from can import BusABC, Message, CanError
-from can.util import len2dlc, dlc2len
-from .exceptions import VectorError
-
-# Define Module Logger
-# ====================
LOG = logging.getLogger(__name__)
# Import safely Vector API module for Travis tests
-vxlapi = None
+xldriver: ModuleType | None = None
try:
- from . import vxlapi
-except Exception as exc:
- LOG.warning('Could not import vxlapi: %s', exc)
+ from . import xldriver
+except FileNotFoundError as exc:
+ LOG.warning("Could not import vxlapi: %s", exc)
+
+WaitForSingleObject: Callable[[int, int], int] | None
+INFINITE: int | None
+try:
+ # Try builtin Python 3 Windows API
+ from _winapi import ( # type: ignore[attr-defined,no-redef,unused-ignore]
+ INFINITE,
+ WaitForSingleObject,
+ )
+
+ HAS_EVENTS = True
+except ImportError:
+ WaitForSingleObject, INFINITE = None, None
+ HAS_EVENTS = False
class VectorBus(BusABC):
"""The CAN Bus implemented for the Vector interface."""
- def __init__(self, channel, can_filters=None, poll_interval=0.01,
- receive_own_messages=False,
- bitrate=None, rx_queue_size=2**14, app_name="CANalyzer", serial=None, fd=False, data_bitrate=None, sjwAbr=2, tseg1Abr=6, tseg2Abr=3, sjwDbr=2, tseg1Dbr=6, tseg2Dbr=3, **config):
+ @deprecated_args_alias(
+ deprecation_start="4.0.0",
+ deprecation_end="5.0.0",
+ **{
+ "sjwAbr": "sjw_abr",
+ "tseg1Abr": "tseg1_abr",
+ "tseg2Abr": "tseg2_abr",
+ "sjwDbr": "sjw_dbr",
+ "tseg1Dbr": "tseg1_dbr",
+ "tseg2Dbr": "tseg2_dbr",
+ },
+ )
+ def __init__(
+ self,
+ channel: int | Sequence[int] | str,
+ can_filters: CanFilters | None = None,
+ poll_interval: float = 0.01,
+ receive_own_messages: bool = False,
+ timing: BitTiming | BitTimingFd | None = None,
+ bitrate: int | None = None,
+ rx_queue_size: int = 2**14,
+ app_name: str | None = "CANalyzer",
+ serial: int | None = None,
+ fd: bool = False,
+ data_bitrate: int | None = None,
+ sjw_abr: int = 2,
+ tseg1_abr: int = 6,
+ tseg2_abr: int = 3,
+ sjw_dbr: int = 2,
+ tseg1_dbr: int = 6,
+ tseg2_dbr: int = 3,
+ listen_only: bool | None = False,
+ **kwargs: Any,
+ ) -> None:
"""
- :param list channel:
+ :param channel:
The channel indexes to create this bus with.
Can also be a single integer or a comma separated string.
- :param float poll_interval:
+ :param can_filters:
+ See :class:`can.BusABC`.
+ :param receive_own_messages:
+ See :class:`can.BusABC`.
+ :param timing:
+ An instance of :class:`~can.BitTiming` or :class:`~can.BitTimingFd`
+ to specify the bit timing parameters for the VectorBus interface. The
+ `f_clock` value of the timing instance must be set to 8_000_000 (8MHz)
+ or 16_000_000 (16MHz) for CAN 2.0 or 80_000_000 (80MHz) for CAN FD.
+ If this parameter is provided, it takes precedence over all other
+ timing-related parameters.
+ Otherwise, the bit timing can be specified using the following parameters:
+ `bitrate` for standard CAN or `fd`, `data_bitrate`, `sjw_abr`, `tseg1_abr`,
+ `tseg2_abr`, `sjw_dbr`, `tseg1_dbr`, and `tseg2_dbr` for CAN FD.
+ :param poll_interval:
Poll interval in seconds.
- :param int bitrate:
+ :param bitrate:
Bitrate in bits/s.
- :param int rx_queue_size:
+ :param rx_queue_size:
Number of messages in receive queue (power of 2).
- CAN: range 16…32768
- CAN-FD: range 8192…524288
- :param str app_name:
- Name of application in Hardware Config.
- If set to None, the channel should be a global channel index.
- :param int serial:
+ CAN: range `16…32768`
+ CAN-FD: range `8192…524288`
+ :param app_name:
+ Name of application in *Vector Hardware Config*.
+ If set to `None`, the channel should be a global channel index.
+ :param serial:
Serial number of the hardware to be used.
If set, the channel parameter refers to the channels ONLY on the specified hardware.
- If set, the app_name is unused.
- :param bool fd:
+ If set, the `app_name` does not have to be previously defined in
+ *Vector Hardware Config*.
+ :param fd:
If CAN-FD frames should be supported.
- :param int data_bitrate:
+ :param data_bitrate:
Which bitrate to use for data phase in CAN FD.
Defaults to arbitration bitrate.
+ :param sjw_abr:
+ Bus timing value sample jump width (arbitration).
+ :param tseg1_abr:
+ Bus timing value tseg1 (arbitration)
+ :param tseg2_abr:
+ Bus timing value tseg2 (arbitration)
+ :param sjw_dbr:
+ Bus timing value sample jump width (data)
+ :param tseg1_dbr:
+ Bus timing value tseg1 (data)
+ :param tseg2_dbr:
+ Bus timing value tseg2 (data)
+ :param listen_only:
+ if the bus should be set to listen only mode.
+
+ :raise ~can.exceptions.CanInterfaceNotImplementedError:
+ If the current operating system is not supported or the driver could not be loaded.
+ :raise ~can.exceptions.CanInitializationError:
+ If the bus could not be set up.
+ This may or may not be a :class:`~can.interfaces.vector.VectorInitializationError`.
"""
- if vxlapi is None:
- raise ImportError("The Vector API has not been loaded")
+ self.__testing = kwargs.get("_testing", False)
+ if os.name != "nt" and not self.__testing:
+ raise CanInterfaceNotImplementedError(
+ f"The Vector interface is only supported on Windows, "
+ f'but you are running "{os.name}"'
+ )
+
+ if xldriver is None:
+ raise CanInterfaceNotImplementedError("The Vector API has not been loaded")
+ self.xldriver = xldriver # keep reference so mypy knows it is not None
+ self.xldriver.xlOpenDriver()
+
self.poll_interval = poll_interval
- if isinstance(channel, (list, tuple)):
- self.channels = channel
- elif isinstance(channel, int):
+
+ self.channels: Sequence[int]
+ if isinstance(channel, int):
self.channels = [channel]
- else:
+ elif isinstance(channel, str): # must be checked before generic Sequence
# Assume comma separated string of channels
- self.channels = [int(ch.strip()) for ch in channel.split(',')]
- self._app_name = app_name.encode()
- self.channel_info = 'Application %s: %s' % (
- app_name, ', '.join('CAN %d' % (ch + 1) for ch in self.channels))
+ self.channels = [int(ch.strip()) for ch in channel.split(",")]
+ elif isinstance(channel, Sequence):
+ self.channels = [int(ch) for ch in channel]
+ else:
+ raise TypeError(
+ f"Invalid type for parameter 'channel': {type(channel).__name__}"
+ )
- if serial is not None:
- app_name = None
- channel_index = []
- channel_configs = get_channel_configs()
- for channel_config in channel_configs:
- if channel_config.serialNumber == serial:
- if channel_config.hwChannel in self.channels:
- channel_index.append(channel_config.channelIndex)
- if len(channel_index) > 0:
- if len(channel_index) != len(self.channels):
- LOG.info("At least one defined channel wasn't found on the specified hardware.")
- self.channels = channel_index
- else:
- # Is there any better way to raise the error?
- raise Exception("None of the configured channels could be found on the specified hardware.")
+ self._app_name = app_name.encode() if app_name is not None else b""
+ self.channel_info = "Application {}: {}".format(
+ app_name,
+ ", ".join(f"CAN {ch + 1}" for ch in self.channels),
+ )
+
+ channel_configs = get_channel_configs()
+ is_fd = isinstance(timing, BitTimingFd) if timing else fd
- vxlapi.xlOpenDriver()
- self.port_handle = vxlapi.XLportHandle(vxlapi.XL_INVALID_PORTHANDLE)
self.mask = 0
- self.fd = fd
- # Get channels masks
- self.channel_masks = {}
- self.index_to_channel = {}
-
+ self.channel_masks: dict[int, int] = {}
+ self.index_to_channel: dict[int, int] = {}
+ self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20
+
+ self._listen_only = listen_only
+
for channel in self.channels:
- if app_name:
- # Get global channel index from application channel
- hw_type = ctypes.c_uint(0)
- hw_index = ctypes.c_uint(0)
- hw_channel = ctypes.c_uint(0)
- vxlapi.xlGetApplConfig(self._app_name, channel, hw_type, hw_index,
- hw_channel, vxlapi.XL_BUS_TYPE_CAN)
- LOG.debug('Channel index %d found', channel)
- idx = vxlapi.xlGetChannelIndex(hw_type.value, hw_index.value,
- hw_channel.value)
- if idx < 0:
- # Undocumented behavior! See issue #353.
- # If hardware is unavailable, this function returns -1.
- # Raise an exception as if the driver
- # would have signalled XL_ERR_HW_NOT_PRESENT.
- raise VectorError(vxlapi.XL_ERR_HW_NOT_PRESENT,
- "XL_ERR_HW_NOT_PRESENT",
- "xlGetChannelIndex")
+ if (
+ len(self.channels) == 1
+ and (_channel_index := kwargs.get("channel_index", None)) is not None
+ ):
+ # VectorBus._detect_available_configs() might return multiple
+ # devices with the same serial number, e.g. if a VN8900 is connected via both USB and Ethernet
+ # at the same time. If the VectorBus is instantiated with a config, that was returned from
+ # VectorBus._detect_available_configs(), then use the contained global channel_index
+ # to avoid any ambiguities.
+ channel_index = cast("int", _channel_index)
else:
- # Channel already given as global channel
- idx = channel
- mask = 1 << idx
- self.channel_masks[channel] = mask
- self.index_to_channel[idx] = channel
- self.mask |= mask
-
- permission_mask = vxlapi.XLaccess()
+ channel_index = self._find_global_channel_idx(
+ channel=channel,
+ serial=serial,
+ app_name=app_name,
+ channel_configs=channel_configs,
+ )
+ LOG.debug("Channel index %d found", channel)
+
+ channel_mask = 1 << channel_index
+ self.channel_masks[channel] = channel_mask
+ self.index_to_channel[channel_index] = channel
+ self.mask |= channel_mask
+
+ permission_mask = xlclass.XLaccess()
# Set mask to request channel init permission if needed
- if bitrate or fd:
+ if bitrate or fd or timing or self._listen_only:
permission_mask.value = self.mask
- if fd:
- vxlapi.xlOpenPort(self.port_handle, self._app_name, self.mask,
- permission_mask, rx_queue_size,
- vxlapi.XL_INTERFACE_VERSION_V4, vxlapi.XL_BUS_TYPE_CAN)
- else:
- vxlapi.xlOpenPort(self.port_handle, self._app_name, self.mask,
- permission_mask, rx_queue_size,
- vxlapi.XL_INTERFACE_VERSION, vxlapi.XL_BUS_TYPE_CAN)
+
+ interface_version = (
+ xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4
+ if is_fd
+ else xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION
+ )
+
+ self.port_handle = xlclass.XLportHandle(xldefine.XL_INVALID_PORTHANDLE)
+ self.xldriver.xlOpenPort(
+ self.port_handle,
+ self._app_name,
+ self.mask,
+ permission_mask,
+ rx_queue_size,
+ interface_version,
+ xldefine.XL_BusTypes.XL_BUS_TYPE_CAN,
+ )
+ self.permission_mask = permission_mask.value
+
LOG.debug(
- 'Open Port: PortHandle: %d, PermissionMask: 0x%X',
- self.port_handle.value, permission_mask.value)
-
- if permission_mask.value == self.mask:
- if fd:
- self.canFdConf = vxlapi.XLcanFdConf()
- if bitrate:
- self.canFdConf.arbitrationBitRate = ctypes.c_uint(bitrate)
- else:
- self.canFdConf.arbitrationBitRate = ctypes.c_uint(500000)
- self.canFdConf.sjwAbr = ctypes.c_uint(sjwAbr)
- self.canFdConf.tseg1Abr = ctypes.c_uint(tseg1Abr)
- self.canFdConf.tseg2Abr = ctypes.c_uint(tseg2Abr)
- if data_bitrate:
- self.canFdConf.dataBitRate = ctypes.c_uint(data_bitrate)
- else:
- self.canFdConf.dataBitRate = self.canFdConf.arbitrationBitRate
- self.canFdConf.sjwDbr = ctypes.c_uint(sjwDbr)
- self.canFdConf.tseg1Dbr = ctypes.c_uint(tseg1Dbr)
- self.canFdConf.tseg2Dbr = ctypes.c_uint(tseg2Dbr)
-
- vxlapi.xlCanFdSetConfiguration(self.port_handle, self.mask, self.canFdConf)
- LOG.info('SetFdConfig.: ABaudr.=%u, DBaudr.=%u', self.canFdConf.arbitrationBitRate, self.canFdConf.dataBitRate)
- LOG.info('SetFdConfig.: sjwAbr=%u, tseg1Abr=%u, tseg2Abr=%u', self.canFdConf.sjwAbr, self.canFdConf.tseg1Abr, self.canFdConf.tseg2Abr)
- LOG.info('SetFdConfig.: sjwDbr=%u, tseg1Dbr=%u, tseg2Dbr=%u', self.canFdConf.sjwDbr, self.canFdConf.tseg1Dbr, self.canFdConf.tseg2Dbr)
- else:
- if bitrate:
- vxlapi.xlCanSetChannelBitrate(self.port_handle, permission_mask, bitrate)
- LOG.info('SetChannelBitrate: baudr.=%u',bitrate)
- else:
- LOG.info('No init access!')
+ "Open Port: PortHandle: %d, ChannelMask: 0x%X, PermissionMask: 0x%X",
+ self.port_handle.value,
+ self.mask,
+ self.permission_mask,
+ )
+
+ assert_timing = (bitrate or timing) and not self.__testing
+
+ # set CAN settings
+ if isinstance(timing, BitTiming):
+ timing = check_or_adjust_timing_clock(timing, [16_000_000, 8_000_000])
+ self._set_bit_timing(channel_mask=self.mask, timing=timing)
+ if assert_timing:
+ self._check_can_settings(
+ channel_mask=self.mask,
+ bitrate=timing.bitrate,
+ sample_point=timing.sample_point,
+ )
+ elif isinstance(timing, BitTimingFd):
+ timing = check_or_adjust_timing_clock(timing, [80_000_000])
+ self._set_bit_timing_fd(channel_mask=self.mask, timing=timing)
+ if assert_timing:
+ self._check_can_settings(
+ channel_mask=self.mask,
+ bitrate=timing.nom_bitrate,
+ sample_point=timing.nom_sample_point,
+ fd=True,
+ data_bitrate=timing.data_bitrate,
+ data_sample_point=timing.data_sample_point,
+ )
+ elif fd:
+ timing = BitTimingFd.from_bitrate_and_segments(
+ f_clock=80_000_000,
+ nom_bitrate=bitrate or 500_000,
+ nom_tseg1=tseg1_abr,
+ nom_tseg2=tseg2_abr,
+ nom_sjw=sjw_abr,
+ data_bitrate=data_bitrate or bitrate or 500_000,
+ data_tseg1=tseg1_dbr,
+ data_tseg2=tseg2_dbr,
+ data_sjw=sjw_dbr,
+ )
+ self._set_bit_timing_fd(channel_mask=self.mask, timing=timing)
+ if assert_timing:
+ self._check_can_settings(
+ channel_mask=self.mask,
+ bitrate=timing.nom_bitrate,
+ sample_point=timing.nom_sample_point,
+ fd=True,
+ data_bitrate=timing.data_bitrate,
+ data_sample_point=timing.data_sample_point,
+ )
+ elif bitrate:
+ self._set_bitrate(channel_mask=self.mask, bitrate=bitrate)
+ if assert_timing:
+ self._check_can_settings(channel_mask=self.mask, bitrate=bitrate)
+
+ if self._listen_only:
+ self._set_output_mode(channel_mask=self.mask, listen_only=True)
# Enable/disable TX receipts
tx_receipts = 1 if receive_own_messages else 0
- vxlapi.xlCanSetChannelMode(self.port_handle, self.mask, tx_receipts, 0)
+ self.xldriver.xlCanSetChannelMode(self.port_handle, self.mask, tx_receipts, 0)
if HAS_EVENTS:
- self.event_handle = vxlapi.XLhandle()
- vxlapi.xlSetNotification(self.port_handle, self.event_handle, 1)
+ self.event_handle = xlclass.XLhandle()
+ self.xldriver.xlSetNotification(self.port_handle, self.event_handle, 1)
else:
- LOG.info('Install pywin32 to avoid polling')
+ LOG.info("Install pywin32 to avoid polling")
+ # Calculate time offset for absolute timestamps
+ offset = xlclass.XLuint64()
try:
- vxlapi.xlActivateChannel(self.port_handle, self.mask,
- vxlapi.XL_BUS_TYPE_CAN, 0)
- except VectorError:
- self.shutdown()
- raise
+ if time.get_clock_info("time").resolution > 1e-5:
+ ts, perfcounter = time_perfcounter_correlation()
+ try:
+ self.xldriver.xlGetSyncTime(self.port_handle, offset)
+ except VectorInitializationError:
+ self.xldriver.xlGetChannelTime(self.port_handle, self.mask, offset)
+ current_perfcounter = time.perf_counter()
+ now = ts + (current_perfcounter - perfcounter)
+ self._time_offset = now - offset.value * 1e-9
+ else:
+ try:
+ self.xldriver.xlGetSyncTime(self.port_handle, offset)
+ except VectorInitializationError:
+ self.xldriver.xlGetChannelTime(self.port_handle, self.mask, offset)
+ self._time_offset = time.time() - offset.value * 1e-9
- # Calculate time offset for absolute timestamps
- offset = vxlapi.XLuint64()
- vxlapi.xlGetSyncTime(self.port_handle, offset)
- self._time_offset = time.time() - offset.value * 1e-9
+ except VectorInitializationError:
+ self._time_offset = 0.0
self._is_filtered = False
- super(VectorBus, self).__init__(channel=channel, can_filters=can_filters,
- **config)
+ super().__init__(
+ channel=channel,
+ can_filters=can_filters,
+ **kwargs,
+ )
+
+ # activate channels after CAN filters were applied
+ try:
+ self.xldriver.xlActivateChannel(
+ self.port_handle, self.mask, xldefine.XL_BusTypes.XL_BUS_TYPE_CAN, 0
+ )
+ except VectorOperationError as error:
+ self.shutdown()
+ raise VectorInitializationError.from_generic(error) from None
+
+ @property
+ def fd(self) -> bool:
+ class_name = self.__class__.__name__
+ warnings.warn(
+ f"The {class_name}.fd property is deprecated and superseded by "
+ f"{class_name}.protocol. It is scheduled for removal in python-can version 5.0.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ return self._can_protocol is CanProtocol.CAN_FD
+
+ def _find_global_channel_idx(
+ self,
+ channel: int,
+ serial: int | None,
+ app_name: str | None,
+ channel_configs: list["VectorChannelConfig"],
+ ) -> int:
+ if serial is not None:
+ serial_found = False
+ for channel_config in channel_configs:
+ if channel_config.serial_number != serial:
+ continue
+
+ serial_found = True
+ if channel_config.hw_channel == channel:
+ return channel_config.channel_index
+
+ if not serial_found:
+ err_msg = f"No interface with serial {serial} found."
+ else:
+ err_msg = (
+ f"Channel {channel} not found on interface with serial {serial}."
+ )
+ raise CanInitializationError(
+ err_msg, error_code=xldefine.XL_Status.XL_ERR_HW_NOT_PRESENT
+ )
+
+ if app_name:
+ hw_type, hw_index, hw_channel = self.get_application_config(
+ app_name, channel
+ )
+ idx = cast(
+ "int", self.xldriver.xlGetChannelIndex(hw_type, hw_index, hw_channel)
+ )
+ if idx < 0:
+ # Undocumented behavior! See issue #353.
+ # If hardware is unavailable, this function returns -1.
+ # Raise an exception as if the driver
+ # would have signalled XL_ERR_HW_NOT_PRESENT.
+ raise VectorInitializationError(
+ xldefine.XL_Status.XL_ERR_HW_NOT_PRESENT,
+ xldefine.XL_Status.XL_ERR_HW_NOT_PRESENT.name,
+ "xlGetChannelIndex",
+ )
+ return idx
+
+ # check if channel is a valid global channel index
+ for channel_config in channel_configs:
+ if channel == channel_config.channel_index:
+ return channel
+
+ raise CanInitializationError(
+ f"Channel {channel} not found. The 'channel' parameter must be "
+ f"a valid global channel index if neither 'app_name' nor 'serial' were given.",
+ error_code=xldefine.XL_Status.XL_ERR_HW_NOT_PRESENT,
+ )
+
+ def _has_init_access(self, channel: int) -> bool:
+ return bool(self.permission_mask & self.channel_masks[channel])
+
+ def _read_bus_params(
+ self, channel_index: int, vcc_list: list["VectorChannelConfig"]
+ ) -> "VectorBusParams":
+ for vcc in vcc_list:
+ if vcc.channel_index == channel_index:
+ bus_params = vcc.bus_params
+ if bus_params is None:
+ # for CAN channels, this should never be `None`
+ raise ValueError("Invalid bus parameters.")
+ return bus_params
+
+ channel = self.index_to_channel[channel_index]
+ raise CanInitializationError(
+ f"Channel configuration for channel {channel} not found."
+ )
+
+ def _set_output_mode(self, channel_mask: int, listen_only: bool) -> None:
+ # set parameters for channels with init access
+ channel_mask = channel_mask & self.permission_mask
+
+ if channel_mask:
+ if listen_only:
+ self.xldriver.xlCanSetChannelOutput(
+ self.port_handle,
+ channel_mask,
+ xldefine.XL_OutputMode.XL_OUTPUT_MODE_SILENT,
+ )
+ else:
+ self.xldriver.xlCanSetChannelOutput(
+ self.port_handle,
+ channel_mask,
+ xldefine.XL_OutputMode.XL_OUTPUT_MODE_NORMAL,
+ )
+
+ LOG.info("xlCanSetChannelOutput: listen_only=%u", listen_only)
+ else:
+ LOG.warning("No channels with init access to set listen only mode")
- def _apply_filters(self, filters):
+ def _set_bitrate(self, channel_mask: int, bitrate: int) -> None:
+ # set parameters for channels with init access
+ channel_mask = channel_mask & self.permission_mask
+ if channel_mask:
+ self.xldriver.xlCanSetChannelBitrate(
+ self.port_handle,
+ channel_mask,
+ bitrate,
+ )
+ LOG.info("xlCanSetChannelBitrate: baudr.=%u", bitrate)
+
+ def _set_bit_timing(self, channel_mask: int, timing: BitTiming) -> None:
+ # set parameters for channels with init access
+ channel_mask = channel_mask & self.permission_mask
+ if channel_mask:
+ if timing.f_clock == 8_000_000:
+ self.xldriver.xlCanSetChannelParamsC200(
+ self.port_handle,
+ channel_mask,
+ timing.btr0,
+ timing.btr1,
+ )
+ LOG.info(
+ "xlCanSetChannelParamsC200: BTR0=%#02x, BTR1=%#02x",
+ timing.btr0,
+ timing.btr1,
+ )
+ elif timing.f_clock == 16_000_000:
+ chip_params = xlclass.XLchipParams()
+ chip_params.bitRate = timing.bitrate
+ chip_params.sjw = timing.sjw
+ chip_params.tseg1 = timing.tseg1
+ chip_params.tseg2 = timing.tseg2
+ chip_params.sam = timing.nof_samples
+ self.xldriver.xlCanSetChannelParams(
+ self.port_handle,
+ channel_mask,
+ chip_params,
+ )
+ LOG.info(
+ "xlCanSetChannelParams: baudr.=%u, sjwAbr=%u, tseg1Abr=%u, tseg2Abr=%u",
+ chip_params.bitRate,
+ chip_params.sjw,
+ chip_params.tseg1,
+ chip_params.tseg2,
+ )
+ else:
+ raise CanInitializationError(
+ f"timing.f_clock must be 8_000_000 or 16_000_000 (is {timing.f_clock})"
+ )
+
+ def _set_bit_timing_fd(
+ self,
+ channel_mask: int,
+ timing: BitTimingFd,
+ ) -> None:
+ # set parameters for channels with init access
+ channel_mask = channel_mask & self.permission_mask
+ if channel_mask:
+ canfd_conf = xlclass.XLcanFdConf()
+ canfd_conf.arbitrationBitRate = timing.nom_bitrate
+ canfd_conf.sjwAbr = timing.nom_sjw
+ canfd_conf.tseg1Abr = timing.nom_tseg1
+ canfd_conf.tseg2Abr = timing.nom_tseg2
+ canfd_conf.dataBitRate = timing.data_bitrate
+ canfd_conf.sjwDbr = timing.data_sjw
+ canfd_conf.tseg1Dbr = timing.data_tseg1
+ canfd_conf.tseg2Dbr = timing.data_tseg2
+ self.xldriver.xlCanFdSetConfiguration(
+ self.port_handle, channel_mask, canfd_conf
+ )
+ LOG.info(
+ "xlCanFdSetConfiguration.: ABaudr.=%u, DBaudr.=%u",
+ canfd_conf.arbitrationBitRate,
+ canfd_conf.dataBitRate,
+ )
+ LOG.info(
+ "xlCanFdSetConfiguration.: sjwAbr=%u, tseg1Abr=%u, tseg2Abr=%u",
+ canfd_conf.sjwAbr,
+ canfd_conf.tseg1Abr,
+ canfd_conf.tseg2Abr,
+ )
+ LOG.info(
+ "xlCanFdSetConfiguration.: sjwDbr=%u, tseg1Dbr=%u, tseg2Dbr=%u",
+ canfd_conf.sjwDbr,
+ canfd_conf.tseg1Dbr,
+ canfd_conf.tseg2Dbr,
+ )
+
+ def _check_can_settings(
+ self,
+ channel_mask: int,
+ bitrate: int,
+ sample_point: float | None = None,
+ fd: bool = False,
+ data_bitrate: int | None = None,
+ data_sample_point: float | None = None,
+ ) -> None:
+ """Compare requested CAN settings to active settings in driver."""
+ vcc_list = get_channel_configs()
+ for channel_index in _iterate_channel_index(channel_mask):
+ bus_params = self._read_bus_params(
+ channel_index=channel_index, vcc_list=vcc_list
+ )
+ # use bus_params.canfd even if fd==False, bus_params.can and bus_params.canfd are a C union
+ bus_params_data = bus_params.canfd
+ settings_acceptable = True
+
+ # check bus type
+ settings_acceptable &= (
+ bus_params.bus_type is xldefine.XL_BusTypes.XL_BUS_TYPE_CAN
+ )
+
+ # check CAN operation mode
+ # skip the check if can_op_mode is 0
+ # as it happens for cancaseXL, VN7600 and sometimes on other hardware (VN1640)
+ if bus_params_data.can_op_mode:
+ if fd:
+ settings_acceptable &= bool(
+ bus_params_data.can_op_mode
+ & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CANFD
+ )
+ else:
+ settings_acceptable &= bool(
+ bus_params_data.can_op_mode
+ & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CAN20
+ )
+
+ # check bitrates
+ if bitrate:
+ settings_acceptable &= (
+ abs(bus_params_data.bitrate - bitrate) < bitrate / 256
+ )
+ if fd and data_bitrate:
+ settings_acceptable &= (
+ abs(bus_params_data.data_bitrate - data_bitrate)
+ < data_bitrate / 256
+ )
+
+ # check sample points
+ if sample_point:
+ nom_sample_point_act = (
+ 100
+ * (1 + bus_params_data.tseg1_abr)
+ / (1 + bus_params_data.tseg1_abr + bus_params_data.tseg2_abr)
+ )
+ settings_acceptable &= (
+ abs(nom_sample_point_act - sample_point)
+ < 2.0 # 2 percent tolerance
+ )
+ if fd and data_sample_point:
+ data_sample_point_act = (
+ 100
+ * (1 + bus_params_data.tseg1_dbr)
+ / (1 + bus_params_data.tseg1_dbr + bus_params_data.tseg2_dbr)
+ )
+ settings_acceptable &= (
+ abs(data_sample_point_act - data_sample_point)
+ < 2.0 # 2 percent tolerance
+ )
+
+ if not settings_acceptable:
+ # The error message depends on the currently active CAN settings.
+ # If the active operation mode is CAN FD, show the active CAN FD timings,
+ # otherwise show CAN 2.0 timings.
+ if bool(
+ bus_params_data.can_op_mode
+ & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CANFD
+ ):
+ active_settings = bus_params.canfd._asdict()
+ active_settings["can_op_mode"] = "CAN FD"
+ else:
+ active_settings = bus_params.can._asdict()
+ active_settings["can_op_mode"] = "CAN 2.0"
+ settings_string = ", ".join(
+ [f"{key}: {val}" for key, val in active_settings.items()]
+ )
+ channel = self.index_to_channel[channel_index]
+ raise CanInitializationError(
+ f"The requested settings could not be set for channel {channel}. "
+ f"Another application might have set incompatible settings. "
+ f"These are the currently active settings: {settings_string}."
+ )
+
+ def _apply_filters(self, filters: CanFilters | None) -> None:
if filters:
# Only up to one filter per ID type allowed
- if len(filters) == 1 or (len(filters) == 2 and
- filters[0].get("extended") != filters[1].get("extended")):
+ if len(filters) == 1 or (
+ len(filters) == 2
+ and filters[0].get("extended") != filters[1].get("extended")
+ ):
try:
for can_filter in filters:
- vxlapi.xlCanSetChannelAcceptance(self.port_handle, self.mask,
- can_filter["can_id"], can_filter["can_mask"],
- vxlapi.XL_CAN_EXT if can_filter.get("extended") else vxlapi.XL_CAN_STD)
- except VectorError as exc:
- LOG.warning("Could not set filters: %s", exc)
+ self.xldriver.xlCanSetChannelAcceptance(
+ self.port_handle,
+ self.mask,
+ can_filter["can_id"],
+ can_filter["can_mask"],
+ (
+ xldefine.XL_AcceptanceFilter.XL_CAN_EXT
+ if can_filter.get("extended")
+ else xldefine.XL_AcceptanceFilter.XL_CAN_STD
+ ),
+ )
+ except VectorOperationError as exception:
+ LOG.warning("Could not set filters: %s", exception)
# go to fallback
else:
self._is_filtered = True
@@ -235,174 +686,608 @@ def _apply_filters(self, filters):
# fallback: reset filters
self._is_filtered = False
try:
- vxlapi.xlCanSetChannelAcceptance(self.port_handle, self.mask, 0x0, 0x0, vxlapi.XL_CAN_EXT)
- vxlapi.xlCanSetChannelAcceptance(self.port_handle, self.mask, 0x0, 0x0, vxlapi.XL_CAN_STD)
- except VectorError as exc:
+ self.xldriver.xlCanSetChannelAcceptance(
+ self.port_handle,
+ self.mask,
+ 0x0,
+ 0x0,
+ xldefine.XL_AcceptanceFilter.XL_CAN_EXT,
+ )
+ self.xldriver.xlCanSetChannelAcceptance(
+ self.port_handle,
+ self.mask,
+ 0x0,
+ 0x0,
+ xldefine.XL_AcceptanceFilter.XL_CAN_STD,
+ )
+ except VectorOperationError as exc:
LOG.warning("Could not reset filters: %s", exc)
- def _recv_internal(self, timeout):
+ def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]:
end_time = time.time() + timeout if timeout is not None else None
- if self.fd:
- event = vxlapi.XLcanRxEvent()
- else:
- event = vxlapi.XLevent()
- event_count = ctypes.c_uint()
-
while True:
- if self.fd:
- try:
- vxlapi.xlCanReceive(self.port_handle, event)
- except VectorError as exc:
- if exc.error_code != vxlapi.XL_ERR_QUEUE_IS_EMPTY:
- raise
+ try:
+ if self._can_protocol is CanProtocol.CAN_FD:
+ msg = self._recv_canfd()
else:
- if event.tag == vxlapi.XL_CAN_EV_TAG_RX_OK or event.tag == vxlapi.XL_CAN_EV_TAG_TX_OK:
- msg_id = event.tagData.canRxOkMsg.canId
- dlc = dlc2len(event.tagData.canRxOkMsg.dlc)
- flags = event.tagData.canRxOkMsg.msgFlags
- timestamp = event.timeStamp * 1e-9
- channel = self.index_to_channel.get(event.chanIndex)
- msg = Message(
- timestamp=timestamp + self._time_offset,
- arbitration_id=msg_id & 0x1FFFFFFF,
- extended_id=bool(msg_id & vxlapi.XL_CAN_EXT_MSG_ID),
- is_remote_frame=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_RTR),
- is_error_frame=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_EF),
- is_fd=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_EDL),
- error_state_indicator=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_ESI),
- bitrate_switch=bool(flags & vxlapi.XL_CAN_RXMSG_FLAG_BRS),
- dlc=dlc,
- data=event.tagData.canRxOkMsg.data[:dlc],
- channel=channel)
- return msg, self._is_filtered
+ msg = self._recv_can()
+
+ except VectorOperationError as exception:
+ if exception.error_code != xldefine.XL_Status.XL_ERR_QUEUE_IS_EMPTY:
+ raise
else:
- event_count.value = 1
- try:
- vxlapi.xlReceive(self.port_handle, event_count, event)
- except VectorError as exc:
- if exc.error_code != vxlapi.XL_ERR_QUEUE_IS_EMPTY:
- raise
- else:
- if event.tag == vxlapi.XL_RECEIVE_MSG:
- msg_id = event.tagData.msg.id
- dlc = event.tagData.msg.dlc
- flags = event.tagData.msg.flags
- timestamp = event.timeStamp * 1e-9
- channel = self.index_to_channel.get(event.chanIndex)
- msg = Message(
- timestamp=timestamp + self._time_offset,
- arbitration_id=msg_id & 0x1FFFFFFF,
- extended_id=bool(msg_id & vxlapi.XL_CAN_EXT_MSG_ID),
- is_remote_frame=bool(flags & vxlapi.XL_CAN_MSG_FLAG_REMOTE_FRAME),
- is_error_frame=bool(flags & vxlapi.XL_CAN_MSG_FLAG_ERROR_FRAME),
- is_fd=False,
- dlc=dlc,
- data=event.tagData.msg.data[:dlc],
- channel=channel)
- return msg, self._is_filtered
+ if msg:
+ return msg, self._is_filtered
+ # if no message was received, wait or return on timeout
if end_time is not None and time.time() > end_time:
return None, self._is_filtered
if HAS_EVENTS:
# Wait for receive event to occur
- if timeout is None:
+ if end_time is None:
time_left_ms = INFINITE
else:
time_left = end_time - time.time()
time_left_ms = max(0, int(time_left * 1000))
- WaitForSingleObject(self.event_handle.value, time_left_ms)
+ WaitForSingleObject(self.event_handle.value, time_left_ms) # type: ignore
else:
# Wait a short time until we try again
time.sleep(self.poll_interval)
- def send(self, msg, timeout=None):
+ def _recv_canfd(self) -> Message | None:
+ xl_can_rx_event = xlclass.XLcanRxEvent()
+ self.xldriver.xlCanReceive(self.port_handle, xl_can_rx_event)
+
+ if xl_can_rx_event.tag == xldefine.XL_CANFD_RX_EventTags.XL_CAN_EV_TAG_RX_OK:
+ is_rx = True
+ data_struct = xl_can_rx_event.tagData.canRxOkMsg
+ elif xl_can_rx_event.tag == xldefine.XL_CANFD_RX_EventTags.XL_CAN_EV_TAG_TX_OK:
+ is_rx = False
+ data_struct = xl_can_rx_event.tagData.canTxOkMsg
+ else:
+ self.handle_canfd_event(xl_can_rx_event)
+ return None
+
+ msg_id = data_struct.canId
+ dlc = dlc2len(data_struct.dlc)
+ flags = data_struct.msgFlags
+ timestamp = xl_can_rx_event.timeStamp * 1e-9
+ channel = self.index_to_channel.get(xl_can_rx_event.chanIndex)
+
+ return Message(
+ timestamp=timestamp + self._time_offset,
+ arbitration_id=msg_id & 0x1FFFFFFF,
+ is_extended_id=bool(
+ msg_id & xldefine.XL_MessageFlagsExtended.XL_CAN_EXT_MSG_ID
+ ),
+ is_remote_frame=bool(
+ flags & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_RTR
+ ),
+ is_error_frame=bool(
+ flags & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_EF
+ ),
+ is_fd=bool(flags & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_EDL),
+ bitrate_switch=bool(
+ flags & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_BRS
+ ),
+ error_state_indicator=bool(
+ flags & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_ESI
+ ),
+ is_rx=is_rx,
+ channel=channel,
+ dlc=dlc,
+ data=data_struct.data[:dlc],
+ )
+
+ def _recv_can(self) -> Message | None:
+ xl_event = xlclass.XLevent()
+ event_count = ctypes.c_uint(1)
+ self.xldriver.xlReceive(self.port_handle, event_count, xl_event)
+
+ if xl_event.tag != xldefine.XL_EventTags.XL_RECEIVE_MSG:
+ self.handle_can_event(xl_event)
+ return None
+
+ msg_id = xl_event.tagData.msg.id
+ dlc = xl_event.tagData.msg.dlc
+ flags = xl_event.tagData.msg.flags
+ timestamp = xl_event.timeStamp * 1e-9
+ channel = self.index_to_channel.get(xl_event.chanIndex)
+
+ return Message(
+ timestamp=timestamp + self._time_offset,
+ arbitration_id=msg_id & 0x1FFFFFFF,
+ is_extended_id=bool(
+ msg_id & xldefine.XL_MessageFlagsExtended.XL_CAN_EXT_MSG_ID
+ ),
+ is_remote_frame=bool(
+ flags & xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_REMOTE_FRAME
+ ),
+ is_error_frame=bool(
+ flags & xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_ERROR_FRAME
+ ),
+ is_rx=not bool(
+ flags & xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_TX_COMPLETED
+ ),
+ is_fd=False,
+ dlc=dlc,
+ data=xl_event.tagData.msg.data[:dlc],
+ channel=channel,
+ )
+
+ def handle_can_event(self, event: xlclass.XLevent) -> None:
+ """Handle non-message CAN events.
+
+ Method is called by :meth:`~can.interfaces.vector.VectorBus._recv_internal`
+ when `event.tag` is not `XL_RECEIVE_MSG`. Subclasses can implement this method.
+
+ :param event: XLevent that could have a `XL_CHIP_STATE`, `XL_TIMER` or `XL_SYNC_PULSE` tag.
+ """
+
+ def handle_canfd_event(self, event: xlclass.XLcanRxEvent) -> None:
+ """Handle non-message CAN FD events.
+
+ Method is called by :meth:`~can.interfaces.vector.VectorBus._recv_internal`
+ when `event.tag` is not `XL_CAN_EV_TAG_RX_OK` or `XL_CAN_EV_TAG_TX_OK`.
+ Subclasses can implement this method.
+
+ :param event: `XLcanRxEvent` that could have a `XL_CAN_EV_TAG_RX_ERROR`,
+ `XL_CAN_EV_TAG_TX_ERROR`, `XL_TIMER` or `XL_CAN_EV_TAG_CHIP_STATE` tag.
+ """
+
+ def send(self, msg: Message, timeout: float | None = None) -> None:
+ self._send_sequence([msg])
+
+ def _send_sequence(self, msgs: Sequence[Message]) -> int:
+ """Send messages and return number of successful transmissions."""
+ if self._can_protocol is CanProtocol.CAN_FD:
+ return self._send_can_fd_msg_sequence(msgs)
+ else:
+ return self._send_can_msg_sequence(msgs)
+
+ def _get_tx_channel_mask(self, msgs: Sequence[Message]) -> int:
+ if len(msgs) == 1:
+ return self.channel_masks.get(msgs[0].channel, self.mask) # type: ignore[arg-type]
+ else:
+ return self.mask
+
+ def _send_can_msg_sequence(self, msgs: Sequence[Message]) -> int:
+ """Send CAN messages and return number of successful transmissions."""
+ mask = self._get_tx_channel_mask(msgs)
+ message_count = ctypes.c_uint(len(msgs))
+
+ xl_event_array = (xlclass.XLevent * message_count.value)(
+ *map(self._build_xl_event, msgs)
+ )
+
+ self.xldriver.xlCanTransmit(
+ self.port_handle, mask, message_count, xl_event_array
+ )
+ return message_count.value
+
+ @staticmethod
+ def _build_xl_event(msg: Message) -> xlclass.XLevent:
msg_id = msg.arbitration_id
+ if msg.is_extended_id:
+ msg_id |= xldefine.XL_MessageFlagsExtended.XL_CAN_EXT_MSG_ID
- if msg.id_type:
- msg_id |= vxlapi.XL_CAN_EXT_MSG_ID
+ flags = 0
+ if msg.is_remote_frame:
+ flags |= xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_REMOTE_FRAME
+
+ xl_event = xlclass.XLevent()
+ xl_event.tag = xldefine.XL_EventTags.XL_TRANSMIT_MSG
+ xl_event.tagData.msg.id = msg_id
+ xl_event.tagData.msg.dlc = msg.dlc
+ xl_event.tagData.msg.flags = flags
+ xl_event.tagData.msg.data = tuple(msg.data)
+
+ return xl_event
+
+ def _send_can_fd_msg_sequence(self, msgs: Sequence[Message]) -> int:
+ """Send CAN FD messages and return number of successful transmissions."""
+ mask = self._get_tx_channel_mask(msgs)
+ message_count = len(msgs)
+
+ xl_can_tx_event_array = (xlclass.XLcanTxEvent * message_count)(
+ *map(self._build_xl_can_tx_event, msgs)
+ )
+
+ msg_count_sent = ctypes.c_uint(0)
+ self.xldriver.xlCanTransmitEx(
+ self.port_handle, mask, message_count, msg_count_sent, xl_can_tx_event_array
+ )
+ return msg_count_sent.value
+
+ @staticmethod
+ def _build_xl_can_tx_event(msg: Message) -> xlclass.XLcanTxEvent:
+ msg_id = msg.arbitration_id
+ if msg.is_extended_id:
+ msg_id |= xldefine.XL_MessageFlagsExtended.XL_CAN_EXT_MSG_ID
flags = 0
+ if msg.is_fd:
+ flags |= xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_EDL
+ if msg.bitrate_switch:
+ flags |= xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_BRS
+ if msg.is_remote_frame:
+ flags |= xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_RTR
+
+ xl_can_tx_event = xlclass.XLcanTxEvent()
+ xl_can_tx_event.tag = xldefine.XL_CANFD_TX_EventTags.XL_CAN_EV_TAG_TX_MSG
+ xl_can_tx_event.transId = 0xFFFF
+
+ xl_can_tx_event.tagData.canMsg.canId = msg_id
+ xl_can_tx_event.tagData.canMsg.msgFlags = flags
+ xl_can_tx_event.tagData.canMsg.dlc = len2dlc(msg.dlc)
+ xl_can_tx_event.tagData.canMsg.data = tuple(msg.data)
+
+ return xl_can_tx_event
- # If channel has been specified, try to send only to that one.
- # Otherwise send to all channels
- mask = self.channel_masks.get(msg.channel, self.mask)
-
- if self.fd:
- if msg.is_fd:
- flags |= vxlapi.XL_CAN_TXMSG_FLAG_EDL
- if msg.bitrate_switch:
- flags |= vxlapi.XL_CAN_TXMSG_FLAG_BRS
- if msg.is_remote_frame:
- flags |= vxlapi.XL_CAN_TXMSG_FLAG_RTR
-
- message_count = 1
- MsgCntSent = ctypes.c_uint(1)
-
- XLcanTxEvent = vxlapi.XLcanTxEvent()
- XLcanTxEvent.tag = vxlapi.XL_CAN_EV_TAG_TX_MSG
- XLcanTxEvent.transId = 0xffff
-
- XLcanTxEvent.tagData.canMsg.canId = msg_id
- XLcanTxEvent.tagData.canMsg.msgFlags = flags
- XLcanTxEvent.tagData.canMsg.dlc = len2dlc(msg.dlc)
- for idx, value in enumerate(msg.data):
- XLcanTxEvent.tagData.canMsg.data[idx] = value
- vxlapi.xlCanTransmitEx(self.port_handle, mask, message_count, MsgCntSent, XLcanTxEvent)
+ def flush_tx_buffer(self) -> None:
+ """
+ Flush the TX buffer of the bus.
+
+ Implementation does not use function ``xlCanFlushTransmitQueue`` of the XL driver, as it works only
+ for XL family devices.
+
+ .. warning::
+ Using this function will flush the queue and send a high voltage message (ID = 0, DLC = 0, no data).
+ """
+ if self._can_protocol is CanProtocol.CAN_FD:
+ xl_can_tx_event = xlclass.XLcanTxEvent()
+ xl_can_tx_event.tag = xldefine.XL_CANFD_TX_EventTags.XL_CAN_EV_TAG_TX_MSG
+ xl_can_tx_event.tagData.canMsg.msgFlags |= (
+ xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_HIGHPRIO
+ )
+ self.xldriver.xlCanTransmitEx(
+ self.port_handle,
+ self.mask,
+ ctypes.c_uint(1),
+ ctypes.c_uint(0),
+ xl_can_tx_event,
+ )
else:
- if msg.is_remote_frame:
- flags |= vxlapi.XL_CAN_MSG_FLAG_REMOTE_FRAME
-
- message_count = ctypes.c_uint(1)
-
- xl_event = vxlapi.XLevent()
- xl_event.tag = vxlapi.XL_TRANSMIT_MSG
-
- xl_event.tagData.msg.id = msg_id
- xl_event.tagData.msg.dlc = msg.dlc
- xl_event.tagData.msg.flags = flags
- for idx, value in enumerate(msg.data):
- xl_event.tagData.msg.data[idx] = value
- vxlapi.xlCanTransmit(self.port_handle, mask, message_count, xl_event)
-
-
- def flush_tx_buffer(self):
- vxlapi.xlCanFlushTransmitQueue(self.port_handle, self.mask)
-
- def shutdown(self):
- vxlapi.xlDeactivateChannel(self.port_handle, self.mask)
- vxlapi.xlClosePort(self.port_handle)
- vxlapi.xlCloseDriver()
-
- def reset(self):
- vxlapi.xlDeactivateChannel(self.port_handle, self.mask)
- vxlapi.xlActivateChannel(self.port_handle, self.mask,
- vxlapi.XL_BUS_TYPE_CAN, 0)
+ xl_event = xlclass.XLevent()
+ xl_event.tag = xldefine.XL_EventTags.XL_TRANSMIT_MSG
+ xl_event.tagData.msg.flags |= (
+ xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_OVERRUN
+ | xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_WAKEUP
+ )
+
+ self.xldriver.xlCanTransmit(
+ self.port_handle, self.mask, ctypes.c_uint(1), xl_event
+ )
+
+ def shutdown(self) -> None:
+ super().shutdown()
+
+ with contextlib.suppress(VectorError):
+ self.xldriver.xlDeactivateChannel(self.port_handle, self.mask)
+ self.xldriver.xlClosePort(self.port_handle)
+ self.xldriver.xlCloseDriver()
+
+ def reset(self) -> None:
+ self.xldriver.xlDeactivateChannel(self.port_handle, self.mask)
+ self.xldriver.xlActivateChannel(
+ self.port_handle, self.mask, xldefine.XL_BusTypes.XL_BUS_TYPE_CAN, 0
+ )
@staticmethod
- def _detect_available_configs():
- configs = []
+ def _detect_available_configs() -> Sequence["AutoDetectedVectorConfig"]:
+ configs: list[AutoDetectedVectorConfig] = []
channel_configs = get_channel_configs()
- LOG.info('Found %d channels', len(channel_configs))
+ LOG.info("Found %d channels", len(channel_configs))
for channel_config in channel_configs:
- LOG.info('Channel index %d: %s',
- channel_config.channelIndex,
- channel_config.name.decode('ascii'))
- configs.append({'interface': 'vector',
- 'app_name': None,
- 'channel': channel_config.channelIndex})
+ if (
+ not channel_config.channel_bus_capabilities
+ & xldefine.XL_BusCapabilities.XL_BUS_ACTIVE_CAP_CAN
+ ):
+ continue
+ LOG.info(
+ "Channel index %d: %s",
+ channel_config.channel_index,
+ channel_config.name,
+ )
+ configs.append(
+ {
+ "interface": "vector",
+ "channel": channel_config.hw_channel,
+ "serial": channel_config.serial_number,
+ "channel_index": channel_config.channel_index,
+ "hw_type": channel_config.hw_type,
+ "hw_index": channel_config.hw_index,
+ "hw_channel": channel_config.hw_channel,
+ "supports_fd": bool(
+ channel_config.channel_capabilities
+ & xldefine.XL_ChannelCapabilities.XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT
+ ),
+ "vector_channel_config": channel_config,
+ }
+ )
return configs
-def get_channel_configs():
- if vxlapi is None:
+ @staticmethod
+ def popup_vector_hw_configuration(wait_for_finish: int = 0) -> None:
+ """Open vector hardware configuration window.
+
+ :param wait_for_finish:
+ Time to wait for user input in milliseconds.
+ """
+ if xldriver is None:
+ raise CanInterfaceNotImplementedError("The Vector API has not been loaded")
+
+ xldriver.xlPopupHwConfig(ctypes.c_char_p(), ctypes.c_uint(wait_for_finish))
+
+ @staticmethod
+ def get_application_config(
+ app_name: str, app_channel: int
+ ) -> tuple[int | xldefine.XL_HardwareType, int, int]:
+ """Retrieve information for an application in Vector Hardware Configuration.
+
+ :param app_name:
+ The name of the application.
+ :param app_channel:
+ The channel of the application.
+ :return:
+ Returns a tuple of the hardware type, the hardware index and the
+ hardware channel.
+
+ :raises can.interfaces.vector.VectorInitializationError:
+ If the application name does not exist in the Vector hardware configuration.
+ """
+ if xldriver is None:
+ raise CanInterfaceNotImplementedError("The Vector API has not been loaded")
+
+ hw_type = ctypes.c_uint()
+ hw_index = ctypes.c_uint()
+ hw_channel = ctypes.c_uint()
+ _app_channel = ctypes.c_uint(app_channel)
+
+ try:
+ xldriver.xlGetApplConfig(
+ app_name.encode(),
+ _app_channel,
+ hw_type,
+ hw_index,
+ hw_channel,
+ xldefine.XL_BusTypes.XL_BUS_TYPE_CAN,
+ )
+ except VectorError as e:
+ raise VectorInitializationError(
+ error_code=e.error_code,
+ error_string=(
+ f"Vector HW Config: Channel '{app_channel}' of "
+ f"application '{app_name}' is not assigned to any interface"
+ ),
+ function="xlGetApplConfig",
+ ) from None
+ return _hw_type(hw_type.value), hw_index.value, hw_channel.value
+
+ @staticmethod
+ def set_application_config(
+ app_name: str,
+ app_channel: int,
+ hw_type: int | xldefine.XL_HardwareType,
+ hw_index: int,
+ hw_channel: int,
+ **kwargs: Any,
+ ) -> None:
+ """Modify the application settings in Vector Hardware Configuration.
+
+ This method can also be used with a channel config dictionary::
+
+ import can
+ from can.interfaces.vector import VectorBus
+
+ configs = can.detect_available_configs(interfaces=['vector'])
+ cfg = configs[0]
+ VectorBus.set_application_config(app_name="MyApplication", app_channel=0, **cfg)
+
+ :param app_name:
+ The name of the application. Creates a new application if it does
+ not exist yet.
+ :param app_channel:
+ The channel of the application.
+ :param hw_type:
+ The hardware type of the interface.
+ E.g XL_HardwareType.XL_HWTYPE_VIRTUAL
+ :param hw_index:
+ The index of the interface if multiple interface with the same
+ hardware type are present.
+ :param hw_channel:
+ The channel index of the interface.
+
+ :raises can.interfaces.vector.VectorInitializationError:
+ If the application name does not exist in the Vector hardware configuration.
+ """
+ if xldriver is None:
+ raise CanInterfaceNotImplementedError("The Vector API has not been loaded")
+
+ xldriver.xlSetApplConfig(
+ app_name.encode(),
+ app_channel,
+ hw_type,
+ hw_index,
+ hw_channel,
+ xldefine.XL_BusTypes.XL_BUS_TYPE_CAN,
+ )
+
+ def set_timer_rate(self, timer_rate_ms: int) -> None:
+ """Set the cyclic event rate of the port.
+
+ Once set, the port will generate a cyclic event with the tag XL_EventTags.XL_TIMER.
+ This timer can be used to keep an application alive. See XL Driver Library Description
+ for more information
+
+ :param timer_rate_ms:
+ The timer rate in ms. The minimal timer rate is 1ms, a value of 0 deactivates
+ the timer events.
+ """
+ timer_rate_10us = timer_rate_ms * 100
+ self.xldriver.xlSetTimerRate(self.port_handle, timer_rate_10us)
+
+
+class VectorCanParams(NamedTuple):
+ bitrate: int
+ sjw: int
+ tseg1: int
+ tseg2: int
+ sam: int
+ output_mode: xldefine.XL_OutputMode
+ can_op_mode: xldefine.XL_CANFD_BusParams_CanOpMode
+
+
+class VectorCanFdParams(NamedTuple):
+ bitrate: int
+ data_bitrate: int
+ sjw_abr: int
+ tseg1_abr: int
+ tseg2_abr: int
+ sam_abr: int
+ sjw_dbr: int
+ tseg1_dbr: int
+ tseg2_dbr: int
+ output_mode: xldefine.XL_OutputMode
+ can_op_mode: xldefine.XL_CANFD_BusParams_CanOpMode
+
+
+class VectorBusParams(NamedTuple):
+ bus_type: xldefine.XL_BusTypes
+ can: VectorCanParams
+ canfd: VectorCanFdParams
+
+
+class VectorChannelConfig(NamedTuple):
+ """NamedTuple which contains the channel properties from Vector XL API."""
+
+ name: str
+ hw_type: int | xldefine.XL_HardwareType
+ hw_index: int
+ hw_channel: int
+ channel_index: int
+ channel_mask: int
+ channel_capabilities: xldefine.XL_ChannelCapabilities
+ channel_bus_capabilities: xldefine.XL_BusCapabilities
+ is_on_bus: bool
+ connected_bus_type: xldefine.XL_BusTypes
+ bus_params: VectorBusParams | None
+ serial_number: int
+ article_number: int
+ transceiver_name: str
+
+
+class AutoDetectedVectorConfig(AutoDetectedConfig):
+ # data for use in VectorBus.__init__():
+ serial: int
+ channel_index: int
+ # data for use in VectorBus.set_application_config():
+ hw_type: int
+ hw_index: int
+ hw_channel: int
+ # additional information:
+ supports_fd: bool
+ vector_channel_config: VectorChannelConfig
+
+
+def _get_xl_driver_config() -> xlclass.XLdriverConfig:
+ if xldriver is None:
+ raise VectorError(
+ error_code=xldefine.XL_Status.XL_ERR_DLL_NOT_FOUND,
+ error_string="xldriver is unavailable",
+ function="_get_xl_driver_config",
+ )
+ driver_config = xlclass.XLdriverConfig()
+ xldriver.xlOpenDriver()
+ xldriver.xlGetDriverConfig(driver_config)
+ xldriver.xlCloseDriver()
+ return driver_config
+
+
+def _read_bus_params_from_c_struct(
+ bus_params: xlclass.XLbusParams,
+) -> VectorBusParams | None:
+ bus_type = xldefine.XL_BusTypes(bus_params.busType)
+ if bus_type is not xldefine.XL_BusTypes.XL_BUS_TYPE_CAN:
+ return None
+ return VectorBusParams(
+ bus_type=bus_type,
+ can=VectorCanParams(
+ bitrate=bus_params.data.can.bitRate,
+ sjw=bus_params.data.can.sjw,
+ tseg1=bus_params.data.can.tseg1,
+ tseg2=bus_params.data.can.tseg2,
+ sam=bus_params.data.can.sam,
+ output_mode=xldefine.XL_OutputMode(bus_params.data.can.outputMode),
+ can_op_mode=xldefine.XL_CANFD_BusParams_CanOpMode(
+ bus_params.data.can.canOpMode
+ ),
+ ),
+ canfd=VectorCanFdParams(
+ bitrate=bus_params.data.canFD.arbitrationBitRate,
+ data_bitrate=bus_params.data.canFD.dataBitRate,
+ sjw_abr=bus_params.data.canFD.sjwAbr,
+ tseg1_abr=bus_params.data.canFD.tseg1Abr,
+ tseg2_abr=bus_params.data.canFD.tseg2Abr,
+ sam_abr=bus_params.data.canFD.samAbr,
+ sjw_dbr=bus_params.data.canFD.sjwDbr,
+ tseg1_dbr=bus_params.data.canFD.tseg1Dbr,
+ tseg2_dbr=bus_params.data.canFD.tseg2Dbr,
+ output_mode=xldefine.XL_OutputMode(bus_params.data.canFD.outputMode),
+ can_op_mode=xldefine.XL_CANFD_BusParams_CanOpMode(
+ bus_params.data.canFD.canOpMode
+ ),
+ ),
+ )
+
+
+def get_channel_configs() -> list[VectorChannelConfig]:
+ """Read channel properties from Vector XL API."""
+ try:
+ driver_config = _get_xl_driver_config()
+ except VectorError:
return []
- driver_config = vxlapi.XLdriverConfig()
+
+ channel_list: list[VectorChannelConfig] = []
+ for i in range(driver_config.channelCount):
+ xlcc: xlclass.XLchannelConfig = driver_config.channel[i]
+ vcc = VectorChannelConfig(
+ name=xlcc.name.decode(),
+ hw_type=_hw_type(xlcc.hwType),
+ hw_index=xlcc.hwIndex,
+ hw_channel=xlcc.hwChannel,
+ channel_index=xlcc.channelIndex,
+ channel_mask=xlcc.channelMask,
+ channel_capabilities=xldefine.XL_ChannelCapabilities(
+ xlcc.channelCapabilities
+ ),
+ channel_bus_capabilities=xldefine.XL_BusCapabilities(
+ xlcc.channelBusCapabilities
+ ),
+ is_on_bus=bool(xlcc.isOnBus),
+ bus_params=_read_bus_params_from_c_struct(xlcc.busParams),
+ connected_bus_type=xldefine.XL_BusTypes(xlcc.connectedBusType),
+ serial_number=xlcc.serialNumber,
+ article_number=xlcc.articleNumber,
+ transceiver_name=xlcc.transceiverName.decode(),
+ )
+ channel_list.append(vcc)
+ return channel_list
+
+
+def _hw_type(hw_type: int) -> int | xldefine.XL_HardwareType:
try:
- vxlapi.xlOpenDriver()
- vxlapi.xlGetDriverConfig(driver_config)
- vxlapi.xlCloseDriver()
- except:
- pass
- return [driver_config.channel[i] for i in range(driver_config.channelCount)]
+ return xldefine.XL_HardwareType(hw_type)
+ except ValueError:
+ LOG.warning(f'Unknown XL_HardwareType value "{hw_type}"')
+ return hw_type
+
+
+def _iterate_channel_index(channel_mask: int) -> Iterator[int]:
+ """Iterate over channel indexes in channel mask."""
+ for channel_index, bit in enumerate(reversed(bin(channel_mask)[2:])):
+ if bit == "1":
+ yield channel_index
diff --git a/can/interfaces/vector/exceptions.py b/can/interfaces/vector/exceptions.py
index ab50ff60d..779365893 100644
--- a/can/interfaces/vector/exceptions.py
+++ b/can/interfaces/vector/exceptions.py
@@ -1,15 +1,32 @@
-#!/usr/bin/env python
-# coding: utf-8
+"""Exception/error declarations for the vector interface."""
-"""
-"""
+from typing import Any
-from can import CanError
+from can import CanError, CanInitializationError, CanOperationError
class VectorError(CanError):
+ def __init__(
+ self, error_code: int | None, error_string: str, function: str
+ ) -> None:
+ super().__init__(
+ message=f"{function} failed ({error_string})", error_code=error_code
+ )
- def __init__(self, error_code, error_string, function):
- self.error_code = error_code
- text = "%s failed (%s)" % (function, error_string)
- super(VectorError, self).__init__(text)
+ # keep reference to args for pickling
+ self._args = error_code, error_string, function
+
+ def __reduce__(self) -> str | tuple[Any, ...]:
+ return type(self), self._args, {}
+
+
+class VectorInitializationError(VectorError, CanInitializationError):
+ @staticmethod
+ def from_generic(error: VectorError) -> "VectorInitializationError":
+ return VectorInitializationError(*error._args)
+
+
+class VectorOperationError(VectorError, CanOperationError):
+ @staticmethod
+ def from_generic(error: VectorError) -> "VectorOperationError":
+ return VectorOperationError(*error._args)
diff --git a/can/interfaces/vector/vxlapi.py b/can/interfaces/vector/vxlapi.py
deleted file mode 100644
index 3ddf521fc..000000000
--- a/can/interfaces/vector/vxlapi.py
+++ /dev/null
@@ -1,341 +0,0 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-"""
-Ctypes wrapper module for Vector CAN Interface on win32/win64 systems.
-
-Authors: Julien Grave , Christian Sandberg
-"""
-
-# Import Standard Python Modules
-# ==============================
-import ctypes
-import logging
-import platform
-from .exceptions import VectorError
-
-# Define Module Logger
-# ====================
-LOG = logging.getLogger(__name__)
-
-# Vector XL API Definitions
-# =========================
-# Load Windows DLL
-DLL_NAME = 'vxlapi64' if platform.architecture()[0] == '64bit' else 'vxlapi'
-_xlapi_dll = ctypes.windll.LoadLibrary(DLL_NAME)
-
-XL_BUS_TYPE_CAN = 0x00000001
-
-XL_ERR_QUEUE_IS_EMPTY = 10
-XL_ERR_HW_NOT_PRESENT = 129
-
-XL_RECEIVE_MSG = 1
-XL_CAN_EV_TAG_RX_OK = 1024
-XL_CAN_EV_TAG_TX_OK = 1028
-XL_TRANSMIT_MSG = 10
-XL_CAN_EV_TAG_TX_MSG = 1088
-
-XL_CAN_EXT_MSG_ID = 0x80000000
-XL_CAN_MSG_FLAG_ERROR_FRAME = 0x01
-XL_CAN_MSG_FLAG_REMOTE_FRAME = 0x10
-XL_CAN_MSG_FLAG_TX_COMPLETED = 0x40
-
-XL_CAN_TXMSG_FLAG_EDL = 0x0001
-XL_CAN_TXMSG_FLAG_BRS = 0x0002
-XL_CAN_TXMSG_FLAG_RTR = 0x0010
-XL_CAN_RXMSG_FLAG_EDL = 0x0001
-XL_CAN_RXMSG_FLAG_BRS = 0x0002
-XL_CAN_RXMSG_FLAG_ESI = 0x0004
-XL_CAN_RXMSG_FLAG_RTR = 0x0010
-XL_CAN_RXMSG_FLAG_EF = 0x0200
-
-XL_CAN_STD = 1
-XL_CAN_EXT = 2
-
-XLuint64 = ctypes.c_int64
-XLaccess = XLuint64
-XLhandle = ctypes.c_void_p
-
-MAX_MSG_LEN = 8
-
-XL_CAN_MAX_DATA_LEN = 64
-
-# current version
-XL_INTERFACE_VERSION = 3
-XL_INTERFACE_VERSION_V4 = 4
-
-XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT = 0x80000000
-
-# structure for XL_RECEIVE_MSG, XL_TRANSMIT_MSG
-class s_xl_can_msg(ctypes.Structure):
- _fields_ = [('id', ctypes.c_ulong), ('flags', ctypes.c_ushort),
- ('dlc', ctypes.c_ushort), ('res1', XLuint64),
- ('data', ctypes.c_ubyte * MAX_MSG_LEN), ('res2', XLuint64)]
-
-
-
-class s_xl_can_ev_error(ctypes.Structure):
- _fields_ = [('errorCode', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte * 95)]
-
-class s_xl_can_ev_chip_state(ctypes.Structure):
- _fields_ = [('busStatus', ctypes.c_ubyte), ('txErrorCounter', ctypes.c_ubyte),
- ('rxErrorCounter', ctypes.c_ubyte),('reserved', ctypes.c_ubyte),
- ('reserved0', ctypes.c_uint)]
-
-class s_xl_can_ev_sync_pulse(ctypes.Structure):
- _fields_ = [('triggerSource', ctypes.c_uint), ('reserved', ctypes.c_uint),
- ('time', XLuint64)]
-
-# BASIC bus message structure
-class s_xl_tag_data(ctypes.Union):
- _fields_ = [('msg', s_xl_can_msg)]
-
-# CAN FD messages
-class s_xl_can_ev_rx_msg(ctypes.Structure):
- _fields_ = [('canId', ctypes.c_uint), ('msgFlags', ctypes.c_uint),
- ('crc', ctypes.c_uint), ('reserved1', ctypes.c_ubyte * 12),
- ('totalBitCnt', ctypes.c_ushort), ('dlc', ctypes.c_ubyte),
- ('reserved', ctypes.c_ubyte * 5), ('data', ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN)]
-
-class s_xl_can_ev_tx_request(ctypes.Structure):
- _fields_ = [('canId', ctypes.c_uint), ('msgFlags', ctypes.c_uint),
- ('dlc', ctypes.c_ubyte),('txAttemptConf', ctypes.c_ubyte),
- ('reserved', ctypes.c_ushort), ('data', ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN)]
-
-class s_xl_can_tx_msg(ctypes.Structure):
- _fields_ = [('canId', ctypes.c_uint), ('msgFlags', ctypes.c_uint),
- ('dlc', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte * 7),
- ('data', ctypes.c_ubyte * XL_CAN_MAX_DATA_LEN)]
-
-class s_rxTagData(ctypes.Union):
- _fields_ = [('canRxOkMsg', s_xl_can_ev_rx_msg), ('canTxOkMsg', s_xl_can_ev_rx_msg),
- ('canTxRequest', s_xl_can_ev_tx_request),('canError', s_xl_can_ev_error),
- ('canChipState', s_xl_can_ev_chip_state),('canSyncPulse', s_xl_can_ev_sync_pulse)]
-
-class s_txTagData(ctypes.Union):
- _fields_ = [('canMsg', s_xl_can_tx_msg)]
-
-# BASIC events
-XLeventTag = ctypes.c_ubyte
-
-class XLevent(ctypes.Structure):
- _fields_ = [('tag', XLeventTag), ('chanIndex', ctypes.c_ubyte),
- ('transId', ctypes.c_ushort), ('portHandle', ctypes.c_ushort),
- ('flags', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte),
- ('timeStamp', XLuint64), ('tagData', s_xl_tag_data)]
-
-# CAN FD events
-class XLcanRxEvent(ctypes.Structure):
- _fields_ = [('size',ctypes.c_int),('tag', ctypes.c_ushort),
- ('chanIndex', ctypes.c_ubyte),('reserved', ctypes.c_ubyte),
- ('userHandle', ctypes.c_int),('flagsChip', ctypes.c_ushort),
- ('reserved0', ctypes.c_ushort),('reserved1', XLuint64),
- ('timeStamp', XLuint64),('tagData', s_rxTagData)]
-
-class XLcanTxEvent(ctypes.Structure):
- _fields_ = [('tag', ctypes.c_ushort), ('transId', ctypes.c_ushort),
- ('chanIndex', ctypes.c_ubyte), ('reserved', ctypes.c_ubyte * 3),
- ('tagData', s_txTagData)]
-
-# CAN FD configuration structure
-class XLcanFdConf(ctypes.Structure):
- _fields_ = [('arbitrationBitRate', ctypes.c_uint), ('sjwAbr', ctypes.c_uint),
- ('tseg1Abr', ctypes.c_uint), ('tseg2Abr', ctypes.c_uint),
- ('dataBitRate', ctypes.c_uint), ('sjwDbr', ctypes.c_uint),
- ('tseg1Dbr', ctypes.c_uint), ('tseg2Dbr', ctypes.c_uint),
- ('reserved', ctypes.c_uint * 2)]
-
-class XLchannelConfig(ctypes.Structure):
- _pack_ = 1
- _fields_ = [
- ('name', ctypes.c_char * 32),
- ('hwType', ctypes.c_ubyte),
- ('hwIndex', ctypes.c_ubyte),
- ('hwChannel', ctypes.c_ubyte),
- ('transceiverType', ctypes.c_ushort),
- ('transceiverState', ctypes.c_ushort),
- ('configError', ctypes.c_ushort),
- ('channelIndex', ctypes.c_ubyte),
- ('channelMask', XLuint64),
- ('channelCapabilities', ctypes.c_uint),
- ('channelBusCapabilities', ctypes.c_uint),
- ('isOnBus', ctypes.c_ubyte),
- ('connectedBusType', ctypes.c_uint),
- ('busParams', ctypes.c_ubyte * 32),
- ('_doNotUse', ctypes.c_uint),
- ('driverVersion', ctypes.c_uint),
- ('interfaceVersion', ctypes.c_uint),
- ('raw_data', ctypes.c_uint * 10),
- ('serialNumber', ctypes.c_uint),
- ('articleNumber', ctypes.c_uint),
- ('transceiverName', ctypes.c_char * 32),
- ('specialCabFlags', ctypes.c_uint),
- ('dominantTimeout', ctypes.c_uint),
- ('dominantRecessiveDelay', ctypes.c_ubyte),
- ('recessiveDominantDelay', ctypes.c_ubyte),
- ('connectionInfo', ctypes.c_ubyte),
- ('currentlyAvailableTimestamps', ctypes.c_ubyte),
- ('minimalSupplyVoltage', ctypes.c_ushort),
- ('maximalSupplyVoltage', ctypes.c_ushort),
- ('maximalBaudrate', ctypes.c_uint),
- ('fpgaCoreCapabilities', ctypes.c_ubyte),
- ('specialDeviceStatus', ctypes.c_ubyte),
- ('channelBusActiveCapabilities', ctypes.c_ushort),
- ('breakOffset', ctypes.c_ushort),
- ('delimiterOffset', ctypes.c_ushort),
- ('reserved', ctypes.c_uint * 3)
- ]
-
-class XLdriverConfig(ctypes.Structure):
- _fields_ = [
- ('dllVersion', ctypes.c_uint),
- ('channelCount', ctypes.c_uint),
- ('reserved', ctypes.c_uint * 10),
- ('channel', XLchannelConfig * 64)
- ]
-
-# driver status
-XLstatus = ctypes.c_short
-
-# porthandle
-XL_INVALID_PORTHANDLE = (-1)
-XLportHandle = ctypes.c_long
-
-
-def check_status(result, function, arguments):
- if result > 0:
- raise VectorError(result, xlGetErrorString(result).decode(), function.__name__)
- return result
-
-
-xlGetDriverConfig = _xlapi_dll.xlGetDriverConfig
-xlGetDriverConfig.argtypes = [ctypes.POINTER(XLdriverConfig)]
-xlGetDriverConfig.restype = XLstatus
-xlGetDriverConfig.errcheck = check_status
-
-xlOpenDriver = _xlapi_dll.xlOpenDriver
-xlOpenDriver.argtypes = []
-xlOpenDriver.restype = XLstatus
-xlOpenDriver.errcheck = check_status
-
-xlCloseDriver = _xlapi_dll.xlCloseDriver
-xlCloseDriver.argtypes = []
-xlCloseDriver.restype = XLstatus
-xlCloseDriver.errcheck = check_status
-
-xlGetApplConfig = _xlapi_dll.xlGetApplConfig
-xlGetApplConfig.argtypes = [
- ctypes.c_char_p, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint),
- ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint), ctypes.c_uint
-]
-xlGetApplConfig.restype = XLstatus
-xlGetApplConfig.errcheck = check_status
-
-xlGetChannelIndex = _xlapi_dll.xlGetChannelIndex
-xlGetChannelIndex.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int]
-xlGetChannelIndex.restype = ctypes.c_int
-
-xlGetChannelMask = _xlapi_dll.xlGetChannelMask
-xlGetChannelMask.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int]
-xlGetChannelMask.restype = XLaccess
-
-xlOpenPort = _xlapi_dll.xlOpenPort
-xlOpenPort.argtypes = [
- ctypes.POINTER(XLportHandle), ctypes.c_char_p, XLaccess,
- ctypes.POINTER(XLaccess), ctypes.c_uint, ctypes.c_uint, ctypes.c_uint
-]
-xlOpenPort.restype = XLstatus
-xlOpenPort.errcheck = check_status
-
-xlGetSyncTime = _xlapi_dll.xlGetSyncTime
-xlGetSyncTime.argtypes = [XLportHandle, ctypes.POINTER(XLuint64)]
-xlGetSyncTime.restype = XLstatus
-xlGetSyncTime.errcheck = check_status
-
-xlClosePort = _xlapi_dll.xlClosePort
-xlClosePort.argtypes = [XLportHandle]
-xlClosePort.restype = XLstatus
-xlClosePort.errcheck = check_status
-
-xlSetNotification = _xlapi_dll.xlSetNotification
-xlSetNotification.argtypes = [XLportHandle, ctypes.POINTER(XLhandle),
- ctypes.c_int]
-xlSetNotification.restype = XLstatus
-xlSetNotification.errcheck = check_status
-
-xlCanSetChannelMode = _xlapi_dll.xlCanSetChannelMode
-xlCanSetChannelMode.argtypes = [
- XLportHandle, XLaccess, ctypes.c_int, ctypes.c_int
-]
-xlCanSetChannelMode.restype = XLstatus
-xlCanSetChannelMode.errcheck = check_status
-
-xlActivateChannel = _xlapi_dll.xlActivateChannel
-xlActivateChannel.argtypes = [
- XLportHandle, XLaccess, ctypes.c_uint, ctypes.c_uint
-]
-xlActivateChannel.restype = XLstatus
-xlActivateChannel.errcheck = check_status
-
-xlDeactivateChannel = _xlapi_dll.xlDeactivateChannel
-xlDeactivateChannel.argtypes = [XLportHandle, XLaccess]
-xlDeactivateChannel.restype = XLstatus
-xlDeactivateChannel.errcheck = check_status
-
-xlCanFdSetConfiguration = _xlapi_dll.xlCanFdSetConfiguration
-xlCanFdSetConfiguration.argtypes = [XLportHandle, XLaccess, ctypes.POINTER(XLcanFdConf)]
-xlCanFdSetConfiguration.restype = XLstatus
-xlCanFdSetConfiguration.errcheck = check_status
-
-xlReceive = _xlapi_dll.xlReceive
-xlReceive.argtypes = [
- XLportHandle, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(XLevent)
-]
-xlReceive.restype = XLstatus
-xlReceive.errcheck = check_status
-
-xlCanReceive = _xlapi_dll.xlCanReceive
-xlCanReceive.argtypes = [
- XLportHandle, ctypes.POINTER(XLcanRxEvent)
-]
-xlCanReceive.restype = XLstatus
-xlCanReceive.errcheck = check_status
-
-xlGetErrorString = _xlapi_dll.xlGetErrorString
-xlGetErrorString.argtypes = [XLstatus]
-xlGetErrorString.restype = ctypes.c_char_p
-
-xlCanSetChannelBitrate = _xlapi_dll.xlCanSetChannelBitrate
-xlCanSetChannelBitrate.argtypes = [XLportHandle, XLaccess, ctypes.c_ulong]
-xlCanSetChannelBitrate.restype = XLstatus
-xlCanSetChannelBitrate.errcheck = check_status
-
-xlCanTransmit = _xlapi_dll.xlCanTransmit
-xlCanTransmit.argtypes = [
- XLportHandle, XLaccess, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(XLevent)
-]
-xlCanTransmit.restype = XLstatus
-xlCanTransmit.errcheck = check_status
-
-xlCanTransmitEx = _xlapi_dll.xlCanTransmitEx
-xlCanTransmitEx.argtypes = [
- XLportHandle, XLaccess, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(XLcanTxEvent)
-]
-xlCanTransmitEx.restype = XLstatus
-xlCanTransmitEx.errcheck = check_status
-
-xlCanFlushTransmitQueue = _xlapi_dll.xlCanFlushTransmitQueue
-xlCanFlushTransmitQueue.argtypes = [XLportHandle, XLaccess]
-xlCanFlushTransmitQueue.restype = XLstatus
-xlCanFlushTransmitQueue.errcheck = check_status
-
-xlCanSetChannelAcceptance = _xlapi_dll.xlCanSetChannelAcceptance
-xlCanSetChannelAcceptance.argtypes = [
- XLportHandle, XLaccess, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_uint]
-xlCanSetChannelAcceptance.restype = XLstatus
-xlCanSetChannelAcceptance.errcheck = check_status
-
-xlCanResetAcceptance = _xlapi_dll.xlCanResetAcceptance
-xlCanResetAcceptance.argtypes = [XLportHandle, XLaccess, ctypes.c_uint]
-xlCanResetAcceptance.restype = XLstatus
-xlCanResetAcceptance.errcheck = check_status
diff --git a/can/interfaces/vector/xlclass.py b/can/interfaces/vector/xlclass.py
new file mode 100644
index 000000000..6441ad4e5
--- /dev/null
+++ b/can/interfaces/vector/xlclass.py
@@ -0,0 +1,293 @@
+"""
+Definition of data types and structures for vxlapi.
+
+Authors: Julien Grave , Christian Sandberg
+"""
+
+# Import Standard Python Modules
+# ==============================
+import ctypes
+
+# Vector XL API Definitions
+# =========================
+from . import xldefine
+
+XLuint64 = ctypes.c_int64
+XLaccess = XLuint64
+XLhandle = ctypes.c_void_p
+XLstatus = ctypes.c_short
+XLportHandle = ctypes.c_long
+XLeventTag = ctypes.c_ubyte
+XLstringType = ctypes.c_char_p
+
+
+# structure for XL_RECEIVE_MSG, XL_TRANSMIT_MSG
+class s_xl_can_msg(ctypes.Structure):
+ _fields_ = [
+ ("id", ctypes.c_ulong),
+ ("flags", ctypes.c_ushort),
+ ("dlc", ctypes.c_ushort),
+ ("res1", XLuint64),
+ ("data", ctypes.c_ubyte * xldefine.MAX_MSG_LEN),
+ ("res2", XLuint64),
+ ]
+
+
+class s_xl_can_ev_error(ctypes.Structure):
+ _fields_ = [("errorCode", ctypes.c_ubyte), ("reserved", ctypes.c_ubyte * 95)]
+
+
+class s_xl_chip_state(ctypes.Structure):
+ _fields_ = [
+ ("busStatus", ctypes.c_ubyte),
+ ("txErrorCounter", ctypes.c_ubyte),
+ ("rxErrorCounter", ctypes.c_ubyte),
+ ]
+
+
+class s_xl_sync_pulse(ctypes.Structure):
+ _fields_ = [
+ ("pulseCode", ctypes.c_ubyte),
+ ("time", XLuint64),
+ ]
+
+
+class s_xl_can_ev_chip_state(ctypes.Structure):
+ _fields_ = [
+ ("busStatus", ctypes.c_ubyte),
+ ("txErrorCounter", ctypes.c_ubyte),
+ ("rxErrorCounter", ctypes.c_ubyte),
+ ("reserved", ctypes.c_ubyte),
+ ("reserved0", ctypes.c_uint),
+ ]
+
+
+class s_xl_can_ev_sync_pulse(ctypes.Structure):
+ _fields_ = [
+ ("triggerSource", ctypes.c_uint),
+ ("reserved", ctypes.c_uint),
+ ("time", XLuint64),
+ ]
+
+
+# BASIC bus message structure
+class s_xl_tag_data(ctypes.Union):
+ _fields_ = [
+ ("msg", s_xl_can_msg),
+ ("chipState", s_xl_chip_state),
+ ("syncPulse", s_xl_sync_pulse),
+ ]
+
+
+# CAN FD messages
+class s_xl_can_ev_rx_msg(ctypes.Structure):
+ _fields_ = [
+ ("canId", ctypes.c_uint),
+ ("msgFlags", ctypes.c_uint),
+ ("crc", ctypes.c_uint),
+ ("reserved1", ctypes.c_ubyte * 12),
+ ("totalBitCnt", ctypes.c_ushort),
+ ("dlc", ctypes.c_ubyte),
+ ("reserved", ctypes.c_ubyte * 5),
+ ("data", ctypes.c_ubyte * xldefine.XL_CAN_MAX_DATA_LEN),
+ ]
+
+
+class s_xl_can_ev_tx_request(ctypes.Structure):
+ _fields_ = [
+ ("canId", ctypes.c_uint),
+ ("msgFlags", ctypes.c_uint),
+ ("dlc", ctypes.c_ubyte),
+ ("txAttemptConf", ctypes.c_ubyte),
+ ("reserved", ctypes.c_ushort),
+ ("data", ctypes.c_ubyte * xldefine.XL_CAN_MAX_DATA_LEN),
+ ]
+
+
+class s_xl_can_tx_msg(ctypes.Structure):
+ _fields_ = [
+ ("canId", ctypes.c_uint),
+ ("msgFlags", ctypes.c_uint),
+ ("dlc", ctypes.c_ubyte),
+ ("reserved", ctypes.c_ubyte * 7),
+ ("data", ctypes.c_ubyte * xldefine.XL_CAN_MAX_DATA_LEN),
+ ]
+
+
+class s_rxTagData(ctypes.Union):
+ _fields_ = [
+ ("canRxOkMsg", s_xl_can_ev_rx_msg),
+ ("canTxOkMsg", s_xl_can_ev_rx_msg),
+ ("canTxRequest", s_xl_can_ev_tx_request),
+ ("canError", s_xl_can_ev_error),
+ ("canChipState", s_xl_can_ev_chip_state),
+ ("canSyncPulse", s_xl_can_ev_sync_pulse),
+ ]
+
+
+class s_txTagData(ctypes.Union):
+ _fields_ = [("canMsg", s_xl_can_tx_msg)]
+
+
+class XLevent(ctypes.Structure):
+ _fields_ = [
+ ("tag", XLeventTag),
+ ("chanIndex", ctypes.c_ubyte),
+ ("transId", ctypes.c_ushort),
+ ("portHandle", ctypes.c_ushort),
+ ("flags", ctypes.c_ubyte),
+ ("reserved", ctypes.c_ubyte),
+ ("timeStamp", XLuint64),
+ ("tagData", s_xl_tag_data),
+ ]
+
+
+# CAN FD events
+class XLcanRxEvent(ctypes.Structure):
+ _fields_ = [
+ ("size", ctypes.c_int),
+ ("tag", ctypes.c_ushort),
+ ("chanIndex", ctypes.c_ubyte),
+ ("reserved", ctypes.c_ubyte),
+ ("userHandle", ctypes.c_int),
+ ("flagsChip", ctypes.c_ushort),
+ ("reserved0", ctypes.c_ushort),
+ ("reserved1", XLuint64),
+ ("timeStamp", XLuint64),
+ ("tagData", s_rxTagData),
+ ]
+
+
+class XLcanTxEvent(ctypes.Structure):
+ _fields_ = [
+ ("tag", ctypes.c_ushort),
+ ("transId", ctypes.c_ushort),
+ ("chanIndex", ctypes.c_ubyte),
+ ("reserved", ctypes.c_ubyte * 3),
+ ("tagData", s_txTagData),
+ ]
+
+
+# CAN configuration structure
+class XLchipParams(ctypes.Structure):
+ _fields_ = [
+ ("bitRate", ctypes.c_ulong),
+ ("sjw", ctypes.c_ubyte),
+ ("tseg1", ctypes.c_ubyte),
+ ("tseg2", ctypes.c_ubyte),
+ ("sam", ctypes.c_ubyte),
+ ]
+
+
+# CAN FD configuration structure
+class XLcanFdConf(ctypes.Structure):
+ _fields_ = [
+ ("arbitrationBitRate", ctypes.c_uint),
+ ("sjwAbr", ctypes.c_uint),
+ ("tseg1Abr", ctypes.c_uint),
+ ("tseg2Abr", ctypes.c_uint),
+ ("dataBitRate", ctypes.c_uint),
+ ("sjwDbr", ctypes.c_uint),
+ ("tseg1Dbr", ctypes.c_uint),
+ ("tseg2Dbr", ctypes.c_uint),
+ ("reserved", ctypes.c_ubyte),
+ ("options", ctypes.c_ubyte),
+ ("reserved1", ctypes.c_ubyte * 2),
+ ("reserved2", ctypes.c_ubyte),
+ ]
+
+
+# channel configuration structures
+class s_xl_bus_params_data_can(ctypes.Structure):
+ _fields_ = [
+ ("bitRate", ctypes.c_uint),
+ ("sjw", ctypes.c_ubyte),
+ ("tseg1", ctypes.c_ubyte),
+ ("tseg2", ctypes.c_ubyte),
+ ("sam", ctypes.c_ubyte),
+ ("outputMode", ctypes.c_ubyte),
+ ("reserved", ctypes.c_ubyte * 7),
+ ("canOpMode", ctypes.c_ubyte),
+ ]
+
+
+class s_xl_bus_params_data_canfd(ctypes.Structure):
+ _fields_ = [
+ ("arbitrationBitRate", ctypes.c_uint),
+ ("sjwAbr", ctypes.c_ubyte),
+ ("tseg1Abr", ctypes.c_ubyte),
+ ("tseg2Abr", ctypes.c_ubyte),
+ ("samAbr", ctypes.c_ubyte),
+ ("outputMode", ctypes.c_ubyte),
+ ("sjwDbr", ctypes.c_ubyte),
+ ("tseg1Dbr", ctypes.c_ubyte),
+ ("tseg2Dbr", ctypes.c_ubyte),
+ ("dataBitRate", ctypes.c_uint),
+ ("canOpMode", ctypes.c_ubyte),
+ ]
+
+
+class s_xl_bus_params_data(ctypes.Union):
+ _fields_ = [
+ ("can", s_xl_bus_params_data_can),
+ ("canFD", s_xl_bus_params_data_canfd),
+ ("most", ctypes.c_ubyte * 12),
+ ("flexray", ctypes.c_ubyte * 12),
+ ("ethernet", ctypes.c_ubyte * 12),
+ ("a429", ctypes.c_ubyte * 28),
+ ]
+
+
+class XLbusParams(ctypes.Structure):
+ _fields_ = [("busType", ctypes.c_uint), ("data", s_xl_bus_params_data)]
+
+
+class XLchannelConfig(ctypes.Structure):
+ _pack_ = 1
+ _fields_ = [
+ ("name", ctypes.c_char * 32),
+ ("hwType", ctypes.c_ubyte),
+ ("hwIndex", ctypes.c_ubyte),
+ ("hwChannel", ctypes.c_ubyte),
+ ("transceiverType", ctypes.c_ushort),
+ ("transceiverState", ctypes.c_ushort),
+ ("configError", ctypes.c_ushort),
+ ("channelIndex", ctypes.c_ubyte),
+ ("channelMask", XLuint64),
+ ("channelCapabilities", ctypes.c_uint),
+ ("channelBusCapabilities", ctypes.c_uint),
+ ("isOnBus", ctypes.c_ubyte),
+ ("connectedBusType", ctypes.c_uint),
+ ("busParams", XLbusParams),
+ ("_doNotUse", ctypes.c_uint),
+ ("driverVersion", ctypes.c_uint),
+ ("interfaceVersion", ctypes.c_uint),
+ ("raw_data", ctypes.c_uint * 10),
+ ("serialNumber", ctypes.c_uint),
+ ("articleNumber", ctypes.c_uint),
+ ("transceiverName", ctypes.c_char * 32),
+ ("specialCabFlags", ctypes.c_uint),
+ ("dominantTimeout", ctypes.c_uint),
+ ("dominantRecessiveDelay", ctypes.c_ubyte),
+ ("recessiveDominantDelay", ctypes.c_ubyte),
+ ("connectionInfo", ctypes.c_ubyte),
+ ("currentlyAvailableTimestamps", ctypes.c_ubyte),
+ ("minimalSupplyVoltage", ctypes.c_ushort),
+ ("maximalSupplyVoltage", ctypes.c_ushort),
+ ("maximalBaudrate", ctypes.c_uint),
+ ("fpgaCoreCapabilities", ctypes.c_ubyte),
+ ("specialDeviceStatus", ctypes.c_ubyte),
+ ("channelBusActiveCapabilities", ctypes.c_ushort),
+ ("breakOffset", ctypes.c_ushort),
+ ("delimiterOffset", ctypes.c_ushort),
+ ("reserved", ctypes.c_uint * 3),
+ ]
+
+
+class XLdriverConfig(ctypes.Structure):
+ _fields_ = [
+ ("dllVersion", ctypes.c_uint),
+ ("channelCount", ctypes.c_uint),
+ ("reserved", ctypes.c_uint * 10),
+ ("channel", XLchannelConfig * 64),
+ ]
diff --git a/can/interfaces/vector/xldefine.py b/can/interfaces/vector/xldefine.py
new file mode 100644
index 000000000..ebc0971c1
--- /dev/null
+++ b/can/interfaces/vector/xldefine.py
@@ -0,0 +1,330 @@
+"""
+Definition of constants for vxlapi.
+"""
+
+# Import Python Modules
+# ==============================
+from enum import IntEnum, IntFlag
+
+MAX_MSG_LEN = 8
+XL_CAN_MAX_DATA_LEN = 64
+XL_INVALID_PORTHANDLE = -1
+
+
+class XL_AC_Flags(IntEnum):
+ XL_ACTIVATE_NONE = 0
+ XL_ACTIVATE_RESET_CLOCK = 8
+
+
+class XL_AcceptanceFilter(IntEnum):
+ XL_CAN_STD = 1
+ XL_CAN_EXT = 2
+
+
+class XL_BusCapabilities(IntFlag):
+ XL_BUS_COMPATIBLE_CAN = 1
+ XL_BUS_ACTIVE_CAP_CAN = 1 << 16
+ XL_BUS_COMPATIBLE_LIN = 2
+ XL_BUS_ACTIVE_CAP_LIN = 2 << 16
+ XL_BUS_COMPATIBLE_FLEXRAY = 4
+ XL_BUS_ACTIVE_CAP_FLEXRAY = 4 << 16
+ XL_BUS_COMPATIBLE_MOST = 16
+ XL_BUS_ACTIVE_CAP_MOST = 16 << 16
+ XL_BUS_COMPATIBLE_DAIO = 64
+ XL_BUS_ACTIVE_CAP_DAIO = 64 << 16
+ XL_BUS_COMPATIBLE_J1708 = 256
+ XL_BUS_ACTIVE_CAP_J1708 = 256 << 16
+ XL_BUS_COMPATIBLE_KLINE = 2048
+ XL_BUS_ACTIVE_CAP_KLINE = 2048 << 16
+ XL_BUS_COMPATIBLE_ETHERNET = 4096
+ XL_BUS_ACTIVE_CAP_ETHERNET = 4096 << 16
+ XL_BUS_COMPATIBLE_A429 = 8192
+ XL_BUS_ACTIVE_CAP_A429 = 8192 << 16
+
+
+class XL_BusStatus(IntEnum):
+ XL_CHIPSTAT_BUSOFF = 1
+ XL_CHIPSTAT_ERROR_PASSIVE = 2
+ XL_CHIPSTAT_ERROR_WARNING = 4
+ XL_CHIPSTAT_ERROR_ACTIVE = 8
+
+
+class XL_BusTypes(IntFlag):
+ XL_BUS_TYPE_NONE = 0 # =0x00000000
+ XL_BUS_TYPE_CAN = 1 # =0x00000001
+ XL_BUS_TYPE_LIN = 2 # =0x00000002
+ XL_BUS_TYPE_FLEXRAY = 4 # =0x00000004
+ XL_BUS_TYPE_AFDX = 8 # =0x00000008
+ XL_BUS_TYPE_MOST = 16 # =0x00000010
+ XL_BUS_TYPE_DAIO = 64 # =0x00000040
+ XL_BUS_TYPE_J1708 = 256 # =0x00000100
+ XL_BUS_TYPE_KLINE = 2048 # =0x00000800
+ XL_BUS_TYPE_ETHERNET = 4096 # =0x00001000
+ XL_BUS_TYPE_A429 = 8192 # =0x00002000
+
+
+class XL_CANFD_BusParams_CanOpMode(IntFlag):
+ XL_BUS_PARAMS_CANOPMODE_CAN20 = 1
+ XL_BUS_PARAMS_CANOPMODE_CANFD = 2
+ XL_BUS_PARAMS_CANOPMODE_CANFD_NO_ISO = 8
+
+
+class XL_CANFD_ConfigOptions(IntEnum):
+ CANFD_CONFOPT_NO_ISO = 8
+
+
+class XL_CANFD_RX_EV_ERROR_errorCode(IntEnum):
+ XL_CAN_ERRC_BIT_ERROR = 1
+ XL_CAN_ERRC_FORM_ERROR = 2
+ XL_CAN_ERRC_STUFF_ERROR = 3
+ XL_CAN_ERRC_OTHER_ERROR = 4
+ XL_CAN_ERRC_CRC_ERROR = 5
+ XL_CAN_ERRC_ACK_ERROR = 6
+ XL_CAN_ERRC_NACK_ERROR = 7
+ XL_CAN_ERRC_OVLD_ERROR = 8
+ XL_CAN_ERRC_EXCPT_ERROR = 9
+
+
+class XL_CANFD_RX_EventTags(IntEnum):
+ XL_SYNC_PULSE = 11
+ XL_CAN_EV_TAG_RX_OK = 1024
+ XL_CAN_EV_TAG_RX_ERROR = 1025
+ XL_CAN_EV_TAG_TX_ERROR = 1026
+ XL_CAN_EV_TAG_TX_REQUEST = 1027
+ XL_CAN_EV_TAG_TX_OK = 1028
+ XL_CAN_EV_TAG_CHIP_STATE = 1033
+
+
+class XL_CANFD_RX_MessageFlags(IntFlag):
+ XL_CAN_RXMSG_FLAG_NONE = 0
+ XL_CAN_RXMSG_FLAG_EDL = 1
+ XL_CAN_RXMSG_FLAG_BRS = 2
+ XL_CAN_RXMSG_FLAG_ESI = 4
+ XL_CAN_RXMSG_FLAG_RTR = 16
+ XL_CAN_RXMSG_FLAG_EF = 512
+ XL_CAN_RXMSG_FLAG_ARB_LOST = 1024
+ XL_CAN_RXMSG_FLAG_WAKEUP = 8192
+ XL_CAN_RXMSG_FLAG_TE = 16384
+
+
+class XL_CANFD_TX_EventTags(IntEnum):
+ XL_CAN_EV_TAG_TX_MSG = 1088 # =0x0440
+ XL_CAN_EV_TAG_TX_ERRFR = 1089 # =0x0441
+
+
+class XL_CANFD_TX_MessageFlags(IntFlag):
+ XL_CAN_TXMSG_FLAG_NONE = 0
+ XL_CAN_TXMSG_FLAG_EDL = 1
+ XL_CAN_TXMSG_FLAG_BRS = 2
+ XL_CAN_TXMSG_FLAG_RTR = 16
+ XL_CAN_TXMSG_FLAG_HIGHPRIO = 128
+ XL_CAN_TXMSG_FLAG_WAKEUP = 512
+
+
+class XL_ChannelCapabilities(IntFlag):
+ XL_CHANNEL_FLAG_TIME_SYNC_RUNNING = 1
+ XL_CHANNEL_FLAG_NO_HWSYNC_SUPPORT = 1024
+ XL_CHANNEL_FLAG_SPDIF_CAPABLE = 16384
+ XL_CHANNEL_FLAG_CANFD_BOSCH_SUPPORT = 536870912
+ XL_CHANNEL_FLAG_CMACTLICENSE_SUPPORT = 1073741824
+ XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT = 2147483648
+
+
+class XL_EventFlags(IntEnum):
+ XL_EVENT_FLAG_OVERRUN = 1
+
+
+class XL_EventTags(IntEnum):
+ XL_NO_COMMAND = 0
+ XL_RECEIVE_MSG = 1
+ XL_CHIP_STATE = 4
+ XL_TRANSCEIVER = 6
+ XL_TIMER = 8
+ XL_TRANSMIT_MSG = 10
+ XL_SYNC_PULSE = 11
+ XL_APPLICATION_NOTIFICATION = 15
+
+
+class XL_InterfaceVersion(IntEnum):
+ XL_INTERFACE_VERSION_V2 = 2
+ XL_INTERFACE_VERSION_V3 = 3
+ XL_INTERFACE_VERSION = XL_INTERFACE_VERSION_V3
+ XL_INTERFACE_VERSION_V4 = 4
+
+
+class XL_MessageFlags(IntEnum):
+ XL_CAN_MSG_FLAG_NONE = 0
+ XL_CAN_MSG_FLAG_ERROR_FRAME = 1
+ XL_CAN_MSG_FLAG_OVERRUN = 2
+ XL_CAN_MSG_FLAG_NERR = 4
+ XL_CAN_MSG_FLAG_WAKEUP = 8
+ XL_CAN_MSG_FLAG_REMOTE_FRAME = 16
+ XL_CAN_MSG_FLAG_RESERVED_1 = 32
+ XL_CAN_MSG_FLAG_TX_COMPLETED = 64
+ XL_CAN_MSG_FLAG_TX_REQUEST = 128
+ XL_CAN_MSG_FLAG_SRR_BIT_DOM = 512
+
+
+class XL_MessageFlagsExtended(IntEnum):
+ XL_CAN_EXT_MSG_ID = 2147483648
+
+
+class XL_OutputMode(IntEnum):
+ XL_OUTPUT_MODE_SILENT = 0
+ XL_OUTPUT_MODE_NORMAL = 1
+ XL_OUTPUT_MODE_TX_OFF = 2
+ XL_OUTPUT_MODE_SJA_1000_SILENT = 3
+
+
+class XL_Sizes(IntEnum):
+ XL_MAX_LENGTH = 31
+ XL_MAX_APPNAME = 32
+ XL_MAX_NAME_LENGTH = 48
+ XLEVENT_SIZE = 48
+ XL_CONFIG_MAX_CHANNELS = 64
+ XL_APPLCONFIG_MAX_CHANNELS = 256
+
+
+class XL_Status(IntEnum):
+ XL_SUCCESS = 0 # =0x0000
+ XL_PENDING = 1 # =0x0001
+ XL_ERR_QUEUE_IS_EMPTY = 10 # =0x000A
+ XL_ERR_QUEUE_IS_FULL = 11 # =0x000B
+ XL_ERR_TX_NOT_POSSIBLE = 12 # =0x000C
+ XL_ERR_NO_LICENSE = 14 # =0x000E
+ XL_ERR_WRONG_PARAMETER = 101 # =0x0065
+ XL_ERR_TWICE_REGISTER = 110 # =0x006E
+ XL_ERR_INVALID_CHAN_INDEX = 111 # =0x006F
+ XL_ERR_INVALID_ACCESS = 112 # =0x0070
+ XL_ERR_PORT_IS_OFFLINE = 113 # =0x0071
+ XL_ERR_CHAN_IS_ONLINE = 116 # =0x0074
+ XL_ERR_NOT_IMPLEMENTED = 117 # =0x0075
+ XL_ERR_INVALID_PORT = 118 # =0x0076
+ XL_ERR_HW_NOT_READY = 120 # =0x0078
+ XL_ERR_CMD_TIMEOUT = 121 # =0x0079
+ XL_ERR_CMD_HANDLING = 122 # = 0x007A
+ XL_ERR_HW_NOT_PRESENT = 129 # =0x0081
+ XL_ERR_NOTIFY_ALREADY_ACTIVE = 131 # =0x0083
+ XL_ERR_INVALID_TAG = 132 # = 0x0084
+ XL_ERR_INVALID_RESERVED_FLD = 133 # = 0x0085
+ XL_ERR_INVALID_SIZE = 134 # = 0x0086
+ XL_ERR_INSUFFICIENT_BUFFER = 135 # = 0x0087
+ XL_ERR_ERROR_CRC = 136 # = 0x0088
+ XL_ERR_BAD_EXE_FORMAT = 137 # = 0x0089
+ XL_ERR_NO_SYSTEM_RESOURCES = 138 # = 0x008A
+ XL_ERR_NOT_FOUND = 139 # = 0x008B
+ XL_ERR_INVALID_ADDRESS = 140 # = 0x008C
+ XL_ERR_REQ_NOT_ACCEP = 141 # = 0x008D
+ XL_ERR_INVALID_LEVEL = 142 # = 0x008E
+ XL_ERR_NO_DATA_DETECTED = 143 # = 0x008F
+ XL_ERR_INTERNAL_ERROR = 144 # = 0x0090
+ XL_ERR_UNEXP_NET_ERR = 145 # = 0x0091
+ XL_ERR_INVALID_USER_BUFFER = 146 # = 0x0092
+ XL_ERR_INVALID_PORT_ACCESS_TYPE = 147 # = 0x0093
+ XL_ERR_NO_RESOURCES = 152 # =0x0098
+ XL_ERR_WRONG_CHIP_TYPE = 153 # =0x0099
+ XL_ERR_WRONG_COMMAND = 154 # =0x009A
+ XL_ERR_INVALID_HANDLE = 155 # =0x009B
+ XL_ERR_RESERVED_NOT_ZERO = 157 # =0x009D
+ XL_ERR_INIT_ACCESS_MISSING = 158 # =0x009E
+ XL_ERR_WRONG_VERSION = 160 # = 0x00A0
+ XL_ERR_CANNOT_OPEN_DRIVER = 201 # =0x00C9
+ XL_ERR_WRONG_BUS_TYPE = 202 # =0x00CA
+ XL_ERR_DLL_NOT_FOUND = 203 # =0x00CB
+ XL_ERR_INVALID_CHANNEL_MASK = 204 # =0x00CC
+ XL_ERR_NOT_SUPPORTED = 205 # =0x00CD
+ XL_ERR_CONNECTION_BROKEN = 210 # =0x00D2
+ XL_ERR_CONNECTION_CLOSED = 211 # =0x00D3
+ XL_ERR_INVALID_STREAM_NAME = 212 # =0x00D4
+ XL_ERR_CONNECTION_FAILED = 213 # =0x00D5
+ XL_ERR_STREAM_NOT_FOUND = 214 # =0x00D6
+ XL_ERR_STREAM_NOT_CONNECTED = 215 # =0x00D7
+ XL_ERR_QUEUE_OVERRUN = 216 # =0x00D8
+ XL_ERROR = 255 # =0x00FF
+
+ # CAN FD Error Codes
+ XL_ERR_INVALID_DLC = 513 # =0x0201
+ XL_ERR_INVALID_CANID = 514 # =0x0202
+ XL_ERR_INVALID_FDFLAG_MODE20 = 515 # =0x203
+ XL_ERR_EDL_RTR = 516 # =0x204
+ XL_ERR_EDL_NOT_SET = 517 # =0x205
+ XL_ERR_UNKNOWN_FLAG = 518 # =0x206
+
+
+class XL_TimeSyncNewValue(IntEnum):
+ XL_SET_TIMESYNC_NO_CHANGE = 0
+ XL_SET_TIMESYNC_ON = 1
+ XL_SET_TIMESYNC_OFF = 2
+
+
+class XL_HardwareType(IntEnum):
+ XL_HWTYPE_NONE = 0
+ XL_HWTYPE_VIRTUAL = 1
+ XL_HWTYPE_CANCARDX = 2
+ XL_HWTYPE_CANAC2PCI = 6
+ XL_HWTYPE_CANCARDY = 12
+ XL_HWTYPE_CANCARDXL = 15
+ XL_HWTYPE_CANCASEXL = 21
+ XL_HWTYPE_CANCASEXL_LOG_OBSOLETE = 23
+ XL_HWTYPE_CANBOARDXL = 25
+ XL_HWTYPE_CANBOARDXL_PXI = 27
+ XL_HWTYPE_VN2600 = 29
+ XL_HWTYPE_VN2610 = XL_HWTYPE_VN2600
+ XL_HWTYPE_VN3300 = 37
+ XL_HWTYPE_VN3600 = 39
+ XL_HWTYPE_VN7600 = 41
+ XL_HWTYPE_CANCARDXLE = 43
+ XL_HWTYPE_VN8900 = 45
+ XL_HWTYPE_VN8950 = 47
+ XL_HWTYPE_VN2640 = 53
+ XL_HWTYPE_VN1610 = 55
+ XL_HWTYPE_VN1630 = 57
+ XL_HWTYPE_VN1640 = 59
+ XL_HWTYPE_VN8970 = 61
+ XL_HWTYPE_VN1611 = 63
+ XL_HWTYPE_VN5240 = 64
+ XL_HWTYPE_VN5610 = 65
+ XL_HWTYPE_VN5620 = 66
+ XL_HWTYPE_VN7570 = 67
+ XL_HWTYPE_VN5650 = 68
+ XL_HWTYPE_IPCLIENT = 69
+ XL_HWTYPE_VN5611 = 70
+ XL_HWTYPE_IPSERVER = 71
+ XL_HWTYPE_VN5612 = 72
+ XL_HWTYPE_VX1121 = 73
+ XL_HWTYPE_VN5601 = 74
+ XL_HWTYPE_VX1131 = 75
+ XL_HWTYPE_VT6204 = 77
+ XL_HWTYPE_VN1630_LOG = 79
+ XL_HWTYPE_VN7610 = 81
+ XL_HWTYPE_VN7572 = 83
+ XL_HWTYPE_VN8972 = 85
+ XL_HWTYPE_VN0601 = 87
+ XL_HWTYPE_VN5640 = 89
+ XL_HWTYPE_VX0312 = 91
+ XL_HWTYPE_VH6501 = 94
+ XL_HWTYPE_VN8800 = 95
+ XL_HWTYPE_IPCL8800 = 96
+ XL_HWTYPE_IPSRV8800 = 97
+ XL_HWTYPE_CSMCAN = 98
+ XL_HWTYPE_VN5610A = 101
+ XL_HWTYPE_VN7640 = 102
+ XL_HWTYPE_VX1135 = 104
+ XL_HWTYPE_VN4610 = 105
+ XL_HWTYPE_VT6306 = 107
+ XL_HWTYPE_VT6104A = 108
+ XL_HWTYPE_VN5430 = 109
+ XL_HWTYPE_VTSSERVICE = 110
+ XL_HWTYPE_VN1530 = 112
+ XL_HWTYPE_VN1531 = 113
+ XL_HWTYPE_VX1161A = 114
+ XL_HWTYPE_VX1161B = 115
+ XL_HWTYPE_VGNSS = 116
+ XL_HWTYPE_VXLAPINIC = 118
+ XL_MAX_HWTYPE = 120
+
+
+class XL_SyncPulseSource(IntEnum):
+ XL_SYNC_PULSE_EXTERNAL = 0
+ XL_SYNC_PULSE_OUR = 1
+ XL_SYNC_PULSE_OUR_SHARED = 2
diff --git a/can/interfaces/vector/xldriver.py b/can/interfaces/vector/xldriver.py
new file mode 100644
index 000000000..faed23b36
--- /dev/null
+++ b/can/interfaces/vector/xldriver.py
@@ -0,0 +1,296 @@
+# type: ignore
+"""
+Ctypes wrapper module for Vector CAN Interface on win32/win64 systems.
+
+Authors: Julien Grave , Christian Sandberg
+"""
+
+import ctypes
+import logging
+import platform
+from ctypes.util import find_library
+
+from . import xlclass
+from .exceptions import VectorInitializationError, VectorOperationError
+
+LOG = logging.getLogger(__name__)
+
+# Load Windows DLL
+DLL_NAME = "vxlapi64" if platform.architecture()[0] == "64bit" else "vxlapi"
+if dll_path := find_library(DLL_NAME):
+ _xlapi_dll = ctypes.windll.LoadLibrary(dll_path)
+else:
+ raise FileNotFoundError(f"Vector XL library not found: {DLL_NAME}")
+
+# ctypes wrapping for API functions
+xlGetErrorString = _xlapi_dll.xlGetErrorString
+xlGetErrorString.argtypes = [xlclass.XLstatus]
+xlGetErrorString.restype = xlclass.XLstringType
+
+
+def check_status_operation(result, function, arguments):
+ """Check the status and raise a :class:`VectorOperationError` on error."""
+ if result > 0:
+ raise VectorOperationError(
+ result, xlGetErrorString(result).decode(), function.__name__
+ )
+ return result
+
+
+def check_status_initialization(result, function, arguments):
+ """Check the status and raise a :class:`VectorInitializationError` on error."""
+ if result > 0:
+ raise VectorInitializationError(
+ result, xlGetErrorString(result).decode(), function.__name__
+ )
+ return result
+
+
+xlGetDriverConfig = _xlapi_dll.xlGetDriverConfig
+xlGetDriverConfig.argtypes = [ctypes.POINTER(xlclass.XLdriverConfig)]
+xlGetDriverConfig.restype = xlclass.XLstatus
+xlGetDriverConfig.errcheck = check_status_operation
+
+xlOpenDriver = _xlapi_dll.xlOpenDriver
+xlOpenDriver.argtypes = []
+xlOpenDriver.restype = xlclass.XLstatus
+xlOpenDriver.errcheck = check_status_initialization
+
+xlCloseDriver = _xlapi_dll.xlCloseDriver
+xlCloseDriver.argtypes = []
+xlCloseDriver.restype = xlclass.XLstatus
+xlCloseDriver.errcheck = check_status_operation
+
+xlGetApplConfig = _xlapi_dll.xlGetApplConfig
+xlGetApplConfig.argtypes = [
+ ctypes.c_char_p,
+ ctypes.c_uint,
+ ctypes.POINTER(ctypes.c_uint),
+ ctypes.POINTER(ctypes.c_uint),
+ ctypes.POINTER(ctypes.c_uint),
+ ctypes.c_uint,
+]
+xlGetApplConfig.restype = xlclass.XLstatus
+xlGetApplConfig.errcheck = check_status_initialization
+
+xlSetApplConfig = _xlapi_dll.xlSetApplConfig
+xlSetApplConfig.argtypes = [
+ ctypes.c_char_p,
+ ctypes.c_uint,
+ ctypes.c_uint,
+ ctypes.c_uint,
+ ctypes.c_uint,
+ ctypes.c_uint,
+]
+xlSetApplConfig.restype = xlclass.XLstatus
+xlSetApplConfig.errcheck = check_status_initialization
+
+xlGetChannelIndex = _xlapi_dll.xlGetChannelIndex
+xlGetChannelIndex.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int]
+xlGetChannelIndex.restype = ctypes.c_int
+
+xlGetChannelMask = _xlapi_dll.xlGetChannelMask
+xlGetChannelMask.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int]
+xlGetChannelMask.restype = xlclass.XLaccess
+
+xlOpenPort = _xlapi_dll.xlOpenPort
+xlOpenPort.argtypes = [
+ ctypes.POINTER(xlclass.XLportHandle),
+ ctypes.c_char_p,
+ xlclass.XLaccess,
+ ctypes.POINTER(xlclass.XLaccess),
+ ctypes.c_uint,
+ ctypes.c_uint,
+ ctypes.c_uint,
+]
+xlOpenPort.restype = xlclass.XLstatus
+xlOpenPort.errcheck = check_status_initialization
+
+xlGetSyncTime = _xlapi_dll.xlGetSyncTime
+xlGetSyncTime.argtypes = [xlclass.XLportHandle, ctypes.POINTER(xlclass.XLuint64)]
+xlGetSyncTime.restype = xlclass.XLstatus
+xlGetSyncTime.errcheck = check_status_initialization
+
+xlGetChannelTime = _xlapi_dll.xlGetChannelTime
+xlGetChannelTime.argtypes = [
+ xlclass.XLportHandle,
+ xlclass.XLaccess,
+ ctypes.POINTER(xlclass.XLuint64),
+]
+xlGetChannelTime.restype = xlclass.XLstatus
+xlGetChannelTime.errcheck = check_status_initialization
+
+xlClosePort = _xlapi_dll.xlClosePort
+xlClosePort.argtypes = [xlclass.XLportHandle]
+xlClosePort.restype = xlclass.XLstatus
+xlClosePort.errcheck = check_status_operation
+
+xlSetNotification = _xlapi_dll.xlSetNotification
+xlSetNotification.argtypes = [
+ xlclass.XLportHandle,
+ ctypes.POINTER(xlclass.XLhandle),
+ ctypes.c_int,
+]
+xlSetNotification.restype = xlclass.XLstatus
+xlSetNotification.errcheck = check_status_initialization
+
+xlCanSetChannelMode = _xlapi_dll.xlCanSetChannelMode
+xlCanSetChannelMode.argtypes = [
+ xlclass.XLportHandle,
+ xlclass.XLaccess,
+ ctypes.c_int,
+ ctypes.c_int,
+]
+xlCanSetChannelMode.restype = xlclass.XLstatus
+xlCanSetChannelMode.errcheck = check_status_initialization
+
+xlActivateChannel = _xlapi_dll.xlActivateChannel
+xlActivateChannel.argtypes = [
+ xlclass.XLportHandle,
+ xlclass.XLaccess,
+ ctypes.c_uint,
+ ctypes.c_uint,
+]
+xlActivateChannel.restype = xlclass.XLstatus
+xlActivateChannel.errcheck = check_status_operation
+
+xlDeactivateChannel = _xlapi_dll.xlDeactivateChannel
+xlDeactivateChannel.argtypes = [xlclass.XLportHandle, xlclass.XLaccess]
+xlDeactivateChannel.restype = xlclass.XLstatus
+xlDeactivateChannel.errcheck = check_status_operation
+
+xlCanFdSetConfiguration = _xlapi_dll.xlCanFdSetConfiguration
+xlCanFdSetConfiguration.argtypes = [
+ xlclass.XLportHandle,
+ xlclass.XLaccess,
+ ctypes.POINTER(xlclass.XLcanFdConf),
+]
+xlCanFdSetConfiguration.restype = xlclass.XLstatus
+xlCanFdSetConfiguration.errcheck = check_status_initialization
+
+xlReceive = _xlapi_dll.xlReceive
+xlReceive.argtypes = [
+ xlclass.XLportHandle,
+ ctypes.POINTER(ctypes.c_uint),
+ ctypes.POINTER(xlclass.XLevent),
+]
+xlReceive.restype = xlclass.XLstatus
+xlReceive.errcheck = check_status_operation
+
+xlCanReceive = _xlapi_dll.xlCanReceive
+xlCanReceive.argtypes = [xlclass.XLportHandle, ctypes.POINTER(xlclass.XLcanRxEvent)]
+xlCanReceive.restype = xlclass.XLstatus
+xlCanReceive.errcheck = check_status_operation
+
+xlCanSetChannelBitrate = _xlapi_dll.xlCanSetChannelBitrate
+xlCanSetChannelBitrate.argtypes = [
+ xlclass.XLportHandle,
+ xlclass.XLaccess,
+ ctypes.c_ulong,
+]
+xlCanSetChannelBitrate.restype = xlclass.XLstatus
+xlCanSetChannelBitrate.errcheck = check_status_initialization
+
+xlCanSetChannelParams = _xlapi_dll.xlCanSetChannelParams
+xlCanSetChannelParams.argtypes = [
+ xlclass.XLportHandle,
+ xlclass.XLaccess,
+ ctypes.POINTER(xlclass.XLchipParams),
+]
+xlCanSetChannelParams.restype = xlclass.XLstatus
+xlCanSetChannelParams.errcheck = check_status_initialization
+
+xlCanSetChannelParamsC200 = _xlapi_dll.xlCanSetChannelParamsC200
+xlCanSetChannelParamsC200.argtypes = [
+ xlclass.XLportHandle,
+ xlclass.XLaccess,
+ ctypes.c_ubyte,
+ ctypes.c_ubyte,
+]
+xlCanSetChannelParams.restype = xlclass.XLstatus
+xlCanSetChannelParams.errcheck = check_status_initialization
+
+xlCanTransmit = _xlapi_dll.xlCanTransmit
+xlCanTransmit.argtypes = [
+ xlclass.XLportHandle,
+ xlclass.XLaccess,
+ ctypes.POINTER(ctypes.c_uint),
+ ctypes.POINTER(xlclass.XLevent),
+]
+xlCanTransmit.restype = xlclass.XLstatus
+xlCanTransmit.errcheck = check_status_operation
+
+xlCanTransmitEx = _xlapi_dll.xlCanTransmitEx
+xlCanTransmitEx.argtypes = [
+ xlclass.XLportHandle,
+ xlclass.XLaccess,
+ ctypes.c_uint,
+ ctypes.POINTER(ctypes.c_uint),
+ ctypes.POINTER(xlclass.XLcanTxEvent),
+]
+xlCanTransmitEx.restype = xlclass.XLstatus
+xlCanTransmitEx.errcheck = check_status_operation
+
+xlCanFlushTransmitQueue = _xlapi_dll.xlCanFlushTransmitQueue
+xlCanFlushTransmitQueue.argtypes = [xlclass.XLportHandle, xlclass.XLaccess]
+xlCanFlushTransmitQueue.restype = xlclass.XLstatus
+xlCanFlushTransmitQueue.errcheck = check_status_operation
+
+xlCanSetChannelAcceptance = _xlapi_dll.xlCanSetChannelAcceptance
+xlCanSetChannelAcceptance.argtypes = [
+ xlclass.XLportHandle,
+ xlclass.XLaccess,
+ ctypes.c_ulong,
+ ctypes.c_ulong,
+ ctypes.c_uint,
+]
+xlCanSetChannelAcceptance.restype = xlclass.XLstatus
+xlCanSetChannelAcceptance.errcheck = check_status_operation
+
+xlCanResetAcceptance = _xlapi_dll.xlCanResetAcceptance
+xlCanResetAcceptance.argtypes = [xlclass.XLportHandle, xlclass.XLaccess, ctypes.c_uint]
+xlCanResetAcceptance.restype = xlclass.XLstatus
+xlCanResetAcceptance.errcheck = check_status_operation
+
+xlCanRequestChipState = _xlapi_dll.xlCanRequestChipState
+xlCanRequestChipState.argtypes = [xlclass.XLportHandle, xlclass.XLaccess]
+xlCanRequestChipState.restype = xlclass.XLstatus
+xlCanRequestChipState.errcheck = check_status_operation
+
+xlCanSetChannelOutput = _xlapi_dll.xlCanSetChannelOutput
+xlCanSetChannelOutput.argtypes = [xlclass.XLportHandle, xlclass.XLaccess, ctypes.c_char]
+xlCanSetChannelOutput.restype = xlclass.XLstatus
+xlCanSetChannelOutput.errcheck = check_status_operation
+
+xlPopupHwConfig = _xlapi_dll.xlPopupHwConfig
+xlPopupHwConfig.argtypes = [ctypes.c_char_p, ctypes.c_uint]
+xlPopupHwConfig.restype = xlclass.XLstatus
+xlPopupHwConfig.errcheck = check_status_operation
+
+xlSetTimerRate = _xlapi_dll.xlSetTimerRate
+xlSetTimerRate.argtypes = [xlclass.XLportHandle, ctypes.c_ulong]
+xlSetTimerRate.restype = xlclass.XLstatus
+xlSetTimerRate.errcheck = check_status_operation
+
+xlGetEventString = _xlapi_dll.xlGetEventString
+xlGetEventString.argtypes = [ctypes.POINTER(xlclass.XLevent)]
+xlGetEventString.restype = xlclass.XLstringType
+
+xlCanGetEventString = _xlapi_dll.xlCanGetEventString
+xlCanGetEventString.argtypes = [ctypes.POINTER(xlclass.XLcanRxEvent)]
+xlCanGetEventString.restype = xlclass.XLstringType
+
+xlGetReceiveQueueLevel = _xlapi_dll.xlGetReceiveQueueLevel
+xlGetReceiveQueueLevel.argtypes = [xlclass.XLportHandle, ctypes.POINTER(ctypes.c_int)]
+xlGetReceiveQueueLevel.restype = xlclass.XLstatus
+xlGetReceiveQueueLevel.errcheck = check_status_operation
+
+xlGenerateSyncPulse = _xlapi_dll.xlGenerateSyncPulse
+xlGenerateSyncPulse.argtypes = [xlclass.XLportHandle, xlclass.XLaccess]
+xlGenerateSyncPulse.restype = xlclass.XLstatus
+xlGenerateSyncPulse.errcheck = check_status_operation
+
+xlFlushReceiveQueue = _xlapi_dll.xlFlushReceiveQueue
+xlFlushReceiveQueue.argtypes = [xlclass.XLportHandle]
+xlFlushReceiveQueue.restype = xlclass.XLstatus
+xlFlushReceiveQueue.errcheck = check_status_operation
diff --git a/can/interfaces/virtual.py b/can/interfaces/virtual.py
index c53d0cf21..ba33a6ea8 100644
--- a/can/interfaces/virtual.py
+++ b/can/interfaces/virtual.py
@@ -1,6 +1,3 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
This module implements an OS and hardware independent
virtual CAN interface for testing purposes.
@@ -9,70 +6,119 @@
and reside in the same process will receive the same messages.
"""
-import copy
import logging
+import queue
import time
-try:
- import queue
-except ImportError:
- import Queue as queue
-from threading import RLock
+from copy import deepcopy
from random import randint
+from threading import RLock
+from typing import Any, Final
-from can.bus import BusABC
-from can import CanError
+from can import CanOperationError
+from can.bus import BusABC, CanProtocol
+from can.message import Message
+from can.typechecking import AutoDetectedConfig, Channel
logger = logging.getLogger(__name__)
-
# Channels are lists of queues, one for each connection
-channels = {}
-channels_lock = RLock()
+channels: Final[dict[Channel, list[queue.Queue[Message]]]] = {}
+channels_lock: Final = RLock()
class VirtualBus(BusABC):
"""
- A virtual CAN bus using an internal message queue. It can be
- used for example for testing.
+ A virtual CAN bus using an internal message queue. It can be used for
+ example for testing.
In this interface, a channel is an arbitrary object used as
an identifier for connected buses.
Implements :meth:`can.BusABC._detect_available_configs`; see
- :meth:`can.VirtualBus._detect_available_configs` for how it
+ :meth:`_detect_available_configs` for how it
behaves here.
+
+ .. note::
+ The timeout when sending a message applies to each receiver
+ individually. This means that sending can block up to 5 seconds
+ if a message is sent to 5 receivers with the timeout set to 1.0.
+
+ .. warning::
+ This interface guarantees reliable delivery and message ordering, but
+ does *not* implement rate limiting or ID arbitration/prioritization
+ under high loads. Please refer to the section
+ :ref:`virtual_interfaces_doc` for more information on this and a
+ comparison to alternatives.
"""
- def __init__(self, channel=None, receive_own_messages=False,
- rx_queue_size=0, **config):
- super(VirtualBus, self).__init__(channel=channel,
- receive_own_messages=receive_own_messages, **config)
+ def __init__(
+ self,
+ channel: Channel = "channel-0",
+ receive_own_messages: bool = False,
+ rx_queue_size: int = 0,
+ preserve_timestamps: bool = False,
+ protocol: CanProtocol = CanProtocol.CAN_20,
+ **kwargs: Any,
+ ) -> None:
+ """
+ The constructed instance has access to the bus identified by the
+ channel parameter. It is able to see all messages transmitted on the
+ bus by virtual instances constructed with the same channel identifier.
+
+ :param channel: The channel identifier. This parameter can be an
+ arbitrary hashable value. The bus instance will be able to see
+ messages from other virtual bus instances that were created with
+ the same value.
+ :param receive_own_messages: If set to True, sent messages will be
+ reflected back on the input queue.
+ :param rx_queue_size: The size of the reception queue. The reception
+ queue stores messages until they are read. If the queue reaches
+ its capacity, it will start dropping the oldest messages to make
+ room for new ones. If set to 0, the queue has an infinite capacity.
+ Be aware that this can cause memory leaks if messages are read
+ with a lower frequency than they arrive on the bus.
+ :param preserve_timestamps: If set to True, messages transmitted via
+ :func:`~can.BusABC.send` will keep the timestamp set in the
+ :class:`~can.Message` instance. Otherwise, the timestamp value
+ will be replaced with the current system time.
+ :param protocol: The protocol implemented by this bus instance. The
+ value does not affect the operation of the bus instance and can
+ be set to an arbitrary value for testing purposes.
+ :param kwargs: Additional keyword arguments passed to the parent
+ constructor.
+ """
+ super().__init__(
+ channel=channel,
+ receive_own_messages=receive_own_messages,
+ **kwargs,
+ )
# the channel identifier may be an arbitrary object
self.channel_id = channel
- self.channel_info = 'Virtual bus channel %s' % self.channel_id
+ self._can_protocol = protocol
+ self.channel_info = f"Virtual bus channel {self.channel_id}"
self.receive_own_messages = receive_own_messages
+ self.preserve_timestamps = preserve_timestamps
self._open = True
with channels_lock:
-
# Create a new channel if one does not exist
if self.channel_id not in channels:
channels[self.channel_id] = []
self.channel = channels[self.channel_id]
- self.queue = queue.Queue(rx_queue_size)
+ self.queue: queue.Queue[Message] = queue.Queue(rx_queue_size)
self.channel.append(self.queue)
- def _check_if_open(self):
- """Raises CanError if the bus is not open.
+ def _check_if_open(self) -> None:
+ """Raises :exc:`~can.exceptions.CanOperationError` if the bus is not open.
Has to be called in every method that accesses the bus.
"""
if not self._open:
- raise CanError('Operation on closed bus')
+ raise CanOperationError("Cannot operate on a closed bus")
- def _recv_internal(self, timeout):
+ def _recv_internal(self, timeout: float | None) -> tuple[Message | None, bool]:
self._check_if_open()
try:
msg = self.queue.get(block=True, timeout=timeout)
@@ -81,37 +127,41 @@ def _recv_internal(self, timeout):
else:
return msg, False
- def send(self, msg, timeout=None):
+ def send(self, msg: Message, timeout: float | None = None) -> None:
self._check_if_open()
- # Create a shallow copy for this channel
- msg_copy = copy.copy(msg)
- msg_copy.timestamp = time.time()
- msg_copy.data = bytearray(msg.data)
- msg_copy.channel = self.channel_id
- all_sent = True
+
+ timestamp = msg.timestamp if self.preserve_timestamps else time.time()
# Add message to all listening on this channel
+ all_sent = True
for bus_queue in self.channel:
- if bus_queue is not self.queue or self.receive_own_messages:
- try:
- bus_queue.put(msg_copy, block=True, timeout=timeout)
- except queue.Full:
- all_sent = False
+ if bus_queue is self.queue and not self.receive_own_messages:
+ continue
+ msg_copy = deepcopy(msg)
+ msg_copy.timestamp = timestamp
+ msg_copy.channel = self.channel_id
+ msg_copy.is_rx = bus_queue is not self.queue
+ try:
+ bus_queue.put(msg_copy, block=True, timeout=timeout)
+ except queue.Full:
+ all_sent = False
+
if not all_sent:
- raise CanError('Could not send message to one or more recipients')
+ raise CanOperationError("Could not send message to one or more recipients")
- def shutdown(self):
- self._check_if_open()
- self._open = False
+ def shutdown(self) -> None:
+ super().shutdown()
+ if self._open:
+ self._open = False
- with channels_lock:
- self.channel.remove(self.queue)
+ with channels_lock:
+ self.channel.remove(self.queue)
- # remove if empty
- if not self.channel:
- del channels[self.channel_id]
+ # remove if empty
+ if not self.channel:
+ del channels[self.channel_id]
@staticmethod
- def _detect_available_configs():
+ def _detect_available_configs() -> list[AutoDetectedConfig]:
"""
Returns all currently used channels as well as
one other currently unused channel.
@@ -119,14 +169,16 @@ def _detect_available_configs():
.. note::
This method will run into problems if thousands of
- autodetected busses are used at once.
+ autodetected buses are used at once.
"""
with channels_lock:
available_channels = list(channels.keys())
# find a currently unused channel
- get_extra = lambda: "channel-{}".format(randint(0, 9999))
+ def get_extra() -> str:
+ return f"channel-{randint(0, 9999)}"
+
extra = get_extra()
while extra in available_channels:
extra = get_extra()
@@ -134,6 +186,6 @@ def _detect_available_configs():
available_channels += [extra]
return [
- {'interface': 'virtual', 'channel': channel}
+ {"interface": "virtual", "channel": channel}
for channel in available_channels
]
diff --git a/can/io/__init__.py b/can/io/__init__.py
index 1d5269912..69894c3d0 100644
--- a/can/io/__init__.py
+++ b/can/io/__init__.py
@@ -1,21 +1,57 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
-Read and Write CAN bus messages using a range of Readers
+Read and write CAN bus messages using a range of Readers
and Writers based off the file extension.
"""
-from __future__ import absolute_import
+__all__ = [
+ "MESSAGE_READERS",
+ "MESSAGE_WRITERS",
+ "ASCReader",
+ "ASCWriter",
+ "BLFReader",
+ "BLFWriter",
+ "BaseRotatingLogger",
+ "CSVReader",
+ "CSVWriter",
+ "CanutilsLogReader",
+ "CanutilsLogWriter",
+ "LogReader",
+ "Logger",
+ "MF4Reader",
+ "MF4Writer",
+ "MessageSync",
+ "Printer",
+ "SizedRotatingLogger",
+ "SqliteReader",
+ "SqliteWriter",
+ "TRCFileVersion",
+ "TRCReader",
+ "TRCWriter",
+ "asc",
+ "blf",
+ "canutils",
+ "csv",
+ "generic",
+ "logger",
+ "mf4",
+ "player",
+ "printer",
+ "sqlite",
+ "trc",
+]
# Generic
-from .logger import Logger
-from .player import LogReader, MessageSync
+from .logger import MESSAGE_WRITERS, BaseRotatingLogger, Logger, SizedRotatingLogger
+from .player import MESSAGE_READERS, LogReader, MessageSync
+
+# isort: split
# Format specific
-from .asc import ASCWriter, ASCReader
+from .asc import ASCReader, ASCWriter
from .blf import BLFReader, BLFWriter
from .canutils import CanutilsLogReader, CanutilsLogWriter
-from .csv import CSVWriter, CSVReader
-from .sqlite import SqliteReader, SqliteWriter
+from .csv import CSVReader, CSVWriter
+from .mf4 import MF4Reader, MF4Writer
from .printer import Printer
+from .sqlite import SqliteReader, SqliteWriter
+from .trc import TRCFileVersion, TRCReader, TRCWriter
diff --git a/can/io/asc.py b/can/io/asc.py
index bbc8807b7..fcf8fc5e4 100644
--- a/can/io/asc.py
+++ b/can/io/asc.py
@@ -1,128 +1,326 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Contains handling of ASC logging files.
Example .asc files:
- - https://bitbucket.org/tobylorenz/vector_asc/src/47556e1a6d32c859224ca62d075e1efcc67fa690/src/Vector/ASC/tests/unittests/data/CAN_Log_Trigger_3_2.asc?at=master&fileviewer=file-view-default
+ - https://bitbucket.org/tobylorenz/vector_asc/src/master/src/Vector/ASC/tests/unittests/data/
- under `test/data/logfile.asc`
"""
-from __future__ import absolute_import
-
-from datetime import datetime
-import time
import logging
+import re
+from collections.abc import Generator
+from datetime import datetime
+from typing import Any, Final, TextIO
from ..message import Message
-from ..listener import Listener
-from ..util import channel2int
-from .generic import BaseIOHandler
+from ..typechecking import StringPathLike
+from ..util import channel2int, dlc2len, len2dlc
+from .generic import TextIOMessageReader, TextIOMessageWriter
CAN_MSG_EXT = 0x80000000
CAN_ID_MASK = 0x1FFFFFFF
+BASE_HEX = 16
+BASE_DEC = 10
+ASC_TRIGGER_REGEX: Final = re.compile(
+ r"begin\s+triggerblock\s+\w+\s+(?P.+)", re.IGNORECASE
+)
+ASC_MESSAGE_REGEX: Final = re.compile(
+ r"\d+\.\d+\s+(\d+\s+(\w+\s+(Tx|Rx)|ErrorFrame)|CANFD)",
+ re.ASCII | re.IGNORECASE,
+)
-logger = logging.getLogger('can.io.asc')
+logger = logging.getLogger("can.io.asc")
-class ASCReader(BaseIOHandler):
- """
- Iterator of CAN messages from a ASC logging file.
- TODO: turn relative timestamps back to absolute form
+class ASCReader(TextIOMessageReader):
+ """
+ Iterator of CAN messages from a ASC logging file. Meta data (comments,
+ bus statistics, J1939 Transport Protocol messages) is ignored.
"""
- def __init__(self, file):
+ def __init__(
+ self,
+ file: StringPathLike | TextIO,
+ base: str = "hex",
+ relative_timestamp: bool = True,
+ **kwargs: Any,
+ ) -> None:
"""
:param file: a path-like object or as file-like object to read from
If this is a file-like object, is has to opened in text
read mode, not binary read mode.
+ :param base: Select the base(hex or dec) of id and data.
+ If the header of the asc file contains base information,
+ this value will be overwritten. Default "hex".
+ :param relative_timestamp: Select whether the timestamps are
+ `relative` (starting at 0.0) or `absolute` (starting at
+ the system time). Default `True = relative`.
"""
- super(ASCReader, self).__init__(file, mode='r')
-
- @staticmethod
- def _extract_can_id(str_can_id):
- if str_can_id[-1:].lower() == 'x':
- is_extended = True
- can_id = int(str_can_id[0:-1], 16)
- else:
- is_extended = False
- can_id = int(str_can_id, 16)
- #logging.debug('ASCReader: _extract_can_id("%s") -> %x, %r', str_can_id, can_id, is_extended)
- return can_id, is_extended
-
- def __iter__(self):
- for line in self.file:
- #logger.debug("ASCReader: parsing line: '%s'", line.splitlines()[0])
+ super().__init__(file, mode="r")
+
+ if not self.file:
+ raise ValueError("The given file cannot be None")
+ self.base = base
+ self._converted_base = self._check_base(base)
+ self.relative_timestamp = relative_timestamp
+ self.date: str | None = None
+ self.start_time = 0.0
+ # TODO - what is this used for? The ASC Writer only prints `absolute`
+ self.timestamps_format: str | None = None
+ self.internal_events_logged = False
+
+ def _extract_header(self) -> None:
+ for _line in self.file:
+ line = _line.strip()
+
+ datetime_match = re.match(
+ r"date\s+\w+\s+(?P.+)", line, re.IGNORECASE
+ )
+ base_match = re.match(
+ r"base\s+(?Phex|dec)(?:\s+timestamps\s+"
+ r"(?Pabsolute|relative))?",
+ line,
+ re.IGNORECASE,
+ )
+ comment_match = re.match(r"//.*", line)
+ events_match = re.match(
+ r"(?Pno)?\s*internal\s+events\s+logged", line, re.IGNORECASE
+ )
+
+ if datetime_match:
+ self.date = datetime_match.group("datetime_string")
+ self.start_time = (
+ 0.0
+ if self.relative_timestamp
+ else self._datetime_to_timestamp(self.date)
+ )
+ continue
- temp = line.strip()
- if not temp or not temp[0].isdigit():
+ if base_match:
+ base = base_match.group("base")
+ timestamp_format = base_match.group("timestamp_format")
+ self.base = base
+ self._converted_base = self._check_base(self.base)
+ self.timestamps_format = timestamp_format or "absolute"
continue
- try:
- timestamp, channel, dummy = temp.split(None, 2) # , frameType, dlc, frameData
- except ValueError:
- # we parsed an empty comment
+ if comment_match:
continue
- timestamp = float(timestamp)
+ if events_match:
+ self.internal_events_logged = events_match.group("no_events") is None
+ break
+
+ break
+
+ @staticmethod
+ def _datetime_to_timestamp(datetime_string: str) -> float:
+ month_map = {
+ "jan": 1,
+ "feb": 2,
+ "mar": 3,
+ "apr": 4,
+ "may": 5,
+ "jun": 6,
+ "jul": 7,
+ "aug": 8,
+ "sep": 9,
+ "oct": 10,
+ "nov": 11,
+ "dec": 12,
+ "mär": 3,
+ "mai": 5,
+ "okt": 10,
+ "dez": 12,
+ }
+
+ datetime_formats = (
+ "%m %d %I:%M:%S.%f %p %Y",
+ "%m %d %I:%M:%S %p %Y",
+ "%m %d %H:%M:%S.%f %Y",
+ "%m %d %H:%M:%S %Y",
+ "%m %d %H:%M:%S.%f %p %Y",
+ "%m %d %H:%M:%S %p %Y",
+ )
+
+ datetime_string_parts = datetime_string.split(" ", 1)
+ month = datetime_string_parts[0].strip().lower()
+
+ try:
+ datetime_string_parts[0] = f"{month_map[month]:02d}"
+ except KeyError:
+ raise ValueError(f"Unsupported month abbreviation: {month}") from None
+ datetime_string = " ".join(datetime_string_parts)
+
+ for format_str in datetime_formats:
try:
- # See ASCWriter
- channel = int(channel) - 1
+ return datetime.strptime(datetime_string, format_str).timestamp()
except ValueError:
- pass
+ continue
- if dummy.strip()[0:10] == 'ErrorFrame':
- msg = Message(timestamp=timestamp, is_error_frame=True,
- channel=channel)
- yield msg
+ raise ValueError(f"Unsupported datetime format: '{datetime_string}'")
- elif not isinstance(channel, int) or dummy.strip()[0:10] == 'Statistic:':
- pass
-
- elif dummy[-1:].lower() == 'r':
- can_id_str, _ = dummy.split(None, 1)
- can_id_num, is_extended_id = self._extract_can_id(can_id_str)
- msg = Message(timestamp=timestamp,
- arbitration_id=can_id_num & CAN_ID_MASK,
- extended_id=is_extended_id,
- is_remote_frame=True,
- channel=channel)
- yield msg
+ def _extract_can_id(self, str_can_id: str, msg_kwargs: dict[str, Any]) -> None:
+ if str_can_id[-1:].lower() == "x":
+ msg_kwargs["is_extended_id"] = True
+ can_id = int(str_can_id[0:-1], self._converted_base)
+ else:
+ msg_kwargs["is_extended_id"] = False
+ can_id = int(str_can_id, self._converted_base)
+ msg_kwargs["arbitration_id"] = can_id
+ @staticmethod
+ def _check_base(base: str) -> int:
+ if base not in ["hex", "dec"]:
+ raise ValueError('base should be either "hex" or "dec"')
+ return BASE_DEC if base == "dec" else BASE_HEX
+
+ def _process_data_string(
+ self, data_str: str, data_length: int, msg_kwargs: dict[str, Any]
+ ) -> None:
+ frame = bytearray()
+ data = data_str.split()
+ for byte in data[:data_length]:
+ frame.append(int(byte, self._converted_base))
+ msg_kwargs["data"] = frame
+
+ def _process_classic_can_frame(
+ self, line: str, msg_kwargs: dict[str, Any]
+ ) -> Message:
+ # CAN error frame
+ if line.strip()[0:10].lower() == "errorframe":
+ # Error Frame
+ msg_kwargs["is_error_frame"] = True
+ else:
+ abr_id_str, direction, rest_of_message = line.split(None, 2)
+ msg_kwargs["is_rx"] = direction == "Rx"
+ self._extract_can_id(abr_id_str, msg_kwargs)
+
+ if rest_of_message[0].lower() == "r":
+ # CAN Remote Frame
+ msg_kwargs["is_remote_frame"] = True
+ remote_data = rest_of_message.split()
+ if len(remote_data) > 1:
+ dlc_str = remote_data[1]
+ if dlc_str.isdigit():
+ msg_kwargs["dlc"] = int(dlc_str, self._converted_base)
else:
+ # Classic CAN Message
try:
- # this only works if dlc > 0 and thus data is availabe
- can_id_str, _, _, dlc, data = dummy.split(None, 4)
+ # There is data after DLC
+ _, dlc_str, data = rest_of_message.split(None, 2)
except ValueError:
- # but if not, we only want to get the stuff up to the dlc
- can_id_str, _, _, dlc = dummy.split(None, 3)
- # and we set data to an empty sequence manually
- data = ''
-
- dlc = int(dlc)
- frame = bytearray()
- data = data.split()
- for byte in data[0:dlc]:
- frame.append(int(byte, 16))
-
- can_id_num, is_extended_id = self._extract_can_id(can_id_str)
-
- yield Message(
- timestamp=timestamp,
- arbitration_id=can_id_num & CAN_ID_MASK,
- extended_id=is_extended_id,
- is_remote_frame=False,
- dlc=dlc,
- data=frame,
- channel=channel
+ # No data after DLC
+ _, dlc_str = rest_of_message.split(None, 1)
+ data = ""
+
+ dlc = dlc2len(int(dlc_str, self._converted_base))
+ msg_kwargs["dlc"] = dlc
+ self._process_data_string(data, min(8, dlc), msg_kwargs)
+
+ return Message(**msg_kwargs)
+
+ def _process_fd_can_frame(self, line: str, msg_kwargs: dict[str, Any]) -> Message:
+ channel, direction, rest_of_message = line.split(None, 2)
+ # See ASCWriter
+ msg_kwargs["channel"] = int(channel) - 1
+ msg_kwargs["is_rx"] = direction == "Rx"
+
+ # CAN FD error frame
+ if rest_of_message.strip()[:10].lower() == "errorframe":
+ # Error Frame
+ # TODO: maybe use regex to parse BRS, ESI, etc?
+ msg_kwargs["is_error_frame"] = True
+ else:
+ can_id_str, frame_name_or_brs, rest_of_message = rest_of_message.split(
+ None, 2
+ )
+
+ if frame_name_or_brs.isdigit():
+ brs = frame_name_or_brs
+ esi, dlc_str, data_length_str, data = rest_of_message.split(None, 3)
+ else:
+ brs, esi, dlc_str, data_length_str, data = rest_of_message.split(
+ None, 4
+ )
+
+ self._extract_can_id(can_id_str, msg_kwargs)
+ msg_kwargs["bitrate_switch"] = brs == "1"
+ msg_kwargs["error_state_indicator"] = esi == "1"
+ dlc = int(dlc_str, self._converted_base)
+ data_length = int(data_length_str)
+ if data_length == 0:
+ # CAN remote Frame
+ msg_kwargs["is_remote_frame"] = True
+ msg_kwargs["dlc"] = dlc
+ else:
+ if dlc2len(dlc) != data_length:
+ logger.warning(
+ "DLC vs Data Length mismatch %d[%d] != %d",
+ dlc,
+ dlc2len(dlc),
+ data_length,
+ )
+ msg_kwargs["dlc"] = data_length
+
+ self._process_data_string(data, data_length, msg_kwargs)
+
+ return Message(**msg_kwargs)
+
+ def __iter__(self) -> Generator[Message, None, None]:
+ self._extract_header()
+
+ for _line in self.file:
+ line = _line.strip()
+
+ if trigger_match := ASC_TRIGGER_REGEX.match(line):
+ datetime_str = trigger_match.group("datetime_string")
+ self.start_time = (
+ 0.0
+ if self.relative_timestamp
+ else self._datetime_to_timestamp(datetime_str)
)
+ continue
+
+ # Handle the "Start of measurement" line
+ if re.match(r"^\d+\.\d+\s+Start of measurement", line):
+ # Skip this line as it's just an indicator
+ continue
+
+ if not ASC_MESSAGE_REGEX.match(line):
+ # line might be a comment, chip status,
+ # J1939 message or some other unsupported event
+ continue
+
+ msg_kwargs: dict[str, float | bool | int] = {}
+ try:
+ _timestamp, channel, rest_of_message = line.split(None, 2)
+ timestamp = float(_timestamp) + self.start_time
+ msg_kwargs["timestamp"] = timestamp
+ if channel == "CANFD":
+ msg_kwargs["is_fd"] = True
+ elif channel.isdigit():
+ # See ASCWriter
+ msg_kwargs["channel"] = int(channel) - 1
+ else:
+ # Not a CAN message. Possible values include "statistic", J1939TP
+ continue
+ except ValueError:
+ # Some other unprocessed or unknown format
+ continue
+
+ if "is_fd" not in msg_kwargs:
+ msg = self._process_classic_can_frame(rest_of_message, msg_kwargs)
+ else:
+ msg = self._process_fd_can_frame(rest_of_message, msg_kwargs)
+ if msg is not None:
+ yield msg
self.stop()
-class ASCWriter(BaseIOHandler, Listener):
+class ASCWriter(TextIOMessageWriter):
"""Logs CAN data to an ASCII log file (.asc).
The measurement starts with the timestamp of the first registered message.
@@ -131,11 +329,37 @@ class ASCWriter(BaseIOHandler, Listener):
It the first message does not have a timestamp, it is set to zero.
"""
- FORMAT_MESSAGE = "{channel} {id:<15} Rx {dtype} {data}"
- FORMAT_DATE = "%a %b %m %I:%M:%S %p %Y"
- FORMAT_EVENT = "{timestamp: 9.4f} {message}\n"
-
- def __init__(self, file, channel=1):
+ FORMAT_MESSAGE = "{channel} {id:<15} {dir:<4} {dtype} {data}"
+ FORMAT_MESSAGE_FD = " ".join(
+ [
+ "CANFD",
+ "{channel:>3}",
+ "{dir:<4}",
+ "{id:>8} {symbolic_name:>32}",
+ "{brs}",
+ "{esi}",
+ "{dlc:x}",
+ "{data_length:>2}",
+ "{data}",
+ "{message_duration:>8}",
+ "{message_length:>4}",
+ "{flags:>8X}",
+ "{crc:>8}",
+ "{bit_timing_conf_arb:>8}",
+ "{bit_timing_conf_data:>8}",
+ "{bit_timing_conf_ext_arb:>8}",
+ "{bit_timing_conf_ext_data:>8}",
+ ]
+ )
+ FORMAT_DATE = "%a %b %d %H:%M:%S.{} %Y"
+ FORMAT_EVENT = "{timestamp: 9.6f} {message}\n"
+
+ def __init__(
+ self,
+ file: StringPathLike | TextIO,
+ channel: int = 1,
+ **kwargs: Any,
+ ) -> None:
"""
:param file: a path-like object or as file-like object to write to
If this is a file-like object, is has to opened in text
@@ -143,73 +367,72 @@ def __init__(self, file, channel=1):
:param channel: a default channel to use when the message does not
have a channel set
"""
- super(ASCWriter, self).__init__(file, mode='w')
+ if kwargs.get("append", False):
+ raise ValueError(
+ f"{self.__class__.__name__} is currently not equipped to "
+ f"append messages to an existing file."
+ )
+ super().__init__(file, mode="w")
+
self.channel = channel
# write start of file header
- now = datetime.now().strftime("%a %b %m %I:%M:%S %p %Y")
- self.file.write("date %s\n" % now)
+ start_time = self._format_header_datetime(datetime.now())
+ self.file.write(f"date {start_time}\n")
self.file.write("base hex timestamps absolute\n")
self.file.write("internal events logged\n")
# the last part is written with the timestamp of the first message
self.header_written = False
- self.last_timestamp = None
- self.started = None
-
- def stop(self):
+ self.last_timestamp = 0.0
+ self.started = 0.0
+
+ def _format_header_datetime(self, dt: datetime) -> str:
+ # Note: CANoe requires that the microsecond field only have 3 digits
+ # Since Python strftime only supports microsecond formatters, we must
+ # manually include the millisecond portion before passing the format
+ # to strftime
+ msec = dt.microsecond // 1000 % 1000
+ format_w_msec = self.FORMAT_DATE.format(msec)
+ return dt.strftime(format_w_msec)
+
+ def stop(self) -> None:
+ # This is guaranteed to not be None since we raise ValueError in __init__
if not self.file.closed:
self.file.write("End TriggerBlock\n")
- super(ASCWriter, self).stop()
+ super().stop()
- def log_event(self, message, timestamp=None):
+ def log_event(self, message: str, timestamp: float | None = None) -> None:
"""Add a message to the log file.
- :param str message: an arbitrary message
- :param float timestamp: the absolute timestamp of the event
+ :param message: an arbitrary message
+ :param timestamp: the absolute timestamp of the event
"""
- if not message: # if empty or None
+ if not message: # if empty or None
logger.debug("ASCWriter: ignoring empty message")
return
# this is the case for the very first message:
if not self.header_written:
- self.last_timestamp = (timestamp or 0.0)
- self.started = self.last_timestamp
- formatted_date = time.strftime(self.FORMAT_DATE, time.localtime(self.last_timestamp))
- self.file.write("Begin Triggerblock %s\n" % formatted_date)
- self.header_written = True
- self.log_event("Start of measurement") # caution: this is a recursive call!
+ self.started = self.last_timestamp = timestamp or 0.0
- # figure out the correct timestamp
- if timestamp is None or timestamp < self.last_timestamp:
- timestamp = self.last_timestamp
+ start_time = datetime.fromtimestamp(self.last_timestamp)
+ formatted_date = self._format_header_datetime(start_time)
+ self.file.write(f"Begin Triggerblock {formatted_date}\n")
+ self.header_written = True
+ self.log_event("Start of measurement") # caution: this is a recursive call!
+ # Use last known timestamp if unknown
+ if timestamp is None:
+ timestamp = self.last_timestamp
# turn into relative timestamps if necessary
if timestamp >= self.started:
timestamp -= self.started
-
line = self.FORMAT_EVENT.format(timestamp=timestamp, message=message)
self.file.write(line)
- def on_message_received(self, msg):
-
- if msg.is_error_frame:
- self.log_event("{} ErrorFrame".format(self.channel), msg.timestamp)
- return
-
- if msg.is_remote_frame:
- dtype = 'r'
- data = []
- else:
- dtype = "d {}".format(msg.dlc)
- data = ["{:02X}".format(byte) for byte in msg.data]
-
- arb_id = "{:X}".format(msg.arbitration_id)
- if msg.is_extended_id:
- arb_id += 'x'
-
+ def on_message_received(self, msg: Message) -> None:
channel = channel2int(msg.channel)
if channel is None:
channel = self.channel
@@ -217,9 +440,50 @@ def on_message_received(self, msg):
# Many interfaces start channel numbering at 0 which is invalid
channel += 1
- serialized = self.FORMAT_MESSAGE.format(channel=channel,
- id=arb_id,
- dtype=dtype,
- data=' '.join(data))
-
+ if msg.is_error_frame:
+ self.log_event(f"{channel} ErrorFrame", msg.timestamp)
+ return
+ if msg.is_remote_frame:
+ dtype = f"r {msg.dlc:x}" # New after v8.5
+ data: str = ""
+ else:
+ dtype = f"d {msg.dlc:x}"
+ data = msg.data.hex(" ").upper()
+ arb_id = f"{msg.arbitration_id:X}"
+ if msg.is_extended_id:
+ arb_id += "x"
+ if msg.is_fd:
+ flags = 0
+ flags |= 1 << 12
+ if msg.bitrate_switch:
+ flags |= 1 << 13
+ if msg.error_state_indicator:
+ flags |= 1 << 14
+ serialized = self.FORMAT_MESSAGE_FD.format(
+ channel=channel,
+ id=arb_id,
+ dir="Rx" if msg.is_rx else "Tx",
+ symbolic_name="",
+ brs=1 if msg.bitrate_switch else 0,
+ esi=1 if msg.error_state_indicator else 0,
+ dlc=len2dlc(msg.dlc),
+ data_length=len(msg.data),
+ data=data,
+ message_duration=0,
+ message_length=0,
+ flags=flags,
+ crc=0,
+ bit_timing_conf_arb=0,
+ bit_timing_conf_data=0,
+ bit_timing_conf_ext_arb=0,
+ bit_timing_conf_ext_data=0,
+ )
+ else:
+ serialized = self.FORMAT_MESSAGE.format(
+ channel=channel,
+ id=arb_id,
+ dir="Rx" if msg.is_rx else "Tx",
+ dtype=dtype,
+ data=data,
+ )
self.log_event(serialized, msg.timestamp)
diff --git a/can/io/blf.py b/can/io/blf.py
index 9de76aec4..77bd02fae 100644
--- a/can/io/blf.py
+++ b/can/io/blf.py
@@ -1,6 +1,3 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Implements support for BLF (Binary Logging Format) which is a proprietary
CAN log format from Vector Informatik GmbH (Germany).
@@ -15,28 +12,28 @@
objects types.
"""
-from __future__ import absolute_import
-
-import struct
-import zlib
import datetime
-import time
import logging
+import struct
+import time
+import zlib
+from collections.abc import Generator, Iterator
+from decimal import Decimal
+from typing import Any, BinaryIO, cast
+
+from ..message import Message
+from ..typechecking import StringPathLike
+from ..util import channel2int, dlc2len, len2dlc
+from .generic import BinaryIOMessageReader, BinaryIOMessageWriter
-from can.message import Message
-from can.listener import Listener
-from can.util import len2dlc, dlc2len, channel2int
-from .generic import BaseIOHandler
+TSystemTime = tuple[int, int, int, int, int, int, int, int]
class BLFParseError(Exception):
"""BLF file could not be parsed correctly."""
- pass
-LOG = logging.getLogger(__name__)
-# 0 = unknown, 2 = CANoe
-APPLICATION_ID = 5
+LOG = logging.getLogger(__name__)
# signature ("LOGG"), header size,
# application ID, application major, application minor, application build,
@@ -54,8 +51,8 @@ class BLFParseError(Exception):
# flags, client index, object version, timestamp
OBJ_HEADER_V1_STRUCT = struct.Struct(" TSystemTime:
if timestamp is None or timestamp < 631152000:
# Probably not a Unix timestamp
- return (0, 0, 0, 0, 0, 0, 0, 0)
- t = datetime.datetime.fromtimestamp(timestamp)
- return (t.year, t.month, t.isoweekday() % 7, t.day,
- t.hour, t.minute, t.second, int(round(t.microsecond / 1000.0)))
-
-
-def systemtime_to_timestamp(systemtime):
+ return 0, 0, 0, 0, 0, 0, 0, 0
+ t = datetime.datetime.fromtimestamp(round(timestamp, 3), tz=datetime.timezone.utc)
+ return (
+ t.year,
+ t.month,
+ t.isoweekday() % 7,
+ t.day,
+ t.hour,
+ t.minute,
+ t.second,
+ t.microsecond // 1000,
+ )
+
+
+def systemtime_to_timestamp(systemtime: TSystemTime) -> float:
try:
t = datetime.datetime(
- systemtime[0], systemtime[1], systemtime[3],
- systemtime[4], systemtime[5], systemtime[6], systemtime[7] * 1000)
- return time.mktime(t.timetuple()) + systemtime[7] / 1000.0
+ systemtime[0],
+ systemtime[1],
+ systemtime[3],
+ systemtime[4],
+ systemtime[5],
+ systemtime[6],
+ systemtime[7] * 1000,
+ tzinfo=datetime.timezone.utc,
+ )
+ return t.timestamp()
except ValueError:
return 0
-class BLFReader(BaseIOHandler):
+class BLFReader(BinaryIOMessageReader):
"""
Iterator of CAN messages from a Binary Logging File.
@@ -123,13 +146,17 @@ class BLFReader(BaseIOHandler):
silently ignored.
"""
- def __init__(self, file):
+ def __init__(
+ self,
+ file: StringPathLike | BinaryIO,
+ **kwargs: Any,
+ ) -> None:
"""
:param file: a path-like object or as file-like object to read from
If this is a file-like object, is has to opened in binary
read mode, not text read mode.
"""
- super(BLFReader, self).__init__(file, mode='rb')
+ super().__init__(file, mode="rb")
data = self.file.read(FILE_HEADER_STRUCT.size)
header = FILE_HEADER_STRUCT.unpack(data)
if header[0] != b"LOGG":
@@ -137,155 +164,299 @@ def __init__(self, file):
self.file_size = header[10]
self.uncompressed_size = header[11]
self.object_count = header[12]
- self.start_timestamp = systemtime_to_timestamp(header[14:22])
- self.stop_timestamp = systemtime_to_timestamp(header[22:30])
+ self.start_timestamp = systemtime_to_timestamp(
+ cast("TSystemTime", header[14:22])
+ )
+ self.stop_timestamp = systemtime_to_timestamp(
+ cast("TSystemTime", header[22:30])
+ )
# Read rest of header
self.file.read(header[1] - FILE_HEADER_STRUCT.size)
+ self._tail = b""
+ self._pos = 0
- def __iter__(self):
- tail = b""
+ def __iter__(self) -> Generator[Message, None, None]:
while True:
data = self.file.read(OBJ_HEADER_BASE_STRUCT.size)
if not data:
# EOF
break
- header = OBJ_HEADER_BASE_STRUCT.unpack(data)
- if header[0] != b"LOBJ":
+ signature, _, _, obj_size, obj_type = OBJ_HEADER_BASE_STRUCT.unpack(data)
+ if signature != b"LOBJ":
raise BLFParseError()
- obj_type = header[4]
- obj_data_size = header[3] - OBJ_HEADER_BASE_STRUCT.size
- obj_data = self.file.read(obj_data_size)
+ obj_data = self.file.read(obj_size - OBJ_HEADER_BASE_STRUCT.size)
# Read padding bytes
- self.file.read(obj_data_size % 4)
+ self.file.read(obj_size % 4)
if obj_type == LOG_CONTAINER:
- method, uncompressed_size = LOG_CONTAINER_STRUCT.unpack_from(
- obj_data)
- container_data = obj_data[LOG_CONTAINER_STRUCT.size:]
+ method, _ = LOG_CONTAINER_STRUCT.unpack_from(obj_data)
+ container_data = obj_data[LOG_CONTAINER_STRUCT.size :]
if method == NO_COMPRESSION:
data = container_data
elif method == ZLIB_DEFLATE:
- data = zlib.decompress(container_data, 15, uncompressed_size)
+ zobj = zlib.decompressobj()
+ data = zobj.decompress(container_data)
else:
# Unknown compression method
LOG.warning("Unknown compression method (%d)", method)
continue
-
- if tail:
- data = tail + data
- pos = 0
- while pos + OBJ_HEADER_BASE_STRUCT.size < len(data):
- header = OBJ_HEADER_BASE_STRUCT.unpack_from(data, pos)
- #print(header)
- if header[0] != b"LOBJ":
- raise BLFParseError()
-
- obj_size = header[3]
- # Calculate position of next object
- next_pos = pos + obj_size + (obj_size % 4)
- if next_pos > len(data):
- # Object continues in next log container
- break
- pos += OBJ_HEADER_BASE_STRUCT.size
-
- # Read rest of header
- header_version = header[2]
- if header_version == 1:
- flags, _, _, timestamp = OBJ_HEADER_V1_STRUCT.unpack_from(data, pos)
- pos += OBJ_HEADER_V1_STRUCT.size
- elif header_version == 2:
- flags, _, _, timestamp, _ = OBJ_HEADER_V2_STRUCT.unpack_from(data, pos)
- pos += OBJ_HEADER_V2_STRUCT.size
- else:
- # Unknown header version
- LOG.warning("Unknown object header version (%d)", header_version)
- pos = next_pos
- continue
-
- if flags == TIME_TEN_MICS:
- factor = 10 * 1e-6
- else:
- factor = 1e-9
- timestamp = timestamp * factor + self.start_timestamp
-
- obj_type = header[4]
- # Both CAN message types have the same starting content
- if obj_type in (CAN_MESSAGE, CAN_MESSAGE2):
- (channel, flags, dlc, can_id,
- can_data) = CAN_MSG_STRUCT.unpack_from(data, pos)
- msg = Message(timestamp=timestamp,
- arbitration_id=can_id & 0x1FFFFFFF,
- extended_id=bool(can_id & CAN_MSG_EXT),
- is_remote_frame=bool(flags & REMOTE_FLAG),
- dlc=dlc,
- data=can_data[:dlc],
- channel=channel - 1)
- yield msg
- elif obj_type == CAN_FD_MESSAGE:
- (channel, flags, dlc, can_id, _, _, fd_flags,
- _, can_data) = CAN_FD_MSG_STRUCT.unpack_from(data, pos)
- length = dlc2len(dlc)
- msg = Message(timestamp=timestamp,
- arbitration_id=can_id & 0x1FFFFFFF,
- extended_id=bool(can_id & CAN_MSG_EXT),
- is_remote_frame=bool(flags & REMOTE_FLAG),
- is_fd=bool(fd_flags & EDL),
- bitrate_switch=bool(fd_flags & BRS),
- error_state_indicator=bool(fd_flags & ESI),
- dlc=length,
- data=can_data[:length],
- channel=channel - 1)
- yield msg
- elif obj_type == CAN_ERROR_EXT:
- (channel, _, _, _, _, dlc, _, can_id, _,
- can_data) = CAN_ERROR_EXT_STRUCT.unpack_from(data, pos)
- msg = Message(timestamp=timestamp,
- is_error_frame=True,
- extended_id=bool(can_id & CAN_MSG_EXT),
- arbitration_id=can_id & 0x1FFFFFFF,
- dlc=dlc,
- data=can_data[:dlc],
- channel=channel - 1)
- yield msg
-
- pos = next_pos
-
- # save the remaining data that could not be processed
- tail = data[pos:]
-
+ yield from self._parse_container(data)
self.stop()
+ def _parse_container(self, data: bytes) -> Iterator[Message]:
+ if self._tail:
+ data = b"".join((self._tail, data))
+ try:
+ yield from self._parse_data(data)
+ except struct.error:
+ # There was not enough data in the container to unpack a struct
+ pass
+ # Save the remaining data that could not be processed
+ self._tail = data[self._pos :]
+
+ def _parse_data(self, data: bytes) -> Iterator[Message]:
+ """Optimized inner loop by making local copies of global variables
+ and class members and hardcoding some values."""
+ unpack_obj_header_base = OBJ_HEADER_BASE_STRUCT.unpack_from
+ obj_header_base_size = OBJ_HEADER_BASE_STRUCT.size
+ unpack_obj_header_v1 = OBJ_HEADER_V1_STRUCT.unpack_from
+ obj_header_v1_size = OBJ_HEADER_V1_STRUCT.size
+ unpack_obj_header_v2 = OBJ_HEADER_V2_STRUCT.unpack_from
+ obj_header_v2_size = OBJ_HEADER_V2_STRUCT.size
+ unpack_can_msg = CAN_MSG_STRUCT.unpack_from
+ unpack_can_fd_msg = CAN_FD_MSG_STRUCT.unpack_from
+ unpack_can_fd_64_msg = CAN_FD_MSG_64_STRUCT.unpack_from
+ can_fd_64_msg_size = CAN_FD_MSG_64_STRUCT.size
+ unpack_can_error_ext = CAN_ERROR_EXT_STRUCT.unpack_from
+
+ start_timestamp = self.start_timestamp
+ max_pos = len(data)
+ pos = 0
+
+ # Loop until a struct unpack raises an exception
+ while True:
+ self._pos = pos
+ # Find next object after padding (depends on object type)
+ try:
+ pos = data.index(b"LOBJ", pos, pos + 8)
+ except ValueError:
+ if pos + 8 > max_pos:
+ # Not enough data in container
+ return
+ raise BLFParseError("Could not find next object") from None
+ header = unpack_obj_header_base(data, pos)
+ # print(header)
+ signature, header_size, header_version, obj_size, obj_type = header
+ if signature != b"LOBJ":
+ raise BLFParseError()
-class BLFWriter(BaseIOHandler, Listener):
+ # Calculate position of next object
+ next_pos = pos + obj_size
+ if next_pos > max_pos:
+ # This object continues in the next container
+ return
+ pos += obj_header_base_size
+
+ # Read rest of header
+ if header_version == 1:
+ flags, _, _, timestamp = unpack_obj_header_v1(data, pos)
+ pos += obj_header_v1_size
+ elif header_version == 2:
+ flags, _, _, timestamp = unpack_obj_header_v2(data, pos)
+ pos += obj_header_v2_size
+ else:
+ LOG.warning("Unknown object header version (%d)", header_version)
+ pos = next_pos
+ continue
+
+ # Calculate absolute timestamp in seconds
+ factor = TIME_TEN_MICS_FACTOR if flags == 1 else TIME_ONE_NANS_FACTOR
+ timestamp = float(Decimal(timestamp) * factor) + start_timestamp
+
+ if obj_type in (CAN_MESSAGE, CAN_MESSAGE2):
+ channel, flags, dlc, can_id, can_data = unpack_can_msg(data, pos)
+ yield Message(
+ timestamp=timestamp,
+ arbitration_id=can_id & 0x1FFFFFFF,
+ is_extended_id=bool(can_id & CAN_MSG_EXT),
+ is_remote_frame=bool(flags & REMOTE_FLAG),
+ is_rx=not bool(flags & DIR),
+ dlc=dlc,
+ data=can_data[:dlc],
+ channel=channel - 1,
+ )
+ elif obj_type == CAN_ERROR_EXT:
+ members = unpack_can_error_ext(data, pos)
+ channel = members[0]
+ dlc = members[5]
+ can_id = members[7]
+ can_data = members[9]
+ yield Message(
+ timestamp=timestamp,
+ is_error_frame=True,
+ is_extended_id=bool(can_id & CAN_MSG_EXT),
+ arbitration_id=can_id & 0x1FFFFFFF,
+ dlc=dlc,
+ data=can_data[:dlc],
+ channel=channel - 1,
+ )
+ elif obj_type == CAN_FD_MESSAGE:
+ members = unpack_can_fd_msg(data, pos)
+ (
+ channel,
+ flags,
+ dlc,
+ can_id,
+ _,
+ _,
+ fd_flags,
+ valid_bytes,
+ can_data,
+ ) = members
+ yield Message(
+ timestamp=timestamp,
+ arbitration_id=can_id & 0x1FFFFFFF,
+ is_extended_id=bool(can_id & CAN_MSG_EXT),
+ is_remote_frame=bool(flags & REMOTE_FLAG),
+ is_fd=bool(fd_flags & 0x1),
+ is_rx=not bool(flags & DIR),
+ bitrate_switch=bool(fd_flags & 0x2),
+ error_state_indicator=bool(fd_flags & 0x4),
+ dlc=dlc2len(dlc),
+ data=can_data[:valid_bytes],
+ channel=channel - 1,
+ )
+ elif obj_type == CAN_FD_MESSAGE_64:
+ (
+ channel,
+ dlc,
+ valid_bytes,
+ _,
+ can_id,
+ _,
+ fd_flags,
+ _,
+ _,
+ _,
+ _,
+ _,
+ direction,
+ ext_data_offset,
+ _,
+ ) = unpack_can_fd_64_msg(data, pos)
+
+ # :issue:`1905`: `valid_bytes` can be higher than the actually available data.
+ # Add zero-byte padding to mimic behavior of CANoe and binlog.dll.
+ data_field_length = min(
+ valid_bytes,
+ (ext_data_offset or obj_size) - header_size - can_fd_64_msg_size,
+ )
+ msg_data_offset = pos + can_fd_64_msg_size
+ msg_data = data[msg_data_offset : msg_data_offset + data_field_length]
+ msg_data = msg_data.ljust(valid_bytes, b"\x00")
+
+ yield Message(
+ timestamp=timestamp,
+ arbitration_id=can_id & 0x1FFFFFFF,
+ is_extended_id=bool(can_id & CAN_MSG_EXT),
+ is_remote_frame=bool(fd_flags & 0x0010),
+ is_fd=bool(fd_flags & 0x1000),
+ is_rx=not direction,
+ bitrate_switch=bool(fd_flags & 0x2000),
+ error_state_indicator=bool(fd_flags & 0x4000),
+ dlc=dlc2len(dlc),
+ data=msg_data,
+ channel=channel - 1,
+ )
+
+ pos = next_pos
+
+
+class BLFWriter(BinaryIOMessageWriter):
"""
Logs CAN data to a Binary Logging File compatible with Vector's tools.
"""
#: Max log container size of uncompressed data
- MAX_CACHE_SIZE = 128 * 1024
-
- #: ZLIB compression level
- COMPRESSION_LEVEL = 9
-
- def __init__(self, file, channel=1):
+ max_container_size = 128 * 1024
+
+ #: Application identifier for the log writer
+ application_id = 5
+
+ def __init__(
+ self,
+ file: StringPathLike | BinaryIO,
+ append: bool = False,
+ channel: int = 1,
+ compression_level: int = -1,
+ **kwargs: Any,
+ ) -> None:
"""
:param file: a path-like object or as file-like object to write to
- If this is a file-like object, is has to opened in binary
- write mode, not text write mode.
+ If this is a file-like object, is has to opened in mode "wb+".
+ :param channel:
+ Default channel to log as if not specified by the interface.
+ :param append:
+ Append messages to an existing log file.
+ :param compression_level:
+ An integer from 0 to 9 or -1 controlling the level of compression.
+ 1 (Z_BEST_SPEED) is fastest and produces the least compression.
+ 9 (Z_BEST_COMPRESSION) is slowest and produces the most.
+ 0 means that data will be stored without processing.
+ The default value is -1 (Z_DEFAULT_COMPRESSION).
+ Z_DEFAULT_COMPRESSION represents a default compromise between
+ speed and compression (currently equivalent to level 6).
"""
- super(BLFWriter, self).__init__(file, mode='wb')
+ try:
+ super().__init__(file, mode="rb+" if append else "wb")
+ except FileNotFoundError:
+ # Trying to append to a non-existing file, create a new one
+ append = False
+ super().__init__(file, mode="wb")
+ assert self.file is not None
self.channel = channel
- # Header will be written after log is done
- self.file.write(b"\x00" * FILE_HEADER_SIZE)
- self.cache = []
- self.cache_size = 0
- self.count_of_objects = 0
- self.uncompressed_size = FILE_HEADER_SIZE
- self.start_timestamp = None
- self.stop_timestamp = None
-
- def on_message_received(self, msg):
+ self.compression_level = compression_level
+ self._buffer: list[bytes] = []
+ self._buffer_size = 0
+ # If max container size is located in kwargs, then update the instance
+ if kwargs.get("max_container_size", False):
+ self.max_container_size = kwargs["max_container_size"]
+ if append:
+ # Parse file header
+ data = self.file.read(FILE_HEADER_STRUCT.size)
+ header = FILE_HEADER_STRUCT.unpack(data)
+ if header[0] != b"LOGG":
+ raise BLFParseError("Unexpected file format")
+ self.uncompressed_size = header[11]
+ self.object_count = header[12]
+ self.start_timestamp: float | None = systemtime_to_timestamp(
+ cast("TSystemTime", header[14:22])
+ )
+ self.stop_timestamp: float | None = systemtime_to_timestamp(
+ cast("TSystemTime", header[22:30])
+ )
+ # Jump to the end of the file
+ self.file.seek(0, 2)
+ else:
+ self.object_count = 0
+ self.uncompressed_size = FILE_HEADER_SIZE
+ self.start_timestamp = None
+ self.stop_timestamp = None
+ # Write a default header which will be updated when stopped
+ self._write_header(FILE_HEADER_SIZE)
+
+ def _write_header(self, filesize: int) -> None:
+ header = [b"LOGG", FILE_HEADER_SIZE, self.application_id, 0, 0, 0, 2, 6, 8, 1]
+ # The meaning of "count of objects read" is unknown
+ header.extend([filesize, self.uncompressed_size, self.object_count, 0])
+ header.extend(timestamp_to_systemtime(self.start_timestamp))
+ header.extend(timestamp_to_systemtime(self.stop_timestamp))
+ self.file.write(FILE_HEADER_STRUCT.pack(*header))
+ # Pad to header size
+ self.file.write(b"\x00" * (FILE_HEADER_SIZE - FILE_HEADER_STRUCT.size))
+
+ def on_message_received(self, msg: Message) -> None:
channel = channel2int(msg.channel)
if channel is None:
channel = self.channel
@@ -294,22 +465,26 @@ def on_message_received(self, msg):
channel += 1
arb_id = msg.arbitration_id
- if msg.id_type:
+ if msg.is_extended_id:
arb_id |= CAN_MSG_EXT
flags = REMOTE_FLAG if msg.is_remote_frame else 0
- data = bytes(msg.data)
+ if not msg.is_rx:
+ flags |= DIR
+ can_data = bytes(msg.data)
if msg.is_error_frame:
- data = CAN_ERROR_EXT_STRUCT.pack(channel,
- 0, # length
- 0, # flags
- 0, # ecc
- 0, # position
- len2dlc(msg.dlc),
- 0, # frame length
- arb_id,
- 0, # ext flags
- data)
+ data = CAN_ERROR_EXT_STRUCT.pack(
+ channel,
+ 0, # length
+ 0, # flags
+ 0, # ecc
+ 0, # position
+ len2dlc(msg.dlc),
+ 0, # frame length
+ arb_id,
+ 0, # ext flags
+ can_data,
+ )
self._add_object(CAN_ERROR_EXT, data, msg.timestamp)
elif msg.is_fd:
fd_flags = EDL
@@ -317,14 +492,23 @@ def on_message_received(self, msg):
fd_flags |= BRS
if msg.error_state_indicator:
fd_flags |= ESI
- data = CAN_FD_MSG_STRUCT.pack(channel, flags, len2dlc(msg.dlc),
- arb_id, 0, 0, fd_flags, msg.dlc, data)
+ data = CAN_FD_MSG_STRUCT.pack(
+ channel,
+ flags,
+ len2dlc(msg.dlc),
+ arb_id,
+ 0,
+ 0,
+ fd_flags,
+ len(can_data),
+ can_data,
+ )
self._add_object(CAN_FD_MESSAGE, data, msg.timestamp)
else:
- data = CAN_MSG_STRUCT.pack(channel, flags, msg.dlc, arb_id, data)
+ data = CAN_MSG_STRUCT.pack(channel, flags, msg.dlc, arb_id, can_data)
self._add_object(CAN_MESSAGE, data, msg.timestamp)
- def log_event(self, text, timestamp=None):
+ def log_event(self, text: str, timestamp: float | None = None) -> None:
"""Add an arbitrary message to the log file as a global marker.
:param str text:
@@ -335,82 +519,92 @@ def log_event(self, text, timestamp=None):
"""
try:
# Only works on Windows
- text = text.encode("mbcs")
+ encoded = text.encode("mbcs")
except LookupError:
- text = text.encode("ascii")
+ encoded = text.encode("ascii")
comment = b"Added by python-can"
marker = b"python-can"
data = GLOBAL_MARKER_STRUCT.pack(
- 0, 0xFFFFFF, 0xFF3300, 0, len(text), len(marker), len(comment))
- self._add_object(GLOBAL_MARKER, data + text + marker + comment, timestamp)
+ 0, 0xFFFFFF, 0xFF3300, 0, len(encoded), len(marker), len(comment)
+ )
+ self._add_object(GLOBAL_MARKER, data + encoded + marker + comment, timestamp)
- def _add_object(self, obj_type, data, timestamp=None):
+ def _add_object(
+ self, obj_type: int, data: bytes, timestamp: float | None = None
+ ) -> None:
if timestamp is None:
timestamp = self.stop_timestamp or time.time()
if self.start_timestamp is None:
- self.start_timestamp = timestamp
+ # Save start timestamp using the same precision as the BLF format
+ # Truncating to milliseconds to avoid rounding errors when calculating
+ # the timestamp difference
+ self.start_timestamp = int(timestamp * 1000) / 1000
self.stop_timestamp = timestamp
timestamp = int((timestamp - self.start_timestamp) * 1e9)
header_size = OBJ_HEADER_BASE_STRUCT.size + OBJ_HEADER_V1_STRUCT.size
obj_size = header_size + len(data)
base_header = OBJ_HEADER_BASE_STRUCT.pack(
- b"LOBJ", header_size, 1, obj_size, obj_type)
+ b"LOBJ", header_size, 1, obj_size, obj_type
+ )
obj_header = OBJ_HEADER_V1_STRUCT.pack(TIME_ONE_NANS, 0, 0, max(timestamp, 0))
- self.cache.append(base_header)
- self.cache.append(obj_header)
- self.cache.append(data)
+ self._buffer.append(base_header)
+ self._buffer.append(obj_header)
+ self._buffer.append(data)
padding_size = len(data) % 4
if padding_size:
- self.cache.append(b"\x00" * padding_size)
+ self._buffer.append(b"\x00" * padding_size)
- self.cache_size += obj_size + padding_size
- self.count_of_objects += 1
- if self.cache_size >= self.MAX_CACHE_SIZE:
+ self._buffer_size += obj_size + padding_size
+ self.object_count += 1
+ if self._buffer_size >= self.max_container_size:
self._flush()
- def _flush(self):
- """Compresses and writes data in the cache to file."""
+ def _flush(self) -> None:
+ """Compresses and writes data in the buffer to file."""
if self.file.closed:
return
- cache = b"".join(self.cache)
- if not cache:
+ buffer = b"".join(self._buffer)
+ if not buffer:
# Nothing to write
return
- uncompressed_data = cache[:self.MAX_CACHE_SIZE]
- # Save data that comes after max size to next round
- tail = cache[self.MAX_CACHE_SIZE:]
- self.cache = [tail]
- self.cache_size = len(tail)
- compressed_data = zlib.compress(uncompressed_data,
- self.COMPRESSION_LEVEL)
- obj_size = (OBJ_HEADER_V1_STRUCT.size + LOG_CONTAINER_STRUCT.size +
- len(compressed_data))
+ uncompressed_data = memoryview(buffer)[: self.max_container_size]
+ # Save data that comes after max size to next container
+ tail = buffer[self.max_container_size :]
+ self._buffer = [tail]
+ self._buffer_size = len(tail)
+ if not self.compression_level:
+ data: "bytes | memoryview[int]" = uncompressed_data # noqa: UP037
+ method = NO_COMPRESSION
+ else:
+ data = zlib.compress(uncompressed_data, self.compression_level)
+ method = ZLIB_DEFLATE
+ obj_size = OBJ_HEADER_BASE_STRUCT.size + LOG_CONTAINER_STRUCT.size + len(data)
base_header = OBJ_HEADER_BASE_STRUCT.pack(
- b"LOBJ", OBJ_HEADER_BASE_STRUCT.size, 1, obj_size, LOG_CONTAINER)
- container_header = LOG_CONTAINER_STRUCT.pack(
- ZLIB_DEFLATE, len(uncompressed_data))
+ b"LOBJ", OBJ_HEADER_BASE_STRUCT.size, 1, obj_size, LOG_CONTAINER
+ )
+ container_header = LOG_CONTAINER_STRUCT.pack(method, len(uncompressed_data))
self.file.write(base_header)
self.file.write(container_header)
- self.file.write(compressed_data)
+ self.file.write(data)
# Write padding bytes
self.file.write(b"\x00" * (obj_size % 4))
- self.uncompressed_size += OBJ_HEADER_V1_STRUCT.size + LOG_CONTAINER_STRUCT.size
+ self.uncompressed_size += OBJ_HEADER_BASE_STRUCT.size
+ self.uncompressed_size += LOG_CONTAINER_STRUCT.size
self.uncompressed_size += len(uncompressed_data)
- def stop(self):
+ def file_size(self) -> int:
+ """Return an estimate of the current file size in bytes."""
+ return self.file.tell() + self._buffer_size
+
+ def stop(self) -> None:
"""Stops logging and closes the file."""
self._flush()
- filesize = self.file.tell()
- super(BLFWriter, self).stop()
-
- # Write header in the beginning of the file
- header = [b"LOGG", FILE_HEADER_SIZE,
- APPLICATION_ID, 0, 0, 0, 2, 6, 8, 1]
- # The meaning of "count of objects read" is unknown
- header.extend([filesize, self.uncompressed_size,
- self.count_of_objects, 0])
- header.extend(timestamp_to_systemtime(self.start_timestamp))
- header.extend(timestamp_to_systemtime(self.stop_timestamp))
- with open(self.file.name, "r+b") as f:
- f.write(FILE_HEADER_STRUCT.pack(*header))
+ if self.file.seekable():
+ filesize = self.file.tell()
+ # Write header in the beginning of the file
+ self.file.seek(0)
+ self._write_header(filesize)
+ else:
+ LOG.error("Could not write BLF header since file is not seekable")
+ super().stop()
diff --git a/can/io/canutils.py b/can/io/canutils.py
index f3b436b13..800125b73 100644
--- a/can/io/canutils.py
+++ b/can/io/canutils.py
@@ -1,32 +1,30 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
This module works with CAN data in ASCII log files (*.log).
It is is compatible with "candump -L" from the canutils program
(https://github.com/linux-can/can-utils).
"""
-from __future__ import absolute_import, division
-
-import time
-import datetime
import logging
+from collections.abc import Generator
+from typing import Any, TextIO
from can.message import Message
-from can.listener import Listener
-from .generic import BaseIOHandler
+from ..typechecking import StringPathLike
+from .generic import TextIOMessageReader, TextIOMessageWriter
+
+log = logging.getLogger("can.io.canutils")
-log = logging.getLogger('can.io.canutils')
+CAN_MSG_EXT = 0x80000000
+CAN_ERR_FLAG = 0x20000000
+CAN_ERR_BUSERROR = 0x00000080
+CAN_ERR_DLC = 8
-CAN_MSG_EXT = 0x80000000
-CAN_ERR_FLAG = 0x20000000
-CAN_ERR_BUSERROR = 0x00000080
-CAN_ERR_DLC = 8
+CANFD_BRS = 0x01
+CANFD_ESI = 0x02
-class CanutilsLogReader(BaseIOHandler):
+class CanutilsLogReader(TextIOMessageReader):
"""
Iterator over CAN messages from a .log Logging File (candump -L).
@@ -36,60 +34,94 @@ class CanutilsLogReader(BaseIOHandler):
``(0.0) vcan0 001#8d00100100820100``
"""
- def __init__(self, file):
+ def __init__(
+ self,
+ file: StringPathLike | TextIO,
+ **kwargs: Any,
+ ) -> None:
"""
:param file: a path-like object or as file-like object to read from
If this is a file-like object, is has to opened in text
read mode, not binary read mode.
"""
- super(CanutilsLogReader, self).__init__(file, mode='r')
+ super().__init__(file, mode="r")
- def __iter__(self):
+ def __iter__(self) -> Generator[Message, None, None]:
for line in self.file:
-
# skip empty lines
temp = line.strip()
if not temp:
continue
- timestamp, channel, frame = temp.split()
- timestamp = float(timestamp[1:-1])
- canId, data = frame.split('#')
- if channel.isdigit():
- channel = int(channel)
-
- if len(canId) > 3:
- isExtended = True
+ channel_string: str
+ if temp[-2:].lower() in (" r", " t"):
+ timestamp_string, channel_string, frame, is_rx_string = temp.split()
+ is_rx = is_rx_string.strip().lower() == "r"
else:
- isExtended = False
- canId = int(canId, 16)
+ timestamp_string, channel_string, frame = temp.split()
+ is_rx = True
+ timestamp = float(timestamp_string[1:-1])
+ can_id_string, data = frame.split("#", maxsplit=1)
+
+ channel: int | str
+ if channel_string.isdigit():
+ channel = int(channel_string)
+ else:
+ channel = channel_string
+
+ is_extended = len(can_id_string) > 3
+ can_id = int(can_id_string, 16)
+
+ is_fd = False
+ brs = False
+ esi = False
+
+ if data and data[0] == "#":
+ is_fd = True
+ fd_flags = int(data[1])
+ brs = bool(fd_flags & CANFD_BRS)
+ esi = bool(fd_flags & CANFD_ESI)
+ data = data[2:]
+
+ if data and data[0].lower() == "r":
+ is_remote_frame = True
- if data and data[0].lower() == 'r':
- isRemoteFrame = True
if len(data) > 1:
dlc = int(data[1:])
else:
dlc = 0
+
+ data_bin = None
else:
- isRemoteFrame = False
+ is_remote_frame = False
dlc = len(data) // 2
- dataBin = bytearray()
+ data_bin = bytearray()
for i in range(0, len(data), 2):
- dataBin.append(int(data[i:(i + 2)], 16))
+ data_bin.append(int(data[i : (i + 2)], 16))
- if canId & CAN_ERR_FLAG and canId & CAN_ERR_BUSERROR:
+ if can_id & CAN_ERR_FLAG and can_id & CAN_ERR_BUSERROR:
msg = Message(timestamp=timestamp, is_error_frame=True)
else:
- msg = Message(timestamp=timestamp, arbitration_id=canId & 0x1FFFFFFF,
- extended_id=isExtended, is_remote_frame=isRemoteFrame,
- dlc=dlc, data=dataBin, channel=channel)
+ msg = Message(
+ timestamp=timestamp,
+ arbitration_id=can_id & 0x1FFFFFFF,
+ is_extended_id=is_extended,
+ is_remote_frame=is_remote_frame,
+ is_fd=is_fd,
+ is_rx=is_rx,
+ bitrate_switch=brs,
+ error_state_indicator=esi,
+ dlc=dlc,
+ data=data_bin,
+ channel=channel,
+ )
yield msg
self.stop()
-class CanutilsLogWriter(BaseIOHandler, Listener):
+class CanutilsLogWriter(TextIOMessageWriter):
"""Logs CAN data to an ASCII log file (.log).
This class is is compatible with "candump -L".
@@ -98,7 +130,13 @@ class CanutilsLogWriter(BaseIOHandler, Listener):
It the first message does not have a timestamp, it is set to zero.
"""
- def __init__(self, file, channel="vcan0", append=False):
+ def __init__(
+ self,
+ file: StringPathLike | TextIO,
+ channel: str = "vcan0",
+ append: bool = False,
+ **kwargs: Any,
+ ):
"""
:param file: a path-like object or as file-like object to write to
If this is a file-like object, is has to opened in text
@@ -108,16 +146,15 @@ def __init__(self, file, channel="vcan0", append=False):
:param bool append: if set to `True` messages are appended to
the file, else the file is truncated
"""
- mode = 'a' if append else 'w'
- super(CanutilsLogWriter, self).__init__(file, mode=mode)
+ super().__init__(file, mode="a" if append else "w")
self.channel = channel
- self.last_timestamp = None
+ self.last_timestamp: float | None = None
- def on_message_received(self, msg):
+ def on_message_received(self, msg: Message) -> None:
# this is the case for the very first message:
if self.last_timestamp is None:
- self.last_timestamp = (msg.timestamp or 0.0)
+ self.last_timestamp = msg.timestamp or 0.0
# figure out the correct timestamp
if msg.timestamp is None or msg.timestamp < self.last_timestamp:
@@ -126,19 +163,33 @@ def on_message_received(self, msg):
timestamp = msg.timestamp
channel = msg.channel if msg.channel is not None else self.channel
+ if isinstance(channel, int) or (isinstance(channel, str) and channel.isdigit()):
+ channel = f"can{channel}"
+
+ framestr = f"({timestamp:f}) {channel}"
if msg.is_error_frame:
- self.file.write("(%f) %s %08X#0000000000000000\n" % (timestamp, channel, CAN_ERR_FLAG | CAN_ERR_BUSERROR))
+ framestr += f" {CAN_ERR_FLAG | CAN_ERR_BUSERROR:08X}#"
+ elif msg.is_extended_id:
+ framestr += f" {msg.arbitration_id:08X}#"
+ else:
+ framestr += f" {msg.arbitration_id:03X}#"
- elif msg.is_remote_frame:
- if msg.is_extended_id:
- self.file.write("(%f) %s %08X#R\n" % (timestamp, channel, msg.arbitration_id))
- else:
- self.file.write("(%f) %s %03X#R\n" % (timestamp, channel, msg.arbitration_id))
+ if msg.is_error_frame:
+ eol = "\n"
+ else:
+ eol = " R\n" if msg.is_rx else " T\n"
+ if msg.is_remote_frame:
+ framestr += f"R{eol}"
else:
- data = ["{:02X}".format(byte) for byte in msg.data]
- if msg.is_extended_id:
- self.file.write("(%f) %s %08X#%s\n" % (timestamp, channel, msg.arbitration_id, ''.join(data)))
- else:
- self.file.write("(%f) %s %03X#%s\n" % (timestamp, channel, msg.arbitration_id, ''.join(data)))
+ if msg.is_fd:
+ fd_flags = 0
+ if msg.bitrate_switch:
+ fd_flags |= CANFD_BRS
+ if msg.error_state_indicator:
+ fd_flags |= CANFD_ESI
+ framestr += f"#{fd_flags:X}"
+ framestr += f"{msg.data.hex().upper()}{eol}"
+
+ self.file.write(framestr)
diff --git a/can/io/csv.py b/can/io/csv.py
index e108679b8..0c8ba02a4 100644
--- a/can/io/csv.py
+++ b/can/io/csv.py
@@ -1,8 +1,5 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
-This module contains handling for CSV (comma seperated values) files.
+This module contains handling for CSV (comma separated values) files.
TODO: CAN FD messages are not yet supported.
@@ -12,15 +9,64 @@
of a CSV file.
"""
-from __future__ import absolute_import
-
-from base64 import b64encode, b64decode
+from base64 import b64decode, b64encode
+from collections.abc import Generator
+from typing import Any, TextIO
from can.message import Message
-from can.listener import Listener
-from .generic import BaseIOHandler
-class CSVWriter(BaseIOHandler, Listener):
+from ..typechecking import StringPathLike
+from .generic import TextIOMessageReader, TextIOMessageWriter
+
+
+class CSVReader(TextIOMessageReader):
+ """Iterator over CAN messages from a .csv file that was
+ generated by :class:`~can.CSVWriter` or that uses the same
+ format as described there. Assumes that there is a header
+ and thus skips the first line.
+
+ Any line separator is accepted.
+ """
+
+ def __init__(
+ self,
+ file: StringPathLike | TextIO,
+ **kwargs: Any,
+ ) -> None:
+ """
+ :param file: a path-like object or as file-like object to read from
+ If this is a file-like object, is has to opened in text
+ read mode, not binary read mode.
+ """
+ super().__init__(file, mode="r")
+
+ def __iter__(self) -> Generator[Message, None, None]:
+ # skip the header line
+ try:
+ next(self.file)
+ except StopIteration:
+ # don't crash on a file with only a header
+ return
+
+ for line in self.file:
+ timestamp, arbitration_id, extended, remote, error, dlc, data = line.split(
+ ","
+ )
+
+ yield Message(
+ timestamp=float(timestamp),
+ is_remote_frame=(remote == "1"),
+ is_extended_id=(extended == "1"),
+ is_error_frame=(error == "1"),
+ arbitration_id=int(arbitration_id, base=16),
+ dlc=int(dlc),
+ data=b64decode(data),
+ )
+
+ self.stop()
+
+
+class CSVWriter(TextIOMessageWriter):
"""Writes a comma separated text file with a line for
each message. Includes a header line.
@@ -38,73 +84,41 @@ class CSVWriter(BaseIOHandler, Listener):
data base64 encoded WzQyLCA5XQ==
================ ======================= ===============
- Each line is terminated with a platform specific line seperator.
+ Each line is terminated with a platform specific line separator.
"""
- def __init__(self, file, append=False):
+ def __init__(
+ self,
+ file: StringPathLike | TextIO,
+ append: bool = False,
+ **kwargs: Any,
+ ) -> None:
"""
- :param file: a path-like object or as file-like object to write to
- If this is a file-like object, is has to opened in text
+ :param file: a path-like object or a file-like object to write to.
+ If this is a file-like object, is has to open in text
write mode, not binary write mode.
:param bool append: if set to `True` messages are appended to
the file and no header line is written, else
the file is truncated and starts with a newly
written header line
"""
- mode = 'a' if append else 'w'
- super(CSVWriter, self).__init__(file, mode=mode)
+ super().__init__(file, mode="a" if append else "w")
# Write a header row
if not append:
self.file.write("timestamp,arbitration_id,extended,remote,error,dlc,data\n")
- def on_message_received(self, msg):
- row = ','.join([
- repr(msg.timestamp), # cannot use str() here because that is rounding
- hex(msg.arbitration_id),
- '1' if msg.id_type else '0',
- '1' if msg.is_remote_frame else '0',
- '1' if msg.is_error_frame else '0',
- str(msg.dlc),
- b64encode(msg.data).decode('utf8')
- ])
+ def on_message_received(self, msg: Message) -> None:
+ row = ",".join(
+ [
+ repr(msg.timestamp), # cannot use str() here because that is rounding
+ hex(msg.arbitration_id),
+ "1" if msg.is_extended_id else "0",
+ "1" if msg.is_remote_frame else "0",
+ "1" if msg.is_error_frame else "0",
+ str(msg.dlc),
+ b64encode(msg.data).decode("utf8"),
+ ]
+ )
self.file.write(row)
- self.file.write('\n')
-
-
-class CSVReader(BaseIOHandler):
- """Iterator over CAN messages from a .csv file that was
- generated by :class:`~can.CSVWriter` or that uses the same
- format as described there. Assumes that there is a header
- and thus skips the first line.
-
- Any line seperator is accepted.
- """
-
- def __init__(self, file):
- """
- :param file: a path-like object or as file-like object to read from
- If this is a file-like object, is has to opened in text
- read mode, not binary read mode.
- """
- super(CSVReader, self).__init__(file, mode='r')
-
- def __iter__(self):
- # skip the header line
- next(self.file)
-
- for line in self.file:
-
- timestamp, arbitration_id, extended, remote, error, dlc, data = line.split(',')
-
- yield Message(
- timestamp=float(timestamp),
- is_remote_frame=(remote == '1'),
- extended_id=(extended == '1'),
- is_error_frame=(error == '1'),
- arbitration_id=int(arbitration_id, base=16),
- dlc=int(dlc),
- data=b64decode(data),
- )
-
- self.stop()
+ self.file.write("\n")
diff --git a/can/io/generic.py b/can/io/generic.py
index 4f278d223..bda4e1cce 100644
--- a/can/io/generic.py
+++ b/can/io/generic.py
@@ -1,51 +1,258 @@
-#!/usr/bin/env python
-# coding: utf-8
+"""This module provides abstract base classes for CAN message reading and writing operations
+to various file formats.
+.. note::
+ All classes in this module are abstract and should be subclassed to implement
+ specific file format handling.
"""
-Contains a generic class for file IO.
-"""
-from abc import ABCMeta, abstractmethod
+import locale
+import os
+from abc import ABC, abstractmethod
+from collections.abc import Iterable
+from contextlib import AbstractContextManager
+from io import BufferedIOBase, TextIOWrapper
+from pathlib import Path
+from types import TracebackType
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ BinaryIO,
+ Generic,
+ Literal,
+ TextIO,
+ TypeVar,
+)
+
+from typing_extensions import Self
+
+from ..listener import Listener
+from ..message import Message
+from ..typechecking import FileLike, StringPathLike
+
+if TYPE_CHECKING:
+ from _typeshed import (
+ OpenBinaryModeReading,
+ OpenBinaryModeUpdating,
+ OpenBinaryModeWriting,
+ OpenTextModeReading,
+ OpenTextModeUpdating,
+ OpenTextModeWriting,
+ )
+
-from can import Listener
+#: type parameter used in generic classes :class:`MessageReader` and :class:`MessageWriter`
+_IoTypeVar = TypeVar("_IoTypeVar", bound=FileLike)
-class BaseIOHandler(object):
- """A generic file handler that can be used for reading and writing.
+class MessageWriter(AbstractContextManager["MessageWriter"], Listener, ABC):
+ """Abstract base class for all CAN message writers.
- Can be used as a context manager.
+ This class serves as a foundation for implementing different message writer formats.
+ It combines context manager capabilities with the message listener interface.
- :attr file-like file:
- the file-like object that is kept internally, or None if none
- was opened
+ :param file: Path-like object or string representing the output file location
+ :param kwargs: Additional keyword arguments for specific writer implementations
"""
- __metaclass__ = ABCMeta
+ @abstractmethod
+ def __init__(self, file: StringPathLike, **kwargs: Any) -> None:
+ pass
- def __init__(self, file, mode='rt'):
- """
- :param file: a path-like object to open a file, a file-like object
- to be used as a file or `None` to not use a file at all
- :param str mode: the mode that should be used to open the file, see
- :func:`builtin.open`, ignored if *file* is `None`
+ @abstractmethod
+ def stop(self) -> None:
+ """Stop handling messages and cleanup any resources."""
+
+ def __enter__(self) -> Self:
+ """Enter the context manager."""
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> Literal[False]:
+ """Exit the context manager and ensure proper cleanup."""
+ self.stop()
+ return False
+
+
+class SizedMessageWriter(MessageWriter, ABC):
+ """Abstract base class for message writers that can report their file size.
+
+ This class extends :class:`MessageWriter` with the ability to determine the size
+ of the output file.
+ """
+
+ @abstractmethod
+ def file_size(self) -> int:
+ """Get the current size of the output file in bytes.
+
+ :return: The size of the file in bytes
+ :rtype: int
"""
- if file is None or (hasattr(file, 'read') and hasattr(file, 'write')):
- # file is None or some file-like object
+
+
+class FileIOMessageWriter(SizedMessageWriter, Generic[_IoTypeVar]):
+ """Base class for writers that operate on file descriptors.
+
+ This class provides common functionality for writers that work with file objects.
+
+ :param file: A path-like object or file object to write to
+ :param kwargs: Additional keyword arguments for specific writer implementations
+
+ :ivar file: The file object being written to
+ """
+
+ file: _IoTypeVar
+
+ @abstractmethod
+ def __init__(self, file: StringPathLike | _IoTypeVar, **kwargs: Any) -> None:
+ pass
+
+ def stop(self) -> None:
+ """Close the file and stop writing."""
+ self.file.close()
+
+ def file_size(self) -> int:
+ """Get the current file size."""
+ return self.file.tell()
+
+
+class TextIOMessageWriter(FileIOMessageWriter[TextIO | TextIOWrapper], ABC):
+ """Text-based message writer implementation.
+
+ :param file: Text file to write to
+ :param mode: File open mode for text operations
+ :param kwargs: Additional arguments like encoding
+ """
+
+ def __init__(
+ self,
+ file: StringPathLike | TextIO | TextIOWrapper,
+ mode: "OpenTextModeUpdating | OpenTextModeWriting" = "w",
+ **kwargs: Any,
+ ) -> None:
+ if isinstance(file, (str, os.PathLike)):
+ encoding: str = kwargs.get("encoding", locale.getpreferredencoding(False))
+ # pylint: disable=consider-using-with
+ self.file = Path(file).open(mode=mode, encoding=encoding)
+ else:
self.file = file
+
+
+class BinaryIOMessageWriter(FileIOMessageWriter[BinaryIO | BufferedIOBase], ABC):
+ """Binary file message writer implementation.
+
+ :param file: Binary file to write to
+ :param mode: File open mode for binary operations
+ :param kwargs: Additional implementation specific arguments
+ """
+
+ def __init__( # pylint: disable=unused-argument
+ self,
+ file: StringPathLike | BinaryIO | BufferedIOBase,
+ mode: "OpenBinaryModeUpdating | OpenBinaryModeWriting" = "wb",
+ **kwargs: Any,
+ ) -> None:
+ if isinstance(file, (str, os.PathLike)):
+ # pylint: disable=consider-using-with,unspecified-encoding
+ self.file = Path(file).open(mode=mode)
else:
- # file is some path-like object
- self.file = open(file, mode)
+ self.file = file
- # for multiple inheritance
- super(BaseIOHandler, self).__init__()
- def __enter__(self):
+class MessageReader(AbstractContextManager["MessageReader"], Iterable[Message], ABC):
+ """Abstract base class for all CAN message readers.
+
+ This class serves as a foundation for implementing different message reader formats.
+ It combines context manager capabilities with iteration interface.
+
+ :param file: Path-like object or string representing the input file location
+ :param kwargs: Additional keyword arguments for specific reader implementations
+ """
+
+ @abstractmethod
+ def __init__(self, file: StringPathLike, **kwargs: Any) -> None:
+ pass
+
+ @abstractmethod
+ def stop(self) -> None:
+ """Stop reading messages and cleanup any resources."""
+
+ def __enter__(self) -> Self:
return self
- def __exit__(self, *args):
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> Literal[False]:
self.stop()
+ return False
+
+
+class FileIOMessageReader(MessageReader, Generic[_IoTypeVar]):
+ """Base class for readers that operate on file descriptors.
+
+ This class provides common functionality for readers that work with file objects.
+
+ :param file: A path-like object or file object to read from
+ :param kwargs: Additional keyword arguments for specific reader implementations
+
+ :ivar file: The file object being read from
+ """
- def stop(self):
- if self.file is not None:
- # this also implies a flush()
- self.file.close()
+ file: _IoTypeVar
+
+ @abstractmethod
+ def __init__(self, file: StringPathLike | _IoTypeVar, **kwargs: Any) -> None:
+ pass
+
+ def stop(self) -> None:
+ self.file.close()
+
+
+class TextIOMessageReader(FileIOMessageReader[TextIO | TextIOWrapper], ABC):
+ """Text-based message reader implementation.
+
+ :param file: Text file to read from
+ :param mode: File open mode for text operations
+ :param kwargs: Additional arguments like encoding
+ """
+
+ def __init__(
+ self,
+ file: StringPathLike | TextIO | TextIOWrapper,
+ mode: "OpenTextModeReading" = "r",
+ **kwargs: Any,
+ ) -> None:
+ if isinstance(file, (str, os.PathLike)):
+ encoding: str = kwargs.get("encoding", locale.getpreferredencoding(False))
+ # pylint: disable=consider-using-with
+ self.file = Path(file).open(mode=mode, encoding=encoding)
+ else:
+ self.file = file
+
+
+class BinaryIOMessageReader(FileIOMessageReader[BinaryIO | BufferedIOBase], ABC):
+ """Binary file message reader implementation.
+
+ :param file: Binary file to read from
+ :param mode: File open mode for binary operations
+ :param kwargs: Additional implementation specific arguments
+ """
+
+ def __init__( # pylint: disable=unused-argument
+ self,
+ file: StringPathLike | BinaryIO | BufferedIOBase,
+ mode: "OpenBinaryModeReading" = "rb",
+ **kwargs: Any,
+ ) -> None:
+ if isinstance(file, (str, os.PathLike)):
+ # pylint: disable=consider-using-with,unspecified-encoding
+ self.file = Path(file).open(mode=mode)
+ else:
+ self.file = file
diff --git a/can/io/logger.py b/can/io/logger.py
old mode 100755
new mode 100644
index 9095c5898..5009c9756
--- a/can/io/logger.py
+++ b/can/io/logger.py
@@ -1,64 +1,410 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
See the :class:`Logger` class.
"""
-from __future__ import absolute_import
+import gzip
+import os
+import pathlib
+from abc import ABC, abstractmethod
+from collections.abc import Callable
+from datetime import datetime
+from types import TracebackType
+from typing import (
+ Any,
+ ClassVar,
+ Final,
+ Literal,
+)
-import logging
+from typing_extensions import Self
-from ..listener import Listener
-from .generic import BaseIOHandler
+from .._entry_points import read_entry_points
+from ..message import Message
+from ..typechecking import StringPathLike
from .asc import ASCWriter
from .blf import BLFWriter
from .canutils import CanutilsLogWriter
from .csv import CSVWriter
-from .sqlite import SqliteWriter
+from .generic import (
+ BinaryIOMessageWriter,
+ FileIOMessageWriter,
+ MessageWriter,
+ SizedMessageWriter,
+ TextIOMessageWriter,
+)
+from .mf4 import MF4Writer
from .printer import Printer
+from .sqlite import SqliteWriter
+from .trc import TRCWriter
+
+#: A map of file suffixes to their corresponding
+#: :class:`can.io.generic.MessageWriter` class
+MESSAGE_WRITERS: Final[dict[str, type[MessageWriter]]] = {
+ ".asc": ASCWriter,
+ ".blf": BLFWriter,
+ ".csv": CSVWriter,
+ ".db": SqliteWriter,
+ ".log": CanutilsLogWriter,
+ ".mf4": MF4Writer,
+ ".trc": TRCWriter,
+ ".txt": Printer,
+}
+
+
+def _update_writer_plugins() -> None:
+ """Update available message writer plugins from entry points."""
+ for entry_point in read_entry_points("can.io.message_writer"):
+ if entry_point.key in MESSAGE_WRITERS:
+ continue
-log = logging.getLogger("can.io.logger")
+ writer_class = entry_point.load()
+ if issubclass(writer_class, MessageWriter):
+ MESSAGE_WRITERS[entry_point.key] = writer_class
-class Logger(BaseIOHandler, Listener):
+def _get_logger_for_suffix(suffix: str) -> type[MessageWriter]:
+ try:
+ return MESSAGE_WRITERS[suffix]
+ except KeyError:
+ raise ValueError(
+ f'No write support for unknown log format "{suffix}"'
+ ) from None
+
+
+def _compress(filename: StringPathLike, **kwargs: Any) -> FileIOMessageWriter[Any]:
+ """
+ Return the suffix and io object of the decompressed file.
+ File will automatically recompress upon close.
"""
- Logs CAN messages to a file.
+ suffixes = pathlib.Path(filename).suffixes
+ if len(suffixes) != 2:
+ raise ValueError(
+ f"No write support for unknown log format \"{''.join(suffixes)}\""
+ ) from None
- The format is determined from the file format which can be one of:
- * .asc: :class:`can.ASCWriter`
+ real_suffix = suffixes[-2].lower()
+ if real_suffix in (".blf", ".db"):
+ raise ValueError(
+ f"The file type {real_suffix} is currently incompatible with gzip."
+ )
+ logger_type = _get_logger_for_suffix(real_suffix)
+ append = kwargs.get("append", False)
+
+ if issubclass(logger_type, BinaryIOMessageWriter):
+ return logger_type(
+ file=gzip.open(filename=filename, mode="ab" if append else "wb"), **kwargs
+ )
+
+ elif issubclass(logger_type, TextIOMessageWriter):
+ return logger_type(
+ file=gzip.open(filename=filename, mode="at" if append else "wt"), **kwargs
+ )
+
+ raise ValueError(
+ f"The file type {real_suffix} is currently incompatible with gzip."
+ )
+
+
+def Logger( # noqa: N802
+ filename: StringPathLike | None, **kwargs: Any
+) -> MessageWriter:
+ """Find and return the appropriate :class:`~can.io.generic.MessageWriter` instance
+ for a given file suffix.
+
+ The format is determined from the file suffix which can be one of:
+ * .asc :class:`can.ASCWriter`
* .blf :class:`can.BLFWriter`
* .csv: :class:`can.CSVWriter`
- * .db: :class:`can.SqliteWriter`
+ * .db :class:`can.SqliteWriter`
* .log :class:`can.CanutilsLogWriter`
- * other: :class:`can.Printer`
+ * .mf4 :class:`can.MF4Writer`
+ (optional, depends on `asammdf `_)
+ * .trc :class:`can.TRCWriter`
+ * .txt :class:`can.Printer`
+
+ Any of these formats can be used with gzip compression by appending
+ the suffix .gz (e.g. filename.asc.gz). However, third-party tools might not
+ be able to read these files.
+
+ The **filename** may also be *None*, to fall back to :class:`can.Printer`.
+
+ The log files may be incomplete until `stop()` is called due to buffering.
+
+ :param filename:
+ the filename/path of the file to write to,
+ may be a path-like object or None to
+ instantiate a :class:`~can.Printer`
+ :raises ValueError:
+ if the filename's suffix is of an unknown file type
.. note::
- This class itself is just a dispatcher, and any positional an keyword
+ This function itself is just a dispatcher, and any positional and keyword
arguments are passed on to the returned instance.
"""
- @staticmethod
- def __new__(cls, filename, *args, **kwargs):
+ if filename is None:
+ return Printer(**kwargs)
+
+ _update_writer_plugins()
+
+ suffix = pathlib.PurePath(filename).suffix.lower()
+ if suffix == ".gz":
+ return _compress(filename, **kwargs)
+
+ logger_type = _get_logger_for_suffix(suffix)
+ return logger_type(file=filename, **kwargs)
+
+
+class BaseRotatingLogger(MessageWriter, ABC):
+ """
+ Base class for rotating CAN loggers. This class is not meant to be
+ instantiated directly. Subclasses must implement the :meth:`should_rollover`
+ and :meth:`do_rollover` methods according to their rotation strategy.
+
+ The rotation behavior can be further customized by the user by setting
+ the :attr:`namer` and :attr:`rotator` attributes after instantiating the subclass.
+
+ These attributes as well as the methods :meth:`rotation_filename` and :meth:`rotate`
+ and the corresponding docstrings are carried over from the python builtin
+ :class:`~logging.handlers.BaseRotatingHandler`.
+
+ Subclasses must set the `_writer` attribute upon initialization.
+ """
+
+ _supported_formats: ClassVar[set[str]] = set()
+
+ #: If this attribute is set to a callable, the :meth:`~BaseRotatingLogger.rotation_filename`
+ #: method delegates to this callable. The parameters passed to the callable are
+ #: those passed to :meth:`~BaseRotatingLogger.rotation_filename`.
+ namer: Callable[[StringPathLike], StringPathLike] | None = None
+
+ #: If this attribute is set to a callable, the :meth:`~BaseRotatingLogger.rotate` method
+ #: delegates to this callable. The parameters passed to the callable are those
+ #: passed to :meth:`~BaseRotatingLogger.rotate`.
+ rotator: Callable[[StringPathLike, StringPathLike], None] | None = None
+
+ #: An integer counter to track the number of rollovers.
+ rollover_count: int = 0
+
+ def __init__(self, **kwargs: Any) -> None:
+ self.writer_kwargs = kwargs
+
+ @property
+ @abstractmethod
+ def writer(self) -> MessageWriter:
+ """This attribute holds an instance of a writer class which manages the actual file IO."""
+ raise NotImplementedError
+
+ def rotation_filename(self, default_name: StringPathLike) -> StringPathLike:
+ """Modify the filename of a log file when rotating.
+
+ This is provided so that a custom filename can be provided.
+ The default implementation calls the :attr:`namer` attribute of the
+ handler, if it's callable, passing the default name to
+ it. If the attribute isn't callable (the default is :obj:`None`), the name
+ is returned unchanged.
+
+ :param default_name:
+ The default name for the log file.
"""
- :type filename: str or None or path-like
- :param filename: the filename/path the file to write to,
- may be a path-like object if the target logger supports
- it, and may be None to instantiate a :class:`~can.Printer`
+ if not callable(self.namer):
+ return default_name
+
+ return self.namer(default_name) # pylint: disable=not-callable
+
+ def rotate(self, source: StringPathLike, dest: StringPathLike) -> None:
+ """When rotating, rotate the current log.
+ The default implementation calls the :attr:`rotator` attribute of the
+ handler, if it's callable, passing the `source` and `dest` arguments to
+ it. If the attribute isn't callable (the default is :obj:`None`), the source
+ is simply renamed to the destination.
+
+ :param source:
+ The source filename. This is normally the base
+ filename, e.g. `"test.log"`
+ :param dest:
+ The destination filename. This is normally
+ what the source is rotated to, e.g. `"test_#001.log"`.
"""
- if filename:
- if filename.endswith(".asc"):
- return ASCWriter(filename, *args, **kwargs)
- elif filename.endswith(".blf"):
- return BLFWriter(filename, *args, **kwargs)
- elif filename.endswith(".csv"):
- return CSVWriter(filename, *args, **kwargs)
- elif filename.endswith(".db"):
- return SqliteWriter(filename, *args, **kwargs)
- elif filename.endswith(".log"):
- return CanutilsLogWriter(filename, *args, **kwargs)
-
- # else:
- log.info('unknown file type "%s", falling pack to can.Printer', filename)
- return Printer(filename, *args, **kwargs)
+ if not callable(self.rotator):
+ if os.path.exists(source):
+ os.rename(source, dest)
+ else:
+ self.rotator(source, dest) # pylint: disable=not-callable
+
+ def on_message_received(self, msg: Message) -> None:
+ """This method is called to handle the given message.
+
+ :param msg:
+ the delivered message
+ """
+ if self.should_rollover(msg):
+ self.do_rollover()
+ self.rollover_count += 1
+
+ self.writer.on_message_received(msg)
+
+ def _get_new_writer(self, filename: StringPathLike) -> MessageWriter:
+ """Instantiate a new writer.
+
+ .. note::
+ The :attr:`self.writer` should be closed prior to calling this function.
+
+ :param filename:
+ Path-like object that specifies the location and name of the log file.
+ The log file format is defined by the suffix of `filename`.
+ :return:
+ An instance of a writer class.
+ """
+ suffixes = pathlib.Path(filename).suffixes
+ for suffix_length in range(len(suffixes), 0, -1):
+ suffix = "".join(suffixes[-suffix_length:]).lower()
+ if suffix not in self._supported_formats:
+ continue
+ logger = Logger(filename=filename, **self.writer_kwargs)
+ return logger
+
+ raise ValueError(
+ f'The log format of "{pathlib.Path(filename).name}" '
+ f"is not supported by {self.__class__.__name__}. "
+ f"{self.__class__.__name__} supports the following formats: "
+ f"{', '.join(self._supported_formats)}"
+ )
+
+ def stop(self) -> None:
+ """Stop handling new messages.
+
+ Carry out any final tasks to ensure
+ data is persisted and cleanup any open resources.
+ """
+ self.writer.stop()
+
+ def __enter__(self) -> Self:
+ return self
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> Literal[False]:
+ self.stop()
+ return False
+
+ @abstractmethod
+ def should_rollover(self, msg: Message) -> bool:
+ """Determine if the rollover conditions are met."""
+
+ @abstractmethod
+ def do_rollover(self) -> None:
+ """Perform rollover."""
+
+
+class SizedRotatingLogger(BaseRotatingLogger):
+ """Log CAN messages to a sequence of files with a given maximum size.
+
+ The logger creates a log file with the given `base_filename`. When the
+ size threshold is reached the current log file is closed and renamed
+ by adding a timestamp and the rollover count. A new log file is then
+ created and written to.
+
+ This behavior can be customized by setting the
+ :attr:`~can.io.BaseRotatingLogger.namer` and
+ :attr:`~can.io.BaseRotatingLogger.rotator`
+ attribute.
+
+ Example::
+
+ from can import Notifier, SizedRotatingLogger
+ from can.interfaces.vector import VectorBus
+
+ bus = VectorBus(channel=[0], app_name="CANape", fd=True)
+
+ logger = SizedRotatingLogger(
+ base_filename="my_logfile.asc",
+ max_bytes=5 * 1024 ** 2, # =5MB
+ )
+ logger.rollover_count = 23 # start counter at 23
+
+ notifier = Notifier(bus=bus, listeners=[logger])
+
+ The SizedRotatingLogger currently supports the formats
+ * .asc: :class:`can.ASCWriter`
+ * .blf :class:`can.BLFWriter`
+ * .csv: :class:`can.CSVWriter`
+ * .log :class:`can.CanutilsLogWriter`
+ * .txt :class:`can.Printer` (if pointing to a file)
+
+ .. note::
+ The :class:`can.SqliteWriter` is not supported yet.
+
+ The log files on disk may be incomplete due to buffering until
+ :meth:`~can.Listener.stop` is called.
+ """
+
+ _supported_formats: ClassVar[set[str]] = {".asc", ".blf", ".csv", ".log", ".txt"}
+
+ def __init__(
+ self,
+ base_filename: StringPathLike,
+ max_bytes: int = 0,
+ **kwargs: Any,
+ ) -> None:
+ """
+ :param base_filename:
+ A path-like object for the base filename. The log file format is defined by
+ the suffix of `base_filename`.
+ :param max_bytes:
+ The size threshold at which a new log file shall be created. If set to 0, no
+ rollover will be performed.
+ """
+ super().__init__(**kwargs)
+
+ self.base_filename = os.path.abspath(base_filename)
+ self.max_bytes = max_bytes
+
+ self._writer = self._get_new_writer(self.base_filename)
+
+ def _get_new_writer(self, filename: StringPathLike) -> SizedMessageWriter:
+ writer = super()._get_new_writer(filename)
+ if isinstance(writer, SizedMessageWriter):
+ return writer
+ raise TypeError
+
+ @property
+ def writer(self) -> SizedMessageWriter:
+ return self._writer
+
+ def should_rollover(self, msg: Message) -> bool:
+ if self.max_bytes <= 0:
+ return False
+
+ file_size = self.writer.file_size()
+ if file_size is None:
+ return False
+
+ return file_size >= self.max_bytes
+
+ def do_rollover(self) -> None:
+ if self.writer:
+ self.writer.stop()
+
+ sfn = self.base_filename
+ dfn = self.rotation_filename(self._default_name())
+ self.rotate(sfn, dfn)
+
+ self._writer = self._get_new_writer(self.base_filename)
+
+ def _default_name(self) -> StringPathLike:
+ """Generate the default rotation filename."""
+ path = pathlib.Path(self.base_filename)
+ new_name = (
+ path.stem.split(".")[0]
+ + "_"
+ + datetime.now().strftime("%Y-%m-%dT%H%M%S")
+ + "_"
+ + f"#{self.rollover_count:03}"
+ + "".join(path.suffixes[-2:])
+ )
+ return str(path.parent / new_name)
diff --git a/can/io/mf4.py b/can/io/mf4.py
new file mode 100644
index 000000000..fcde2e193
--- /dev/null
+++ b/can/io/mf4.py
@@ -0,0 +1,540 @@
+"""
+Contains handling of MF4 logging files.
+
+MF4 files represent Measurement Data Format (MDF) version 4 as specified by
+the ASAM MDF standard (see https://www.asam.net/standards/detail/mdf/)
+"""
+
+import abc
+import heapq
+import logging
+from collections.abc import Generator, Iterator
+from datetime import datetime
+from hashlib import md5
+from io import BufferedIOBase, BytesIO
+from pathlib import Path
+from typing import Any, BinaryIO, cast
+
+from ..message import Message
+from ..typechecking import StringPathLike
+from ..util import channel2int, len2dlc
+from .generic import BinaryIOMessageReader, BinaryIOMessageWriter
+
+logger = logging.getLogger("can.io.mf4")
+
+try:
+ import asammdf
+ import numpy as np
+ from asammdf import Signal, Source
+ from asammdf.blocks.mdf_v4 import MDF4
+ from asammdf.blocks.v4_blocks import ChannelGroup, SourceInformation
+ from asammdf.blocks.v4_constants import BUS_TYPE_CAN, FLAG_CG_BUS_EVENT, SOURCE_BUS
+ from asammdf.mdf import MDF
+
+ STD_DTYPE = np.dtype(
+ [
+ ("CAN_DataFrame.BusChannel", " None:
+ """
+ :param file:
+ A path-like object or as file-like object to write to.
+ If this is a file-like object, is has to be opened in
+ binary write mode, not text write mode.
+ :param database:
+ optional path to a DBC or ARXML file that contains message description.
+ :param compression_level:
+ compression option as integer (default 2)
+ * 0 - no compression
+ * 1 - deflate (slower, but produces smaller files)
+ * 2 - transposition + deflate (slowest, but produces the smallest files)
+ """
+ if asammdf is None:
+ raise NotImplementedError(
+ "The asammdf package was not found. Install python-can with "
+ "the optional dependency [mf4] to use the MF4Writer."
+ )
+
+ if kwargs.get("append", False):
+ raise ValueError(
+ f"{self.__class__.__name__} is currently not equipped to "
+ f"append messages to an existing file."
+ )
+
+ super().__init__(file, mode="w+b")
+ now = datetime.now()
+ self._mdf = cast("MDF4", MDF(version="4.10"))
+ self._mdf.header.start_time = now
+ self.last_timestamp = self._start_time = now.timestamp()
+
+ self._compression_level = compression_level
+
+ if database:
+ database = Path(database).resolve()
+ if database.exists():
+ data = database.read_bytes()
+ attachment = data, database.name, md5(data).digest()
+ else:
+ attachment = None
+ else:
+ attachment = None
+
+ acquisition_source = SourceInformation(
+ source_type=SOURCE_BUS, bus_type=BUS_TYPE_CAN
+ )
+
+ # standard frames group
+ self._mdf.append(
+ Signal(
+ name="CAN_DataFrame",
+ samples=np.array([], dtype=STD_DTYPE),
+ timestamps=np.array([], dtype=" int:
+ """Return an estimate of the current file size in bytes."""
+ # TODO: find solution without accessing private attributes of asammdf
+ return cast(
+ "int",
+ self._mdf._tempfile.tell(), # pylint: disable=protected-access,no-member
+ )
+
+ def stop(self) -> None:
+ self._mdf.save(self.file, compression=self._compression_level)
+ self._mdf.close()
+ super().stop()
+
+ def on_message_received(self, msg: Message) -> None:
+ channel = channel2int(msg.channel)
+
+ timestamp = msg.timestamp
+ if timestamp is None:
+ timestamp = self.last_timestamp
+ else:
+ self.last_timestamp = max(self.last_timestamp, timestamp)
+
+ timestamp -= self._start_time
+
+ if msg.is_remote_frame:
+ if channel is not None:
+ self._rtr_buffer["CAN_RemoteFrame.BusChannel"] = channel
+
+ self._rtr_buffer["CAN_RemoteFrame.ID"] = msg.arbitration_id
+ self._rtr_buffer["CAN_RemoteFrame.IDE"] = int(msg.is_extended_id)
+ self._rtr_buffer["CAN_RemoteFrame.Dir"] = 0 if msg.is_rx else 1
+ self._rtr_buffer["CAN_RemoteFrame.DLC"] = msg.dlc
+
+ sigs = [(np.array([timestamp]), None), (self._rtr_buffer, None)]
+ self._mdf.extend(2, sigs)
+
+ elif msg.is_error_frame:
+ if channel is not None:
+ self._err_buffer["CAN_ErrorFrame.BusChannel"] = channel
+
+ self._err_buffer["CAN_ErrorFrame.ID"] = msg.arbitration_id
+ self._err_buffer["CAN_ErrorFrame.IDE"] = int(msg.is_extended_id)
+ self._err_buffer["CAN_ErrorFrame.Dir"] = 0 if msg.is_rx else 1
+ data = msg.data
+ size = len(data)
+ self._err_buffer["CAN_ErrorFrame.DataLength"] = size
+ self._err_buffer["CAN_ErrorFrame.DataBytes"][0, :size] = data
+ if msg.is_fd:
+ self._err_buffer["CAN_ErrorFrame.DLC"] = len2dlc(msg.dlc)
+ self._err_buffer["CAN_ErrorFrame.ESI"] = int(msg.error_state_indicator)
+ self._err_buffer["CAN_ErrorFrame.BRS"] = int(msg.bitrate_switch)
+ self._err_buffer["CAN_ErrorFrame.EDL"] = 1
+ else:
+ self._err_buffer["CAN_ErrorFrame.DLC"] = msg.dlc
+ self._err_buffer["CAN_ErrorFrame.ESI"] = 0
+ self._err_buffer["CAN_ErrorFrame.BRS"] = 0
+ self._err_buffer["CAN_ErrorFrame.EDL"] = 0
+
+ sigs = [(np.array([timestamp]), None), (self._err_buffer, None)]
+ self._mdf.extend(1, sigs)
+
+ else:
+ if channel is not None:
+ self._std_buffer["CAN_DataFrame.BusChannel"] = channel
+
+ self._std_buffer["CAN_DataFrame.ID"] = msg.arbitration_id
+ self._std_buffer["CAN_DataFrame.IDE"] = int(msg.is_extended_id)
+ self._std_buffer["CAN_DataFrame.Dir"] = 0 if msg.is_rx else 1
+ data = msg.data
+ size = len(data)
+ self._std_buffer["CAN_DataFrame.DataLength"] = size
+ self._std_buffer["CAN_DataFrame.DataBytes"][0, :size] = data
+ if msg.is_fd:
+ self._std_buffer["CAN_DataFrame.DLC"] = len2dlc(msg.dlc)
+ self._std_buffer["CAN_DataFrame.ESI"] = int(msg.error_state_indicator)
+ self._std_buffer["CAN_DataFrame.BRS"] = int(msg.bitrate_switch)
+ self._std_buffer["CAN_DataFrame.EDL"] = 1
+ else:
+ self._std_buffer["CAN_DataFrame.DLC"] = msg.dlc
+ self._std_buffer["CAN_DataFrame.ESI"] = 0
+ self._std_buffer["CAN_DataFrame.BRS"] = 0
+ self._std_buffer["CAN_DataFrame.EDL"] = 0
+
+ sigs = [(np.array([timestamp]), None), (self._std_buffer, None)]
+ self._mdf.extend(0, sigs)
+
+ # reset buffer structure
+ self._std_buffer = np.zeros(1, dtype=STD_DTYPE)
+ self._err_buffer = np.zeros(1, dtype=ERR_DTYPE)
+ self._rtr_buffer = np.zeros(1, dtype=RTR_DTYPE)
+
+
+class FrameIterator(abc.ABC):
+ """
+ Iterator helper class for common handling among CAN DataFrames, ErrorFrames and RemoteFrames.
+ """
+
+ # Number of records to request for each asammdf call
+ _chunk_size = 1000
+
+ def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float, name: str):
+ self._mdf = mdf
+ self._group_index = group_index
+ self._start_timestamp = start_timestamp
+ self._name = name
+
+ # Extract names
+ channel_group: ChannelGroup = self._mdf.groups[self._group_index]
+
+ self._channel_names = []
+
+ for channel in channel_group.channels:
+ if str(channel.name).startswith(f"{self._name}."):
+ self._channel_names.append(channel.name)
+
+ def _get_data(self, current_offset: int) -> Signal:
+ # NOTE: asammdf suggests using select instead of get. Select seem to miss converting some
+ # channels which get does convert as expected.
+ data_raw = self._mdf.get(
+ self._name,
+ self._group_index,
+ record_offset=current_offset,
+ record_count=self._chunk_size,
+ raw=False,
+ )
+
+ return data_raw
+
+ @abc.abstractmethod
+ def __iter__(self) -> Generator[Message, None, None]:
+ pass
+
+
+class MF4Reader(BinaryIOMessageReader):
+ """
+ Iterator of CAN messages from a MF4 logging file.
+
+ The MF4Reader only supports MF4 files with CAN bus logging.
+ """
+
+ # NOTE: Readout based on the bus logging code from asammdf GUI
+
+ class _CANDataFrameIterator(FrameIterator):
+
+ def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float):
+ super().__init__(mdf, group_index, start_timestamp, "CAN_DataFrame")
+
+ def __iter__(self) -> Generator[Message, None, None]:
+ for current_offset in range(
+ 0,
+ self._mdf.groups[self._group_index].channel_group.cycles_nr,
+ self._chunk_size,
+ ):
+ data = self._get_data(current_offset)
+ names = data.samples[0].dtype.names
+
+ for i in range(len(data)):
+ data_length = int(data["CAN_DataFrame.DataLength"][i])
+
+ kv: dict[str, Any] = {
+ "timestamp": float(data.timestamps[i]) + self._start_timestamp,
+ "arbitration_id": int(data["CAN_DataFrame.ID"][i]) & 0x1FFFFFFF,
+ "data": data["CAN_DataFrame.DataBytes"][i][
+ :data_length
+ ].tobytes(),
+ }
+
+ if "CAN_DataFrame.BusChannel" in names:
+ kv["channel"] = int(data["CAN_DataFrame.BusChannel"][i])
+ if "CAN_DataFrame.Dir" in names:
+ if data["CAN_DataFrame.Dir"][i].dtype.kind == "S":
+ kv["is_rx"] = data["CAN_DataFrame.Dir"][i] == b"Rx"
+ else:
+ kv["is_rx"] = int(data["CAN_DataFrame.Dir"][i]) == 0
+ if "CAN_DataFrame.IDE" in names:
+ kv["is_extended_id"] = bool(data["CAN_DataFrame.IDE"][i])
+ if "CAN_DataFrame.EDL" in names:
+ kv["is_fd"] = bool(data["CAN_DataFrame.EDL"][i])
+ if "CAN_DataFrame.BRS" in names:
+ kv["bitrate_switch"] = bool(data["CAN_DataFrame.BRS"][i])
+ if "CAN_DataFrame.ESI" in names:
+ kv["error_state_indicator"] = bool(data["CAN_DataFrame.ESI"][i])
+
+ yield Message(**kv)
+
+ class _CANErrorFrameIterator(FrameIterator):
+
+ def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float):
+ super().__init__(mdf, group_index, start_timestamp, "CAN_ErrorFrame")
+
+ def __iter__(self) -> Generator[Message, None, None]:
+ for current_offset in range(
+ 0,
+ self._mdf.groups[self._group_index].channel_group.cycles_nr,
+ self._chunk_size,
+ ):
+ data = self._get_data(current_offset)
+ names = data.samples[0].dtype.names
+
+ for i in range(len(data)):
+ kv: dict[str, Any] = {
+ "timestamp": float(data.timestamps[i]) + self._start_timestamp,
+ "is_error_frame": True,
+ }
+
+ if "CAN_ErrorFrame.BusChannel" in names:
+ kv["channel"] = int(data["CAN_ErrorFrame.BusChannel"][i])
+ if "CAN_ErrorFrame.Dir" in names:
+ if data["CAN_ErrorFrame.Dir"][i].dtype.kind == "S":
+ kv["is_rx"] = data["CAN_ErrorFrame.Dir"][i] == b"Rx"
+ else:
+ kv["is_rx"] = int(data["CAN_ErrorFrame.Dir"][i]) == 0
+ if "CAN_ErrorFrame.ID" in names:
+ kv["arbitration_id"] = (
+ int(data["CAN_ErrorFrame.ID"][i]) & 0x1FFFFFFF
+ )
+ if "CAN_ErrorFrame.IDE" in names:
+ kv["is_extended_id"] = bool(data["CAN_ErrorFrame.IDE"][i])
+ if "CAN_ErrorFrame.EDL" in names:
+ kv["is_fd"] = bool(data["CAN_ErrorFrame.EDL"][i])
+ if "CAN_ErrorFrame.BRS" in names:
+ kv["bitrate_switch"] = bool(data["CAN_ErrorFrame.BRS"][i])
+ if "CAN_ErrorFrame.ESI" in names:
+ kv["error_state_indicator"] = bool(
+ data["CAN_ErrorFrame.ESI"][i]
+ )
+ if "CAN_ErrorFrame.RTR" in names:
+ kv["is_remote_frame"] = bool(data["CAN_ErrorFrame.RTR"][i])
+ if (
+ "CAN_ErrorFrame.DataLength" in names
+ and "CAN_ErrorFrame.DataBytes" in names
+ ):
+ data_length = int(data["CAN_ErrorFrame.DataLength"][i])
+ kv["data"] = data["CAN_ErrorFrame.DataBytes"][i][
+ :data_length
+ ].tobytes()
+
+ yield Message(**kv)
+
+ class _CANRemoteFrameIterator(FrameIterator):
+
+ def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float):
+ super().__init__(mdf, group_index, start_timestamp, "CAN_RemoteFrame")
+
+ def __iter__(self) -> Generator[Message, None, None]:
+ for current_offset in range(
+ 0,
+ self._mdf.groups[self._group_index].channel_group.cycles_nr,
+ self._chunk_size,
+ ):
+ data = self._get_data(current_offset)
+ names = data.samples[0].dtype.names
+
+ for i in range(len(data)):
+ kv: dict[str, Any] = {
+ "timestamp": float(data.timestamps[i]) + self._start_timestamp,
+ "arbitration_id": int(data["CAN_RemoteFrame.ID"][i])
+ & 0x1FFFFFFF,
+ "dlc": int(data["CAN_RemoteFrame.DLC"][i]),
+ "is_remote_frame": True,
+ }
+
+ if "CAN_RemoteFrame.BusChannel" in names:
+ kv["channel"] = int(data["CAN_RemoteFrame.BusChannel"][i])
+ if "CAN_RemoteFrame.Dir" in names:
+ if data["CAN_RemoteFrame.Dir"][i].dtype.kind == "S":
+ kv["is_rx"] = data["CAN_RemoteFrame.Dir"][i] == b"Rx"
+ else:
+ kv["is_rx"] = int(data["CAN_RemoteFrame.Dir"][i]) == 0
+ if "CAN_RemoteFrame.IDE" in names:
+ kv["is_extended_id"] = bool(data["CAN_RemoteFrame.IDE"][i])
+
+ yield Message(**kv)
+
+ def __init__(
+ self,
+ file: StringPathLike | BinaryIO,
+ **kwargs: Any,
+ ) -> None:
+ """
+ :param file: a path-like object or as file-like object to read from
+ If this is a file-like object, is has to be opened in
+ binary read mode, not text read mode.
+ """
+ if asammdf is None:
+ raise NotImplementedError(
+ "The asammdf package was not found. Install python-can with "
+ "the optional dependency [mf4] to use the MF4Reader."
+ )
+
+ super().__init__(file, mode="rb")
+
+ self._mdf: MDF4
+ if isinstance(file, BufferedIOBase):
+ self._mdf = cast("MDF4", MDF(BytesIO(file.read())))
+ else:
+ self._mdf = cast("MDF4", MDF(file))
+
+ self._start_timestamp = self._mdf.header.start_time.timestamp()
+
+ def __iter__(self) -> Iterator[Message]:
+ # To handle messages split over multiple channel groups, create a single iterator per
+ # channel group and merge these iterators into a single iterator using heapq.
+ iterators: list[FrameIterator] = []
+ for group_index, group in enumerate(self._mdf.groups):
+ channel_group: ChannelGroup = group.channel_group
+
+ if not channel_group.flags & FLAG_CG_BUS_EVENT:
+ # Not a bus event, skip
+ continue
+
+ if channel_group.cycles_nr == 0:
+ # No data, skip
+ continue
+
+ acquisition_source: Source | None = channel_group.acq_source
+
+ if acquisition_source is None:
+ # No source information, skip
+ continue
+ if not acquisition_source.source_type & Source.SOURCE_BUS:
+ # Not a bus type (likely already covered by the channel group flag), skip
+ continue
+
+ channel_names = [channel.name for channel in group.channels]
+
+ if acquisition_source.bus_type == Source.BUS_TYPE_CAN:
+ if "CAN_DataFrame" in channel_names:
+ iterators.append(
+ self._CANDataFrameIterator(
+ self._mdf, group_index, self._start_timestamp
+ )
+ )
+ elif "CAN_ErrorFrame" in channel_names:
+ iterators.append(
+ self._CANErrorFrameIterator(
+ self._mdf, group_index, self._start_timestamp
+ )
+ )
+ elif "CAN_RemoteFrame" in channel_names:
+ iterators.append(
+ self._CANRemoteFrameIterator(
+ self._mdf, group_index, self._start_timestamp
+ )
+ )
+ else:
+ # Unknown bus type, skip
+ continue
+
+ # Create merged iterator over all the groups, using the timestamps as comparison key
+ return iter(heapq.merge(*iterators, key=lambda x: x.timestamp))
+
+ def stop(self) -> None:
+ self._mdf.close()
+ self._mdf = None
+ super().stop()
diff --git a/can/io/player.py b/can/io/player.py
old mode 100755
new mode 100644
index 4af42c479..c0015b185
--- a/can/io/player.py
+++ b/can/io/player.py
@@ -1,110 +1,189 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
This module contains the generic :class:`LogReader` as
well as :class:`MessageSync` which plays back messages
-in the recorded order an time intervals.
+in the recorded order and time intervals.
"""
-from __future__ import absolute_import
-
+import gzip
+import pathlib
import time
-import logging
-
-from .generic import BaseIOHandler
+from collections.abc import Generator, Iterable
+from typing import (
+ Any,
+ Final,
+)
+
+from .._entry_points import read_entry_points
+from ..message import Message
+from ..typechecking import StringPathLike
from .asc import ASCReader
from .blf import BLFReader
from .canutils import CanutilsLogReader
from .csv import CSVReader
+from .generic import BinaryIOMessageReader, MessageReader, TextIOMessageReader
+from .mf4 import MF4Reader
from .sqlite import SqliteReader
+from .trc import TRCReader
+
+#: A map of file suffixes to their corresponding
+#: :class:`can.io.generic.MessageReader` class
+MESSAGE_READERS: Final[dict[str, type[MessageReader]]] = {
+ ".asc": ASCReader,
+ ".blf": BLFReader,
+ ".csv": CSVReader,
+ ".db": SqliteReader,
+ ".log": CanutilsLogReader,
+ ".mf4": MF4Reader,
+ ".trc": TRCReader,
+}
+
+
+def _update_reader_plugins() -> None:
+ """Update available message reader plugins from entry points."""
+ for entry_point in read_entry_points("can.io.message_reader"):
+ if entry_point.key in MESSAGE_READERS:
+ continue
+
+ reader_class = entry_point.load()
+ if issubclass(reader_class, MessageReader):
+ MESSAGE_READERS[entry_point.key] = reader_class
+
-log = logging.getLogger('can.io.player')
+def _get_logger_for_suffix(suffix: str) -> type[MessageReader]:
+ """Find MessageReader class for given suffix."""
+ try:
+ return MESSAGE_READERS[suffix]
+ except KeyError:
+ raise ValueError(f'No read support for unknown log format "{suffix}"') from None
-class LogReader(BaseIOHandler):
+def _decompress(filename: StringPathLike, **kwargs: Any) -> MessageReader:
"""
- Replay logged CAN messages from a file.
+ Return the suffix and io object of the decompressed file.
+ """
+ suffixes = pathlib.Path(filename).suffixes
+ if len(suffixes) != 2:
+ raise ValueError(
+ f"No read support for unknown log format \"{''.join(suffixes)}\""
+ )
+
+ real_suffix = suffixes[-2].lower()
+ reader_type = _get_logger_for_suffix(real_suffix)
+
+ if issubclass(reader_type, TextIOMessageReader):
+ return reader_type(gzip.open(filename, mode="rt"), **kwargs)
+ elif issubclass(reader_type, BinaryIOMessageReader):
+ return reader_type(gzip.open(filename, mode="rb"), **kwargs)
+
+ raise ValueError(f"No read support for unknown log format \"{''.join(suffixes)}\"")
+
+
+def LogReader(filename: StringPathLike, **kwargs: Any) -> MessageReader: # noqa: N802
+ """Find and return the appropriate :class:`~can.io.generic.MessageReader` instance
+ for a given file suffix.
+
+ The format is determined from the file suffix which can be one of:
+ * .asc :class:`can.ASCReader`
+ * .blf :class:`can.BLFReader`
+ * .csv :class:`can.CSVReader`
+ * .db :class:`can.SqliteReader`
+ * .log :class:`can.CanutilsLogReader`
+ * .mf4 :class:`can.MF4Reader`
+ (optional, depends on `asammdf `_)
+ * .trc :class:`can.TRCReader`
+
+ Gzip compressed files can be used as long as the original
+ files suffix is one of the above (e.g. filename.asc.gz).
- The format is determined from the file format which can be one of:
- * .asc
- * .blf
- * .csv
- * .db
- * .log
- Exposes a simple iterator interface, to use simply:
+ Exposes a simple iterator interface, to use simply::
- >>> for msg in LogReader("some/path/to/my_file.log"):
- ... print(msg)
+ for msg in can.LogReader("some/path/to/my_file.log"):
+ print(msg)
+
+ :param filename:
+ the filename/path of the file to read from
+ :raises ValueError:
+ if the filename's suffix is of an unknown file type
.. note::
There are no time delays, if you want to reproduce the measured
delays between messages look at the :class:`can.MessageSync` class.
.. note::
- This class itself is just a dispatcher, and any positional an keyword
+ This function itself is just a dispatcher, and any positional and keyword
arguments are passed on to the returned instance.
"""
- @staticmethod
- def __new__(cls, filename, *args, **kwargs):
- """
- :param str filename: the filename/path the file to read from
- """
- if filename.endswith(".asc"):
- return ASCReader(filename, *args, **kwargs)
- elif filename.endswith(".blf"):
- return BLFReader(filename, *args, **kwargs)
- elif filename.endswith(".csv"):
- return CSVReader(filename, *args, **kwargs)
- elif filename.endswith(".db"):
- return SqliteReader(filename, *args, **kwargs)
- elif filename.endswith(".log"):
- return CanutilsLogReader(filename, *args, **kwargs)
- else:
- raise NotImplementedError("No read support for this log format: {}".format(filename))
-
-
-class MessageSync(object):
+ _update_reader_plugins()
+
+ suffix = pathlib.PurePath(filename).suffix.lower()
+ if suffix == ".gz":
+ return _decompress(filename)
+
+ reader_type = _get_logger_for_suffix(suffix)
+ return reader_type(file=filename, **kwargs)
+
+
+class MessageSync:
"""
Used to iterate over some given messages in the recorded time.
"""
- def __init__(self, messages, timestamps=True, gap=0.0001, skip=60):
+ def __init__(
+ self,
+ messages: Iterable[Message],
+ timestamps: bool = True,
+ gap: float = 0.0001,
+ skip: float = 60.0,
+ ) -> None:
"""Creates an new **MessageSync** instance.
:param messages: An iterable of :class:`can.Message` instances.
- :param bool timestamps: Use the messages' timestamps.
- :param float gap: Minimum time between sent messages in seconds
- :param float skip: Skip periods of inactivity greater than this (in seconds).
+ :param timestamps: Use the messages' timestamps. If False, uses the *gap* parameter
+ as the time between messages.
+ :param gap: Minimum time between sent messages in seconds
+ :param skip: Skip periods of inactivity greater than this (in seconds).
+
+ Example::
+
+ import can
+
+ with can.LogReader("my_logfile.asc") as reader, can.Bus(interface="virtual") as bus:
+ for msg in can.MessageSync(messages=reader):
+ print(msg)
+ bus.send(msg)
+
"""
self.raw_messages = messages
self.timestamps = timestamps
self.gap = gap
self.skip = skip
- def __iter__(self):
- log.debug("Iterating over messages at real speed")
-
- playback_start_time = time.time()
+ def __iter__(self) -> Generator[Message, None, None]:
+ t_wakeup = playback_start_time = time.perf_counter()
recorded_start_time = None
+ t_skipped = 0.0
- for m in self.raw_messages:
- if recorded_start_time is None:
- recorded_start_time = m.timestamp
-
+ for message in self.raw_messages:
+ # Work out the correct wait time
if self.timestamps:
- # Work out the correct wait time
- now = time.time()
- current_offset = now - playback_start_time
- recorded_offset_from_start = m.timestamp - recorded_start_time
- remaining_gap = recorded_offset_from_start - current_offset
+ if recorded_start_time is None:
+ recorded_start_time = message.timestamp
- sleep_period = max(self.gap, min(self.skip, remaining_gap))
+ t_wakeup = playback_start_time + (
+ message.timestamp - t_skipped - recorded_start_time
+ )
else:
- sleep_period = self.gap
+ t_wakeup += self.gap
+
+ sleep_period = t_wakeup - time.perf_counter()
+
+ if self.skip and sleep_period > self.skip:
+ t_skipped += sleep_period - self.skip
+ sleep_period = self.skip
- time.sleep(sleep_period)
+ if sleep_period > 1e-4:
+ time.sleep(sleep_period)
- yield m
+ yield message
diff --git a/can/io/printer.py b/can/io/printer.py
index 4e9333fa2..c41a83691 100644
--- a/can/io/printer.py
+++ b/can/io/printer.py
@@ -1,42 +1,54 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
This Listener simply prints to stdout / the terminal or a file.
"""
-from __future__ import print_function, absolute_import
-
import logging
+import sys
+from io import TextIOWrapper
+from typing import Any, TextIO
-from can.listener import Listener
-from .generic import BaseIOHandler
+from ..message import Message
+from ..typechecking import StringPathLike
+from .generic import TextIOMessageWriter
-log = logging.getLogger('can.io.printer')
+log = logging.getLogger("can.io.printer")
-class Printer(BaseIOHandler, Listener):
+class Printer(TextIOMessageWriter):
"""
The Printer class is a subclass of :class:`~can.Listener` which simply prints
- any messages it receives to the terminal (stdout). A message is tunred into a
+ any messages it receives to the terminal (stdout). A message is turned into a
string using :meth:`~can.Message.__str__`.
- :attr bool write_to_file: `True` iff this instance prints to a file instead of
- standard out
+ :attr write_to_file: `True` if this instance prints to a file instead of
+ standard out
"""
- def __init__(self, file=None):
+ def __init__(
+ self,
+ file: StringPathLike | TextIO | TextIOWrapper = sys.stdout,
+ append: bool = False,
+ **kwargs: Any,
+ ) -> None:
"""
- :param file: an optional path-like object or as file-like object to "print"
- to instead of writing to standard out (stdout)
- If this is a file-like object, is has to opened in text
+ :param file: An optional path-like object or a file-like object to "print"
+ to instead of writing to standard out (stdout).
+ If this is a file-like object, it has to be opened in text
write mode, not binary write mode.
+ :param append: If set to `True` messages, are appended to the file,
+ else the file is truncated
"""
- self.write_to_file = file is not None
- super(Printer, self).__init__(file, mode='w')
-
- def on_message_received(self, msg):
- if self.write_to_file:
- self.file.write(str(msg) + '\n')
- else:
- print(msg)
+ super().__init__(file, mode="a" if append else "w")
+
+ def on_message_received(self, msg: Message) -> None:
+ self.file.write(str(msg) + "\n")
+
+ def file_size(self) -> int:
+ """Return an estimate of the current file size in bytes."""
+ if self.file is not sys.stdout:
+ return self.file.tell()
+ return 0
+
+ def stop(self) -> None:
+ if self.file is not sys.stdout:
+ super().stop()
diff --git a/can/io/sqlite.py b/can/io/sqlite.py
index 23be2b8f5..5f4885adb 100644
--- a/can/io/sqlite.py
+++ b/can/io/sqlite.py
@@ -1,48 +1,49 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Implements an SQL database writer and reader for storing CAN messages.
.. note:: The database schema is given in the documentation of the loggers.
"""
-from __future__ import absolute_import
-
-import sys
-import time
-import threading
import logging
import sqlite3
+import threading
+import time
+from collections.abc import Generator, Iterator
+from typing import Any, TypeAlias
from can.listener import BufferedReader
from can.message import Message
-from .generic import BaseIOHandler
-log = logging.getLogger('can.io.sqlite')
+from ..typechecking import StringPathLike
+from .generic import MessageReader, MessageWriter
+
+log = logging.getLogger("can.io.sqlite")
-if sys.version_info.major < 3:
- # legacy fallback for Python 2
- memoryview = buffer
+_MessageTuple: TypeAlias = "tuple[float, int, bool, bool, bool, int, memoryview[int]]"
-class SqliteReader(BaseIOHandler):
+class SqliteReader(MessageReader):
"""
Reads recorded CAN messages from a simple SQL database.
This class can be iterated over or used to fetch all messages in the
database with :meth:`~SqliteReader.read_all`.
- Calling :func:`~builtin.len` on this object might not run in constant time.
+ Calling :func:`len` on this object might not run in constant time.
:attr str table_name: the name of the database table used for storing the messages
.. note:: The database schema is given in the documentation of the loggers.
"""
- def __init__(self, file, table_name="messages"):
+ def __init__(
+ self,
+ file: StringPathLike,
+ table_name: str = "messages",
+ **kwargs: Any,
+ ) -> None:
"""
- :param file: a `str` or since Python 3.7 a path like object that points
+ :param file: a `str` path like object that points
to the database file to use
:param str table_name: the name of the table to look for the messages
@@ -50,49 +51,46 @@ def __init__(self, file, table_name="messages"):
do not accept file-like objects as the `file` parameter.
It also runs in ``append=True`` mode all the time.
"""
- super(SqliteReader, self).__init__(file=None)
self._conn = sqlite3.connect(file)
self._cursor = self._conn.cursor()
self.table_name = table_name
- def __iter__(self):
- for frame_data in self._cursor.execute("SELECT * FROM {}".format(self.table_name)):
+ def __iter__(self) -> Generator[Message, None, None]:
+ for frame_data in self._cursor.execute(f"SELECT * FROM {self.table_name}"):
yield SqliteReader._assemble_message(frame_data)
- @staticmethod
- def _assemble_message(frame_data):
+ @staticmethod
+ def _assemble_message(frame_data: _MessageTuple) -> Message:
timestamp, can_id, is_extended, is_remote, is_error, dlc, data = frame_data
return Message(
timestamp=timestamp,
is_remote_frame=bool(is_remote),
- extended_id=bool(is_extended),
+ is_extended_id=bool(is_extended),
is_error_frame=bool(is_error),
arbitration_id=can_id,
dlc=dlc,
- data=data
+ data=data,
)
- def __len__(self):
+ def __len__(self) -> int:
# this might not run in constant time
- result = self._cursor.execute("SELECT COUNT(*) FROM {}".format(self.table_name))
+ result = self._cursor.execute(f"SELECT COUNT(*) FROM {self.table_name}")
return int(result.fetchone()[0])
- def read_all(self):
+ def read_all(self) -> Iterator[Message]:
"""Fetches all messages in the database.
:rtype: Generator[can.Message]
"""
- result = self._cursor.execute("SELECT * FROM {}".format(self.table_name)).fetchall()
+ result = self._cursor.execute(f"SELECT * FROM {self.table_name}").fetchall()
return (SqliteReader._assemble_message(frame) for frame in result)
- def stop(self):
- """Closes the connection to the database.
- """
- super(SqliteReader, self).stop()
+ def stop(self) -> None:
+ """Closes the connection to the database."""
self._conn.close()
-class SqliteWriter(BaseIOHandler, BufferedReader):
+class SqliteWriter(MessageWriter, BufferedReader):
"""Logs received CAN data to a simple SQL database.
The sqlite database may already exist, otherwise it will
@@ -104,7 +102,7 @@ class SqliteWriter(BaseIOHandler, BufferedReader):
:meth:`~can.SqliteWriter.stop()` may take a while.
:attr str table_name: the name of the database table used for storing the messages
- :attr int num_frames: the number of frames actally writtem to the database, this
+ :attr int num_frames: the number of frames actually written to the database, this
excludes messages that are still buffered
:attr float last_write: the last time a message war actually written to the database,
as given by ``time.time()``
@@ -137,16 +135,26 @@ class SqliteWriter(BaseIOHandler, BufferedReader):
MAX_BUFFER_SIZE_BEFORE_WRITES = 500
"""Maximum number of messages to buffer before writing to the database"""
- def __init__(self, file, table_name="messages"):
+ def __init__(
+ self,
+ file: StringPathLike,
+ table_name: str = "messages",
+ **kwargs: Any,
+ ) -> None:
"""
- :param file: a `str` or since Python 3.7 a path like object that points
+ :param file: a `str` or path like object that points
to the database file to use
:param str table_name: the name of the table to store messages in
.. warning:: In contrary to all other readers/writers the Sqlite handlers
do not accept file-like objects as the `file` parameter.
"""
- super(SqliteWriter, self).__init__(file=None)
+ if kwargs.get("append", False):
+ raise ValueError(
+ f"The append argument should not be used in "
+ f"conjunction with the {self.__class__.__name__}."
+ )
+ BufferedReader.__init__(self)
self.table_name = table_name
self._db_filename = file
self._stop_running_event = threading.Event()
@@ -154,8 +162,12 @@ def __init__(self, file, table_name="messages"):
self._writer_thread.start()
self.num_frames = 0
self.last_write = time.time()
+ self._insert_template = (
+ f"INSERT INTO {self.table_name} VALUES (?, ?, ?, ?, ?, ?, ?)"
+ )
- def _create_db(self):
+ @staticmethod
+ def _create_db(file: StringPathLike, table_name: str) -> sqlite3.Connection:
"""Creates a new databae or opens a connection to an existing one.
.. note::
@@ -163,59 +175,63 @@ def _create_db(self):
hence we setup the db here. It has the upside of running async.
"""
log.debug("Creating sqlite database")
- self._conn = sqlite3.connect(self._db_filename)
+ conn = sqlite3.connect(file)
# create table structure
- self._conn.cursor().execute("""
- CREATE TABLE IF NOT EXISTS {}
- (
- ts REAL,
- arbitration_id INTEGER,
- extended INTEGER,
- remote INTEGER,
- error INTEGER,
- dlc INTEGER,
- data BLOB
+ conn.cursor().execute(
+ f"""CREATE TABLE IF NOT EXISTS {table_name}
+ (
+ ts REAL,
+ arbitration_id INTEGER,
+ extended INTEGER,
+ remote INTEGER,
+ error INTEGER,
+ dlc INTEGER,
+ data BLOB
+ )"""
)
- """.format(self.table_name))
- self._conn.commit()
+ conn.commit()
- self._insert_template = "INSERT INTO {} VALUES (?, ?, ?, ?, ?, ?, ?)".format(self.table_name)
+ return conn
- def _db_writer_thread(self):
- self._create_db()
+ def _db_writer_thread(self) -> None:
+ conn = SqliteWriter._create_db(self._db_filename, self.table_name)
try:
while True:
- messages = [] # reset buffer
+ messages: list[_MessageTuple] = [] # reset buffer
msg = self.get_message(self.GET_MESSAGE_TIMEOUT)
while msg is not None:
- #log.debug("SqliteWriter: buffering message")
-
- messages.append((
- msg.timestamp,
- msg.arbitration_id,
- msg.id_type,
- msg.is_remote_frame,
- msg.is_error_frame,
- msg.dlc,
- memoryview(msg.data)
- ))
-
- if time.time() - self.last_write > self.MAX_TIME_BETWEEN_WRITES or \
- len(messages) > self.MAX_BUFFER_SIZE_BEFORE_WRITES:
- break
- else:
- # just go on
- msg = self.get_message(self.GET_MESSAGE_TIMEOUT)
+ # log.debug("SqliteWriter: buffering message")
+
+ messages.append(
+ (
+ msg.timestamp,
+ msg.arbitration_id,
+ msg.is_extended_id,
+ msg.is_remote_frame,
+ msg.is_error_frame,
+ msg.dlc,
+ memoryview(msg.data),
+ )
+ )
+
+ if (
+ time.time() - self.last_write > self.MAX_TIME_BETWEEN_WRITES
+ or len(messages) > self.MAX_BUFFER_SIZE_BEFORE_WRITES
+ ):
+ break
+
+ # just go on
+ msg = self.get_message(self.GET_MESSAGE_TIMEOUT)
count = len(messages)
if count > 0:
- with self._conn:
- #log.debug("Writing %d frames to db", count)
- self._conn.executemany(self._insert_template, messages)
- self._conn.commit() # make the changes visible to the entire database
+ with conn:
+ # log.debug("Writing %d frames to db", count)
+ conn.executemany(self._insert_template, messages)
+ conn.commit() # make the changes visible to the entire database
self.num_frames += count
self.last_write = time.time()
@@ -224,14 +240,13 @@ def _db_writer_thread(self):
break
finally:
- self._conn.close()
+ conn.close()
log.info("Stopped sqlite writer after writing %d messages", self.num_frames)
- def stop(self):
+ def stop(self) -> None:
"""Stops the reader an writes all remaining messages to the database. Thus, this
- might take a while an block.
+ might take a while and block.
"""
BufferedReader.stop(self)
self._stop_running_event.set()
self._writer_thread.join()
- BaseIOHandler.stop(self)
diff --git a/can/io/trc.py b/can/io/trc.py
new file mode 100644
index 000000000..c02bdcfe9
--- /dev/null
+++ b/can/io/trc.py
@@ -0,0 +1,443 @@
+"""
+Reader and writer for can logging files in peak trc format
+
+See https://www.peak-system.com/produktcd/Pdf/English/PEAK_CAN_TRC_File_Format.pdf
+for file format description
+
+Version 1.1 will be implemented as it is most commonly used
+"""
+
+import logging
+import os
+from collections.abc import Callable, Generator
+from datetime import datetime, timedelta, timezone
+from enum import Enum
+from io import TextIOWrapper
+from typing import Any, TextIO
+
+from ..message import Message
+from ..typechecking import StringPathLike
+from ..util import channel2int, len2dlc
+from .generic import TextIOMessageReader, TextIOMessageWriter
+
+logger = logging.getLogger("can.io.trc")
+
+
+class TRCFileVersion(Enum):
+ UNKNOWN = 0
+ V1_0 = 100
+ V1_1 = 101
+ V1_2 = 102
+ V1_3 = 103
+ V2_0 = 200
+ V2_1 = 201
+
+ def __ge__(self, other: Any) -> bool:
+ if isinstance(other, TRCFileVersion):
+ return self.value >= other.value
+ return NotImplemented
+
+
+class TRCReader(TextIOMessageReader):
+ """
+ Iterator of CAN messages from a TRC logging file.
+ """
+
+ def __init__(
+ self,
+ file: StringPathLike | TextIO,
+ **kwargs: Any,
+ ) -> None:
+ """
+ :param file: a path-like object or as file-like object to read from
+ If this is a file-like object, is has to opened in text
+ read mode, not binary read mode.
+ """
+ super().__init__(file, mode="r")
+ self.file_version = TRCFileVersion.UNKNOWN
+ self._start_time: float = 0
+ self.columns: dict[str, int] = {}
+ self._num_columns = -1
+
+ if not self.file:
+ raise ValueError("The given file cannot be None")
+
+ self._parse_cols: Callable[[tuple[str, ...]], Message | None] = lambda x: None
+
+ @property
+ def start_time(self) -> datetime | None:
+ if self._start_time:
+ return datetime.fromtimestamp(self._start_time, timezone.utc)
+ return None
+
+ def _extract_header(self) -> str:
+ line = ""
+ for _line in self.file:
+ line = _line.strip()
+ if line.startswith(";$FILEVERSION"):
+ logger.debug("TRCReader: Found file version '%s'", line)
+ try:
+ file_version = line.split("=")[1]
+ if file_version == "1.1":
+ self.file_version = TRCFileVersion.V1_1
+ elif file_version == "1.3":
+ self.file_version = TRCFileVersion.V1_3
+ elif file_version == "2.0":
+ self.file_version = TRCFileVersion.V2_0
+ elif file_version == "2.1":
+ self.file_version = TRCFileVersion.V2_1
+ else:
+ self.file_version = TRCFileVersion.UNKNOWN
+ except IndexError:
+ logger.debug("TRCReader: Failed to parse version")
+ elif line.startswith(";$STARTTIME"):
+ logger.debug("TRCReader: Found start time '%s'", line)
+ try:
+ self._start_time = (
+ datetime(1899, 12, 30, tzinfo=timezone.utc)
+ + timedelta(days=float(line.split("=")[1]))
+ ).timestamp()
+ except IndexError:
+ logger.debug("TRCReader: Failed to parse start time")
+ elif line.startswith(";$COLUMNS"):
+ logger.debug("TRCReader: Found columns '%s'", line)
+ try:
+ columns = line.split("=")[1].split(",")
+ self.columns = {column: columns.index(column) for column in columns}
+ self._num_columns = len(columns) - 1
+ except IndexError:
+ logger.debug("TRCReader: Failed to parse columns")
+ elif line.startswith(";"):
+ continue
+ else:
+ break
+
+ if self.file_version >= TRCFileVersion.V1_1:
+ if self._start_time is None:
+ raise ValueError("File has no start time information")
+
+ if self.file_version >= TRCFileVersion.V2_0:
+ if not self.columns:
+ raise ValueError("File has no column information")
+
+ if self.file_version == TRCFileVersion.UNKNOWN:
+ logger.info(
+ "TRCReader: No file version was found, so version 1.0 is assumed"
+ )
+ self._parse_cols = self._parse_msg_v1_0
+ elif self.file_version == TRCFileVersion.V1_0:
+ self._parse_cols = self._parse_msg_v1_0
+ elif self.file_version == TRCFileVersion.V1_1:
+ self._parse_cols = self._parse_cols_v1_1
+ elif self.file_version == TRCFileVersion.V1_3:
+ self._parse_cols = self._parse_cols_v1_3
+ elif self.file_version in [TRCFileVersion.V2_0, TRCFileVersion.V2_1]:
+ self._parse_cols = self._parse_cols_v2_x
+ else:
+ raise NotImplementedError("File version not fully implemented for reading")
+
+ return line
+
+ def _parse_msg_v1_0(self, cols: tuple[str, ...]) -> Message | None:
+ arbit_id = cols[2]
+ if arbit_id == "FFFFFFFF":
+ logger.info("TRCReader: Dropping bus info line")
+ return None
+
+ msg = Message()
+ msg.timestamp = float(cols[1]) / 1000
+ msg.arbitration_id = int(arbit_id, 16)
+ msg.is_extended_id = len(arbit_id) > 4
+ msg.channel = 1
+ msg.dlc = int(cols[3])
+ if len(cols) > 4 and cols[4] == "RTR":
+ msg.is_remote_frame = True
+ else:
+ msg.data = bytearray([int(cols[i + 4], 16) for i in range(msg.dlc)])
+ return msg
+
+ def _parse_msg_v1_1(self, cols: tuple[str, ...]) -> Message | None:
+ arbit_id = cols[3]
+
+ msg = Message()
+ msg.timestamp = float(cols[1]) / 1000 + self._start_time
+ msg.arbitration_id = int(arbit_id, 16)
+ msg.is_extended_id = len(arbit_id) > 4
+ msg.channel = 1
+ msg.dlc = int(cols[4])
+ if len(cols) > 5 and cols[5] == "RTR":
+ msg.is_remote_frame = True
+ else:
+ msg.data = bytearray([int(cols[i + 5], 16) for i in range(msg.dlc)])
+ msg.is_rx = cols[2] == "Rx"
+ return msg
+
+ def _parse_msg_v1_3(self, cols: tuple[str, ...]) -> Message | None:
+ arbit_id = cols[4]
+
+ msg = Message()
+ msg.timestamp = float(cols[1]) / 1000 + self._start_time
+ msg.arbitration_id = int(arbit_id, 16)
+ msg.is_extended_id = len(arbit_id) > 4
+ msg.channel = int(cols[2])
+ msg.dlc = int(cols[6])
+ if len(cols) > 7 and cols[7] == "RTR":
+ msg.is_remote_frame = True
+ else:
+ msg.data = bytearray([int(cols[i + 7], 16) for i in range(msg.dlc)])
+ msg.is_rx = cols[3] == "Rx"
+ return msg
+
+ def _parse_msg_v2_x(self, cols: tuple[str, ...]) -> Message | None:
+ type_ = cols[self.columns["T"]]
+ bus = self.columns.get("B", None)
+
+ if "l" in self.columns:
+ length = int(cols[self.columns["l"]])
+ dlc = len2dlc(length)
+ elif "L" in self.columns:
+ dlc = int(cols[self.columns["L"]])
+ else:
+ raise ValueError("No length/dlc columns present.")
+
+ msg = Message()
+ msg.timestamp = float(cols[self.columns["O"]]) / 1000 + self._start_time
+ msg.arbitration_id = int(cols[self.columns["I"]], 16)
+ msg.is_extended_id = len(cols[self.columns["I"]]) > 4
+ msg.channel = int(cols[bus]) if bus is not None else 1
+ msg.dlc = dlc
+ msg.is_remote_frame = type_ in {"RR"}
+ if dlc and not msg.is_remote_frame:
+ msg.data = bytearray.fromhex(cols[self.columns["D"]])
+ msg.is_rx = cols[self.columns["d"]] == "Rx"
+ msg.is_fd = type_ in {"FD", "FB", "FE", "BI"}
+ msg.bitrate_switch = type_ in {"FB", "FE"}
+ msg.error_state_indicator = type_ in {"FE", "BI"}
+
+ return msg
+
+ def _parse_cols_v1_1(self, cols: tuple[str, ...]) -> Message | None:
+ dtype = cols[2]
+ if dtype in ("Tx", "Rx"):
+ return self._parse_msg_v1_1(cols)
+ else:
+ logger.info("TRCReader: Unsupported type '%s'", dtype)
+ return None
+
+ def _parse_cols_v1_3(self, cols: tuple[str, ...]) -> Message | None:
+ dtype = cols[3]
+ if dtype in ("Tx", "Rx"):
+ return self._parse_msg_v1_3(cols)
+ else:
+ logger.info("TRCReader: Unsupported type '%s'", dtype)
+ return None
+
+ def _parse_cols_v2_x(self, cols: tuple[str, ...]) -> Message | None:
+ dtype = cols[self.columns["T"]]
+ if dtype in {"DT", "FD", "FB", "FE", "BI", "RR"}:
+ return self._parse_msg_v2_x(cols)
+ else:
+ logger.info("TRCReader: Unsupported type '%s'", dtype)
+ return None
+
+ def _parse_line(self, line: str) -> Message | None:
+ logger.debug("TRCReader: Parse '%s'", line)
+ try:
+ cols = tuple(line.split(maxsplit=self._num_columns))
+ return self._parse_cols(cols)
+ except IndexError:
+ logger.warning("TRCReader: Failed to parse message '%s'", line)
+ return None
+
+ def __iter__(self) -> Generator[Message, None, None]:
+ first_line = self._extract_header()
+
+ if first_line is not None:
+ msg = self._parse_line(first_line)
+ if msg is not None:
+ yield msg
+
+ for line in self.file:
+ temp = line.strip()
+ if temp.startswith(";"):
+ # Comment line
+ continue
+
+ if len(temp) == 0:
+ # Empty line
+ continue
+
+ msg = self._parse_line(temp)
+ if msg is not None:
+ yield msg
+
+ self.stop()
+
+
+class TRCWriter(TextIOMessageWriter):
+ """Logs CAN data to text file (.trc).
+
+ The measurement starts with the timestamp of the first registered message.
+ If a message has a timestamp smaller than the previous one or None,
+ it gets assigned the timestamp that was written for the last message.
+ If the first message does not have a timestamp, it is set to zero.
+ """
+
+ FORMAT_MESSAGE = (
+ "{msgnr:>7} {time:13.3f} DT {channel:>2} {id:>8} {dir:>2} - {dlc:<4} {data}"
+ )
+ FORMAT_MESSAGE_V1_0 = "{msgnr:>6}) {time:7.0f} {id:>8} {dlc:<1} {data}"
+
+ def __init__(
+ self,
+ file: StringPathLike | TextIO | TextIOWrapper,
+ channel: int = 1,
+ **kwargs: Any,
+ ) -> None:
+ """
+ :param file: a path-like object or as file-like object to write to
+ If this is a file-like object, is has to opened in text
+ write mode, not binary write mode.
+ :param channel: a default channel to use when the message does not
+ have a channel set
+ """
+ super().__init__(file, mode="w")
+ self.channel = channel
+
+ if hasattr(self.file, "reconfigure"):
+ self.file.reconfigure(newline="\r\n")
+ else:
+ raise TypeError("File must be opened in text mode.")
+
+ self.filepath = os.path.abspath(self.file.name)
+ self.header_written = False
+ self.msgnr = 0
+ self.first_timestamp: float | None = None
+ self.file_version = TRCFileVersion.V2_1
+ self._msg_fmt_string = self.FORMAT_MESSAGE_V1_0
+ self._format_message = self._format_message_init
+
+ def _write_header_v1_0(self, start_time: datetime) -> None:
+ lines = [
+ ";##########################################################################",
+ f"; {self.filepath}",
+ ";",
+ "; Generated by python-can TRCWriter",
+ f"; Start time: {start_time}",
+ "; PCAN-Net: N/A",
+ ";",
+ "; Columns description:",
+ "; ~~~~~~~~~~~~~~~~~~~~~",
+ "; +-current number in actual sample",
+ "; | +time offset of message (ms",
+ "; | | +ID of message (hex",
+ "; | | | +data length code",
+ "; | | | | +data bytes (hex ...",
+ "; | | | | |",
+ ";----+- ---+--- ----+--- + -+ -- -- ...",
+ ]
+ self.file.writelines(line + "\n" for line in lines)
+
+ def _write_header_v2_1(self, start_time: datetime) -> None:
+ header_time = start_time - datetime(
+ year=1899, month=12, day=30, tzinfo=timezone.utc
+ )
+ lines = [
+ ";$FILEVERSION=2.1",
+ f";$STARTTIME={header_time/timedelta(days=1)}",
+ ";$COLUMNS=N,O,T,B,I,d,R,L,D",
+ ";",
+ f"; {self.filepath}",
+ ";",
+ f"; Start time: {start_time}",
+ "; Generated by python-can TRCWriter",
+ ";-------------------------------------------------------------------------------",
+ "; Bus Name Connection Protocol",
+ "; N/A N/A N/A N/A",
+ ";-------------------------------------------------------------------------------",
+ "; Message Time Type ID Rx/Tx",
+ "; Number Offset | Bus [hex] | Reserved",
+ "; | [ms] | | | | | Data Length Code",
+ "; | | | | | | | | Data [hex] ...",
+ "; | | | | | | | | |",
+ ";---+-- ------+------ +- +- --+----- +- +- +--- +- -- -- -- -- -- -- --",
+ ]
+ self.file.writelines(line + "\n" for line in lines)
+
+ def _format_message_by_format(self, msg: Message, channel: int) -> str:
+ if msg.is_extended_id:
+ arb_id = f"{msg.arbitration_id:07X}"
+ else:
+ arb_id = f"{msg.arbitration_id:04X}"
+
+ data = [f"{byte:02X}" for byte in msg.data]
+
+ if self.first_timestamp is None:
+ raise ValueError
+ serialized = self._msg_fmt_string.format(
+ msgnr=self.msgnr,
+ time=(msg.timestamp - self.first_timestamp) * 1000,
+ channel=channel,
+ id=arb_id,
+ dir="Rx" if msg.is_rx else "Tx",
+ dlc=msg.dlc,
+ data=" ".join(data),
+ )
+ return serialized
+
+ def _format_message_init(self, msg: Message, channel: int) -> str:
+ if self.file_version == TRCFileVersion.V1_0:
+ self._format_message = self._format_message_by_format
+ self._msg_fmt_string = self.FORMAT_MESSAGE_V1_0
+ elif self.file_version == TRCFileVersion.V2_1:
+ self._format_message = self._format_message_by_format
+ self._msg_fmt_string = self.FORMAT_MESSAGE
+ else:
+ raise NotImplementedError("File format is not supported")
+
+ return self._format_message_by_format(msg, channel)
+
+ def write_header(self, timestamp: float) -> None:
+ # write start of file header
+ start_time = datetime.fromtimestamp(timestamp, timezone.utc)
+
+ if self.file_version == TRCFileVersion.V1_0:
+ self._write_header_v1_0(start_time)
+ elif self.file_version == TRCFileVersion.V2_1:
+ self._write_header_v2_1(start_time)
+ else:
+ raise NotImplementedError("File format is not supported")
+ self.header_written = True
+
+ def log_event(self, message: str, timestamp: float) -> None:
+ if not self.header_written:
+ self.write_header(timestamp)
+
+ self.file.write(message + "\n")
+
+ def on_message_received(self, msg: Message) -> None:
+ if self.first_timestamp is None:
+ self.first_timestamp = msg.timestamp
+
+ if msg.is_error_frame:
+ logger.warning("TRCWriter: Logging error frames is not implemented")
+ return
+
+ if msg.is_remote_frame:
+ logger.warning("TRCWriter: Logging remote frames is not implemented")
+ return
+
+ channel = channel2int(msg.channel)
+ if channel is None:
+ channel = self.channel
+ else:
+ # Many interfaces start channel numbering at 0 which is invalid
+ channel += 1
+
+ if msg.is_fd:
+ logger.warning("TRCWriter: Logging CAN FD is not implemented")
+ return
+
+ serialized = self._format_message(msg, channel)
+ self.msgnr += 1
+ self.log_event(serialized, msg.timestamp)
diff --git a/can/listener.py b/can/listener.py
index d9a31c5fa..1e289bea6 100644
--- a/can/listener.py
+++ b/can/listener.py
@@ -1,30 +1,19 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
This module contains the implementation of `can.Listener` and some readers.
"""
-from abc import ABCMeta, abstractmethod
-
-try:
- # Python 3.7
- from queue import SimpleQueue, Empty
-except ImportError:
- try:
- # Python 3.0 - 3.6
- from queue import Queue as SimpleQueue, Empty
- except ImportError:
- # Python 2
- from Queue import Queue as SimpleQueue, Empty
+import asyncio
+import warnings
+from abc import ABC, abstractmethod
+from collections.abc import AsyncIterator
+from queue import Empty, SimpleQueue
+from typing import Any
-try:
- import asyncio
-except ImportError:
- asyncio = None
+from can.bus import BusABC
+from can.message import Message
-class Listener(object):
+class Listener(ABC):
"""The basic listener that can be called directly to handle some
CAN message::
@@ -36,48 +25,50 @@ class Listener(object):
# or
listener.on_message_received(msg)
+ # Important to ensure all outputs are flushed
+ listener.stop()
"""
- __metaclass__ = ABCMeta
-
@abstractmethod
- def on_message_received(self, msg):
+ def on_message_received(self, msg: Message) -> None:
"""This method is called to handle the given message.
- :param can.Message msg: the delivered message
-
+ :param msg: the delivered message
"""
- pass
- def __call__(self, msg):
- return self.on_message_received(msg)
+ def __call__(self, msg: Message) -> None:
+ self.on_message_received(msg)
- def on_error(self, exc):
+ def on_error(self, exc: Exception) -> None:
"""This method is called to handle any exception in the receive thread.
- :param Exception exc: The exception causing the thread to stop
+ :param exc: The exception causing the thread to stop
"""
+ raise NotImplementedError()
- def stop(self):
+ def stop(self) -> None: # noqa: B027
"""
- Override to cleanup any open resources.
+ Stop handling new messages, carry out any final tasks to ensure
+ data is persisted and cleanup any open resources.
+
+ Concrete implementations override.
"""
-class RedirectReader(Listener):
+class RedirectReader(Listener): # pylint: disable=abstract-method
"""
A RedirectReader sends all received messages to another Bus.
-
"""
- def __init__(self, bus):
+ def __init__(self, bus: BusABC, *args: Any, **kwargs: Any) -> None:
+ super().__init__(*args, **kwargs)
self.bus = bus
- def on_message_received(self, msg):
+ def on_message_received(self, msg: Message) -> None:
self.bus.send(msg)
-class BufferedReader(Listener):
+class BufferedReader(Listener): # pylint: disable=abstract-method
"""
A BufferedReader is a subclass of :class:`~can.Listener` which implements a
**message buffer**: that is, when the :class:`can.BufferedReader` instance is
@@ -85,18 +76,18 @@ class BufferedReader(Listener):
be serviced. The messages can then be fetched with
:meth:`~can.BufferedReader.get_message`.
- Putting in messages after :meth:`~can.BufferedReader.stop` has be called will raise
+ Putting in messages after :meth:`~can.BufferedReader.stop` has been called will raise
an exception, see :meth:`~can.BufferedReader.on_message_received`.
- :attr bool is_stopped: ``True`` iff the reader has been stopped
+ :attr is_stopped: ``True`` if the reader has been stopped
"""
- def __init__(self):
+ def __init__(self) -> None:
# set to "infinite" size
- self.buffer = SimpleQueue()
- self.is_stopped = False
+ self.buffer: SimpleQueue[Message] = SimpleQueue()
+ self.is_stopped: bool = False
- def on_message_received(self, msg):
+ def on_message_received(self, msg: Message) -> None:
"""Append a message to the buffer.
:raises: BufferError
@@ -107,64 +98,78 @@ def on_message_received(self, msg):
else:
self.buffer.put(msg)
- def get_message(self, timeout=0.5):
+ def get_message(self, timeout: float = 0.5) -> Message | None:
"""
- Attempts to retrieve the latest message received by the instance. If no message is
- available it blocks for given timeout or until a message is received, or else
- returns None (whichever is shorter). This method does not block after
- :meth:`can.BufferedReader.stop` has been called.
-
- :param float timeout: The number of seconds to wait for a new message.
- :rytpe: can.Message or None
- :return: the message if there is one, or None if there is not.
+ Attempts to retrieve the message that has been in the queue for the longest amount
+ of time (FIFO). If no message is available, it blocks for given timeout or until a
+ message is received (whichever is shorter), or else returns None. This method does
+ not block after :meth:`can.BufferedReader.stop` has been called.
+
+ :param timeout: The number of seconds to wait for a new message.
+ :return: the received :class:`can.Message` or `None`, if the queue is empty.
"""
try:
- return self.buffer.get(block=not self.is_stopped, timeout=timeout)
+ if self.is_stopped:
+ return self.buffer.get(block=False)
+ else:
+ return self.buffer.get(block=True, timeout=timeout)
except Empty:
return None
- def stop(self):
- """Prohibits any more additions to this reader.
- """
+ def stop(self) -> None:
+ """Prohibits any more additions to this reader."""
self.is_stopped = True
-if asyncio is not None:
- class AsyncBufferedReader(Listener):
- """A message buffer for use with :mod:`asyncio`.
+class AsyncBufferedReader(
+ Listener, AsyncIterator[Message]
+): # pylint: disable=abstract-method
+ """A message buffer for use with :mod:`asyncio`.
- See :ref:`asyncio` for how to use with :class:`can.Notifier`.
-
- Can also be used as an asynchronous iterator::
+ See :ref:`asyncio` for how to use with :class:`can.Notifier`.
- async for msg in reader:
- print(msg)
- """
+ Can also be used as an asynchronous iterator::
+
+ async for msg in reader:
+ print(msg)
+ """
- def __init__(self, loop=None):
- # set to "infinite" size
- self.buffer = asyncio.Queue(loop=loop)
+ def __init__(self, **kwargs: Any) -> None:
+ self._is_stopped: bool = False
+ self.buffer: asyncio.Queue[Message]
- def on_message_received(self, msg):
- """Append a message to the buffer.
-
- Must only be called inside an event loop!
- """
+ if "loop" in kwargs:
+ warnings.warn(
+ "The 'loop' argument is deprecated since python-can 4.0.0 "
+ "and has no effect starting with Python 3.10",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ self.buffer = asyncio.Queue()
+
+ def on_message_received(self, msg: Message) -> None:
+ """Append a message to the buffer.
+
+ Must only be called inside an event loop!
+ """
+ if not self._is_stopped:
self.buffer.put_nowait(msg)
- def get_message(self):
- """
- Retrieve the latest message when awaited for::
-
- msg = await reader.get_message()
-
- :rtype: can.Message
- :return: The CAN message.
- """
- return self.buffer.get()
-
- def __aiter__(self):
- return self
-
- def __anext__(self):
- return self.buffer.get()
+ async def get_message(self) -> Message:
+ """
+ Retrieve the latest message when awaited for::
+
+ msg = await reader.get_message()
+
+ :return: The CAN message.
+ """
+ return await self.buffer.get()
+
+ def __aiter__(self) -> AsyncIterator[Message]:
+ return self
+
+ async def __anext__(self) -> Message:
+ return await self.buffer.get()
+
+ def stop(self) -> None:
+ self._is_stopped = True
diff --git a/can/logconvert.py b/can/logconvert.py
new file mode 100644
index 000000000..4527dc23e
--- /dev/null
+++ b/can/logconvert.py
@@ -0,0 +1,69 @@
+"""
+Convert a log file from one format to another.
+"""
+
+import argparse
+import errno
+import sys
+from typing import TYPE_CHECKING, NoReturn
+
+from can import Logger, LogReader, SizedRotatingLogger
+
+if TYPE_CHECKING:
+ from can.io.generic import MessageWriter
+
+
+class ArgumentParser(argparse.ArgumentParser):
+ def error(self, message: str) -> NoReturn:
+ self.print_help(sys.stderr)
+ self.exit(errno.EINVAL, f"{self.prog}: error: {message}\n")
+
+
+def main() -> None:
+ parser = ArgumentParser(
+ description="Convert a log file from one format to another.",
+ )
+
+ parser.add_argument(
+ "-s",
+ "--file_size",
+ dest="file_size",
+ type=int,
+ help="Maximum file size in bytes. Rotate log file when size threshold is reached.",
+ default=None,
+ )
+
+ parser.add_argument(
+ "input",
+ metavar="INFILE",
+ type=str,
+ help="Input filename. The type is dependent on the suffix, see can.LogReader.",
+ )
+
+ parser.add_argument(
+ "output",
+ metavar="OUTFILE",
+ type=str,
+ help="Output filename. The type is dependent on the suffix, see can.Logger.",
+ )
+
+ args = parser.parse_args()
+
+ with LogReader(args.input) as reader:
+ if args.file_size:
+ logger: MessageWriter = SizedRotatingLogger(
+ base_filename=args.output, max_bytes=args.file_size
+ )
+ else:
+ logger = Logger(filename=args.output)
+
+ with logger:
+ try:
+ for m in reader:
+ logger(m)
+ except KeyboardInterrupt:
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/can/logger.py b/can/logger.py
index bf68d856c..537356643 100644
--- a/can/logger.py
+++ b/can/logger.py
@@ -1,111 +1,128 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-"""
-logger.py logs CAN traffic to the terminal and to a file on disk.
-
- logger.py can0
-
-See candump in the can-utils package for a C implementation.
-Efficient filtering has been implemented for the socketcan backend.
-For example the command
-
- logger.py can0 F03000:FFF000
-
-Will filter for can frames with a can_id containing XXF03XXX.
-
-Dynamic Controls 2010
-"""
-
-from __future__ import absolute_import, print_function
-
-import sys
import argparse
-import socket
+import errno
+import sys
from datetime import datetime
+from typing import (
+ TYPE_CHECKING,
+)
+
+from can import BusState, Logger, SizedRotatingLogger
+from can.cli import (
+ _add_extra_args,
+ _parse_additional_config,
+ _set_logging_level_from_namespace,
+ add_bus_arguments,
+ create_bus_from_namespace,
+)
+from can.typechecking import TAdditionalCliArgs
+
+if TYPE_CHECKING:
+ from can.io import BaseRotatingLogger
+ from can.io.generic import MessageWriter
+
+
+def _parse_logger_args(
+ args: list[str],
+) -> tuple[argparse.Namespace, TAdditionalCliArgs]:
+ """Parse command line arguments for logger script."""
-import can
-from can import Bus, BusState, Logger
-
-
-def main():
parser = argparse.ArgumentParser(
- "python -m can.logger",
- description="Log CAN traffic, printing messages to stdout or to a given file.")
-
- parser.add_argument("-f", "--file_name", dest="log_file",
- help="""Path and base log filename, for supported types see can.Logger.""",
- default=None)
-
- parser.add_argument("-v", action="count", dest="verbosity",
- help='''How much information do you want to see at the command line?
- You can add several of these e.g., -vv is DEBUG''', default=2)
-
- parser.add_argument('-c', '--channel', help='''Most backend interfaces require some sort of channel.
- For example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0"
- With the socketcan interfaces valid channel examples include: "can0", "vcan0"''')
-
- parser.add_argument('-i', '--interface', dest="interface",
- help='''Specify the backend CAN interface to use. If left blank,
- fall back to reading from configuration files.''',
- choices=can.VALID_INTERFACES)
-
- parser.add_argument('--filter', help='''Comma separated filters can be specified for the given CAN interface:
- : (matches when & mask == can_id & mask)
- ~ (matches when & mask != can_id & mask)
- ''', nargs=argparse.REMAINDER, default='')
-
- parser.add_argument('-b', '--bitrate', type=int,
- help='''Bitrate to use for the CAN bus.''')
-
- group = parser.add_mutually_exclusive_group(required=False)
- group.add_argument('--active', help="Start the bus as active, this is applied the default.",
- action='store_true')
- group.add_argument('--passive', help="Start the bus as passive.",
- action='store_true')
-
- # print help message when no arguments wre given
- if len(sys.argv) < 2:
+ description="Log CAN traffic, printing messages to stdout or to a "
+ "given file.",
+ )
+
+ logger_group = parser.add_argument_group("logger arguments")
+
+ logger_group.add_argument(
+ "-f",
+ "--file_name",
+ dest="log_file",
+ help="Path and base log filename, for supported types see can.Logger.",
+ default=None,
+ )
+
+ logger_group.add_argument(
+ "-a",
+ "--append",
+ dest="append",
+ help="Append to the log file if it already exists.",
+ action="store_true",
+ )
+
+ logger_group.add_argument(
+ "-s",
+ "--file_size",
+ dest="file_size",
+ type=int,
+ help="Maximum file size in bytes. Rotate log file when size threshold "
+ "is reached. (The resulting file sizes will be consistent, but are not "
+ "guaranteed to be exactly what is specified here due to the rollover "
+ "conditions being logger implementation specific.)",
+ default=None,
+ )
+
+ logger_group.add_argument(
+ "-v",
+ action="count",
+ dest="verbosity",
+ help="""How much information do you want to see at the command line?
+ You can add several of these e.g., -vv is DEBUG""",
+ default=2,
+ )
+
+ state_group = logger_group.add_mutually_exclusive_group(required=False)
+ state_group.add_argument(
+ "--active",
+ help="Start the bus as active, this is applied by default.",
+ action="store_true",
+ )
+ state_group.add_argument(
+ "--passive", help="Start the bus as passive.", action="store_true"
+ )
+
+ # handle remaining arguments
+ _add_extra_args(logger_group)
+
+ # add bus options
+ add_bus_arguments(parser, filter_arg=True)
+
+ # print help message when no arguments were given
+ if not args:
parser.print_help(sys.stderr)
- import errno
raise SystemExit(errno.EINVAL)
- results = parser.parse_args()
-
- verbosity = results.verbosity
-
- logging_level_name = ['critical', 'error', 'warning', 'info', 'debug', 'subdebug'][min(5, verbosity)]
- can.set_logging_level(logging_level_name)
-
- can_filters = []
- if len(results.filter) > 0:
- print('Adding filter/s', results.filter)
- for filt in results.filter:
- if ':' in filt:
- _ = filt.split(":")
- can_id, can_mask = int(_[0], base=16), int(_[1], base=16)
- elif "~" in filt:
- can_id, can_mask = filt.split("~")
- can_id = int(can_id, base=16) | 0x20000000 # CAN_INV_FILTER
- can_mask = int(can_mask, base=16) & socket.CAN_ERR_FLAG
- can_filters.append({"can_id": can_id, "can_mask": can_mask})
-
- config = {"can_filters": can_filters, "single_handle": True}
- if results.interface:
- config["interface"] = results.interface
- if results.bitrate:
- config["bitrate"] = results.bitrate
- bus = Bus(results.channel, **config)
+ results, unknown_args = parser.parse_known_args(args)
+ additional_config = _parse_additional_config([*results.extra_args, *unknown_args])
+ return results, additional_config
+
+
+def main() -> None:
+ results, additional_config = _parse_logger_args(sys.argv[1:])
+ bus = create_bus_from_namespace(results)
+ _set_logging_level_from_namespace(results)
if results.active:
bus.state = BusState.ACTIVE
-
- if results.passive:
+ elif results.passive:
bus.state = BusState.PASSIVE
- print('Connected to {}: {}'.format(bus.__class__.__name__, bus.channel_info))
- print('Can Logger (Started on {})\n'.format(datetime.now()))
- logger = Logger(results.log_file)
+ print(f"Connected to {bus.__class__.__name__}: {bus.channel_info}")
+ print(f"Can Logger (Started on {datetime.now()})")
+
+ logger: MessageWriter | BaseRotatingLogger
+ if results.file_size:
+ logger = SizedRotatingLogger(
+ base_filename=results.log_file,
+ max_bytes=results.file_size,
+ append=results.append,
+ **additional_config,
+ )
+ else:
+ logger = Logger(
+ filename=results.log_file,
+ append=results.append,
+ **additional_config,
+ )
try:
while True:
@@ -118,5 +135,6 @@ def main():
bus.shutdown()
logger.stop()
+
if __name__ == "__main__":
main()
diff --git a/can/message.py b/can/message.py
index 0898f99fc..3e60ca641 100644
--- a/can/message.py
+++ b/can/message.py
@@ -1,49 +1,90 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
This module contains the implementation of :class:`can.Message`.
+
+.. note::
+ Could use `@dataclass `__
+ starting with Python 3.7.
"""
-import logging
-logger = logging.getLogger(__name__)
+from copy import deepcopy
+from math import isinf, isnan
+from typing import Any
+
+from . import typechecking
-class Message(object):
+class Message: # pylint: disable=too-many-instance-attributes; OK for a dataclass
"""
The :class:`~can.Message` object is used to represent CAN messages for
- both sending and receiving.
+ sending, receiving and other purposes like converting between different
+ logging formats.
Messages can use extended identifiers, be remote or error frames, contain
- data and can be associated to a channel.
+ data and may be associated to a channel.
- When testing for equality of the messages, the timestamp and the channel
- is not used for comparing.
+ Messages are always compared by identity and never by value, because that
+ may introduce unexpected behaviour. See also :meth:`~can.Message.equals`.
- .. note::
-
- This class does not strictly check the input. Thus, the caller must
- prevent the creation of invalid messages. Possible problems include
- the `dlc` field not matching the length of `data` or creating a message
- with both `is_remote_frame` and `is_error_frame` set to True.
+ :func:`~copy.copy`/:func:`~copy.deepcopy` is supported as well.
+ Messages do not support "dynamic" attributes, meaning any others than the
+ documented ones, since it uses :obj:`~object.__slots__`.
"""
- def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True,
- is_error_frame=False, arbitration_id=0, dlc=None, data=None,
- is_fd=False, bitrate_switch=False, error_state_indicator=False,
- channel=None):
+ __slots__ = (
+ "__weakref__", # support weak references to messages
+ "arbitration_id",
+ "bitrate_switch",
+ "channel",
+ "data",
+ "dlc",
+ "error_state_indicator",
+ "is_error_frame",
+ "is_extended_id",
+ "is_fd",
+ "is_remote_frame",
+ "is_rx",
+ "timestamp",
+ )
- self.timestamp = timestamp
- self.id_type = extended_id
- self.is_extended_id = extended_id
+ def __init__( # pylint: disable=too-many-locals, too-many-arguments
+ self,
+ timestamp: float = 0.0,
+ arbitration_id: int = 0,
+ is_extended_id: bool = True,
+ is_remote_frame: bool = False,
+ is_error_frame: bool = False,
+ channel: typechecking.Channel | None = None,
+ dlc: int | None = None,
+ data: typechecking.CanData | None = None,
+ is_fd: bool = False,
+ is_rx: bool = True,
+ bitrate_switch: bool = False,
+ error_state_indicator: bool = False,
+ check: bool = False,
+ ):
+ """
+ To create a message object, simply provide any of the below attributes
+ together with additional parameters as keyword arguments to the constructor.
+
+ :param check: By default, the constructor of this class does not strictly check the input.
+ Thus, the caller must prevent the creation of invalid messages or
+ set this parameter to `True`, to raise an Error on invalid inputs.
+ Possible problems include the `dlc` field not matching the length of `data`
+ or creating a message with both `is_remote_frame` and `is_error_frame` set
+ to `True`.
+ :raises ValueError:
+ If and only if `check` is set to `True` and one or more arguments were invalid
+ """
+ self.timestamp = timestamp
+ self.arbitration_id = arbitration_id
+ self.is_extended_id = is_extended_id
self.is_remote_frame = is_remote_frame
self.is_error_frame = is_error_frame
- self.arbitration_id = arbitration_id
self.channel = channel
-
self.is_fd = is_fd
+ self.is_rx = is_rx
self.bitrate_switch = bitrate_switch
self.error_state_indicator = error_state_indicator
@@ -54,116 +95,237 @@ def __init__(self, timestamp=0.0, is_remote_frame=False, extended_id=True,
else:
try:
self.data = bytearray(data)
- except TypeError:
- err = "Couldn't create message from {} ({})".format(data, type(data))
- raise TypeError(err)
+ except TypeError as error:
+ err = f"Couldn't create message from {data} ({type(data)})"
+ raise TypeError(err) from error
if dlc is None:
self.dlc = len(self.data)
else:
self.dlc = dlc
- if is_fd and self.dlc > 64:
- logger.warning("data link count was %d but it should be less than or equal to 64", self.dlc)
- if not is_fd and self.dlc > 8:
- logger.warning("data link count was %d but it should be less than or equal to 8", self.dlc)
+ if check:
+ self._check()
- def __str__(self):
- field_strings = ["Timestamp: {0:>15.6f}".format(self.timestamp)]
- if self.id_type:
- # Extended arbitrationID
- arbitration_id_string = "ID: {0:08x}".format(self.arbitration_id)
+ def __str__(self) -> str:
+ field_strings = [f"Timestamp: {self.timestamp:>15.6f}"]
+ if self.is_extended_id:
+ arbitration_id_string = f"{self.arbitration_id:08x}"
else:
- arbitration_id_string = "ID: {0:04x}".format(self.arbitration_id)
- field_strings.append(arbitration_id_string.rjust(12, " "))
+ arbitration_id_string = f"{self.arbitration_id:03x}"
+ field_strings.append(f"ID: {arbitration_id_string:>8}")
- flag_string = " ".join([
- "X" if self.id_type else "S",
- "E" if self.is_error_frame else " ",
- "R" if self.is_remote_frame else " ",
- "F" if self.is_fd else " ",
- ])
+ flag_string = " ".join(
+ [
+ "X" if self.is_extended_id else "S",
+ "Rx" if self.is_rx else "Tx",
+ "E" if self.is_error_frame else " ",
+ "R" if self.is_remote_frame else " ",
+ "F" if self.is_fd else " ",
+ "BS" if self.bitrate_switch else " ",
+ "EI" if self.error_state_indicator else " ",
+ ]
+ )
field_strings.append(flag_string)
- field_strings.append("DLC: {0:2d}".format(self.dlc))
- data_strings = []
+ field_strings.append(f"DL: {self.dlc:2d}")
+ data_strings = ""
if self.data is not None:
- for index in range(0, min(self.dlc, len(self.data))):
- data_strings.append("{0:02x}".format(self.data[index]))
- if data_strings: # if not empty
- field_strings.append(" ".join(data_strings).ljust(24, " "))
+ data_strings = self.data[: min(self.dlc, len(self.data))].hex(" ")
+ if data_strings: # if not empty
+ field_strings.append(data_strings.ljust(24, " "))
else:
field_strings.append(" " * 24)
if (self.data is not None) and (self.data.isalnum()):
+ field_strings.append(f"'{self.data.decode('utf-8', 'replace')}'")
+
+ if self.channel is not None:
try:
- field_strings.append("'{}'".format(self.data.decode('utf-8')))
- except UnicodeError:
+ field_strings.append(f"Channel: {self.channel}")
+ except UnicodeEncodeError:
pass
return " ".join(field_strings).strip()
- def __len__(self):
- return len(self.data)
+ def __len__(self) -> int:
+ # return the dlc such that it also works on remote frames
+ return self.dlc
- def __bool__(self):
+ def __bool__(self) -> bool:
return True
- def __nonzero__(self):
- return self.__bool__()
-
- def __repr__(self):
- data = ["{:#02x}".format(byte) for byte in self.data]
- args = ["timestamp={}".format(self.timestamp),
- "is_remote_frame={}".format(self.is_remote_frame),
- "extended_id={}".format(self.id_type),
- "is_error_frame={}".format(self.is_error_frame),
- "arbitration_id={:#x}".format(self.arbitration_id),
- "dlc={}".format(self.dlc),
- "data=[{}]".format(", ".join(data))]
+ def __repr__(self) -> str:
+ args = [
+ f"timestamp={self.timestamp}",
+ f"arbitration_id={self.arbitration_id:#x}",
+ f"is_extended_id={self.is_extended_id}",
+ ]
+
+ if not self.is_rx:
+ args.append("is_rx=False")
+
+ if self.is_remote_frame:
+ args.append(f"is_remote_frame={self.is_remote_frame}")
+
+ if self.is_error_frame:
+ args.append(f"is_error_frame={self.is_error_frame}")
+
if self.channel is not None:
- args.append("channel={!r}".format(self.channel))
+ args.append(f"channel={self.channel!r}")
+
+ data = [f"{byte:#02x}" for byte in self.data]
+ args += [f"dlc={self.dlc}", f"data=[{', '.join(data)}]"]
+
if self.is_fd:
args.append("is_fd=True")
- args.append("bitrate_switch={}".format(self.bitrate_switch))
- args.append("error_state_indicator={}".format(self.error_state_indicator))
- return "can.Message({})".format(", ".join(args))
-
- def __eq__(self, other):
- if isinstance(other, self.__class__):
- return (
- self.arbitration_id == other.arbitration_id and
- #self.timestamp == other.timestamp and # allow the timestamp to differ
- self.id_type == other.id_type and
- self.dlc == other.dlc and
- self.data == other.data and
- self.is_remote_frame == other.is_remote_frame and
- self.is_error_frame == other.is_error_frame and
- self.is_fd == other.is_fd and
- self.bitrate_switch == other.bitrate_switch
- )
- else:
- return NotImplemented
+ args.append(f"bitrate_switch={self.bitrate_switch}")
+ args.append(f"error_state_indicator={self.error_state_indicator}")
+
+ return f"can.Message({', '.join(args)})"
- def __ne__(self, other):
- if isinstance(other, self.__class__):
- return not self.__eq__(other)
+ def __format__(self, format_spec: str | None) -> str:
+ if not format_spec:
+ return self.__str__()
else:
- return NotImplemented
-
- def __hash__(self):
- return hash((
- self.arbitration_id,
- # self.timestamp # excluded, like in self.__eq__(self, other)
- self.id_type,
- self.dlc,
- self.data,
- self.is_fd,
- self.bitrate_switch,
- self.is_remote_frame,
- self.is_error_frame
- ))
-
- def __format__(self, format_spec):
- return self.__str__()
+ raise ValueError("non-empty format_specs are not supported")
+
+ def __bytes__(self) -> bytes:
+ return bytes(self.data)
+
+ def __copy__(self) -> "Message":
+ return Message(
+ timestamp=self.timestamp,
+ arbitration_id=self.arbitration_id,
+ is_extended_id=self.is_extended_id,
+ is_remote_frame=self.is_remote_frame,
+ is_error_frame=self.is_error_frame,
+ channel=self.channel,
+ dlc=self.dlc,
+ data=self.data,
+ is_fd=self.is_fd,
+ is_rx=self.is_rx,
+ bitrate_switch=self.bitrate_switch,
+ error_state_indicator=self.error_state_indicator,
+ )
+
+ def __deepcopy__(self, memo: dict[int, Any] | None) -> "Message":
+ return Message(
+ timestamp=self.timestamp,
+ arbitration_id=self.arbitration_id,
+ is_extended_id=self.is_extended_id,
+ is_remote_frame=self.is_remote_frame,
+ is_error_frame=self.is_error_frame,
+ channel=deepcopy(self.channel, memo),
+ dlc=self.dlc,
+ data=deepcopy(self.data, memo),
+ is_fd=self.is_fd,
+ is_rx=self.is_rx,
+ bitrate_switch=self.bitrate_switch,
+ error_state_indicator=self.error_state_indicator,
+ )
+
+ def _check(self) -> None:
+ """Checks if the message parameters are valid.
+
+ Assumes that the attribute types are already correct.
+
+ :raises ValueError: If and only if one or more attributes are invalid
+ """
+
+ if self.timestamp < 0.0:
+ raise ValueError("the timestamp may not be negative")
+ if isinf(self.timestamp):
+ raise ValueError("the timestamp may not be infinite")
+ if isnan(self.timestamp):
+ raise ValueError("the timestamp may not be NaN")
+
+ if self.is_remote_frame:
+ if self.is_error_frame:
+ raise ValueError(
+ "a message cannot be a remote and an error frame at the same time"
+ )
+ if self.is_fd:
+ raise ValueError("CAN FD does not support remote frames")
+
+ if self.arbitration_id < 0:
+ raise ValueError("arbitration IDs may not be negative")
+
+ if self.is_extended_id:
+ if self.arbitration_id >= 0x20000000:
+ raise ValueError("Extended arbitration IDs must be less than 2^29")
+ elif self.arbitration_id >= 0x800:
+ raise ValueError("Normal arbitration IDs must be less than 2^11")
+
+ if self.dlc < 0:
+ raise ValueError("DLC may not be negative")
+ if self.is_fd:
+ if self.dlc > 64:
+ raise ValueError(
+ f"DLC was {self.dlc} but it should be <= 64 for CAN FD frames"
+ )
+ elif self.dlc > 8:
+ raise ValueError(
+ f"DLC was {self.dlc} but it should be <= 8 for normal CAN frames"
+ )
+
+ if self.is_remote_frame:
+ if self.data:
+ raise ValueError("remote frames may not carry any data")
+ elif self.dlc != len(self.data):
+ raise ValueError(
+ "the DLC and the length of the data must match up for non remote frames"
+ )
+
+ if not self.is_fd:
+ if self.bitrate_switch:
+ raise ValueError("bitrate switch is only allowed for CAN FD frames")
+ if self.error_state_indicator:
+ raise ValueError(
+ "error state indicator is only allowed for CAN FD frames"
+ )
+
+ def equals(
+ self,
+ other: "Message",
+ timestamp_delta: float | None = 1.0e-6,
+ check_channel: bool = True,
+ check_direction: bool = True,
+ ) -> bool:
+ """
+ Compares a given message with this one.
+
+ :param other: the message to compare with
+ :param timestamp_delta: the maximum difference in seconds at which two timestamps are
+ still considered equal or `None` to not compare timestamps
+ :param check_channel: whether to compare the message channel
+ :param check_direction: whether to compare the messages' directions (Tx/Rx)
+
+ :return: True if and only if the given message equals this one
+ """
+ # see https://github.com/hardbyte/python-can/pull/413 for a discussion
+ # on why a delta of 1.0e-6 was chosen
+ return (
+ # check for identity first and finish fast
+ self is other
+ or
+ # then check for equality by value
+ (
+ (
+ timestamp_delta is None
+ or abs(self.timestamp - other.timestamp) <= timestamp_delta
+ )
+ and (self.is_rx == other.is_rx or not check_direction)
+ and self.arbitration_id == other.arbitration_id
+ and self.is_extended_id == other.is_extended_id
+ and self.dlc == other.dlc
+ and self.data == other.data
+ and self.is_remote_frame == other.is_remote_frame
+ and self.is_error_frame == other.is_error_frame
+ and (self.channel == other.channel or not check_channel)
+ and self.is_fd == other.is_fd
+ and self.bitrate_switch == other.bitrate_switch
+ and self.error_state_indicator == other.error_state_indicator
+ )
+ )
diff --git a/can/notifier.py b/can/notifier.py
index 91af82a87..cb91cf7b4 100644
--- a/can/notifier.py
+++ b/can/notifier.py
@@ -1,142 +1,327 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
This module contains the implementation of :class:`~can.Notifier`.
"""
-import threading
+import asyncio
+import functools
import logging
+import threading
import time
-try:
- import asyncio
-except ImportError:
- asyncio = None
+from collections.abc import Awaitable, Callable, Iterable
+from contextlib import AbstractContextManager
+from types import TracebackType
+from typing import (
+ Any,
+ Final,
+ NamedTuple,
+)
+
+from can.bus import BusABC
+from can.listener import Listener
+from can.message import Message
+
+logger = logging.getLogger("can.Notifier")
+
+MessageRecipient = Listener | Callable[[Message], Awaitable[None] | None]
+
+
+class _BusNotifierPair(NamedTuple):
+ bus: "BusABC"
+ notifier: "Notifier"
+
+
+class _NotifierRegistry:
+ """A registry to manage the association between CAN buses and Notifiers.
+
+ This class ensures that a bus is not added to multiple active Notifiers.
+ """
+
+ def __init__(self) -> None:
+ """Initialize the registry with an empty list of bus-notifier pairs and a threading lock."""
+ self.pairs: list[_BusNotifierPair] = []
+ self.lock = threading.Lock()
+
+ def register(self, bus: BusABC, notifier: "Notifier") -> None:
+ """Register a bus and its associated notifier.
+
+ Ensures that a bus is not added to multiple active :class:`~can.Notifier` instances.
+
+ :param bus:
+ The CAN bus to register.
+ :param notifier:
+ The :class:`~can.Notifier` instance associated with the bus.
+ :raises ValueError:
+ If the bus is already assigned to an active Notifier.
+ """
+ with self.lock:
+ for pair in self.pairs:
+ if bus is pair.bus and not pair.notifier.stopped:
+ raise ValueError(
+ "A bus can not be added to multiple active Notifier instances."
+ )
+ self.pairs.append(_BusNotifierPair(bus, notifier))
+
+ def unregister(self, bus: BusABC, notifier: "Notifier") -> None:
+ """Unregister a bus and its associated notifier.
+
+ Removes the bus-notifier pair from the registry.
+
+ :param bus:
+ The CAN bus to unregister.
+ :param notifier:
+ The :class:`~can.Notifier` instance associated with the bus.
+ """
+ with self.lock:
+ registered_pairs_to_remove: list[_BusNotifierPair] = []
+ for pair in self.pairs:
+ if pair.bus is bus and pair.notifier is notifier:
+ registered_pairs_to_remove.append(pair)
+ for pair in registered_pairs_to_remove:
+ self.pairs.remove(pair)
+
+ def find_instances(self, bus: BusABC) -> tuple["Notifier", ...]:
+ """Find the :class:`~can.Notifier` instances associated with a given CAN bus.
+
+ This method searches the registry for the :class:`~can.Notifier`
+ that is linked to the specified bus. If the bus is found, the
+ corresponding :class:`~can.Notifier` instances are returned. If the bus is not
+ found in the registry, an empty tuple is returned.
+
+ :param bus:
+ The CAN bus for which to find the associated :class:`~can.Notifier` .
+ :return:
+ A tuple of :class:`~can.Notifier` instances associated with the given bus.
+ """
+ instance_list = []
+ with self.lock:
+ for pair in self.pairs:
+ if bus is pair.bus:
+ instance_list.append(pair.notifier)
+ return tuple(instance_list)
-logger = logging.getLogger('can.Notifier')
+class Notifier(AbstractContextManager["Notifier"]):
-class Notifier(object):
+ _registry: Final = _NotifierRegistry()
- def __init__(self, bus, listeners, timeout=1.0, loop=None):
- """Manages the distribution of **Messages** from a given bus/buses to a
- list of listeners.
+ def __init__(
+ self,
+ bus: BusABC | list[BusABC],
+ listeners: Iterable[MessageRecipient],
+ timeout: float = 1.0,
+ loop: asyncio.AbstractEventLoop | None = None,
+ ) -> None:
+ """Manages the distribution of :class:`~can.Message` instances to listeners.
- :param can.BusABC bus: A :ref:`bus` or a list of buses to listen to.
- :param list listeners: An iterable of :class:`~can.Listener`
- :param float timeout: An optional maximum number of seconds to wait for any message.
- :param asyncio.AbstractEventLoop loop:
- An :mod:`asyncio` event loop to schedule listeners in.
+ Supports multiple buses and listeners.
+
+ .. Note::
+
+ Remember to call :meth:`~can.Notifier.stop` after all messages are received as
+ many listeners carry out flush operations to persist data.
+
+
+ :param bus:
+ A :ref:`bus` or a list of buses to consume messages from.
+ :param listeners:
+ An iterable of :class:`~can.Listener` or callables that receive a :class:`~can.Message`
+ and return nothing.
+ :param timeout:
+ An optional maximum number of seconds to wait for any :class:`~can.Message`.
+ :param loop:
+ An :mod:`asyncio` event loop to schedule the ``listeners`` in.
+ :raises ValueError:
+ If a passed in *bus* is already assigned to an active :class:`~can.Notifier`.
"""
- self.listeners = listeners
- self.bus = bus
+ self.listeners: list[MessageRecipient] = list(listeners)
+ self._bus_list: list[BusABC] = []
self.timeout = timeout
self._loop = loop
#: Exception raised in thread
- self.exception = None
+ self.exception: Exception | None = None
- self._running = True
+ self._stopped = False
self._lock = threading.Lock()
- self._readers = []
- buses = self.bus if isinstance(self.bus, list) else [self.bus]
- for bus in buses:
- self.add_bus(bus)
+ self._readers: list[int | threading.Thread] = []
+ self._tasks: set[asyncio.Task] = set()
+ _bus_list: list[BusABC] = bus if isinstance(bus, list) else [bus]
+ for each_bus in _bus_list:
+ self.add_bus(each_bus)
+
+ @property
+ def bus(self) -> BusABC | tuple["BusABC", ...]:
+ """Return the associated bus or a tuple of buses."""
+ if len(self._bus_list) == 1:
+ return self._bus_list[0]
+ return tuple(self._bus_list)
- def add_bus(self, bus):
+ def add_bus(self, bus: BusABC) -> None:
"""Add a bus for notification.
- :param can.BusABC bus:
+ :param bus:
CAN bus instance.
+ :raises ValueError:
+ If the *bus* is already assigned to an active :class:`~can.Notifier`.
"""
- if self._loop is not None and hasattr(bus, 'fileno') and bus.fileno() >= 0:
- # Use file descriptor to watch for messages
- reader = bus.fileno()
- self._loop.add_reader(reader, self._on_message_available, bus)
+ # add bus to notifier registry
+ Notifier._registry.register(bus, self)
+
+ # add bus to internal bus list
+ self._bus_list.append(bus)
+
+ file_descriptor: int = -1
+ try:
+ file_descriptor = bus.fileno()
+ except NotImplementedError:
+ # Bus doesn't support fileno, we fall back to thread based reader
+ pass
+
+ if self._loop is not None and file_descriptor >= 0:
+ # Use bus file descriptor to watch for messages
+ self._loop.add_reader(file_descriptor, self._on_message_available, bus)
+ self._readers.append(file_descriptor)
else:
- reader = threading.Thread(target=self._rx_thread, args=(bus,),
- name='can.notifier for bus "{}"'.format(bus.channel_info))
- reader.daemon = True
- reader.start()
- self._readers.append(reader)
+ reader_thread = threading.Thread(
+ target=self._rx_thread,
+ args=(bus,),
+ name=f'{self.__class__.__qualname__} for bus "{bus.channel_info}"',
+ )
+ reader_thread.daemon = True
+ reader_thread.start()
+ self._readers.append(reader_thread)
- def stop(self, timeout=5):
+ def stop(self, timeout: float = 5.0) -> None:
"""Stop notifying Listeners when new :class:`~can.Message` objects arrive
and call :meth:`~can.Listener.stop` on each Listener.
- :param float timeout:
+ :param timeout:
Max time in seconds to wait for receive threads to finish.
Should be longer than timeout given at instantiation.
"""
- self._running = False
+ self._stopped = True
end_time = time.time() + timeout
for reader in self._readers:
if isinstance(reader, threading.Thread):
now = time.time()
if now < end_time:
reader.join(end_time - now)
- else:
+ elif self._loop:
# reader is a file descriptor
self._loop.remove_reader(reader)
for listener in self.listeners:
- if hasattr(listener, 'stop'):
+ if hasattr(listener, "stop"):
listener.stop()
- def _rx_thread(self, bus):
- msg = None
- try:
- while self._running:
- if msg is not None:
+ # remove bus from registry
+ for bus in self._bus_list:
+ Notifier._registry.unregister(bus, self)
+
+ def _rx_thread(self, bus: BusABC) -> None:
+ # determine message handling callable early, not inside while loop
+ if self._loop:
+ handle_message: Callable[[Message], Any] = functools.partial(
+ self._loop.call_soon_threadsafe,
+ self._on_message_received, # type: ignore[arg-type]
+ )
+ else:
+ handle_message = self._on_message_received
+
+ while not self._stopped:
+ try:
+ if msg := bus.recv(self.timeout):
with self._lock:
- if self._loop is not None:
- self._loop.call_soon_threadsafe(
- self._on_message_received, msg)
- else:
- self._on_message_received(msg)
- msg = bus.recv(self.timeout)
- except Exception as exc:
- self.exception = exc
- if self._loop is not None:
- self._loop.call_soon_threadsafe(self._on_error, exc)
- else:
- self._on_error(exc)
- raise
-
- def _on_message_available(self, bus):
- msg = bus.recv(0)
- if msg is not None:
+ handle_message(msg)
+ except Exception as exc: # pylint: disable=broad-except
+ self.exception = exc
+ if self._loop is not None:
+ self._loop.call_soon_threadsafe(self._on_error, exc)
+ # Raise anyway
+ raise
+ elif not self._on_error(exc):
+ # If it was not handled, raise the exception here
+ raise
+ else:
+ # It was handled, so only log it
+ logger.debug("suppressed exception: %s", exc)
+
+ def _on_message_available(self, bus: BusABC) -> None:
+ if msg := bus.recv(0):
self._on_message_received(msg)
- def _on_message_received(self, msg):
+ def _on_message_received(self, msg: Message) -> None:
for callback in self.listeners:
res = callback(msg)
- if self._loop is not None and asyncio.iscoroutine(res):
- # Schedule coroutine
- self._loop.create_task(res)
+ if res and self._loop and asyncio.iscoroutine(res):
+ # Schedule coroutine and keep a reference to the task
+ task = self._loop.create_task(res)
+ self._tasks.add(task)
+ task.add_done_callback(self._tasks.discard)
+
+ def _on_error(self, exc: Exception) -> bool:
+ """Calls ``on_error()`` for all listeners if they implement it.
+
+ :returns: ``True`` if at least one error handler was called.
+ """
+ was_handled = False
- def _on_error(self, exc):
for listener in self.listeners:
- if hasattr(listener, 'on_error'):
- listener.on_error(exc)
+ if hasattr(listener, "on_error"):
+ try:
+ listener.on_error(exc)
+ except NotImplementedError:
+ pass
+ else:
+ was_handled = True
+
+ return was_handled
- def add_listener(self, listener):
- """Add new Listener to the notification list.
+ def add_listener(self, listener: MessageRecipient) -> None:
+ """Add new Listener to the notification list.
If it is already present, it will be called two times
each time a message arrives.
- :param can.Listener listener: Listener to be added to
- the list to be notified
+ :param listener: Listener to be added to the list to be notified
"""
self.listeners.append(listener)
- def remove_listener(self, listener):
+ def remove_listener(self, listener: MessageRecipient) -> None:
"""Remove a listener from the notification list. This method
- trows an exception if the given listener is not part of the
+ throws an exception if the given listener is not part of the
stored listeners.
- :param can.Listener listener: Listener to be removed from
- the list to be notified
+ :param listener: Listener to be removed from the list to be notified
:raises ValueError: if `listener` was never added to this notifier
"""
self.listeners.remove(listener)
+
+ @property
+ def stopped(self) -> bool:
+ """Return ``True``, if Notifier was properly shut down with :meth:`~can.Notifier.stop`."""
+ return self._stopped
+
+ @staticmethod
+ def find_instances(bus: BusABC) -> tuple["Notifier", ...]:
+ """Find :class:`~can.Notifier` instances associated with a given CAN bus.
+
+ This method searches the registry for the :class:`~can.Notifier`
+ that is linked to the specified bus. If the bus is found, the
+ corresponding :class:`~can.Notifier` instances are returned. If the bus is not
+ found in the registry, an empty tuple is returned.
+
+ :param bus:
+ The CAN bus for which to find the associated :class:`~can.Notifier` .
+ :return:
+ A tuple of :class:`~can.Notifier` instances associated with the given bus.
+ """
+ return Notifier._registry.find_instances(bus)
+
+ def __exit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_value: BaseException | None,
+ traceback: TracebackType | None,
+ ) -> None:
+ if not self._stopped:
+ self.stop()
diff --git a/can/player.py b/can/player.py
index 5c1c0a23c..6190a58d8 100644
--- a/can/player.py
+++ b/can/player.py
@@ -1,6 +1,3 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Replays CAN traffic saved with can.logger back
to a CAN bus.
@@ -8,91 +5,160 @@
Similar to canplayer in the can-utils package.
"""
-from __future__ import absolute_import, print_function
-
-import sys
import argparse
+import errno
+import math
+import sys
from datetime import datetime
+from typing import TYPE_CHECKING, cast
-import can
-from can import Bus, LogReader, MessageSync
-
-
-def main():
- parser = argparse.ArgumentParser(
- "python -m can.player",
- description="Replay CAN traffic.")
-
- parser.add_argument("-f", "--file_name", dest="log_file",
- help="""Path and base log filename, for supported types see can.LogReader.""",
- default=None)
-
- parser.add_argument("-v", action="count", dest="verbosity",
- help='''Also print can frames to stdout.
- You can add several of these to enable debugging''', default=2)
-
- parser.add_argument('-c', '--channel',
- help='''Most backend interfaces require some sort of channel.
- For example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0"
- With the socketcan interfaces valid channel examples include: "can0", "vcan0"''')
+from can import LogReader, MessageSync
+from can.cli import (
+ _add_extra_args,
+ _parse_additional_config,
+ _set_logging_level_from_namespace,
+ add_bus_arguments,
+ create_bus_from_namespace,
+)
- parser.add_argument('-i', '--interface', dest="interface",
- help='''Specify the backend CAN interface to use. If left blank,
- fall back to reading from configuration files.''',
- choices=can.VALID_INTERFACES)
+if TYPE_CHECKING:
+ from collections.abc import Iterable
- parser.add_argument('-b', '--bitrate', type=int,
- help='''Bitrate to use for the CAN bus.''')
+ from can import Message
- parser.add_argument('--ignore-timestamps', dest='timestamps',
- help='''Ignore timestamps (send all frames immediately with minimum gap between frames)''',
- action='store_false')
- parser.add_argument('-g', '--gap', type=float, help=''' minimum time between replayed frames''',
- default=0.0001)
- parser.add_argument('-s', '--skip', type=float, default=60*60*24,
- help=''' skip gaps greater than 's' seconds''')
-
- parser.add_argument('infile', metavar='input-file', type=str,
- help='The file to replay. For supported types see can.LogReader.')
+def _parse_loop(value: str) -> int | float:
+ """Parse the loop argument, allowing integer or 'i' for infinite."""
+ if value == "i":
+ return float("inf")
+ try:
+ return int(value)
+ except ValueError as exc:
+ err_msg = "Loop count must be an integer or 'i' for infinite."
+ raise argparse.ArgumentTypeError(err_msg) from exc
+
+
+def _format_player_start_message(iteration: int, loop_count: int | float) -> str:
+ """
+ Generate a status message indicating the start of a CAN log replay iteration.
+
+ :param iteration:
+ The current loop iteration (zero-based).
+ :param loop_count:
+ Total number of replay loops, or infinity for endless replay.
+ :return:
+ A formatted string describing the replay start and loop information.
+ """
+ if loop_count < 2:
+ loop_info = ""
+ else:
+ loop_val = "∞" if math.isinf(loop_count) else str(loop_count)
+ loop_info = f" [loop {iteration + 1}/{loop_val}]"
+ return f"Can LogReader (Started on {datetime.now()}){loop_info}"
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="Replay CAN traffic.")
+
+ player_group = parser.add_argument_group("Player arguments")
+
+ player_group.add_argument(
+ "-v",
+ action="count",
+ dest="verbosity",
+ help="""Also print can frames to stdout.
+ You can add several of these to enable debugging""",
+ default=2,
+ )
+
+ player_group.add_argument(
+ "--ignore-timestamps",
+ dest="timestamps",
+ help="""Ignore timestamps (send all frames immediately with minimum gap between frames)""",
+ action="store_false",
+ )
+
+ player_group.add_argument(
+ "--error-frames",
+ help="Also send error frames to the interface.",
+ action="store_true",
+ )
+
+ player_group.add_argument(
+ "-g",
+ "--gap",
+ type=float,
+ help=" minimum time between replayed frames",
+ default=0.0001,
+ )
+ player_group.add_argument(
+ "-s",
+ "--skip",
+ type=float,
+ default=60 * 60 * 24,
+ help="Skip gaps greater than 's' seconds between messages. "
+ "Default is 86400 (24 hours), meaning only very large gaps are skipped. "
+ "Set to 0 to never skip any gaps (all delays are preserved). "
+ "Set to a very small value (e.g., 1e-4) "
+ "to skip all gaps and send messages as fast as possible.",
+ )
+ player_group.add_argument(
+ "-l",
+ "--loop",
+ type=_parse_loop,
+ metavar="NUM",
+ default=1,
+ help="Replay file NUM times. Use 'i' for infinite loop (default: 1)",
+ )
+ player_group.add_argument(
+ "infile",
+ metavar="input-file",
+ type=str,
+ help="The file to replay. For supported types see can.LogReader.",
+ )
+
+ # handle remaining arguments
+ _add_extra_args(player_group)
+
+ # add bus options
+ add_bus_arguments(parser)
# print help message when no arguments were given
if len(sys.argv) < 2:
parser.print_help(sys.stderr)
- import errno
raise SystemExit(errno.EINVAL)
- results = parser.parse_args()
+ results, unknown_args = parser.parse_known_args()
+ additional_config = _parse_additional_config([*results.extra_args, *unknown_args])
+ _set_logging_level_from_namespace(results)
verbosity = results.verbosity
- logging_level_name = ['critical', 'error', 'warning', 'info', 'debug', 'subdebug'][min(5, verbosity)]
- can.set_logging_level(logging_level_name)
-
- config = {"single_handle": True}
- if results.interface:
- config["interface"] = results.interface
- if results.bitrate:
- config["bitrate"] = results.bitrate
- bus = Bus(results.channel, **config)
-
- reader = LogReader(results.infile)
-
- in_sync = MessageSync(reader, timestamps=results.timestamps,
- gap=results.gap, skip=results.skip)
-
- print('Can LogReader (Started on {})'.format(datetime.now()))
-
- try:
- for m in in_sync:
- if verbosity >= 3:
- print(m)
- bus.send(m)
- except KeyboardInterrupt:
- pass
- finally:
- bus.shutdown()
- reader.stop()
+ error_frames = results.error_frames
+
+ with create_bus_from_namespace(results) as bus:
+ loop_count: int | float = results.loop
+ iteration = 0
+ try:
+ while iteration < loop_count:
+ with LogReader(results.infile, **additional_config) as reader:
+ in_sync = MessageSync(
+ cast("Iterable[Message]", reader),
+ timestamps=results.timestamps,
+ gap=results.gap,
+ skip=results.skip,
+ )
+ print(_format_player_start_message(iteration, loop_count))
+
+ for message in in_sync:
+ if message.is_error_frame and not error_frames:
+ continue
+ if verbosity >= 3:
+ print(message)
+ bus.send(message)
+ iteration += 1
+ except KeyboardInterrupt:
+ pass
if __name__ == "__main__":
diff --git a/can/py.typed b/can/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/can/thread_safe_bus.py b/can/thread_safe_bus.py
index 1b0f75a2d..518604364 100644
--- a/can/thread_safe_bus.py
+++ b/can/thread_safe_bus.py
@@ -1,42 +1,29 @@
-#!/usr/bin/env python
-# coding: utf-8
+from contextlib import nullcontext
+from threading import RLock
+from typing import TYPE_CHECKING, Any, cast
-from __future__ import print_function, absolute_import
+from . import typechecking
+from .bus import BusState, CanProtocol
+from .interface import Bus
+from .message import Message
-from threading import RLock
+if TYPE_CHECKING:
+ from .bus import BusABC
try:
# Only raise an exception on instantiation but allow module
# to be imported
from wrapt import ObjectProxy
+
import_exc = None
except ImportError as exc:
- ObjectProxy = object
+ ObjectProxy = None # type: ignore[misc,assignment]
import_exc = exc
-from .interface import Bus
-
-
-try:
- from contextlib import nullcontext
-
-except ImportError:
- class nullcontext(object):
- """A context manager that does nothing at all.
- A fallback for Python 3.7's :class:`contextlib.nullcontext` manager.
- """
-
- def __init__(self, enter_result=None):
- self.enter_result = enter_result
-
- def __enter__(self):
- return self.enter_result
-
- def __exit__(self, *args):
- pass
-
-class ThreadSafeBus(ObjectProxy):
+class ThreadSafeBus(
+ ObjectProxy
+): # pylint: disable=abstract-method # type: ignore[assignment]
"""
Contains a thread safe :class:`can.BusABC` implementation that
wraps around an existing interface instance. All public methods
@@ -53,59 +40,82 @@ class ThreadSafeBus(ObjectProxy):
instead of :meth:`~can.BusABC.recv` directly.
"""
- def __init__(self, *args, **kwargs):
+ def __init__(
+ self,
+ channel: typechecking.Channel | None = None,
+ interface: str | None = None,
+ config_context: str | None = None,
+ ignore_config: bool = False,
+ **kwargs: Any,
+ ) -> None:
if import_exc is not None:
raise import_exc
- super(ThreadSafeBus, self).__init__(Bus(*args, **kwargs))
+ super().__init__(
+ Bus(
+ channel=channel,
+ interface=interface,
+ config_context=config_context,
+ ignore_config=ignore_config,
+ **kwargs,
+ )
+ )
+
+ # store wrapped bus as a proxy-local attribute. Name it with the
+ # `_self_` prefix so wrapt won't forward it onto the wrapped object.
+ self._self_wrapped = cast(
+ "BusABC", object.__getattribute__(self, "__wrapped__")
+ )
# now, BusABC.send_periodic() does not need a lock anymore, but the
# implementation still requires a context manager
- self.__wrapped__._lock_send_periodic = nullcontext()
+ self._self_wrapped._lock_send_periodic = nullcontext() # type: ignore[assignment]
# init locks for sending and receiving separately
- self._lock_send = RLock()
- self._lock_recv = RLock()
-
- def recv(self, timeout=None, *args, **kwargs):
- with self._lock_recv:
- return self.__wrapped__.recv(timeout=timeout, *args, **kwargs)
+ self._self_lock_send = RLock()
+ self._self_lock_recv = RLock()
- def send(self, msg, timeout=None, *args, **kwargs):
- with self._lock_send:
- return self.__wrapped__.send(msg, timeout=timeout, *args, **kwargs)
+ def recv(self, timeout: float | None = None) -> Message | None:
+ with self._self_lock_recv:
+ return self._self_wrapped.recv(timeout=timeout)
- # send_periodic does not need a lock, since the underlying
- # `send` method is already synchronized
+ def send(self, msg: Message, timeout: float | None = None) -> None:
+ with self._self_lock_send:
+ return self._self_wrapped.send(msg=msg, timeout=timeout)
@property
- def filters(self):
- with self._lock_recv:
- return self.__wrapped__.filters
+ def filters(self) -> typechecking.CanFilters | None:
+ with self._self_lock_recv:
+ return self._self_wrapped.filters
@filters.setter
- def filters(self, filters):
- with self._lock_recv:
- self.__wrapped__.filters = filters
+ def filters(self, filters: typechecking.CanFilters | None) -> None:
+ with self._self_lock_recv:
+ self._self_wrapped.filters = filters
- def set_filters(self, can_filters=None, *args, **kwargs):
- with self._lock_recv:
- return self.__wrapped__.set_filters(can_filters=can_filters, *args, **kwargs)
+ def set_filters(self, filters: typechecking.CanFilters | None = None) -> None:
+ with self._self_lock_recv:
+ return self._self_wrapped.set_filters(filters=filters)
- def flush_tx_buffer(self, *args, **kwargs):
- with self._lock_send:
- return self.__wrapped__.flush_tx_buffer(*args, **kwargs)
+ def flush_tx_buffer(self) -> None:
+ with self._self_lock_send:
+ return self._self_wrapped.flush_tx_buffer()
- def shutdown(self, *args, **kwargs):
- with self._lock_send, self._lock_recv:
- return self.__wrapped__.shutdown(*args, **kwargs)
+ def shutdown(self) -> None:
+ with self._self_lock_send, self._self_lock_recv:
+ return self._self_wrapped.shutdown()
@property
- def state(self):
- with self._lock_send, self._lock_recv:
- return self.__wrapped__.state
+ def state(self) -> BusState:
+ with self._self_lock_send, self._self_lock_recv:
+ return self._self_wrapped.state
@state.setter
- def state(self, new_state):
- with self._lock_send, self._lock_recv:
- self.__wrapped__.state = new_state
+ def state(self, new_state: BusState) -> None:
+ with self._self_lock_send, self._self_lock_recv:
+ self._self_wrapped.state = new_state
+
+ @property
+ def protocol(self) -> CanProtocol:
+ with self._self_lock_send, self._self_lock_recv:
+ return self._self_wrapped.protocol
diff --git a/can/typechecking.py b/can/typechecking.py
new file mode 100644
index 000000000..56ac5927f
--- /dev/null
+++ b/can/typechecking.py
@@ -0,0 +1,82 @@
+"""Types for mypy type-checking"""
+
+import io
+import os
+import sys
+from collections.abc import Iterable, Sequence
+from typing import IO, TYPE_CHECKING, Any, NewType, TypeAlias
+
+if sys.version_info >= (3, 12):
+ from typing import TypedDict
+else:
+ from typing_extensions import TypedDict
+
+
+if TYPE_CHECKING:
+ import struct
+
+
+class _CanFilterBase(TypedDict):
+ can_id: int
+ can_mask: int
+
+
+class CanFilter(_CanFilterBase, total=False):
+ extended: bool
+
+
+CanFilters = Sequence[CanFilter]
+
+# TODO: Once buffer protocol support lands in typing, we should switch to that,
+# since can.message.Message attempts to call bytearray() on the given data, so
+# this should have the same typing info.
+#
+# See: https://github.com/python/typing/issues/593
+CanData = bytes | bytearray | int | Iterable[int]
+
+# Used for the Abstract Base Class
+ChannelStr = str
+ChannelInt = int
+Channel = ChannelInt | ChannelStr | Sequence[ChannelInt]
+
+# Used by the IO module
+FileLike = IO[Any] | io.TextIOWrapper | io.BufferedIOBase
+StringPathLike = str | os.PathLike[str]
+
+BusConfig = NewType("BusConfig", dict[str, Any])
+
+# Used by CLI scripts
+TAdditionalCliArgs: TypeAlias = dict[str, str | int | float | bool]
+TDataStructs: TypeAlias = dict[
+ int | tuple[int, ...],
+ "struct.Struct | tuple[struct.Struct, *tuple[float, ...]]",
+]
+
+
+class AutoDetectedConfig(TypedDict):
+ interface: str
+ channel: Channel
+
+
+ReadableBytesLike = bytes | bytearray | memoryview
+
+
+class BitTimingDict(TypedDict):
+ f_clock: int
+ brp: int
+ tseg1: int
+ tseg2: int
+ sjw: int
+ nof_samples: int
+
+
+class BitTimingFdDict(TypedDict):
+ f_clock: int
+ nom_brp: int
+ nom_tseg1: int
+ nom_tseg2: int
+ nom_sjw: int
+ data_brp: int
+ data_tseg1: int
+ data_tseg2: int
+ data_sjw: int
diff --git a/can/util.py b/can/util.py
index 56b3d5d63..4cbeec60e 100644
--- a/can/util.py
+++ b/can/util.py
@@ -1,60 +1,54 @@
-#!/usr/bin/env python
-# coding: utf-8
-
"""
Utilities and configuration file parsing.
"""
-from __future__ import absolute_import, print_function
-
+import contextlib
+import copy
+import functools
+import json
+import logging
import os
import os.path
-import sys
import platform
import re
-import logging
-try:
- from configparser import ConfigParser
-except ImportError:
- from ConfigParser import SafeConfigParser as ConfigParser
+import warnings
+from collections.abc import Callable, Iterable
+from configparser import ConfigParser
+from time import get_clock_info, perf_counter, time
+from typing import (
+ Any,
+ TypeVar,
+ cast,
+)
+
+from typing_extensions import ParamSpec
import can
-from can.interfaces import VALID_INTERFACES
-log = logging.getLogger('can.util')
+from . import typechecking
+from .bit_timing import BitTiming, BitTimingFd
+from .exceptions import CanInitializationError, CanInterfaceNotImplementedError
+from .interfaces import VALID_INTERFACES
+
+log = logging.getLogger("can.util")
# List of valid data lengths for a CAN FD message
-CAN_FD_DLC = [
- 0, 1, 2, 3, 4, 5, 6, 7, 8,
- 12, 16, 20, 24, 32, 48, 64
-]
+CAN_FD_DLC = [0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64]
-REQUIRED_KEYS = [
- 'interface',
- 'channel',
-]
+REQUIRED_KEYS = ["interface", "channel"]
-CONFIG_FILES = ['~/can.conf']
+CONFIG_FILES = ["~/can.conf"]
-if platform.system() == "Linux":
- CONFIG_FILES.extend(
- [
- '/etc/can.conf',
- '~/.can',
- '~/.canrc'
- ]
- )
+if platform.system() in ("Linux", "Darwin"):
+ CONFIG_FILES.extend(["/etc/can.conf", "~/.can", "~/.canrc"])
elif platform.system() == "Windows" or platform.python_implementation() == "IronPython":
- CONFIG_FILES.extend(
- [
- 'can.ini',
- os.path.join(os.getenv('APPDATA', ''), 'can.ini')
- ]
- )
+ CONFIG_FILES.extend(["can.ini", os.path.join(os.getenv("APPDATA", ""), "can.ini")])
-def load_file_config(path=None, section=None):
+def load_file_config(
+ path: typechecking.StringPathLike | None = None, section: str = "default"
+) -> dict[str, str]:
"""
Loads configuration from file with following content::
@@ -69,45 +63,64 @@ def load_file_config(path=None, section=None):
name of the section to read configuration from.
"""
config = ConfigParser()
+
+ # make sure to not transform the entries such that capitalization is preserved
+ config.optionxform = lambda optionstr: optionstr # type: ignore[method-assign]
+
if path is None:
config.read([os.path.expanduser(path) for path in CONFIG_FILES])
else:
config.read(path)
- _config = {}
+ _config: dict[str, str] = {}
- section = section if section is not None else 'default'
if config.has_section(section):
- if config.has_section('default'):
- _config.update(
- dict((key, val) for key, val in config.items('default')))
- _config.update(dict((key, val) for key, val in config.items(section)))
+ _config.update(config.items(section))
return _config
-def load_environment_config():
+def load_environment_config(context: str | None = None) -> dict[str, str]:
"""
Loads config dict from environmental variables (if set):
* CAN_INTERFACE
* CAN_CHANNEL
* CAN_BITRATE
+ * CAN_CONFIG
+
+ if context is supplied, "_{context}" is appended to the environment
+ variable name we will look at. For example if context="ABC":
+
+ * CAN_INTERFACE_ABC
+ * CAN_CHANNEL_ABC
+ * CAN_BITRATE_ABC
+ * CAN_CONFIG_ABC
"""
mapper = {
- 'interface': 'CAN_INTERFACE',
- 'channel': 'CAN_CHANNEL',
- 'bitrate': 'CAN_BITRATE',
+ "interface": "CAN_INTERFACE",
+ "channel": "CAN_CHANNEL",
+ "bitrate": "CAN_BITRATE",
}
- return dict(
- (key, os.environ.get(val))
- for key, val in mapper.items()
- if val in os.environ
- )
+
+ context_suffix = f"_{context}" if context else ""
+ can_config_key = f"CAN_CONFIG{context_suffix}"
+ config: dict[str, str] = json.loads(os.environ.get(can_config_key, "{}"))
+
+ for key, val in mapper.items():
+ config_option = os.environ.get(val + context_suffix, None)
+ if config_option:
+ config[key] = config_option
+
+ return config
-def load_config(path=None, config=None, context=None):
+def load_config(
+ path: typechecking.StringPathLike | None = None,
+ config: dict[str, Any] | None = None,
+ context: str | None = None,
+) -> typechecking.BusConfig:
"""
Returns a dict with configuration details which is loaded from (in this order):
@@ -121,7 +134,7 @@ def load_config(path=None, config=None, context=None):
kvaser, socketcan, pcan, usb2can, ixxat, nican, virtual.
.. note::
-
+
The key ``bustype`` is copied to ``interface`` if that one is missing
and does never appear in the result.
@@ -133,7 +146,7 @@ def load_config(path=None, config=None, context=None):
It may set other values that are passed through.
:param context:
- Extra 'context' pass to config sources. This can be use to section
+ Extra 'context' pass to config sources. This can be used to section
other than 'default' in the configuration file.
:return:
@@ -151,77 +164,145 @@ def load_config(path=None, config=None, context=None):
All unused values are passed from ``config`` over to this.
:raises:
- NotImplementedError if the ``interface`` isn't recognized
+ CanInterfaceNotImplementedError if the ``interface`` name isn't recognized
"""
- # start with an empty dict to apply filtering to all sources
+ # Start with an empty dict to apply filtering to all sources
given_config = config or {}
config = {}
- # use the given dict for default values
- config_sources = [
- given_config,
- can.rc,
- lambda _context: load_environment_config(), # context is not supported
- lambda _context: load_file_config(path, _context)
- ]
+ # Use the given dict for default values
+ config_sources = cast(
+ "Iterable[dict[str, Any] | Callable[[Any], dict[str, Any]]]",
+ [
+ given_config,
+ can.rc,
+ lambda _context: load_environment_config( # pylint: disable=unnecessary-lambda
+ _context
+ ),
+ lambda _context: load_environment_config(),
+ lambda _context: load_file_config(path, _context),
+ lambda _context: load_file_config(path),
+ ],
+ )
# Slightly complex here to only search for the file config if required
- for cfg in config_sources:
- if callable(cfg):
- cfg = cfg(context)
+ for _cfg in config_sources:
+ cfg = _cfg(context) if callable(_cfg) else _cfg
# remove legacy operator (and copy to interface if not already present)
- if 'bustype' in cfg:
- if 'interface' not in cfg or not cfg['interface']:
- cfg['interface'] = cfg['bustype']
- del cfg['bustype']
+ if "bustype" in cfg:
+ if "interface" not in cfg or not cfg["interface"]:
+ cfg["interface"] = cfg["bustype"]
+ del cfg["bustype"]
# copy all new parameters
- for key in cfg:
+ for key, val in cfg.items():
if key not in config:
- config[key] = cfg[key]
+ if isinstance(val, str):
+ config[key] = cast_from_string(val)
+ else:
+ config[key] = cfg[key]
+
+ bus_config = _create_bus_config(config)
+ can.log.debug("can config: %s", bus_config)
+ return bus_config
+
+def _create_bus_config(config: dict[str, Any]) -> typechecking.BusConfig:
+ """Validates some config values, performs compatibility mappings and creates specific
+ structures (e.g. for bit timings).
+
+ :param config: The raw config as specified by the user
+ :return: A config that can be used by a :class:`~can.BusABC`
+ :raises NotImplementedError: if the ``interface`` is unknown
+ """
# substitute None for all values not found
for key in REQUIRED_KEYS:
if key not in config:
config[key] = None
- # deprecated socketcan types
- if config['interface'] in ('socketcan_native', 'socketcan_ctypes'):
- # Change this to a DeprecationWarning in future 2.x releases
- # Remove completely in 3.0
- log.warning('%s is deprecated, use socketcan instead', config['interface'])
- config['interface'] = 'socketcan'
+ if config["interface"] not in VALID_INTERFACES:
+ raise CanInterfaceNotImplementedError(
+ f'Unknown interface type "{config["interface"]}"'
+ )
+ if "port" in config:
+ # convert port to integer if necessary
+ if isinstance(config["port"], int):
+ port = config["port"]
+ elif isinstance(config["port"], str):
+ if config["port"].isnumeric():
+ config["port"] = port = int(config["port"])
+ else:
+ raise ValueError("Port config must be a number!")
+ else:
+ raise TypeError("Port config must be string or integer!")
+
+ if not 0 < port < 65535:
+ raise ValueError("Port config must be inside 0-65535 range!")
+
+ if "timing" not in config:
+ if timing := _dict2timing(config):
+ config["timing"] = timing
+
+ if "fd" in config:
+ config["fd"] = config["fd"] not in (0, False)
+
+ if "state" in config and not isinstance(config["state"], can.BusState):
+ try:
+ config["state"] = can.BusState[config["state"]]
+ except KeyError as e:
+ raise ValueError("State config not valid!") from e
+
+ return cast("typechecking.BusConfig", config)
+
+
+def _dict2timing(data: dict[str, Any]) -> BitTiming | BitTimingFd | None:
+ """Try to instantiate a :class:`~can.BitTiming` or :class:`~can.BitTimingFd` from
+ a dictionary. Return `None` if not possible."""
+
+ with contextlib.suppress(ValueError, TypeError):
+ if set(typechecking.BitTimingFdDict.__annotations__).issubset(data):
+ return BitTimingFd(
+ **{
+ key: int(data[key])
+ for key in typechecking.BitTimingFdDict.__annotations__
+ },
+ strict=False,
+ )
+ elif set(typechecking.BitTimingDict.__annotations__).issubset(data):
+ return BitTiming(
+ **{
+ key: int(data[key])
+ for key in typechecking.BitTimingDict.__annotations__
+ },
+ strict=False,
+ )
- if config['interface'] not in VALID_INTERFACES:
- raise NotImplementedError('Invalid CAN Bus Type - {}'.format(config['interface']))
+ return None
- if 'bitrate' in config:
- config['bitrate'] = int(config['bitrate'])
- can.log.debug("can config: {}".format(config))
- return config
+def set_logging_level(level_name: str) -> None:
+ """Set the logging level for the `"can"` logger.
-
-def set_logging_level(level_name=None):
- """Set the logging level for the "can" logger.
- Expects one of: 'critical', 'error', 'warning', 'info', 'debug', 'subdebug'
+ :param level_name:
+ One of: `'critical'`, `'error'`, `'warning'`, `'info'`,
+ `'debug'`, `'subdebug'`, or the value :obj:`None` (=default).
+ Defaults to `'debug'`.
"""
- can_logger = logging.getLogger('can')
+ can_logger = logging.getLogger("can")
try:
can_logger.setLevel(getattr(logging, level_name.upper()))
except AttributeError:
can_logger.setLevel(logging.DEBUG)
- log.debug("Logging set to {}".format(level_name))
+ log.debug("Logging set to %s", level_name)
-def len2dlc(length):
+def len2dlc(length: int) -> int:
"""Calculate the DLC from data length.
- :param int length: Length in number of bytes (0-64)
+ :param length: Length in number of bytes (0-64)
:returns: DLC (0-15)
- :rtype: int
"""
if length <= 8:
return length
@@ -231,41 +312,209 @@ def len2dlc(length):
return 15
-def dlc2len(dlc):
+def dlc2len(dlc: int) -> int:
"""Calculate the data length from DLC.
- :param int dlc: DLC (0-15)
+ :param dlc: DLC (0-15)
:returns: Data length in number of bytes (0-64)
- :rtype: int
"""
return CAN_FD_DLC[dlc] if dlc <= 15 else 64
-def channel2int(channel):
+def channel2int(channel: typechecking.Channel | None) -> int | None:
"""Try to convert the channel to an integer.
:param channel:
- Channel string (e.g. can0, CAN1) or integer
-
- :returns: Channel integer or `None` if unsuccessful
- :rtype: int
+ Channel string (e.g. `"can0"`, `"CAN1"`) or an integer
+
+ :returns: Channel integer or ``None`` if unsuccessful
"""
- if channel is None:
- return None
if isinstance(channel, int):
return channel
- # String and byte objects have a lower() method
- if hasattr(channel, "lower"):
- match = re.match(r'.*(\d+)$', channel)
+ if isinstance(channel, str):
+ match = re.match(r".*?(\d+)$", channel)
if match:
return int(match.group(1))
return None
-if __name__ == "__main__":
- print("Searching for configuration named:")
- print("\n".join(CONFIG_FILES))
- print()
- print("Settings:")
- print(load_config())
+P1 = ParamSpec("P1")
+T1 = TypeVar("T1")
+
+
+def deprecated_args_alias(
+ deprecation_start: str,
+ deprecation_end: str | None = None,
+ **aliases: str | None,
+) -> Callable[[Callable[P1, T1]], Callable[P1, T1]]:
+ """Allows to rename/deprecate a function kwarg(s) and optionally
+ have the deprecated kwarg(s) set as alias(es)
+
+ Example::
+
+ @deprecated_args_alias("1.2.0", oldArg="new_arg", anotherOldArg="another_new_arg")
+ def library_function(new_arg, another_new_arg):
+ pass
+
+ @deprecated_args_alias(
+ deprecation_start="1.2.0",
+ deprecation_end="3.0.0",
+ oldArg="new_arg",
+ obsoleteOldArg=None,
+ )
+ def library_function(new_arg):
+ pass
+
+ :param deprecation_start:
+ The *python-can* version, that introduced the :class:`DeprecationWarning`.
+ :param deprecation_end:
+ The *python-can* version, that marks the end of the deprecation period.
+ :param aliases:
+ keyword arguments, that map the deprecated argument names
+ to the new argument names or ``None``.
+
+ """
+
+ def deco(f: Callable[P1, T1]) -> Callable[P1, T1]:
+ @functools.wraps(f)
+ def wrapper(*args: P1.args, **kwargs: P1.kwargs) -> T1:
+ _rename_kwargs(
+ func_name=f.__name__,
+ start=deprecation_start,
+ end=deprecation_end,
+ kwargs=kwargs,
+ aliases=aliases,
+ )
+ return f(*args, **kwargs)
+
+ return wrapper
+
+ return deco
+
+
+def _rename_kwargs(
+ func_name: str,
+ start: str,
+ end: str | None,
+ kwargs: dict[str, Any],
+ aliases: dict[str, str | None],
+) -> None:
+ """Helper function for `deprecated_args_alias`"""
+ for alias, new in aliases.items():
+ if alias in kwargs:
+ deprecation_notice = (
+ f"The '{alias}' argument is deprecated since python-can v{start}"
+ )
+ if end:
+ deprecation_notice += (
+ f", and scheduled for removal in python-can v{end}"
+ )
+ deprecation_notice += "."
+
+ value = kwargs.pop(alias)
+ if new is not None:
+ deprecation_notice += f" Use '{new}' instead."
+
+ if new in kwargs:
+ raise TypeError(
+ f"{func_name} received both '{alias}' (deprecated) and '{new}'."
+ )
+ kwargs[new] = value
+
+ warnings.warn(deprecation_notice, DeprecationWarning, stacklevel=3)
+
+
+T2 = TypeVar("T2", BitTiming, BitTimingFd)
+
+
+def check_or_adjust_timing_clock(timing: T2, valid_clocks: Iterable[int]) -> T2:
+ """Adjusts the given timing instance to have an *f_clock* value that is within the
+ allowed values specified by *valid_clocks*. If the *f_clock* value of timing is
+ already within *valid_clocks*, then *timing* is returned unchanged.
+
+ :param timing:
+ The :class:`~can.BitTiming` or :class:`~can.BitTimingFd` instance to adjust.
+ :param valid_clocks:
+ An iterable of integers representing the valid *f_clock* values that the timing instance
+ can be changed to. The order of the values in *valid_clocks* determines the priority in
+ which they are tried, with earlier values being tried before later ones.
+ :return:
+ A new :class:`~can.BitTiming` or :class:`~can.BitTimingFd` instance with an
+ *f_clock* value within *valid_clocks*.
+ :raises ~can.exceptions.CanInitializationError:
+ If no compatible *f_clock* value can be found within *valid_clocks*.
+ """
+ if timing.f_clock in valid_clocks:
+ # create a copy so this function always returns a new instance
+ return copy.deepcopy(timing)
+
+ for clock in valid_clocks:
+ try:
+ # Try to use a different f_clock
+ adjusted_timing = timing.recreate_with_f_clock(clock)
+ warnings.warn(
+ f"Adjusted f_clock in {timing.__class__.__name__} from "
+ f"{timing.f_clock} to {adjusted_timing.f_clock}",
+ stacklevel=2,
+ )
+ return adjusted_timing
+ except ValueError:
+ pass
+
+ raise CanInitializationError(
+ f"The specified timing.f_clock value {timing.f_clock} "
+ f"doesn't match any of the allowed device f_clock values: "
+ f"{', '.join([str(f) for f in valid_clocks])}"
+ ) from None
+
+
+def time_perfcounter_correlation() -> tuple[float, float]:
+ """Get the `perf_counter` value nearest to when time.time() is updated
+
+ Computed if the default timer used by `time.time` on this platform has a resolution
+ higher than 10μs, otherwise the current time and perf_counter is directly returned.
+ This was chosen as typical timer resolution on Linux/macOS is ~1μs, and the Windows
+ platform can vary from ~500μs to 10ms.
+
+ Note this value is based on when `time.time()` is observed to update from Python,
+ it is not directly returned by the operating system.
+
+ :returns:
+ (t, performance_counter) time.time value and time.perf_counter value when the time.time
+ is updated
+
+ """
+
+ # use this if the resolution is higher than 10us
+ if get_clock_info("time").resolution > 1e-5:
+ t0 = time()
+ while True:
+ t1, performance_counter = time(), perf_counter()
+ if t1 != t0:
+ break
+ else:
+ return time(), perf_counter()
+ return t1, performance_counter
+
+
+def cast_from_string(string_val: str) -> str | int | float | bool:
+ """Perform trivial type conversion from :class:`str` values.
+
+ :param string_val:
+ the string, that shall be converted
+ """
+ if re.match(r"^[-+]?\d+$", string_val):
+ # value is integer
+ return int(string_val)
+
+ if re.match(r"^[-+]?\d*\.\d+(?:e[-+]?\d+)?$", string_val):
+ # value is float
+ return float(string_val)
+
+ if re.match(r"^(?:True|False)$", string_val, re.IGNORECASE):
+ # value is bool
+ return string_val.lower() == "true"
+
+ # value is string
+ return string_val
diff --git a/can/viewer.py b/can/viewer.py
index c120df994..8d9d228bb 100644
--- a/can/viewer.py
+++ b/can/viewer.py
@@ -1,7 +1,18 @@
-#!/usr/bin/python
-# coding: utf-8
+# Copyright (C) 2018 Kristian Sloth Lauszus.
#
-# Copyright (C) 2018 Kristian Sloth Lauszus. All rights reserved.
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Contact information
# -------------------
@@ -9,45 +20,64 @@
# Web : http://www.lauszus.com
# e-mail : lauszus@gmail.com
-from __future__ import absolute_import, print_function
-
import argparse
-import can
-import curses
+import errno
+import logging
import os
import struct
import sys
import time
-from curses.ascii import ESC as KEY_ESC, SP as KEY_SPACE
-from typing import Dict, List, Tuple, Union
-
from can import __version__
-
-
-class CanViewer:
-
+from can.cli import (
+ _set_logging_level_from_namespace,
+ add_bus_arguments,
+ create_bus_from_namespace,
+)
+from can.typechecking import TDataStructs
+
+logger = logging.getLogger("can.viewer")
+
+try:
+ import curses
+ from curses.ascii import ESC as KEY_ESC # type: ignore[attr-defined,unused-ignore]
+ from curses.ascii import SP as KEY_SPACE # type: ignore[attr-defined,unused-ignore]
+except ImportError:
+ # Probably on Windows while windows-curses is not installed (e.g. in PyPy)
+ logger.warning(
+ "You won't be able to use the viewer program without curses installed!"
+ )
+ curses = None # type: ignore[assignment]
+
+
+class CanViewer: # pylint: disable=too-many-instance-attributes
def __init__(self, stdscr, bus, data_structs, testing=False):
self.stdscr = stdscr
self.bus = bus
self.data_structs = data_structs
- # Initialise the ID dictionary, start timestamp, scroll and variable for pausing the viewer
+ # Initialise the ID dictionary, Previous values dict, start timestamp,
+ # scroll and variables for pausing the viewer and enabling byte highlighting
self.ids = {}
self.start_time = None
self.scroll = 0
self.paused = False
+ self.highlight_changed_bytes = False
+ self.previous_values = {}
# Get the window dimensions - used for resizing the window
self.y, self.x = self.stdscr.getmaxyx()
- # Do not wait for key inputs, disable the cursor and choose the background color automatically
+ # Do not wait for key inputs, disable the cursor and choose the background color
+ # automatically
self.stdscr.nodelay(True)
curses.curs_set(0)
curses.use_default_colors()
# Used to color error frames red
curses.init_pair(1, curses.COLOR_RED, -1)
+ # Used to color changed bytes
+ curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLUE)
if not testing: # pragma: no cover
self.run()
@@ -56,41 +86,49 @@ def run(self):
# Clear the terminal and draw the header
self.draw_header()
- while 1:
+ while True:
# Do not read the CAN-Bus when in paused mode
if not self.paused:
# Read the CAN-Bus and draw it in the terminal window
- msg = self.bus.recv(timeout=1. / 1000.)
+ msg = self.bus.recv(timeout=1.0 / 1000.0)
if msg is not None:
self.draw_can_bus_message(msg)
else:
# Sleep 1 ms, so the application does not use 100 % of the CPU resources
- time.sleep(1. / 1000.)
+ time.sleep(1.0 / 1000.0)
# Read the terminal input
key = self.stdscr.getch()
# Stop program if the user presses ESC or 'q'
- if key == KEY_ESC or key == ord('q'):
+ if key == KEY_ESC or key == ord("q"):
break
# Clear by pressing 'c'
- elif key == ord('c'):
+ if key == ord("c"):
self.ids = {}
self.start_time = None
self.scroll = 0
self.draw_header()
+ # Toggle byte change highlighting pressing 'h'
+ elif key == ord("h"):
+ self.highlight_changed_bytes = not self.highlight_changed_bytes
+ if not self.highlight_changed_bytes:
+ # empty the previous values dict when leaving higlighting mode
+ self.previous_values.clear()
+ self.draw_header()
+
# Sort by pressing 's'
- elif key == ord('s'):
+ elif key == ord("s"):
# Sort frames based on the CAN-Bus ID
self.draw_header()
for i, key in enumerate(sorted(self.ids.keys())):
# Set the new row index, but skip the header
- self.ids[key]['row'] = i + 1
+ self.ids[key]["row"] = i + 1
# Do a recursive call, so the frames are repositioned
- self.draw_can_bus_message(self.ids[key]['msg'], sorting=True)
+ self.draw_can_bus_message(self.ids[key]["msg"], sorting=True)
# Pause by pressing space
elif key == KEY_SPACE:
@@ -112,7 +150,7 @@ def run(self):
resized = curses.is_term_resized(self.y, self.x)
if resized is True:
self.y, self.x = self.stdscr.getmaxyx()
- if hasattr(curses, 'resizeterm'): # pragma: no cover
+ if hasattr(curses, "resizeterm"): # pragma: no cover
curses.resizeterm(self.y, self.x)
self.redraw_screen()
@@ -121,29 +159,32 @@ def run(self):
# Unpack the data and then convert it into SI-units
@staticmethod
- def unpack_data(cmd, cmd_to_struct, data): # type: (int, Dict, bytes) -> List[Union[float, int]]
- if not cmd_to_struct or len(data) == 0:
+ def unpack_data(cmd: int, cmd_to_struct: TDataStructs, data: bytes) -> list[float]:
+ if not cmd_to_struct or not data:
# These messages do not contain a data package
return []
- for key in cmd_to_struct.keys():
+ for key, value in cmd_to_struct.items():
if cmd == key if isinstance(key, int) else cmd in key:
- value = cmd_to_struct[key]
if isinstance(value, tuple):
# The struct is given as the fist argument
- struct_t = value[0] # type: struct.Struct
+ struct_t: struct.Struct = value[0]
# The conversion from raw values to SI-units are given in the rest of the tuple
- values = [d // val if isinstance(val, int) else float(d) / val
- for d, val in zip(struct_t.unpack(data), value[1:])]
+ values = [
+ d // val if isinstance(val, int) else float(d) / val
+ for d, val in zip(
+ struct_t.unpack(data), value[1:], strict=False
+ )
+ ]
else:
# No conversion from SI-units is needed
- struct_t = value # type: struct.Struct
- values = list(struct_t.unpack(data))
+ as_struct_t: struct.Struct = value
+ values = list(as_struct_t.unpack(data))
return values
- else:
- raise ValueError('Unknown command: 0x{:02X}'.format(cmd))
+
+ raise ValueError(f"Unknown command: 0x{cmd:02X}")
def draw_can_bus_message(self, msg, sorting=False):
# Use the CAN-Bus ID as the key in the dict
@@ -151,7 +192,7 @@ def draw_can_bus_message(self, msg, sorting=False):
# Sort the extended IDs at the bottom by setting the 32-bit high
if msg.is_extended_id:
- key |= (1 << 32)
+ key |= 1 << 32
new_id_added, length_changed = False, False
if not sorting:
@@ -161,35 +202,45 @@ def draw_can_bus_message(self, msg, sorting=False):
# Set the start time when the first message has been received
if not self.start_time:
self.start_time = msg.timestamp
- elif msg.dlc != self.ids[key]['msg'].dlc:
+ elif msg.dlc != self.ids[key]["msg"].dlc:
length_changed = True
+ # Clear the old data bytes when the length of the new message is shorter
+ if msg.dlc < self.ids[key]["msg"].dlc:
+ self.draw_line(
+ self.ids[key]["row"],
+ # Start drawing at the last byte that is not in the new message
+ 52 + msg.dlc * 3,
+ # Draw spaces where the old bytes were drawn
+ " " * ((self.ids[key]["msg"].dlc - msg.dlc) * 3 - 1),
+ )
+
if new_id_added or length_changed:
# Increment the index if it was just added, but keep it if the length just changed
- row = len(self.ids) + 1 if new_id_added else self.ids[key]['row']
+ row = len(self.ids) + 1 if new_id_added else self.ids[key]["row"]
# It's a new message ID or the length has changed, so add it to the dict
# The first index is the row index, the second is the frame counter,
# the third is a copy of the CAN-Bus frame
# and the forth index is the time since the previous message
- self.ids[key] = {'row': row, 'count': 0, 'msg': msg, 'dt': 0}
+ self.ids[key] = {"row": row, "count": 0, "msg": msg, "dt": 0}
else:
# Calculate the time since the last message and save the timestamp
- self.ids[key]['dt'] = msg.timestamp - self.ids[key]['msg'].timestamp
+ self.ids[key]["dt"] = msg.timestamp - self.ids[key]["msg"].timestamp
# Copy the CAN-Bus frame - this is used for sorting
- self.ids[key]['msg'] = msg
+ self.ids[key]["msg"] = msg
# Increment frame counter
- self.ids[key]['count'] += 1
+ self.ids[key]["count"] += 1
# Format the CAN-Bus ID as a hex value
- arbitration_id_string = '0x{0:0{1}X}'.format(msg.arbitration_id, 8 if msg.is_extended_id else 3)
-
- # Generate data string
- data_string = ''
- if msg.dlc > 0:
- data_string = ' '.join('{:02X}'.format(x) for x in msg.data)
+ arbitration_id_string = (
+ "0x{0:0{1}X}".format( # pylint: disable=consider-using-f-string
+ msg.arbitration_id,
+ 8 if msg.is_extended_id else 3,
+ )
+ )
# Use red for error frames
if msg.is_error_frame:
@@ -198,24 +249,62 @@ def draw_can_bus_message(self, msg, sorting=False):
color = curses.color_pair(0)
# Now draw the CAN-Bus message on the terminal window
- self.draw_line(self.ids[key]['row'], 0, str(self.ids[key]['count']), color)
- self.draw_line(self.ids[key]['row'], 8, '{0:.6f}'.format(self.ids[key]['msg'].timestamp - self.start_time),
- color)
- self.draw_line(self.ids[key]['row'], 23, '{0:.6f}'.format(self.ids[key]['dt']), color)
- self.draw_line(self.ids[key]['row'], 35, arbitration_id_string, color)
- self.draw_line(self.ids[key]['row'], 47, str(msg.dlc), color)
- self.draw_line(self.ids[key]['row'], 52, data_string, color)
+ self.draw_line(self.ids[key]["row"], 0, str(self.ids[key]["count"]), color)
+ self.draw_line(
+ self.ids[key]["row"],
+ 8,
+ f"{self.ids[key]['msg'].timestamp - self.start_time:.6f}",
+ color,
+ )
+ self.draw_line(self.ids[key]["row"], 23, f"{self.ids[key]['dt']:.6f}", color)
+ self.draw_line(self.ids[key]["row"], 35, arbitration_id_string, color)
+ self.draw_line(self.ids[key]["row"], 47, str(msg.dlc), color)
+
+ try:
+ previous_byte_values = self.previous_values[key]
+ except KeyError: # no row of previous values exists for the current message ID
+ # initialise a row to store the values for comparison next time
+ self.previous_values[key] = {}
+ previous_byte_values = self.previous_values[key]
+ for i, b in enumerate(msg.data):
+ col = 52 + i * 3
+ if col > self.x - 2:
+ # Data does not fit
+ self.draw_line(self.ids[key]["row"], col - 4, "...", color)
+ break
+ if self.highlight_changed_bytes:
+ try:
+ if b != previous_byte_values[i]:
+ # set colour to highlight a changed value
+ data_color = curses.color_pair(2)
+ else:
+ data_color = color
+ except KeyError:
+ # previous entry for byte didn't exist - default to rest of line colour
+ data_color = color
+ finally:
+ # write the new value to the previous values dict for next time
+ previous_byte_values[i] = b
+ else:
+ data_color = color
+ text = f"{b:02X}"
+ self.draw_line(self.ids[key]["row"], col, text, data_color)
if self.data_structs:
try:
values_list = []
- for x in self.unpack_data(msg.arbitration_id, self.data_structs, msg.data):
+ for x in self.unpack_data(
+ msg.arbitration_id, self.data_structs, msg.data
+ ):
if isinstance(x, float):
- values_list.append('{0:.6f}'.format(x))
+ values_list.append(f"{x:.6f}")
else:
values_list.append(str(x))
- values_string = ' '.join(values_list)
- self.draw_line(self.ids[key]['row'], 77, values_string, color)
+ values_string = " ".join(values_list)
+ self.ids[key]["values_string_length"] = len(values_string)
+ values_string += " " * (self.x - len(values_string))
+
+ self.draw_line(self.ids[key]["row"], 77, values_string, color)
except (ValueError, struct.error):
pass
@@ -223,7 +312,7 @@ def draw_can_bus_message(self, msg, sorting=False):
def draw_line(self, row, col, txt, *args):
if row - self.scroll < 0:
- # Skip if we have scrolled passed the line
+ # Skip if we have scrolled past the line
return
try:
self.stdscr.addstr(row - self.scroll, col, txt, *args)
@@ -234,43 +323,46 @@ def draw_line(self, row, col, txt, *args):
def draw_header(self):
self.stdscr.erase()
- self.draw_line(0, 0, 'Count', curses.A_BOLD)
- self.draw_line(0, 8, 'Time', curses.A_BOLD)
- self.draw_line(0, 23, 'dt', curses.A_BOLD)
- self.draw_line(0, 35, 'ID', curses.A_BOLD)
- self.draw_line(0, 47, 'DLC', curses.A_BOLD)
- self.draw_line(0, 52, 'Data', curses.A_BOLD)
- if self.data_structs: # Only draw if the dictionary is not empty
- self.draw_line(0, 77, 'Parsed values', curses.A_BOLD)
+ self.draw_line(0, 0, "Count", curses.A_BOLD)
+ self.draw_line(0, 8, "Time", curses.A_BOLD)
+ self.draw_line(0, 23, "dt", curses.A_BOLD)
+ self.draw_line(0, 35, "ID", curses.A_BOLD)
+ self.draw_line(0, 47, "DLC", curses.A_BOLD)
+ self.draw_line(0, 52, "Data", curses.A_BOLD)
+
+ # Indicate that byte change highlighting is enabled
+ if self.highlight_changed_bytes:
+ self.draw_line(0, 57, "(changed)", curses.color_pair(2))
+ # Only draw if the dictionary is not empty
+ if self.data_structs:
+ self.draw_line(0, 77, "Parsed values", curses.A_BOLD)
def redraw_screen(self):
# Trigger a complete redraw
self.draw_header()
- for key in self.ids.keys():
- self.draw_can_bus_message(self.ids[key]['msg'])
+ for ids in self.ids.values():
+ self.draw_can_bus_message(ids["msg"])
-# noinspection PyProtectedMember
class SmartFormatter(argparse.HelpFormatter):
-
def _get_default_metavar_for_optional(self, action):
return action.dest.upper()
def _format_usage(self, usage, actions, groups, prefix):
# Use uppercase for "Usage:" text
- return super(SmartFormatter, self)._format_usage(usage, actions, groups, 'Usage: ')
+ return super()._format_usage(usage, actions, groups, "Usage: ")
def _format_args(self, action, default_metavar):
- if action.nargs != argparse.REMAINDER and action.nargs != argparse.ONE_OR_MORE:
- return super(SmartFormatter, self)._format_args(action, default_metavar)
+ if action.nargs not in (argparse.REMAINDER, argparse.ONE_OR_MORE):
+ return super()._format_args(action, default_metavar)
# Use the metavar if "REMAINDER" or "ONE_OR_MORE" is set
get_metavar = self._metavar_formatter(action, default_metavar)
- return '%s' % get_metavar(1)
+ return str(get_metavar(1))
def _format_action_invocation(self, action):
if not action.option_strings or action.nargs == 0:
- return super(SmartFormatter, self)._format_action_invocation(action)
+ return super()._format_action_invocation(action)
# Modified so "-s ARGS, --long ARGS" is replaced with "-s, --long ARGS"
else:
@@ -279,133 +371,122 @@ def _format_action_invocation(self, action):
args_string = self._format_args(action, default)
for i, option_string in enumerate(action.option_strings):
if i == len(action.option_strings) - 1:
- parts.append('%s %s' % (option_string, args_string))
+ parts.append(f"{option_string} {args_string}")
else:
- parts.append('%s' % option_string)
- return ', '.join(parts)
+ parts.append(str(option_string))
+ return ", ".join(parts)
def _split_lines(self, text, width):
# Allow to manually split the lines
- if text.startswith('R|'):
+ if text.startswith("R|"):
return text[2:].splitlines()
- return super(SmartFormatter, self)._split_lines(text, width)
+ return super()._split_lines(text, width)
def _fill_text(self, text, width, indent):
- if text.startswith('R|'):
- # noinspection PyTypeChecker
- return ''.join(indent + line + '\n' for line in text[2:].splitlines())
+ if text.startswith("R|"):
+ return "".join(indent + line + "\n" for line in text[2:].splitlines())
else:
- return super(SmartFormatter, self)._fill_text(text, width, indent)
-
+ return super()._fill_text(text, width, indent)
-def parse_args(args):
- # Python versions >= 3.5
- kwargs = {}
- if sys.version_info[0] * 10 + sys.version_info[1] >= 35: # pragma: no cover
- kwargs = {'allow_abbrev': False}
+def _parse_viewer_args(
+ args: list[str],
+) -> tuple[argparse.Namespace, TDataStructs]:
# Parse command line arguments
- parser = argparse.ArgumentParser('python -m can.viewer',
- description='A simple CAN viewer terminal application written in Python',
- epilog='R|Shortcuts: '
- '\n +---------+-------------------------+'
- '\n | Key | Description |'
- '\n +---------+-------------------------+'
- '\n | ESQ/q | Exit the viewer |'
- '\n | c | Clear the stored frames |'
- '\n | s | Sort the stored frames |'
- '\n | SPACE | Pause the viewer |'
- '\n | UP/DOWN | Scroll the viewer |'
- '\n +---------+-------------------------+',
- formatter_class=SmartFormatter, add_help=False, **kwargs)
-
- optional = parser.add_argument_group('Optional arguments')
-
- optional.add_argument('-h', '--help', action='help', help='Show this help message and exit')
-
- optional.add_argument('--version', action='version', help="Show program's version number and exit",
- version='%(prog)s (version {version})'.format(version=__version__))
-
- # Copied from: https://github.com/hardbyte/python-can/blob/develop/can/logger.py
- optional.add_argument('-b', '--bitrate', type=int, help='''Bitrate to use for the given CAN interface''')
-
- optional.add_argument('-c', '--channel', help='''Most backend interfaces require some sort of channel.
- For example with the serial interface the channel might be a rfcomm device: "/dev/rfcomm0"
- with the socketcan interfaces valid channel examples include: "can0", "vcan0".
- (default: use default for the specified interface)''')
-
- optional.add_argument('-d', '--decode', dest='decode',
- help='R|Specify how to convert the raw bytes into real values.'
- '\nThe ID of the frame is given as the first argument and the format as the second.'
- '\nThe Python struct package is used to unpack the received data'
- '\nwhere the format characters have the following meaning:'
- '\n < = little-endian, > = big-endian'
- '\n x = pad byte'
- '\n c = char'
- '\n ? = bool'
- '\n b = int8_t, B = uint8_t'
- '\n h = int16, H = uint16'
- '\n l = int32_t, L = uint32_t'
- '\n q = int64_t, Q = uint64_t'
- '\n f = float (32-bits), d = double (64-bits)'
- '\nFx to convert six bytes with ID 0x100 into uint8_t, uint16 and uint32_t:'
- '\n $ python -m can.viewer -d "100:: (matches when & mask == can_id & mask)'
- '\n ~ (matches when & mask != can_id & mask)'
- '\nFx to show only frames with ID 0x100 to 0x103:'
- '\n python -m can.viewer -f 100:7FC'
- '\nNote that the ID and mask are alway interpreted as hex values',
- metavar='{:,~}', nargs=argparse.ONE_OR_MORE, default='')
-
- optional.add_argument('-i', '--interface', dest='interface',
- help='R|Specify the backend CAN interface to use.',
- choices=sorted(can.VALID_INTERFACES))
+ parser = argparse.ArgumentParser(
+ "python -m can.viewer",
+ description="A simple CAN viewer terminal application written in Python",
+ epilog="R|Shortcuts: "
+ "\n +---------+-------------------------------+"
+ "\n | Key | Description |"
+ "\n +---------+-------------------------------+"
+ "\n | ESQ/q | Exit the viewer |"
+ "\n | c | Clear the stored frames |"
+ "\n | s | Sort the stored frames |"
+ "\n | h | Toggle highlight byte changes |"
+ "\n | SPACE | Pause the viewer |"
+ "\n | UP/DOWN | Scroll the viewer |"
+ "\n +---------+-------------------------------+",
+ formatter_class=SmartFormatter,
+ add_help=False,
+ allow_abbrev=False,
+ )
+
+ # add bus options group
+ add_bus_arguments(parser, filter_arg=True, group_title="Bus arguments")
+
+ optional = parser.add_argument_group("Optional arguments")
+
+ optional.add_argument(
+ "-h", "--help", action="help", help="Show this help message and exit"
+ )
+
+ optional.add_argument(
+ "--version",
+ action="version",
+ help="Show program's version number and exit",
+ version=f"%(prog)s (version {__version__})",
+ )
+
+ optional.add_argument(
+ "-d",
+ "--decode",
+ dest="decode",
+ help="R|Specify how to convert the raw bytes into real values."
+ "\nThe ID of the frame is given as the first argument and the format as the second."
+ "\nThe Python struct package is used to unpack the received data"
+ "\nwhere the format characters have the following meaning:"
+ "\n < = little-endian, > = big-endian"
+ "\n x = pad byte"
+ "\n c = char"
+ "\n ? = bool"
+ "\n b = int8_t, B = uint8_t"
+ "\n h = int16, H = uint16"
+ "\n l = int32_t, L = uint32_t"
+ "\n q = int64_t, Q = uint64_t"
+ "\n f = float (32-bits), d = double (64-bits)"
+ "\nFx to convert six bytes with ID 0x100 into uint8_t, uint16 and uint32_t:"
+ '\n $ python -m can.viewer -d "100: 0:
- # print('Adding filter/s', parsed_args.filter)
- for flt in parsed_args.filter:
- # print(filter)
- if ':' in flt:
- _ = flt.split(':')
- can_id, can_mask = int(_[0], base=16), int(_[1], base=16)
- elif '~' in flt:
- can_id, can_mask = flt.split('~')
- can_id = int(can_id, base=16) | 0x20000000 # CAN_INV_FILTER
- can_mask = int(can_mask, base=16) & 0x20000000 # socket.CAN_ERR_FLAG
- else:
- raise argparse.ArgumentError(None, 'Invalid filter argument')
- can_filters.append({'can_id': can_id, 'can_mask': can_mask})
+ parsed_args, unknown_args = parser.parse_known_args(args)
+ if unknown_args:
+ print("Unknown arguments:", unknown_args)
# Dictionary used to convert between Python values and C structs represented as Python strings.
# If the value is 'None' then the message does not contain any data package.
@@ -423,24 +504,26 @@ def parse_args(args):
# f = float (32-bits), d = double (64-bits)
#
# An optional conversion from real units to integers can be given as additional arguments.
- # In order to convert from raw integer value the real units are multiplied with the values and similarly the values
+ # In order to convert from raw integer value the real units are multiplied with the values and
+ # similarly the values
# are divided by the value in order to convert from real units to raw integer values.
- data_structs = {} # type: Dict[Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None]]
- if len(parsed_args.decode) > 0:
+
+ data_structs: TDataStructs = {}
+ if parsed_args.decode:
if os.path.isfile(parsed_args.decode[0]):
- with open(parsed_args.decode[0], 'r') as f:
+ with open(parsed_args.decode[0], encoding="utf-8") as f:
structs = f.readlines()
else:
structs = parsed_args.decode
for s in structs:
- tmp = s.rstrip('\n').split(':')
+ tmp = s.rstrip("\n").split(":")
# The ID is given as a hex value, the format needs no conversion
key, fmt = int(tmp[0], base=16), tmp[1]
# The scaling
- scaling = [] # type: list
+ scaling: list[float] = []
for t in tmp[2:]:
# First try to convert to int, if that fails, then convert to a float
try:
@@ -449,33 +532,21 @@ def parse_args(args):
scaling.append(float(t))
if scaling:
- data_structs[key] = (struct.Struct(fmt),) + tuple(scaling)
+ data_structs[key] = (struct.Struct(fmt), *scaling)
else:
data_structs[key] = struct.Struct(fmt)
- # print(data_structs[key])
-
- return parsed_args, can_filters, data_structs
-
-
-def main(): # pragma: no cover
- parsed_args, can_filters, data_structs = parse_args(sys.argv[1:])
- config = {'single_handle': True}
- if can_filters:
- config['can_filters'] = can_filters
- if parsed_args.interface:
- config['interface'] = parsed_args.interface
- if parsed_args.bitrate:
- config['bitrate'] = parsed_args.bitrate
+ return parsed_args, data_structs
- # Create a CAN-Bus interface
- bus = can.Bus(parsed_args.channel, **config)
- # print('Connected to {}: {}'.format(bus.__class__.__name__, bus.channel_info))
- curses.wrapper(CanViewer, bus, data_structs)
+def main() -> None:
+ parsed_args, data_structs = _parse_viewer_args(sys.argv[1:])
+ bus = create_bus_from_namespace(parsed_args)
+ _set_logging_level_from_namespace(parsed_args)
+ curses.wrapper(CanViewer, bus, data_structs) # type: ignore[attr-defined,unused-ignore]
-if __name__ == '__main__': # pragma: no cover
+if __name__ == "__main__":
# Catch ctrl+c
try:
main()
diff --git a/doc/api.rst b/doc/api.rst
index 8e657bd3c..50095589c 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -1,7 +1,7 @@
Library API
===========
-The main objects are the :class:`~can.BusABC` and the :class:`~can.Message`.
+The main objects are the :class:`~can.Bus` and the :class:`~can.Message`.
A form of CAN interface is also required.
.. hint::
@@ -10,37 +10,17 @@ A form of CAN interface is also required.
.. toctree::
- :maxdepth: 1
-
+ :maxdepth: 2
+
bus
message
- listeners
+ notifier
+ file_io
asyncio
bcm
+ errors
+ bit_timing
+ utils
+ internal-api
-Utilities
----------
-
-.. automodule:: can.util
- :members:
-
-.. automethod:: can.detect_available_configs
-
-
-
-
-.. _notifier:
-
-Notifier
---------
-
-The Notifier object is used as a message distributor for a bus.
-
-.. autoclass:: can.Notifier
- :members:
-
-Errors
-------
-
-.. autoclass:: can.CanError
diff --git a/doc/asyncio.rst b/doc/asyncio.rst
index f5bd7771b..cd8d65de5 100644
--- a/doc/asyncio.rst
+++ b/doc/asyncio.rst
@@ -4,8 +4,9 @@ Asyncio support
===============
The :mod:`asyncio` module built into Python 3.4 and later can be used to write
-asynchronos code in a single thread. This library supports receiving messages
-asynchronosly in an event loop using the :class:`can.Notifier` class.
+asynchronous code in a single thread. This library supports receiving messages
+asynchronously in an event loop using the :class:`can.Notifier` class.
+
There will still be one thread per CAN bus but the user application will execute
entirely in the event loop, allowing simpler concurrency without worrying about
threading issues. Interfaces that have a valid file descriptor will however be
diff --git a/doc/bcm.rst b/doc/bcm.rst
index 0676a77eb..94cde0e60 100644
--- a/doc/bcm.rst
+++ b/doc/bcm.rst
@@ -1,13 +1,13 @@
+.. _bcm:
+
Broadcast Manager
=================
.. module:: can.broadcastmanager
-The broadcast manager isn't yet supported by all interfaces.
-Currently SocketCAN and IXXAT are supported at least partially.
-It allows the user to setup periodic message jobs.
-
-If periodic transmission is not supported natively, a software thread
+The broadcast manager allows the user to setup periodic message jobs.
+For example sending a particular message at a given period. The broadcast
+manager supported natively by several interfaces and a software thread
based scheduler is used as a fallback.
This example shows the socketcan backend using the broadcast manager:
@@ -23,6 +23,10 @@ Message Sending Tasks
The class based api for the broadcast manager uses a series of
`mixin classes `_.
All mixins inherit from :class:`~can.broadcastmanager.CyclicSendTaskABC`
+which inherits from :class:`~can.broadcastmanager.CyclicTask`.
+
+.. autoclass:: can.broadcastmanager.CyclicTask
+ :members:
.. autoclass:: can.broadcastmanager.CyclicSendTaskABC
:members:
@@ -39,13 +43,6 @@ All mixins inherit from :class:`~can.broadcastmanager.CyclicSendTaskABC`
.. autoclass:: can.RestartableCyclicTaskABC
:members:
+.. autoclass:: can.broadcastmanager.ThreadBasedCyclicSendTask
+ :members:
-Functional API
---------------
-
-.. warning::
- The functional API in :func:`can.broadcastmanager.send_periodic` is now deprecated
- and will be removed in version 2.3.
- Use the object oriented API via :meth:`can.BusABC.send_periodic` instead.
-
-.. autofunction:: can.broadcastmanager.send_periodic
diff --git a/doc/bit_timing.rst b/doc/bit_timing.rst
new file mode 100644
index 000000000..bf7aad486
--- /dev/null
+++ b/doc/bit_timing.rst
@@ -0,0 +1,136 @@
+Bit Timing Configuration
+========================
+
+The CAN protocol, specified in ISO 11898, allows the bitrate, sample point
+and number of samples to be optimized for a given application. These
+parameters, known as bit timings, can be adjusted to meet the requirements
+of the communication system and the physical communication channel.
+
+These parameters include:
+
+* **tseg1**: The time segment 1 (TSEG1) is the amount of time from the end
+ of the sync segment until the sample point. It is expressed in time quanta (TQ).
+* **tseg2**: The time segment 2 (TSEG2) is the amount of time from the
+ sample point until the end of the bit. It is expressed in TQ.
+* **sjw**: The synchronization jump width (SJW) is the maximum number
+ of TQ that the controller can resynchronize every bit.
+* **sample point**: The sample point is defined as the point in time
+ within a bit where the bus controller samples the bus for dominant or
+ recessive levels. It is typically expressed as a percentage of the bit time.
+ The sample point depends on the bus length and propagation time as well
+ as the information processing time of the nodes.
+
+.. figure:: images/bit_timing_light.svg
+ :align: center
+ :class: only-light
+
+.. figure:: images/bit_timing_dark.svg
+ :align: center
+ :class: only-dark
+
+ Bit Timing and Sample Point
+
+
+For example, consider a bit with a total duration of 8 TQ and a sample
+point at 75%. The values for TSEG1, TSEG2 and SJW would be 5, 2, and 2,
+respectively. The sample point would be 6 TQ after the start of the bit,
+leaving 2 TQ for the information processing by the bus nodes.
+
+.. note::
+ The values for TSEG1, TSEG2 and SJW are chosen such that the
+ sample point is at least 50% of the total bit time. This ensures that
+ there is sufficient time for the signal to stabilize before it is sampled.
+
+.. note::
+ In CAN FD, the arbitration (nominal) phase and the data phase can have
+ different bit rates. As a result, there are two separate sample points
+ to consider.
+
+Another important parameter is **f_clock**: The CAN system clock frequency
+in Hz. This frequency is used to derive the TQ size from the bit rate.
+The relationship is ``f_clock = (tseg1+tseg2+1) * bitrate * brp``.
+The bit rate prescaler value **brp** is usually determined by the controller
+and is chosen to ensure that the resulting bit time is an integer value.
+Typical CAN clock frequencies are 8-80 MHz.
+
+In most cases, the recommended settings for a predefined set of common
+bit rates will work just fine. In some cases, however, it may be necessary
+to specify custom bit timings. The :class:`~can.BitTiming` and
+:class:`~can.BitTimingFd` classes can be used for this purpose to specify
+bit timings in a relatively interface agnostic manner.
+
+:class:`~can.BitTiming` or :class:`~can.BitTimingFd` can also help you to
+produce an overview of possible bit timings for your desired bit rate:
+
+ >>> import contextlib
+ >>> import can
+ ...
+ >>> timings = set()
+ >>> for sample_point in range(50, 100):
+ ... with contextlib.suppress(ValueError):
+ ... timings.add(
+ ... can.BitTiming.from_sample_point(
+ ... f_clock=8_000_000,
+ ... bitrate=250_000,
+ ... sample_point=sample_point,
+ ... )
+ ... )
+ ...
+ >>> for timing in sorted(timings, key=lambda x: x.sample_point):
+ ... print(timing)
+ BR: 250_000 bit/s, SP: 50.00%, BRP: 2, TSEG1: 7, TSEG2: 8, SJW: 4, BTR: C176h, CLK: 8MHz
+ BR: 250_000 bit/s, SP: 56.25%, BRP: 2, TSEG1: 8, TSEG2: 7, SJW: 4, BTR: C167h, CLK: 8MHz
+ BR: 250_000 bit/s, SP: 62.50%, BRP: 2, TSEG1: 9, TSEG2: 6, SJW: 4, BTR: C158h, CLK: 8MHz
+ BR: 250_000 bit/s, SP: 68.75%, BRP: 2, TSEG1: 10, TSEG2: 5, SJW: 4, BTR: C149h, CLK: 8MHz
+ BR: 250_000 bit/s, SP: 75.00%, BRP: 2, TSEG1: 11, TSEG2: 4, SJW: 4, BTR: C13Ah, CLK: 8MHz
+ BR: 250_000 bit/s, SP: 81.25%, BRP: 2, TSEG1: 12, TSEG2: 3, SJW: 3, BTR: 812Bh, CLK: 8MHz
+ BR: 250_000 bit/s, SP: 87.50%, BRP: 2, TSEG1: 13, TSEG2: 2, SJW: 2, BTR: 411Ch, CLK: 8MHz
+ BR: 250_000 bit/s, SP: 93.75%, BRP: 2, TSEG1: 14, TSEG2: 1, SJW: 1, BTR: 010Dh, CLK: 8MHz
+
+
+It is possible to specify CAN 2.0 bit timings
+using the config file:
+
+.. code-block:: none
+
+ [default]
+ f_clock=8000000
+ brp=1
+ tseg1=5
+ tseg2=2
+ sjw=1
+ nof_samples=1
+
+The same is possible for CAN FD:
+
+.. code-block:: none
+
+ [default]
+ f_clock=80000000
+ nom_brp=1
+ nom_tseg1=119
+ nom_tseg2=40
+ nom_sjw=40
+ data_brp=1
+ data_tseg1=29
+ data_tseg2=10
+ data_sjw=10
+
+A :class:`dict` of the relevant config parameters can be easily obtained by calling
+``dict(timing)`` or ``{**timing}`` where ``timing`` is the :class:`~can.BitTiming` or
+:class:`~can.BitTimingFd` instance.
+
+Check :doc:`configuration` for more information about saving and loading configurations.
+
+
+.. autoclass:: can.BitTiming
+ :class-doc-from: both
+ :show-inheritance:
+ :members:
+ :member-order: bysource
+
+.. autoclass:: can.BitTimingFd
+ :class-doc-from: both
+ :show-inheritance:
+ :members:
+ :member-order: bysource
diff --git a/doc/bus.rst b/doc/bus.rst
index 071d4094c..f63c244c2 100644
--- a/doc/bus.rst
+++ b/doc/bus.rst
@@ -3,41 +3,42 @@
Bus
---
-The :class:`~can.BusABC` class, as the name suggests, provides an abstraction of a CAN bus.
-The bus provides a wrapper around a physical or virtual CAN Bus.
-An interface specific instance of the :class:`~can.BusABC` is created by the :class:`~can.Bus`
-class, for example::
+The :class:`~can.BusABC` class provides a wrapper around a physical or virtual CAN Bus.
+
+An interface specific instance is created by calling the :func:`~can.Bus`
+function with a particular ``interface``, for example::
vector_bus = can.Bus(interface='vector', ...)
-That bus is then able to handle the interface specific software/hardware interactions
-and implements the :class:`~can.BusABC` API. It itself is an instance of ``VectorBus``,
-but these specififc buses should not be instantiated directly.
+The created bus is then able to handle the interface specific software/hardware interactions
+while giving the user the same top level API.
A thread safe bus wrapper is also available, see `Thread safe bus`_.
-Autoconfig Bus
-''''''''''''''
-.. autoclass:: can.Bus
- :members:
- :undoc-members:
+Transmitting
+''''''''''''
+Writing individual messages to the bus is done by calling the :meth:`~can.BusABC.send` method
+and passing a :class:`~can.Message` instance.
-API
-'''
+.. code-block:: python
+ :emphasize-lines: 8
-.. autoclass:: can.BusABC
- :members:
- :undoc-members:
+ with can.Bus() as bus:
+ msg = can.Message(
+ arbitration_id=0xC0FFEE,
+ data=[0, 25, 0, 1, 3, 1, 4, 1],
+ is_extended_id=True
+ )
+ try:
+ bus.send(msg)
+ print(f"Message sent on {bus.channel_info}")
+ except can.CanError:
+ print("Message NOT sent")
-Transmitting
-''''''''''''
-
-Writing to the bus is done by calling the :meth:`~can.BusABC.send` method and
-passing a :class:`~can.Message` instance.
-
+Periodic sending is controlled by the :ref:`broadcast manager `.
Receiving
'''''''''
@@ -45,22 +46,55 @@ Receiving
Reading from the bus is achieved by either calling the :meth:`~can.BusABC.recv` method or
by directly iterating over the bus::
- for msg in bus:
- print(msg.data)
+ with can.Bus() as bus:
+ for msg in bus:
+ print(msg.data)
-Alternatively the :class:`~can.Listener` api can be used, which is a list of :class:`~can.Listener`
-subclasses that receive notifications when new messages arrive.
+Alternatively the :ref:`listeners_doc` api can be used, which is a list of various
+:class:`~can.Listener` implementations that receive and handle messages from a :class:`~can.Notifier`.
Filtering
'''''''''
Message filtering can be set up for each bus. Where the interface supports it, this is carried
-out in the hardware or kernel layer - not in Python.
+out in the hardware or kernel layer - not in Python. All messages that match at least one filter
+are returned.
+
+Example defining two filters, one to pass 11-bit ID ``0x451``, the other to pass 29-bit ID ``0xA0000``:
+
+.. code-block:: python
+
+ filters = [
+ {"can_id": 0x451, "can_mask": 0x7FF, "extended": False},
+ {"can_id": 0xA0000, "can_mask": 0x1FFFFFFF, "extended": True},
+ ]
+ bus = can.interface.Bus(channel="can0", interface="socketcan", can_filters=filters)
+
+
+See :meth:`~can.BusABC.set_filters` for the implementation.
+
+Bus API
+'''''''
+
+.. autofunction:: can.Bus
+
+.. autoclass:: can.BusABC
+ :class-doc-from: class
+ :members:
+ :inherited-members:
+
+.. autoclass:: can.BusState
+ :members:
+ :undoc-members:
+
+.. autoclass:: can.bus.CanProtocol
+ :members:
+ :undoc-members:
Thread safe bus
----------------
+'''''''''''''''
This thread safe version of the :class:`~can.BusABC` class can be used by multiple threads at once.
Sending and receiving is locked separately to avoid unnecessary delays.
@@ -68,7 +102,9 @@ Conflicting calls are executed by blocking until the bus is accessible.
It can be used exactly like the normal :class:`~can.BusABC`:
- # 'socketcan' is only an exemple interface, it works with all the others too
+.. code-block:: python
+
+ # 'socketcan' is only an example interface, it works with all the others too
my_bus = can.ThreadSafeBus(interface='socketcan', channel='vcan0')
my_bus.send(...)
my_bus.recv(...)
diff --git a/doc/changelog.d/.gitignore b/doc/changelog.d/.gitignore
new file mode 100644
index 000000000..b56b00acb
--- /dev/null
+++ b/doc/changelog.d/.gitignore
@@ -0,0 +1,11 @@
+# Ignore everything...
+*
+!.gitignore
+
+# ...except markdown news fragments
+!*.security.md
+!*.removed.md
+!*.deprecated.md
+!*.added.md
+!*.changed.md
+!*.fixed.md
diff --git a/doc/changelog.d/1815.added.md b/doc/changelog.d/1815.added.md
new file mode 100644
index 000000000..65756fb41
--- /dev/null
+++ b/doc/changelog.d/1815.added.md
@@ -0,0 +1 @@
+Added support for replaying CAN log files multiple times or infinitely in the player script via the new --loop/-l argument.
diff --git a/doc/changelog.d/1815.removed.md b/doc/changelog.d/1815.removed.md
new file mode 100644
index 000000000..61b4e9b1d
--- /dev/null
+++ b/doc/changelog.d/1815.removed.md
@@ -0,0 +1 @@
+Removed the unused --file_name/-f argument from the player CLI.
diff --git a/doc/changelog.d/1938.fixed.md b/doc/changelog.d/1938.fixed.md
new file mode 100644
index 000000000..f9aad1089
--- /dev/null
+++ b/doc/changelog.d/1938.fixed.md
@@ -0,0 +1 @@
+Keep a reference to asyncio tasks in `can.Notifier` as recommended by [python documentation](https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task).
diff --git a/doc/changelog.d/1987.added.md b/doc/changelog.d/1987.added.md
new file mode 100644
index 000000000..398add3e3
--- /dev/null
+++ b/doc/changelog.d/1987.added.md
@@ -0,0 +1 @@
+Add [python-can-coe](https://c0d3.sh/smarthome/python-can-coe) interface plugin to the documentation.
diff --git a/doc/changelog.d/1995.added.md b/doc/changelog.d/1995.added.md
new file mode 100644
index 000000000..81e39d0df
--- /dev/null
+++ b/doc/changelog.d/1995.added.md
@@ -0,0 +1 @@
+Added hardware filter support for SeeedBus during initialization, with a software fallback.
\ No newline at end of file
diff --git a/doc/changelog.d/1996.removed.md b/doc/changelog.d/1996.removed.md
new file mode 100644
index 000000000..77458ad75
--- /dev/null
+++ b/doc/changelog.d/1996.removed.md
@@ -0,0 +1 @@
+Remove support for end-of-life Python 3.9.
\ No newline at end of file
diff --git a/doc/changelog.d/2009.changed.md b/doc/changelog.d/2009.changed.md
new file mode 100644
index 000000000..6e68198a1
--- /dev/null
+++ b/doc/changelog.d/2009.changed.md
@@ -0,0 +1 @@
+Improved datetime parsing and added support for “double-defined” datetime strings (such as, e.g., `"30 15:06:13.191 pm 2017"`) for ASCReader class.
\ No newline at end of file
diff --git a/doc/changelog.d/2023.changed.md b/doc/changelog.d/2023.changed.md
new file mode 100644
index 000000000..f20e2997c
--- /dev/null
+++ b/doc/changelog.d/2023.changed.md
@@ -0,0 +1 @@
+Improve IXXAT VCI exception handling
diff --git a/doc/conf.py b/doc/conf.py
index e61801a6f..5e413361c 100755
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -1,188 +1,226 @@
-# -*- coding: utf-8 -*-
-#
-# python-can documentation build configuration file
-#
-# This file is execfile()d with the current directory set to its containing dir.
+"""
+python-can documentation build configuration file
-from __future__ import unicode_literals, absolute_import
+This file is execfile()d with the current directory set to its containing dir.
+"""
-import sys
-import os
+# -- Imports -------------------------------------------------------------------
-# General information about the project.
-project = u'python-can'
+import ctypes
+import os
+import sys
+from importlib.metadata import version as get_version
+from unittest.mock import MagicMock
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
-sys.path.insert(0, os.path.abspath('..'))
+sys.path.insert(0, os.path.abspath(".."))
+
+from can import ctypesutil # pylint: disable=wrong-import-position
+
+# -- General configuration -----------------------------------------------------
+
+# pylint: disable=invalid-name
-import can
# The version info for the project, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
+# The full version, including alpha/beta/rc tags.
+release: str = get_version("python-can")
# The short X.Y version.
-version = can.__version__.split('-')[0]
-release = can.__version__
+version = ".".join(release.split(".")[:2])
-# -- General configuration -----------------------------------------------------
+# General information about the project.
+project = "python-can"
-primary_domain = 'py'
+primary_domain = "py"
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = ['sphinx.ext.autodoc',
- 'sphinx.ext.doctest',
- 'sphinx.ext.extlinks',
- 'sphinx.ext.todo',
- 'sphinx.ext.intersphinx',
- 'sphinx.ext.coverage',
- 'sphinx.ext.viewcode',
- 'sphinx.ext.graphviz']
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.doctest",
+ "sphinx.ext.extlinks",
+ "sphinx.ext.todo",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.coverage",
+ "sphinx.ext.viewcode",
+ "sphinx.ext.graphviz",
+ "sphinxcontrib.programoutput",
+ "sphinx_inline_tabs",
+ "sphinx_copybutton",
+]
# Now, you can use the alias name as a new role, e.g. :issue:`123`.
-extlinks = {
- 'issue': ('https://github.com/hardbyte/python-can/issues/%s/', 'issue '),
-}
+extlinks = {"issue": ("https://github.com/hardbyte/python-can/issues/%s/", "issue #%s")}
-intersphinx_mapping = {
- 'python': ('https://docs.python.org/3/', None),
-}
+intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
# If this is True, todo and todolist produce output, else they produce nothing.
# The default is False.
todo_include_todos = True
# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
-graphviz_output_format = 'png' # 'svg'
+graphviz_output_format = "png" # 'svg'
# The suffix of source filenames.
-source_suffix = '.rst'
+source_suffix = {".rst": "restructuredtext"}
# The encoding of source files.
-#source_encoding = 'utf-8-sig'
+# source_encoding = 'utf-8-sig'
# The master toctree document.
-master_doc = 'index'
+master_doc = "index"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
-language = 'en'
+language = "en"
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
-#today = ''
+# today = ''
# Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
-exclude_patterns = ['_build']
+exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all documents
-#default_role = None
+# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
+# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
-#add_module_names = True
+# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
-#show_authors = False
+# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
# Include documentation from both the class level and __init__
autoclass_content = "both"
# The default autodoc directive flags
-autodoc_default_flags = ['members', 'show-inheritance']
+autodoc_default_flags = ["members", "show-inheritance"]
# Keep cached intersphinx inventories indefinitely
intersphinx_cache_limit = -1
+# location of typehints
+autodoc_typehints = "description"
+
+# disable specific warnings
+nitpick_ignore = [
+ # Ignore warnings for type aliases. Remove once Sphinx supports PEP613
+ ("py:class", "OpenTextModeUpdating"),
+ ("py:class", "OpenTextModeWriting"),
+ ("py:class", "OpenBinaryModeUpdating"),
+ ("py:class", "OpenBinaryModeWriting"),
+ ("py:class", "OpenTextModeReading"),
+ ("py:class", "OpenBinaryModeReading"),
+ ("py:class", "BusConfig"),
+ ("py:class", "can.typechecking.BusConfig"),
+ ("py:class", "can.typechecking.CanFilter"),
+ ("py:class", "can.typechecking.CanFilterExtended"),
+ ("py:class", "can.typechecking.AutoDetectedConfig"),
+ ("py:class", "can.util.T1"),
+ ("py:class", "can.util.T2"),
+ ("py:class", "~P1"),
+ # intersphinx fails to reference some builtins
+ ("py:class", "asyncio.events.AbstractEventLoop"),
+ ("py:class", "_thread.lock"),
+]
+
+# mock windows specific attributes
+autodoc_mock_imports = ["win32com", "pythoncom"]
+ctypes.windll = MagicMock()
+ctypesutil.HRESULT = ctypes.c_long
+
# -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
-html_theme = 'default'
+html_theme = "furo"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
-#html_theme_options = {}
+# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
+# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
-#html_title = None
+# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
-#html_short_title = None
+# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
-#html_logo = None
+# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
-#html_favicon = None
+# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
-#html_static_path = ['_static']
+# html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
-#html_use_smartypants = True
+# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
+# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
-#html_additional_pages = {}
+# html_additional_pages = {}
# If false, no module index is generated.
-#html_domain_indices = True
+# html_domain_indices = True
# If false, no index is generated.
-#html_use_index = True
+# html_use_index = True
# If true, the index is split into individual pages for each letter.
-#html_split_index = False
+# html_split_index = False
# If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
+# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
+# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
+# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
+# html_file_suffix = None
# Output file base name for HTML help builder.
-htmlhelp_basename = 'python-can'
+htmlhelp_basename = "python-can"
diff --git a/doc/configuration.rst b/doc/configuration.rst
index a7da791f6..2951a63b1 100644
--- a/doc/configuration.rst
+++ b/doc/configuration.rst
@@ -1,3 +1,5 @@
+.. _configuration:
+
Configuration
=============
@@ -11,7 +13,7 @@ In Code
-------
The ``can`` object exposes an ``rc`` dictionary which can be used to set
-the **interface** and **channel** before importing from ``can.interfaces``.
+the **interface** and **channel**.
::
@@ -19,7 +21,7 @@ the **interface** and **channel** before importing from ``can.interfaces``.
can.rc['interface'] = 'socketcan'
can.rc['channel'] = 'vcan0'
can.rc['bitrate'] = 500000
- from can.interfaces.interface import Bus
+ from can.interface import Bus
bus = Bus()
@@ -28,24 +30,24 @@ You can also specify the interface and channel for each Bus instance::
import can
- bus = can.interface.Bus(bustype='socketcan', channel='vcan0', bitrate=500000)
+ bus = can.interface.Bus(interface='socketcan', channel='vcan0', bitrate=500000)
Configuration File
------------------
-On Linux systems the config file is searched in the following paths:
+On Linux and macOS systems the config file is searched in the following paths:
-1. ``~/can.conf``
-2. ``/etc/can.conf``
-3. ``$HOME/.can``
-4. ``$HOME/.canrc``
+#. ``~/can.conf``
+#. ``/etc/can.conf``
+#. ``$HOME/.can``
+#. ``$HOME/.canrc``
On Windows systems the config file is searched in the following paths:
-1. ``~/can.conf``
-1. ``can.ini`` (current working directory)
-2. ``$APPDATA/can.ini``
+#. ``%USERPROFILE%/can.conf``
+#. ``can.ini`` (current working directory)
+#. ``%APPDATA%/can.ini``
The configuration file sets the default interface and channel:
@@ -57,7 +59,7 @@ The configuration file sets the default interface and channel:
bitrate =
-The configuration can also contain additional sections:
+The configuration can also contain additional sections (or context):
::
@@ -79,10 +81,10 @@ The configuration can also contain additional sections:
::
- from can.interfaces.interface import Bus
+ from can.interface import Bus
- hs_bus = Bus(config_section='HS')
- ms_bus = Bus(config_section='MS')
+ hs_bus = Bus(config_context='HS')
+ ms_bus = Bus(config_context='MS')
Environment Variables
---------------------
@@ -92,7 +94,15 @@ Configuration can be pulled from these environmental variables:
* CAN_INTERFACE
* CAN_CHANNEL
* CAN_BITRATE
+ * CAN_CONFIG
+
+The ``CAN_CONFIG`` environment variable allows to set any bus configuration using JSON.
+
+For example:
+
+``CAN_INTERFACE=socketcan CAN_CONFIG={"receive_own_messages": true, "fd": true}``
+.. _interface names:
Interface Names
---------------
@@ -102,27 +112,51 @@ Lookup table of interface names:
+---------------------+-------------------------------------+
| Name | Documentation |
+=====================+=====================================+
-| ``"socketcan"`` | :doc:`interfaces/socketcan` |
+| ``"canalystii"`` | :doc:`interfaces/canalystii` |
+---------------------+-------------------------------------+
-| ``"kvaser"`` | :doc:`interfaces/kvaser` |
+| ``"cantact"`` | :doc:`interfaces/cantact` |
+---------------------+-------------------------------------+
-| ``"serial"`` | :doc:`interfaces/serial` |
+| ``"etas"`` | :doc:`interfaces/etas` |
+---------------------+-------------------------------------+
-| ``"slcan"`` | :doc:`interfaces/slcan` |
+| ``"gs_usb"`` | :doc:`interfaces/gs_usb` |
++---------------------+-------------------------------------+
+| ``"iscan"`` | :doc:`interfaces/iscan` |
+---------------------+-------------------------------------+
| ``"ixxat"`` | :doc:`interfaces/ixxat` |
+---------------------+-------------------------------------+
-| ``"pcan"`` | :doc:`interfaces/pcan` |
+| ``"kvaser"`` | :doc:`interfaces/kvaser` |
+---------------------+-------------------------------------+
-| ``"usb2can"`` | :doc:`interfaces/usb2can` |
+| ``"neousys"`` | :doc:`interfaces/neousys` |
++---------------------+-------------------------------------+
+| ``"neovi"`` | :doc:`interfaces/neovi` |
+---------------------+-------------------------------------+
| ``"nican"`` | :doc:`interfaces/nican` |
+---------------------+-------------------------------------+
-| ``"iscan"`` | :doc:`interfaces/iscan` |
+| ``"nixnet"`` | :doc:`interfaces/nixnet` |
+---------------------+-------------------------------------+
-| ``"neovi"`` | :doc:`interfaces/neovi` |
+| ``"pcan"`` | :doc:`interfaces/pcan` |
++---------------------+-------------------------------------+
+| ``"robotell"`` | :doc:`interfaces/robotell` |
++---------------------+-------------------------------------+
+| ``"seeedstudio"`` | :doc:`interfaces/seeedstudio` |
++---------------------+-------------------------------------+
+| ``"serial"`` | :doc:`interfaces/serial` |
++---------------------+-------------------------------------+
+| ``"slcan"`` | :doc:`interfaces/slcan` |
++---------------------+-------------------------------------+
+| ``"socketcan"`` | :doc:`interfaces/socketcan` |
++---------------------+-------------------------------------+
+| ``"socketcand"`` | :doc:`interfaces/socketcand` |
++---------------------+-------------------------------------+
+| ``"systec"`` | :doc:`interfaces/systec` |
++---------------------+-------------------------------------+
+| ``"udp_multicast"`` | :doc:`interfaces/udp_multicast` |
++---------------------+-------------------------------------+
+| ``"usb2can"`` | :doc:`interfaces/usb2can` |
+---------------------+-------------------------------------+
| ``"vector"`` | :doc:`interfaces/vector` |
+---------------------+-------------------------------------+
| ``"virtual"`` | :doc:`interfaces/virtual` |
+---------------------+-------------------------------------+
+
+Additional interface types can be added via the :ref:`plugin interface`.
diff --git a/doc/development.rst b/doc/development.rst
index 51924be16..40604c346 100644
--- a/doc/development.rst
+++ b/doc/development.rst
@@ -1,97 +1,29 @@
Developer's Overview
====================
+Quick Start for Contributors
+----------------------------
+* Fork the repository on GitHub and clone your fork.
+* Create a new branch for your changes.
+* Set up your development environment.
+* Make your changes, add/update tests and documentation as needed.
+* Run `tox` to check your changes.
+* Push your branch and open a pull request.
Contributing
------------
-Contribute to source code, documentation, examples and report issues:
-https://github.com/hardbyte/python-can
-
-There is also a `python-can `__
-mailing list for development discussion.
-
-
-Building & Installing
----------------------
-
-The following assumes that the commands are executed from the root of the repository:
-
-- The project can be built and installed with ``python setup.py build`` and
- ``python setup.py install``.
-- The unit tests can be run with ``python setup.py test``. The tests can be run with ``python2``,
- ``python3``, ``pypy`` or ``pypy3`` to test with other python versions, if they are installed.
- Maybe, you need to execute ``pip3 install python-can[test]`` (or only ``pip`` for Python 2),
- if some dependencies are missing.
-- The docs can be built with ``sphinx-build doc/ doc/_build``. Appending ``-n`` to the command
- makes Sphinx complain about more subtle problems.
-
-
-Creating a new interface/backend
---------------------------------
-
-These steps are a guideline on how to add a new backend to python-can.
-
-- Create a module (either a ``*.py`` or an entire subdirctory depending
- on the complexity) inside ``can.interfaces``
-- Implement the central part of the backend: the bus class that extends
- :class:`can.BusABC`. See below for more info on this one!
-- Register your backend bus class in ``can.interface.BACKENDS`` and
- ``can.interfaces.VALID_INTERFACES``.
-- Add docs where appropiate, like in ``doc/interfaces.rst`` and add
- an entry in ``doc/interface/*``.
- Update ``doc/scripts.rst`` accordingly.
-- Add tests in ``test/*`` where appropiate.
-
-
-About the ``BusABC`` class
---------------------------
-
-Concrete implementations *have to* implement the following:
- * :meth:`~can.BusABC.send` to send individual messages
- * :meth:`~can.BusABC._recv_internal` to receive individual messages
- (see note below!)
- * set the :attr:`~can.BusABC.channel_info` attribute to a string describing
- the underlying bus and/or channel
-
-They *might* implement the following:
- * :meth:`~can.BusABC.flush_tx_buffer` to allow discarding any
- messages yet to be sent
- * :meth:`~can.BusABC.shutdown` to override how the bus should
- shut down
- * :meth:`~can.BusABC.send_periodic` to override the software based
- periodic sending and push it down to the kernel or hardware
- * :meth:`~can.BusABC._apply_filters` to apply efficient filters
- to lower level systems like the OS kernel or hardware
- * :meth:`~can.BusABC._detect_available_configs` to allow the interface
- to report which configurations are currently available for new
- connections
- * :meth:`~can.BusABC.state` property to allow reading and/or changing
- the bus state
-
-.. note::
-
- *TL;DR*: Only override :meth:`~can.BusABC._recv_internal`,
- never :meth:`~can.BusABC.recv` directly.
-
- Previously, concrete bus classes had to override :meth:`~can.BusABC.recv`
- directly instead of :meth:`~can.BusABC._recv_internal`, but that has
- changed to allow the abstract base class to handle in-software message
- filtering as a fallback. All internal interfaces now implement that new
- behaviour. Older (custom) interfaces might still be implemented like that
- and thus might not provide message filtering:
-
-This is the entire ABC bus class with all internal methods:
-
-.. autoclass:: can.BusABC
- :private-members:
- :special-members:
-
-Concrete instances are created by :class:`can.Bus`.
+Welcome! Thank you for your interest in python-can. Whether you want to fix a bug,
+add a feature, improve documentation, write examples, help solve issues,
+or simply report a problem, your contribution is valued.
+Contributions are made via the `python-can GitHub repository `_.
+If you have questions, feel free to open an issue or start a discussion on GitHub.
+If you're new to the codebase, see the next section for an overview of the code structure.
+For more about the internals, see :ref:`internalapi` and information on extending the ``can.io`` module.
Code Structure
---------------
+^^^^^^^^^^^^^^
The modules in ``python-can`` are:
@@ -104,28 +36,246 @@ The modules in ``python-can`` are:
+---------------------------------+------------------------------------------------------+
|:doc:`message ` | Contains the interface independent Message object. |
+---------------------------------+------------------------------------------------------+
-|:doc:`io ` | Contains a range of file readers and writers. |
+|:doc:`io ` | Contains a range of file readers and writers. |
+---------------------------------+------------------------------------------------------+
|:doc:`broadcastmanager ` | Contains interface independent broadcast manager |
| | code. |
+---------------------------------+------------------------------------------------------+
-|:doc:`CAN ` | Legacy API. Deprecated. |
-+---------------------------------+------------------------------------------------------+
+Step-by-Step Contribution Guide
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+1. **Fork and Clone the Repository**
+
+ * Fork the python-can repository on GitHub to your own account.
+ * Clone your fork:
+
+ .. code-block:: shell
+
+ git clone https://github.com//python-can.git
+ cd python-can
+
+ * Create a new branch for your work:
+
+ .. code-block:: shell
+
+ git checkout -b my-feature-branch
+
+ * Ensure your branch is up to date with the latest changes from the main repository.
+ First, add the main repository as a remote (commonly named `upstream`) if you haven't already:
+
+ .. code-block:: shell
+
+ git remote add upstream https://github.com/hardbyte/python-can.git
+
+ Then, regularly fetch and rebase from the main branch:
+
+ .. code-block:: shell
+
+ git fetch upstream
+ git rebase upstream/main
+
+2. **Set Up Your Development Environment**
+
+ We recommend using `uv `__ to install development tools and run CLI utilities.
+ `uv` is a modern Python packaging tool that can quickly create virtual environments and manage dependencies,
+ including downloading required Python versions automatically. The `uvx` command allows you to run CLI tools
+ in isolated environments, separate from your global Python installation. This is useful for installing and
+ running Python applications (such as tox) without affecting your project's dependencies or environment.
+
+ **Install tox (if not already available):**
+
+
+ .. code-block:: shell
+
+ uv tool install tox --with tox-uv
+
+
+ **Quickly running your local python-can code**
+
+ To run a local script (e.g., `snippet.py`) using your current python-can code,
+ you can use either the traditional `virtualenv` and `pip` workflow or the modern `uv` tool.
+
+ **Traditional method (virtualenv + pip):**
+
+ Create a virtual environment and install the package in editable mode.
+ This allows changes to your local code to be reflected immediately, without reinstalling.
+
+ .. code-block:: shell
+
+ # Create a new virtual environment
+ python -m venv .venv
+
+ # Activate the environment
+ .venv\Scripts\activate # On Windows
+ source .venv/bin/activate # On Unix/macOS
+
+ # Upgrade pip and install python-can in editable mode with development dependencies
+ python -m pip install --upgrade pip
+ pip install -e .[dev]
+
+ # Run your script
+ python snippet.py
+
+ **Modern method (uv):**
+
+ With `uv`, you can run your script directly:
+
+ .. code-block:: shell
+
+ uv run snippet.py
+
+ When ``uv run ...`` is called inside a project,
+ `uv` automatically sets up the environment and symlinks local packages.
+ No editable install is needed—changes to your code are reflected immediately.
+
+3. **Make Your Changes**
+
+ * Edit code, documentation, or tests as needed.
+ * If you fix a bug or add a feature, add or update tests in the ``test/`` directory.
+ * If your change affects users, update documentation in ``doc/`` and relevant docstrings.
+
+4. **Test Your Changes**
+
+ This project uses `tox `__ to automate all checks (tests, lint, type, docs).
+ Tox will set up isolated environments and run the right tools for you.
+
+ To run all checks:
+
+ .. code-block:: shell
+
+ tox
+
+ To run a specific check, use:
+
+ .. code-block:: shell
+
+ tox -e lint # Run code style and lint checks (black, ruff, pylint)
+ tox -e type # Run type checks (mypy)
+ tox -e docs # Build and test documentation (sphinx)
+ tox -e py # Run tests (pytest)
+
+ To run all checks in parallel (where supported), you can use:
+
+ .. code-block:: shell
+
+ tox p
+
+ Some environments require specific Python versions.
+ If you use `uv`, it will automatically download and manage these for you.
+
+
+
+5. **Add a News Fragment for the Changelog**
+
+ This project uses `towncrier `__ to manage the changelog in
+ ``CHANGELOG.md``. For every user-facing change (new feature, bugfix, deprecation, etc.), you
+ must add a news fragment:
+
+ * News fragments are short files describing your change, stored in ``doc/changelog.d``.
+ * Name each fragment ``..md``, where ```` is one of:
+ ``added``, ``changed``, ``deprecated``, ``removed``, ``fixed``, or ``security``.
+ * Example (for a feature added in PR #1234):
+
+ .. code-block:: shell
+
+ echo "Added support for CAN FD." > doc/changelog.d/1234.added.md
+
+ * Or use the towncrier CLI:
+
+ .. code-block:: shell
+
+ uvx towncrier create --dir doc/changelog.d -c "Added support for CAN FD." 1234.added.md
+
+ * For changes not tied to an issue/PR, the fragment name must start with a plus symbol
+ (e.g., ``+mychange.added.md``). Towncrier calls these "orphan fragments".
+
+ .. note:: You do not need to manually update ``CHANGELOG.md``—maintainers will build the
+ changelog at release time.
+
+6. **(Optional) Build Source Distribution and Wheels**
+
+ If you want to manually build the source distribution (sdist) and wheels for python-can,
+ you can use `uvx` to run the build and twine tools:
+
+ .. code-block:: shell
+
+ uv build
+ uvx twine check --strict dist/*
+
+7. **Push and Submit Your Contribution**
+
+ * Push your branch:
+
+ .. code-block:: shell
+
+ git push origin my-feature-branch
+
+ * Open a pull request from your branch to the ``main`` branch of the main python-can repository on GitHub.
+
+ Please be patient — maintainers review contributions as time allows.
+
+Creating a new interface/backend
+--------------------------------
+
+.. attention::
+ Please note: Pull requests that attempt to add new hardware interfaces directly to the
+ python-can codebase will not be accepted. Instead, we encourage contributors to create
+ plugins by publishing a Python package containing your :class:`can.BusABC` subclass and
+ using it within the python-can API. We will mention your package in this documentation
+ and add it as an optional dependency. For current best practices, please refer to
+ :ref:`plugin interface`.
+
+ The following guideline is retained for informational purposes only and is not valid for new
+ contributions.
+
+These steps are a guideline on how to add a new backend to python-can.
+
+* Create a module (either a ``*.py`` or an entire subdirectory depending
+ on the complexity) inside ``can.interfaces``. See ``can/interfaces/socketcan`` for a reference implementation.
+* Implement the central part of the backend: the bus class that extends
+ :class:`can.BusABC`.
+ See :ref:`businternals` for more info on this one!
+* Register your backend bus class in ``BACKENDS`` in the file ``can.interfaces.__init__.py``.
+* Add docs where appropriate. At a minimum add to ``doc/interfaces.rst`` and add
+ a new interface specific document in ``doc/interface/*``.
+ It should document the supported platforms and also the hardware/software it requires.
+ A small snippet of how to install the dependencies would also be useful to get people started without much friction.
+* Also, don't forget to document your classes, methods and function with docstrings.
+* Add tests in ``test/*`` where appropriate. For example, see ``test/back2back_test.py`` and add a test case like ``BasicTestSocketCan`` for your new interface.
Creating a new Release
----------------------
-- Release from the ``master`` branch.
-- Update the library version in ``__init__.py`` using `semantic versioning `__.
-- Run all tests and examples against available hardware.
-- Update `CONTRIBUTORS.txt` with any new contributors.
-- For larger changes update ``doc/history.rst``.
-- Sanity check that documentation has stayed inline with code.
-- Create a temporary virtual environment. Run ``python setup.py install`` and ``python setup.py test``
-- Create and upload the distribution: ``python setup.py sdist bdist_wheel``
-- Sign the packages with gpg ``gpg --detach-sign -a dist/python_can-X.Y.Z-py3-none-any.whl``
-- Upload with twine ``twine upload dist/python-can-X.Y.Z*``
-- In a new virtual env check that the package can be installed with pip: ``pip install python-can==X.Y.Z``
-- Create a new tag in the repository.
-- Check the release on PyPi, Read the Docs and GitHub.
+Releases are automated via GitHub Actions. To create a new release:
+
+* Build the changelog with towncrier:
+
+
+ * Collect all news fragments and update ``CHANGELOG.md`` by running:
+
+ .. code-block:: shell
+
+ uvx towncrier build --yes --version vX.Y.Z
+
+ (Replace ``vX.Y.Z`` with the new version number. **The version must exactly match the tag you will create for the release.**)
+ This will add all news fragments to the changelog and remove the fragments by default.
+
+ .. note:: You can generate the changelog for prereleases, but keep the news
+ fragments so they are included in the final release. To do this, replace ``--yes`` with ``--keep``.
+ This will update ``CHANGELOG.md`` but leave the fragments in place for future builds.
+
+ * Review ``CHANGELOG.md`` for accuracy and completeness.
+
+* Ensure all tests pass and documentation is up-to-date.
+* Update ``CONTRIBUTORS.txt`` with any new contributors.
+* For larger changes, update ``doc/history.rst``.
+* Create a new tag and GitHub release (e.g., ``vX.Y.Z``) targeting the ``main``
+ branch. Add release notes and publish.
+* The CI workflow will automatically build, check, and upload the release to PyPI
+ and other platforms.
+
+* You can monitor the release status on:
+ `PyPi `__,
+ `Read the Docs `__ and
+ `GitHub Releases `__.
diff --git a/doc/errors.rst b/doc/errors.rst
new file mode 100644
index 000000000..bc954738a
--- /dev/null
+++ b/doc/errors.rst
@@ -0,0 +1,8 @@
+.. _errors:
+
+Error Handling
+==============
+
+.. automodule:: can.exceptions
+ :members:
+ :show-inheritance:
diff --git a/doc/listeners.rst b/doc/file_io.rst
similarity index 56%
rename from doc/listeners.rst
rename to doc/file_io.rst
index b807cc7a7..329ccac53 100644
--- a/doc/listeners.rst
+++ b/doc/file_io.rst
@@ -1,151 +1,204 @@
-Listeners
-=========
-
-Listener
---------
-
-The Listener class is an "abstract" base class for any objects which wish to
-register to receive notifications of new messages on the bus. A Listener can
-be used in two ways; the default is to **call** the Listener with a new
-message, or by calling the method **on_message_received**.
-
-Listeners are registered with :ref:`notifier` object(s) which ensure they are
-notified whenever a new message is received.
-
-Subclasses of Listener that do not override **on_message_received** will cause
-:class:`NotImplementedError` to be thrown when a message is received on
-the CAN bus.
-
-.. autoclass:: can.Listener
- :members:
-
-
-BufferedReader
---------------
-
-.. autoclass:: can.BufferedReader
- :members:
-
-.. autoclass:: can.AsyncBufferedReader
- :members:
-
-
-Logger
-------
-
-The :class:`can.Logger` uses the following :class:`can.Listener` types to
-create log files with different file types of the messages received.
-
-.. autoclass:: can.Logger
- :members:
-
-
-Printer
--------
-
-.. autoclass:: can.Printer
- :members:
-
-
-CSVWriter
----------
-
-.. autoclass:: can.CSVWriter
- :members:
-
-.. autoclass:: can.CSVReader
- :members:
-
-
-SqliteWriter
-------------
-
-.. autoclass:: can.SqliteWriter
- :members:
-
-.. autoclass:: can.SqliteReader
- :members:
-
-
-Database table format
-~~~~~~~~~~~~~~~~~~~~~
-
-The messages are written to the table ``messages`` in the sqlite database
-by default. The table is created if it does not already exist.
-
-The entries are as follows:
-
-============== ============== ==============
-Name Data type Note
--------------- -------------- --------------
-ts REAL The timestamp of the message
-arbitration_id INTEGER The arbitration id, might use the extended format
-extended INTEGER ``1`` if the arbitration id uses the extended format, else ``0``
-remote INTEGER ``1`` if the message is a remote frame, else ``0``
-error INTEGER ``1`` if the message is an error frame, else ``0``
-dlc INTEGER The data length code (DLC)
-data BLOB The content of the message
-============== ============== ==============
-
-
-ASC (.asc Logging format)
--------------------------
-ASCWriter logs CAN data to an ASCII log file compatible with other CAN tools such as
-Vector CANalyzer/CANoe and other.
-Since no official specification exists for the format, it has been reverse-
-engineered from existing log files. One description of the format can be found `here
-`_.
-
-
-.. note::
-
- Channels will be converted to integers.
-
-
-.. autoclass:: can.ASCWriter
- :members:
-
-ASCReader reads CAN data from ASCII log files .asc,
-as further references can-utils can be used:
-`asc2log `_,
-`log2asc `_.
-
-.. autoclass:: can.ASCReader
- :members:
-
-
-Log (.log can-utils Logging format)
------------------------------------
-
-CanutilsLogWriter logs CAN data to an ASCII log file compatible with `can-utils `
-As specification following references can-utils can be used:
-`asc2log `_,
-`log2asc `_.
-
-
-.. autoclass:: can.CanutilsLogWriter
- :members:
-
-**CanutilsLogReader** reads CAN data from ASCII log files .log
-
-.. autoclass:: can.CanutilsLogReader
- :members:
-
-
-BLF (Binary Logging Format)
----------------------------
-
-Implements support for BLF (Binary Logging Format) which is a proprietary
-CAN log format from Vector Informatik GmbH.
-
-The data is stored in a compressed format which makes it very compact.
-
-.. note:: Channels will be converted to integers.
-
-.. autoclass:: can.BLFWriter
- :members:
-
-The following class can be used to read messages from BLF file:
-
-.. autoclass:: can.BLFReader
- :members:
+File IO
+=======
+
+
+Reading and Writing Files
+-------------------------
+
+.. autofunction:: can.LogReader
+.. autofunction:: can.Logger
+.. autodata:: can.io.logger.MESSAGE_WRITERS
+.. autodata:: can.io.player.MESSAGE_READERS
+
+Printer
+-------
+
+.. autoclass:: can.Printer
+ :show-inheritance:
+ :members:
+
+
+CSVWriter
+---------
+
+.. autoclass:: can.CSVWriter
+ :show-inheritance:
+ :members:
+
+.. autoclass:: can.CSVReader
+ :show-inheritance:
+ :members:
+
+
+SqliteWriter
+------------
+
+.. autoclass:: can.SqliteWriter
+ :show-inheritance:
+ :members:
+
+.. autoclass:: can.SqliteReader
+ :show-inheritance:
+ :members:
+
+
+Database table format
+~~~~~~~~~~~~~~~~~~~~~
+
+The messages are written to the table ``messages`` in the sqlite database
+by default. The table is created if it does not already exist.
+
+The entries are as follows:
+
+============== ============== ==============
+Name Data type Note
+-------------- -------------- --------------
+ts REAL The timestamp of the message
+arbitration_id INTEGER The arbitration id, might use the extended format
+extended INTEGER ``1`` if the arbitration id uses the extended format, else ``0``
+remote INTEGER ``1`` if the message is a remote frame, else ``0``
+error INTEGER ``1`` if the message is an error frame, else ``0``
+dlc INTEGER The data length code (DLC)
+data BLOB The content of the message
+============== ============== ==============
+
+
+ASC (.asc Logging format)
+-------------------------
+ASCWriter logs CAN data to an ASCII log file compatible with other CAN tools such as
+Vector CANalyzer/CANoe and other.
+Since no official specification exists for the format, it has been reverse-
+engineered from existing log files. One description of the format can be found `here
+`_.
+
+
+.. note::
+
+ Channels will be converted to integers.
+
+
+.. autoclass:: can.ASCWriter
+ :show-inheritance:
+ :members:
+
+ASCReader reads CAN data from ASCII log files .asc,
+as further references can-utils can be used:
+`asc2log `_,
+`log2asc `_.
+
+.. autoclass:: can.ASCReader
+ :show-inheritance:
+ :members:
+
+
+Log (.log can-utils Logging format)
+-----------------------------------
+
+CanutilsLogWriter logs CAN data to an ASCII log file compatible with `can-utils `_
+As specification following references can-utils can be used:
+`asc2log `_,
+`log2asc `_.
+
+
+.. autoclass:: can.CanutilsLogWriter
+ :show-inheritance:
+ :members:
+
+**CanutilsLogReader** reads CAN data from ASCII log files .log
+
+.. autoclass:: can.CanutilsLogReader
+ :show-inheritance:
+ :members:
+
+
+BLF (Binary Logging Format)
+---------------------------
+
+Implements support for BLF (Binary Logging Format) which is a proprietary
+CAN log format from Vector Informatik GmbH.
+
+The data is stored in a compressed format which makes it very compact.
+
+.. note:: Channels will be converted to integers.
+
+.. autoclass:: can.BLFWriter
+ :show-inheritance:
+ :members:
+
+The following class can be used to read messages from BLF file:
+
+.. autoclass:: can.BLFReader
+ :show-inheritance:
+ :members:
+
+
+MF4 (Measurement Data Format v4)
+--------------------------------
+
+Implements support for MF4 (Measurement Data Format v4) which is a proprietary
+format from ASAM (Association for Standardization of Automation and Measuring Systems), widely used in
+many automotive software (Vector CANape, ETAS INCA, dSPACE ControlDesk, etc.).
+
+The data is stored in a compressed format which makes it compact.
+
+.. note:: MF4 support has to be installed as an extra with for example ``pip install python-can[mf4]``.
+
+.. note:: Channels will be converted to integers.
+
+.. note:: MF4Writer does not suppport the append mode.
+
+
+.. autoclass:: can.MF4Writer
+ :show-inheritance:
+ :members:
+
+The MDF format is very flexible regarding the internal structure and it is used to handle data from multiple sources, not just CAN bus logging.
+MDF4Writer will always create a fixed internal file structure where there will be three channel groups (for standard, error and remote frames).
+Using this fixed file structure allows for a simple implementation of MDF4Writer and MF4Reader classes.
+Therefor MF4Reader can only replay files created with MF4Writer.
+
+The following class can be used to read messages from MF4 file:
+
+.. autoclass:: can.MF4Reader
+ :show-inheritance:
+ :members:
+
+
+TRC
+----
+
+Implements basic support for the TRC file format.
+
+
+.. note::
+ Comments and contributions are welcome on what file versions might be relevant.
+
+.. autoclass:: can.TRCWriter
+ :show-inheritance:
+ :members:
+
+The following class can be used to read messages from TRC file:
+
+.. autoclass:: can.TRCReader
+ :show-inheritance:
+ :members:
+
+
+Rotating Loggers
+----------------
+
+.. autoclass:: can.io.BaseRotatingLogger
+ :show-inheritance:
+ :members:
+
+.. autoclass:: can.SizedRotatingLogger
+ :show-inheritance:
+ :members:
+
+
+Replaying Files
+---------------
+
+.. autoclass:: can.MessageSync
+ :members:
+
diff --git a/doc/history.rst b/doc/history.rst
index 3ffc9bb3b..73371af4c 100644
--- a/doc/history.rst
+++ b/doc/history.rst
@@ -1,5 +1,5 @@
-History and Roadmap
-===================
+History
+=======
Background
----------
@@ -44,16 +44,47 @@ and 2018.
The CAN viewer terminal script was contributed by Kristian Sloth Lauszus in 2018.
+The CANalyst-II interface was contributed by Shaoyu Meng in 2018.
+
+@deonvdw added support for the Robotell interface in 2019.
+
+Felix Divo and Karl Ding added type hints for the core library and many
+interfaces leading up to the 4.0 release.
+
+Eric Evenchick added support for the CANtact devices in 2020.
+
+Felix Divo added an interprocess virtual bus interface in 2020.
+
+@jxltom added the gs_usb interface in 2020 supporting Geschwister Schneider USB/CAN devices
+and bytewerk.org candleLight USB CAN devices such as candlelight, canable, cantact, etc.
+
+@jaesc added the nixnet interface in 2021 supporting NI-XNET devices from National Instruments.
+
+Tuukka Pasanen @illuusio added the neousys interface in 2021.
+
+Francisco Javier Burgos Maciá @fjburgos added ixxat FD support.
+
+@domologic contributed a socketcand interface in 2021.
+
+Felix N @felixn contributed the ETAS interface in 2021.
+
+Felix Divo unified exception handling across every interface in the lead up to
+the 4.0 release.
+
+Felix Divo prepared the python-can 4.0 release.
+
+
Support for CAN within Python
-----------------------------
-Python natively supports the CAN protocol from version 3.3 on, if running on Linux:
+Python natively supports the CAN protocol from version 3.3 on, if running on Linux (with a sufficiently new kernel):
============== ============================================================== ====
Python version Feature Link
============== ============================================================== ====
3.3 Initial SocketCAN support `Docs `__
-3.4 Broadcast Banagement (BCM) commands are natively supported `Docs `__
+3.4 Broadcast Management (BCM) commands are natively supported `Docs `__
3.5 CAN FD support `Docs `__
3.7 Support for CAN ISO-TP `Docs `__
+3.9 Native support for joining CAN filters `Docs `__
============== ============================================================== ====
diff --git a/doc/images/bit_timing_dark.svg b/doc/images/bit_timing_dark.svg
new file mode 100644
index 000000000..cc54a3f51
--- /dev/null
+++ b/doc/images/bit_timing_dark.svg
@@ -0,0 +1,96 @@
+
\ No newline at end of file
diff --git a/doc/images/bit_timing_light.svg b/doc/images/bit_timing_light.svg
new file mode 100644
index 000000000..eb021ea34
--- /dev/null
+++ b/doc/images/bit_timing_light.svg
@@ -0,0 +1,92 @@
+
\ No newline at end of file
diff --git a/doc/images/viewer_changed_bytes_highlighting.png b/doc/images/viewer_changed_bytes_highlighting.png
new file mode 100644
index 000000000..53e838488
Binary files /dev/null and b/doc/images/viewer_changed_bytes_highlighting.png differ
diff --git a/doc/index.rst b/doc/index.rst
index f24831c7c..402a485e7 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -8,22 +8,22 @@ different hardware devices, and a suite of utilities for sending and receiving
messages on a CAN bus.
**python-can** runs any where Python runs; from high powered computers
-with commercial `CAN to usb` devices right down to low powered devices running
+with commercial `CAN to USB` devices right down to low powered devices running
linux such as a BeagleBone or RaspberryPi.
More concretely, some example uses of the library:
-- Passively logging what occurs on a CAN bus. For example monitoring a
- commercial vehicle using its **OBD-II** port.
+* Passively logging what occurs on a CAN bus. For example monitoring a
+ commercial vehicle using its `OBD-II port `__.
-- Testing of hardware that interacts via CAN. Modules found in
- modern cars, motocycles, boats, and even wheelchairs have had components tested
+* Testing of hardware that interacts via CAN. Modules found in
+ modern cars, motorcycles, boats, and even wheelchairs have had components tested
from Python using this library.
-- Prototyping new hardware modules or software algorithms in-the-loop. Easily
+* Prototyping new hardware modules or software algorithms in-the-loop. Easily
interact with an existing bus.
-- Creating virtual modules to prototype CAN bus communication.
+* Creating virtual modules to prototype CAN bus communication.
Brief example of the library in action: connecting to a CAN bus, creating and sending a message:
@@ -37,12 +37,15 @@ Brief example of the library in action: connecting to a CAN bus, creating and se
Contents:
.. toctree::
- :maxdepth: 2
+ :maxdepth: 1
installation
configuration
api
interfaces
+ virtual-interfaces
+ plugin-interface
+ other-tools
scripts
development
history
diff --git a/doc/installation.rst b/doc/installation.rst
index 0dc498583..822de2ce0 100644
--- a/doc/installation.rst
+++ b/doc/installation.rst
@@ -1,93 +1,144 @@
-Installation
-============
-
-
-Install ``can`` with ``pip``:
-::
-
- $ pip install python-can
-
-
-As most likely you will want to interface with some hardware, you may
-also have to install platform dependencies. Be sure to check any other
-specifics for your hardware in :doc:`interfaces`.
-
-
-GNU/Linux dependencies
-----------------------
-
-Reasonably modern Linux Kernels (2.6.25 or newer) have an implementation
-of ``socketcan``. This version of python-can will directly use socketcan
-if called with Python 3.3 or greater, otherwise that interface is used
-via ctypes.
-
-Windows dependencies
---------------------
-
-Kvaser
-~~~~~~
-
-To install ``python-can`` using the Kvaser CANLib SDK as the backend:
-
-1. Install the `latest stable release of
- Python `__.
-
-2. Install `Kvaser's latest Windows CANLib
- drivers `__.
-
-3. Test that Kvaser's own tools work to ensure the driver is properly
- installed and that the hardware is working.
-
-PCAN
-~~~~
-
-Download and install the latest driver for your interface from
-`PEAK-System's download page `__.
-
-Note that PCANBasic API timestamps count seconds from system startup. To
-convert these to epoch times, the uptime library is used. If it is not
-available, the times are returned as number of seconds from system
-startup. To install the uptime library, run ``pip install uptime``.
-
-This library can take advantage of the `Python for Windows Extensions
-`__ library if installed.
-It will be used to get notified of new messages instead of
-the CPU intensive polling that will otherwise have be used.
-
-IXXAT
-~~~~~
-
-To install ``python-can`` using the IXXAT VCI V3 SDK as the backend:
-
-1. Install `IXXAT's latest Windows VCI V3 SDK
- drivers `__.
-
-2. Test that IXXAT's own tools (i.e. MiniMon) work to ensure the driver
- is properly installed and that the hardware is working.
-
-NI-CAN
-~~~~~~
-
-Download and install the NI-CAN drivers from
-`National Instruments `__.
-
-Currently the driver only supports 32-bit Python on Windows.
-
-neoVI
-~~~~~
-
-See :doc:`interfaces/neovi`.
-
-
-Installing python-can in development mode
------------------------------------------
-
-A "development" install of this package allows you to make changes locally
-or pull updates from the Mercurial repository and use them without having to
-reinstall. Download or clone the source repository then:
-
-::
-
- python setup.py develop
-
-
+Installation
+============
+
+
+Install the ``can`` package from PyPi with ``pip`` or similar::
+
+ $ pip install python-can
+
+
+
+
+.. warning::
+ As most likely you will want to interface with some hardware, you may
+ also have to install platform dependencies. Be sure to check any other
+ specifics for your hardware in :doc:`interfaces`.
+
+ Many interfaces can install their dependencies at the same time as ``python-can``,
+ for instance the interface ``serial`` includes the ``pyserial`` dependency which can
+ be installed with the ``serial`` extra::
+
+ $ pip install python-can[serial]
+
+
+Pre-releases
+------------
+
+The latest pre-release can be installed with::
+
+ pip install --upgrade --pre python-can
+
+
+GNU/Linux dependencies
+----------------------
+
+Reasonably modern Linux Kernels (2.6.25 or newer) have an implementation
+of ``socketcan``. This version of python-can will directly use socketcan
+if called with Python 3.3 or greater, otherwise that interface is used
+via ctypes.
+
+Windows dependencies
+--------------------
+
+Kvaser
+~~~~~~
+
+To install ``python-can`` using the Kvaser CANLib SDK as the backend:
+
+1. Install `Kvaser's latest Windows CANLib drivers `__.
+
+2. Test that Kvaser's own tools work to ensure the driver is properly
+ installed and that the hardware is working.
+
+PCAN
+~~~~
+
+Download and install the latest driver for your interface:
+
+- `Windows `__ (also supported on *Cygwin*)
+- `Linux `__ (`also works without `__, see also :ref:`pcandoc linux installation`)
+- `macOS `__
+
+Note that PCANBasic API timestamps count seconds from system startup. To
+convert these to epoch times, the uptime library is used. If it is not
+available, the times are returned as number of seconds from system
+startup. To install the uptime library, run ``pip install python-can[pcan]``.
+
+This library can take advantage of the `Python for Windows Extensions
+`__ library if installed.
+It will be used to get notified of new messages instead of
+the CPU intensive polling that will otherwise have be used.
+
+IXXAT
+~~~~~
+
+To install ``python-can`` using the IXXAT VCI V3 or V4 SDK as the backend:
+
+1. Install `IXXAT's latest Windows VCI V3 SDK or VCI V4 SDK (Win10)
+ drivers `__.
+
+2. Test that IXXAT's own tools (i.e. MiniMon) work to ensure the driver
+ is properly installed and that the hardware is working.
+
+NI-CAN
+~~~~~~
+
+Download and install the NI-CAN drivers from
+`National Instruments `__.
+
+Currently the driver only supports 32-bit Python on Windows.
+
+neoVI
+~~~~~
+
+See :doc:`interfaces/neovi`.
+
+Vector
+~~~~~~
+
+To install ``python-can`` using the XL Driver Library as the backend:
+
+1. Install the `latest drivers `__ for your Vector hardware interface.
+
+2. Install the `XL Driver Library `__ or copy the ``vxlapi.dll`` and/or
+ ``vxlapi64.dll`` into your working directory.
+
+3. Use Vector Hardware Configuration to assign a channel to your application.
+
+CANtact
+~~~~~~~
+
+CANtact is supported on Linux, Windows, and macOS.
+To install ``python-can`` using the CANtact driver backend:
+
+``python3 -m pip install "python-can[cantact]"``
+
+If ``python-can`` is already installed, the CANtact backend can be installed separately:
+
+``pip install cantact``
+
+Additional CANtact documentation is available at `cantact.io `__.
+
+CanViewer
+~~~~~~~~~
+
+``python-can`` has support for showing a simple CAN viewer terminal application
+by running ``python -m can.viewer``. On Windows, this depends on the
+`windows-curses library `__ which can
+be installed with:
+
+``python -m pip install "python-can[viewer]"``
+
+Installing python-can in development mode
+-----------------------------------------
+
+A "development" install of this package allows you to make changes locally
+or pull updates from the Git repository and use them without having to
+reinstall. Download or clone the source repository then:
+
+::
+
+ # install in editable mode
+ cd
+ python3 -m pip install -e .
+
diff --git a/doc/interfaces.rst b/doc/interfaces.rst
index 794959ee1..6645ec338 100644
--- a/doc/interfaces.rst
+++ b/doc/interfaces.rst
@@ -1,43 +1,43 @@
-CAN Interface Modules
----------------------
+.. _can interface modules:
+
+Hardware Interfaces
+===================
**python-can** hides the low-level, device-specific interfaces to controller
area network adapters in interface dependant modules. However as each hardware
device is different, you should carefully go through your interface's
documentation.
-The available interfaces are:
+.. note::
+ The *Interface Names* are listed in :doc:`configuration`.
+
+
+The following hardware interfaces are included in python-can:
.. toctree::
:maxdepth: 1
- interfaces/socketcan
+ interfaces/canalystii
+ interfaces/cantact
+ interfaces/etas
+ interfaces/gs_usb
+ interfaces/iscan
+ interfaces/ixxat
interfaces/kvaser
+ interfaces/neousys
+ interfaces/neovi
+ interfaces/nican
+ interfaces/nixnet
+ interfaces/pcan
+ interfaces/robotell
+ interfaces/seeedstudio
interfaces/serial
interfaces/slcan
- interfaces/ixxat
- interfaces/pcan
+ interfaces/socketcan
+ interfaces/socketcand
+ interfaces/systec
interfaces/usb2can
- interfaces/nican
- interfaces/iscan
- interfaces/neovi
interfaces/vector
- interfaces/virtual
-
-Additional interfaces can be added via a plugin interface. An external package
-can register a new interface by using the ``can.interface`` entry point in its setup.py.
-
-The format of the entry point is ``interface_name=module:classname`` where
-``classname`` is a concrete :class:`can.BusABC` implementation.
-
-::
-
- entry_points={
- 'can.interface': [
- "interface_name=module:classname",
- ]
- },
-
-The *Interface Names* are listed in :doc:`configuration`.
+Additional interface types can be added via the :ref:`plugin interface`, or by installing a third party package that utilises the :ref:`plugin interface`.
diff --git a/doc/interfaces/canalystii.rst b/doc/interfaces/canalystii.rst
new file mode 100644
index 000000000..b48782259
--- /dev/null
+++ b/doc/interfaces/canalystii.rst
@@ -0,0 +1,34 @@
+CANalyst-II
+===========
+
+CANalyst-II is a USB to CAN Analyzer device produced by Chuangxin Technology.
+
+Install: ``pip install "python-can[canalystii]"``
+
+Supported platform
+------------------
+
+Windows, Linux and Mac.
+
+.. note::
+
+ The backend driver depends on `pyusb `_ so a ``pyusb`` backend driver library such as ``libusb`` must be installed. On Windows a tool such as `Zadig `_ can be used to set the Canalyst-II USB device driver to ``libusb-win32``.
+
+Limitations
+-----------
+
+Multiple Channels
+^^^^^^^^^^^^^^^^^
+
+The USB protocol transfers messages grouped by channel. Messages received on channel 0 and channel 1 may be returned by software out of order between the two channels (although inside each channel, all messages are in order). The timestamp field of each message comes from the hardware and shows the exact time each message was received. To compare ordering of messages on channel 0 vs channel 1, sort the received messages by the timestamp field first.
+
+Backend Driver
+--------------
+
+The backend driver module `canalystii ` must be installed to use this interface. This open source driver is unofficial and based on reverse engineering. Earlier versions of python-can required a binary library from the vendor for this functionality.
+
+Bus
+---
+
+.. autoclass:: can.interfaces.canalystii.CANalystIIBus
+
diff --git a/doc/interfaces/cantact.rst b/doc/interfaces/cantact.rst
new file mode 100644
index 000000000..dc9667218
--- /dev/null
+++ b/doc/interfaces/cantact.rst
@@ -0,0 +1,8 @@
+CANtact CAN Interface
+=====================
+
+Interface for CANtact devices from Linklayer Labs
+
+.. autoclass:: can.interfaces.cantact.CantactBus
+ :show-inheritance:
+ :members:
diff --git a/doc/interfaces/etas.rst b/doc/interfaces/etas.rst
new file mode 100644
index 000000000..7986142be
--- /dev/null
+++ b/doc/interfaces/etas.rst
@@ -0,0 +1,46 @@
+ETAS
+====
+
+This interface adds support for CAN interfaces by `ETAS`_.
+The ETAS BOA_ (Basic Open API) is used.
+
+Installation
+------------
+
+Install the "ETAS ECU and Bus Interfaces – Distribution Package".
+
+.. warning::
+ Only Windows is supported by this interface.
+
+ The Linux kernel v5.13 (and greater) natively supports ETAS ES581.4, ES582.1 and ES584.1
+ USB modules. To use these under Linux, please refer to the :ref:`SocketCAN` interface
+ documentation.
+
+
+Configuration
+-------------
+
+The simplest configuration file would be::
+
+ [default]
+ interface = etas
+ channel = ETAS://ETH/ES910:abcd/CAN:1
+
+Channels are the URIs used by the underlying API.
+
+To find available URIs, use :meth:`~can.detect_available_configs`::
+
+ configs = can.interface.detect_available_configs(interfaces="etas")
+ for c in configs:
+ print(c)
+
+
+Bus
+---
+
+.. autoclass:: can.interfaces.etas.EtasBus
+ :members:
+
+
+.. _ETAS: https://www.etas.com/
+.. _BOA: https://www.etas.com/de/downloadcenter/18102.php
diff --git a/doc/interfaces/gs_usb.rst b/doc/interfaces/gs_usb.rst
new file mode 100755
index 000000000..8bab07c6f
--- /dev/null
+++ b/doc/interfaces/gs_usb.rst
@@ -0,0 +1,80 @@
+.. _gs_usb:
+
+Geschwister Schneider and candleLight
+=====================================
+
+Windows/Linux/Mac CAN driver based on usbfs or WinUSB WCID for Geschwister Schneider USB/CAN devices
+and candleLight USB CAN interfaces.
+
+Install: ``pip install "python-can[gs-usb]"``
+
+Usage: pass device ``index`` or ``channel`` (starting from 0) if using automatic device detection:
+
+::
+
+ import can
+ import usb
+ dev = usb.core.find(idVendor=0x1D50, idProduct=0x606F)
+
+ bus = can.Bus(interface="gs_usb", channel=dev.product, index=0, bitrate=250000)
+ bus = can.Bus(interface="gs_usb", channel=0, bitrate=250000) # same
+
+Alternatively, pass ``bus`` and ``address`` to open a specific device. The parameters can be got by ``pyusb`` as shown below:
+
+.. code-block:: python
+
+ import usb
+ import can
+
+ dev = usb.core.find(idVendor=0x1D50, idProduct=0x606F)
+ bus = can.Bus(
+ interface="gs_usb",
+ channel=dev.product,
+ bus=dev.bus,
+ address=dev.address,
+ bitrate=250000
+ )
+
+
+Supported devices
+-----------------
+
+Geschwister Schneider USB/CAN devices and bytewerk.org candleLight USB CAN interfaces such as candleLight, canable, cantact, etc.
+
+
+Supported platform
+------------------
+
+Windows, Linux and Mac.
+
+.. note::
+
+ The backend driver depends on `pyusb `_ so a ``pyusb`` backend driver library such as
+ ``libusb`` must be installed.
+
+ On Windows a tool such as `Zadig `_ can be used to set the USB device driver to
+ ``libusbK``.
+
+
+Supplementary Info
+------------------
+
+The firmware implementation for Geschwister Schneider USB/CAN devices and candleLight USB CAN can be found in `candle-usb/candleLight_fw `_.
+The Linux kernel driver can be found in `linux/drivers/net/can/usb/gs_usb.c `_.
+
+The ``gs_usb`` interface in ``python-can`` relies on upstream ``gs_usb`` package, which can be found in
+`https://pypi.org/project/gs-usb/ `_ or
+`https://github.com/jxltom/gs_usb `_.
+
+The ``gs_usb`` package uses ``pyusb`` as backend, which brings better cross-platform compatibility.
+
+Note: The bitrate ``10K``, ``20K``, ``50K``, ``83.333K``, ``100K``, ``125K``, ``250K``, ``500K``, ``800K`` and ``1M`` are supported in this interface, as implemented in the upstream ``gs_usb`` package's ``set_bitrate`` method.
+
+.. warning::
+ Message filtering is not supported in Geschwister Schneider USB/CAN devices and bytewerk.org candleLight USB CAN interfaces.
+
+Bus
+---
+
+.. autoclass:: can.interfaces.gs_usb.GsUsbBus
+ :members:
diff --git a/doc/interfaces/ixxat.rst b/doc/interfaces/ixxat.rst
index ff52776b8..1337bf738 100644
--- a/doc/interfaces/ixxat.rst
+++ b/doc/interfaces/ixxat.rst
@@ -1,67 +1,144 @@
.. _ixxatdoc:
-IXXAT Virtual CAN Interface
-===========================
+IXXAT Virtual Communication Interface
+=====================================
-Interface to `IXXAT `__ Virtual CAN Interface V3 SDK. Works on Windows.
+Interface to `IXXAT `__ Virtual Communication Interface V3 SDK. Works on Windows.
The Linux ECI SDK is currently unsupported, however on Linux some devices are
supported with :doc:`socketcan`.
-The :meth:`~can.interfaces.ixxat.canlib.IXXATBus.send_periodic` method is supported
+The :meth:`~can.BusABC.send_periodic` method is supported
natively through the on-board cyclic transmit list.
Modifying cyclic messages is not possible. You will need to stop it, and then
start a new periodic message.
+Configuration
+-------------
+The simplest configuration file would be::
+
+ [default]
+ interface = ixxat
+ channel = 0
+
+Python-can will search for the first IXXAT device available and open the first channel.
+``interface`` and ``channel`` parameters are interpreted by frontend ``can.interfaces.interface``
+module, while the following parameters are optional and are interpreted by IXXAT implementation.
+
+* ``receive_own_messages`` (default False) Enable self-reception of sent messages.
+* ``unique_hardware_id`` (default first device) Unique hardware ID of the IXXAT device.
+* ``extended`` (default True) Allow usage of extended IDs.
+* ``fd`` (default False) Enable CAN-FD capabilities.
+* ``rx_fifo_size`` (default 16 for CAN, 1024 for CAN-FD) Number of RX mailboxes.
+* ``tx_fifo_size`` (default 16 for CAN, 128 for CAN-FD) Number of TX mailboxes.
+* ``bitrate`` (default 500000) Channel bitrate.
+* ``data_bitrate`` (defaults to 2Mbps) Channel data bitrate (only canfd, to use when message bitrate_switch is used).
+* ``sjw_abr`` (optional, only canfd) Bus timing value sample jump width (arbitration).
+* ``tseg1_abr`` (optional, only canfd) Bus timing value tseg1 (arbitration).
+* ``tseg2_abr`` (optional, only canfd) Bus timing value tseg2 (arbitration).
+* ``sjw_dbr`` (optional, only used if baudrate switch enabled) Bus timing value sample jump width (data).
+* ``tseg1_dbr`` (optional, only used if baudrate switch enabled) Bus timing value tseg1 (data).
+* ``tseg2_dbr`` (optional, only used if baudrate switch enabled) Bus timing value tseg2 (data).
+* ``ssp_dbr`` (optional, only used if baudrate switch enabled) Secondary sample point (data).
+
+
+
+Filtering
+---------
+
+The CAN filters act as an allow list in IXXAT implementation, that is if you
+supply a non-empty filter list you must explicitly state EVERY frame you want
+to receive (including RTR field).
+The can_id/mask must be specified according to IXXAT behaviour, that is
+bit 0 of can_id/mask parameters represents the RTR field in CAN frame. See IXXAT
+VCI documentation, section "Message filters" for more info.
+
+List available devices
+----------------------
+
+In case you have connected multiple IXXAT devices, you have to select them by using their unique hardware id.
+The function :meth:`~can.detect_available_configs` can be used to generate a list of :class:`~can.BusABC` constructors
+(including the channel number and unique hardware ID number for the connected devices).
+
+ .. testsetup:: ixxat
+
+ from unittest.mock import Mock
+ import can
+ assert hasattr(can, "detect_available_configs")
+ can.detect_available_configs = Mock(
+ "interface",
+ return_value=[{'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW441489'}, {'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW107422'}, {'interface': 'ixxat', 'channel': 1, 'unique_hardware_id': 'HW107422'}],
+ )
+
+ .. doctest:: ixxat
+
+ >>> import can
+ >>> configs = can.detect_available_configs("ixxat")
+ >>> for config in configs:
+ ... print(config)
+ {'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW441489'}
+ {'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW107422'}
+ {'interface': 'ixxat', 'channel': 1, 'unique_hardware_id': 'HW107422'}
+
+
+You may also get a list of all connected IXXAT devices using the function ``get_ixxat_hwids()`` as demonstrated below:
+
+ .. testsetup:: ixxat2
+
+ from unittest.mock import Mock
+ import can.interfaces.ixxat
+ assert hasattr(can.interfaces.ixxat, "get_ixxat_hwids")
+ can.interfaces.ixxat.get_ixxat_hwids = Mock(side_effect=lambda: ['HW441489', 'HW107422'])
+
+ .. doctest:: ixxat2
+
+ >>> from can.interfaces.ixxat import get_ixxat_hwids
+ >>> for hwid in get_ixxat_hwids():
+ ... print("Found IXXAT with hardware id '%s'." % hwid)
+ Found IXXAT with hardware id 'HW441489'.
+ Found IXXAT with hardware id 'HW107422'.
+
+
Bus
---
.. autoclass:: can.interfaces.ixxat.IXXATBus
:members:
-.. autoclass:: can.interfaces.ixxat.canlib.CyclicSendTask
+Implementation based on vcinpl.dll
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. autoclass:: can.interfaces.ixxat.canlib_vcinpl.IXXATBus
:members:
+.. autoclass:: can.interfaces.ixxat.canlib_vcinpl.CyclicSendTask
+ :members:
-Configuration file
-------------------
-The simplest configuration file would be::
+Implementation based on vcinpl2.dll
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- [default]
- interface = ixxat
- channel = 0
+.. autoclass:: can.interfaces.ixxat.canlib_vcinpl2.IXXATBus
+ :members:
-Python-can will search for the first IXXAT device available and open the first channel.
-``interface`` and ``channel`` parameters are interpreted by frontend ``can.interfaces.interface``
-module, while the following parameters are optional and are interpreted by IXXAT implementation.
+.. autoclass:: can.interfaces.ixxat.canlib_vcinpl2.CyclicSendTask
+ :members:
-* ``bitrate`` (default 500000) Channel bitrate
-* ``UniqueHardwareId`` (default first device) Unique hardware ID of the IXXAT device
-* ``rxFifoSize`` (default 16) Number of RX mailboxes
-* ``txFifoSize`` (default 16) Number of TX mailboxes
-* ``extended`` (default False) Allow usage of extended IDs
Internals
---------
-The IXXAT :class:`~can.BusABC` object is a farly straightforward interface
+The IXXAT :class:`~can.BusABC` object is a fairly straightforward interface
to the IXXAT VCI library. It can open a specific device ID or use the
first one found.
-The frame exchange *do not involve threads* in the background but is
+The frame exchange *does not involve threads* in the background but is
explicitly instantiated by the caller.
- ``recv()`` is a blocking call with optional timeout.
- ``send()`` is not blocking but may raise a VCIError if the TX FIFO is full
-RX and TX FIFO sizes are configurable with ``rxFifoSize`` and ``txFifoSize``
-options, defaulting at 16 for both.
+RX and TX FIFO sizes are configurable with ``rx_fifo_size`` and ``tx_fifo_size``
+options, defaulting to 16 for both.
-The CAN filters act as a "whitelist" in IXXAT implementation, that is if you
-supply a non-empty filter list you must explicitly state EVERY frame you want
-to receive (including RTR field).
-The can_id/mask must be specified according to IXXAT behaviour, that is
-bit 0 of can_id/mask parameters represents the RTR field in CAN frame. See IXXAT
-VCI documentation, section "Message filters" for more info.
diff --git a/doc/interfaces/kvaser.rst b/doc/interfaces/kvaser.rst
index 289300093..f2c93f85b 100644
--- a/doc/interfaces/kvaser.rst
+++ b/doc/interfaces/kvaser.rst
@@ -10,6 +10,8 @@ Bus
---
.. autoclass:: can.interfaces.kvaser.canlib.KvaserBus
+ :members:
+ :exclude-members: get_stats
Internals
@@ -18,7 +20,7 @@ Internals
The Kvaser :class:`~can.Bus` object with a physical CAN Bus can be operated in two
modes; ``single_handle`` mode with one shared bus handle used for both reading and
writing to the CAN bus, or with two separate bus handles.
-Two separate handles are needed if receiving and sending messages are done in
+Two separate handles are needed if receiving and sending messages in
different threads (see `Kvaser documentation
`_).
@@ -35,3 +37,14 @@ If one filter is requested, this is will be handled by the Kvaser driver.
If more than one filter is needed, these will be handled in Python code
in the ``recv`` method. If a message does not match any of the filters,
``recv()`` will return None.
+
+
+Custom methods
+~~~~~~~~~~~~~~
+
+This section contains Kvaser driver specific methods.
+
+
+.. automethod:: can.interfaces.kvaser.canlib.KvaserBus.get_stats
+.. autoclass:: can.interfaces.kvaser.structures.BusStatistics
+ :members:
diff --git a/doc/interfaces/neousys.rst b/doc/interfaces/neousys.rst
new file mode 100644
index 000000000..97a37868c
--- /dev/null
+++ b/doc/interfaces/neousys.rst
@@ -0,0 +1,13 @@
+Neousys CAN Interface
+=====================
+
+This kind of interface can be found for example on Neousys POC-551VTC
+One needs to have correct drivers and DLL (Share object for Linux) from
+`Neousys `_.
+
+Beware this is only tested on Linux kernel higher than v5.3. This should be drop in
+with Windows but you have to replace with correct named DLL
+
+.. autoclass:: can.interfaces.neousys.NeousysBus
+ :show-inheritance:
+ :members:
diff --git a/doc/interfaces/neovi.rst b/doc/interfaces/neovi.rst
index dbb753479..bc711d86b 100644
--- a/doc/interfaces/neovi.rst
+++ b/doc/interfaces/neovi.rst
@@ -1,9 +1,9 @@
-NEOVI Interface
-==================
+Intrepid Control Systems neoVI
+==============================
-.. warning::
+.. note::
- This ``ICS NeoVI`` documentation is a work in progress. Feedback and revisions
+ This ``ICS neoVI`` documentation is a work in progress. Feedback and revisions
are most welcome!
@@ -14,16 +14,16 @@ wrapper on Windows.
Installation
------------
-This neovi interface requires the installation of the ICS neoVI DLL and python-ics
+This neoVI interface requires the installation of the ICS neoVI DLL and ``python-ics``
package.
- Download and install the Intrepid Product Drivers
`Intrepid Product Drivers `__
-- Install python-ics
+- Install ``python-can`` with the ``neovi`` extras:
.. code-block:: bash
- pip install python-ics
+ pip install python-can[neovi]
Configuration
@@ -42,5 +42,6 @@ Bus
---
.. autoclass:: can.interfaces.ics_neovi.NeoViBus
-
-
+.. autoexception:: can.interfaces.ics_neovi.ICSApiError
+.. autoexception:: can.interfaces.ics_neovi.ICSInitializationError
+.. autoexception:: can.interfaces.ics_neovi.ICSOperationError
diff --git a/doc/interfaces/nican.rst b/doc/interfaces/nican.rst
index ec4e82cb6..6e802a3d4 100644
--- a/doc/interfaces/nican.rst
+++ b/doc/interfaces/nican.rst
@@ -1,7 +1,7 @@
-NI-CAN
-======
+National Instruments NI-CAN
+===========================
-This interface adds support for CAN controllers by `National Instruments`_.
+This interface adds support for NI-CAN controllers by `National Instruments`_.
.. warning::
@@ -12,7 +12,7 @@ This interface adds support for CAN controllers by `National Instruments`_.
.. warning::
- CAN filtering has not been tested throughly and may not work as expected.
+ CAN filtering has not been tested thoroughly and may not work as expected.
Bus
@@ -21,6 +21,7 @@ Bus
.. autoclass:: can.interfaces.nican.NicanBus
.. autoexception:: can.interfaces.nican.NicanError
+.. autoexception:: can.interfaces.nican.NicanInitializationError
.. _National Instruments: http://www.ni.com/can/
diff --git a/doc/interfaces/nixnet.rst b/doc/interfaces/nixnet.rst
new file mode 100644
index 000000000..5a17e7e8d
--- /dev/null
+++ b/doc/interfaces/nixnet.rst
@@ -0,0 +1,21 @@
+National Instruments NI-XNET
+============================
+
+This interface adds support for NI-XNET CAN controllers by `National Instruments`_.
+
+
+.. note::
+
+ NI-XNET only supports windows platforms.
+
+
+Bus
+---
+
+.. autoclass:: can.interfaces.nixnet.NiXNETcanBus
+ :show-inheritance:
+ :member-order: bysource
+ :members:
+
+
+.. _National Instruments: http://www.ni.com/can/
diff --git a/doc/interfaces/pcan.rst b/doc/interfaces/pcan.rst
index 9bbaec9cb..48e7dba05 100644
--- a/doc/interfaces/pcan.rst
+++ b/doc/interfaces/pcan.rst
@@ -3,13 +3,7 @@
PCAN Basic API
==============
-Interface to `Peak-System `__'s PCAN-Basic API.
-
-Windows driver: https://www.peak-system.com/Downloads.76.0.html?&L=1
-
-Linux driver: https://www.peak-system.com/fileadmin/media/linux/index.htm#download and https://www.peak-system.com/Downloads.76.0.html?&L=1 (PCAN-Basic API (Linux))
-
-Mac driver: http://www.mac-can.com
+Interface to `Peak-System `__'s PCAN-Basic API.
Configuration
-------------
@@ -21,18 +15,12 @@ Here is an example configuration file for using `PCAN-USB = 3.4 supports the PCAN adapters natively via :doc:`/interfaces/socketcan`, refer to: :ref:`socketcan-pcan`.
+Beginning with version 3.4, Linux kernels support the PCAN adapters natively via :doc:`/interfaces/socketcan`, refer to: :ref:`socketcan-pcan`.
Bus
---
.. autoclass:: can.interfaces.pcan.PcanBus
+ :members:
diff --git a/doc/interfaces/robotell.rst b/doc/interfaces/robotell.rst
new file mode 100644
index 000000000..65b9dfff6
--- /dev/null
+++ b/doc/interfaces/robotell.rst
@@ -0,0 +1,27 @@
+.. _robotell:
+
+Robotell CAN-USB interface
+==========================
+
+An USB to CAN adapter sold on Aliexpress, etc. with the manufacturer name Robotell printed on the case.
+There is also a USB stick version with a clear case. If the description or screenshots refer to ``EmbededDebug`` or ``EmbededConfig``
+the device should be compatible with this driver.
+These USB devices are based on a STM32 controller with a CH340 serial interface and use a binary protocol - NOT compatible with SLCAN
+
+See `https://www.amobbs.com/thread-4651667-1-1.html `_ for some background on these devices.
+
+This driver directly uses either the local or remote (not tested) serial port.
+Remote serial ports will be specified via special URL. Both raw TCP sockets as also RFC2217 ports are supported.
+
+Usage: use ``port or URL[@baurate]`` to open the device.
+For example use ``/dev/ttyUSB0@115200`` or ``COM4@9600`` for local serial ports and
+``socket://192.168.254.254:5000`` or ``rfc2217://192.168.254.254:5000`` for remote ports.
+
+
+
+Bus
+---
+
+.. autoclass:: can.interfaces.robotell.robotellBus
+ :members:
+
diff --git a/doc/interfaces/seeedstudio.rst b/doc/interfaces/seeedstudio.rst
new file mode 100644
index 000000000..173944e0d
--- /dev/null
+++ b/doc/interfaces/seeedstudio.rst
@@ -0,0 +1,92 @@
+.. _seeeddoc:
+
+
+Seeed Studio USB-CAN Analyzer
+=============================
+
+SKU: 114991193
+
+Links:
+
+- https://www.seeedstudio.com/USB-CAN-Analyzer-p-2888.html
+- https://github.com/SeeedDocument/USB-CAN_Analyzer
+- https://copperhilltech.com/blog/usbcan-analyzer-usb-to-can-bus-serial-protocol-definition/
+
+
+Installation
+------------
+
+This interface has additional dependencies which can be installed using pip and the optional extra ``seeedstudio``. That will include the dependency ``pyserial``::
+
+ pip install python-can[seeedstudio]
+
+
+
+Interface
+---------
+
+::
+
+ can.interfaces.seeedstudio.SeeedBus
+
+A bus example::
+
+ bus = can.interface.Bus(interface='seeedstudio', channel='/dev/ttyUSB0', bitrate=500000)
+
+
+
+Configuration
+-------------
+::
+
+ SeeedBus(channel,
+ baudrate=2000000,
+ timeout=0.1,
+ frame_type='STD',
+ operation_mode='normal',
+ bitrate=500000,
+ can_filters=None)
+
+CHANNEL
+ The serial port created by the USB device when connected.
+
+TIMEOUT
+ Only used by the underling serial port, it probably should not be changed. The serial port baudrate=2000000 and rtscts=false are also matched to the device so are not added here.
+
+FRAMETYPE
+ - "STD"
+ - "EXT"
+
+OPERATIONMODE
+ - "normal"
+ - "loopback"
+ - "silent"
+ - "loopback_and_silent"
+
+BITRATE
+ - 1000000
+ - 800000
+ - 500000
+ - 400000
+ - 250000
+ - 200000
+ - 125000
+ - 100000
+ - 50000
+ - 20000
+ - 10000
+ - 5000
+
+CAN_FILTERS
+ A list of can filter dictionaries. Defaults to None (i.e. no filtering).
+ Each filter dictionary should have the following keys:
+ - ``can_id``: The CAN ID to filter on.
+ - ``can_mask``: The mask to apply to the ID.
+
+ Example: ``[{"can_id": 0x11, "can_mask": 0x21},]``
+
+ If one filter is provided, it will be used by the high-performance
+ hardware filter. If zero or more than one filter is provided,
+ software-based filtering will be used.
+
+
diff --git a/doc/interfaces/serial.rst b/doc/interfaces/serial.rst
index 413d9cfd1..566ec7755 100644
--- a/doc/interfaces/serial.rst
+++ b/doc/interfaces/serial.rst
@@ -21,6 +21,8 @@ Bus
.. autoclass:: can.interfaces.serial.serial_can.SerialBus
+ .. automethod:: _recv_internal
+
Internals
---------
The frames that will be sent and received over the serial interface consist of
@@ -28,7 +30,9 @@ six parts. The start and the stop byte for the frame, the timestamp, DLC,
arbitration ID and the payload. The payload has a variable length of between
0 and 8 bytes, the other parts are fixed. Both, the timestamp and the
arbitration ID will be interpreted as 4 byte unsigned integers. The DLC is
-also an unsigned integer with a length of 1 byte.
+also an unsigned integer with a length of 1 byte. Extended (29-bit)
+identifiers are encoded by adding 0x80000000 to the ID. For example, a
+29-bit CAN ID of 0x123 is encoded with an arbitration ID of 0x80000123.
Serial frame format
^^^^^^^^^^^^^^^^^^^
@@ -98,5 +102,23 @@ Examples of serial frames
+----------------+---------------------+------+---------------------+--------------+
| Start of frame | Timestamp | DLC | Arbitration ID | End of frame |
+================+=====================+======+=====================+==============+
-| 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x00 | 0xBBS |
+| 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x00 | 0xBB |
++----------------+---------------------+------+---------------------+--------------+
+
+.. rubric:: Extended Frame CAN message with 0 byte payload with an 29-bit CAN ID
+
++----------------+---------+
+| CAN message |
++----------------+---------+
+| Arbitration ID | Payload |
++================+=========+
+| 0x80000001 (1) | None |
++----------------+---------+
+
++----------------+---------------------+------+---------------------+--------------+
+| Serial frame |
++----------------+---------------------+------+---------------------+--------------+
+| Start of frame | Timestamp | DLC | Arbitration ID | End of frame |
++================+=====================+======+=====================+==============+
+| 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x80 | 0xBB |
+----------------+---------------------+------+---------------------+--------------+
diff --git a/doc/interfaces/socketcan.rst b/doc/interfaces/socketcan.rst
index f9c674174..c55a93bc5 100644
--- a/doc/interfaces/socketcan.rst
+++ b/doc/interfaces/socketcan.rst
@@ -1,16 +1,27 @@
+.. _SocketCAN:
+
SocketCAN
=========
-The full documentation for socketcan can be found in the kernel docs at
-`networking/can.txt `_.
+The SocketCAN documentation can be found in the `Linux kernel docs`_ in the
+``networking`` directory. Quoting from the SocketCAN Linux documentation:
+ The socketcan package is an implementation of CAN protocols
+ (Controller Area Network) for Linux. CAN is a networking technology
+ which has widespread use in automation, embedded devices, and
+ automotive fields. While there have been other CAN implementations
+ for Linux based on character devices, SocketCAN uses the Berkeley
+ socket API, the Linux network stack and implements the CAN device
+ drivers as network interfaces. The CAN socket API has been designed
+ as similar as possible to the TCP/IP protocols to allow programmers,
+ familiar with network programming, to easily learn how to use CAN
+ sockets.
-.. note::
+.. important::
- Versions before 2.2 had two different implementations named
- ``socketcan_ctypes`` and ``socketcan_native``. These are now
- deprecated and the aliases to ``socketcan`` will be removed in
- version 3.0. Future 2.x release may raise a DeprecationWarning.
+ `python-can` versions before 2.2 had two different implementations named
+ ``socketcan_ctypes`` and ``socketcan_native``. These were removed in
+ version 4.0.0 after a deprecation period.
Socketcan Quickstart
@@ -48,12 +59,35 @@ existing ``can0`` interface with a bitrate of 1MB:
sudo ip link set can0 up type can bitrate 1000000
+CAN over Serial / SLCAN
+~~~~~~~~~~~~~~~~~~~~~~~
+
+SLCAN adapters can be used directly via :doc:`/interfaces/slcan`, or
+via :doc:`/interfaces/socketcan` with some help from the ``slcand`` utility
+which can be found in the `can-utils `_ package.
+
+To create a socketcan interface for an SLCAN adapter run the following:
+
+.. code-block:: bash
+
+ slcand -f -o -c -s5 /dev/ttyAMA0
+ ip link set up slcan0
+
+Names of the interfaces created by ``slcand`` match the ``slcan\d+`` regex.
+If a custom name is required, it can be specified as the last argument. E.g.:
+
+.. code-block:: bash
+
+ slcand -f -o -c -s5 /dev/ttyAMA0 can0
+ ip link set up can0
+
.. _socketcan-pcan:
PCAN
~~~~
-Kernels >= 3.4 supports the PCAN adapters natively via :doc:`/interfaces/socketcan`, so there is no need to install any drivers. The CAN interface can be brought like so:
+Kernels >= 3.4 supports the PCAN adapters natively via :doc:`/interfaces/socketcan`,
+so there is no need to install any drivers. The CAN interface can be brought like so:
::
@@ -61,12 +95,22 @@ Kernels >= 3.4 supports the PCAN adapters natively via :doc:`/interfaces/socketc
sudo modprobe peak_pci
sudo ip link set can0 up type can bitrate 500000
+Intrepid
+~~~~~~~~
+
+The Intrepid Control Systems, Inc provides several devices (e.g. ValueCAN) as well
+as Linux module and user-space daemon to make it possible to use them via SocketCAN.
+
+Refer to below repositories for installation instructions:
+
+- `Intrepid kernel module`_
+- `Intrepid user-space daemon`_
+
Send Test Message
^^^^^^^^^^^^^^^^^
-The `can-utils `_ library for linux
-includes a script `cansend` which is useful to send known payloads. For
-example to send a message on `vcan0`:
+The `can-utils`_ library for Linux includes a `cansend` tool which is useful to
+send known payloads. For example to send a message on `vcan0`:
.. code-block:: bash
@@ -133,16 +177,16 @@ To spam a bus:
import time
import can
- bustype = 'socketcan'
+ interface = 'socketcan'
channel = 'vcan0'
def producer(id):
""":param id: Spam the bus with messages including the data id."""
- bus = can.interface.Bus(channel=channel, bustype=bustype)
+ bus = can.Bus(channel=channel, interface=interface)
for i in range(10):
- msg = can.Message(arbitration_id=0xc0ffee, data=[id, i, 0, 1, 3, 1, 4, 1], extended_id=False)
+ msg = can.Message(arbitration_id=0xc0ffee, data=[id, i, 0, 1, 3, 1, 4, 1], is_extended_id=False)
bus.send(msg)
- # Issue #3: Need to keep running to ensure the writing threads stay alive. ?
+
time.sleep(1)
producer(10)
@@ -170,8 +214,7 @@ function:
import can
- can_interface = 'vcan0'
- bus = can.interface.Bus(can_interface, bustype='socketcan')
+ bus = can.Bus(channel='vcan0', interface='socketcan')
message = bus.recv()
By default, this performs a blocking read, which means ``bus.recv()`` won't
@@ -204,30 +247,47 @@ socket api. This allows the cyclic transmission of CAN messages at given interva
The overhead for periodic message sending is extremely low as all the heavy lifting occurs
within the linux kernel.
-send_periodic()
-~~~~~~~~~~~~~~~
+The :class:`~can.BusABC` initialized for `socketcan` interface transparently handles
+scheduling of CAN messages to Linux BCM via :meth:`~can.BusABC.send_periodic`:
+
+.. code-block:: python
-An example that uses the send_periodic is included in ``python-can/examples/cyclic.py``
+ with can.interface.Bus(interface="socketcan", channel="can0") as bus:
+ task = bus.send_periodic(...)
-The object returned can be used to halt, alter or cancel the periodic message task.
+More examples that uses :meth:`~can.BusABC.send_periodic` are included
+in ``python-can/examples/cyclic.py``.
+
+The `task` object returned by :meth:`~can.BusABC.send_periodic` can be used to halt,
+alter or cancel the periodic message task:
.. autoclass:: can.interfaces.socketcan.CyclicSendTask
+ :members:
+
+Buffer Sizes
+------------
+Currently, the sending buffer size cannot be adjusted by this library.
+However, `this issue `__ describes how to change it via the command line/shell.
Bus
---
-.. autoclass:: can.interfaces.socketcan.SocketcanBus
+The :class:`~can.interfaces.socketcan.SocketcanBus` specializes :class:`~can.BusABC`
+to ensure usage of SocketCAN Linux API. The most important differences are:
- .. method:: recv(timeout=None)
+- usage of SocketCAN BCM for periodic messages scheduling;
+- filtering of CAN messages on Linux kernel level;
+- usage of nanosecond timings from the kernel.
+
+.. autoclass:: can.interfaces.socketcan.SocketcanBus
+ :members:
+ :inherited-members:
- Block waiting for a message from the Bus.
- :param float timeout:
- seconds to wait for a message or None to wait indefinitely
+.. External references
- :rtype: can.Message or None
- :return:
- None on timeout or a :class:`can.Message` object.
- :raises can.CanError:
- if an error occurred while reading
+.. _Linux kernel docs: https://www.kernel.org/doc/Documentation/networking/can.txt
+.. _Intrepid kernel module: https://github.com/intrepidcs/intrepid-socketcan-kernel-module
+.. _Intrepid user-space daemon: https://github.com/intrepidcs/icsscand
+.. _can-utils: https://github.com/linux-can/can-utils
diff --git a/doc/interfaces/socketcand.rst b/doc/interfaces/socketcand.rst
new file mode 100644
index 000000000..f861c81b9
--- /dev/null
+++ b/doc/interfaces/socketcand.rst
@@ -0,0 +1,155 @@
+.. _socketcand_doc:
+
+socketcand Interface
+====================
+`Socketcand `__ is part of the
+`Linux-CAN `__ project, providing a
+Network-to-CAN bridge as a Linux damon. It implements a specific
+`TCP/IP based communication protocol `__
+to transfer CAN frames and control commands.
+
+The main advantage compared to UDP-based protocols (e.g. virtual interface)
+is, that TCP guarantees delivery and that the message order is kept.
+
+Here is a small example dumping all can messages received by a socketcand
+daemon running on a remote Raspberry Pi:
+
+.. code-block:: python
+
+ import can
+
+ bus = can.interface.Bus(interface='socketcand', host="10.0.16.15", port=29536, channel="can0")
+
+ # loop until Ctrl-C
+ try:
+ while True:
+ msg = bus.recv()
+ print(msg)
+ except KeyboardInterrupt:
+ pass
+
+The output may look like this::
+
+ Timestamp: 1637791111.209224 ID: 000006fd X Rx DLC: 8 c4 10 e3 2d 96 ff 25 6b
+ Timestamp: 1637791111.233951 ID: 000001ad X Rx DLC: 4 4d 47 c7 64
+ Timestamp: 1637791111.409415 ID: 000005f7 X Rx DLC: 8 86 de e6 0f 42 55 5d 39
+ Timestamp: 1637791111.434377 ID: 00000665 X Rx DLC: 8 97 96 51 0f 23 25 fc 28
+ Timestamp: 1637791111.609763 ID: 0000031d X Rx DLC: 8 16 27 d8 3d fe d8 31 24
+ Timestamp: 1637791111.634630 ID: 00000587 X Rx DLC: 8 4e 06 85 23 6f 81 2b 65
+
+
+This interface also supports :meth:`~can.detect_available_configs`.
+
+.. code-block:: python
+
+ import can
+ import can.interfaces.socketcand
+
+ cfg = can.interfaces.socketcand._detect_available_configs()
+ if cfg:
+ bus = can.Bus(**cfg[0])
+
+The socketcand daemon broadcasts UDP beacons every 3 seconds. The default
+detection method waits for slightly more than 3 seconds to receive the beacon
+packet. If you want to increase the timeout, you can use
+:meth:`can.interfaces.socketcand.detect_beacon` directly. Below is an example
+which detects the beacon and uses the configuration to create a socketcand bus.
+
+.. code-block:: python
+
+ import can
+ import can.interfaces.socketcand
+
+ cfg = can.interfaces.socketcand.detect_beacon(6000)
+ if cfg:
+ bus = can.Bus(**cfg[0])
+
+Bus
+---
+
+.. autoclass:: can.interfaces.socketcand.SocketCanDaemonBus
+ :show-inheritance:
+ :member-order: bysource
+ :members:
+
+.. autofunction:: can.interfaces.socketcand.detect_beacon
+
+Socketcand Quickstart
+---------------------
+
+The following section will show how to get the stuff installed on a Raspberry Pi with a MCP2515-based
+CAN interface, e.g. available from `Waveshare `__.
+However, it will also work with any other socketcan device.
+
+Install CAN Interface for a MCP2515 based interface on a Raspberry Pi
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Add the following lines to ``/boot/config.txt``.
+Please take care on the frequency of the crystal on your MCP2515 board::
+
+ dtparam=spi=on
+ dtoverlay=mcp2515-can0,oscillator=12000000,interrupt=25,spimaxfrequency=1000000
+
+Reboot after ``/boot/config.txt`` has been modified.
+
+
+Enable socketcan for can0
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Create config file for systemd-networkd to start the socketcan interface automatically:
+
+.. code-block:: bash
+
+ cat >/etc/systemd/network/80-can.network <<'EOT'
+ [Match]
+ Name=can0
+ [CAN]
+ BitRate=250K
+ RestartSec=100ms
+ EOT
+
+Enable ``systemd-networkd`` on reboot and start it immediately (if it was not already startet):
+
+.. code-block:: bash
+
+ sudo systemctl enable systemd-networkd
+ sudo systemctl start systemd-networkd
+
+
+Build socketcand from source
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: bash
+
+ # autoconf is needed to build socketcand
+ sudo apt-get install -y autoconf
+ # clone & build sources
+ git clone https://github.com/linux-can/socketcand.git
+ cd socketcand
+ ./autogen.sh
+ ./configure
+ make
+
+
+Install socketcand
+~~~~~~~~~~~~~~~~~~
+.. code-block:: bash
+
+ make install
+
+
+Run socketcand
+~~~~~~~~~~~~~~
+.. code-block:: bash
+
+ ./socketcand -v -i can0
+
+During start, socketcand will prompt its IP address and port it listens to::
+
+ Verbose output activated
+
+ Using network interface 'eth0'
+ Listen adress is 10.0.16.15
+ Broadcast adress is 10.0.255.255
+ creating broadcast thread...
+ binding socket to 10.0.16.15:29536
diff --git a/doc/interfaces/systec.rst b/doc/interfaces/systec.rst
new file mode 100644
index 000000000..6b04fdfe0
--- /dev/null
+++ b/doc/interfaces/systec.rst
@@ -0,0 +1,78 @@
+.. _systec:
+
+SYSTEC interface
+================
+
+Windows interface for the USBCAN devices supporting up to 2 channels based on the
+particular product. There is support for the devices also on Linux through the :doc:`socketcan` interface and for Windows using this
+``systec`` interface.
+
+Installation
+------------
+
+The interface requires installation of the **USBCAN32.dll** library. Download and install the
+driver for specific `SYSTEC `__ device.
+
+Supported devices
+-----------------
+
+The interface supports following devices:
+
+- GW-001 (obsolete),
+- GW-002 (obsolete),
+- Multiport CAN-to-USB G3,
+- USB-CANmodul1 G3,
+- USB-CANmodul2 G3,
+- USB-CANmodul8 G3,
+- USB-CANmodul16 G3,
+- USB-CANmodul1 G4,
+- USB-CANmodul2 G4.
+
+Configuration
+-------------
+
+The simplest configuration would be::
+
+ interface = systec
+ channel = 0
+
+Python-can will search for the first device found if not specified explicitly by the
+``device_number`` parameter. The ``interface`` and ``channel`` are the only mandatory
+parameters. The interface supports two channels 0 and 1. The maximum number of entries in the receive and transmit buffer can be set by the
+parameters ``rx_buffer_entries`` and ``tx_buffer_entries``, with default value 4096
+set for both.
+
+Optional parameters:
+
+* ``bitrate`` (default 500000) Channel bitrate in bit/s
+* ``device_number`` (default first device) The device number of the USB-CAN
+* ``rx_buffer_entries`` (default 4096) The maximum number of entries in the receive buffer
+* ``tx_buffer_entries`` (default 4096) The maximum number of entries in the transmit buffer
+* ``state`` (default BusState.ACTIVE) BusState of the channel
+* ``receive_own_messages`` (default False) If messages transmitted should also be received back
+
+
+Bus
+---
+
+.. autoclass:: can.interfaces.systec.ucanbus.UcanBus
+ :members:
+
+
+Internals
+---------
+
+Message filtering
+~~~~~~~~~~~~~~~~~
+
+The interface and driver supports only setting of one filter per channel. If one filter
+is requested, this is will be handled by the driver itself. If more than one filter is
+needed, these will be handled in Python code in the ``recv`` method. If a message does
+not match any of the filters, ``recv()`` will return None.
+
+Periodic tasks
+~~~~~~~~~~~~~~
+
+The driver supports periodic message sending but without the possibility to set
+the interval between messages. Therefore the handling of the periodic messages is done
+by the interface using the :class:`~can.broadcastmanager.ThreadBasedCyclicSendTask`.
diff --git a/doc/interfaces/udp_multicast.rst b/doc/interfaces/udp_multicast.rst
new file mode 100644
index 000000000..b2a049d83
--- /dev/null
+++ b/doc/interfaces/udp_multicast.rst
@@ -0,0 +1,74 @@
+.. _udp_multicast_doc:
+
+Multicast IP Interface
+======================
+
+This module implements transport of CAN and CAN FD messages over UDP via Multicast IPv4 and IPv6.
+This virtual interface allows for communication between multiple processes and even hosts.
+This differentiates it from the :ref:`virtual_interface_doc` interface,
+which can only passes messages within a single process but does not require a network stack.
+
+It runs on UDP to have the lowest possible latency (as opposed to using TCP), and because
+normal IP multicast is inherently unreliable, as the recipients are unknown.
+This enables ad-hoc networks that do not require a central server but is also a so-called
+*unreliable network*. In practice however, local area networks (LANs) should most often be
+sufficiently reliable for this interface to function properly.
+
+.. note::
+ For an overview over the different virtual buses in this library and beyond, please refer
+ to the section :ref:`virtual_interfaces_doc`. It also describes important limitations
+ of this interface.
+
+Please refer to the `Bus class documentation`_ below for configuration options and useful resources
+for specifying multicast IP addresses.
+
+Installation
+-------------------
+
+The Multicast IP Interface depends on the **msgpack** python library,
+which is automatically installed with the `multicast` extra keyword::
+
+ $ pip install python-can[multicast]
+
+
+Supported Platforms
+-------------------
+
+It should work on most Unix systems (including Linux with kernel 2.6.22+ and macOS) but currently not on Windows.
+
+Example
+-------
+
+This example should print a single line indicating that a CAN message was successfully sent
+from ``bus_1`` to ``bus_2``:
+
+.. code-block:: python
+
+ import time
+ import can
+ from can.interfaces.udp_multicast import UdpMulticastBus
+
+ # The bus can be created using the can.Bus wrapper class or using UdpMulticastBus directly
+ with can.Bus(channel=UdpMulticastBus.DEFAULT_GROUP_IPv6, interface='udp_multicast') as bus_1, \
+ UdpMulticastBus(channel=UdpMulticastBus.DEFAULT_GROUP_IPv6) as bus_2:
+
+ # register a callback on the second bus that prints messages to the standard out
+ notifier = can.Notifier(bus_2, [can.Printer()])
+
+ # create and send a message with the first bus, which should arrive at the second one
+ message = can.Message(arbitration_id=0x123, data=[1, 2, 3])
+ bus_1.send(message)
+
+ # give the notifier enough time to get triggered by the second bus
+ time.sleep(2.0)
+
+ # clean-up
+ notifier.stop()
+
+
+Bus Class Documentation
+-----------------------
+
+.. autoclass:: can.interfaces.udp_multicast.UdpMulticastBus
+ :members:
+ :exclude-members: send
diff --git a/doc/interfaces/usb2can.rst b/doc/interfaces/usb2can.rst
index 3a7e291cf..1ac9dd61f 100644
--- a/doc/interfaces/usb2can.rst
+++ b/doc/interfaces/usb2can.rst
@@ -1,35 +1,35 @@
USB2CAN Interface
=================
-OVERVIEW
---------
-
The `USB2CAN `_ is a cheap CAN interface based on an ARM7 chip (STR750FV2).
There is support for this device on Linux through the :doc:`socketcan` interface and for Windows using this
``usb2can`` interface.
-
-WINDOWS SUPPORT
----------------
-
Support though windows is achieved through a DLL very similar to the way the PCAN functions. The API is called CANAL
(CAN Abstraction Layer) which is a separate project designed to be used with VSCP which is a socket like messaging system
-that is not only cross platform but also supports other types of devices. This device can be used through one of three ways
-1)Through python-can
-2)CANAL API either using the DLL and C/C++ or through the python wrapper that has been added to this project
-3)VSCP
-Using python-can is strongly suggested as with little extra work the same interface can be used on both Windows and Linux.
+that is not only cross platform but also supports other types of devices.
+
+
+Installation
+------------
+
+1. To install on Windows download the USB2CAN Windows driver. It is compatible with XP, Vista, Win7, Win8/8.1. (Written against driver version v1.0.2.1)
+
+2. Install the appropriate version of `pywin32 `_ (win32com)
+
+3. Download the USB2CAN CANAL DLL from the USB2CAN website.
+ Place this in either the same directory you are running usb2can.py from or your DLL folder in your python install.
+ Note that only a 32-bit version is currently available, so this only works in a 32-bit Python environment.
+
+
+Internals
+---------
-WINDOWS INSTALL
----------------
+This interface originally written against CANAL DLL version ``v1.0.6``.
- 1. To install on Windows download the USB2CAN Windows driver. It is compatible with XP, Vista, Win7, Win8/8.1. (Written against driver version v1.0.2.1)
- 2. Install the appropriate version of `pywin32 `_ (win32com)
- 3. Download the USB2CAN CANAL DLL from the USB2CAN website. Place this in either the same directory you are running usb2can.py from or your DLL folder in your python install.
- (Written against CANAL DLL version v1.0.6)
Interface Layout
-----------------
+~~~~~~~~~~~~~~~~
- ``usb2canabstractionlayer.py``
This file is only a wrapper for the CANAL API that the interface expects. There are also a couple of constants here to try and make dealing with the
@@ -49,20 +49,26 @@ Interface Layout
Interface Specific Items
------------------------
-There are a few things that are kinda strange about this device and are not overly obvious about the code or things that are not done being implemented in the DLL.
-
-1. You need the Serial Number to connect to the device under Windows. This is part of the "setup string" that configures the device. There are a few options for how to get this.
- 1. Use usb2canWin.py to find the serial number
- 2. Look on the device and enter it either through a prompt/barcode scanner/hardcode it.(Not recommended)
- 3. Reprogram the device serial number to something and do that for all the devices you own. (Really Not Recommended, can no longer use multiple devices on one computer)
+There are a few things that are kinda strange about this device and are not overly obvious about the code or things that
+are not done being implemented in the DLL.
+
+1. You need the Serial Number to connect to the device under Windows. This is part of the "setup string" that configures the device. There are a few options for how to get this.
+
+ 1. Use ``usb2canWin.py`` to find the serial number.
+ 2. Look on the device and enter it either through a prompt/barcode scanner/hardcode it. (Not recommended)
+ 3. Reprogram the device serial number to something and do that for all the devices you own. (Really Not Recommended, can no longer use multiple devices on one computer)
-2. In usb2canabstractionlayer.py there is a structure called CanalMsg which has a unsigned byte array of size 8. In the usb2canInterface file it passes in an unsigned byte array of
- size 8 also which if you pass less than 8 bytes in it stuffs it with extra zeros. So if the data "01020304" is sent the message would look like "0102030400000000".
- There is also a part of this structure called sizeData which is the actual length of the data that was sent not the stuffed message (in this case would be 4).
- What then happens is although a message of size 8 is sent to the device only the length of information so the first 4 bytes of information would be sent. This
- is done because the DLL expects a length of 8 and nothing else. So to make it compatible that has to be sent through the wrapper. If usb2canInterface sent an
- array of length 4 with sizeData of 4 as well the array would throw an incompatible data type error. There is a Wireshark file posted in Issue #36 that demonstrates
- that the bus is only sending the data and not the extra zeros.
+2. In ``usb2canabstractionlayer.py`` there is a structure called ``CanalMsg`` which has a unsigned byte array of size 8.
+ In the ``usb2canInterface`` file it passes in an unsigned byte array of size 8 also which if you pass less than 8
+ bytes in it stuffs it with extra zeros. So if the data ``"01020304"`` is sent the message would look like
+ ``"0102030400000000"``.
+
+ There is also a part of this structure called ``sizeData`` which is the actual length of the data that was sent not
+ the stuffed message (in this case would be 4). What then happens is although a message of size 8 is sent to the device
+ only the first 4 bytes of information would be sent. This is done because the DLL expects a length of 8 and nothing
+ else. So to make it compatible that has to be sent through the wrapper. If ``usb2canInterface`` sent an
+ array of length 4 with sizeData of 4 as well the array would throw an incompatible data type error.
+
3. The masking features have not been implemented currently in the CANAL interface in the version currently on the USB2CAN website.
@@ -78,10 +84,16 @@ Bus
.. autoclass:: can.interfaces.usb2can.Usb2canBus
+Exceptions
+----------
+
+.. autoexception:: can.interfaces.usb2can.usb2canabstractionlayer.CanalError
-Internals
----------
+
+Miscellaneous
+-------------
.. autoclass:: can.interfaces.usb2can.Usb2CanAbstractionLayer
:members:
:undoc-members:
+
diff --git a/doc/interfaces/vector.rst b/doc/interfaces/vector.rst
index a936e693e..d3e2bed45 100644
--- a/doc/interfaces/vector.rst
+++ b/doc/interfaces/vector.rst
@@ -1,10 +1,13 @@
Vector
======
-This interface adds support for CAN controllers by `Vector`_.
+This interface adds support for CAN controllers by `Vector`_. Only Windows is supported.
+
+Configuration
+-------------
By default this library uses the channel configuration for CANalyzer.
-To use a different application, open Vector Hardware Config program and create
+To use a different application, open **Vector Hardware Configuration** program and create
a new application and assign the channels you may want to use.
Specify the application name as ``app_name='Your app name'`` when constructing
the bus or in a config file.
@@ -12,24 +15,107 @@ the bus or in a config file.
Channel should be given as a list of channels starting at 0.
Here is an example configuration file connecting to CAN 1 and CAN 2 for an
-application named "python-can"::
+application named "python-can":
+
+::
[default]
interface = vector
channel = 0, 1
app_name = python-can
-If you are using Python 2.7 it is recommended to install pywin32_, otherwise a
-slow and CPU intensive polling will be used when waiting for new messages.
-Bus
----
+VectorBus
+---------
.. autoclass:: can.interfaces.vector.VectorBus
+ :show-inheritance:
+ :member-order: bysource
+ :members:
+ set_filters,
+ recv,
+ send,
+ send_periodic,
+ stop_all_periodic_tasks,
+ flush_tx_buffer,
+ reset,
+ shutdown,
+ popup_vector_hw_configuration,
+ get_application_config,
+ set_application_config
+
+Exceptions
+----------
.. autoexception:: can.interfaces.vector.VectorError
+ :show-inheritance:
+.. autoexception:: can.interfaces.vector.VectorInitializationError
+ :show-inheritance:
+.. autoexception:: can.interfaces.vector.VectorOperationError
+ :show-inheritance:
+
+Miscellaneous
+-------------
+
+.. autofunction:: can.interfaces.vector.get_channel_configs
+
+.. autoclass:: can.interfaces.vector.VectorChannelConfig
+ :show-inheritance:
+ :class-doc-from: class
+
+.. autoclass:: can.interfaces.vector.canlib.VectorBusParams
+ :show-inheritance:
+ :class-doc-from: class
+
+.. autoclass:: can.interfaces.vector.canlib.VectorCanParams
+ :show-inheritance:
+ :class-doc-from: class
+
+.. autoclass:: can.interfaces.vector.canlib.VectorCanFdParams
+ :show-inheritance:
+ :class-doc-from: class
+
+.. autoclass:: can.interfaces.vector.xldefine.XL_HardwareType
+ :show-inheritance:
+ :member-order: bysource
+ :members:
+ :undoc-members:
+
+.. autoclass:: can.interfaces.vector.xldefine.XL_ChannelCapabilities
+ :show-inheritance:
+ :member-order: bysource
+ :members:
+ :undoc-members:
+
+.. autoclass:: can.interfaces.vector.xldefine.XL_BusCapabilities
+ :show-inheritance:
+ :member-order: bysource
+ :members:
+ :undoc-members:
+
+.. autoclass:: can.interfaces.vector.xldefine.XL_BusTypes
+ :show-inheritance:
+ :member-order: bysource
+ :members:
+ :undoc-members:
+
+.. autoclass:: can.interfaces.vector.xldefine.XL_OutputMode
+ :show-inheritance:
+ :member-order: bysource
+ :members:
+ :undoc-members:
+
+.. autoclass:: can.interfaces.vector.xldefine.XL_CANFD_BusParams_CanOpMode
+ :show-inheritance:
+ :member-order: bysource
+ :members:
+ :undoc-members:
+.. autoclass:: can.interfaces.vector.xldefine.XL_Status
+ :show-inheritance:
+ :member-order: bysource
+ :members:
+ :undoc-members:
.. _Vector: https://vector.com/
-.. _pywin32: https://sourceforge.net/projects/pywin32/
diff --git a/doc/interfaces/virtual.rst b/doc/interfaces/virtual.rst
index ed6681a57..bdadcb08d 100644
--- a/doc/interfaces/virtual.rst
+++ b/doc/interfaces/virtual.rst
@@ -1,23 +1,67 @@
+.. _virtual_interface_doc:
+
Virtual
=======
-The virtual interface can be used as a way to write OS and driver independent
-tests.
+The virtual interface can be used as a way to write OS and driver independent tests.
+Any `VirtualBus` instances connecting to the same channel (from within the same Python
+process) will receive each others messages.
-A virtual CAN bus that can be used for automatic tests. Any Bus instances
-connecting to the same channel (in the same python program) will get each
-others messages.
+If messages shall be sent across process or host borders, consider using the
+:ref:`udp_multicast_doc` and refer to :ref:`virtual_interfaces_doc`
+for a comparison and general discussion of different virtual interfaces.
+Example
+-------
.. code-block:: python
-
+
import can
- bus1 = can.interface.Bus('test', bustype='virtual')
- bus2 = can.interface.Bus('test', bustype='virtual')
+ bus1 = can.interface.Bus('test', interface='virtual')
+ bus2 = can.interface.Bus('test', interface='virtual')
msg1 = can.Message(arbitration_id=0xabcde, data=[1,2,3])
bus1.send(msg1)
msg2 = bus2.recv()
- assert msg1 == msg2
+ #assert msg1 == msg2
+ assert msg1.arbitration_id == msg2.arbitration_id
+ assert msg1.data == msg2.data
+ assert msg1.timestamp != msg2.timestamp
+
+.. code-block:: python
+
+ import can
+
+ bus1 = can.interface.Bus('test', interface='virtual', preserve_timestamps=True)
+ bus2 = can.interface.Bus('test', interface='virtual')
+
+ msg1 = can.Message(timestamp=1639740470.051948, arbitration_id=0xabcde, data=[1,2,3])
+
+ # Messages sent on bus1 will have their timestamps preserved when received
+ # on bus2
+ bus1.send(msg1)
+ msg2 = bus2.recv()
+
+ assert msg1.arbitration_id == msg2.arbitration_id
+ assert msg1.data == msg2.data
+ assert msg1.timestamp == msg2.timestamp
+
+ # Messages sent on bus2 will not have their timestamps preserved when
+ # received on bus1
+ bus2.send(msg1)
+ msg3 = bus1.recv()
+
+ assert msg1.arbitration_id == msg3.arbitration_id
+ assert msg1.data == msg3.data
+ assert msg1.timestamp != msg3.timestamp
+
+
+Bus Class Documentation
+-----------------------
+
+.. autoclass:: can.interfaces.virtual.VirtualBus
+ :members:
+
+ .. automethod:: _detect_available_configs
diff --git a/doc/internal-api.rst b/doc/internal-api.rst
new file mode 100644
index 000000000..9e544f052
--- /dev/null
+++ b/doc/internal-api.rst
@@ -0,0 +1,141 @@
+.. _internalapi:
+
+Internal API
+============
+
+Here we document the odds and ends that are more helpful for creating your own interfaces
+or listeners but generally shouldn't be required to interact with python-can.
+
+
+BusABC
+------
+
+The :class:`~can.BusABC` class, as the name suggests, provides an abstraction of a CAN bus.
+The bus provides a wrapper around a physical or virtual CAN Bus.
+
+An interface specific instance of the :class:`~can.BusABC` is created by the :class:`~can.Bus`
+class, see :ref:`bus` for the user facing API.
+
+.. _businternals:
+
+
+Extending the ``BusABC`` class
+------------------------------
+
+Concrete implementations **must** implement the following:
+ * :meth:`~can.BusABC.send` to send individual messages
+ * :meth:`~can.BusABC._recv_internal` to receive individual messages
+ (see note below!)
+ * set the :attr:`~can.BusABC.channel_info` attribute to a string describing
+ the underlying bus and/or channel
+
+They **might** implement the following:
+ * :meth:`~can.BusABC.flush_tx_buffer` to allow discarding any
+ messages yet to be sent
+ * :meth:`~can.BusABC.shutdown` to override how the bus should
+ shut down
+ * :meth:`~can.BusABC._send_periodic_internal` to override the software based
+ periodic sending and push it down to the kernel or hardware.
+ * :meth:`~can.BusABC._apply_filters` to apply efficient filters
+ to lower level systems like the OS kernel or hardware.
+ * :meth:`~can.BusABC._detect_available_configs` to allow the interface
+ to report which configurations are currently available for new
+ connections.
+ * :meth:`~can.BusABC.state` property to allow reading and/or changing
+ the bus state.
+
+.. note::
+
+ *TL;DR*: Only override :meth:`~can.BusABC._recv_internal`,
+ never :meth:`~can.BusABC.recv` directly.
+
+ Previously, concrete bus classes had to override :meth:`~can.BusABC.recv`
+ directly instead of :meth:`~can.BusABC._recv_internal`, but that has
+ changed to allow the abstract base class to handle in-software message
+ filtering as a fallback. All internal interfaces now implement that new
+ behaviour. Older (custom) interfaces might still be implemented like that
+ and thus might not provide message filtering:
+
+
+Concrete instances are usually created by :func:`can.Bus` which takes the users
+configuration into account.
+
+
+Bus Internals
+~~~~~~~~~~~~~
+
+Several methods are not documented in the main :class:`can.BusABC`
+as they are primarily useful for library developers as opposed to
+library users.
+
+.. automethod:: can.BusABC.__init__
+
+.. automethod:: can.BusABC.__iter__
+
+.. automethod:: can.BusABC.__str__
+
+.. autoattribute:: can.BusABC.__weakref__
+
+.. automethod:: can.BusABC._recv_internal
+
+.. automethod:: can.BusABC._apply_filters
+
+.. automethod:: can.BusABC._send_periodic_internal
+
+.. automethod:: can.BusABC._detect_available_configs
+
+
+About the IO module
+-------------------
+
+Handling of the different file formats is implemented in ``can.io``.
+Each file/IO type is within a separate module and ideally implements both a *Reader* and a *Writer*.
+The reader extends :class:`can.io.generic.MessageReader`, while the writer extends
+:class:`can.io.generic.MessageWriter`, a subclass of the :class:`can.Listener`,
+to be able to be passed directly to a :class:`can.Notifier`.
+
+
+
+Adding support for new file formats
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This assumes that you want to add a new file format, called *canstore*.
+Ideally add both reading and writing support for the new file format, although this is not strictly required.
+
+1. Create a new module: *can/io/canstore.py*
+ (*or* simply copy some existing one like *can/io/csv.py*)
+2. Implement a reader ``CanstoreReader`` which extends :class:`can.io.generic.MessageReader`.
+ Besides from a constructor, only ``__iter__(self)`` needs to be implemented.
+3. Implement a writer ``CanstoreWriter`` which extends :class:`can.io.generic.MessageWriter`.
+ Besides from a constructor, only ``on_message_received(self, msg)`` needs to be implemented.
+4. Add a case to ``can.io.player.LogReader``'s ``__new__()``.
+5. Document the two new classes (and possibly additional helpers) with docstrings and comments.
+ Please mention features and limitations of the implementation.
+6. Add a short section to the bottom of *doc/listeners.rst*.
+7. Add tests where appropriate, for example by simply adding a test case called
+ `class TestCanstoreFileFormat(ReaderWriterTest)` to *test/logformats_test.py*.
+ That should already handle all of the general testing.
+ Just follow the way the other tests in there do it.
+8. Add imports to *can/__init__py* and *can/io/__init__py* so that the
+ new classes can be simply imported as *from can import CanstoreReader, CanstoreWriter*.
+
+
+
+IO Utilities
+~~~~~~~~~~~~
+
+
+.. automodule:: can.io.generic
+ :show-inheritance:
+ :members:
+ :private-members:
+ :member-order: bysource
+
+
+
+Other Utilities
+---------------
+
+
+.. automodule:: can.util
+ :members:
diff --git a/doc/message.rst b/doc/message.rst
index cd350d9f1..e0003cfe5 100644
--- a/doc/message.rst
+++ b/doc/message.rst
@@ -15,7 +15,7 @@ Message
>>> test.dlc
5
>>> print(test)
- Timestamp: 0.000000 ID: 00000000 010 DLC: 5 01 02 03 04 05
+ Timestamp: 0.000000 ID: 00000000 X Rx DL: 5 01 02 03 04 05
The :attr:`~can.Message.arbitration_id` field in a CAN message may be either
@@ -23,6 +23,15 @@ Message
2.0B) in length, and ``python-can`` exposes this difference with the
:attr:`~can.Message.is_extended_id` attribute.
+ .. attribute:: timestamp
+
+ :type: float
+
+ The timestamp field in a CAN message is a floating point number representing when
+ the message was received since the epoch in seconds. Where possible this will be
+ timestamped in hardware.
+
+
.. attribute:: arbitration_id
:type: int
@@ -30,12 +39,12 @@ Message
The frame identifier used for arbitration on the bus.
The arbitration ID can take an int between 0 and the
- maximum value allowed depending on the is_extended_id flag
+ maximum value allowed depending on the ``is_extended_id`` flag
(either 2\ :sup:`11` - 1 for 11-bit IDs, or
2\ :sup:`29` - 1 for 29-bit identifiers).
- >>> print(Message(extended_id=False, arbitration_id=100))
- Timestamp: 0.000000 ID: 0064 S DLC: 0
+ >>> print(Message(is_extended_id=False, arbitration_id=100))
+ Timestamp: 0.000000 ID: 064 S Rx DL: 0
.. attribute:: data
@@ -47,7 +56,7 @@ Message
>>> example_data = bytearray([1, 2, 3])
>>> print(Message(data=example_data))
- Timestamp: 0.000000 ID: 00000000 X DLC: 3 01 02 03
+ Timestamp: 0.000000 ID: 00000000 X Rx DL: 3 01 02 03
A :class:`~can.Message` can also be created with bytes, or lists of ints:
@@ -63,7 +72,7 @@ Message
:type: int
- The :abbr:`DLC (Data Link Count)` parameter of a CAN message is an integer
+ The :abbr:`DLC (Data Length Code)` parameter of a CAN message is an integer
between 0 and 8 representing the frame payload length.
In the case of a CAN FD message, this indicates the data length in
@@ -82,20 +91,30 @@ Message
represents the amount of data contained in the message, in remote
frames it represents the amount of data being requested.
+ .. attribute:: channel
+
+ :type: str or int or None
+
+ This might store the channel from which the message came.
+
.. attribute:: is_extended_id
:type: bool
This flag controls the size of the :attr:`~can.Message.arbitration_id` field.
+ Previously this was exposed as `id_type`.
- >>> print(Message(extended_id=False))
- Timestamp: 0.000000 ID: 0000 S DLC: 0
- >>> print(Message(extended_id=True))
- Timestamp: 0.000000 ID: 00000000 X DLC: 0
+ >>> print(Message(is_extended_id=False))
+ Timestamp: 0.000000 ID: 000 S Rx DL: 0
+ >>> print(Message(is_extended_id=True))
+ Timestamp: 0.000000 ID: 00000000 X Rx DL: 0
- Previously this was exposed as `id_type`.
+ .. note::
+
+ The initializer argument and attribute ``extended_id`` has been deprecated in favor of
+ ``is_extended_id``, but will continue to work for the ``3.x`` release series.
.. attribute:: is_error_frame
@@ -105,18 +124,18 @@ Message
This boolean parameter indicates if the message is an error frame or not.
>>> print(Message(is_error_frame=True))
- Timestamp: 0.000000 ID: 00000000 X E DLC: 0
+ Timestamp: 0.000000 ID: 00000000 X Rx E DL: 0
.. attribute:: is_remote_frame
- :type: boolean
+ :type: bool
This boolean attribute indicates if the message is a remote frame or a data frame, and
modifies the bit in the CAN message's flags field indicating this.
>>> print(Message(is_remote_frame=True))
- Timestamp: 0.000000 ID: 00000000 X R DLC: 0
+ Timestamp: 0.000000 ID: 00000000 X Rx R DL: 0
.. attribute:: is_fd
@@ -126,6 +145,13 @@ Message
Indicates that this message is a CAN FD message.
+ .. attribute:: is_rx
+
+ :type: bool
+
+ Indicates whether this message is a transmitted (Tx) or received (Rx) frame
+
+
.. attribute:: bitrate_switch
:type: bool
@@ -141,15 +167,6 @@ Message
If this is a CAN FD message, this indicates an error active state.
- .. attribute:: timestamp
-
- :type: float
-
- The timestamp field in a CAN message is a floating point number representing when
- the message was received since the epoch in seconds. Where possible this will be
- timestamped in hardware.
-
-
.. method:: __str__
A string representation of a CAN message:
@@ -157,17 +174,17 @@ Message
>>> from can import Message
>>> test = Message()
>>> print(test)
- Timestamp: 0.000000 ID: 00000000 X DLC: 0
+ Timestamp: 0.000000 ID: 00000000 X Rx DL: 0
>>> test2 = Message(data=[1, 2, 3, 4, 5])
>>> print(test2)
- Timestamp: 0.000000 ID: 00000000 X DLC: 5 01 02 03 04 05
+ Timestamp: 0.000000 ID: 00000000 X Rx DL: 5 01 02 03 04 05
The fields in the printed message are (in order):
- timestamp,
- arbitration ID,
- flags,
- - dlc,
+ - data length (DL),
- and data.
@@ -183,3 +200,5 @@ Message
Each of the bytes in the data field (when present) are represented as
two-digit hexadecimal numbers.
+
+ .. automethod:: equals
diff --git a/doc/notifier.rst b/doc/notifier.rst
new file mode 100644
index 000000000..e1b160a6e
--- /dev/null
+++ b/doc/notifier.rst
@@ -0,0 +1,87 @@
+Notifier and Listeners
+======================
+
+.. _notifier:
+
+Notifier
+--------
+
+The Notifier object is used as a message distributor for a bus. The Notifier
+uses an event loop or creates a thread to read messages from the bus and
+distributes them to listeners.
+
+.. autoclass:: can.Notifier
+ :members:
+
+.. _listeners_doc:
+
+Listener
+--------
+
+The Listener class is an "abstract" base class for any objects which wish to
+register to receive notifications of new messages on the bus. A Listener can
+be used in two ways; the default is to **call** the Listener with a new
+message, or by calling the method **on_message_received**.
+
+Listeners are registered with :ref:`notifier` object(s) which ensure they are
+notified whenever a new message is received.
+
+.. literalinclude:: ../examples/print_notifier.py
+ :language: python
+ :linenos:
+ :emphasize-lines: 8,9
+
+
+Subclasses of Listener that do not override **on_message_received** will cause
+:class:`NotImplementedError` to be thrown when a message is received on
+the CAN bus.
+
+.. autoclass:: can.Listener
+ :members:
+
+There are some listeners that already ship together with `python-can`
+and are listed below.
+Some of them allow messages to be written to files, and the corresponding file
+readers are also documented here.
+
+.. note ::
+
+ Please note that writing and the reading a message might not always yield a
+ completely unchanged message again, since some properties are not (yet)
+ supported by some file formats.
+
+.. note ::
+
+ Additional file formats for both reading/writing log files can be added via
+ a plugin reader/writer. An external package can register a new reader
+ by using the ``can.io.message_reader`` entry point. Similarly, a writer can
+ be added using the ``can.io.message_writer`` entry point.
+
+ The format of the entry point is ``reader_name=module:classname`` where ``classname``
+ is a concrete implementation of :class:`~can.io.generic.MessageReader` or
+ :class:`~can.io.generic.MessageWriter`.
+
+ ::
+
+ entry_points={
+ 'can.io.message_reader': [
+ '.asc = my_package.io.asc:ASCReader'
+ ]
+ },
+
+
+BufferedReader
+--------------
+
+.. autoclass:: can.BufferedReader
+ :members:
+
+.. autoclass:: can.AsyncBufferedReader
+ :members:
+
+
+RedirectReader
+--------------
+
+.. autoclass:: can.RedirectReader
+ :members:
diff --git a/doc/other-tools.rst b/doc/other-tools.rst
new file mode 100644
index 000000000..607db6c8a
--- /dev/null
+++ b/doc/other-tools.rst
@@ -0,0 +1,79 @@
+Other CAN Bus Tools
+===================
+
+In order to keep the project maintainable, the scope of the package is limited to providing common
+abstractions to different hardware devices, and a basic suite of utilities for sending and
+receiving messages on a CAN bus. Other tools are available that either extend the functionality
+of python-can, or provide complementary features that python-can users might find useful.
+
+Some of these tools are listed below for convenience.
+
+CAN Message protocols (implemented in Python)
+---------------------------------------------
+
+#. SAE J1939 Message Protocol
+ * The `can-j1939`_ module provides an implementation of the CAN SAE J1939 standard for Python,
+ including J1939-22. `can-j1939`_ uses python-can to provide support for multiple hardware
+ interfaces.
+#. CIA CANopen
+ * The `canopen`_ module provides an implementation of the CIA CANopen protocol, aiming to be
+ used for automation and testing purposes
+#. ISO 15765-2 (ISO TP)
+ * The `can-isotp`_ module provides an implementation of the ISO TP CAN protocol for sending
+ data packets via a CAN transport layer.
+
+#. UDS
+ * The `python-uds`_ module is a communication protocol agnostic implementation of the Unified
+ Diagnostic Services (UDS) protocol defined in ISO 14229-1, although it does have extensions
+ for performing UDS over CAN utilising the ISO TP protocol. This module has not been updated
+ for some time.
+ * The `uds`_ module is another tool that implements the UDS protocol, although it does have
+ extensions for performing UDS over CAN utilising the ISO TP protocol. This module has not
+ been updated for some time.
+#. XCP
+ * The `pyxcp`_ module implements the Universal Measurement and Calibration Protocol (XCP).
+ The purpose of XCP is to adjust parameters and acquire current values of internal
+ variables in an ECU.
+
+.. _can-j1939: https://github.com/juergenH87/python-can-j1939
+.. _canopen: https://canopen.readthedocs.io/en/latest/
+.. _can-isotp: https://can-isotp.readthedocs.io/en/latest/
+.. _python-uds: https://python-uds.readthedocs.io/en/latest/index.html
+.. _uds: https://uds.readthedocs.io/en/latest/
+.. _pyxcp: https://pyxcp.readthedocs.io/en/latest/
+
+CAN Frame Parsing tools etc. (implemented in Python)
+----------------------------------------------------
+
+#. CAN Message / Database scripting
+ * The `cantools`_ package provides multiple methods for interacting with can message database
+ files, and using these files to monitor live buses with a command line monitor tool.
+#. CAN Message / Log Decoding
+ * The `canmatrix`_ module provides methods for converting between multiple popular message
+ frame definition file formats (e.g. .DBC files, .KCD files, .ARXML files etc.).
+ * The `pretty_j1939`_ module can be used to post-process CAN logs of J1939 traffic into human
+ readable terminal prints or into a JSON file for consumption elsewhere in your scripts.
+
+.. _cantools: https://cantools.readthedocs.io/en/latest/
+.. _canmatrix: https://canmatrix.readthedocs.io/en/latest/
+.. _pretty_j1939: https://github.com/nmfta-repo/pretty_j1939
+
+Other CAN related tools, programs etc.
+--------------------------------------
+
+#. Micropython CAN class
+ * A `CAN class`_ is available for the original micropython pyboard, with much of the same
+ functionality as is available with python-can (but with a different API!).
+#. ASAM MDF Files
+ * The `asammdf`_ module provides many methods for processing ASAM (Association for
+ Standardization of Automation and Measuring Systems) MDF (Measurement Data Format) files.
+
+.. _`CAN class`: https://docs.micropython.org/en/latest/library/pyb.CAN.html
+.. _`asammdf`: https://asammdf.readthedocs.io/en/master/
+
+|
+|
+
+.. note::
+ See also the available plugins for python-can in :ref:`plugin interface`.
+
diff --git a/doc/plugin-interface.rst b/doc/plugin-interface.rst
new file mode 100644
index 000000000..612148033
--- /dev/null
+++ b/doc/plugin-interface.rst
@@ -0,0 +1,99 @@
+
+.. _plugin interface:
+
+Plugin Interface
+================
+
+External packages can register new interfaces by using the ``can.interface`` entry point
+in its project configuration. The format of the entry point depends on your project
+configuration format (*pyproject.toml*, *setup.cfg* or *setup.py*).
+
+In the following example ``module`` defines the location of your bus class inside your
+package e.g. ``my_package.subpackage.bus_module`` and ``classname`` is the name of
+your :class:`can.BusABC` subclass.
+
+.. tab:: pyproject.toml (PEP 621)
+
+ .. code-block:: toml
+
+ # Note the quotes around can.interface in order to escape the dot .
+ [project.entry-points."can.interface"]
+ interface_name = "module:classname"
+
+.. tab:: setup.cfg
+
+ .. code-block:: ini
+
+ [options.entry_points]
+ can.interface =
+ interface_name = module:classname
+
+.. tab:: setup.py
+
+ .. code-block:: python
+
+ from setuptools import setup
+
+ setup(
+ # ...,
+ entry_points = {
+ 'can.interface': [
+ 'interface_name = module:classname'
+ ]
+ }
+ )
+
+The ``interface_name`` can be used to
+create an instance of the bus in the **python-can** API:
+
+.. code-block:: python
+
+ import can
+
+ bus = can.Bus(interface="interface_name", channel=0)
+
+
+
+Example Interface Plugins
+-------------------------
+
+The table below lists interface drivers that can be added by installing additional packages that utilise the plugin API. These modules are optional dependencies of python-can.
+
+.. note::
+ The packages listed below are maintained by other authors. Any issues should be reported in their corresponding repository and **not** in the python-can repository.
+
++----------------------------+----------------------------------------------------------+
+| Name | Description |
++============================+==========================================================+
+| `python-can-canine`_ | CAN Driver for the CANine CAN interface |
++----------------------------+----------------------------------------------------------+
+| `python-can-cvector`_ | Cython based version of the 'VectorBus' |
++----------------------------+----------------------------------------------------------+
+| `python-can-remote`_ | CAN over network bridge |
++----------------------------+----------------------------------------------------------+
+| `python-can-sontheim`_ | CAN Driver for Sontheim CAN interfaces (e.g. CANfox) |
++----------------------------+----------------------------------------------------------+
+| `zlgcan`_ | Python wrapper for zlgcan-driver-rs |
++----------------------------+----------------------------------------------------------+
+| `python-can-cando`_ | Python wrapper for Netronics' CANdo and CANdoISO |
++----------------------------+----------------------------------------------------------+
+| `python-can-candle`_ | A full-featured driver for candleLight |
++----------------------------+----------------------------------------------------------+
+| `python-can-coe`_ | A CAN-over-Ethernet interface for Technische Alternative |
++----------------------------+----------------------------------------------------------+
+| `RP1210`_ | CAN channels in RP1210 Vehicle Diagnostic Adapters |
++----------------------------+----------------------------------------------------------+
+| `python-can-damiao`_ | Interface for Damiao USB-CAN adapters |
++----------------------------+----------------------------------------------------------+
+
+.. _python-can-canine: https://github.com/tinymovr/python-can-canine
+.. _python-can-cvector: https://github.com/zariiii9003/python-can-cvector
+.. _python-can-remote: https://github.com/christiansandberg/python-can-remote
+.. _python-can-sontheim: https://github.com/MattWoodhead/python-can-sontheim
+.. _zlgcan: https://github.com/jesses2025smith/zlgcan-driver
+.. _python-can-cando: https://github.com/belliriccardo/python-can-cando
+.. _python-can-candle: https://github.com/BIRLab/python-can-candle
+.. _python-can-coe: https://c0d3.sh/smarthome/python-can-coe
+.. _RP1210: https://github.com/dfieschko/RP1210
+.. _python-can-damiao: https://github.com/gaoyichuan/python-can-damiao
+
diff --git a/doc/pycanlib.pml b/doc/pycanlib.pml
index 0ddcf25e5..907fadabb 100644
--- a/doc/pycanlib.pml
+++ b/doc/pycanlib.pml
@@ -1,4 +1,4 @@
-/* This promela model was used to verify the concurrent design of the bus object. */
+/* This promela model was used to verify a past design of the bus object. */
bool lock = false;
diff --git a/doc/scripts.rst b/doc/scripts.rst
index b58125ea5..1d730a74b 100644
--- a/doc/scripts.rst
+++ b/doc/scripts.rst
@@ -1,176 +1,73 @@
-Scripts
-=======
-
-The following modules are callable from python-can.
-
-They can be called for example by ``python -m can.logger`` or ``can_logger.py`` (if installed using pip).
-
-can.logger
-----------
-
-Command line help, called with ``--help``::
-
- usage: python -m can.logger [-h] [-f LOG_FILE] [-v] [-c CHANNEL]
- [-i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}]
- [--filter ...] [-b BITRATE]
- [--active | --passive]
-
- Log CAN traffic, printing messages to stdout or to a given file.
-
- optional arguments:
- -h, --help show this help message and exit
- -f LOG_FILE, --file_name LOG_FILE
- Path and base log filename, for supported types see
- can.Logger.
- -v How much information do you want to see at the command
- line? You can add several of these e.g., -vv is DEBUG
- -c CHANNEL, --channel CHANNEL
- Most backend interfaces require some sort of channel.
- For example with the serial interface the channel
- might be a rfcomm device: "/dev/rfcomm0" With the
- socketcan interfaces valid channel examples include:
- "can0", "vcan0"
- -i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}, --interface {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}
- Specify the backend CAN interface to use. If left
- blank, fall back to reading from configuration files.
- --filter ... Comma separated filters can be specified for the given
- CAN interface: : (matches when
- & mask == can_id & mask)
- ~ (matches when &
- mask != can_id & mask)
- -b BITRATE, --bitrate BITRATE
- Bitrate to use for the CAN bus.
- --active Start the bus as active, this is applied the default.
- --passive Start the bus as passive.
-
-
-can.player
-----------
-
-Command line help, called with ``--help``::
-
- usage: python -m can.player [-h] [-f LOG_FILE] [-v] [-c CHANNEL]
- [-i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}]
- [-b BITRATE] [--ignore-timestamps]
- [-g GAP] [-s SKIP]
- input-file
-
- Replay CAN traffic.
-
- positional arguments:
- input-file The file to replay. For supported types see
- can.LogReader.
-
- optional arguments:
- -h, --help show this help message and exit
- -f LOG_FILE, --file_name LOG_FILE
- Path and base log filename, for supported types see
- can.LogReader.
- -v Also print can frames to stdout. You can add several
- of these to enable debugging
- -c CHANNEL, --channel CHANNEL
- Most backend interfaces require some sort of channel.
- For example with the serial interface the channel
- might be a rfcomm device: "/dev/rfcomm0" With the
- socketcan interfaces valid channel examples include:
- "can0", "vcan0"
- -i {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}, --interface {pcan,ixxat,socketcan_ctypes,kvaser,virtual,usb2can,vector,slcan,nican,socketcan,iscan,neovi,serial,socketcan_native}
- Specify the backend CAN interface to use. If left
- blank, fall back to reading from configuration files.
- -b BITRATE, --bitrate BITRATE
- Bitrate to use for the CAN bus.
- --ignore-timestamps Ignore timestamps (send all frames immediately with
- minimum gap between frames)
- -g GAP, --gap GAP minimum time between replayed frames
- -s SKIP, --skip SKIP skip gaps greater than 's' seconds
-
-can.viewer
-----------
-
-A screenshot of the application can be seen below:
-
-.. image:: ../images/viewer.png
- :width: 100%
-
-The first column is the number of times a frame with the particular ID that has been received, next is the timestamp of the frame relative to the first received message. The third column is the time between the current frame relative to the previous one. Next is the length of the frame, the data and then the decoded data converted according to the ``-d`` argument. The top red row indicates an error frame.
-
-Command line arguments
-^^^^^^^^^^^^^^^^^^^^^^
-
-By default it will be using the :doc:`/interfaces/socketcan` interface. All interfaces supported are supported and can be specified using the ``-i`` argument.
-
-The full usage page can be seen below::
-
- Usage: python -m can.viewer [-h] [--version] [-b BITRATE] [-c CHANNEL]
- [-d {:,:::...:,file.txt}]
- [-f {:,~}]
- [-i {iscan,ixxat,kvaser,neovi,nican,pcan,serial,slcan,socketcan,socketcan_ctypes,socketcan_native,usb2can,vector,virtual}]
-
- A simple CAN viewer terminal application written in Python
-
- Optional arguments:
- -h, --help Show this help message and exit
- --version Show program's version number and exit
- -b, --bitrate BITRATE
- Bitrate to use for the given CAN interface
- -c, --channel CHANNEL
- Most backend interfaces require some sort of channel.
- For example with the serial interface the channel
- might be a rfcomm device: "/dev/rfcomm0" with the
- socketcan interfaces valid channel examples include:
- "can0", "vcan0". (default: use default for the
- specified interface)
- -d, --decode {:,:::...:,file.txt}
- Specify how to convert the raw bytes into real values.
- The ID of the frame is given as the first argument and the format as the second.
- The Python struct package is used to unpack the received data
- where the format characters have the following meaning:
- < = little-endian, > = big-endian
- x = pad byte
- c = char
- ? = bool
- b = int8_t, B = uint8_t
- h = int16, H = uint16
- l = int32_t, L = uint32_t
- q = int64_t, Q = uint64_t
- f = float (32-bits), d = double (64-bits)
- Fx to convert six bytes with ID 0x100 into uint8_t, uint16 and uint32_t:
- $ python -m can.viewer -d "100::,~}
- Comma separated CAN filters for the given CAN interface:
- : (matches when & mask == can_id & mask)
- ~ (matches when & mask != can_id & mask)
- Fx to show only frames with ID 0x100 to 0x103:
- python -m can.viewer -f 100:7FC
- Note that the ID and mask are alway interpreted as hex values
- -i, --interface {iscan,ixxat,kvaser,neovi,nican,pcan,serial,slcan,socketcan,socketcan_ctypes,socketcan_native,usb2can,vector,virtual}
- Specify the backend CAN interface to use.
-
- Shortcuts:
- +---------+-------------------------+
- | Key | Description |
- +---------+-------------------------+
- | ESQ/q | Exit the viewer |
- | c | Clear the stored frames |
- | s | Sort the stored frames |
- | SPACE | Pause the viewer |
- | UP/DOWN | Scroll the viewer |
- +---------+-------------------------+
+Command Line Tools
+==================
+
+The following modules are callable from ``python-can``.
+
+They can be called for example by ``python -m can.logger`` or ``can_logger`` (if installed using pip).
+
+can.logger
+----------
+
+Command line help, called with ``--help``:
+
+
+.. command-output:: python -m can.logger -h
+ :shell:
+
+
+can.player
+----------
+
+.. command-output:: python -m can.player -h
+ :shell:
+
+
+can.viewer
+----------
+
+A screenshot of the application can be seen below:
+
+.. image:: images/viewer.png
+ :width: 100%
+
+The first column is the number of times a frame with the particular ID that has been received, next is the timestamp of the frame relative to the first received message. The third column is the time between the current frame relative to the previous one. Next is the length of the frame, the data and then the decoded data converted according to the ``-d`` argument. The top red row indicates an error frame.
+There are several keyboard shortcuts that can be used with the viewer script, they function as follows:
+
+* ESCAPE - Quit the viewer script
+* q - as ESCAPE
+* c - Clear the stored frames
+* s - Sort the stored frames
+* h - Toggle highlighting of changed bytes in the data field - see the below image
+* SPACE - Pause the viewer
+* UP/DOWN - Scroll the viewer
+
+.. image:: images/viewer_changed_bytes_highlighting.png
+ :width: 50%
+
+A byte in the data field is highlighted blue if the value is different from the last time the message was received.
+
+Command line arguments
+^^^^^^^^^^^^^^^^^^^^^^
+
+By default the ``can.viewer`` uses the :doc:`/interfaces/socketcan` interface. All interfaces are supported and can be specified using the ``-i`` argument or configured following :doc:`/configuration`.
+
+The full usage page can be seen below:
+
+.. command-output:: python -m can.viewer -h
+ :shell:
+
+
+can.bridge
+----------
+
+A small application that can be used to connect two can buses:
+
+.. command-output:: python -m can.bridge -h
+ :shell:
+
+
+can.logconvert
+--------------
+
+.. command-output:: python -m can.logconvert -h
+ :shell:
diff --git a/doc/utils.rst b/doc/utils.rst
new file mode 100644
index 000000000..9c742e2fb
--- /dev/null
+++ b/doc/utils.rst
@@ -0,0 +1,10 @@
+Utilities
+---------
+
+
+.. autofunction:: can.detect_available_configs
+
+.. autofunction:: can.cli.add_bus_arguments
+
+.. autofunction:: can.cli.create_bus_from_namespace
+
diff --git a/doc/virtual-interfaces.rst b/doc/virtual-interfaces.rst
new file mode 100644
index 000000000..70ac601fa
--- /dev/null
+++ b/doc/virtual-interfaces.rst
@@ -0,0 +1,77 @@
+
+.. _virtual_interfaces_doc:
+
+Virtual Interfaces
+==================
+
+There are quite a few implementations for CAN networks that do not require physical
+CAN hardware. The built in virtual interfaces are:
+
+.. toctree::
+ :maxdepth: 1
+
+ interfaces/virtual
+ interfaces/udp_multicast
+
+
+Comparison
+----------
+
+The following table compares some known virtual interfaces:
+
++----------------------------------------------------+-----------------------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+
+| **Name** | **Availability** | **Applicability** | **Implementation** |
+| | +-----------+-------------+-------------+--------------------+---------------------------------------------+---------------------------------------------------------------------+
+| | | **Within | **Between | **Via (IP) | **Without Central | **Transport | **Serialization |
+| | | Process** | Processes** | Networks** | Server** | Technology** | Format** |
++----------------------------------------------------+-----------------------------------------------------------------------+-----------+-------------+-------------+--------------------+---------------------------------------------+---------------------------------------------------------------------+
+| ``virtual`` (this) | *included* | ✓ | ✗ | ✗ | ✓ | Singleton & Mutex | none |
+| | | | | | | (reliable) | |
++----------------------------------------------------+-----------------------------------------------------------------------+-----------+-------------+-------------+--------------------+---------------------------------------------+---------------------------------------------------------------------+
+| ``udp_multicast`` (:ref:`doc `) | *included* | ✓ | ✓ | ✓ | ✓ | UDP via IP multicast | custom using `msgpack `__ |
+| | | | | | | (unreliable) | |
++----------------------------------------------------+-----------------------------------------------------------------------+-----------+-------------+-------------+--------------------+---------------------------------------------+---------------------------------------------------------------------+
+| *christiansandberg/ | `external `__ | ✓ | ✓ | ✓ | ✗ | Websockets via TCP/IP | custom binary |
+| python-can-remote* | | | | | | (reliable) | |
++----------------------------------------------------+-----------------------------------------------------------------------+-----------+-------------+-------------+--------------------+---------------------------------------------+---------------------------------------------------------------------+
+| *windelbouwman/ | `external `__ | ✓ | ✓ | ✓ | ✗ | `ZeroMQ `__ via TCP/IP | custom binary [#f1]_ |
+| virtualcan* | | | | | | (reliable) | |
++----------------------------------------------------+-----------------------------------------------------------------------+-----------+-------------+-------------+--------------------+---------------------------------------------+---------------------------------------------------------------------+
+
+.. [#f1]
+ The only option in this list that implements interoperability with other languages
+ out of the box. For the others (except the first intra-process one), other programs written
+ in potentially different languages could effortlessly interface with the bus
+ once they mimic the serialization format. The last one, however, has already implemented
+ the entire bus functionality in *C++* and *Rust*, besides the Python variant.
+
+Common Limitations
+------------------
+
+**Guaranteed delivery** and **message ordering** is one major point of difference:
+While in a physical CAN network, a message is either sent or in queue (or an explicit error occurred),
+this may not be the case for virtual networks.
+The ``udp_multicast`` bus for example, drops this property for the benefit of lower
+latencies by using unreliable UDP/IP instead of reliable TCP/IP (and because normal IP multicast
+is inherently unreliable, as the recipients are unknown by design). The other three buses faithfully
+model a physical CAN network in this regard: They ensure that all recipients actually receive
+(and acknowledge each message), much like in a physical CAN network. They also ensure that
+messages are relayed in the order they have arrived at the central server and that messages
+arrive at the recipients exactly once. Both is not guaranteed to hold for the best-effort
+``udp_multicast`` bus as it uses UDP/IP as a transport layer.
+
+**Central servers** are, however, required by interfaces 3 and 4 (the external tools) to provide
+these guarantees of message delivery and message ordering. The central servers receive and distribute
+the CAN messages to all other bus participants, unlike in a real physical CAN network.
+The first intra-process ``virtual`` interface only runs within one Python process, effectively the
+Python instance of :class:`~can.interfaces.virtual.VirtualBus` acts as a central server.
+Notably the ``udp_multicast`` bus does not require a central server.
+
+**Arbitration and throughput** are two interrelated functions/properties of CAN networks which
+are typically abstracted in virtual interfaces. In all four interfaces, an unlimited amount
+of messages can be sent per unit of time (given the computational power of the machines and
+networks that are involved). In a real CAN/CAN FD networks, however, throughput is usually much
+more restricted and prioritization of arbitration IDs is thus an important feature once the bus
+is starting to get saturated. None of the interfaces presented above support any sort of throttling
+or ID arbitration under high loads.
+
diff --git a/examples/asyncio_demo.py b/examples/asyncio_demo.py
old mode 100644
new mode 100755
index 3e71ae6db..6befbe7a9
--- a/examples/asyncio_demo.py
+++ b/examples/asyncio_demo.py
@@ -1,44 +1,55 @@
+#!/usr/bin/env python
+
+"""
+This example demonstrates how to use async IO with python-can.
+"""
+
import asyncio
+from typing import TYPE_CHECKING
+
import can
-def print_message(msg):
+if TYPE_CHECKING:
+ from can.notifier import MessageRecipient
+
+
+def print_message(msg: can.Message) -> None:
"""Regular callback function. Can also be a coroutine."""
print(msg)
-async def main():
- can0 = can.Bus('vcan0', bustype='virtual', receive_own_messages=True)
- reader = can.AsyncBufferedReader()
- logger = can.Logger('logfile.asc')
-
- listeners = [
- print_message, # Callback function
- reader, # AsyncBufferedReader() listener
- logger # Regular Listener object
- ]
- # Create Notifier with an explicit loop to use for scheduling of callbacks
- loop = asyncio.get_event_loop()
- notifier = can.Notifier(can0, listeners, loop=loop)
- # Start sending first message
- can0.send(can.Message(arbitration_id=0))
-
- print('Bouncing 10 messages...')
- for _ in range(10):
- # Wait for next message from AsyncBufferedReader
- msg = await reader.get_message()
- # Delay response
- await asyncio.sleep(0.5)
- msg.arbitration_id += 1
- can0.send(msg)
- # Wait for last message to arrive
- await reader.get_message()
- print('Done!')
-
- # Clean-up
- notifier.stop()
- can0.shutdown()
-
-# Get the default event loop
-loop = asyncio.get_event_loop()
-# Run until main coroutine finishes
-loop.run_until_complete(main())
-loop.close()
+
+async def main() -> None:
+ """The main function that runs in the loop."""
+
+ with can.Bus(
+ interface="virtual", channel="my_channel_0", receive_own_messages=True
+ ) as bus:
+ reader = can.AsyncBufferedReader()
+ logger = can.Logger("logfile.asc")
+
+ listeners: list[MessageRecipient] = [
+ print_message, # Callback function
+ reader, # AsyncBufferedReader() listener
+ logger, # Regular Listener object
+ ]
+ # Create Notifier with an explicit loop to use for scheduling of callbacks
+ with can.Notifier(bus, listeners, loop=asyncio.get_running_loop()):
+ # Start sending first message
+ bus.send(can.Message(arbitration_id=0))
+
+ print("Bouncing 10 messages...")
+ for _ in range(10):
+ # Wait for next message from AsyncBufferedReader
+ msg = await reader.get_message()
+ # Delay response
+ await asyncio.sleep(0.5)
+ msg.arbitration_id += 1
+ bus.send(msg)
+
+ # Wait for last message to arrive
+ await reader.get_message()
+ print("Done!")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/examples/crc.py b/examples/crc.py
new file mode 100755
index 000000000..fff3dce25
--- /dev/null
+++ b/examples/crc.py
@@ -0,0 +1,82 @@
+#!/usr/bin/env python
+
+"""
+This example exercises the periodic task's multiple message sending capabilities
+to send a message containing a counter and a checksum.
+
+Expects a vcan0 interface:
+
+ python3 -m examples.crc
+
+"""
+
+import logging
+import time
+
+import can
+
+logging.basicConfig(level=logging.INFO)
+
+
+def crc_send(bus):
+ """
+ Sends periodic messages every 1 s with no explicit timeout. Modifies messages
+ after 8 seconds, sends for 10 more seconds, then stops.
+ """
+ msg = can.Message(arbitration_id=0x12345678, data=[1, 2, 3, 4, 5, 6, 7, 0])
+ messages = build_crc_msgs(msg)
+
+ print(
+ "Starting to send a message with updating counter and checksum every 1 s for 8 s"
+ )
+ task = bus.send_periodic(messages, 1)
+ assert isinstance(task, can.CyclicSendTaskABC)
+ time.sleep(8)
+
+ msg = can.Message(arbitration_id=0x12345678, data=[8, 9, 10, 11, 12, 13, 14, 0])
+ messages = build_crc_msgs(msg)
+
+ print("Sending modified message data every 1 s for 10 s")
+ task.modify_data(messages)
+ time.sleep(10)
+ task.stop()
+ print("stopped cyclic send")
+
+
+def build_crc_msgs(msg):
+ """
+ Using the input message as base, create 16 messages with SAE J1939 SPN 3189 counters
+ and SPN 3188 checksums placed in the final byte.
+ """
+ messages = []
+
+ for counter in range(16):
+ checksum = compute_xbr_checksum(msg, counter)
+ msg.data[7] = counter + (checksum << 4)
+ messages.append(
+ can.Message(arbitration_id=msg.arbitration_id, data=msg.data[:])
+ )
+
+ return messages
+
+
+def compute_xbr_checksum(message, counter):
+ """
+ Computes an XBR checksum per SAE J1939 SPN 3188.
+ """
+ checksum = sum(message.data[:7])
+ checksum += sum(message.arbitration_id.to_bytes(length=4, byteorder="big"))
+ checksum += counter & 0x0F
+ xbr_checksum = ((checksum >> 4) + checksum) & 0x0F
+
+ return xbr_checksum
+
+
+if __name__ == "__main__":
+ for interface, channel in [("socketcan", "vcan0")]:
+ print(f"Carrying out crc test with {interface} interface")
+
+ with can.Bus(interface=interface, channel=channel, bitrate=500000) as BUS:
+ crc_send(BUS)
+
+ time.sleep(2)
diff --git a/examples/cyclic.py b/examples/cyclic.py
index f7972f55c..bdd69eef2 100755
--- a/examples/cyclic.py
+++ b/examples/cyclic.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python
-# coding: utf-8
"""
This example exercises the periodic sending capabilities.
@@ -10,8 +9,6 @@
"""
-from __future__ import print_function
-
import logging
import time
@@ -26,7 +23,9 @@ def simple_periodic_send(bus):
Sleeps for 2 seconds then stops the task.
"""
print("Starting to send a message every 200ms for 2s")
- msg = can.Message(arbitration_id=0x123, data=[1, 2, 3, 4, 5, 6], extended_id=False)
+ msg = can.Message(
+ arbitration_id=0x123, data=[1, 2, 3, 4, 5, 6], is_extended_id=False
+ )
task = bus.send_periodic(msg, 0.20)
assert isinstance(task, can.CyclicSendTaskABC)
time.sleep(2)
@@ -35,21 +34,29 @@ def simple_periodic_send(bus):
def limited_periodic_send(bus):
+ """Send using LimitedDurationCyclicSendTaskABC."""
print("Starting to send a message every 200ms for 1s")
- msg = can.Message(arbitration_id=0x12345678, data=[0, 0, 0, 0, 0, 0], extended_id=True)
- task = bus.send_periodic(msg, 0.20, 1)
+ msg = can.Message(
+ arbitration_id=0x12345678, data=[0, 0, 0, 0, 0, 0], is_extended_id=True
+ )
+ task = bus.send_periodic(msg, 0.20, 1, store_task=False)
if not isinstance(task, can.LimitedDurationCyclicSendTaskABC):
- print("This interface doesn't seem to support a ")
+ print("This interface doesn't seem to support LimitedDurationCyclicSendTaskABC")
task.stop()
return
- time.sleep(1.5)
- print("stopped cyclic send")
+ time.sleep(2)
+ print("Cyclic send should have stopped as duration expired")
+ # Note the (finished) task will still be tracked by the Bus
+ # unless we pass `store_task=False` to bus.send_periodic
+ # alternatively calling stop removes the task from the bus
+ # task.stop()
def test_periodic_send_with_modifying_data(bus):
- print("Starting to send a message every 200ms. Initial data is ones")
- msg = can.Message(arbitration_id=0x0cf02200, data=[1, 1, 1, 1])
+ """Send using ModifiableCyclicTaskABC."""
+ print("Starting to send a message every 200ms. Initial data is four consecutive 1s")
+ msg = can.Message(arbitration_id=0x0CF02200, data=[1, 1, 1, 1])
task = bus.send_periodic(msg, 0.20)
if not isinstance(task, can.ModifiableCyclicTaskABC):
print("This interface doesn't seem to support modification")
@@ -64,7 +71,7 @@ def test_periodic_send_with_modifying_data(bus):
task.stop()
print("stopped cyclic send")
print("Changing data of stopped task to single ff byte")
- msg.data = bytearray([0xff])
+ msg.data = bytearray([0xFF])
msg.dlc = 1
task.modify_data(msg)
time.sleep(1)
@@ -101,17 +108,15 @@ def test_periodic_send_with_modifying_data(bus):
# print("done")
-if __name__ == "__main__":
-
- reset_msg = can.Message(arbitration_id=0x00, data=[0, 0, 0, 0, 0, 0], extended_id=False)
-
- for interface, channel in [
- ('socketcan', 'can0'),
- #('ixxat', 0)
- ]:
- print("Carrying out cyclic tests with {} interface".format(interface))
+def main():
+ """Test different cyclic sending tasks."""
+ reset_msg = can.Message(
+ arbitration_id=0x00, data=[0, 0, 0, 0, 0, 0], is_extended_id=False
+ )
- bus = can.Bus(interface=interface, channel=channel, bitrate=500000)
+ # this uses the default configuration (for example from environment variables, or a
+ # config file) see https://python-can.readthedocs.io/en/stable/configuration.html
+ with can.Bus() as bus:
bus.send(reset_msg)
simple_periodic_send(bus)
@@ -122,10 +127,12 @@ def test_periodic_send_with_modifying_data(bus):
test_periodic_send_with_modifying_data(bus)
- #print("Carrying out multirate cyclic test for {} interface".format(interface))
- #can.rc['interface'] = interface
- #test_dual_rate_periodic_send()
-
- bus.shutdown()
+ # print("Carrying out multirate cyclic test for {} interface".format(interface))
+ # can.rc['interface'] = interface
+ # test_dual_rate_periodic_send()
time.sleep(2)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/cyclic_checksum.py b/examples/cyclic_checksum.py
new file mode 100644
index 000000000..763fcd72b
--- /dev/null
+++ b/examples/cyclic_checksum.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+
+"""
+This example demonstrates how to send a periodic message containing
+an automatically updating counter and checksum.
+
+Expects a virtual interface:
+
+ python3 -m examples.cyclic_checksum
+"""
+
+import logging
+import time
+
+import can
+
+logging.basicConfig(level=logging.INFO)
+
+
+def cyclic_checksum_send(bus: can.BusABC) -> None:
+ """
+ Sends periodic messages every 1 s with no explicit timeout.
+ The message's counter and checksum is updated before each send.
+ Sleeps for 10 seconds then stops the task.
+ """
+ message = can.Message(arbitration_id=0x78, data=[0, 1, 2, 3, 4, 5, 6, 0])
+ print("Starting to send an auto-updating message every 100ms for 3 s")
+ task = bus.send_periodic(msgs=message, period=0.1, modifier_callback=update_message)
+ time.sleep(3)
+ task.stop()
+ print("stopped cyclic send")
+
+
+def update_message(message: can.Message) -> None:
+ counter = increment_counter(message)
+ checksum = compute_xbr_checksum(message, counter)
+ message.data[7] = (checksum << 4) + counter
+
+
+def increment_counter(message: can.Message) -> int:
+ counter = message.data[7] & 0x0F
+ counter += 1
+ counter %= 16
+
+ return counter
+
+
+def compute_xbr_checksum(message: can.Message, counter: int) -> int:
+ """
+ Computes an XBR checksum as per SAE J1939 SPN 3188.
+ """
+ checksum = sum(message.data[:7])
+ checksum += sum(message.arbitration_id.to_bytes(length=4, byteorder="big"))
+ checksum += counter & 0x0F
+ xbr_checksum = ((checksum >> 4) + checksum) & 0x0F
+
+ return xbr_checksum
+
+
+if __name__ == "__main__":
+ with can.Bus(channel=0, interface="virtual", receive_own_messages=True) as _bus:
+ with can.Notifier(bus=_bus, listeners=[print]):
+ cyclic_checksum_send(_bus)
diff --git a/examples/cyclic_multiple.py b/examples/cyclic_multiple.py
new file mode 100755
index 000000000..43dc0cd17
--- /dev/null
+++ b/examples/cyclic_multiple.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python
+
+"""
+This example exercises the periodic task's multiple message sending capabilities
+
+Expects a vcan0 interface:
+
+ python3 -m examples.cyclic_multiple
+
+"""
+
+import logging
+import time
+
+import can
+
+logging.basicConfig(level=logging.INFO)
+
+
+def cyclic_multiple_send(bus):
+ """
+ Sends periodic messages every 1 s with no explicit timeout
+ Sleeps for 10 seconds then stops the task.
+ """
+ print("Starting to send a message every 1 s for 10 s")
+ messages = []
+
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+ )
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22],
+ is_extended_id=False,
+ )
+ )
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33],
+ is_extended_id=False,
+ )
+ )
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44],
+ is_extended_id=False,
+ )
+ )
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55],
+ is_extended_id=False,
+ )
+ )
+ task = bus.send_periodic(messages, 1)
+ assert isinstance(task, can.CyclicSendTaskABC)
+ time.sleep(10)
+ task.stop()
+ print("stopped cyclic send")
+
+
+def cyclic_multiple_send_modify(bus):
+ """
+ Sends initial set of 3 Messages containing Odd data sent every 1 s with
+ no explicit timeout. Sleeps for 8 s.
+
+ Then the set is updated to 3 Messages containing Even data.
+ Sleeps for 10 s.
+ """
+ messages_odd = []
+ messages_odd.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+ )
+ messages_odd.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33],
+ is_extended_id=False,
+ )
+ )
+ messages_odd.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55],
+ is_extended_id=False,
+ )
+ )
+ messages_even = []
+ messages_even.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22],
+ is_extended_id=False,
+ )
+ )
+ messages_even.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44],
+ is_extended_id=False,
+ )
+ )
+ messages_even.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x66, 0x66, 0x66, 0x66, 0x66, 0x66],
+ is_extended_id=False,
+ )
+ )
+ print("Starting to send a message with odd every 1 s for 8 s with odd data")
+ task = bus.send_periodic(messages_odd, 1)
+ assert isinstance(task, can.CyclicSendTaskABC)
+ time.sleep(8)
+ print("Starting to send a message with even data every 1 s for 10 s with even data")
+ task.modify_data(messages_even)
+ time.sleep(10)
+ print("stopped cyclic modify send")
+
+
+if __name__ == "__main__":
+ for interface, channel in [("socketcan", "vcan0")]:
+ print(f"Carrying out cyclic multiple tests with {interface} interface")
+
+ with can.Bus(interface=interface, channel=channel, bitrate=500000) as BUS:
+ cyclic_multiple_send(BUS)
+ cyclic_multiple_send_modify(BUS)
+
+ time.sleep(2)
diff --git a/examples/print_notifier.py b/examples/print_notifier.py
new file mode 100755
index 000000000..e6e11dbec
--- /dev/null
+++ b/examples/print_notifier.py
@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+
+import time
+
+import can
+
+
+def main():
+ with can.Bus(interface="virtual", receive_own_messages=True) as bus:
+ print_listener = can.Printer()
+ with can.Notifier(bus, listeners=[print_listener]):
+ # using Notifier as a context manager automatically calls `Notifier.stop()`
+ # at the end of the `with` block
+ bus.send(can.Message(arbitration_id=1, is_extended_id=True))
+ bus.send(can.Message(arbitration_id=2, is_extended_id=True))
+ bus.send(can.Message(arbitration_id=1, is_extended_id=False))
+ time.sleep(1.0)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/receive_all.py b/examples/receive_all.py
old mode 100644
new mode 100755
index 6801f481d..e9410e49f
--- a/examples/receive_all.py
+++ b/examples/receive_all.py
@@ -1,25 +1,33 @@
-from __future__ import print_function
+#!/usr/bin/env python
+
+"""
+Shows how to receive messages via polling.
+"""
import can
from can.bus import BusState
def receive_all():
-
- bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000)
- #bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000)
- #bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000)
-
- bus.state = BusState.ACTIVE
- #bus.state = BusState.PASSIVE
-
- try:
- while True:
- msg = bus.recv(1)
- if msg is not None:
- print(msg)
- except KeyboardInterrupt:
- pass
+ """Receives all messages and prints them to the console until Ctrl+C is pressed."""
+
+ # this uses the default configuration (for example from environment variables, or a
+ # config file) see https://python-can.readthedocs.io/en/stable/configuration.html
+ with can.Bus() as bus:
+ # set to read-only, only supported on some interfaces
+ try:
+ bus.state = BusState.PASSIVE
+ except NotImplementedError:
+ pass
+
+ try:
+ while True:
+ msg = bus.recv(1)
+ if msg is not None:
+ print(msg)
+
+ except KeyboardInterrupt:
+ pass # exit normally
if __name__ == "__main__":
diff --git a/examples/send_multiple.py b/examples/send_multiple.py
new file mode 100755
index 000000000..9123e1bc8
--- /dev/null
+++ b/examples/send_multiple.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python
+
+"""
+This demo creates multiple processes of producers to spam a socketcan bus.
+"""
+
+from concurrent.futures import ProcessPoolExecutor
+from time import sleep
+
+import can
+
+
+def producer(thread_id: int, message_count: int = 16) -> None:
+ """Spam the bus with messages including the data id.
+
+ :param thread_id: the id of the thread/process
+ :param message_count: the number of messages that shall be sent
+ """
+
+ # this uses the default configuration (for example from environment variables, or a
+ # config file) see https://python-can.readthedocs.io/en/stable/configuration.html
+ with can.Bus() as bus:
+ for i in range(message_count):
+ msg = can.Message(
+ arbitration_id=0x0CF02200 + thread_id,
+ data=[thread_id, i, 0, 1, 3, 1, 4, 1],
+ )
+ bus.send(msg)
+ sleep(1.0)
+
+ print(f"Producer #{thread_id} finished sending {message_count} messages")
+
+
+if __name__ == "__main__":
+ with ProcessPoolExecutor() as executor:
+ executor.map(producer, range(5))
diff --git a/examples/send_one.py b/examples/send_one.py
index ebf0d1790..41b3a3cd0 100755
--- a/examples/send_one.py
+++ b/examples/send_one.py
@@ -1,36 +1,35 @@
#!/usr/bin/env python
-# coding: utf-8
"""
This example shows how sending a single message works.
"""
-from __future__ import print_function
-
import can
+
def send_one():
+ """Sends a single message."""
# this uses the default configuration (for example from the config file)
- # see http://python-can.readthedocs.io/en/latest/configuration.html
- bus = can.interface.Bus()
-
- # Using specific buses works similar:
- # bus = can.interface.Bus(bustype='socketcan', channel='vcan0', bitrate=250000)
- # bus = can.interface.Bus(bustype='pcan', channel='PCAN_USBBUS1', bitrate=250000)
- # bus = can.interface.Bus(bustype='ixxat', channel=0, bitrate=250000)
- # bus = can.interface.Bus(bustype='vector', app_name='CANalyzer', channel=0, bitrate=250000)
- # ...
-
- msg = can.Message(arbitration_id=0xc0ffee,
- data=[0, 25, 0, 1, 3, 1, 4, 1],
- extended_id=True)
-
- try:
- bus.send(msg)
- print("Message sent on {}".format(bus.channel_info))
- except can.CanError:
- print("Message NOT sent")
-
-if __name__ == '__main__':
+ # see https://python-can.readthedocs.io/en/stable/configuration.html
+ with can.Bus() as bus:
+ # Using specific buses works similar:
+ # bus = can.Bus(interface='socketcan', channel='vcan0', bitrate=250000)
+ # bus = can.Bus(interface='pcan', channel='PCAN_USBBUS1', bitrate=250000)
+ # bus = can.Bus(interface='ixxat', channel=0, bitrate=250000)
+ # bus = can.Bus(interface='vector', app_name='CANalyzer', channel=0, bitrate=250000)
+ # ...
+
+ msg = can.Message(
+ arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True
+ )
+
+ try:
+ bus.send(msg)
+ print(f"Message sent on {bus.channel_info}")
+ except can.CanError:
+ print("Message NOT sent")
+
+
+if __name__ == "__main__":
send_one()
diff --git a/examples/serial_com.py b/examples/serial_com.py
old mode 100644
new mode 100755
index efa0bcdb5..9f203b2e0
--- a/examples/serial_com.py
+++ b/examples/serial_com.py
@@ -1,8 +1,7 @@
#!/usr/bin/env python
-# coding: utf-8
"""
-This example sends every second a messages over the serial interface and also
+This example sends every second a messages over the serial interface and also
receives incoming messages.
python3 -m examples.serial_com
@@ -19,55 +18,63 @@
com0com: http://com0com.sourceforge.net/
"""
-from __future__ import print_function
-
-import time
import threading
+import time
import can
def send_cyclic(bus, msg, stop_event):
+ """The loop for sending."""
print("Start to send a message every 1s")
start_time = time.time()
while not stop_event.is_set():
msg.timestamp = time.time() - start_time
bus.send(msg)
- print("tx: {}".format(tx_msg))
+ print(f"tx: {msg}")
time.sleep(1)
print("Stopped sending messages")
def receive(bus, stop_event):
+ """The loop for receiving."""
print("Start receiving messages")
while not stop_event.is_set():
rx_msg = bus.recv(1)
if rx_msg is not None:
- print("rx: {}".format(rx_msg))
+ print(f"rx: {rx_msg}")
print("Stopped receiving messages")
-if __name__ == "__main__":
- server = can.interface.Bus(bustype='serial', channel='/dev/ttyS10')
- client = can.interface.Bus(bustype='serial', channel='/dev/ttyS11')
-
- tx_msg = can.Message(arbitration_id=0x01, data=[0x11, 0x22, 0x33, 0x44,
- 0x55, 0x66, 0x77, 0x88])
-
- # Thread for sending and receiving messages
- stop_event = threading.Event()
- t_send_cyclic = threading.Thread(target=send_cyclic, args=(server, tx_msg,
- stop_event))
- t_receive = threading.Thread(target=receive, args=(client, stop_event))
- t_receive.start()
- t_send_cyclic.start()
-
- try:
- while True:
- pass
- except KeyboardInterrupt:
- pass
-
- stop_event.set()
- server.shutdown()
- client.shutdown()
+
+def main():
+ """Controls the sender and receiver."""
+ with can.Bus(interface="serial", channel="/dev/ttyS10") as server:
+ with can.Bus(interface="serial", channel="/dev/ttyS11") as client:
+ tx_msg = can.Message(
+ arbitration_id=0x01,
+ data=[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88],
+ )
+
+ # Thread for sending and receiving messages
+ stop_event = threading.Event()
+ t_send_cyclic = threading.Thread(
+ target=send_cyclic, args=(server, tx_msg, stop_event)
+ )
+ t_receive = threading.Thread(target=receive, args=(client, stop_event))
+ t_receive.start()
+ t_send_cyclic.start()
+
+ try:
+ while True:
+ time.sleep(0) # yield
+ except KeyboardInterrupt:
+ pass # exit normally
+
+ stop_event.set()
+ time.sleep(0.5)
+
print("Stopped script")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/simple_log_converter.py b/examples/simple_log_converter.py
index 782ac9b7c..20e8fba75 100755
--- a/examples/simple_log_converter.py
+++ b/examples/simple_log_converter.py
@@ -1,19 +1,25 @@
#!/usr/bin/env python
-# coding: utf-8
"""
Use this to convert .can/.asc files to .log files.
+Can be easily adapted for all sorts of files.
-Usage: simpleLogConvert.py sourceLog.asc targetLog.log
+Usage: python3 simple_log_convert.py sourceLog.asc targetLog.log
"""
import sys
-import can.io.logger
-import can.io.player
+import can
-reader = can.io.player.LogReader(sys.argv[1])
-writer = can.io.logger.Logger(sys.argv[2])
-for msg in reader:
- writer.on_message_received(msg)
+def main():
+ """The transcoder"""
+
+ with can.LogReader(sys.argv[1]) as reader:
+ with can.Logger(sys.argv[2]) as writer:
+ for msg in reader:
+ writer.on_message_received(msg)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/vcan_filtered.py b/examples/vcan_filtered.py
old mode 100644
new mode 100755
index 86ee7f5ed..22bca706c
--- a/examples/vcan_filtered.py
+++ b/examples/vcan_filtered.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python
-# coding: utf-8
"""
This shows how message filtering works.
@@ -9,16 +8,22 @@
import can
-if __name__ == '__main__':
- bus = can.interface.Bus(bustype='socketcan',
- channel='vcan0',
- receive_own_messages=True)
- can_filters = [{"can_id": 1, "can_mask": 0xf, "extended": True}]
- bus.set_filters(can_filters)
- notifier = can.Notifier(bus, [can.Printer()])
- bus.send(can.Message(arbitration_id=1, extended_id=True))
- bus.send(can.Message(arbitration_id=2, extended_id=True))
- bus.send(can.Message(arbitration_id=1, extended_id=False))
+def main():
+ """Send some messages to itself and apply filtering."""
+ with can.Bus(interface="virtual", receive_own_messages=True) as bus:
+ can_filters = [{"can_id": 1, "can_mask": 0xF, "extended": True}]
+ bus.set_filters(can_filters)
- time.sleep(10)
+ # print all incoming messages, which includes the ones sent,
+ # since we set receive_own_messages to True
+ # assign to some variable so it does not garbage collected
+ with can.Notifier(bus, [can.Printer()]): # pylint: disable=unused-variable
+ bus.send(can.Message(arbitration_id=1, is_extended_id=True))
+ bus.send(can.Message(arbitration_id=2, is_extended_id=True))
+ bus.send(can.Message(arbitration_id=1, is_extended_id=False))
+ time.sleep(1.0)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/virtual_can_demo.py b/examples/virtual_can_demo.py
deleted file mode 100644
index b69fb28da..000000000
--- a/examples/virtual_can_demo.py
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-"""
-This demo creates multiple processes of producers to spam a socketcan bus.
-"""
-
-from time import sleep
-from concurrent.futures import ProcessPoolExecutor
-
-import can
-
-
-def producer(id, message_count=16):
- """Spam the bus with messages including the data id.
-
- :param int id: the id of the thread/process
- """
-
- with can.Bus(bustype='socketcan', channel='vcan0') as bus:
- for i in range(message_count):
- msg = can.Message(arbitration_id=0x0cf02200+id, data=[id, i, 0, 1, 3, 1, 4, 1])
- bus.send(msg)
- sleep(1.0)
-
- print("Producer #{} finished sending {} messages".format(id, message_count))
-
-
-if __name__ == "__main__":
- with ProcessPoolExecutor(max_workers=4) as executor:
- executor.map(producer, range(5))
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 000000000..ddaf61ef5
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,256 @@
+[build-system]
+requires = ["setuptools >= 77.0", "setuptools_scm>=8"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "python-can"
+dynamic = ["readme", "version"]
+description = "Controller Area Network interface module for Python"
+authors = [{ name = "python-can contributors" }]
+dependencies = [
+ "wrapt >= 1.10, < 3",
+ "packaging >= 23.1",
+ "typing_extensions>=3.10.0.0",
+]
+requires-python = ">=3.10"
+license = "LGPL-3.0-only"
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Education",
+ "Intended Audience :: Information Technology",
+ "Intended Audience :: Manufacturing",
+ "Intended Audience :: Telecommunications Industry",
+ "Natural Language :: English",
+ "Operating System :: MacOS",
+ "Operating System :: Microsoft :: Windows",
+ "Operating System :: POSIX :: Linux",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+ "Programming Language :: Python :: Implementation :: CPython",
+ "Programming Language :: Python :: Implementation :: PyPy",
+ "Topic :: Software Development :: Embedded Systems",
+ "Topic :: Software Development :: Embedded Systems :: Controller Area Network (CAN)",
+ "Topic :: System :: Hardware :: Hardware Drivers",
+ "Topic :: System :: Logging",
+ "Topic :: System :: Monitoring",
+ "Topic :: System :: Networking",
+ "Topic :: Utilities",
+]
+
+[project.scripts]
+can_logconvert = "can.logconvert:main"
+can_logger = "can.logger:main"
+can_player = "can.player:main"
+can_viewer = "can.viewer:main"
+can_bridge = "can.bridge:main"
+
+[project.urls]
+homepage = "https://github.com/hardbyte/python-can"
+documentation = "https://python-can.readthedocs.io"
+repository = "https://github.com/hardbyte/python-can"
+changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md"
+
+[project.optional-dependencies]
+pywin32 = ["pywin32>=305; platform_system == 'Windows' and platform_python_implementation == 'CPython'"]
+seeedstudio = ["pyserial>=3.0"]
+serial = ["pyserial~=3.0"]
+neovi = ["filelock", "python-ics>=2.12"]
+canalystii = ["canalystii>=0.1.0"]
+cantact = ["cantact>=0.0.7"]
+cvector = ["python-can-cvector"]
+gs-usb = ["gs-usb>=0.2.1"]
+nixnet = ["nixnet>=0.3.2"]
+pcan = ["uptime~=3.0.1"]
+remote = ["python-can-remote"]
+sontheim = ["python-can-sontheim>=0.1.2"]
+canine = ["python-can-canine>=0.2.2"]
+zlgcan = ["zlgcan"]
+candle = ["python-can-candle>=1.2.2"]
+rp1210 = ["rp1210>=1.0.1"]
+damiao = ["python-can-damiao"]
+viewer = [
+ "windows-curses; platform_system == 'Windows' and platform_python_implementation=='CPython'"
+]
+mf4 = ["asammdf>=6.0.0"]
+multicast = ["msgpack~=1.1.0"]
+
+[dependency-groups]
+docs = [
+ "sphinx>=5.2.3",
+ "sphinxcontrib-programoutput",
+ "sphinx-inline-tabs",
+ "sphinx-copybutton",
+ "furo",
+]
+lint = [
+ "pylint==4.0.*",
+ "ruff==0.14.*",
+ "black==25.12.*",
+ "mypy==1.19.*",
+]
+test = [
+ "pytest==9.0.*",
+ "pytest-timeout==2.4.*",
+ "pytest-modern==0.7.*;platform_system!='Windows'",
+ "coveralls==4.0.*",
+ "pytest-cov==7.0.*",
+ "coverage==7.13.*",
+ "hypothesis==6.*",
+ "parameterized==0.9.*",
+]
+dev = [
+ {include-group = "docs"},
+ {include-group = "lint"},
+ {include-group = "test"},
+]
+
+[tool.setuptools.dynamic]
+readme = { file = "README.rst" }
+[tool.setuptools.package-data]
+"*" = ["README.rst", "CONTRIBUTORS.txt", "LICENSE.txt", "CHANGELOG.md"]
+doc = ["*.*"]
+examples = ["*.py"]
+can = ["py.typed"]
+
+[tool.setuptools.packages.find]
+include = ["can*"]
+
+[tool.setuptools_scm]
+# can be empty if no extra settings are needed, presence enables setuptools_scm
+
+[tool.mypy]
+warn_return_any = true
+warn_unused_configs = true
+ignore_missing_imports = true
+no_implicit_optional = true
+disallow_incomplete_defs = true
+warn_redundant_casts = true
+warn_unused_ignores = true
+exclude = [
+ "^build",
+ "^doc/conf.py$",
+ "^test",
+ "^can/interfaces/etas",
+ "^can/interfaces/gs_usb",
+ "^can/interfaces/ics_neovi",
+ "^can/interfaces/iscan",
+ "^can/interfaces/ixxat",
+ "^can/interfaces/kvaser",
+ "^can/interfaces/nican",
+ "^can/interfaces/neousys",
+ "^can/interfaces/pcan",
+ "^can/interfaces/socketcan",
+ "^can/interfaces/systec",
+ "^can/interfaces/usb2can",
+]
+
+[tool.ruff]
+line-length = 100
+
+[tool.ruff.lint]
+extend-select = [
+ "A", # flake8-builtins
+ "B", # flake8-bugbear
+ "C4", # flake8-comprehensions
+ "F", # pyflakes
+ "E", # pycodestyle errors
+ "I", # isort
+ "N", # pep8-naming
+ "PGH", # pygrep-hooks
+ "PL", # pylint
+ "RUF", # ruff-specific rules
+ "T20", # flake8-print
+ "TCH", # flake8-type-checking
+ "UP", # pyupgrade
+ "W", # pycodestyle warnings
+ "YTT", # flake8-2020
+]
+ignore = [
+ "B026", # star-arg-unpacking-after-keyword-arg
+ "PLR", # pylint refactor
+]
+
+[tool.ruff.lint.per-file-ignores]
+"can/interfaces/*" = [
+ "E501", # Line too long
+ "F403", # undefined-local-with-import-star
+ "F405", # undefined-local-with-import-star-usage
+ "N", # pep8-naming
+ "PGH003", # blanket-type-ignore
+ "RUF012", # mutable-class-default
+]
+"can/cli.py" = ["T20"] # flake8-print
+"can/logger.py" = ["T20"] # flake8-print
+"can/player.py" = ["T20"] # flake8-print
+"can/bridge.py" = ["T20"] # flake8-print
+"can/viewer.py" = ["T20"] # flake8-print
+"examples/*" = ["T20"] # flake8-print
+
+[tool.ruff.lint.isort]
+known-first-party = ["can"]
+
+[tool.pylint]
+extension-pkg-allow-list = ["curses"]
+disable = [
+ "cyclic-import",
+ "duplicate-code",
+ "fixme",
+ "invalid-name",
+ "missing-class-docstring",
+ "missing-function-docstring",
+ "missing-module-docstring",
+ "no-else-raise",
+ "no-else-return",
+ "too-few-public-methods",
+ "too-many-arguments",
+ "too-many-branches",
+ "too-many-instance-attributes",
+ "too-many-locals",
+ "too-many-positional-arguments",
+ "too-many-public-methods",
+ "too-many-statements",
+]
+
+[tool.towncrier]
+directory = "doc/changelog.d"
+filename = "CHANGELOG.md"
+start_string = "\n"
+underlines = ["", "", ""]
+title_format = "## Version [{version}](https://github.com/hardbyte/python-can/tree/{version}) - {project_date}"
+issue_format = "[#{issue}](https://github.com/hardbyte/python-can/issues/{issue})"
+
+[[tool.towncrier.type]]
+directory = "security"
+name = "Security"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "removed"
+name = "Removed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "deprecated"
+name = "Deprecated"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "added"
+name = "Added"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "changed"
+name = "Changed"
+showcontent = true
+
+[[tool.towncrier.type]]
+directory = "fixed"
+name = "Fixed"
+showcontent = true
diff --git a/scripts/can_logger.py b/scripts/can_logger.py
deleted file mode 100644
index 72a92b9d0..000000000
--- a/scripts/can_logger.py
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-"""
-See :mod:`can.logger`.
-"""
-
-from __future__ import absolute_import
-
-from can.logger import main
-
-
-if __name__ == "__main__":
- main()
diff --git a/scripts/can_player.py b/scripts/can_player.py
deleted file mode 100644
index afbd3df6e..000000000
--- a/scripts/can_player.py
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-"""
-See :mod:`can.player`.
-"""
-
-from __future__ import absolute_import
-
-from can.player import main
-
-
-if __name__ == "__main__":
- main()
diff --git a/scripts/can_viewer.py b/scripts/can_viewer.py
deleted file mode 100644
index 3c9ba738c..000000000
--- a/scripts/can_viewer.py
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-"""
-See :mod:`can.viewer`.
-"""
-
-from __future__ import absolute_import
-
-from can.viewer import main
-
-
-if __name__ == "__main__":
- main()
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 21ffc0053..000000000
--- a/setup.cfg
+++ /dev/null
@@ -1,28 +0,0 @@
-[bdist_wheel]
-universal = 1
-
-[metadata]
-license_file = LICENSE.txt
-
-[tool:pytest]
-addopts = -v --timeout=300 --cov=can --cov-config=setup.cfg
-
-[coverage:run]
-# we could also use branch coverage
-branch = False
-# already specified by call to pytest using --cov=can
-#source = can
-omit =
- # legacy code
- can/CAN.py
-
-[coverage:report]
-# two digits after decimal point
-precision = 3
-show_missing = True
-exclude_lines =
- # Have to re-enable the standard pragma, see https://coverage.readthedocs.io/en/coverage-4.5.1a/config.html#syntax
- pragma: no cover
-
- # Don't complain if non-runnable code isn't run:
- if __name__ == .__main__.:
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 4aed26e23..000000000
--- a/setup.py
+++ /dev/null
@@ -1,110 +0,0 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-"""
-python-can requires the setuptools package to be installed.
-"""
-
-from __future__ import absolute_import
-
-from os import listdir
-from os.path import isfile, join
-from sys import version_info
-import re
-import logging
-from setuptools import setup, find_packages
-
-logging.basicConfig(level=logging.WARNING)
-
-with open('can/__init__.py', 'r') as fd:
- version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]',
- fd.read(), re.MULTILINE).group(1)
-
-with open('README.rst', 'r') as f:
- long_description = f.read()
-
-# Dependencies
-extras_require = {
- 'serial': ['pyserial ~= 3.0'],
- 'neovi': ['python-ics >= 2.12']
-}
-
-tests_require = [
- 'mock ~= 2.0',
- 'nose ~= 1.3',
- 'pytest ~= 3.6',
- 'pytest-timeout ~= 1.2',
- 'pytest-cov ~= 2.5',
- 'codecov ~= 2.0',
- 'future',
- 'six'
-] + extras_require['serial']
-
-extras_require['test'] = tests_require
-
-
-setup(
- # Description
- name="python-can",
- url="https://github.com/hardbyte/python-can",
- description="Controller Area Network interface module for Python",
- long_description=long_description,
- classifiers=(
- # a list of all available ones: https://pypi.org/classifiers/
- "Programming Language :: Python",
- "Programming Language :: Python :: 2.7",
- "Programming Language :: Python :: 3.4",
- "Programming Language :: Python :: 3.5",
- "Programming Language :: Python :: 3.6",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: Implementation :: CPython",
- "Programming Language :: Python :: Implementation :: PyPy",
- "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
- "Operating System :: MacOS",
- "Operating System :: POSIX :: Linux",
- "Operating System :: Microsoft :: Windows",
- "Development Status :: 5 - Production/Stable",
- "Environment :: Console",
- "Intended Audience :: Developers",
- "Intended Audience :: Education",
- "Intended Audience :: Information Technology",
- "Intended Audience :: Manufacturing",
- "Intended Audience :: Telecommunications Industry",
- "Natural Language :: English",
- "Topic :: System :: Logging",
- "Topic :: System :: Monitoring",
- "Topic :: System :: Networking",
- "Topic :: System :: Hardware :: Hardware Drivers",
- "Topic :: Utilities"
- ),
-
- # Code
- version=version,
- packages=find_packages(exclude=["test", "test.*"]),
- scripts=list(filter(isfile, (join("scripts/", f) for f in listdir("scripts/")))),
-
- # Author
- author="Brian Thorne",
- author_email="brian@thorne.link",
-
- # License
- license="LGPL v3",
-
- # Package data
- package_data={
- "": ["CONTRIBUTORS.txt", "LICENSE.txt", "CHANGELOG.txt"],
- "doc": ["*.*"]
- },
-
- # Installation
- # see https://www.python.org/dev/peps/pep-0345/#version-specifiers
- python_requires=">=2.7,!=3.0,!=3.1,!=3.2,!=3.3",
- install_requires=[
- 'wrapt ~= 1.10', 'typing', 'windows-curses;platform_system=="Windows"',
- ],
- extras_require=extras_require,
-
- # Testing
- test_suite="nose.collector",
- tests_require=tests_require,
-)
diff --git a/test/__init__.py b/test/__init__.py
index 394a0a067..4265cc3e6 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -1,2 +1 @@
#!/usr/bin/env python
-# coding: utf-8
diff --git a/test/back2back_test.py b/test/back2back_test.py
index 5d0034330..ce7c39e2a 100644
--- a/test/back2back_test.py
+++ b/test/back2back_test.py
@@ -1,59 +1,73 @@
#!/usr/bin/env python
-# coding: utf-8
"""
-This module tests two virtual busses attached to each other.
+This module tests two buses attached to each other.
"""
-from __future__ import absolute_import, print_function
-
-import sys
+import random
import unittest
-from time import sleep
-from multiprocessing.dummy import Pool as ThreadPool
+from multiprocessing.dummy import Pool as ThreadPool
+from time import sleep, time
import pytest
import can
+from can import CanInterfaceNotImplementedError
+from can.interfaces.udp_multicast import UdpMulticastBus
+from can.interfaces.udp_multicast.utils import is_msgpack_installed
-from .config import *
+from .config import (
+ IS_CI,
+ IS_OSX,
+ IS_PYPY,
+ TEST_CAN_FD,
+ TEST_INTERFACE_SOCKETCAN,
+)
class Back2BackTestCase(unittest.TestCase):
- """
- Use two interfaces connected to the same CAN bus and test them against
- each other.
+ """Use two interfaces connected to the same CAN bus and test them against each other.
+
+ This very class declaration runs the test on the *virtual* interface but subclasses can be created for
+ other buses.
"""
BITRATE = 500000
- TIMEOUT = 0.1
+ TIMEOUT = 1.0 if IS_PYPY else 0.1
- INTERFACE_1 = 'virtual'
- CHANNEL_1 = 'virtual_channel_0'
- INTERFACE_2 = 'virtual'
- CHANNEL_2 = 'virtual_channel_0'
+ INTERFACE_1 = "virtual"
+ CHANNEL_1 = "virtual_channel_0"
+ INTERFACE_2 = "virtual"
+ CHANNEL_2 = "virtual_channel_0"
def setUp(self):
- self.bus1 = can.Bus(channel=self.CHANNEL_1,
- bustype=self.INTERFACE_1,
- bitrate=self.BITRATE,
- fd=TEST_CAN_FD,
- single_handle=True)
- self.bus2 = can.Bus(channel=self.CHANNEL_2,
- bustype=self.INTERFACE_2,
- bitrate=self.BITRATE,
- fd=TEST_CAN_FD,
- single_handle=True)
+ self.bus1 = can.Bus(
+ channel=self.CHANNEL_1,
+ interface=self.INTERFACE_1,
+ bitrate=self.BITRATE,
+ fd=TEST_CAN_FD,
+ single_handle=True,
+ )
+ self.bus2 = can.Bus(
+ channel=self.CHANNEL_2,
+ interface=self.INTERFACE_2,
+ bitrate=self.BITRATE,
+ fd=TEST_CAN_FD,
+ single_handle=True,
+ )
def tearDown(self):
self.bus1.shutdown()
self.bus2.shutdown()
- def _check_received_message(self, recv_msg, sent_msg):
- self.assertIsNotNone(recv_msg,
- "No message was received on %s" % self.INTERFACE_2)
+ def _check_received_message(
+ self, recv_msg: can.Message, sent_msg: can.Message
+ ) -> None:
+ self.assertIsNotNone(
+ recv_msg, "No message was received on %s" % self.INTERFACE_2
+ )
self.assertEqual(recv_msg.arbitration_id, sent_msg.arbitration_id)
- self.assertEqual(recv_msg.id_type, sent_msg.id_type)
+ self.assertEqual(recv_msg.is_extended_id, sent_msg.is_extended_id)
self.assertEqual(recv_msg.is_remote_frame, sent_msg.is_remote_frame)
self.assertEqual(recv_msg.is_error_frame, sent_msg.is_error_frame)
self.assertEqual(recv_msg.is_fd, sent_msg.is_fd)
@@ -62,7 +76,7 @@ def _check_received_message(self, recv_msg, sent_msg):
if not sent_msg.is_remote_frame:
self.assertSequenceEqual(recv_msg.data, sent_msg.data)
- def _send_and_receive(self, msg):
+ def _send_and_receive(self, msg: can.Message) -> None:
# Send with bus 1, receive with bus 2
self.bus1.send(msg)
recv_msg = self.bus2.recv(self.TIMEOUT)
@@ -74,13 +88,23 @@ def _send_and_receive(self, msg):
# Add 1 to arbitration ID to make it a different message
msg.arbitration_id += 1
self.bus2.send(msg)
+ # Some buses may receive their own messages. Remove it from the queue
+ self.bus2.recv(0)
recv_msg = self.bus1.recv(self.TIMEOUT)
self._check_received_message(recv_msg, msg)
def test_no_message(self):
+ """Tests that there is no message being received if none was sent."""
self.assertIsNone(self.bus1.recv(0.1))
- @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server")
+ def test_multiple_shutdown(self):
+ """Tests whether shutting down ``bus1`` twice does not throw any errors."""
+ self.bus1.shutdown()
+
+ @unittest.skipIf(
+ IS_CI,
+ "the timing sensitive behaviour cannot be reproduced reliably on a CI server",
+ )
def test_timestamp(self):
self.bus2.send(can.Message())
recv_msg1 = self.bus1.recv(self.TIMEOUT)
@@ -88,111 +112,303 @@ def test_timestamp(self):
self.bus2.send(can.Message())
recv_msg2 = self.bus1.recv(self.TIMEOUT)
delta_time = recv_msg2.timestamp - recv_msg1.timestamp
- self.assertTrue(1.75 <= delta_time <= 2.25,
- 'Time difference should have been 2s +/- 250ms.'
- 'But measured {}'.format(delta_time))
+ self.assertTrue(
+ 1.75 <= delta_time <= 2.25,
+ "Time difference should have been 2s +/- 250ms."
+ f"But measured {delta_time}",
+ )
def test_standard_message(self):
- msg = can.Message(extended_id=False,
- arbitration_id=0x100,
- data=[1, 2, 3, 4, 5, 6, 7, 8])
+ msg = can.Message(
+ is_extended_id=False, arbitration_id=0x100, data=[1, 2, 3, 4, 5, 6, 7, 8]
+ )
self._send_and_receive(msg)
def test_extended_message(self):
- msg = can.Message(extended_id=True,
- arbitration_id=0x123456,
- data=[10, 11, 12, 13, 14, 15, 16, 17])
+ msg = can.Message(
+ is_extended_id=True,
+ arbitration_id=0x123456,
+ data=[10, 11, 12, 13, 14, 15, 16, 17],
+ )
self._send_and_receive(msg)
def test_remote_message(self):
- msg = can.Message(extended_id=False,
- arbitration_id=0x200,
- is_remote_frame=True,
- dlc=4)
+ msg = can.Message(
+ is_extended_id=False, arbitration_id=0x200, is_remote_frame=True, dlc=4
+ )
self._send_and_receive(msg)
def test_dlc_less_than_eight(self):
- msg = can.Message(extended_id=False,
- arbitration_id=0x300,
- data=[4, 5, 6])
+ msg = can.Message(is_extended_id=False, arbitration_id=0x300, data=[4, 5, 6])
self._send_and_receive(msg)
- @unittest.skipUnless(TEST_CAN_FD, "Don't test CAN-FD")
+ @unittest.skip(
+ "TODO: how shall this be treated if sending messages locally? should be done uniformly"
+ )
+ def test_message_is_rx(self):
+ """Verify that received messages have is_rx set to `False` while messages
+ received on the other virtual interfaces have is_rx set to `True`.
+ """
+ msg = can.Message(
+ is_extended_id=False, arbitration_id=0x300, data=[2, 1, 3], is_rx=False
+ )
+ self.bus1.send(msg)
+ # Some buses may receive their own messages. Remove it from the queue
+ self.bus1.recv(0)
+ self_recv_msg = self.bus2.recv(self.TIMEOUT)
+ self.assertIsNotNone(self_recv_msg)
+ self.assertTrue(self_recv_msg.is_rx)
+
+ @unittest.skip(
+ "TODO: how shall this be treated if sending messages locally? should be done uniformly"
+ )
+ def test_message_is_rx_receive_own_messages(self):
+ """The same as `test_message_direction` but testing with `receive_own_messages=True`."""
+ with can.Bus(
+ channel=self.CHANNEL_2,
+ interface=self.INTERFACE_2,
+ bitrate=self.BITRATE,
+ fd=TEST_CAN_FD,
+ single_handle=True,
+ receive_own_messages=True,
+ ) as bus3:
+ msg = can.Message(
+ is_extended_id=False, arbitration_id=0x300, data=[2, 1, 3], is_rx=False
+ )
+ bus3.send(msg)
+ self_recv_msg_bus3 = bus3.recv(self.TIMEOUT)
+ self.assertTrue(self_recv_msg_bus3.is_rx)
+
+ def test_unique_message_instances(self):
+ """Verify that we have a different instances of message for each bus even with
+ `receive_own_messages=True`.
+ """
+ with can.Bus(
+ channel=self.CHANNEL_2,
+ interface=self.INTERFACE_2,
+ bitrate=self.BITRATE,
+ fd=TEST_CAN_FD,
+ single_handle=True,
+ receive_own_messages=True,
+ ) as bus3:
+ msg = can.Message(
+ is_extended_id=False, arbitration_id=0x300, data=[2, 1, 3]
+ )
+ bus3.send(msg)
+ recv_msg_bus1 = self.bus1.recv(self.TIMEOUT)
+ recv_msg_bus2 = self.bus2.recv(self.TIMEOUT)
+ self_recv_msg_bus3 = bus3.recv(self.TIMEOUT)
+
+ self._check_received_message(recv_msg_bus1, recv_msg_bus2)
+ self._check_received_message(recv_msg_bus2, self_recv_msg_bus3)
+
+ recv_msg_bus1.data[0] = 4
+ self.assertNotEqual(recv_msg_bus1.data, recv_msg_bus2.data)
+ self.assertEqual(recv_msg_bus2.data, self_recv_msg_bus3.data)
+
def test_fd_message(self):
- msg = can.Message(is_fd=True,
- extended_id=True,
- arbitration_id=0x56789,
- data=[0xff] * 64)
+ msg = can.Message(
+ is_fd=True, is_extended_id=True, arbitration_id=0x56789, data=[0xFF] * 64
+ )
self._send_and_receive(msg)
- @unittest.skipUnless(TEST_CAN_FD, "Don't test CAN-FD")
def test_fd_message_with_brs(self):
- msg = can.Message(is_fd=True,
- bitrate_switch=True,
- extended_id=True,
- arbitration_id=0x98765,
- data=[0xff] * 48)
+ msg = can.Message(
+ is_fd=True,
+ bitrate_switch=True,
+ is_extended_id=True,
+ arbitration_id=0x98765,
+ data=[0xFF] * 48,
+ )
self._send_and_receive(msg)
+ def test_fileno(self):
+ """Test is the values returned by fileno() are valid."""
+ try:
+ fileno = self.bus1.fileno()
+ except NotImplementedError:
+ pass # allow it to be left non-implemented
+ else:
+ self.assertIsNotNone(fileno)
+ self.assertTrue(fileno == -1 or fileno > 0)
+
+ def test_timestamp_is_absolute(self):
+ """Tests that the timestamp that is returned is an absolute one."""
+ self.bus2.send(can.Message())
+ # Some buses may receive their own messages. Remove it from the queue
+ self.bus2.recv(0)
+ message = self.bus1.recv(self.TIMEOUT)
+ # The allowed delta is still quite large to make this work on the CI server
+ self.assertAlmostEqual(message.timestamp, time(), delta=self.TIMEOUT)
+
+ def test_sub_second_timestamp_resolution(self):
+ """Tests that the timestamp that is returned has sufficient resolution.
+
+ The property that the timestamp has resolution below seconds is
+ checked on two messages to reduce the probability of both having
+ a timestamp of exactly a full second by accident to a negligible
+ level.
+
+ This is a regression test that was added for #1021.
+ """
+ self.bus2.send(can.Message())
+ sleep(0.01)
+ self.bus2.send(can.Message())
+
+ recv_msg_1 = self.bus1.recv(self.TIMEOUT)
+ recv_msg_2 = self.bus1.recv(self.TIMEOUT)
+
+ sub_second_fraction_1 = recv_msg_1.timestamp % 1
+ sub_second_fraction_2 = recv_msg_2.timestamp % 1
+ self.assertGreater(sub_second_fraction_1 + sub_second_fraction_2, 0)
+
+ # Some buses may receive their own messages. Remove it from the queue
+ self.bus2.recv(0)
+ self.bus2.recv(0)
+
+ def test_send_periodic_duration(self):
+ """
+ Verify that send_periodic only transmits for the specified duration.
+
+ Regression test for #1713.
+ """
+ for duration, period in [(0.01, 0.003), (0.1, 0.011), (1, 0.4)]:
+ messages = []
+
+ self.bus2.send_periodic(can.Message(), period, duration)
+ while (msg := self.bus1.recv(period + self.TIMEOUT)) is not None:
+ messages.append(msg)
+
+ delta_t = messages[-1].timestamp - messages[0].timestamp
+ assert delta_t < duration + 0.05
+
@unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan")
class BasicTestSocketCan(Back2BackTestCase):
-
- INTERFACE_1 = 'socketcan'
- CHANNEL_1 = 'vcan0'
- INTERFACE_2 = 'socketcan'
- CHANNEL_2 = 'vcan0'
+ INTERFACE_1 = "socketcan"
+ CHANNEL_1 = "vcan0"
+ INTERFACE_2 = "socketcan"
+ CHANNEL_2 = "vcan0"
+
+
+# this doesn't even work on Travis CI for macOS; for example, see
+# https://travis-ci.org/github/hardbyte/python-can/jobs/745389871
+@unittest.skipIf(
+ IS_CI and IS_OSX,
+ "not supported for macOS CI",
+)
+@unittest.skipUnless(
+ is_msgpack_installed(raise_exception=False),
+ "msgpack not installed",
+)
+class BasicTestUdpMulticastBusIPv4(Back2BackTestCase):
+ INTERFACE_1 = "udp_multicast"
+ CHANNEL_1 = UdpMulticastBus.DEFAULT_GROUP_IPv4
+ INTERFACE_2 = "udp_multicast"
+ CHANNEL_2 = UdpMulticastBus.DEFAULT_GROUP_IPv4
+
+ def test_unique_message_instances(self):
+ with self.assertRaises(CanInterfaceNotImplementedError):
+ super().test_unique_message_instances()
+
+
+# this doesn't even work for loopback multicast addresses on Travis CI; for example, see
+# https://travis-ci.org/github/hardbyte/python-can/builds/745065503
+@unittest.skipIf(
+ IS_CI and IS_OSX,
+ "not supported for macOS CI",
+)
+@unittest.skipUnless(
+ is_msgpack_installed(raise_exception=False),
+ "msgpack not installed",
+)
+class BasicTestUdpMulticastBusIPv6(Back2BackTestCase):
+ HOST_LOCAL_MCAST_GROUP_IPv6 = "ff11:7079:7468:6f6e:6465:6d6f:6d63:6173"
+
+ INTERFACE_1 = "udp_multicast"
+ CHANNEL_1 = HOST_LOCAL_MCAST_GROUP_IPv6
+ INTERFACE_2 = "udp_multicast"
+ CHANNEL_2 = HOST_LOCAL_MCAST_GROUP_IPv6
+
+ def test_unique_message_instances(self):
+ with self.assertRaises(CanInterfaceNotImplementedError):
+ super().test_unique_message_instances()
+
+
+TEST_INTERFACE_ETAS = False
+try:
+ bus_class = can.interface._get_class_for_interface("etas")
+ TEST_INTERFACE_ETAS = True
+except CanInterfaceNotImplementedError:
+ pass
+
+
+@unittest.skipUnless(TEST_INTERFACE_ETAS, "skip testing of etas interface")
+class BasicTestEtas(Back2BackTestCase):
+ if TEST_INTERFACE_ETAS:
+ configs = can.interface.detect_available_configs(interfaces="etas")
+
+ INTERFACE_1 = "etas"
+ CHANNEL_1 = configs[0]["channel"]
+ INTERFACE_2 = "etas"
+ CHANNEL_2 = configs[2]["channel"]
+
+ def test_unique_message_instances(self):
+ self.skipTest(
+ "creating a second instance of a channel with differing self-reception settings is not supported"
+ )
@unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan")
class SocketCanBroadcastChannel(unittest.TestCase):
-
def setUp(self):
- self.broadcast_bus = can.Bus(channel='', bustype='socketcan')
- self.regular_bus = can.Bus(channel='vcan0', bustype='socketcan')
+ self.broadcast_bus = can.Bus(channel="", interface="socketcan")
+ self.regular_bus = can.Bus(channel="vcan0", interface="socketcan")
def tearDown(self):
self.broadcast_bus.shutdown()
self.regular_bus.shutdown()
def test_broadcast_channel(self):
- self.broadcast_bus.send(can.Message(channel='vcan0'))
+ self.broadcast_bus.send(can.Message(channel="vcan0"))
recv_msg = self.regular_bus.recv(1)
self.assertIsNotNone(recv_msg)
- self.assertEqual(recv_msg.channel, 'vcan0')
+ self.assertEqual(recv_msg.channel, "vcan0")
self.regular_bus.send(can.Message())
recv_msg = self.broadcast_bus.recv(1)
self.assertIsNotNone(recv_msg)
- self.assertEqual(recv_msg.channel, 'vcan0')
+ self.assertEqual(recv_msg.channel, "vcan0")
class TestThreadSafeBus(Back2BackTestCase):
- """Does some testing that is better than nothing.
- """
-
def setUp(self):
- self.bus1 = can.ThreadSafeBus(channel=self.CHANNEL_1,
- bustype=self.INTERFACE_1,
- bitrate=self.BITRATE,
- fd=TEST_CAN_FD,
- single_handle=True)
- self.bus2 = can.ThreadSafeBus(channel=self.CHANNEL_2,
- bustype=self.INTERFACE_2,
- bitrate=self.BITRATE,
- fd=TEST_CAN_FD,
- single_handle=True)
-
- @pytest.mark.timeout(5.0)
+ self.bus1 = can.ThreadSafeBus(
+ channel=self.CHANNEL_1,
+ interface=self.INTERFACE_1,
+ bitrate=self.BITRATE,
+ fd=TEST_CAN_FD,
+ single_handle=True,
+ )
+ self.bus2 = can.ThreadSafeBus(
+ channel=self.CHANNEL_2,
+ interface=self.INTERFACE_2,
+ bitrate=self.BITRATE,
+ fd=TEST_CAN_FD,
+ single_handle=True,
+ )
+
+ @pytest.mark.timeout(180.0 if IS_PYPY else 5.0)
def test_concurrent_writes(self):
sender_pool = ThreadPool(100)
receiver_pool = ThreadPool(100)
message = can.Message(
arbitration_id=0x123,
- extended_id=True,
+ channel=self.CHANNEL_1,
+ is_extended_id=True,
timestamp=121334.365,
- data=[254, 255, 1, 2]
+ data=[254, 255, 1, 2],
)
workload = 1000 * [message]
@@ -200,12 +416,57 @@ def sender(msg):
self.bus1.send(msg)
def receiver(_):
- result = self.bus2.recv(timeout=2.0)
- self.assertIsNotNone(result)
- self.assertEqual(result, message)
+ return self.bus2.recv()
sender_pool.map_async(sender, workload)
- receiver_pool.map_async(receiver, len(workload) * [None])
+ for msg in receiver_pool.map(receiver, len(workload) * [None]):
+ self.assertIsNotNone(msg)
+ self.assertEqual(message.arbitration_id, msg.arbitration_id)
+ self.assertTrue(message.equals(msg, timestamp_delta=None))
+
+ sender_pool.close()
+ sender_pool.join()
+ receiver_pool.close()
+ receiver_pool.join()
+
+ @pytest.mark.timeout(180.0 if IS_PYPY else 5.0)
+ def test_filtered_bus(self):
+ sender_pool = ThreadPool(100)
+ receiver_pool = ThreadPool(100)
+
+ included_message = can.Message(
+ arbitration_id=0x123,
+ channel=self.CHANNEL_1,
+ is_extended_id=True,
+ timestamp=121334.365,
+ data=[254, 255, 1, 2],
+ )
+ excluded_message = can.Message(
+ arbitration_id=0x02,
+ channel=self.CHANNEL_1,
+ is_extended_id=True,
+ timestamp=121334.300,
+ data=[1, 2, 3],
+ )
+ workload = 500 * [included_message] + 500 * [excluded_message]
+ random.shuffle(workload)
+
+ self.bus2.set_filters([{"can_id": 0x123, "can_mask": 0xFF, "extended": True}])
+
+ def sender(msg):
+ self.bus1.send(msg)
+
+ def receiver(_):
+ return self.bus2.recv()
+
+ sender_pool.map_async(sender, workload)
+ received_msgs = receiver_pool.map(receiver, 500 * [None])
+
+ for msg in received_msgs:
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, included_message.arbitration_id)
+ self.assertTrue(included_message.equals(msg, timestamp_delta=None))
+ self.assertEqual(len(received_msgs), 500)
sender_pool.close()
sender_pool.join()
@@ -213,5 +474,5 @@ def receiver(_):
receiver_pool.join()
-if __name__ == '__main__':
+if __name__ == "__main__":
unittest.main()
diff --git a/test/config.py b/test/config.py
index 3c37bcbe6..d308a8cc8 100644
--- a/test/config.py
+++ b/test/config.py
@@ -1,32 +1,37 @@
#!/usr/bin/env python
-# coding: utf-8
"""
This module contains various configuration for the tests.
Some tests are skipped when run on a CI server because they are not
-reproducible, see #243 (https://github.com/hardbyte/python-can/issues/243).
+reproducible, see for example #243 and #940.
"""
import platform
from os import environ as environment
-# ############################## Continuos integration
+def env(name: str) -> bool:
+ return environment.get(name, "").lower() in ("yes", "true", "t", "1")
+
+
+# ############################## Continuous integration
# see here for the environment variables that are set on the CI servers:
# - https://docs.travis-ci.com/user/environment-variables/
-# - https://www.appveyor.com/docs/environment-variables/
+# - https://docs.github.com/en/actions/reference/environment-variables#default-environment-variables
-IS_TRAVIS = environment.get('TRAVIS', '').lower() == 'true'
-IS_APPVEYOR = environment.get('APPVEYOR', '').lower() == 'true'
+IS_TRAVIS = env("TRAVIS")
+IS_GITHUB_ACTIONS = env("GITHUB_ACTIONS")
-IS_CI = IS_TRAVIS or IS_APPVEYOR or \
- environment.get('CI', '').lower() == 'true' or \
- environment.get('CONTINUOUS_INTEGRATION', '').lower() == 'true'
+IS_CI = IS_TRAVIS or IS_GITHUB_ACTIONS or env("CI") or env("CONTINUOUS_INTEGRATION")
+
+if IS_TRAVIS and IS_GITHUB_ACTIONS:
+ raise OSError(
+ f"only one of IS_TRAVIS ({IS_TRAVIS}) and IS_GITHUB_ACTIONS ({IS_GITHUB_ACTIONS}) may be True at the "
+ "same time"
+ )
-if IS_APPVEYOR and IS_TRAVIS:
- raise EnvironmentError("IS_APPVEYOR and IS_TRAVIS cannot be both True at the same time")
# ############################## Platforms
@@ -35,18 +40,22 @@
IS_LINUX = "linux" in _sys
IS_OSX = "darwin" in _sys
IS_UNIX = IS_LINUX or IS_OSX
+del _sys
if (IS_WINDOWS and IS_LINUX) or (IS_LINUX and IS_OSX) or (IS_WINDOWS and IS_OSX):
- raise EnvironmentError(
- "only one of IS_WINDOWS ({}), IS_LINUX ({}) and IS_OSX ({}) ".format(IS_WINDOWS, IS_LINUX, IS_OSX) +
- "can be True at the same time " +
- '(platform.system() == "{}")'.format(platform.system())
+ raise OSError(
+ f"only one of IS_WINDOWS ({IS_WINDOWS}), IS_LINUX ({IS_LINUX}) and IS_OSX ({IS_OSX}) "
+ f'can be True at the same time (platform.system() == "{platform.system()}")'
)
-elif not IS_WINDOWS and not IS_LINUX and not IS_OSX:
- raise EnvironmentError("one of IS_WINDOWS, IS_LINUX, IS_OSX has to be True")
+
+
+# ############################## Implementations
+
+IS_PYPY = platform.python_implementation() == "PyPy"
+
# ############################## What tests to run
TEST_CAN_FD = True
-TEST_INTERFACE_SOCKETCAN = IS_CI and IS_LINUX
+TEST_INTERFACE_SOCKETCAN = IS_LINUX and env("TEST_SOCKETCAN")
diff --git a/test/conftest.py b/test/conftest.py
new file mode 100644
index 000000000..c54238be1
--- /dev/null
+++ b/test/conftest.py
@@ -0,0 +1,24 @@
+import pytest
+
+from can.interfaces import virtual
+
+
+@pytest.fixture(autouse=True)
+def check_unclosed_virtual_channel():
+ """
+ Pytest fixture for detecting leaked virtual CAN channels.
+
+ - The fixture yields control to the test.
+ - After the test completes, it acquires `virtual.channels_lock` and asserts
+ that `virtual.channels` is empty.
+ - If a test leaves behind any unclosed virtual CAN channels, the assertion
+ will fail, surfacing resource leaks early.
+
+ This helps maintain test isolation and prevents subtle bugs caused by
+ leftover state between tests.
+ """
+
+ yield
+
+ with virtual.channels_lock:
+ assert len(virtual.channels) == 0
diff --git a/test/contextmanager_test.py b/test/contextmanager_test.py
index ea9321502..fe87f33b0 100644
--- a/test/contextmanager_test.py
+++ b/test/contextmanager_test.py
@@ -1,22 +1,26 @@
#!/usr/bin/env python
-# coding: utf-8
"""
This module tests the context manager of Bus and Notifier classes
"""
import unittest
+
import can
class ContextManagerTest(unittest.TestCase):
-
def setUp(self):
data = [0, 1, 2, 3, 4, 5, 6, 7]
- self.msg_send = can.Message(extended_id=False, arbitration_id=0x100, data=data)
+ self.msg_send = can.Message(
+ is_extended_id=False, arbitration_id=0x100, data=data
+ )
def test_open_buses(self):
- with can.Bus(interface='virtual') as bus_send, can.Bus(interface='virtual') as bus_recv:
+ with (
+ can.Bus(interface="virtual") as bus_send,
+ can.Bus(interface="virtual") as bus_recv,
+ ):
bus_send.send(self.msg_send)
msg_recv = bus_recv.recv()
@@ -24,7 +28,10 @@ def test_open_buses(self):
self.assertTrue(msg_recv)
def test_use_closed_bus(self):
- with can.Bus(interface='virtual') as bus_send, can.Bus(interface='virtual') as bus_recv:
+ with (
+ can.Bus(interface="virtual") as bus_send,
+ can.Bus(interface="virtual") as bus_recv,
+ ):
bus_send.send(self.msg_send)
# Receiving a frame after bus has been closed should raise a CanException
@@ -32,5 +39,5 @@ def test_use_closed_bus(self):
self.assertRaises(can.CanError, bus_send.send, self.msg_send)
-if __name__ == '__main__':
+if __name__ == "__main__":
unittest.main()
diff --git a/test/data/__init__.py b/test/data/__init__.py
index 394a0a067..4265cc3e6 100644
--- a/test/data/__init__.py
+++ b/test/data/__init__.py
@@ -1,2 +1 @@
#!/usr/bin/env python
-# coding: utf-8
diff --git a/test/data/example_data.py b/test/data/example_data.py
index e1a446384..b78420e4e 100644
--- a/test/data/example_data.py
+++ b/test/data/example_data.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python
-# coding: utf-8
"""
This module contains some example data, like messages of different
@@ -22,7 +21,7 @@ def sort_messages(messages):
:param Iterable[can.Message] messages: a sequence of messages to sort
:rtype: list
"""
- return list(sorted(messages, key=attrgetter('timestamp')))
+ return list(sorted(messages, key=attrgetter("timestamp")))
# some random number
@@ -30,114 +29,158 @@ def sort_messages(messages):
# List of messages of different types that can be used in tests
-TEST_MESSAGES_BASE = [
- Message(
- # empty
- ),
- Message(
- # only data
- data=[0x00, 0x42]
- ),
- Message(
- # no data
- arbitration_id=0xAB, extended_id=False
- ),
- Message(
- # no data
- arbitration_id=0x42, extended_id=True
- ),
- Message(
- # no data
- arbitration_id=0xABCDEF,
- ),
- Message(
- # empty data
- data=[]
- ),
- Message(
- # empty data
- data=[0xFF, 0xFE, 0xFD],
- ),
- Message(
- arbitration_id=0xABCDEF, extended_id=True,
- timestamp=TEST_TIME,
- data=[1, 2, 3, 4, 5, 6, 7, 8]
- ),
- Message(
- arbitration_id=0x123, extended_id=False,
- timestamp=TEST_TIME + 42.42,
- data=[0xff, 0xff]
- ),
- Message(
- arbitration_id=0xDADADA, extended_id=True,
- timestamp=TEST_TIME + .165,
- data=[1, 2, 3, 4, 5, 6, 7, 8]
- ),
- Message(
- arbitration_id=0x123, extended_id=False,
- timestamp=TEST_TIME + .365,
- data=[254, 255]
- ),
- Message(
- arbitration_id=0x768, extended_id=False,
- timestamp=TEST_TIME + 3.165
- ),
-]
-TEST_MESSAGES_BASE = sort_messages(TEST_MESSAGES_BASE)
-
-
-TEST_MESSAGES_REMOTE_FRAMES = [
- Message(
- arbitration_id=0xDADADA, extended_id=True, is_remote_frame=True,
- timestamp=TEST_TIME + .165,
- data=[1, 2, 3, 4, 5, 6, 7, 8]
- ),
- Message(
- arbitration_id=0x123, extended_id=False, is_remote_frame=True,
- timestamp=TEST_TIME + .365,
- data=[254, 255]
- ),
- Message(
- arbitration_id=0x768, extended_id=False, is_remote_frame=True,
- timestamp=TEST_TIME + 3.165
- ),
- Message(
- arbitration_id=0xABCDEF, extended_id=True, is_remote_frame=True,
- timestamp=TEST_TIME + 7858.67
- ),
-]
-TEST_MESSAGES_REMOTE_FRAMES = sort_messages(TEST_MESSAGES_REMOTE_FRAMES)
-
-
-TEST_MESSAGES_ERROR_FRAMES = [
- Message(
- is_error_frame=True
- ),
- Message(
- is_error_frame=True,
- timestamp=TEST_TIME + 0.170
- ),
- Message(
- is_error_frame=True,
- timestamp=TEST_TIME + 17.157
- )
-]
-TEST_MESSAGES_ERROR_FRAMES = sort_messages(TEST_MESSAGES_ERROR_FRAMES)
-
-
-TEST_ALL_MESSAGES = sort_messages(TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + \
- TEST_MESSAGES_ERROR_FRAMES)
+TEST_MESSAGES_BASE = sort_messages(
+ [
+ Message(
+ # empty
+ timestamp=1e-4,
+ ),
+ Message(
+ # only data
+ timestamp=2e-4,
+ data=[0x00, 0x42],
+ ),
+ Message(
+ # no data
+ timestamp=3e-4,
+ arbitration_id=0xAB,
+ is_extended_id=False,
+ ),
+ Message(
+ # no data
+ timestamp=4e-4,
+ arbitration_id=0x42,
+ is_extended_id=True,
+ ),
+ Message(
+ # no data
+ timestamp=5e-4,
+ arbitration_id=0xABCDEF,
+ ),
+ Message(
+ # empty data
+ timestamp=6e-4,
+ data=[],
+ ),
+ Message(
+ # empty data
+ timestamp=7e-4,
+ data=[0xFF, 0xFE, 0xFD],
+ ),
+ Message(
+ # with channel as integer
+ timestamp=8e-4,
+ channel=0,
+ ),
+ Message(
+ # with channel as integer
+ timestamp=9e-4,
+ channel=42,
+ ),
+ Message(
+ # with channel as string
+ timestamp=10e-4,
+ channel="vcan0",
+ ),
+ Message(
+ # with channel as string
+ timestamp=11e-4,
+ channel="awesome_channel",
+ ),
+ Message(
+ arbitration_id=0xABCDEF,
+ is_extended_id=True,
+ timestamp=TEST_TIME,
+ data=[1, 2, 3, 4, 5, 6, 7, 8],
+ ),
+ Message(
+ arbitration_id=0x123,
+ is_extended_id=False,
+ timestamp=TEST_TIME + 42.42,
+ data=[0xFF, 0xFF],
+ ),
+ Message(
+ arbitration_id=0xDADADA,
+ is_extended_id=True,
+ timestamp=TEST_TIME + 0.165,
+ data=[1, 2, 3, 4, 5, 6, 7, 8],
+ ),
+ Message(
+ arbitration_id=0x123,
+ is_extended_id=False,
+ timestamp=TEST_TIME + 0.365,
+ data=[254, 255],
+ ),
+ Message(
+ arbitration_id=0x768, is_extended_id=False, timestamp=TEST_TIME + 3.165
+ ),
+ ]
+)
+
+
+TEST_MESSAGES_CAN_FD = sort_messages(
+ [
+ Message(timestamp=12e-4, is_fd=True, data=range(64)),
+ Message(timestamp=13e-4, is_fd=True, data=range(8)),
+ Message(timestamp=14e-4, is_fd=True, data=range(8), bitrate_switch=True),
+ Message(timestamp=15e-4, is_fd=True, data=range(8), error_state_indicator=True),
+ ]
+)
+
+
+TEST_MESSAGES_REMOTE_FRAMES = sort_messages(
+ [
+ Message(
+ arbitration_id=0xDADADA,
+ is_extended_id=True,
+ is_remote_frame=True,
+ timestamp=TEST_TIME + 0.165,
+ ),
+ Message(
+ arbitration_id=0x123,
+ is_extended_id=False,
+ is_remote_frame=True,
+ timestamp=TEST_TIME + 0.365,
+ ),
+ Message(
+ arbitration_id=0x768,
+ is_extended_id=False,
+ is_remote_frame=True,
+ timestamp=TEST_TIME + 3.165,
+ ),
+ Message(
+ arbitration_id=0xABCDEF,
+ is_extended_id=True,
+ is_remote_frame=True,
+ timestamp=TEST_TIME + 7858.67,
+ ),
+ ]
+)
+
+
+TEST_MESSAGES_ERROR_FRAMES = sort_messages(
+ [
+ Message(is_error_frame=True, timestamp=TEST_TIME),
+ Message(is_error_frame=True, timestamp=TEST_TIME + 0.170),
+ Message(is_error_frame=True, timestamp=TEST_TIME + 17.157),
+ ]
+)
+
+
+TEST_ALL_MESSAGES = sort_messages(
+ TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES
+)
TEST_COMMENTS = [
"This is the first comment",
- "", # empty comment
+ "", # empty comment
"This third comment contains some strange characters: 'ä\"§$%&/()=?__::_Öüßêè and ends here.",
(
- "This fourth comment is quite long! " \
- "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. " \
- "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. " \
- "Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi." \
+ "This fourth comment is quite long! "
+ "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. "
+ "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. "
+ "Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi."
),
]
@@ -147,5 +190,10 @@ def generate_message(arbitration_id):
Generates a new message with the given ID, some random data
and a non-extended ID.
"""
- data = bytearray([random.randrange(0, 2 ** 8 - 1) for _ in range(8)])
- return Message(arbitration_id=arbitration_id, data=data, extended_id=False, timestamp=TEST_TIME)
+ data = bytearray([random.randrange(0, 2**8 - 1) for _ in range(8)])
+ return Message(
+ arbitration_id=arbitration_id,
+ data=data,
+ is_extended_id=False,
+ timestamp=TEST_TIME,
+ )
diff --git a/test/data/ip_link_list.json b/test/data/ip_link_list.json
new file mode 100644
index 000000000..a96313b43
--- /dev/null
+++ b/test/data/ip_link_list.json
@@ -0,0 +1,91 @@
+[
+ {
+ "ifindex": 1,
+ "ifname": "lo",
+ "flags": [
+ "LOOPBACK",
+ "UP",
+ "LOWER_UP"
+ ],
+ "mtu": 65536,
+ "qdisc": "noqueue",
+ "operstate": "UNKNOWN",
+ "linkmode": "DEFAULT",
+ "group": "default",
+ "txqlen": 1000,
+ "link_type": "loopback",
+ "address": "00:00:00:00:00:00",
+ "broadcast": "00:00:00:00:00:00"
+ },
+ {
+ "ifindex": 2,
+ "ifname": "eth0",
+ "flags": [
+ "NO-CARRIER",
+ "BROADCAST",
+ "MULTICAST",
+ "UP"
+ ],
+ "mtu": 1500,
+ "qdisc": "fq_codel",
+ "operstate": "DOWN",
+ "linkmode": "DEFAULT",
+ "group": "default",
+ "txqlen": 1000,
+ "link_type": "ether",
+ "address": "11:22:33:44:55:66",
+ "broadcast": "ff:ff:ff:ff:ff:ff"
+ },
+ {
+ "ifindex": 3,
+ "ifname": "wlan0",
+ "flags": [
+ "BROADCAST",
+ "MULTICAST",
+ "UP",
+ "LOWER_UP"
+ ],
+ "mtu": 1500,
+ "qdisc": "noqueue",
+ "operstate": "UP",
+ "linkmode": "DORMANT",
+ "group": "default",
+ "txqlen": 1000,
+ "link_type": "ether",
+ "address": "11:22:33:44:55:66",
+ "broadcast": "ff:ff:ff:ff:ff:ff"
+ },
+ {
+ "ifindex": 48,
+ "ifname": "vcan0",
+ "flags": [
+ "NOARP",
+ "UP",
+ "LOWER_UP"
+ ],
+ "mtu": 72,
+ "qdisc": "noqueue",
+ "operstate": "UNKNOWN",
+ "linkmode": "DEFAULT",
+ "group": "default",
+ "txqlen": 1000,
+ "link_type": "can"
+ },
+ {
+ "ifindex": 50,
+ "ifname": "mycustomCan123",
+ "flags": [
+ "NOARP",
+ "UP",
+ "LOWER_UP"
+ ],
+ "mtu": 72,
+ "qdisc": "noqueue",
+ "operstate": "UNKNOWN",
+ "linkmode": "DEFAULT",
+ "group": "default",
+ "txqlen": 1000,
+ "link_type": "can"
+ },
+ {}
+]
\ No newline at end of file
diff --git a/test/data/issue_1256.asc b/test/data/issue_1256.asc
new file mode 100644
index 000000000..c3eb55199
--- /dev/null
+++ b/test/data/issue_1256.asc
@@ -0,0 +1,1461 @@
+date Tue May 27 04:09:35.000 pm 2014
+base hex timestamps absolute
+internal events logged
+// version 10.0.1
+ 0.019968 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.029964 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.039943 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.039977 1 11 Rx d 8 4A 28 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.049949 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.059945 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.059976 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.060015 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 0.069970 1 11 Rx d 8 84 29 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.070001 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.079951 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.089947 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.099951 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.099982 1 11 Rx d 8 BD 2A CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.109949 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.109983 1 10 Rx d 8 10 27 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 0.110014 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.110032 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 0.110053 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.129997 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.130036 1 11 Rx d 8 F5 2B 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.139949 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.149954 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.159951 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.159983 1 11 Rx d 8 2C 2D 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.160095 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 0.160132 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.169955 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.180007 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.189956 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.189991 1 11 Rx d 8 62 2E 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.199956 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.209970 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.210004 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.210026 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 0.210084 1 10 Rx d 8 F3 28 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 0.219957 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.219987 1 11 Rx d 8 95 2F 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.229990 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.240004 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.249954 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.249988 1 11 Rx d 8 C7 30 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.259976 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.260138 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 0.260170 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.269974 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.279956 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.279989 1 11 Rx d 8 F6 31 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.289959 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.299963 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.309957 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.309989 1 11 Rx d 8 22 33 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.310012 1 10 Rx d 8 D5 2A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 0.310075 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.310097 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 0.319936 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.329956 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.339973 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.339993 1 11 Rx d 8 4B 34 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.349918 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.359922 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.359957 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 0.360032 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.360102 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.360114 1 11 Rx d 8 71 35 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.380012 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.389965 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.399981 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.400017 1 11 Rx d 8 93 36 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.409967 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.410001 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.410024 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 0.410086 1 10 Rx d 8 B5 2C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 0.419955 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.429968 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.430001 1 11 Rx d 8 B2 37 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.439986 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.440148 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.459928 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.459970 1 11 Rx d 8 CC 38 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.459991 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 0.460048 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.469963 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.479988 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.489926 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.489959 1 11 Rx d 8 E2 39 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.499927 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.509930 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.509963 1 10 Rx d 8 91 2E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 0.510027 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 0.510057 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.519946 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.519980 1 11 Rx d 8 F2 3A 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.529932 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.540023 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.549926 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.549958 1 11 Rx d 8 FE 3B D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.559975 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.560137 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.560200 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 0.560231 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.579988 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.580022 1 11 Rx d 8 05 3D DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.589973 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.599972 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.609972 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.610004 1 11 Rx d 8 06 3E F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.610115 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 0.610151 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.610162 1 10 Rx d 8 69 30 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 0.610188 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.629991 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.639991 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.640026 1 11 Rx d 8 01 3F F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.649975 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.659977 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.660010 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.660144 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 0.669992 1 11 Rx d 8 F6 3F F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.670076 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.680000 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.689982 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.690146 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.690159 1 11 Rx d 8 E5 40 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.709978 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.710013 1 10 Rx d 8 3B 32 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 0.710079 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.710100 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 0.720010 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.729980 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.730013 1 11 Rx d 8 CD 41 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.739981 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.749983 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.759997 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.760030 1 11 Rx d 8 AF 42 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.760052 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 0.760113 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.769999 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.779998 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.789982 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.790003 1 11 Rx d 8 8A 43 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.799976 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.809974 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.810125 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.810139 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 0.810168 1 10 Rx d 8 07 34 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 0.819974 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.820007 1 11 Rx d 8 5D 44 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.829974 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.839975 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.849974 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.850006 1 11 Rx d 8 29 45 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.860028 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.860065 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 0.860138 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.860201 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.880033 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.880170 1 11 Rx d 8 EE 45 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.889961 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.900008 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.909943 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.909976 1 11 Rx d 8 AA 46 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.910071 1 10 Rx d 8 CB 35 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 0.910101 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.910113 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 0.919950 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.929950 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.939955 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.940093 1 11 Rx d 8 5F 47 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.940105 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.959949 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.959982 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 0.960053 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 0.969947 1 64 Rx d 4 5C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.969981 1 11 Rx d 8 0B 48 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 0.979953 1 64 Rx d 4 64 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.990010 1 64 Rx d 4 6C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 0.999991 1 64 Rx d 4 74 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.000156 1 11 Rx d 8 AF 48 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.009977 1 64 Rx d 4 7C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.010014 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.010037 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 1.010107 1 10 Rx d 8 86 37 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 1.019975 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.030073 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.030111 1 11 Rx d 8 4B 49 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.040047 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.050030 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.060044 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.060181 1 11 Rx d 8 DE 49 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.060194 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 1.060223 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.060283 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.080019 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.089998 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.090034 1 11 Rx d 8 68 4A 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.100056 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.110002 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.110041 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 1.110098 1 10 Rx d 8 37 39 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 1.110132 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.110145 1 11 Rx d 8 EA 4A 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.110155 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.130015 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.140014 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.150012 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.150061 1 11 Rx d 8 62 4B D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.150160 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.150173 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.150238 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 1.169969 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.179968 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.180014 1 11 Rx d 8 D1 4B DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.189955 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.200037 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.209998 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.210021 1 11 Rx d 8 37 4C F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.210151 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 1.210196 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.210251 1 10 Rx d 8 DE 3A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 1.220057 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.230061 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.240062 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.240099 1 11 Rx d 8 93 4C F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.250061 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.260053 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.260090 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.260113 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 1.270063 1 11 Rx d 8 E6 4C F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.270179 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.270193 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.289924 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.300009 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.300037 1 11 Rx d 8 2F 4D CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.309979 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.310113 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.310183 1 10 Rx d 8 78 3C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 1.310217 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 1.310249 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.329968 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.330021 1 11 Rx d 8 6F 4D 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.339962 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.349962 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.359966 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.359992 1 11 Rx d 8 A5 4D 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.360009 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 1.360074 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.370015 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.380032 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.390025 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.390058 1 11 Rx d 8 D1 4D 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.400054 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.400094 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.400116 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 1.400176 1 10 Rx d 8 06 3E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 1.400209 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.420035 1 11 Rx d 8 F4 4D 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.420068 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.430012 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.439963 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.449976 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.450009 1 11 Rx d 8 0C 4E D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.459963 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.459986 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.460057 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 1.469973 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.480036 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.480071 1 11 Rx d 8 1B 4E DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.489976 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.500035 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.509978 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.510010 1 11 Rx d 8 20 4E F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.510100 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 1.510131 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.510142 1 10 Rx d 8 86 3F 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 1.519982 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.530039 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.540022 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.540051 1 11 Rx d 8 1B 4E F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.550046 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.560021 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.560057 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.560181 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 1.570020 1 11 Rx d 8 0C 4E F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.570138 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.580022 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.590043 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.600024 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.600055 1 11 Rx d 8 F4 4D CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.610019 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.610179 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.610191 1 10 Rx d 8 F7 40 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 1.610217 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 1.620027 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.630024 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.630055 1 11 Rx d 8 D1 4D 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.640032 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.650055 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.650094 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.650119 1 11 Rx d 8 A5 4D 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.650135 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 1.650190 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.670103 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.680104 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.690059 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.690097 1 11 Rx d 8 6F 4D 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.690212 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.710033 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.710070 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 1.710139 1 10 Rx d 8 59 42 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 1.710189 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.720025 1 11 Rx d 8 2F 4D 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.720057 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.730027 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.730186 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.750028 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.750060 1 11 Rx d 8 E6 4C D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.760049 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.760080 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.760095 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 1.770032 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.780053 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.780087 1 11 Rx d 8 93 4C DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.790032 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.800013 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.810033 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.810068 1 11 Rx d 8 37 4C F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.810178 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 1.810211 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.810263 1 10 Rx d 8 AB 43 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 1.810292 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.830028 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.840037 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.840069 1 11 Rx d 8 D1 4B F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.850034 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.860036 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.860070 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.860091 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 1.870040 1 11 Rx d 8 62 4B F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.870173 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.880039 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.890037 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.900074 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.900111 1 11 Rx d 8 EA 4A CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.900134 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.900151 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.900246 1 10 Rx d 8 EB 44 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 1.900276 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 1.920089 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.930090 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.930127 1 11 Rx d 8 68 4A 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.940093 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.940132 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.960058 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.960096 1 11 Rx d 8 DE 49 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 1.960118 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 1.960187 1 12 Rx d 4 01 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 1.970038 1 64 Rx d 4 5C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.980044 1 64 Rx d 4 64 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.990002 1 64 Rx d 4 6C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 1.990149 1 11 Rx d 8 4B 49 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.000000 1 64 Rx d 4 74 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.010062 1 64 Rx d 4 7C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.010100 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 2.010143 1 10 Rx d 8 1A 46 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 2.010182 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.020003 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.020035 1 11 Rx d 8 AF 48 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.030002 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.040047 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.050003 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.050137 1 11 Rx d 8 0B 48 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.060035 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.060072 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.060166 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 2.070114 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.080034 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.080071 1 11 Rx d 8 5F 47 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.090050 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.100064 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.110044 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.110208 1 11 Rx d 8 AA 46 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.110260 1 10 Rx d 8 36 47 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 2.110289 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 2.110316 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.110327 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.130053 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.140028 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.140060 1 11 Rx d 8 EE 45 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.150070 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.150102 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.150125 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.150231 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 2.170064 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.170211 1 11 Rx d 8 29 45 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.180053 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.190065 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.200058 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.200095 1 11 Rx d 8 5D 44 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.210053 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.210089 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 2.210158 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.210227 1 10 Rx d 8 3F 48 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 2.220056 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.230054 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.230215 1 11 Rx d 8 8A 43 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.230267 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.250051 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.260053 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.260089 1 11 Rx d 8 AF 42 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.260111 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.260127 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 2.270057 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.280069 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.290077 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.290234 1 11 Rx d 8 CD 41 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.300073 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.310053 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.310096 1 10 Rx d 8 34 49 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 2.310167 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.310239 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 2.310268 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.310280 1 11 Rx d 8 E5 40 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.330060 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.340074 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.350083 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.350234 1 11 Rx d 8 F6 3F D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.350288 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.350300 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 2.350330 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.370081 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.380078 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.380119 1 11 Rx d 8 01 3F DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.390063 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.400062 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.400097 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.400120 1 11 Rx d 8 06 3E F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.400136 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.400236 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 2.400266 1 10 Rx d 8 14 4A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 2.420096 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.430063 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.440083 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.440116 1 11 Rx d 8 05 3D F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.440230 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.460066 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.460101 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 2.460172 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.470079 1 11 Rx d 8 FE 3B F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.470111 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.480072 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.480232 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.500067 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.500100 1 11 Rx d 8 F2 3A CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.510082 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.510117 1 10 Rx d 8 E0 4A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 2.510181 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 2.510237 1 12 Rx d 4 01 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.520174 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.520211 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.520234 1 11 Rx d 8 E2 39 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.540030 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.550072 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.560027 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.560060 1 11 Rx d 8 CC 38 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.560141 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.560196 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 2.560227 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.580070 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.590075 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.590111 1 11 Rx d 8 B2 37 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.600069 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.610070 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.610104 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 2.610168 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.610189 1 10 Rx d 8 96 4B 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 2.620075 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.620108 1 11 Rx d 8 93 36 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.630077 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.640065 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.650071 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.650103 1 11 Rx d 8 71 35 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.650124 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.650141 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.650240 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 2.670078 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.680091 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.680126 1 11 Rx d 8 4B 34 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.690092 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.690126 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.710092 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.710128 1 11 Rx d 8 22 33 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.710154 1 10 Rx d 8 37 4C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 2.710214 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 2.710259 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.720071 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.730075 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.730236 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.730256 1 11 Rx d 8 F6 31 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.750078 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.760095 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.760128 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.760150 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 2.770101 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.770139 1 11 Rx d 8 C7 30 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.770253 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.790118 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.800075 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.800113 1 11 Rx d 8 95 2F CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.810084 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.810116 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 2.810185 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.810272 1 10 Rx d 8 C1 4C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 2.810301 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.830087 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.830122 1 11 Rx d 8 62 2E 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.840105 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.850098 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.860076 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.860114 1 11 Rx d 8 2C 2D 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.860136 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.860153 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 2.870100 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.880131 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.890145 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.890182 1 11 Rx d 8 F5 2B 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.900088 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.900125 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.900150 1 10 Rx d 8 34 4D 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 2.900213 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.900283 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 2.920086 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.920229 1 11 Rx d 8 BD 2A 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.930105 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.940094 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.940128 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.940151 1 11 Rx d 8 84 29 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.960131 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.960153 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 2.960199 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 2.970084 1 64 Rx d 4 5C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.980091 1 64 Rx d 4 64 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 2.980263 1 11 Rx d 8 4A 28 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 2.980317 1 64 Rx d 4 6C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.000106 1 64 Rx d 4 74 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.010158 1 64 Rx d 4 7C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.010195 1 11 Rx d 8 10 27 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.010218 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.010234 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 3.010278 1 10 Rx d 8 91 4D 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 3.020129 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.030126 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.040128 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.040267 1 11 Rx d 8 D6 25 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.050098 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.060086 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.060119 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 3.060193 1 12 Rx d 4 01 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.060269 1 11 Rx d 8 9C 24 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.060280 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.080052 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.090013 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.100011 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.100139 1 11 Rx d 8 63 23 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.110014 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.110044 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 3.110104 1 10 Rx d 8 D7 4D 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 3.110134 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.120011 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.130110 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.130139 1 11 Rx d 8 2B 22 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.140015 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.150013 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.160153 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.160290 1 11 Rx d 8 F4 20 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.160342 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.160395 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 3.170068 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.180094 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.190101 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.190134 1 11 Rx d 8 BE 1F 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.190157 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.210102 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.210135 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.210157 1 10 Rx d 8 06 4E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 3.210223 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 3.220098 1 11 Rx d 8 8B 1E 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.220219 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.230103 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.230137 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.250083 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.250118 1 11 Rx d 8 59 1D D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.260090 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.260122 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 3.260194 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.270103 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.280074 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.280241 1 11 Rx d 8 2A 1C DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.290127 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.300105 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.310145 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.310178 1 11 Rx d 8 FE 1A F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.310201 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.310217 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 3.310282 1 10 Rx d 8 1D 4E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 3.310326 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.330117 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.340106 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.340272 1 11 Rx d 8 D5 19 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.350133 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.360104 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.360136 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 3.360206 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.370111 1 11 Rx d 8 AF 18 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.370143 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.380073 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.390099 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.400128 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.400301 1 11 Rx d 8 8D 17 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.400355 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.400367 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 3.400394 1 10 Rx d 8 1D 4E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 3.400422 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.420127 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.430111 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.430143 1 11 Rx d 8 6E 16 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.440133 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.440164 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.460133 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.460291 1 11 Rx d 8 54 15 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.460343 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.460396 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 3.470130 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.480128 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.480160 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.480184 1 11 Rx d 8 3E 14 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.500107 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.510123 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.510160 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.510183 1 10 Rx d 8 06 4E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 3.510246 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 3.520133 1 11 Rx d 8 2E 13 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.520256 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.520336 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.540135 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.550123 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.550154 1 11 Rx d 8 22 12 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.560132 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.560163 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 3.560234 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.560312 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.580132 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.580289 1 11 Rx d 8 1B 11 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.590114 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.600140 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.610137 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.610168 1 11 Rx d 8 1A 10 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.610191 1 12 Rx d 4 01 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.610209 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 3.610267 1 10 Rx d 8 D7 4D 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 3.620042 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.630075 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.640082 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.640213 1 11 Rx d 8 1F 0F F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.650082 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.650118 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.650142 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 3.650202 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.670166 1 11 Rx d 8 2A 0E F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.670202 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.680120 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.690128 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.690160 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.690184 1 11 Rx d 8 3B 0D CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.710127 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.710271 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 3.710304 1 10 Rx d 8 91 4D 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 3.710332 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.720163 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.730129 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.730166 1 11 Rx d 8 53 0C 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.730283 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.750126 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.760173 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.760210 1 11 Rx d 8 71 0B 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.760233 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.760249 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 3.770116 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.770268 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.790150 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.790184 1 11 Rx d 8 96 0A 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.800152 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.810126 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.810151 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.810225 1 10 Rx d 8 34 4D 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 3.810258 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 3.810288 1 11 Rx d 8 C3 09 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.810300 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.830121 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.840131 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.850150 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.850182 1 11 Rx d 8 F7 08 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.850304 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.850318 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 3.850348 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.870134 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.880155 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.880187 1 11 Rx d 8 32 08 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.890156 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.890314 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.900151 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.900184 1 11 Rx d 8 76 07 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.900206 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.900308 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 3.900337 1 10 Rx d 8 C1 4C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 3.920150 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.930141 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.940146 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.940177 1 11 Rx d 8 C1 06 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.940289 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.960155 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.960321 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 3.960353 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 3.970143 1 11 Rx d 8 15 06 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 3.970175 1 64 Rx d 4 5C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.980155 1 64 Rx d 4 64 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 3.980187 1 64 Rx d 4 6C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.000161 1 64 Rx d 4 74 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.000193 1 11 Rx d 8 71 05 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.010222 1 64 Rx d 4 7C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.010262 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 4.010319 1 10 Rx d 8 37 4C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 4.010350 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.020171 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.020337 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.020350 1 11 Rx d 8 D5 04 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.040172 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.050149 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.060159 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.060196 1 11 Rx d 8 42 04 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.060314 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.060367 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 4.060397 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.080148 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.090164 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.090198 1 11 Rx d 8 B8 03 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.100207 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.110147 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.110185 1 10 Rx d 8 96 4B 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 4.110267 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.110291 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 4.120175 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.120209 1 11 Rx d 8 36 03 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.130179 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.140158 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.140314 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.140327 1 11 Rx d 8 BE 02 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.150166 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.150201 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 4.150271 1 12 Rx d 4 01 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.170105 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.180168 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.180203 1 11 Rx d 8 4F 02 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.190075 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.190107 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.210114 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.210252 1 11 Rx d 8 E9 01 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.210265 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.210307 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 4.210336 1 10 Rx d 8 E0 4A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 4.220109 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.230170 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.230205 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.230222 1 11 Rx d 8 8D 01 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.250120 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.260111 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.260144 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 4.260213 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.270185 1 11 Rx d 8 3A 01 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.270317 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.270392 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.290125 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.300176 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.300208 1 11 Rx d 8 F1 00 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.310134 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.310165 1 10 Rx d 8 14 4A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 4.310231 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 4.310286 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.310347 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.330186 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.330343 1 11 Rx d 8 B1 00 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.340151 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.350181 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.360178 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.360210 1 11 Rx d 8 7B 00 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.360232 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.360249 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 4.370158 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.380181 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.390169 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.390287 1 11 Rx d 8 4F 00 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.390342 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.400204 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.400241 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 4.400310 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.400388 1 10 Rx d 8 34 49 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 4.420206 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.420243 1 11 Rx d 8 2C 00 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.430163 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.440152 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.440184 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.440208 1 11 Rx d 8 14 00 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.460180 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.460345 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.460388 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 4.470164 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.480182 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.480216 1 11 Rx d 8 05 00 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.480327 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.500107 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.510152 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.510187 1 11 Rx d 8 00 00 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.510205 1 10 Rx d 8 3F 48 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 4.510259 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 4.510290 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.520188 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.520347 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.540146 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.540175 1 11 Rx d 8 05 00 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.550167 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.560189 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.560224 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.560343 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 4.560373 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.560385 1 11 Rx d 8 14 00 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.580185 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.590144 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.600191 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.600219 1 11 Rx d 8 2C 00 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.600293 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.600307 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 4.600335 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.600346 1 10 Rx d 8 36 47 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 4.620191 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.630170 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.630202 1 11 Rx d 8 4F 00 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.640194 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.640338 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.650180 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.650215 1 11 Rx d 8 7B 00 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.650237 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.650349 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 4.670204 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.680196 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.690196 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.690216 1 11 Rx d 8 B1 00 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.690288 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.710150 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.710187 1 10 Rx d 8 1A 46 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 4.710208 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.710224 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 4.720179 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.720293 1 11 Rx d 8 F1 00 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.730133 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.730167 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.750198 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.750232 1 11 Rx d 8 3A 01 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.760140 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.760173 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 4.760240 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.770140 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.770174 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.770197 1 11 Rx d 8 8D 01 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.790183 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.800136 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.810198 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.810231 1 11 Rx d 8 E9 01 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.810306 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.810320 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 4.810345 1 10 Rx d 8 EB 44 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 4.810374 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.830181 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.840222 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.840254 1 11 Rx d 8 4F 02 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.850262 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.850399 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.850413 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 4.850442 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.870188 1 11 Rx d 8 BE 02 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.870319 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.880199 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.890186 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.900200 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.900232 1 11 Rx d 8 36 03 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.900255 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.900274 1 10 Rx d 8 AB 43 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 4.900333 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 4.900379 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.920200 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.930189 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.930226 1 11 Rx d 8 B8 03 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.930343 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.940195 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.960198 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.960231 1 11 Rx d 8 42 04 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 4.960253 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 4.960364 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 4.970188 1 64 Rx d 4 5C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.980200 1 64 Rx d 4 64 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.980360 1 64 Rx d 4 6C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 4.980377 1 11 Rx d 8 D5 04 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.000197 1 64 Rx d 4 74 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.010190 1 64 Rx d 4 7C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.010223 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 5.010289 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.010310 1 10 Rx d 8 59 42 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 5.020239 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.020276 1 11 Rx d 8 71 05 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.020367 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.040200 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.050214 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.050249 1 11 Rx d 8 15 06 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.060174 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.060206 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.060338 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 5.060374 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.080201 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.080233 1 11 Rx d 8 C1 06 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.090209 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.100201 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.100357 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.100370 1 11 Rx d 8 76 07 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.100377 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 5.100404 1 10 Rx d 8 F7 40 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 5.100433 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.120199 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.130213 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.140201 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.140232 1 11 Rx d 8 32 08 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.140341 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.150214 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.150251 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.150351 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 5.170256 1 11 Rx d 8 F7 08 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.170292 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.180234 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.190222 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.190259 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.190284 1 11 Rx d 8 C3 09 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.210201 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.210237 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.210344 1 10 Rx d 8 86 3F 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 5.210373 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 5.220196 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.230215 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.230373 1 11 Rx d 8 96 0A 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.230426 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.250211 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.260116 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.260139 1 11 Rx d 8 71 0B 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.260154 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 5.260198 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.270165 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.270200 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.290162 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.290299 1 11 Rx d 8 53 0C 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.300164 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.310223 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.310257 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 5.310301 1 10 Rx d 8 06 3E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 5.310340 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.310404 1 11 Rx d 8 3B 0D 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.310415 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.330168 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.340163 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.350165 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.350300 1 11 Rx d 8 2A 0E D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.350353 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.350365 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.350373 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 5.370212 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.380203 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.380237 1 11 Rx d 8 1F 0F DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.390213 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.400203 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.400242 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.400266 1 11 Rx d 8 1A 10 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.400283 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 5.400347 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.400410 1 10 Rx d 8 78 3C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 5.420202 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.430213 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.440203 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.440238 1 11 Rx d 8 1B 11 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.440352 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.460203 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.460238 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.460344 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 5.470230 1 11 Rx d 8 22 12 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.470257 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.480180 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.480355 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.500216 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.500252 1 11 Rx d 8 2E 13 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.510235 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.510269 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.510291 1 10 Rx d 8 DE 3A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 5.510349 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 5.510403 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.520203 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.520240 1 11 Rx d 8 3E 14 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.540232 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.550219 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.560240 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.560273 1 11 Rx d 8 54 15 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.560385 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 5.560417 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.560477 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.580241 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.590253 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.590290 1 11 Rx d 8 6E 16 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.600261 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.600398 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.600412 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 5.600439 1 10 Rx d 8 37 39 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 5.600467 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.620237 1 11 Rx d 8 8D 17 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.620368 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.630250 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.640286 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.640323 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.640346 1 11 Rx d 8 AF 18 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.650275 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.650312 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.650411 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 5.670301 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.680244 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.680281 1 11 Rx d 8 D5 19 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.680398 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.690241 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.710227 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.710266 1 11 Rx d 8 FE 1A F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.710289 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 5.710355 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.710408 1 10 Rx d 8 86 37 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 5.720247 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.730242 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.730400 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.730413 1 11 Rx d 8 2A 1C F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.750231 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.760248 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.760281 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.760303 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 5.770228 1 11 Rx d 8 59 1D F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.770350 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.770373 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.790239 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.800150 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.800188 1 11 Rx d 8 8B 1E CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.810189 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.810224 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.810337 1 10 Rx d 8 CB 35 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 5.810367 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 5.810396 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.830234 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.830270 1 11 Rx d 8 BE 1F 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.840299 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.850194 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.850341 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.850354 1 11 Rx d 8 F4 20 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.850362 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 5.850392 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.870185 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.880196 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.890274 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.890311 1 11 Rx d 8 2B 22 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.890425 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.900288 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.900326 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 5.900394 1 10 Rx d 8 07 34 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 5.900429 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.920258 1 11 Rx d 8 63 23 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.920295 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.930234 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.940252 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.940284 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.940307 1 11 Rx d 8 9C 24 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.960253 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.960285 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 5.960389 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 5.970257 1 64 Rx d 4 5C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.980257 1 64 Rx d 4 64 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 5.980424 1 11 Rx d 8 D6 25 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 5.980476 1 64 Rx d 4 6C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.000260 1 64 Rx d 4 74 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.010259 1 64 Rx d 4 7C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.010291 1 11 Rx d 8 10 27 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.010316 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 6.010375 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.010396 1 10 Rx d 8 3B 32 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 6.010437 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.020264 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.040254 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.040431 1 11 Rx d 8 4A 28 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.050267 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.060287 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.060329 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.060459 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 6.060490 1 11 Rx d 8 84 29 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.060501 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.080264 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.090264 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.100264 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.100426 1 11 Rx d 8 BD 2A CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.100478 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.100490 1 10 Rx d 8 69 30 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 6.100516 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.100527 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 6.120259 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.130240 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.130278 1 11 Rx d 8 F5 2B 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.140262 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.140294 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.150266 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.150298 1 11 Rx d 8 2C 2D 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.150320 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 6.150390 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.170276 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.180295 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.180332 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.180356 1 11 Rx d 8 62 2E 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.190293 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.210248 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.210286 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.210397 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 6.210437 1 10 Rx d 8 91 2E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 6.220267 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.220301 1 11 Rx d 8 95 2F 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.230228 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.230392 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.250275 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.250312 1 11 Rx d 8 C7 30 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.260277 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.260314 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 6.260390 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.260402 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.270300 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.270338 1 11 Rx d 8 F6 31 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.290288 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.300282 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.310272 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.310305 1 11 Rx d 8 22 33 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.310417 1 10 Rx d 8 B5 2C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 6.310449 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.310511 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 6.310539 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.330273 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.340288 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.340319 1 11 Rx d 8 4B 34 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.350201 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.350238 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.350258 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 6.350266 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.370218 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.370357 1 11 Rx d 8 71 35 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.380217 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.390218 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.400189 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.400212 1 11 Rx d 8 93 36 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.400228 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.400243 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.400303 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 6.400334 1 10 Rx d 8 D5 2A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 6.420281 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.430259 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.430419 1 11 Rx d 8 B2 37 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.430471 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.440281 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.460278 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.460316 1 11 Rx d 8 CC 38 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.460339 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 6.460410 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.470284 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.480284 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.480317 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.480340 1 11 Rx d 8 E2 39 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.500285 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.510262 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.510296 1 10 Rx d 8 F3 28 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 6.510359 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 6.510415 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.510427 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.510436 1 11 Rx d 8 F2 3A 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.520278 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.540273 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.550261 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.550299 1 11 Rx d 8 FE 3B D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.560258 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.560405 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.560469 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 6.560500 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.580285 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.580323 1 11 Rx d 8 05 3D DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.590290 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.600283 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.600320 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.600343 1 11 Rx d 8 06 3E F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.600359 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 6.600416 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.600427 1 10 Rx d 8 10 27 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 6.620260 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.630273 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.640290 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.640326 1 11 Rx d 8 01 3F F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.640439 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.650286 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.650319 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.650449 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 6.670354 1 11 Rx d 8 F6 3F F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.670389 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.680295 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.680459 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.690274 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.690306 1 11 Rx d 8 E5 40 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.710273 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.710311 1 10 Rx d 8 2D 25 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 6.710380 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.710451 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 6.720295 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.730276 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.730308 1 11 Rx d 8 CD 41 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.730420 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.750294 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.760296 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.760330 1 11 Rx d 8 AF 42 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.760352 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 6.760414 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.760434 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.770281 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.790280 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.790318 1 11 Rx d 8 8A 43 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.800301 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.810282 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.810447 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.810510 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 6.810539 1 10 Rx d 8 4B 23 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 6.810567 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.810579 1 11 Rx d 8 5D 44 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.830296 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.840298 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.850278 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.850309 1 11 Rx d 8 29 45 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.850404 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.850417 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 6.850450 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.870283 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.870861 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.870893 1 11 Rx d 8 EE 45 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.890260 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.890295 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.900196 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.900215 1 11 Rx d 8 AA 46 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.900225 1 10 Rx d 8 6B 21 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 6.900260 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.900326 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 6.920201 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.930203 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.930351 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.930363 1 11 Rx d 8 5F 47 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.940201 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.960203 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.960231 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 6.960293 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 6.970202 1 64 Rx d 4 5C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.970229 1 11 Rx d 8 0B 48 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 6.980204 1 64 Rx d 4 64 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 6.980229 1 64 Rx d 4 6C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.000335 1 64 Rx d 4 74 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.000507 1 11 Rx d 8 AF 48 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.010287 1 64 Rx d 4 7C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.010320 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.010343 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 7.010400 1 10 Rx d 8 8F 1F 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 7.010457 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.020304 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.020339 1 11 Rx d 8 4B 49 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.040311 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.050320 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.060309 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.060477 1 11 Rx d 8 DE 49 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.060529 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 7.060560 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.060620 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.080310 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.090297 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.090329 1 11 Rx d 8 68 4A 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.100292 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.100312 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.100328 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 7.100387 1 10 Rx d 8 B7 1D 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 7.100419 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.120312 1 11 Rx d 8 EA 4A 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.120444 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.130326 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.140333 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.140370 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.140394 1 11 Rx d 8 62 4B D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.150338 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.150376 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.150480 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 7.170318 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.180302 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.180442 1 11 Rx d 8 D1 4B DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.180498 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.190295 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.210313 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.210347 1 11 Rx d 8 37 4C F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.210370 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 7.210430 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.210497 1 10 Rx d 8 E5 1B 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 7.220304 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.230303 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.230336 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.230360 1 11 Rx d 8 93 4C F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.250319 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.260301 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.260335 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.260357 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 7.260418 1 11 Rx d 8 E6 4C F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.260497 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.270301 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.290306 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.300306 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.300341 1 11 Rx d 8 2F 4D CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.310325 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.310486 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.310548 1 10 Rx d 8 19 1A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 7.310577 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 7.310605 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.330337 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.330375 1 11 Rx d 8 6F 4D 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.340304 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.350330 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.350362 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.350386 1 11 Rx d 8 A5 4D 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.350404 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 7.350467 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.370323 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.380343 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.390331 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.390368 1 11 Rx d 8 D1 4D 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.390484 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.400313 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.400345 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 7.400406 1 10 Rx d 8 55 18 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 7.400462 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.420308 1 11 Rx d 8 F4 4D 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.420344 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.430307 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.430471 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.440263 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.440286 1 11 Rx d 8 0C 4E D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.460271 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.460305 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.460395 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 7.470270 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.480274 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.480307 1 11 Rx d 8 1B 4E DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.480398 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.500279 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.510321 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.510356 1 11 Rx d 8 20 4E F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.510371 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 7.510413 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.510426 1 10 Rx d 8 9A 16 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 7.510455 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.520273 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.540273 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.540306 1 11 Rx d 8 1B 4E F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.550279 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.560279 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.560427 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.560491 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 7.560522 1 11 Rx d 8 0C 4E F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.560534 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.580317 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.590327 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.600338 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.600371 1 11 Rx d 8 F4 4D CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.600492 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.600510 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.600518 1 10 Rx d 8 E9 14 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 7.600546 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 7.620313 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.630335 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.630369 1 11 Rx d 8 D1 4D 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.640340 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.640372 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.650332 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.650364 1 11 Rx d 8 A5 4D 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.650386 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 7.650450 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.670400 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.680343 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.680511 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.680525 1 11 Rx d 8 6F 4D 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.690328 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.710318 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.710351 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101
+ 7.710419 1 10 Rx d 8 42 13 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 7.710479 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.720345 1 11 Rx d 8 2F 4D 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.720376 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.720401 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.730346 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.750372 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.750536 1 11 Rx d 8 E6 4C D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.760370 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.760408 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.760431 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 7.760479 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.770375 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.770413 1 11 Rx d 8 93 4C DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.790333 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.800344 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.810334 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.810496 1 11 Rx d 8 37 4C F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.810548 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101
+ 7.810577 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.810629 1 10 Rx d 8 A8 11 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 7.810658 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.830335 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.840350 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.840382 1 11 Rx d 8 D1 4B F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.850350 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.850381 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.850410 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.850429 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102
+ 7.870348 1 11 Rx d 8 62 4B F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.870470 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.880353 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.890353 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.890391 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.890416 1 11 Rx d 8 EA 4A CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.900349 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.900381 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
+ 7.900501 1 10 Rx d 8 1A 10 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16
+ 7.900533 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101
+ 7.920354 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.930328 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.930488 1 11 Rx d 8 68 4A 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.930546 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.940352 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.960354 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100
+ 7.960389 1 11 Rx d 8 DE 49 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17
+ 7.960411 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102
+ 7.960498 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18
diff --git a/test/data/issue_1299.asc b/test/data/issue_1299.asc
new file mode 100644
index 000000000..43c29302e
--- /dev/null
+++ b/test/data/issue_1299.asc
@@ -0,0 +1,11 @@
+date Thu Apr 28 10:44:52.480 am 2022
+base hex timestamps absolute
+internal events logged
+// version 12.0.0
+Begin TriggerBlock Thu Apr 28 10:44:52.480 am 2022
+ 0.000000 Start of measurement
+ 13.258199 1 180 Tx d 8 6A 00 00 00 00 00 00 00 Length = 244016 BitCount = 125 ID = 384
+ 13.258433 1 221 Tx d 8 C2 4A 05 81 00 00 15 10 Length = 228016 BitCount = 117 ID = 545
+ 13.258671 1 3FF Tx d D 55 AA 01 02 03 04 05 06 Length = 232016 BitCount = 119 ID = 1023
+ 13.258907 1 F4 Tx d 8 8A 1A 0D F2 13 00 00 07 Length = 230016 BitCount = 118 ID = 244
+End TriggerBlock
diff --git a/test/data/issue_1905.blf b/test/data/issue_1905.blf
new file mode 100644
index 000000000..a896a6d7c
Binary files /dev/null and b/test/data/issue_1905.blf differ
diff --git a/test/data/logfile.asc b/test/data/logfile.asc
index 4b7c64363..13274a4c4 100644
--- a/test/data/logfile.asc
+++ b/test/data/logfile.asc
@@ -1,18 +1,39 @@
-date Sam Sep 30 15:06:13.191 2017
-base hex timestamps absolute
-internal events logged
-// version 9.0.0
-Begin Triggerblock Sam Sep 30 15:06:13.191 2017
- 0.000000 Start of measurement
- 0.015991 CAN 1 Status:chip status error passive - TxErr: 132 RxErr: 0
- 0.015991 CAN 2 Status:chip status error active
- 1.015991 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
- 1.015991 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
- 2.015992 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
- 17.876707 CAN 1 Status:chip status error passive - TxErr: 131 RxErr: 0
- 17.876708 1 6F9 Rx d 8 05 0C 00 00 00 00 00 00 Length = 240015 BitCount = 124 ID = 1785
- 17.876976 1 6F8 Rx d 8 FF 00 0C FE 00 00 00 00 Length = 239910 BitCount = 124 ID = 1784
- 18.015997 1 Statistic: D 2 R 0 XD 0 XR 0 E 0 O 0 B 0.04%
- 113.016026 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
- 113.016026 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
-End TriggerBlock
+date Sam Sep 30 15:06:13.191 2017
+base hex timestamps absolute
+internal events logged
+// version 9.0.0
+//0.000000 previous log file: logfile_errorframes.asc
+Begin Triggerblock Sam Sep 30 15:06:13.191 2017
+ 0.000000 Start of measurement
+ 0.015991 CAN 1 Status:chip status error passive - TxErr: 132 RxErr: 0
+ 0.015991 CAN 2 Status:chip status error active
+ 1.015991 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
+ 1.015991 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
+ 2.015992 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
+ 2.501000 1 ErrorFrame
+ 2.501010 1 ErrorFrame ECC: 10100010
+ 2.501020 2 ErrorFrame Flags = 0xe CodeExt = 0x20a2 Code = 0x82 ID = 0 DLC = 0 Position = 5 Length = 11300
+ 2.510001 2 100 Tx r
+ 2.520002 3 200 Tx r Length = 1704000 BitCount = 145 ID = 88888888x
+ 2.584921 4 300 Tx r 8 Length = 1704000 BitCount = 145 ID = 88888888x
+ 3.098426 1 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273910 BitCount = 141 ID = 418119424x
+ 3.148421 1 18EBFF00x Rx d 8 02 1F DE 80 25 DF C0 2B Length = 271910 BitCount = 140 ID = 418119424x
+ 3.197693 1 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x
+ 3.248765 1 18EBFF00x Rx d 8 04 00 4B FF FF FF FF FF Length = 283910 BitCount = 146 ID = 418119424x
+ 3.297743 1 J1939TP FEE3p 6 0 0 - Rx d 23 A0 0F A6 60 3B D1 40 1F DE 80 25 DF C0 2B E1 00 4B FF FF 3C 0F 00 4B FF FF FF FF FF FF FF FF FF FF FF FF
+ 17.876707 CAN 1 Status:chip status error passive - TxErr: 131 RxErr: 0
+ 17.876708 1 6F9 Rx d 8 05 0C 00 00 00 00 00 00 Length = 240015 BitCount = 124 ID = 1785
+ 17.876976 1 6F8 Rx d 8 FF 00 0C FE 00 00 00 00 Length = 239910 BitCount = 124 ID = 1784
+ 18.015997 1 Statistic: D 2 R 0 XD 0 XR 0 E 0 O 0 B 0.04%
+ 20.105214 2 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273925 BitCount = 141 ID = 418119424x
+ 20.155119 2 18EBFF00x Rx d 8 02 1F DE 80 25 DF C0 2B Length = 272152 BitCount = 140 ID = 418119424x
+ 20.204671 2 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x
+ 20.248887 2 18EBFF00x Rx d 8 04 00 4B FF FF FF FF FF Length = 283925 BitCount = 146 ID = 418119424x
+ 20.305233 2 J1939TP FEE3p 6 0 0 - Rx d 23 A0 0F A6 60 3B D1 40 1F DE 80 25 DF C0 2B E1 00 4B FF FF 3C 0F 00 4B FF FF FF FF FF FF FF FF FF FF FF FF
+ 30.005071 CANFD 2 Rx 300 Generic_Name_12 1 0 8 8 01 02 03 04 05 06 07 08 102203 133 303000 e0006659 46500250 4b140250 20011736 2001040d
+ 30.300981 CANFD 3 Tx 50005x 0 0 5 0 140000 73 200050 7a60 46500250 460a0250 20011736 20010205
+ 30.506898 CANFD 4 Rx 4EE 0 0 f 64 01 02 03 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 64 1331984 11 0 46500250 460a0250 20011736 20010205
+ 30.806898 CANFD 5 Tx ErrorFrame Not Acknowledge error, dominant error flag fffe c7 31ca Arb. 556 44 0 0 f 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1331984 11 0 46500250 460a0250 20011736 20010205
+ 113.016026 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
+ 113.016026 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00%
+End TriggerBlock
diff --git a/test/data/logfile.blf b/test/data/logfile.blf
deleted file mode 100644
index 98cafe214..000000000
Binary files a/test/data/logfile.blf and /dev/null differ
diff --git a/test/data/logfile_errorframes.asc b/test/data/logfile_errorframes.asc
new file mode 100644
index 000000000..6f751bfac
--- /dev/null
+++ b/test/data/logfile_errorframes.asc
@@ -0,0 +1,21 @@
+date Sam Sep 30 15:06:13.191 2017
+base hex timestamps absolute
+internal events logged
+// version 9.0.0
+Begin Triggerblock Sam Sep 30 15:06:13.191 2017
+ 0.000000 Start of measurement
+ 0.015991 CAN 1 Status:chip status error passive - TxErr: 132 RxErr: 0
+ 0.015991 CAN 2 Status:chip status error active
+ 2.501000 1 ErrorFrame
+ 2.501010 1 ErrorFrame ECC: 10100010
+ 2.501020 2 ErrorFrame Flags = 0xe CodeExt = 0x20a2 Code = 0x82 ID = 0 DLC = 0 Position = 5 Length = 11300
+ 2.520002 3 200 Tx r Length = 1704000 BitCount = 145 ID = 88888888x
+ 2.584921 4 300 Tx r 8 Length = 1704000 BitCount = 145 ID = 88888888x
+ 3.098426 1 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273910 BitCount = 141 ID = 418119424x
+ 3.197693 1 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x
+ 17.876976 1 6F8 Rx d 8 FF 00 0C FE 00 00 00 00 Length = 239910 BitCount = 124 ID = 1784
+ 20.105214 2 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273925 BitCount = 141 ID = 418119424x
+ 20.155119 2 18EBFF00x Rx d 8 02 1F DE 80 25 DF C0 2B Length = 272152 BitCount = 140 ID = 418119424x
+ 20.204671 2 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x
+ 20.248887 2 18EBFF00x Rx d 8 04 00 4B FF FF FF FF FF Length = 283925 BitCount = 146 ID = 418119424x
+End TriggerBlock
diff --git a/test/data/single_frame.asc b/test/data/single_frame.asc
new file mode 100644
index 000000000..cae9d1b4d
--- /dev/null
+++ b/test/data/single_frame.asc
@@ -0,0 +1,7 @@
+date Sat Sep 30 15:06:13.191 2017
+base hex timestamps absolute
+internal events logged
+Begin Triggerblock Sat Sep 30 15:06:13.191 2017
+ 0.000000 Start of measurement
+ 0.000000 1 123x Rx d 40 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F
+End TriggerBlock
diff --git a/test/data/single_frame_us_locale.asc b/test/data/single_frame_us_locale.asc
new file mode 100644
index 000000000..f6bfcc3db
--- /dev/null
+++ b/test/data/single_frame_us_locale.asc
@@ -0,0 +1,7 @@
+date Sat Sep 30 15:06:13.191 2017
+base hex timestamps absolute
+internal events logged
+Begin Triggerblock Sat Sep 30 15:06:13.191 2017
+ 0.000000 Start of measurement
+ 0.000000 1 123x Rx d 1 68
+End TriggerBlock
diff --git a/test/data/test_CanErrorFrameExt.blf b/test/data/test_CanErrorFrameExt.blf
new file mode 100644
index 000000000..f7ea6eb35
Binary files /dev/null and b/test/data/test_CanErrorFrameExt.blf differ
diff --git a/test/data/test_CanErrorFrames.asc b/test/data/test_CanErrorFrames.asc
new file mode 100644
index 000000000..0d400c18c
--- /dev/null
+++ b/test/data/test_CanErrorFrames.asc
@@ -0,0 +1,11 @@
+date Sam Sep 30 15:06:13.191 2017
+base hex timestamps absolute
+internal events logged
+// version 9.0.0
+Begin Triggerblock Sam Sep 30 15:06:13.191 2017
+ 0.000000 Start of measurement
+ 2.501000 1 ErrorFrame
+ 3.501000 1 ErrorFrame ECC: 10100010
+ 4.501000 2 ErrorFrame Flags = 0xe CodeExt = 0x20a2 Code = 0x82 ID = 0 DLC = 0 Position = 5 Length = 11300
+ 30.806898 CANFD 5 Tx ErrorFrame Not Acknowledge error, dominant error flag fffe c7 31ca Arb. 556 44 0 0 f 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1331984 11 0 46500250 460a0250 20011736 20010205
+End TriggerBlock
diff --git a/test/data/test_CanFdMessage.asc b/test/data/test_CanFdMessage.asc
new file mode 100644
index 000000000..51fbcb1ce
--- /dev/null
+++ b/test/data/test_CanFdMessage.asc
@@ -0,0 +1,10 @@
+date Sam Sep 30 15:06:13.191 2017
+base hex timestamps absolute
+internal events logged
+// version 9.0.0
+Begin Triggerblock Sam Sep 30 15:06:13.191 2017
+ 0.000000 Start of measurement
+ 30.005021 CANFD 1 Rx 300 1 0 8 8 11 c2 03 04 05 06 07 08 102203 133 303000 e0006659 46500250 4b140250 20011736 2001040d
+ 30.005041 CANFD 2 Tx 1C4D80A7x 0 1 8 8 12 c2 03 04 05 06 07 08 102203 133 303000 e0006659 46500250 4b140250 20011736 2001040d
+ 30.005071 CANFD 3 Rx 30a Generic_Name_12 1 1 8 8 01 02 03 04 05 06 07 08 102203 133 303000 e0006659 46500250 4b140250 20011736 2001040d
+End TriggerBlock
diff --git a/test/data/test_CanFdMessage.blf b/test/data/test_CanFdMessage.blf
new file mode 100644
index 000000000..55b48bfe4
Binary files /dev/null and b/test/data/test_CanFdMessage.blf differ
diff --git a/test/data/test_CanFdMessage64.asc b/test/data/test_CanFdMessage64.asc
new file mode 100644
index 000000000..ab34ee7ae
--- /dev/null
+++ b/test/data/test_CanFdMessage64.asc
@@ -0,0 +1,9 @@
+date Sam Sep 30 15:06:13.191 2017
+base hex timestamps absolute
+internal events logged
+// version 9.0.0
+Begin Triggerblock Sam Sep 30 15:06:13.191 2017
+ 0.000000 Start of measurement
+ 30.506898 CANFD 4 Rx 4EE 0 1 f 64 A1 02 03 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 64 1331984 11 0 46500250 460a0250 20011736 20010205
+ 31.506898 CANFD 4 Rx 1C4D80A7x AlphaNumericName_2 1 0 f 64 b1 02 03 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 64 1331984 11 0 46500250 460a0250 20011736 20010205
+End TriggerBlock
diff --git a/test/data/test_CanFdMessage64.blf b/test/data/test_CanFdMessage64.blf
new file mode 100644
index 000000000..f26eccfde
Binary files /dev/null and b/test/data/test_CanFdMessage64.blf differ
diff --git a/test/data/test_CanFdRemoteMessage.asc b/test/data/test_CanFdRemoteMessage.asc
new file mode 100644
index 000000000..7c78f5d06
--- /dev/null
+++ b/test/data/test_CanFdRemoteMessage.asc
@@ -0,0 +1,8 @@
+date Sam Sep 30 15:06:13.191 2017
+base hex timestamps absolute
+internal events logged
+// version 9.0.0
+Begin Triggerblock Sam Sep 30 15:06:13.191 2017
+ 0.000000 Start of measurement
+ 30.300981 CANFD 3 Tx 50005x 0 1 5 0 140000 73 200050 7a60 46500250 460a0250 20011736 20010205
+End TriggerBlock
diff --git a/test/data/test_CanMessage.asc b/test/data/test_CanMessage.asc
new file mode 100644
index 000000000..881f16132
--- /dev/null
+++ b/test/data/test_CanMessage.asc
@@ -0,0 +1,9 @@
+date Sam Sep 30 15:06:13.191 2017
+base hex timestamps absolute
+internal events logged
+// version 9.0.0
+Begin Triggerblock Sat Sep 30 10:06:13.191 PM 2017
+ 0.000000 Start of measurement
+ 2.5010 2 C8 Tx d 8 09 08 07 06 05 04 03 02
+ 17.876708 1 6F9 Rx d 8 05 0C 00 00 00 00 00 00 Length = 240015 BitCount = 124 ID = 1785
+End TriggerBlock
diff --git a/test/data/test_CanMessage.asc.gz b/test/data/test_CanMessage.asc.gz
new file mode 100644
index 000000000..25375d1b8
Binary files /dev/null and b/test/data/test_CanMessage.asc.gz differ
diff --git a/test/data/test_CanMessage.blf b/test/data/test_CanMessage.blf
new file mode 100644
index 000000000..f0ad20365
Binary files /dev/null and b/test/data/test_CanMessage.blf differ
diff --git a/test/data/test_CanMessage.trc b/test/data/test_CanMessage.trc
new file mode 100644
index 000000000..8b1361808
--- /dev/null
+++ b/test/data/test_CanMessage.trc
@@ -0,0 +1,23 @@
+;$FILEVERSION=2.1
+;$STARTTIME=43008.920986006946
+;$COLUMNS=N,O,T,B,I,d,R,L,D
+;
+; C:\Users\User\Desktop\python-can\test\data\test_CanMessage.trc
+; Start time: 30.09.2017 22:06:13.191.000
+; Generated by PEAK-Converter Version 2.2.4.136
+; Data imported from C:\Users\User\Desktop\python-can\test\data\test_CanMessage.asc
+;-------------------------------------------------------------------------------
+; Bus Name Connection Protocol
+; N/A N/A N/A N/A
+;-------------------------------------------------------------------------------
+; Message Time Type ID Rx/Tx
+; Number Offset | Bus [hex] | Reserved
+; | [ms] | | | | | Data Length Code
+; | | | | | | | | Data [hex] ...
+; | | | | | | | | |
+;---+-- ------+------ +- +- --+----- +- +- +--- +- -- -- -- -- -- -- --
+;Begin Triggerblock Sat Sep 30 10:06:13.191 PM 2017
+; 0.000000 Start of measurement
+ 1 2501.000 DT 2 00C8 Tx - 8 09 08 07 06 05 04 03 02
+ 2 17876.708 DT 1 06F9 Rx - 8 05 0C 00 00 00 00 00 00
+;End TriggerBlock
diff --git a/test/data/test_CanMessage2.blf b/test/data/test_CanMessage2.blf
new file mode 100644
index 000000000..6beb4fa43
Binary files /dev/null and b/test/data/test_CanMessage2.blf differ
diff --git a/test/data/test_CanMessage_V1_0_BUS1.trc b/test/data/test_CanMessage_V1_0_BUS1.trc
new file mode 100644
index 000000000..c3905ae47
--- /dev/null
+++ b/test/data/test_CanMessage_V1_0_BUS1.trc
@@ -0,0 +1,29 @@
+;##########################################################################
+; C:\NewFileName_BUS1.trc
+;
+; CAN activities imported from C:\test_CanMessage_V1_1.trc
+; Start time: 18.12.2021 14:28:07.062
+; PCAN-Net: N/A
+; Generated by PEAK-Converter Version 3.0.4.594
+;
+; Columns description:
+; ~~~~~~~~~~~~~~~~~~~~~
+; +-current number in actual sample
+; | +time offset of message (ms)
+; | | +ID of message (hex)
+; | | | +data length code
+; | | | | +data bytes (hex) ...
+; | | | | |
+;----+- ---+--- ----+--- + -+ -- -- ...
+ 1) 17535 00000100 8 00 00 00 00 00 00 00 00
+ 2) 17540 FFFFFFFF 4 00 00 00 08 -- -- -- -- BUSHEAVY
+ 3) 17700 00000100 8 00 00 00 00 00 00 00 00
+ 4) 17873 00000100 8 00 00 00 00 00 00 00 00
+ 5) 19295 0000 8 00 00 00 00 00 00 00 00
+ 6) 19500 0000 8 00 00 00 00 00 00 00 00
+ 7) 19705 0000 8 00 00 00 00 00 00 00 00
+ 8) 20592 00000100 8 00 00 00 00 00 00 00 00
+ 9) 20798 00000100 8 00 00 00 00 00 00 00 00
+ 10) 20956 00000100 8 00 00 00 00 00 00 00 00
+ 11) 21097 00000100 8 00 00 00 00 00 00 00 00
+ 12) 48937 0704 1 RTR
\ No newline at end of file
diff --git a/test/data/test_CanMessage_V1_1.trc b/test/data/test_CanMessage_V1_1.trc
new file mode 100644
index 000000000..9a9cc7574
--- /dev/null
+++ b/test/data/test_CanMessage_V1_1.trc
@@ -0,0 +1,26 @@
+;$FILEVERSION=1.1
+;$STARTTIME=44548.6028595139
+;
+; Start time: 18.12.2021 14:28:07.062.0
+; Generated by PCAN-View v5.0.0.814
+;
+; Message Number
+; | Time Offset (ms)
+; | | Type
+; | | | ID (hex)
+; | | | | Data Length
+; | | | | | Data Bytes (hex) ...
+; | | | | | |
+;---+-- ----+---- --+-- ----+--- + -+ -- -- -- -- -- -- --
+ 1) 17535.4 Tx 00000100 8 00 00 00 00 00 00 00 00
+ 2) 17540.3 Warng FFFFFFFF 4 00 00 00 08 BUSHEAVY
+ 3) 17700.3 Tx 00000100 8 00 00 00 00 00 00 00 00
+ 4) 17873.8 Tx 00000100 8 00 00 00 00 00 00 00 00
+ 5) 19295.4 Tx 0000 8 00 00 00 00 00 00 00 00
+ 6) 19500.6 Tx 0000 8 00 00 00 00 00 00 00 00
+ 7) 19705.2 Tx 0000 8 00 00 00 00 00 00 00 00
+ 8) 20592.7 Tx 00000100 8 00 00 00 00 00 00 00 00
+ 9) 20798.6 Tx 00000100 8 00 00 00 00 00 00 00 00
+ 10) 20956.0 Tx 00000100 8 00 00 00 00 00 00 00 00
+ 11) 21097.1 Tx 00000100 8 00 00 00 00 00 00 00 00
+ 12) 48937.6 Rx 0704 1 RTR
diff --git a/test/data/test_CanMessage_V1_3.trc b/test/data/test_CanMessage_V1_3.trc
new file mode 100644
index 000000000..96db1748c
--- /dev/null
+++ b/test/data/test_CanMessage_V1_3.trc
@@ -0,0 +1,35 @@
+;$FILEVERSION=1.3
+;$STARTTIME=44548.6028595139
+; C:\NewFileName_V1_3.trc
+;
+; Start time: 18.12.2021 14:28:07.062.1
+;
+; Generated by PEAK-Converter Version 3.0.4.594
+; Data imported from C:\test_CanMessage_V1_1.trc
+;-------------------------------------------------------------------------------
+; Bus Name Connection Protocol
+; N/A N/A N/A CAN
+;-------------------------------------------------------------------------------
+; Message Number
+; | Time Offset (ms)
+; | | Bus
+; | | | Type
+; | | | | ID (hex)
+; | | | | | Reserved
+; | | | | | | Data Length Code
+; | | | | | | | Data Bytes (hex) ...
+; | | | | | | | |
+; | | | | | | | |
+;---+-- ------+------ +- --+-- ----+--- +- -+-- -+ -- -- -- -- -- -- --
+ 1) 17535.400 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00
+ 2) 17540.300 1 Warng FFFFFFFF - 4 00 00 00 08 BUSHEAVY
+ 3) 17700.300 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00
+ 4) 17873.800 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00
+ 5) 19295.400 1 Tx 0000 - 8 00 00 00 00 00 00 00 00
+ 6) 19500.600 1 Tx 0000 - 8 00 00 00 00 00 00 00 00
+ 7) 19705.200 1 Tx 0000 - 8 00 00 00 00 00 00 00 00
+ 8) 20592.700 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00
+ 9) 20798.600 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00
+ 10) 20956.000 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00
+ 11) 21097.100 1 Tx 00000100 - 8 00 00 00 00 00 00 00 00
+ 12) 48937.600 1 Rx 0704 - 1 RTR
diff --git a/test/data/test_CanMessage_V2_0_BUS1.trc b/test/data/test_CanMessage_V2_0_BUS1.trc
new file mode 100644
index 000000000..c1af8abc1
--- /dev/null
+++ b/test/data/test_CanMessage_V2_0_BUS1.trc
@@ -0,0 +1,29 @@
+;$FILEVERSION=2.0
+;$STARTTIME=44548.6028595139
+;$COLUMNS=N,O,T,I,d,l,D
+;
+; C:\test_CanMessage_V2_0_BUS1.trc
+; Start time: 18.12.2021 14:28:07.062.001
+; Generated by PEAK-Converter Version 3.0.4.594
+; Data imported from C:\test_CanMessage_V1_1.trc
+;-------------------------------------------------------------------------------
+; Connection Bit rate
+; N/A N/A
+;-------------------------------------------------------------------------------
+; Message Time Type ID Rx/Tx
+; Number Offset | [hex] | Data Length
+; | [ms] | | | | Data [hex] ...
+; | | | | | | |
+;---+-- ------+------ +- --+----- +- +- +- -- -- -- -- -- -- --
+ 1 17535.400 DT 00000100 Tx 8 00 00 00 00 00 00 00 00
+ 2 17540.300 ST Rx 00 00 00 08
+ 3 17700.300 DT 00000100 Tx 8 00 00 00 00 00 00 00 00
+ 4 17873.800 DT 00000100 Tx 8 00 00 00 00 00 00 00 00
+ 5 19295.400 DT 0000 Tx 8 00 00 00 00 00 00 00 00
+ 6 19500.600 DT 0000 Tx 8 00 00 00 00 00 00 00 00
+ 7 19705.200 DT 0000 Tx 8 00 00 00 00 00 00 00 00
+ 8 20592.700 DT 00000100 Tx 8 00 00 00 00 00 00 00 00
+ 9 20798.600 DT 00000100 Tx 8 00 00 00 00 00 00 00 00
+ 10 20956.000 DT 00000100 Tx 8 00 00 00 00 00 00 00 00
+ 11 21097.100 DT 00000100 Tx 8 00 00 00 00 00 00 00 00
+ 12 48937.600 RR 0704 Rx 1
\ No newline at end of file
diff --git a/test/data/test_CanMessage_V2_1.trc b/test/data/test_CanMessage_V2_1.trc
new file mode 100644
index 000000000..0d259f084
--- /dev/null
+++ b/test/data/test_CanMessage_V2_1.trc
@@ -0,0 +1,30 @@
+;$FILEVERSION=2.1
+;$STARTTIME=44548.6028595139
+;$COLUMNS=N,O,T,B,I,d,R,L,D
+;
+; C:\test_CanMessage_V2_1.trc
+; Start time: 18.12.2021 14:28:07.062.001
+; Generated by PEAK-Converter Version 3.0.4.594
+; Data imported from C:\test_CanMessage_V1_1.trc
+;-------------------------------------------------------------------------------
+; Bus Name Connection Protocol
+; N/A N/A N/A N/A
+;-------------------------------------------------------------------------------
+; Message Time Type ID Rx/Tx
+; Number Offset | Bus [hex] | Reserved
+; | [ms] | | | | | Data Length Code
+; | | | | | | | | Data [hex] ...
+; | | | | | | | | |
+;---+-- ------+------ +- +- --+----- +- +- +--- +- -- -- -- -- -- -- --
+ 1 17535.400 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00
+ 2 17540.300 ST 1 - Rx - 4 00 00 00 08
+ 3 17700.300 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00
+ 4 17873.800 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00
+ 5 19295.400 DT 1 0000 Tx - 8 00 00 00 00 00 00 00 00
+ 6 19500.600 DT 1 0000 Tx - 8 00 00 00 00 00 00 00 00
+ 7 19705.200 DT 1 0000 Tx - 8 00 00 00 00 00 00 00 00
+ 8 20592.700 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00
+ 9 20798.600 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00
+ 10 20956.000 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00
+ 11 21097.100 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00
+ 12 48937.600 RR 1 0704 Rx - 1
\ No newline at end of file
diff --git a/test/data/test_CanRemoteMessage.asc b/test/data/test_CanRemoteMessage.asc
new file mode 100644
index 000000000..4e6431576
--- /dev/null
+++ b/test/data/test_CanRemoteMessage.asc
@@ -0,0 +1,10 @@
+date Sam Sep 30 15:06:13.191 2017
+base hex timestamps absolute
+internal events logged
+// version 9.0.0
+Begin Triggerblock Sam Sep 30 15:06:13.191 2017
+ 0.000000 Start of measurement
+ 2.510001 2 100 Rx r
+ 2.520002 3 200 Tx r Length = 1704000 BitCount = 145 ID = 88888888x
+ 2.584921 4 300 Rx r 8 Length = 1704000 BitCount = 145 ID = 88888888x
+End TriggerBlock
diff --git a/test/listener_test.py b/test/listener_test.py
index c25a6fb56..bbcbed56e 100644
--- a/test/listener_test.py
+++ b/test/listener_test.py
@@ -1,27 +1,19 @@
#!/usr/bin/env python
-# coding: utf-8
-"""
-"""
-
-from __future__ import absolute_import, print_function
-
-from time import sleep
-import unittest
-import random
+""" """
+import asyncio
import logging
-import tempfile
-import sqlite3
import os
-from os.path import join, dirname
+import random
+import tempfile
+import unittest
+import warnings
+from os.path import dirname, join
import can
from .data.example_data import generate_message
-channel = 'virtual_channel_0'
-can.rc['interface'] = 'virtual'
-
logging.basicConfig(level=logging.DEBUG)
# makes the random number generator deterministic
@@ -29,46 +21,48 @@
class ListenerImportTest(unittest.TestCase):
-
def testClassesImportable(self):
- self.assertTrue(hasattr(can, 'Listener'))
- self.assertTrue(hasattr(can, 'BufferedReader'))
- self.assertTrue(hasattr(can, 'Notifier'))
- self.assertTrue(hasattr(can, 'Logger'))
+ self.assertTrue(hasattr(can, "Listener"))
+ self.assertTrue(hasattr(can, "BufferedReader"))
+ self.assertTrue(hasattr(can, "Notifier"))
+ self.assertTrue(hasattr(can, "Logger"))
- self.assertTrue(hasattr(can, 'ASCWriter'))
- self.assertTrue(hasattr(can, 'ASCReader'))
+ self.assertTrue(hasattr(can, "ASCWriter"))
+ self.assertTrue(hasattr(can, "ASCReader"))
- self.assertTrue(hasattr(can, 'BLFReader'))
- self.assertTrue(hasattr(can, 'BLFWriter'))
+ self.assertTrue(hasattr(can, "BLFReader"))
+ self.assertTrue(hasattr(can, "BLFWriter"))
- self.assertTrue(hasattr(can, 'CSVReader'))
- self.assertTrue(hasattr(can, 'CSVWriter'))
+ self.assertTrue(hasattr(can, "CSVReader"))
+ self.assertTrue(hasattr(can, "CSVWriter"))
- self.assertTrue(hasattr(can, 'CanutilsLogWriter'))
- self.assertTrue(hasattr(can, 'CanutilsLogReader'))
+ self.assertTrue(hasattr(can, "CanutilsLogWriter"))
+ self.assertTrue(hasattr(can, "CanutilsLogReader"))
- self.assertTrue(hasattr(can, 'SqliteReader'))
- self.assertTrue(hasattr(can, 'SqliteWriter'))
+ self.assertTrue(hasattr(can, "SqliteReader"))
+ self.assertTrue(hasattr(can, "SqliteWriter"))
- self.assertTrue(hasattr(can, 'Printer'))
+ self.assertTrue(hasattr(can, "Printer"))
- self.assertTrue(hasattr(can, 'LogReader'))
+ self.assertTrue(hasattr(can, "LogReader"))
- self.assertTrue(hasattr(can, 'MessageSync'))
+ self.assertTrue(hasattr(can, "MessageSync"))
class BusTest(unittest.TestCase):
-
def setUp(self):
+ # Save all can.rc defaults
+ self._can_rc = can.rc
+ can.rc = {"interface": "virtual"}
self.bus = can.interface.Bus()
def tearDown(self):
self.bus.shutdown()
+ # Restore the defaults
+ can.rc = self._can_rc
class ListenerTest(BusTest):
-
def testBasicListenerCanBeAddedToNotifier(self):
a_listener = can.Printer()
notifier = can.Notifier(self.bus, [a_listener], 0.1)
@@ -93,14 +87,18 @@ def testRemoveListenerFromNotifier(self):
def testPlayerTypeResolution(self):
def test_filetype_to_instance(extension, klass):
- print("testing: {}".format(extension))
+ print(f"testing: {extension}")
try:
if extension == ".blf":
delete = False
- file_handler = open(join(dirname(__file__), "data/logfile.blf"))
+ file_handler = open(
+ join(dirname(__file__), "data", "test_CanMessage.blf")
+ )
else:
delete = True
- file_handler = tempfile.NamedTemporaryFile(suffix=extension, delete=False)
+ file_handler = tempfile.NamedTemporaryFile(
+ suffix=extension, delete=False
+ )
with file_handler as my_file:
filename = my_file.name
@@ -113,18 +111,22 @@ def test_filetype_to_instance(extension, klass):
test_filetype_to_instance(".asc", can.ASCReader)
test_filetype_to_instance(".blf", can.BLFReader)
test_filetype_to_instance(".csv", can.CSVReader)
- test_filetype_to_instance(".db" , can.SqliteReader)
+ test_filetype_to_instance(".db", can.SqliteReader)
test_filetype_to_instance(".log", can.CanutilsLogReader)
- # test file extensions that are not supported
- with self.assertRaisesRegexp(NotImplementedError, ".xyz_42"):
- test_filetype_to_instance(".xyz_42", can.Printer)
+ def testPlayerTypeResolutionUnsupportedFileTypes(self):
+ for should_fail_with in ["", ".", ".some_unknown_extention_42"]:
+ with self.assertRaises(ValueError):
+ with can.LogReader(should_fail_with): # make sure we close it anyways
+ pass
def testLoggerTypeResolution(self):
def test_filetype_to_instance(extension, klass):
- print("testing: {}".format(extension))
+ print(f"testing: {extension}")
try:
- with tempfile.NamedTemporaryFile(suffix=extension, delete=False) as my_file:
+ with tempfile.NamedTemporaryFile(
+ suffix=extension, delete=False
+ ) as my_file:
filename = my_file.name
with can.Logger(filename) as writer:
self.assertIsInstance(writer, klass)
@@ -134,17 +136,19 @@ def test_filetype_to_instance(extension, klass):
test_filetype_to_instance(".asc", can.ASCWriter)
test_filetype_to_instance(".blf", can.BLFWriter)
test_filetype_to_instance(".csv", can.CSVWriter)
- test_filetype_to_instance(".db" , can.SqliteWriter)
+ test_filetype_to_instance(".db", can.SqliteWriter)
test_filetype_to_instance(".log", can.CanutilsLogWriter)
test_filetype_to_instance(".txt", can.Printer)
- # test file extensions that should use a fallback
- test_filetype_to_instance("", can.Printer)
- test_filetype_to_instance(".", can.Printer)
- test_filetype_to_instance(".some_unknown_extention_42", can.Printer)
with can.Logger(None) as logger:
self.assertIsInstance(logger, can.Printer)
+ def testLoggerTypeResolutionUnsupportedFileTypes(self):
+ for should_fail_with in ["", ".", ".some_unknown_extention_42"]:
+ with self.assertRaises(ValueError):
+ with can.Logger(should_fail_with): # make sure we close it anyways
+ pass
+
def testBufferedListenerReceives(self):
a_listener = can.BufferedReader()
a_listener(generate_message(0xDADADA))
@@ -154,5 +158,22 @@ def testBufferedListenerReceives(self):
self.assertIsNotNone(a_listener.get_message(0.1))
-if __name__ == '__main__':
+def test_deprecated_loop_arg(recwarn):
+ try:
+ loop = asyncio.get_running_loop()
+ except RuntimeError:
+ loop = asyncio.new_event_loop()
+
+ warnings.simplefilter("always")
+ can.AsyncBufferedReader(loop=loop)
+ assert len(recwarn) > 0
+ assert recwarn.pop(DeprecationWarning)
+ recwarn.clear()
+
+ # assert that no warning is shown when loop argument is not used
+ can.AsyncBufferedReader()
+ assert len(recwarn) == 0
+
+
+if __name__ == "__main__":
unittest.main()
diff --git a/test/logformats_test.py b/test/logformats_test.py
index 2a315352d..f8a8de91d 100644
--- a/test/logformats_test.py
+++ b/test/logformats_test.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python
-# coding: utf-8
"""
This test module test the separate reader/writer combinations of the can.io.*
@@ -10,46 +9,114 @@
different writer/reader pairs - e.g., some don't handle error frames and
comments.
-TODO: implement CAN FD support testing
+TODO: correctly set preserves_channel and adds_default_channel
"""
-
-from __future__ import print_function, absolute_import, division
-
+import locale
import logging
-import unittest
-import tempfile
import os
-from abc import abstractmethod, ABCMeta
+import tempfile
+import unittest
+from abc import ABCMeta, abstractmethod
+from contextlib import contextmanager
+from datetime import datetime
+from itertools import zip_longest
+from pathlib import Path
+from unittest.mock import patch
-try:
- # Python 3
- from itertools import zip_longest
-except ImportError:
- # Python 2
- from itertools import izip_longest as zip_longest
+from parameterized import parameterized
import can
-
-from .data.example_data import TEST_MESSAGES_BASE, TEST_MESSAGES_REMOTE_FRAMES, \
- TEST_MESSAGES_ERROR_FRAMES, TEST_COMMENTS, \
- sort_messages
+from can.io import blf
+from .data.example_data import (
+ TEST_COMMENTS,
+ TEST_MESSAGES_BASE,
+ TEST_MESSAGES_CAN_FD,
+ TEST_MESSAGES_ERROR_FRAMES,
+ TEST_MESSAGES_REMOTE_FRAMES,
+ sort_messages,
+)
+from .message_helper import ComparingMessagesTestCase
logging.basicConfig(level=logging.DEBUG)
+try:
+ import asammdf
+except ModuleNotFoundError:
+ asammdf = None
+
+
+@contextmanager
+def override_locale(category: int, locale_str: str) -> None:
+ prev_locale = locale.getlocale(category)
+ locale.setlocale(category, locale_str)
+ yield
+ locale.setlocale(category, prev_locale)
+
+
+class ReaderWriterExtensionTest(unittest.TestCase):
+ def _get_suffix_case_variants(self, suffix):
+ return [
+ suffix.upper(),
+ suffix.lower(),
+ f"can.msg.ext{suffix}",
+ "".join([c.upper() if i % 2 else c for i, c in enumerate(suffix)]),
+ ]
+
+ def _test_extension(self, suffix):
+ WriterType = can.io.MESSAGE_WRITERS.get(suffix)
+ ReaderType = can.io.MESSAGE_READERS.get(suffix)
+ for suffix_variant in self._get_suffix_case_variants(suffix):
+ tmp_file = tempfile.NamedTemporaryFile(suffix=suffix_variant, delete=False)
+ tmp_file.close()
+ try:
+ if WriterType:
+ with can.Logger(tmp_file.name) as logger:
+ assert type(logger) == WriterType
+ if ReaderType:
+ with can.LogReader(tmp_file.name) as player:
+ assert type(player) == ReaderType
+ finally:
+ os.remove(tmp_file.name)
+
+ def test_extension_matching_asc(self):
+ self._test_extension(".asc")
+
+ def test_extension_matching_blf(self):
+ self._test_extension(".blf")
+
+ def test_extension_matching_csv(self):
+ self._test_extension(".csv")
+
+ def test_extension_matching_db(self):
+ self._test_extension(".db")
+
+ def test_extension_matching_log(self):
+ self._test_extension(".log")
+
+ def test_extension_matching_txt(self):
+ self._test_extension(".txt")
+
+ def test_extension_matching_mf4(self):
+ try:
+ self._test_extension(".mf4")
+ except NotImplementedError:
+ if asammdf is not None:
+ raise
+
-class ReaderWriterTest(unittest.TestCase):
+class ReaderWriterTest(unittest.TestCase, ComparingMessagesTestCase, metaclass=ABCMeta):
"""Tests a pair of writer and reader by writing all data first and
then reading all data and checking if they could be reconstructed
correctly. Optionally writes some comments as well.
+ .. note::
+ This class is prevented from being executed as a test
+ case itself by a *del* statement in at the end of the file.
+ (Source: `*Wojciech B.* on StackOverlfow `_)
"""
- __test__ = False
-
- __metaclass__ = ABCMeta
-
def __init__(self, *args, **kwargs):
- super(ReaderWriterTest, self).__init__(*args, **kwargs)
+ unittest.TestCase.__init__(self, *args, **kwargs)
self._setup_instance()
@abstractmethod
@@ -57,31 +124,48 @@ def _setup_instance(self):
"""Hook for subclasses."""
raise NotImplementedError()
- def _setup_instance_helper(self,
- writer_constructor, reader_constructor, binary_file=False,
- check_remote_frames=True, check_error_frames=True, check_comments=False,
- test_append=False, round_timestamps=False):
+ def _setup_instance_helper(
+ self,
+ writer_constructor,
+ reader_constructor,
+ binary_file=False,
+ check_remote_frames=True,
+ check_error_frames=True,
+ check_fd=True,
+ check_comments=False,
+ test_append=False,
+ allowed_timestamp_delta=0.0,
+ preserves_channel=True,
+ adds_default_channel=None,
+ assert_file_closed=True,
+ ):
"""
:param Callable writer_constructor: the constructor of the writer class
:param Callable reader_constructor: the constructor of the reader class
+ :param bool binary_file: if True, opens files in binary and not in text mode
:param bool check_remote_frames: if True, also tests remote frames
:param bool check_error_frames: if True, also tests error frames
+ :param bool check_fd: if True, also tests CAN FD frames
:param bool check_comments: if True, also inserts comments at some
locations and checks if they are contained anywhere literally
in the resulting file. The locations as selected randomly
but deterministically, which makes the test reproducible.
:param bool test_append: tests the writer in append mode as well
- :param bool round_timestamps: if True, rounds timestamps using :meth:`~builtin.round`
- before comparing the read messages/events
+ :param float or int or None allowed_timestamp_delta: directly passed to :meth:`can.Message.equals`
+ :param bool preserves_channel: if True, checks that the channel attribute is preserved
+ :param any adds_default_channel: sets this as the channel when not other channel was given
+ ignored, if *preserves_channel* is True
"""
# get all test messages
- self.original_messages = TEST_MESSAGES_BASE
+ self.original_messages = list(TEST_MESSAGES_BASE)
if check_remote_frames:
self.original_messages += TEST_MESSAGES_REMOTE_FRAMES
if check_error_frames:
self.original_messages += TEST_MESSAGES_ERROR_FRAMES
+ if check_fd:
+ self.original_messages += TEST_MESSAGES_CAN_FD
# sort them so that for example ASCWriter does not "fix" any messages with timestamp 0.0
self.original_messages = sort_messages(self.original_messages)
@@ -90,9 +174,12 @@ def _setup_instance_helper(self,
# we check this because of the lack of a common base class
# we filter for not starts with '__' so we do not get all the builtin
# methods when logging to the console
- attrs = [attr for attr in dir(writer_constructor) if not attr.startswith('__')]
- assert 'log_event' in attrs, \
- "cannot check comments with this writer: {}".format(writer_constructor)
+ attrs = [
+ attr for attr in dir(writer_constructor) if not attr.startswith("__")
+ ]
+ assert (
+ "log_event" in attrs
+ ), f"cannot check comments with this writer: {writer_constructor}"
# get all test comments
self.original_comments = TEST_COMMENTS if check_comments else ()
@@ -101,10 +188,17 @@ def _setup_instance_helper(self,
self.reader_constructor = reader_constructor
self.binary_file = binary_file
self.test_append_enabled = test_append
- self.round_timestamps = round_timestamps
+ self.assert_file_closed = assert_file_closed
+
+ ComparingMessagesTestCase.__init__(
+ self,
+ allowed_timestamp_delta=allowed_timestamp_delta,
+ preserves_channel=preserves_channel,
+ )
+ # adds_default_channel=adds_default_channel # TODO inlcude in tests
def setUp(self):
- with tempfile.NamedTemporaryFile('w+', delete=False) as test_file:
+ with tempfile.NamedTemporaryFile("w+", delete=False) as test_file:
self.test_file_name = test_file.name
def tearDown(self):
@@ -120,7 +214,7 @@ def test_path_like_explicit_stop(self):
self._write_all(writer)
self._ensure_fsync(writer)
writer.stop()
- if hasattr(writer.file, 'closed'):
+ if self.assert_file_closed:
self.assertTrue(writer.file.closed)
print("reading all messages")
@@ -128,15 +222,18 @@ def test_path_like_explicit_stop(self):
read_messages = list(reader)
# redundant, but this checks if stop() can be called multiple times
reader.stop()
- if hasattr(writer.file, 'closed'):
+ if self.assert_file_closed:
self.assertTrue(writer.file.closed)
# check if at least the number of messages matches
# could use assertCountEqual in later versions of Python and in the other methods
- self.assertEqual(len(read_messages), len(self.original_messages),
- "the number of written messages does not match the number of read messages")
+ self.assertEqual(
+ len(read_messages),
+ len(self.original_messages),
+ "the number of written messages does not match the number of read messages",
+ )
- self.assertMessagesEqual(read_messages)
+ self.assertMessagesEqual(self.original_messages, read_messages)
self.assertIncludesComments(self.test_file_name)
def test_path_like_context_manager(self):
@@ -148,7 +245,7 @@ def test_path_like_context_manager(self):
self._write_all(writer)
self._ensure_fsync(writer)
w = writer
- if hasattr(w.file, 'closed'):
+ if self.assert_file_closed:
self.assertTrue(w.file.closed)
# read all written messages
@@ -156,14 +253,17 @@ def test_path_like_context_manager(self):
with self.reader_constructor(self.test_file_name) as reader:
read_messages = list(reader)
r = reader
- if hasattr(r.file, 'closed'):
+ if self.assert_file_closed:
self.assertTrue(r.file.closed)
- # check if at least the number of messages matches;
- self.assertEqual(len(read_messages), len(self.original_messages),
- "the number of written messages does not match the number of read messages")
+ # check if at least the number of messages matches;
+ self.assertEqual(
+ len(read_messages),
+ len(self.original_messages),
+ "the number of written messages does not match the number of read messages",
+ )
- self.assertMessagesEqual(read_messages)
+ self.assertMessagesEqual(self.original_messages, read_messages)
self.assertIncludesComments(self.test_file_name)
def test_file_like_explicit_stop(self):
@@ -171,29 +271,32 @@ def test_file_like_explicit_stop(self):
# create writer
print("writing all messages/comments")
- my_file = open(self.test_file_name, 'wb' if self.binary_file else 'w')
+ my_file = open(self.test_file_name, "wb" if self.binary_file else "w")
writer = self.writer_constructor(my_file)
self._write_all(writer)
self._ensure_fsync(writer)
writer.stop()
- if hasattr(my_file, 'closed'):
+ if self.assert_file_closed:
self.assertTrue(my_file.closed)
print("reading all messages")
- my_file = open(self.test_file_name, 'rb' if self.binary_file else 'r')
+ my_file = open(self.test_file_name, "rb" if self.binary_file else "r")
reader = self.reader_constructor(my_file)
read_messages = list(reader)
# redundant, but this checks if stop() can be called multiple times
reader.stop()
- if hasattr(my_file, 'closed'):
+ if self.assert_file_closed:
self.assertTrue(my_file.closed)
# check if at least the number of messages matches
# could use assertCountEqual in later versions of Python and in the other methods
- self.assertEqual(len(read_messages), len(self.original_messages),
- "the number of written messages does not match the number of read messages")
+ self.assertEqual(
+ len(read_messages),
+ len(self.original_messages),
+ "the number of written messages does not match the number of read messages",
+ )
- self.assertMessagesEqual(read_messages)
+ self.assertMessagesEqual(self.original_messages, read_messages)
self.assertIncludesComments(self.test_file_name)
def test_file_like_context_manager(self):
@@ -201,28 +304,31 @@ def test_file_like_context_manager(self):
# create writer
print("writing all messages/comments")
- my_file = open(self.test_file_name, 'wb' if self.binary_file else 'w')
+ my_file = open(self.test_file_name, "wb" if self.binary_file else "w")
with self.writer_constructor(my_file) as writer:
self._write_all(writer)
self._ensure_fsync(writer)
w = writer
- if hasattr(my_file, 'closed'):
+ if self.assert_file_closed:
self.assertTrue(my_file.closed)
# read all written messages
print("reading all messages")
- my_file = open(self.test_file_name, 'rb' if self.binary_file else 'r')
+ my_file = open(self.test_file_name, "rb" if self.binary_file else "r")
with self.reader_constructor(my_file) as reader:
read_messages = list(reader)
r = reader
- if hasattr(my_file, 'closed'):
+ if self.assert_file_closed:
self.assertTrue(my_file.closed)
- # check if at least the number of messages matches;
- self.assertEqual(len(read_messages), len(self.original_messages),
- "the number of written messages does not match the number of read messages")
+ # check if at least the number of messages matches;
+ self.assertEqual(
+ len(read_messages),
+ len(self.original_messages),
+ "the number of written messages does not match the number of read messages",
+ )
- self.assertMessagesEqual(read_messages)
+ self.assertMessagesEqual(self.original_messages, read_messages)
self.assertIncludesComments(self.test_file_name)
def test_append_mode(self):
@@ -233,8 +339,8 @@ def test_append_mode(self):
raise unittest.SkipTest("do not test append mode")
count = len(self.original_messages)
- first_part = self.original_messages[:count // 2]
- second_part = self.original_messages[count // 2:]
+ first_part = self.original_messages[: count // 2]
+ second_part = self.original_messages[count // 2 :]
# write first half
with self.writer_constructor(self.test_file_name) as writer:
@@ -245,12 +351,12 @@ def test_append_mode(self):
# use append mode for second half
try:
writer = self.writer_constructor(self.test_file_name, append=True)
- except TypeError as e:
+ except ValueError as e:
# maybe "append" is not a formal parameter (this is the case for SqliteWriter)
try:
writer = self.writer_constructor(self.test_file_name)
except TypeError:
- # is the is still a problem, raise the initial error
+ # if it is still a problem, raise the initial error
raise e
with writer:
for message in second_part:
@@ -259,45 +365,27 @@ def test_append_mode(self):
with self.reader_constructor(self.test_file_name) as reader:
read_messages = list(reader)
- self.assertMessagesEqual(read_messages)
+ self.assertMessagesEqual(self.original_messages, read_messages)
def _write_all(self, writer):
"""Writes messages and insert comments here and there."""
# Note: we make no assumptions about the length of original_messages and original_comments
- for msg, comment in zip_longest(self.original_messages, self.original_comments, fillvalue=None):
+ for msg, comment in zip_longest(
+ self.original_messages, self.original_comments, fillvalue=None
+ ):
# msg and comment might be None
if comment is not None:
print("writing comment: ", comment)
- writer.log_event(comment) # we already know that this method exists
+ writer.log_event(comment) # we already know that this method exists
if msg is not None:
print("writing message: ", msg)
writer(msg)
def _ensure_fsync(self, io_handler):
- if hasattr(io_handler.file, 'fileno'):
+ if hasattr(io_handler, "file") and hasattr(io_handler.file, "fileno"):
io_handler.file.flush()
os.fsync(io_handler.file.fileno())
- def assertMessagesEqual(self, read_messages):
- """
- Checks the order and content of the individual messages.
- """
- for index, (original, read) in enumerate(zip(self.original_messages, read_messages)):
- try:
- # check everything except the timestamp
- self.assertEqual(original, read, "messages are not equal at index #{}".format(index))
- # check the timestamp
- if self.round_timestamps:
- original.timestamp = round(original.timestamp)
- read.timestamp = round(read.timestamp)
- self.assertAlmostEqual(read.timestamp, original.timestamp, places=6,
- msg="message timestamps are not almost_equal at index #{} ({!r} !~= {!r})"
- .format(index, original.timestamp, read.timestamp))
- except:
- print("Comparing: original message: {!r}".format(original))
- print(" read message: {!r}".format(read))
- raise
-
def assertIncludesComments(self, filename):
"""
Ensures that all comments are literally contained in the given file.
@@ -306,7 +394,7 @@ def assertIncludesComments(self, filename):
"""
if self.original_comments:
# read the entire outout file
- with open(filename, 'rb' if self.binary_file else 'r') as file:
+ with open(filename, "rb" if self.binary_file else "r") as file:
output_contents = file.read()
# check each, if they can be found in there literally
for comment in self.original_comments:
@@ -316,79 +404,510 @@ def assertIncludesComments(self, filename):
class TestAscFileFormat(ReaderWriterTest):
"""Tests can.ASCWriter and can.ASCReader"""
- __test__ = True
+ FORMAT_START_OF_FILE_DATE = "%a %b %d %I:%M:%S.%f %p %Y"
def _setup_instance(self):
- super(TestAscFileFormat, self)._setup_instance_helper(
- can.ASCWriter, can.ASCReader,
- check_comments=True, round_timestamps=True
+ super()._setup_instance_helper(
+ can.ASCWriter,
+ can.ASCReader,
+ check_fd=True,
+ check_comments=True,
+ preserves_channel=False,
+ adds_default_channel=0,
+ )
+
+ def _get_logfile_location(self, filename: str) -> Path:
+ my_dir = Path(__file__).parent
+ return my_dir / "data" / filename
+
+ def _read_log_file(self, filename, **kwargs):
+ logfile = self._get_logfile_location(filename)
+ with can.ASCReader(logfile, **kwargs) as reader:
+ return list(reader)
+
+ def test_read_absolute_time(self):
+ time_from_file = "Sat Sep 30 10:06:13.191 PM 2017"
+ start_time = datetime.strptime(
+ time_from_file, self.FORMAT_START_OF_FILE_DATE
+ ).timestamp()
+
+ expected_messages = [
+ can.Message(
+ timestamp=2.5010 + start_time,
+ arbitration_id=0xC8,
+ is_extended_id=False,
+ is_rx=False,
+ channel=1,
+ dlc=8,
+ data=[9, 8, 7, 6, 5, 4, 3, 2],
+ ),
+ can.Message(
+ timestamp=17.876708 + start_time,
+ arbitration_id=0x6F9,
+ is_extended_id=False,
+ channel=0,
+ dlc=0x8,
+ data=[5, 0xC, 0, 0, 0, 0, 0, 0],
+ ),
+ ]
+ actual = self._read_log_file("test_CanMessage.asc", relative_timestamp=False)
+ self.assertMessagesEqual(actual, expected_messages)
+
+ def test_read_can_message(self):
+ expected_messages = [
+ can.Message(
+ timestamp=2.5010,
+ arbitration_id=0xC8,
+ is_extended_id=False,
+ is_rx=False,
+ channel=1,
+ dlc=8,
+ data=[9, 8, 7, 6, 5, 4, 3, 2],
+ ),
+ can.Message(
+ timestamp=17.876708,
+ arbitration_id=0x6F9,
+ is_extended_id=False,
+ channel=0,
+ dlc=0x8,
+ data=[5, 0xC, 0, 0, 0, 0, 0, 0],
+ ),
+ ]
+ actual = self._read_log_file("test_CanMessage.asc")
+ self.assertMessagesEqual(actual, expected_messages)
+
+ def test_read_can_remote_message(self):
+ expected_messages = [
+ can.Message(
+ timestamp=2.510001,
+ arbitration_id=0x100,
+ is_extended_id=False,
+ channel=1,
+ is_remote_frame=True,
+ ),
+ can.Message(
+ timestamp=2.520002,
+ arbitration_id=0x200,
+ is_extended_id=False,
+ is_rx=False,
+ channel=2,
+ is_remote_frame=True,
+ ),
+ can.Message(
+ timestamp=2.584921,
+ arbitration_id=0x300,
+ is_extended_id=False,
+ channel=3,
+ dlc=8,
+ is_remote_frame=True,
+ ),
+ ]
+ actual = self._read_log_file("test_CanRemoteMessage.asc")
+ self.assertMessagesEqual(actual, expected_messages)
+
+ def test_read_can_fd_remote_message(self):
+ expected_messages = [
+ can.Message(
+ timestamp=30.300981,
+ arbitration_id=0x50005,
+ channel=2,
+ dlc=5,
+ is_rx=False,
+ is_fd=True,
+ is_remote_frame=True,
+ error_state_indicator=True,
+ )
+ ]
+ actual = self._read_log_file("test_CanFdRemoteMessage.asc")
+ self.assertMessagesEqual(actual, expected_messages)
+
+ def test_read_can_fd_message(self):
+ expected_messages = [
+ can.Message(
+ timestamp=30.005021,
+ arbitration_id=0x300,
+ is_extended_id=False,
+ channel=0,
+ dlc=8,
+ data=[0x11, 0xC2, 3, 4, 5, 6, 7, 8],
+ is_fd=True,
+ bitrate_switch=True,
+ ),
+ can.Message(
+ timestamp=30.005041,
+ arbitration_id=0x1C4D80A7,
+ channel=1,
+ dlc=8,
+ is_rx=False,
+ data=[0x12, 0xC2, 3, 4, 5, 6, 7, 8],
+ is_fd=True,
+ error_state_indicator=True,
+ ),
+ can.Message(
+ timestamp=30.005071,
+ arbitration_id=0x30A,
+ is_extended_id=False,
+ channel=2,
+ dlc=8,
+ data=[1, 2, 3, 4, 5, 6, 7, 8],
+ is_fd=True,
+ bitrate_switch=True,
+ error_state_indicator=True,
+ ),
+ ]
+ actual = self._read_log_file("test_CanFdMessage.asc")
+ self.assertMessagesEqual(actual, expected_messages)
+
+ def test_read_can_fd_message_64(self):
+ expected_messages = [
+ can.Message(
+ timestamp=30.506898,
+ arbitration_id=0x4EE,
+ is_extended_id=False,
+ channel=3,
+ dlc=64,
+ data=[0xA1, 2, 3, 4] + 59 * [0] + [0x64],
+ is_fd=True,
+ error_state_indicator=True,
+ ),
+ can.Message(
+ timestamp=31.506898,
+ arbitration_id=0x1C4D80A7,
+ channel=3,
+ dlc=64,
+ data=[0xB1, 2, 3, 4] + 59 * [0] + [0x64],
+ is_fd=True,
+ bitrate_switch=True,
+ ),
+ ]
+ actual = self._read_log_file("test_CanFdMessage64.asc")
+ self.assertMessagesEqual(actual, expected_messages)
+
+ def test_read_can_and_canfd_error_frames(self):
+ expected_messages = [
+ can.Message(timestamp=2.501000, channel=0, is_error_frame=True),
+ can.Message(timestamp=3.501000, channel=0, is_error_frame=True),
+ can.Message(timestamp=4.501000, channel=1, is_error_frame=True),
+ can.Message(
+ timestamp=30.806898,
+ channel=4,
+ is_rx=False,
+ is_error_frame=True,
+ is_fd=True,
+ ),
+ ]
+ actual = self._read_log_file("test_CanErrorFrames.asc")
+ self.assertMessagesEqual(actual, expected_messages)
+
+ def test_read_ignore_comments(self):
+ _msg_list = self._read_log_file("logfile.asc")
+
+ def test_read_no_triggerblock(self):
+ _msg_list = self._read_log_file("issue_1256.asc")
+
+ def test_read_can_dlc_greater_than_8(self):
+ _msg_list = self._read_log_file("issue_1299.asc")
+
+ def test_read_error_frame_channel(self):
+ # gh-issue 1578
+ err_frame = can.Message(is_error_frame=True, channel=4)
+
+ temp_file = tempfile.NamedTemporaryFile("w", delete=False)
+ temp_file.close()
+
+ try:
+ with can.ASCWriter(temp_file.name) as writer:
+ writer.on_message_received(err_frame)
+
+ with can.ASCReader(temp_file.name) as reader:
+ msg_list = list(reader)
+ assert len(msg_list) == 1
+ assert err_frame.equals(
+ msg_list[0], check_channel=True
+ ), f"{err_frame!r}!={msg_list[0]!r}"
+ finally:
+ os.unlink(temp_file.name)
+
+ def test_write_millisecond_handling(self):
+ now = datetime(
+ year=2017, month=9, day=30, hour=15, minute=6, second=13, microsecond=191456
)
+ # We temporarily set the locale to C to ensure test reproducibility
+ with override_locale(category=locale.LC_TIME, locale_str="C"):
+ # We mock datetime.now during ASCWriter __init__ for reproducibility
+ # Unfortunately, now() is a readonly attribute, so we mock datetime
+ with patch("can.io.asc.datetime") as mock_datetime:
+ mock_datetime.now.return_value = now
+ writer = can.ASCWriter(self.test_file_name)
+
+ msg = can.Message(
+ timestamp=now.timestamp(), arbitration_id=0x123, data=b"h"
+ )
+ writer.on_message_received(msg)
+
+ writer.stop()
+
+ actual_file = Path(self.test_file_name)
+ expected_file = self._get_logfile_location("single_frame_us_locale.asc")
+
+ self.assertEqual(expected_file.read_text(), actual_file.read_text())
+
+ def test_write(self):
+ now = datetime(
+ year=2017, month=9, day=30, hour=15, minute=6, second=13, microsecond=191456
+ )
+
+ # We temporarily set the locale to C to ensure test reproducibility
+ with override_locale(category=locale.LC_TIME, locale_str="C"):
+ # We mock datetime.now during ASCWriter __init__ for reproducibility
+ # Unfortunately, now() is a readonly attribute, so we mock datetime
+ with patch("can.io.asc.datetime") as mock_datetime:
+ mock_datetime.now.return_value = now
+ writer = can.ASCWriter(self.test_file_name)
+
+ msg = can.Message(
+ timestamp=now.timestamp(),
+ arbitration_id=0x123,
+ data=range(64),
+ )
+
+ with writer:
+ writer.on_message_received(msg)
+
+ actual_file = Path(self.test_file_name)
+ expected_file = self._get_logfile_location("single_frame.asc")
+
+ self.assertEqual(expected_file.read_text(), actual_file.read_text())
+
+ @parameterized.expand(
+ [
+ (
+ "May 27 04:09:35.000 pm 2014",
+ datetime(2014, 5, 27, 16, 9, 35, 0).timestamp(),
+ ),
+ (
+ "Mai 27 04:09:35.000 pm 2014",
+ datetime(2014, 5, 27, 16, 9, 35, 0).timestamp(),
+ ),
+ (
+ "Apr 28 10:44:52.480 2022",
+ datetime(2022, 4, 28, 10, 44, 52, 480000).timestamp(),
+ ),
+ (
+ "Sep 30 15:06:13.191 2017",
+ datetime(2017, 9, 30, 15, 6, 13, 191000).timestamp(),
+ ),
+ (
+ "Sep 30 15:06:13.191 pm 2017",
+ datetime(2017, 9, 30, 15, 6, 13, 191000).timestamp(),
+ ),
+ (
+ "Sep 30 15:06:13.191 am 2017",
+ datetime(2017, 9, 30, 15, 6, 13, 191000).timestamp(),
+ ),
+ ]
+ )
+ def test_datetime_to_timestamp(
+ self, datetime_string: str, expected_timestamp: float
+ ):
+ timestamp = can.ASCReader._datetime_to_timestamp(datetime_string)
+ self.assertAlmostEqual(timestamp, expected_timestamp)
+
class TestBlfFileFormat(ReaderWriterTest):
- """Tests can.BLFWriter and can.BLFReader"""
+ """Tests can.BLFWriter and can.BLFReader.
- __test__ = True
+ Uses log files created by Toby Lorenz:
+ https://bitbucket.org/tobylorenz/vector_blf/src/master/src/Vector/BLF/tests/unittests/events_from_binlog/
+ """
def _setup_instance(self):
- super(TestBlfFileFormat, self)._setup_instance_helper(
- can.BLFWriter, can.BLFReader,
+ super()._setup_instance_helper(
+ can.BLFWriter,
+ can.BLFReader,
binary_file=True,
- check_comments=False
+ check_fd=True,
+ check_comments=False,
+ test_append=True,
+ preserves_channel=False,
+ adds_default_channel=0,
)
- def test_read_known_file(self):
- logfile = os.path.join(os.path.dirname(__file__), "data", "logfile.blf")
+ def _read_log_file(self, filename):
+ logfile = os.path.join(os.path.dirname(__file__), "data", filename)
with can.BLFReader(logfile) as reader:
- messages = list(reader)
- self.assertEqual(len(messages), 2)
- self.assertEqual(messages[0],
- can.Message(
- extended_id=False,
- arbitration_id=0x64,
- data=[0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8]))
- self.assertEqual(messages[0].channel, 0)
- self.assertEqual(messages[1],
- can.Message(
- is_error_frame=True,
- extended_id=True,
- arbitration_id=0x1FFFFFFF))
- self.assertEqual(messages[1].channel, 0)
+ return list(reader)
+
+ def test_can_message(self):
+ expected = can.Message(
+ timestamp=2459565876.494607,
+ arbitration_id=0x4444444,
+ is_extended_id=False,
+ channel=0x1110,
+ dlc=0x33,
+ data=[0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC],
+ )
+ actual = self._read_log_file("test_CanMessage.blf")
+ self.assertMessagesEqual(actual, [expected] * 2)
+ self.assertEqual(actual[0].channel, expected.channel)
+
+ def test_can_message_2(self):
+ expected = can.Message(
+ timestamp=2459565876.494607,
+ arbitration_id=0x4444444,
+ is_extended_id=False,
+ channel=0x1110,
+ dlc=0x33,
+ data=[0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC],
+ )
+ actual = self._read_log_file("test_CanMessage2.blf")
+ self.assertMessagesEqual(actual, [expected] * 2)
+ self.assertEqual(actual[0].channel, expected.channel)
+
+ def test_can_fd_message(self):
+ expected = can.Message(
+ timestamp=2459565876.494607,
+ arbitration_id=0x4444444,
+ is_extended_id=False,
+ channel=0x1110,
+ dlc=64,
+ is_fd=True,
+ bitrate_switch=True,
+ error_state_indicator=True,
+ data=range(64),
+ )
+ actual = self._read_log_file("test_CanFdMessage.blf")
+ self.assertMessagesEqual(actual, [expected] * 2)
+ self.assertEqual(actual[0].channel, expected.channel)
+
+ def test_can_fd_message_64(self):
+ expected = can.Message(
+ timestamp=2459565876.494607,
+ arbitration_id=0x15555555,
+ is_extended_id=False,
+ is_remote_frame=True,
+ channel=0x10,
+ dlc=64,
+ is_fd=True,
+ is_rx=False,
+ bitrate_switch=True,
+ error_state_indicator=True,
+ )
+ actual = self._read_log_file("test_CanFdMessage64.blf")
+ self.assertMessagesEqual(actual, [expected] * 2)
+ self.assertEqual(actual[0].channel, expected.channel)
+
+ def test_can_error_frame_ext(self):
+ expected = can.Message(
+ timestamp=2459565876.494607,
+ is_error_frame=True,
+ arbitration_id=0x19999999,
+ is_extended_id=True,
+ channel=0x1110,
+ dlc=0x66,
+ data=[0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44],
+ )
+ actual = self._read_log_file("test_CanErrorFrameExt.blf")
+ self.assertMessagesEqual(actual, [expected] * 2)
+ self.assertEqual(actual[0].channel, expected.channel)
+
+ def test_timestamp_to_systemtime(self):
+ self.assertAlmostEqual(
+ 1636485425.999,
+ blf.systemtime_to_timestamp(blf.timestamp_to_systemtime(1636485425.998908)),
+ places=3,
+ )
+ self.assertAlmostEqual(
+ 1636485426.0,
+ blf.systemtime_to_timestamp(blf.timestamp_to_systemtime(1636485425.999908)),
+ places=3,
+ )
+
+ def test_issue_1905(self):
+ expected = can.Message(
+ timestamp=1735654183.491113,
+ channel=6,
+ arbitration_id=0x6A9,
+ is_extended_id=False,
+ is_fd=True,
+ bitrate_switch=True,
+ error_state_indicator=False,
+ dlc=64,
+ data=bytearray(
+ b"\xff\xff\xff\xff\xff\xff\xff\xff"
+ b"\xff\xff\xff\xff\xff\xff\xff\xff"
+ b"\xff\xff\xff\xff\xff\xff\xff\xff"
+ b"\xff\xff\xff\xff\xff\xff\xff\xff"
+ b"\xff\xff\xff\xff\xff\xff\xff\xff"
+ b"\xff\xff\xff\xff\xff\xff\xff\xff"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00"
+ ),
+ )
+ msgs = self._read_log_file("issue_1905.blf")
+ self.assertMessageEqual(expected, msgs[0])
class TestCanutilsFileFormat(ReaderWriterTest):
"""Tests can.CanutilsLogWriter and can.CanutilsLogReader"""
- __test__ = True
-
def _setup_instance(self):
- super(TestCanutilsFileFormat, self)._setup_instance_helper(
- can.CanutilsLogWriter, can.CanutilsLogReader,
- test_append=True, check_comments=False
+ super()._setup_instance_helper(
+ can.CanutilsLogWriter,
+ can.CanutilsLogReader,
+ check_fd=True,
+ test_append=True,
+ check_comments=False,
+ preserves_channel=False,
+ adds_default_channel="vcan0",
)
class TestCsvFileFormat(ReaderWriterTest):
- """Tests can.ASCWriter and can.ASCReader"""
+ """Tests can.CSVWriter and can.CSVReader"""
+
+ def _setup_instance(self):
+ super()._setup_instance_helper(
+ can.CSVWriter,
+ can.CSVReader,
+ check_fd=False,
+ test_append=True,
+ check_comments=False,
+ preserves_channel=False,
+ adds_default_channel=None,
+ )
- __test__ = True
+
+@unittest.skipIf(asammdf is None, "MF4 is unavailable")
+class TestMF4FileFormat(ReaderWriterTest):
+ """Tests can.MF4Writer and can.MF4Reader"""
def _setup_instance(self):
- super(TestCsvFileFormat, self)._setup_instance_helper(
- can.CSVWriter, can.CSVReader,
- test_append=True, check_comments=False
+ super()._setup_instance_helper(
+ can.MF4Writer,
+ can.MF4Reader,
+ binary_file=True,
+ check_comments=False,
+ preserves_channel=False,
+ allowed_timestamp_delta=1e-4,
+ adds_default_channel=0,
)
class TestSqliteDatabaseFormat(ReaderWriterTest):
"""Tests can.SqliteWriter and can.SqliteReader"""
- __test__ = True
-
def _setup_instance(self):
- super(TestSqliteDatabaseFormat, self)._setup_instance_helper(
- can.SqliteWriter, can.SqliteReader,
- test_append=True, check_comments=False
+ super()._setup_instance_helper(
+ can.SqliteWriter,
+ can.SqliteReader,
+ check_fd=False,
+ test_append=True,
+ check_comments=False,
+ preserves_channel=False,
+ adds_default_channel=None,
+ assert_file_closed=False,
)
@unittest.skip("not implemented")
@@ -413,17 +932,28 @@ def test_read_all(self):
with self.reader_constructor(self.test_file_name) as reader:
read_messages = list(reader.read_all())
- # check if at least the number of messages matches;
- self.assertEqual(len(read_messages), len(self.original_messages),
- "the number of written messages does not match the number of read messages")
+ # check if at least the number of messages matches;
+ self.assertEqual(
+ len(read_messages),
+ len(self.original_messages),
+ "the number of written messages does not match the number of read messages",
+ )
- self.assertMessagesEqual(read_messages)
+ self.assertMessagesEqual(self.original_messages, read_messages)
class TestPrinter(unittest.TestCase):
- """Tests that can.Printer does not crash"""
+ """Tests that can.Printer does not crash.
+
+ TODO test append mode
+ """
- messages = TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES
+ messages = (
+ TEST_MESSAGES_BASE
+ + TEST_MESSAGES_REMOTE_FRAMES
+ + TEST_MESSAGES_ERROR_FRAMES
+ + TEST_MESSAGES_CAN_FD
+ )
def test_not_crashes_with_stdout(self):
with can.Printer() as printer:
@@ -431,11 +961,168 @@ def test_not_crashes_with_stdout(self):
printer(message)
def test_not_crashes_with_file(self):
- with tempfile.NamedTemporaryFile('w', delete=False) as temp_file:
+ with tempfile.NamedTemporaryFile("w") as temp_file:
with can.Printer(temp_file) as printer:
for message in self.messages:
printer(message)
-if __name__ == '__main__':
+class TestTrcFileFormatBase(ReaderWriterTest):
+ """
+ Base class for Tests with can.TRCWriter and can.TRCReader
+
+ .. note::
+ This class is prevented from being executed as a test
+ case itself by a *del* statement in at the end of the file.
+ """
+
+ def _setup_instance(self):
+ super()._setup_instance_helper(
+ can.TRCWriter,
+ can.TRCReader,
+ check_remote_frames=False,
+ check_error_frames=False,
+ check_fd=False,
+ check_comments=False,
+ preserves_channel=False,
+ allowed_timestamp_delta=0.001,
+ adds_default_channel=0,
+ )
+
+ def _read_log_file(self, filename, **kwargs):
+ logfile = os.path.join(os.path.dirname(__file__), "data", filename)
+ with can.TRCReader(logfile, **kwargs) as reader:
+ return list(reader)
+
+
+class TestTrcFileFormatGen(TestTrcFileFormatBase):
+ """Generic tests for can.TRCWriter and can.TRCReader with different file versions"""
+
+ def test_can_message(self):
+ start_time = 1506809173.191 # 30.09.2017 22:06:13.191.000 as timestamp
+ expected_messages = [
+ can.Message(
+ timestamp=start_time + 2.5010,
+ arbitration_id=0xC8,
+ is_extended_id=False,
+ is_rx=False,
+ channel=1,
+ dlc=8,
+ data=[9, 8, 7, 6, 5, 4, 3, 2],
+ ),
+ can.Message(
+ timestamp=start_time + 17.876708,
+ arbitration_id=0x6F9,
+ is_extended_id=False,
+ channel=0,
+ dlc=0x8,
+ data=[5, 0xC, 0, 0, 0, 0, 0, 0],
+ ),
+ ]
+ actual = self._read_log_file("test_CanMessage.trc")
+ self.assertMessagesEqual(actual, expected_messages)
+
+ @parameterized.expand(
+ [
+ ("V1_0", "test_CanMessage_V1_0_BUS1.trc", False),
+ ("V1_1", "test_CanMessage_V1_1.trc", True),
+ ("V1_3", "test_CanMessage_V1_3.trc", True),
+ ("V2_0", "test_CanMessage_V2_0_BUS1.trc", True),
+ ("V2_1", "test_CanMessage_V2_1.trc", True),
+ ]
+ )
+ def test_can_message_versions(self, name, filename, is_rx_support):
+ with self.subTest(name):
+ if name == "V1_0":
+ # Version 1.0 does not support start time
+ start_time = 0
+ else:
+ start_time = (
+ 1639837687.062001 # 18.12.2021 14:28:07.062.001 as timestamp
+ )
+
+ def msg_std(timestamp):
+ msg = can.Message(
+ timestamp=timestamp + start_time,
+ arbitration_id=0x000,
+ is_extended_id=False,
+ channel=1,
+ dlc=8,
+ data=[0, 0, 0, 0, 0, 0, 0, 0],
+ )
+ if is_rx_support:
+ msg.is_rx = False
+ return msg
+
+ def msg_ext(timestamp):
+ msg = can.Message(
+ timestamp=timestamp + start_time,
+ arbitration_id=0x100,
+ is_extended_id=True,
+ channel=1,
+ dlc=8,
+ data=[0, 0, 0, 0, 0, 0, 0, 0],
+ )
+ if is_rx_support:
+ msg.is_rx = False
+ return msg
+
+ def msg_rtr(timestamp):
+ msg = can.Message(
+ timestamp=timestamp + start_time,
+ arbitration_id=0x704,
+ is_extended_id=False,
+ is_remote_frame=True,
+ channel=1,
+ dlc=1,
+ data=[],
+ )
+ if is_rx_support:
+ msg.is_rx = True
+ return msg
+
+ expected_messages = [
+ msg_ext(17.5354),
+ msg_ext(17.7003),
+ msg_ext(17.8738),
+ msg_std(19.2954),
+ msg_std(19.5006),
+ msg_std(19.7052),
+ msg_ext(20.5927),
+ msg_ext(20.7986),
+ msg_ext(20.9560),
+ msg_ext(21.0971),
+ msg_rtr(48.9376),
+ ]
+ actual = self._read_log_file(filename)
+ self.assertMessagesEqual(actual, expected_messages)
+
+ def test_not_supported_version(self):
+ with tempfile.NamedTemporaryFile(mode="w") as f:
+ with self.assertRaises(NotImplementedError):
+ writer = can.TRCWriter(f)
+ writer.file_version = can.TRCFileVersion.UNKNOWN
+ writer.on_message_received(can.Message())
+
+
+class TestTrcFileFormatV1_0(TestTrcFileFormatBase):
+ """Tests can.TRCWriter and can.TRCReader with file version 1.0"""
+
+ @staticmethod
+ def Writer(filename):
+ writer = can.TRCWriter(filename)
+ writer.file_version = can.TRCFileVersion.V1_0
+ return writer
+
+ def _setup_instance(self):
+ super()._setup_instance()
+ self.writer_constructor = TestTrcFileFormatV1_0.Writer
+
+
+# this excludes the base class from being executed as a test case itself
+del ReaderWriterTest
+del TestTrcFileFormatBase
+
+
+if __name__ == "__main__":
unittest.main()
diff --git a/test/message_helper.py b/test/message_helper.py
new file mode 100644
index 000000000..d10e9195f
--- /dev/null
+++ b/test/message_helper.py
@@ -0,0 +1,48 @@
+#!/usr/bin/env python
+
+"""
+This module contains a helper for writing test cases that need to compare messages.
+"""
+
+
+class ComparingMessagesTestCase:
+ """
+ Must be extended by a class also extending a unittest.TestCase.
+
+ .. note:: This class does not extend unittest.TestCase so it does not get
+ run as a test itself.
+ """
+
+ def __init__(self, allowed_timestamp_delta=0.0, preserves_channel=True):
+ """
+ :param float or int or None allowed_timestamp_delta: directly passed to :meth:`can.Message.equals`
+ :param bool preserves_channel: if True, checks that the channel attribute is preserved
+ """
+ self.allowed_timestamp_delta = allowed_timestamp_delta
+ self.preserves_channel = preserves_channel
+
+ def assertMessageEqual(self, message_1, message_2):
+ """
+ Checks that two messages are equal, according to the given rules.
+ """
+
+ if not message_1.equals(
+ message_2,
+ check_channel=self.preserves_channel,
+ timestamp_delta=self.allowed_timestamp_delta,
+ ):
+ print(f"Comparing: message 1: {message_1!r}")
+ print(f" message 2: {message_2!r}")
+ self.fail(f"messages are unequal: \n{message_1}\n{message_2}")
+
+ def assertMessagesEqual(self, messages_1, messages_2):
+ """
+ Checks the order and content of the individual messages pairwise.
+ Raises an error if the lengths of the sequences are not equal.
+ """
+ self.assertEqual(
+ len(messages_1), len(messages_2), "the number of messages differs"
+ )
+
+ for message_1, message_2 in zip(messages_1, messages_2):
+ self.assertMessageEqual(message_1, message_2)
diff --git a/test/network_test.py b/test/network_test.py
index 830adceca..3a231dff7 100644
--- a/test/network_test.py
+++ b/test/network_test.py
@@ -1,29 +1,21 @@
#!/usr/bin/env python
-# coding: utf-8
-
-from __future__ import print_function
-
-import unittest
-import threading
-try:
- import queue
-except ImportError:
- import Queue as queue
+import contextlib
+import logging
import random
+import threading
+import unittest
+
+import can
+from test.config import IS_PYPY
-import logging
logging.getLogger(__file__).setLevel(logging.WARNING)
-# make a random bool:
-rbool = lambda: bool(round(random.random()))
-import can
-
-channel = 'vcan0'
-can.rc['interface'] = 'virtual'
+# make a random bool:
+def rbool():
+ return random.choice([False, True])
-@unittest.skipIf('interface' not in can.rc, "Need a CAN interface")
class ControllerAreaNetworkTestCase(unittest.TestCase):
"""
This test ensures that what messages go in to the bus is what comes out.
@@ -43,77 +35,66 @@ class ControllerAreaNetworkTestCase(unittest.TestCase):
extended_flags = [rbool() for _ in range(num_messages)]
ids = list(range(num_messages))
- data = list(bytearray([random.randrange(0, 2 ** 8 - 1)
- for a in range(random.randrange(9))])
- for b in range(num_messages))
-
- def producer(self, ready_event, msg_read):
- self.client_bus = can.interface.Bus(channel=channel)
- ready_event.wait()
- for i in range(self.num_messages):
- m = can.Message(
- arbitration_id=self.ids[i],
- is_remote_frame=self.remote_flags[i],
- is_error_frame=self.error_flags[i],
- extended_id=self.extended_flags[i],
- data=self.data[i]
- )
- #logging.debug("writing message: {}".format(m))
- if msg_read is not None:
- # Don't send until the other thread is ready
- msg_read.wait()
- msg_read.clear()
-
- self.client_bus.send(m)
+ data = list(
+ bytearray([random.randrange(0, 2**8 - 1) for a in range(random.randrange(9))])
+ for b in range(num_messages)
+ )
+
+ def setUp(self):
+ # Save all can.rc defaults
+ self._can_rc = can.rc
+ can.rc = {"interface": "virtual"}
+
+ def tearDown(self):
+ # Restore the defaults
+ can.rc = self._can_rc
+
+ def producer(self, channel: str):
+ with can.interface.Bus(channel=channel) as client_bus:
+ for i in range(self.num_messages):
+ m = can.Message(
+ arbitration_id=self.ids[i],
+ is_remote_frame=self.remote_flags[i],
+ is_error_frame=self.error_flags[i],
+ is_extended_id=self.extended_flags[i],
+ data=self.data[i],
+ )
+ client_bus.send(m)
def testProducer(self):
"""Verify that we can send arbitrary messages on the bus"""
logging.debug("testing producer alone")
- ready = threading.Event()
- ready.set()
- self.producer(ready, None)
-
+ self.producer(channel="testProducer")
logging.debug("producer test complete")
def testProducerConsumer(self):
logging.debug("testing producer/consumer")
- ready = threading.Event()
- msg_read = threading.Event()
-
- self.server_bus = can.interface.Bus(channel=channel)
-
- t = threading.Thread(target=self.producer, args=(ready, msg_read))
- t.start()
-
- # Ensure there are no messages on the bus
- while True:
- m = self.server_bus.recv(timeout=0.5)
- if m is None:
- print("No messages... lets go")
- break
- else:
- self.fail("received messages before the test has started ...")
- ready.set()
- i = 0
- while i < self.num_messages:
- msg_read.set()
- msg = self.server_bus.recv(timeout=0.5)
- self.assertIsNotNone(msg, "Didn't receive a message")
- #logging.debug("Received message {} with data: {}".format(i, msg.data))
-
- self.assertEqual(msg.id_type, self.extended_flags[i])
- if not msg.is_remote_frame:
- self.assertEqual(msg.data, self.data[i])
- self.assertEqual(msg.arbitration_id, self.ids[i])
-
- self.assertEqual(msg.is_error_frame, self.error_flags[i])
- self.assertEqual(msg.is_remote_frame, self.remote_flags[i])
-
- i += 1
- t.join()
-
- self.server_bus.flush_tx_buffer()
- self.server_bus.shutdown()
-
-if __name__ == '__main__':
+ read_timeout = 2.0 if IS_PYPY else 0.5
+ channel = "testProducerConsumer"
+
+ with can.interface.Bus(channel=channel, interface="virtual") as server_bus:
+ t = threading.Thread(target=self.producer, args=(channel,))
+ t.start()
+
+ i = 0
+ while i < self.num_messages:
+ msg = server_bus.recv(timeout=read_timeout)
+ self.assertIsNotNone(msg, "Didn't receive a message")
+
+ self.assertEqual(msg.is_extended_id, self.extended_flags[i])
+ if not msg.is_remote_frame:
+ self.assertEqual(msg.data, self.data[i])
+ self.assertEqual(msg.arbitration_id, self.ids[i])
+
+ self.assertEqual(msg.is_error_frame, self.error_flags[i])
+ self.assertEqual(msg.is_remote_frame, self.remote_flags[i])
+
+ i += 1
+ t.join()
+
+ with contextlib.suppress(NotImplementedError):
+ server_bus.flush_tx_buffer()
+
+
+if __name__ == "__main__":
unittest.main()
diff --git a/test/notifier_test.py b/test/notifier_test.py
index ca462a2ad..d8512a00b 100644
--- a/test/notifier_test.py
+++ b/test/notifier_test.py
@@ -1,64 +1,93 @@
#!/usr/bin/env python
-# coding: utf-8
-import unittest
+
+import asyncio
import time
-try:
- import asyncio
-except ImportError:
- asyncio = None
+import unittest
import can
class NotifierTest(unittest.TestCase):
-
def test_single_bus(self):
- bus = can.Bus('test', bustype='virtual', receive_own_messages=True)
- reader = can.BufferedReader()
- notifier = can.Notifier(bus, [reader], 0.1)
- msg = can.Message()
- bus.send(msg)
- self.assertIsNotNone(reader.get_message(1))
- notifier.stop()
- bus.shutdown()
+ with can.Bus("test", interface="virtual", receive_own_messages=True) as bus:
+ reader = can.BufferedReader()
+ notifier = can.Notifier(bus, [reader], 0.1)
+ self.assertFalse(notifier.stopped)
+ msg = can.Message()
+ bus.send(msg)
+ self.assertIsNotNone(reader.get_message(1))
+ notifier.stop()
+ self.assertTrue(notifier.stopped)
def test_multiple_bus(self):
- bus1 = can.Bus(0, bustype='virtual', receive_own_messages=True)
- bus2 = can.Bus(1, bustype='virtual', receive_own_messages=True)
- reader = can.BufferedReader()
- notifier = can.Notifier([bus1, bus2], [reader], 0.1)
- msg = can.Message()
- bus1.send(msg)
- time.sleep(0.1)
- bus2.send(msg)
- recv_msg = reader.get_message(1)
- self.assertIsNotNone(recv_msg)
- self.assertEqual(recv_msg.channel, 0)
- recv_msg = reader.get_message(1)
- self.assertIsNotNone(recv_msg)
- self.assertEqual(recv_msg.channel, 1)
- notifier.stop()
- bus1.shutdown()
- bus2.shutdown()
+ with (
+ can.Bus(0, interface="virtual", receive_own_messages=True) as bus1,
+ can.Bus(1, interface="virtual", receive_own_messages=True) as bus2,
+ ):
+ reader = can.BufferedReader()
+ notifier = can.Notifier([bus1, bus2], [reader], 0.1)
+ self.assertFalse(notifier.stopped)
+ msg = can.Message()
+ bus1.send(msg)
+ time.sleep(0.1)
+ bus2.send(msg)
+ recv_msg = reader.get_message(1)
+ self.assertIsNotNone(recv_msg)
+ self.assertEqual(recv_msg.channel, 0)
+ recv_msg = reader.get_message(1)
+ self.assertIsNotNone(recv_msg)
+ self.assertEqual(recv_msg.channel, 1)
+ notifier.stop()
+ self.assertTrue(notifier.stopped)
+ def test_context_manager(self):
+ with can.Bus("test", interface="virtual", receive_own_messages=True) as bus:
+ reader = can.BufferedReader()
+ with can.Notifier(bus, [reader], 0.1) as notifier:
+ self.assertFalse(notifier.stopped)
+ msg = can.Message()
+ bus.send(msg)
+ self.assertIsNotNone(reader.get_message(1))
+ notifier.stop()
+ self.assertTrue(notifier.stopped)
-class AsyncNotifierTest(unittest.TestCase):
+ def test_registry(self):
+ with can.Bus("test", interface="virtual", receive_own_messages=True) as bus:
+ reader = can.BufferedReader()
+ with can.Notifier(bus, [reader], 0.1) as notifier:
+ # creating a second notifier for the same bus must fail
+ self.assertRaises(ValueError, can.Notifier, bus, [reader], 0.1)
+
+ # find_instance must return the existing instance
+ self.assertEqual(can.Notifier.find_instances(bus), (notifier,))
- @unittest.skipIf(asyncio is None, 'Test requires asyncio')
+ # Notifier is stopped, find_instances() must return an empty tuple
+ self.assertEqual(can.Notifier.find_instances(bus), ())
+
+ # now the first notifier is stopped, a new notifier can be created without error:
+ with can.Notifier(bus, [reader], 0.1) as notifier:
+ # the next notifier call should fail again since there is an active notifier already
+ self.assertRaises(ValueError, can.Notifier, bus, [reader], 0.1)
+
+ # find_instance must return the existing instance
+ self.assertEqual(can.Notifier.find_instances(bus), (notifier,))
+
+
+class AsyncNotifierTest(unittest.TestCase):
def test_asyncio_notifier(self):
- loop = asyncio.get_event_loop()
- bus = can.Bus('test', bustype='virtual', receive_own_messages=True)
- reader = can.AsyncBufferedReader()
- notifier = can.Notifier(bus, [reader], 0.1, loop=loop)
- msg = can.Message()
- bus.send(msg)
- future = asyncio.wait_for(reader.get_message(), 1.0)
- recv_msg = loop.run_until_complete(future)
- self.assertIsNotNone(recv_msg)
- notifier.stop()
- bus.shutdown()
+ async def run_it():
+ with can.Bus("test", interface="virtual", receive_own_messages=True) as bus:
+ reader = can.AsyncBufferedReader()
+ notifier = can.Notifier(
+ bus, [reader], 0.1, loop=asyncio.get_running_loop()
+ )
+ bus.send(can.Message())
+ recv_msg = await asyncio.wait_for(reader.get_message(), 0.5)
+ self.assertIsNotNone(recv_msg)
+ notifier.stop()
+ asyncio.run(run_it())
-if __name__ == '__main__':
+if __name__ == "__main__":
unittest.main()
diff --git a/test/open_vcan.sh b/test/open_vcan.sh
index bd02ad752..b6f4676b7 100755
--- a/test/open_vcan.sh
+++ b/test/open_vcan.sh
@@ -5,3 +5,7 @@
modprobe vcan
ip link add dev vcan0 type vcan
ip link set up vcan0 mtu 72
+ip link add dev vxcan0 type vcan
+ip link set up vxcan0 mtu 72
+ip link add dev slcan0 type vcan
+ip link set up slcan0 mtu 72
diff --git a/test/serial_test.py b/test/serial_test.py
index da67cfaa2..409485112 100644
--- a/test/serial_test.py
+++ b/test/serial_test.py
@@ -1,5 +1,4 @@
#!/usr/bin/env python
-# coding: utf-8
"""
This module is testing the serial interface.
@@ -8,16 +7,23 @@
"""
import unittest
-from mock import patch
+from unittest.mock import patch
import can
from can.interfaces.serial.serial_can import SerialBus
+from .config import IS_PYPY
+from .message_helper import ComparingMessagesTestCase
+
+# Mentioned in #1010
+TIMEOUT = 0.5 if IS_PYPY else 0.1 # 0.1 is the default set in SerialBus
+
class SerialDummy:
"""
Dummy to mock the serial communication
"""
+
msg = None
def __init__(self):
@@ -36,9 +42,17 @@ def reset(self):
self.msg = None
-class SimpleSerialTestBase(object):
+class SimpleSerialTestBase(ComparingMessagesTestCase):
MAX_TIMESTAMP = 0xFFFFFFFF / 1000
+ def __init__(self):
+ ComparingMessagesTestCase.__init__(
+ self, allowed_timestamp_delta=None, preserves_channel=True
+ )
+
+ def test_can_protocol(self):
+ self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20)
+
def test_rx_tx_min_max_data(self):
"""
Tests the transfer from 0x00 to 0xFF for a 1 byte payload
@@ -47,7 +61,7 @@ def test_rx_tx_min_max_data(self):
msg = can.Message(data=[b])
self.bus.send(msg)
msg_receive = self.bus.recv()
- self.assertEqual(msg, msg_receive)
+ self.assertMessageEqual(msg, msg_receive)
def test_rx_tx_min_max_dlc(self):
"""
@@ -59,7 +73,7 @@ def test_rx_tx_min_max_dlc(self):
msg = can.Message(data=payload)
self.bus.send(msg)
msg_receive = self.bus.recv()
- self.assertEqual(msg, msg_receive)
+ self.assertMessageEqual(msg, msg_receive)
def test_rx_tx_data_none(self):
"""
@@ -68,25 +82,43 @@ def test_rx_tx_data_none(self):
msg = can.Message(data=None)
self.bus.send(msg)
msg_receive = self.bus.recv()
- self.assertEqual(msg, msg_receive)
+ self.assertMessageEqual(msg, msg_receive)
- def test_rx_tx_min_id(self):
+ def test_rx_tx_min_std_id(self):
"""
- Tests the transfer with the lowest arbitration id
+ Tests the transfer with the lowest standard arbitration id
"""
- msg = can.Message(arbitration_id=0)
+ msg = can.Message(arbitration_id=0, is_extended_id=False)
self.bus.send(msg)
msg_receive = self.bus.recv()
- self.assertEqual(msg, msg_receive)
+ self.assertMessageEqual(msg, msg_receive)
- def test_rx_tx_max_id(self):
+ def test_rx_tx_max_std_id(self):
"""
- Tests the transfer with the highest arbitration id
+ Tests the transfer with the highest standard arbitration id
"""
- msg = can.Message(arbitration_id=536870911)
+ msg = can.Message(arbitration_id=0x7FF, is_extended_id=False)
self.bus.send(msg)
msg_receive = self.bus.recv()
- self.assertEqual(msg, msg_receive)
+ self.assertMessageEqual(msg, msg_receive)
+
+ def test_rx_tx_min_ext_id(self):
+ """
+ Tests the transfer with the lowest extended arbitration id
+ """
+ msg = can.Message(arbitration_id=0x000, is_extended_id=True)
+ self.bus.send(msg)
+ msg_receive = self.bus.recv()
+ self.assertMessageEqual(msg, msg_receive)
+
+ def test_rx_tx_max_ext_id(self):
+ """
+ Tests the transfer with the highest extended arbitration id
+ """
+ msg = can.Message(arbitration_id=0x1FFFFFFF, is_extended_id=True)
+ self.bus.send(msg)
+ msg_receive = self.bus.recv()
+ self.assertMessageEqual(msg, msg_receive)
def test_rx_tx_max_timestamp(self):
"""
@@ -96,14 +128,14 @@ def test_rx_tx_max_timestamp(self):
msg = can.Message(timestamp=self.MAX_TIMESTAMP)
self.bus.send(msg)
msg_receive = self.bus.recv()
- self.assertEqual(msg, msg_receive)
+ self.assertMessageEqual(msg, msg_receive)
self.assertEqual(msg.timestamp, msg_receive.timestamp)
def test_rx_tx_max_timestamp_error(self):
"""
Tests for an exception with an out of range timestamp (max + 1)
"""
- msg = can.Message(timestamp=self.MAX_TIMESTAMP+1)
+ msg = can.Message(timestamp=self.MAX_TIMESTAMP + 1)
self.assertRaises(ValueError, self.bus.send, msg)
def test_rx_tx_min_timestamp(self):
@@ -113,7 +145,7 @@ def test_rx_tx_min_timestamp(self):
msg = can.Message(timestamp=0)
self.bus.send(msg)
msg_receive = self.bus.recv()
- self.assertEqual(msg, msg_receive)
+ self.assertMessageEqual(msg, msg_receive)
self.assertEqual(msg.timestamp, msg_receive.timestamp)
def test_rx_tx_min_timestamp_error(self):
@@ -123,30 +155,76 @@ def test_rx_tx_min_timestamp_error(self):
msg = can.Message(timestamp=-1)
self.assertRaises(ValueError, self.bus.send, msg)
+ def test_rx_tx_err_frame(self):
+ """
+ Test the transfer of error frames.
+ """
+ msg = can.Message(
+ is_extended_id=False, is_error_frame=True, is_remote_frame=False
+ )
+ self.bus.send(msg)
+ msg_receive = self.bus.recv()
+ self.assertMessageEqual(msg, msg_receive)
+
+ def test_rx_tx_rtr_frame(self):
+ """
+ Test the transfer of remote frames.
+ """
+ msg = can.Message(
+ is_extended_id=False, is_error_frame=False, is_remote_frame=True
+ )
+ self.bus.send(msg)
+ msg_receive = self.bus.recv()
+ self.assertMessageEqual(msg, msg_receive)
+
+ def test_when_no_fileno(self):
+ """
+ Tests for the fileno method catching the missing pyserial implementeation on the Windows platform
+ """
+ try:
+ fileno = self.bus.fileno()
+ except NotImplementedError:
+ pass # allow it to be left non-implemented for Windows platform
+ else:
+ fileno.__gt__ = (
+ lambda self, compare: True
+ ) # Current platform implements fileno, so get the mock to respond to a greater than comparison
+ self.assertIsNotNone(fileno)
+ self.assertFalse(
+ fileno == -1
+ ) # forcing the value to -1 is the old way of managing fileno on Windows but it is not compatible with notifiers
+ self.assertTrue(fileno > 0)
+
class SimpleSerialTest(unittest.TestCase, SimpleSerialTestBase):
+ def __init__(self, *args, **kwargs):
+ unittest.TestCase.__init__(self, *args, **kwargs)
+ SimpleSerialTestBase.__init__(self)
def setUp(self):
- self.patcher = patch('serial.Serial')
+ self.patcher = patch("serial.Serial")
self.mock_serial = self.patcher.start()
self.serial_dummy = SerialDummy()
self.mock_serial.return_value.write = self.serial_dummy.write
self.mock_serial.return_value.read = self.serial_dummy.read
self.addCleanup(self.patcher.stop)
- self.bus = SerialBus('bus')
+ self.bus = SerialBus("bus", timeout=TIMEOUT)
def tearDown(self):
self.serial_dummy.reset()
class SimpleSerialLoopTest(unittest.TestCase, SimpleSerialTestBase):
+ def __init__(self, *args, **kwargs):
+ unittest.TestCase.__init__(self, *args, **kwargs)
+ SimpleSerialTestBase.__init__(self)
def setUp(self):
- self.bus = SerialBus('loop://')
+ self.bus = SerialBus("loop://", timeout=TIMEOUT)
def tearDown(self):
self.bus.shutdown()
-if __name__ == '__main__':
+if __name__ == "__main__":
unittest.main()
diff --git a/test/simplecyclic_test.py b/test/simplecyclic_test.py
index 8763174a8..22a11e643 100644
--- a/test/simplecyclic_test.py
+++ b/test/simplecyclic_test.py
@@ -1,39 +1,313 @@
#!/usr/bin/env python
-# coding: utf-8
"""
This module tests cyclic send tasks.
"""
-from __future__ import absolute_import
-
-from time import sleep
+import gc
+import platform
+import sys
+import time
+import traceback
import unittest
+from threading import Thread
+from time import sleep
+from unittest.mock import MagicMock
import can
-from .config import *
+from .config import IS_CI, IS_PYPY
+from .message_helper import ComparingMessagesTestCase
-class SimpleCyclicSendTaskTest(unittest.TestCase):
- @unittest.skipIf(IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server")
+class SimpleCyclicSendTaskTest(unittest.TestCase, ComparingMessagesTestCase):
+ def __init__(self, *args, **kwargs):
+ unittest.TestCase.__init__(self, *args, **kwargs)
+ ComparingMessagesTestCase.__init__(
+ self, allowed_timestamp_delta=0.016, preserves_channel=True
+ )
+
+ @unittest.skipIf(
+ IS_CI,
+ "the timing sensitive behaviour cannot be reproduced reliably on a CI server",
+ )
def test_cycle_time(self):
- msg = can.Message(extended_id=False, arbitration_id=0x123, data=[0,1,2,3,4,5,6,7])
- bus1 = can.interface.Bus(bustype='virtual')
- bus2 = can.interface.Bus(bustype='virtual')
- task = bus1.send_periodic(msg, 0.01, 1)
- self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC)
-
- sleep(2)
- size = bus2.queue.qsize()
- # About 100 messages should have been transmitted
- self.assertTrue(80 <= size <= 120,
- '100 +/- 20 messages should have been transmitted. But queue contained {}'.format(size))
- last_msg = bus2.recv()
- self.assertEqual(last_msg, msg)
-
- bus1.shutdown()
- bus2.shutdown()
-
-if __name__ == '__main__':
+ msg = can.Message(
+ is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]
+ )
+
+ with (
+ can.interface.Bus(interface="virtual") as bus1,
+ can.interface.Bus(interface="virtual") as bus2,
+ ):
+ # disabling the garbage collector makes the time readings more reliable
+ gc.disable()
+
+ task = bus1.send_periodic(msg, 0.01, 1)
+ self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC)
+
+ sleep(2)
+ size = bus2.queue.qsize()
+ # About 100 messages should have been transmitted
+ self.assertTrue(
+ 80 <= size <= 120,
+ "100 +/- 20 messages should have been transmitted. But queue contained {}".format(
+ size
+ ),
+ )
+ last_msg = bus2.recv()
+ next_last_msg = bus2.recv()
+
+ # we need to reenable the garbage collector again
+ gc.enable()
+
+ # Check consecutive messages are spaced properly in time and have
+ # the same id/data
+ self.assertMessageEqual(last_msg, next_last_msg)
+
+ # Check the message id/data sent is the same as message received
+ # Set timestamp and channel to match recv'd because we don't care
+ # and they are not initialized by the can.Message constructor.
+ msg.timestamp = last_msg.timestamp
+ msg.channel = last_msg.channel
+ self.assertMessageEqual(msg, last_msg)
+
+ def test_removing_bus_tasks(self):
+ with can.interface.Bus(interface="virtual") as bus:
+ tasks = []
+ for task_i in range(10):
+ msg = can.Message(
+ is_extended_id=False,
+ arbitration_id=0x123,
+ data=[0, 1, 2, 3, 4, 5, 6, 7],
+ )
+ msg.arbitration_id = task_i
+ task = bus.send_periodic(msg, 0.1, 1)
+ tasks.append(task)
+ self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC)
+
+ assert len(bus._periodic_tasks) == 10
+
+ for task in tasks:
+ # Note calling task.stop will remove the task from the Bus's internal task management list
+ task.stop()
+
+ self.join_threads([task.thread for task in tasks], 5.0)
+
+ assert len(bus._periodic_tasks) == 0
+
+ def test_managed_tasks(self):
+ with can.interface.Bus(interface="virtual", receive_own_messages=True) as bus:
+ tasks = []
+ for task_i in range(3):
+ msg = can.Message(
+ is_extended_id=False,
+ arbitration_id=0x123,
+ data=[0, 1, 2, 3, 4, 5, 6, 7],
+ )
+ msg.arbitration_id = task_i
+ task = bus.send_periodic(msg, 0.1, 10, store_task=False)
+ tasks.append(task)
+ self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC)
+
+ assert len(bus._periodic_tasks) == 0
+
+ # Self managed tasks should still be sending messages
+ for _ in range(50):
+ received_msg = bus.recv(timeout=5.0)
+ assert received_msg is not None
+ assert received_msg.arbitration_id in {0, 1, 2}
+
+ for task in tasks:
+ task.stop()
+
+ self.join_threads([task.thread for task in tasks], 5.0)
+
+ def test_stopping_perodic_tasks(self):
+ with can.interface.Bus(interface="virtual") as bus:
+ tasks = []
+ for task_i in range(10):
+ msg = can.Message(
+ is_extended_id=False,
+ arbitration_id=0x123,
+ data=[0, 1, 2, 3, 4, 5, 6, 7],
+ )
+ msg.arbitration_id = task_i
+ task = bus.send_periodic(msg, period=0.1)
+ tasks.append(task)
+
+ assert len(bus._periodic_tasks) == 10
+ # stop half the tasks using the task object
+ for task in tasks[::2]:
+ task.stop()
+
+ assert len(bus._periodic_tasks) == 5
+
+ # stop the other half using the bus api
+ bus.stop_all_periodic_tasks(remove_tasks=False)
+ self.join_threads([task.thread for task in tasks], 5.0)
+
+ # Tasks stopped via `stop_all_periodic_tasks` with remove_tasks=False should
+ # still be associated with the bus (e.g. for restarting)
+ assert len(bus._periodic_tasks) == 5
+
+ def test_restart_perodic_tasks(self):
+ period = 0.01
+ safe_timeout = period * 5 if not IS_PYPY else 1.0
+ duration = 0.3
+
+ msg = can.Message(
+ is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7]
+ )
+
+ def _read_all_messages(_bus: "can.interfaces.virtual.VirtualBus") -> None:
+ sleep(safe_timeout)
+ while not _bus.queue.empty():
+ _bus.recv(timeout=period)
+ sleep(safe_timeout)
+
+ with can.ThreadSafeBus(interface="virtual", receive_own_messages=True) as bus:
+ task = bus.send_periodic(msg, period)
+ self.assertIsInstance(task, can.broadcastmanager.ThreadBasedCyclicSendTask)
+
+ # Test that the task is sending messages
+ sleep(safe_timeout)
+ assert not bus.queue.empty(), "messages should have been transmitted"
+
+ # Stop the task and check that messages are no longer being sent
+ bus.stop_all_periodic_tasks(remove_tasks=False)
+ _read_all_messages(bus)
+ assert bus.queue.empty(), "messages should not have been transmitted"
+
+ # Restart the task and check that messages are being sent again
+ task.start()
+ sleep(safe_timeout)
+ assert not bus.queue.empty(), "messages should have been transmitted"
+
+ # Stop the task and check that messages are no longer being sent
+ bus.stop_all_periodic_tasks(remove_tasks=False)
+ _read_all_messages(bus)
+ assert bus.queue.empty(), "messages should not have been transmitted"
+
+ # Restart the task with limited duration and wait until it stops
+ task.duration = duration
+ task.start()
+ sleep(duration + safe_timeout)
+ assert task.stopped
+ assert time.time() > task.end_time
+ assert not bus.queue.empty(), "messages should have been transmitted"
+ _read_all_messages(bus)
+ assert bus.queue.empty(), "messages should not have been transmitted"
+
+ # Restart the task and check that messages are being sent again
+ task.start()
+ sleep(safe_timeout)
+ assert not bus.queue.empty(), "messages should have been transmitted"
+
+ # Stop all tasks and wait for the thread to exit
+ bus.stop_all_periodic_tasks()
+ # Avoids issues where the thread is still running when the bus is shutdown
+ self.join_threads([task.thread], 5.0)
+
+ @unittest.skipIf(IS_CI, "fails randomly when run on CI server")
+ def test_thread_based_cyclic_send_task(self):
+ with can.ThreadSafeBus(interface="virtual") as bus:
+ msg = can.Message(
+ is_extended_id=False,
+ arbitration_id=0x123,
+ data=[0, 1, 2, 3, 4, 5, 6, 7],
+ )
+
+ # good case, bus is up
+ on_error_mock = MagicMock(return_value=False)
+ task = can.broadcastmanager.ThreadBasedCyclicSendTask(
+ bus=bus,
+ lock=bus._lock_send_periodic,
+ messages=msg,
+ period=0.1,
+ duration=3,
+ on_error=on_error_mock,
+ )
+ sleep(1)
+ on_error_mock.assert_not_called()
+ task.stop()
+
+ # bus has been shut down
+ on_error_mock = MagicMock(return_value=False)
+ task = can.broadcastmanager.ThreadBasedCyclicSendTask(
+ bus=bus,
+ lock=bus._lock_send_periodic,
+ messages=msg,
+ period=0.1,
+ duration=3,
+ on_error=on_error_mock,
+ )
+ sleep(1)
+ self.assertEqual(1, on_error_mock.call_count)
+ task.stop()
+
+ # bus is still shut down, but on_error returns True
+ on_error_mock = MagicMock(return_value=True)
+ task = can.broadcastmanager.ThreadBasedCyclicSendTask(
+ bus=bus,
+ lock=bus._lock_send_periodic,
+ messages=msg,
+ period=0.1,
+ duration=3,
+ on_error=on_error_mock,
+ )
+ sleep(1)
+ self.assertTrue(on_error_mock.call_count > 1)
+ task.stop()
+
+ def test_modifier_callback(self) -> None:
+ msg_list: list[can.Message] = []
+
+ def increment_first_byte(msg: can.Message) -> None:
+ msg.data[0] = (msg.data[0] + 1) % 256
+
+ original_msg = can.Message(
+ is_extended_id=False, arbitration_id=0x123, data=[0] * 8
+ )
+
+ with can.ThreadSafeBus(interface="virtual", receive_own_messages=True) as bus:
+ notifier = can.Notifier(bus=bus, listeners=[msg_list.append])
+ task = bus.send_periodic(
+ msgs=original_msg, period=0.001, modifier_callback=increment_first_byte
+ )
+ time.sleep(0.2)
+ task.stop()
+ notifier.stop()
+
+ self.assertEqual(b"\x01\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[0].data))
+ self.assertEqual(b"\x02\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[1].data))
+ self.assertEqual(b"\x03\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[2].data))
+ self.assertEqual(b"\x04\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[3].data))
+ self.assertEqual(b"\x05\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[4].data))
+ self.assertEqual(b"\x06\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[5].data))
+ self.assertEqual(b"\x07\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[6].data))
+
+ @staticmethod
+ def join_threads(threads: list[Thread], timeout: float) -> None:
+ stuck_threads: list[Thread] = []
+ t0 = time.perf_counter()
+ for thread in threads:
+ time_left = timeout - (time.perf_counter() - t0)
+ if time_left > 0.0:
+ thread.join(time_left)
+ if thread.is_alive():
+ if platform.python_implementation() == "CPython":
+ # print thread frame to help with debugging
+ frame = sys._current_frames()[thread.ident]
+ traceback.print_stack(frame, file=sys.stderr)
+ stuck_threads.append(thread)
+ if stuck_threads:
+ err_message = (
+ f"Threads did not stop within {timeout:.1f} seconds: "
+ f"[{', '.join([str(t) for t in stuck_threads])}]"
+ )
+ raise RuntimeError(err_message)
+
+
+if __name__ == "__main__":
unittest.main()
diff --git a/test/test_bit_timing.py b/test/test_bit_timing.py
new file mode 100644
index 000000000..514c31244
--- /dev/null
+++ b/test/test_bit_timing.py
@@ -0,0 +1,531 @@
+#!/usr/bin/env python
+
+import struct
+
+import pytest
+
+import can
+from can.interfaces.pcan.pcan import PCAN_BITRATES
+
+
+def test_sja1000():
+ """Test some values obtained using other bit timing calculators."""
+ timing = can.BitTiming(
+ f_clock=8_000_000, brp=4, tseg1=11, tseg2=4, sjw=2, nof_samples=3, strict=True
+ )
+ assert timing.f_clock == 8_000_000
+ assert timing.bitrate == 125_000
+ assert timing.brp == 4
+ assert timing.nbt == 16
+ assert timing.tseg1 == 11
+ assert timing.tseg2 == 4
+ assert timing.sjw == 2
+ assert timing.nof_samples == 3
+ assert timing.sample_point == 75
+ assert timing.btr0 == 0x43
+ assert timing.btr1 == 0xBA
+
+ timing = can.BitTiming(
+ f_clock=8_000_000, brp=1, tseg1=13, tseg2=2, sjw=1, strict=True
+ )
+ assert timing.f_clock == 8_000_000
+ assert timing.bitrate == 500_000
+ assert timing.brp == 1
+ assert timing.nbt == 16
+ assert timing.tseg1 == 13
+ assert timing.tseg2 == 2
+ assert timing.sjw == 1
+ assert timing.nof_samples == 1
+ assert timing.sample_point == 87.5
+ assert timing.btr0 == 0x00
+ assert timing.btr1 == 0x1C
+
+ timing = can.BitTiming(
+ f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1, strict=True
+ )
+ assert timing.f_clock == 8_000_000
+ assert timing.bitrate == 1_000_000
+ assert timing.brp == 1
+ assert timing.nbt == 8
+ assert timing.tseg1 == 5
+ assert timing.tseg2 == 2
+ assert timing.sjw == 1
+ assert timing.nof_samples == 1
+ assert timing.sample_point == 75
+ assert timing.btr0 == 0x00
+ assert timing.btr1 == 0x14
+
+
+def test_from_bitrate_and_segments():
+ timing = can.BitTiming.from_bitrate_and_segments(
+ f_clock=8_000_000, bitrate=125_000, tseg1=11, tseg2=4, sjw=2, nof_samples=3
+ )
+ assert timing.f_clock == 8_000_000
+ assert timing.bitrate == 125_000
+ assert timing.brp == 4
+ assert timing.nbt == 16
+ assert timing.tseg1 == 11
+ assert timing.tseg2 == 4
+ assert timing.sjw == 2
+ assert timing.nof_samples == 3
+ assert timing.sample_point == 75
+ assert timing.btr0 == 0x43
+ assert timing.btr1 == 0xBA
+
+ timing = can.BitTiming.from_bitrate_and_segments(
+ f_clock=8_000_000, bitrate=500_000, tseg1=13, tseg2=2, sjw=1
+ )
+ assert timing.f_clock == 8_000_000
+ assert timing.bitrate == 500_000
+ assert timing.brp == 1
+ assert timing.nbt == 16
+ assert timing.tseg1 == 13
+ assert timing.tseg2 == 2
+ assert timing.sjw == 1
+ assert timing.nof_samples == 1
+ assert timing.sample_point == 87.5
+ assert timing.btr0 == 0x00
+ assert timing.btr1 == 0x1C
+
+ timing = can.BitTiming.from_bitrate_and_segments(
+ f_clock=8_000_000, bitrate=1_000_000, tseg1=5, tseg2=2, sjw=1, strict=True
+ )
+ assert timing.f_clock == 8_000_000
+ assert timing.bitrate == 1_000_000
+ assert timing.brp == 1
+ assert timing.nbt == 8
+ assert timing.tseg1 == 5
+ assert timing.tseg2 == 2
+ assert timing.sjw == 1
+ assert timing.nof_samples == 1
+ assert timing.sample_point == 75
+ assert timing.btr0 == 0x00
+ assert timing.btr1 == 0x14
+
+ timing = can.BitTimingFd.from_bitrate_and_segments(
+ f_clock=80_000_000,
+ nom_bitrate=500_000,
+ nom_tseg1=119,
+ nom_tseg2=40,
+ nom_sjw=40,
+ data_bitrate=2_000_000,
+ data_tseg1=29,
+ data_tseg2=10,
+ data_sjw=10,
+ )
+
+ assert timing.f_clock == 80_000_000
+ assert timing.nom_bitrate == 500_000
+ assert timing.nom_brp == 1
+ assert timing.nbt == 160
+ assert timing.nom_tseg1 == 119
+ assert timing.nom_tseg2 == 40
+ assert timing.nom_sjw == 40
+ assert timing.nom_sample_point == 75
+ assert timing.f_clock == 80_000_000
+ assert timing.data_bitrate == 2_000_000
+ assert timing.data_brp == 1
+ assert timing.dbt == 40
+ assert timing.data_tseg1 == 29
+ assert timing.data_tseg2 == 10
+ assert timing.data_sjw == 10
+ assert timing.data_sample_point == 75
+
+ # test strict invalid
+ with pytest.raises(ValueError):
+ can.BitTimingFd.from_bitrate_and_segments(
+ f_clock=80_000_000,
+ nom_bitrate=500_000,
+ nom_tseg1=119,
+ nom_tseg2=40,
+ nom_sjw=40,
+ data_bitrate=2_000_000,
+ data_tseg1=29,
+ data_tseg2=10,
+ data_sjw=10,
+ strict=True,
+ )
+
+
+def test_can_fd():
+ # test non-strict
+ timing = can.BitTimingFd(
+ f_clock=80_000_000,
+ nom_brp=1,
+ nom_tseg1=119,
+ nom_tseg2=40,
+ nom_sjw=40,
+ data_brp=1,
+ data_tseg1=29,
+ data_tseg2=10,
+ data_sjw=10,
+ )
+
+ assert timing.f_clock == 80_000_000
+ assert timing.nom_bitrate == 500_000
+ assert timing.nom_brp == 1
+ assert timing.nbt == 160
+ assert timing.nom_tseg1 == 119
+ assert timing.nom_tseg2 == 40
+ assert timing.nom_sjw == 40
+ assert timing.nom_sample_point == 75
+ assert timing.data_bitrate == 2_000_000
+ assert timing.data_brp == 1
+ assert timing.dbt == 40
+ assert timing.data_tseg1 == 29
+ assert timing.data_tseg2 == 10
+ assert timing.data_sjw == 10
+ assert timing.data_sample_point == 75
+
+ # test strict invalid
+ with pytest.raises(ValueError):
+ can.BitTimingFd(
+ f_clock=80_000_000,
+ nom_brp=1,
+ nom_tseg1=119,
+ nom_tseg2=40,
+ nom_sjw=40,
+ data_brp=1,
+ data_tseg1=29,
+ data_tseg2=10,
+ data_sjw=10,
+ strict=True,
+ )
+
+ # test strict valid
+ timing = can.BitTimingFd(
+ f_clock=80_000_000,
+ nom_brp=2,
+ nom_tseg1=59,
+ nom_tseg2=20,
+ nom_sjw=20,
+ data_brp=2,
+ data_tseg1=14,
+ data_tseg2=5,
+ data_sjw=5,
+ strict=True,
+ )
+ assert timing.f_clock == 80_000_000
+ assert timing.nom_bitrate == 500_000
+ assert timing.nom_brp == 2
+ assert timing.nbt == 80
+ assert timing.nom_tseg1 == 59
+ assert timing.nom_tseg2 == 20
+ assert timing.nom_sjw == 20
+ assert timing.nom_sample_point == 75
+ assert timing.data_bitrate == 2_000_000
+ assert timing.data_brp == 2
+ assert timing.dbt == 20
+ assert timing.data_tseg1 == 14
+ assert timing.data_tseg2 == 5
+ assert timing.data_sjw == 5
+ assert timing.data_sample_point == 75
+
+
+def test_from_btr():
+ timing = can.BitTiming.from_registers(f_clock=8_000_000, btr0=0x00, btr1=0x14)
+ assert timing.bitrate == 1_000_000
+ assert timing.brp == 1
+ assert timing.nbt == 8
+ assert timing.tseg1 == 5
+ assert timing.tseg2 == 2
+ assert timing.sjw == 1
+ assert timing.sample_point == 75
+ assert timing.btr0 == 0x00
+ assert timing.btr1 == 0x14
+
+
+def test_btr_persistence():
+ f_clock = 8_000_000
+ for btr0btr1 in PCAN_BITRATES.values():
+ btr0, btr1 = struct.pack(">H", btr0btr1.value)
+
+ t = can.BitTiming.from_registers(f_clock, btr0, btr1)
+ assert t.btr0 == btr0
+ assert t.btr1 == btr1
+
+
+def test_from_sample_point():
+ timing = can.BitTiming.from_sample_point(
+ f_clock=16_000_000,
+ bitrate=500_000,
+ sample_point=69.0,
+ )
+ assert timing.f_clock == 16_000_000
+ assert timing.bitrate == 500_000
+ assert 68 < timing.sample_point < 70
+
+ fd_timing = can.BitTimingFd.from_sample_point(
+ f_clock=80_000_000,
+ nom_bitrate=1_000_000,
+ nom_sample_point=75.0,
+ data_bitrate=8_000_000,
+ data_sample_point=70.0,
+ )
+ assert fd_timing.f_clock == 80_000_000
+ assert fd_timing.nom_bitrate == 1_000_000
+ assert 74 < fd_timing.nom_sample_point < 76
+ assert fd_timing.data_bitrate == 8_000_000
+ assert 69 < fd_timing.data_sample_point < 71
+
+ # check that there is a solution for every sample point
+ for sp in range(50, 100):
+ can.BitTiming.from_sample_point(
+ f_clock=16_000_000, bitrate=500_000, sample_point=sp
+ )
+
+ # check that there is a solution for every sample point
+ for nsp in range(50, 100):
+ for dsp in range(50, 100):
+ can.BitTimingFd.from_sample_point(
+ f_clock=80_000_000,
+ nom_bitrate=500_000,
+ nom_sample_point=nsp,
+ data_bitrate=2_000_000,
+ data_sample_point=dsp,
+ )
+
+
+def test_iterate_from_sample_point():
+ for sp in range(50, 100):
+ solutions = list(
+ can.BitTiming.iterate_from_sample_point(
+ f_clock=16_000_000,
+ bitrate=500_000,
+ sample_point=sp,
+ )
+ )
+ assert len(solutions) >= 2
+
+ for nsp in range(50, 100):
+ for dsp in range(50, 100):
+ solutions = list(
+ can.BitTimingFd.iterate_from_sample_point(
+ f_clock=80_000_000,
+ nom_bitrate=500_000,
+ nom_sample_point=nsp,
+ data_bitrate=2_000_000,
+ data_sample_point=dsp,
+ )
+ )
+
+ assert len(solutions) >= 2
+
+
+def test_equality():
+ t1 = can.BitTiming.from_registers(f_clock=8_000_000, btr0=0x00, btr1=0x14)
+ t2 = can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1, nof_samples=1)
+ t3 = can.BitTiming(
+ f_clock=16_000_000, brp=2, tseg1=5, tseg2=2, sjw=1, nof_samples=1
+ )
+ assert t1 == t2
+ assert t1 != t3
+ assert t2 != t3
+ assert t1 != 10
+
+ t4 = can.BitTimingFd(
+ f_clock=80_000_000,
+ nom_brp=1,
+ nom_tseg1=119,
+ nom_tseg2=40,
+ nom_sjw=40,
+ data_brp=1,
+ data_tseg1=29,
+ data_tseg2=10,
+ data_sjw=10,
+ )
+ t5 = can.BitTimingFd(
+ f_clock=80_000_000,
+ nom_brp=1,
+ nom_tseg1=119,
+ nom_tseg2=40,
+ nom_sjw=40,
+ data_brp=1,
+ data_tseg1=29,
+ data_tseg2=10,
+ data_sjw=10,
+ )
+ t6 = can.BitTimingFd.from_sample_point(
+ f_clock=80_000_000,
+ nom_bitrate=1_000_000,
+ nom_sample_point=75.0,
+ data_bitrate=8_000_000,
+ data_sample_point=70.0,
+ )
+ assert t4 == t5
+ assert t4 != t6
+ assert t4 != t1
+
+
+def test_string_representation():
+ timing = can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1)
+ assert str(timing) == (
+ "BR: 1_000_000 bit/s, SP: 75.00%, BRP: 1, TSEG1: 5, TSEG2: 2, SJW: 1, "
+ "BTR: 0014h, CLK: 8MHz"
+ )
+
+ fd_timing = can.BitTimingFd(
+ f_clock=80_000_000,
+ nom_brp=1,
+ nom_tseg1=119,
+ nom_tseg2=40,
+ nom_sjw=40,
+ data_brp=1,
+ data_tseg1=29,
+ data_tseg2=10,
+ data_sjw=10,
+ )
+ assert str(fd_timing) == (
+ "NBR: 500_000 bit/s, NSP: 75.00%, NBRP: 1, NTSEG1: 119, NTSEG2: 40, NSJW: 40, "
+ "DBR: 2_000_000 bit/s, DSP: 75.00%, DBRP: 1, DTSEG1: 29, DTSEG2: 10, DSJW: 10, "
+ "CLK: 80MHz"
+ )
+
+
+def test_repr():
+ timing = can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1)
+ assert repr(timing) == (
+ "can.BitTiming(f_clock=8000000, brp=1, tseg1=5, tseg2=2, sjw=1, nof_samples=1)"
+ )
+
+ fd_timing = can.BitTimingFd(
+ f_clock=80_000_000,
+ nom_brp=1,
+ nom_tseg1=119,
+ nom_tseg2=40,
+ nom_sjw=40,
+ data_brp=1,
+ data_tseg1=29,
+ data_tseg2=10,
+ data_sjw=10,
+ )
+ assert repr(fd_timing) == (
+ "can.BitTimingFd(f_clock=80000000, nom_brp=1, nom_tseg1=119, nom_tseg2=40, "
+ "nom_sjw=40, data_brp=1, data_tseg1=29, data_tseg2=10, data_sjw=10)"
+ )
+
+
+def test_hash():
+ _timings = {
+ can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1, nof_samples=1),
+ can.BitTimingFd(
+ f_clock=80_000_000,
+ nom_brp=1,
+ nom_tseg1=119,
+ nom_tseg2=40,
+ nom_sjw=40,
+ data_brp=1,
+ data_tseg1=29,
+ data_tseg2=10,
+ data_sjw=10,
+ ),
+ }
+
+
+def test_mapping():
+ timing = can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1)
+ timing_dict = dict(timing)
+ assert timing_dict["f_clock"] == timing["f_clock"]
+ assert timing_dict["brp"] == timing["brp"]
+ assert timing_dict["tseg1"] == timing["tseg1"]
+ assert timing_dict["tseg2"] == timing["tseg2"]
+ assert timing_dict["sjw"] == timing["sjw"]
+ assert timing == can.BitTiming(**timing_dict)
+
+ fd_timing = can.BitTimingFd(
+ f_clock=80_000_000,
+ nom_brp=1,
+ nom_tseg1=119,
+ nom_tseg2=40,
+ nom_sjw=40,
+ data_brp=1,
+ data_tseg1=29,
+ data_tseg2=10,
+ data_sjw=10,
+ )
+ fd_timing_dict = dict(fd_timing)
+ assert fd_timing_dict["f_clock"] == fd_timing["f_clock"]
+ assert fd_timing_dict["nom_brp"] == fd_timing["nom_brp"]
+ assert fd_timing_dict["nom_tseg1"] == fd_timing["nom_tseg1"]
+ assert fd_timing_dict["nom_tseg2"] == fd_timing["nom_tseg2"]
+ assert fd_timing_dict["nom_sjw"] == fd_timing["nom_sjw"]
+ assert fd_timing_dict["data_brp"] == fd_timing["data_brp"]
+ assert fd_timing_dict["data_tseg1"] == fd_timing["data_tseg1"]
+ assert fd_timing_dict["data_tseg2"] == fd_timing["data_tseg2"]
+ assert fd_timing_dict["data_sjw"] == fd_timing["data_sjw"]
+ assert fd_timing == can.BitTimingFd(**fd_timing_dict)
+
+
+def test_oscillator_tolerance():
+ timing = can.BitTiming(f_clock=16_000_000, brp=2, tseg1=10, tseg2=5, sjw=4)
+ osc_tol = timing.oscillator_tolerance(
+ node_loop_delay_ns=250,
+ bus_length_m=10.0,
+ )
+ assert osc_tol == pytest.approx(1.23, abs=1e-2)
+
+ fd_timing = can.BitTimingFd(
+ f_clock=80_000_000,
+ nom_brp=5,
+ nom_tseg1=27,
+ nom_tseg2=4,
+ nom_sjw=4,
+ data_brp=5,
+ data_tseg1=6,
+ data_tseg2=1,
+ data_sjw=1,
+ )
+ osc_tol = fd_timing.oscillator_tolerance(
+ node_loop_delay_ns=250,
+ bus_length_m=10.0,
+ )
+ assert osc_tol == pytest.approx(0.48, abs=1e-2)
+
+
+def test_recreate_with_f_clock():
+ timing_8mhz = can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1)
+ timing_16mhz = timing_8mhz.recreate_with_f_clock(f_clock=16_000_000)
+ assert timing_8mhz.bitrate == timing_16mhz.bitrate
+ assert timing_8mhz.sample_point == timing_16mhz.sample_point
+ assert (timing_8mhz.sjw / timing_8mhz.nbt) == pytest.approx(
+ timing_16mhz.sjw / timing_16mhz.nbt, abs=1e-3
+ )
+ assert timing_8mhz.nof_samples == timing_16mhz.nof_samples
+
+ timing_16mhz = can.BitTiming(
+ f_clock=16000000, brp=2, tseg1=12, tseg2=3, sjw=3, nof_samples=1
+ )
+ timing_8mhz = timing_16mhz.recreate_with_f_clock(f_clock=8_000_000)
+ assert timing_8mhz.bitrate == timing_16mhz.bitrate
+ assert timing_8mhz.sample_point == timing_16mhz.sample_point
+ assert (timing_8mhz.sjw / timing_8mhz.nbt) == pytest.approx(
+ timing_16mhz.sjw / timing_16mhz.nbt, abs=1e-2
+ )
+ assert timing_8mhz.nof_samples == timing_16mhz.nof_samples
+
+ fd_timing_80mhz = can.BitTimingFd(
+ f_clock=80_000_000,
+ nom_brp=5,
+ nom_tseg1=27,
+ nom_tseg2=4,
+ nom_sjw=4,
+ data_brp=5,
+ data_tseg1=6,
+ data_tseg2=1,
+ data_sjw=1,
+ )
+ fd_timing_60mhz = fd_timing_80mhz.recreate_with_f_clock(f_clock=60_000_000)
+ assert fd_timing_80mhz.nom_bitrate == fd_timing_60mhz.nom_bitrate
+ assert fd_timing_80mhz.nom_sample_point == pytest.approx(
+ fd_timing_60mhz.nom_sample_point, abs=1.0
+ )
+ assert (fd_timing_80mhz.nom_sjw / fd_timing_80mhz.nbt) == pytest.approx(
+ fd_timing_60mhz.nom_sjw / fd_timing_60mhz.nbt, abs=1e-2
+ )
+ assert fd_timing_80mhz.data_bitrate == fd_timing_60mhz.data_bitrate
+ assert fd_timing_80mhz.data_sample_point == pytest.approx(
+ fd_timing_60mhz.data_sample_point, abs=1.0
+ )
+ assert (fd_timing_80mhz.data_sjw / fd_timing_80mhz.dbt) == pytest.approx(
+ fd_timing_60mhz.data_sjw / fd_timing_60mhz.dbt, abs=1e-2
+ )
diff --git a/test/test_bridge.py b/test/test_bridge.py
new file mode 100644
index 000000000..ee41bd949
--- /dev/null
+++ b/test/test_bridge.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python
+
+"""
+This module tests the functions inside of bridge.py
+"""
+
+import random
+import string
+import sys
+import threading
+import time
+from time import sleep as real_sleep
+import unittest.mock
+
+import can
+import can.bridge
+from can.interfaces import virtual
+
+from .message_helper import ComparingMessagesTestCase
+
+
+class TestBridgeScriptModule(unittest.TestCase, ComparingMessagesTestCase):
+
+ TIMEOUT = 3.0
+
+ def __init__(self, *args, **kwargs):
+ unittest.TestCase.__init__(self, *args, **kwargs)
+ ComparingMessagesTestCase.__init__(
+ self,
+ allowed_timestamp_delta=None,
+ preserves_channel=False,
+ )
+
+ def setUp(self) -> None:
+ self.stop_event = threading.Event()
+
+ self.channel1 = "".join(random.choices(string.ascii_letters, k=8))
+ self.channel2 = "".join(random.choices(string.ascii_letters, k=8))
+
+ self.cli_args = [
+ "--bus1-interface",
+ "virtual",
+ "--bus1-channel",
+ self.channel1,
+ "--bus2-interface",
+ "virtual",
+ "--bus2-channel",
+ self.channel2,
+ ]
+
+ self.testmsg = can.Message(
+ arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True
+ )
+
+ def fake_sleep(self, duration):
+ """A fake replacement for time.sleep that checks periodically
+ whether self.stop_event is set, and raises KeyboardInterrupt
+ if so.
+
+ This allows tests to simulate an interrupt (like Ctrl+C)
+ during long sleeps, in a controlled and responsive way.
+ """
+ interval = 0.05 # Small interval for responsiveness
+ t_wakeup = time.perf_counter() + duration
+ while time.perf_counter() < t_wakeup:
+ if self.stop_event.is_set():
+ raise KeyboardInterrupt("Simulated interrupt from fake_sleep")
+ real_sleep(interval)
+
+ def test_bridge(self):
+ with (
+ unittest.mock.patch("can.bridge.time.sleep", new=self.fake_sleep),
+ unittest.mock.patch("can.bridge.sys.argv", [sys.argv[0], *self.cli_args]),
+ ):
+ # start script
+ thread = threading.Thread(target=can.bridge.main)
+ thread.start()
+
+ # wait until script instantiates virtual buses
+ t0 = time.perf_counter()
+ while True:
+ with virtual.channels_lock:
+ if (
+ self.channel1 in virtual.channels
+ and self.channel2 in virtual.channels
+ ):
+ break
+ if time.perf_counter() > t0 + 2.0:
+ raise TimeoutError("Bridge script did not create virtual buses")
+ real_sleep(0.2)
+
+ # create buses with the same channels as in scripts
+ with (
+ can.interfaces.virtual.VirtualBus(self.channel1) as bus1,
+ can.interfaces.virtual.VirtualBus(self.channel2) as bus2,
+ ):
+ # send test message to bus1, it should be received on bus2
+ bus1.send(self.testmsg)
+ recv_msg = bus2.recv(self.TIMEOUT)
+ self.assertMessageEqual(self.testmsg, recv_msg)
+
+ # assert that both buses are empty
+ self.assertIsNone(bus1.recv(0))
+ self.assertIsNone(bus2.recv(0))
+
+ # send test message to bus2, it should be received on bus1
+ bus2.send(self.testmsg)
+ recv_msg = bus1.recv(self.TIMEOUT)
+ self.assertMessageEqual(self.testmsg, recv_msg)
+
+ # assert that both buses are empty
+ self.assertIsNone(bus1.recv(0))
+ self.assertIsNone(bus2.recv(0))
+
+ # stop the bridge script
+ self.stop_event.set()
+ thread.join()
+
+ # assert that the virtual buses were closed
+ with virtual.channels_lock:
+ self.assertNotIn(self.channel1, virtual.channels)
+ self.assertNotIn(self.channel2, virtual.channels)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_bus.py b/test/test_bus.py
new file mode 100644
index 000000000..6a09a6deb
--- /dev/null
+++ b/test/test_bus.py
@@ -0,0 +1,23 @@
+import gc
+from unittest.mock import patch
+
+import can
+
+
+def test_bus_ignore_config():
+ with patch.object(
+ target=can.util, attribute="load_config", side_effect=can.util.load_config
+ ):
+ with can.Bus(interface="virtual", ignore_config=True):
+ assert not can.util.load_config.called
+
+ with can.Bus(interface="virtual"):
+ assert can.util.load_config.called
+
+
+@patch.object(can.bus.BusABC, "shutdown")
+def test_bus_attempts_self_cleanup(mock_shutdown):
+ bus = can.Bus(interface="virtual")
+ del bus
+ gc.collect()
+ mock_shutdown.assert_called()
diff --git a/test/test_cantact.py b/test/test_cantact.py
new file mode 100644
index 000000000..f90655ae5
--- /dev/null
+++ b/test/test_cantact.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+
+"""
+Tests for CANtact interfaces
+"""
+
+import unittest
+
+import can
+from can.interfaces import cantact
+
+
+class CantactTest(unittest.TestCase):
+ def test_bus_creation(self):
+ bus = can.Bus(channel=0, interface="cantact", _testing=True)
+ self.assertIsInstance(bus, cantact.CantactBus)
+ self.assertEqual(bus.protocol, can.CanProtocol.CAN_20)
+
+ cantact.MockInterface.set_bitrate.assert_called()
+ cantact.MockInterface.set_bit_timing.assert_not_called()
+ cantact.MockInterface.set_enabled.assert_called()
+ cantact.MockInterface.set_monitor.assert_called()
+ cantact.MockInterface.start.assert_called()
+
+ def test_bus_creation_bittiming(self):
+ cantact.MockInterface.set_bitrate.reset_mock()
+
+ bt = can.BitTiming(f_clock=24_000_000, brp=3, tseg1=13, tseg2=2, sjw=1)
+ bus = can.Bus(channel=0, interface="cantact", timing=bt, _testing=True)
+
+ self.assertIsInstance(bus, cantact.CantactBus)
+ self.assertEqual(bus.protocol, can.CanProtocol.CAN_20)
+
+ cantact.MockInterface.set_bitrate.assert_not_called()
+ cantact.MockInterface.set_bit_timing.assert_called()
+ cantact.MockInterface.set_enabled.assert_called()
+ cantact.MockInterface.set_monitor.assert_called()
+ cantact.MockInterface.start.assert_called()
+
+ def test_transmit(self):
+ bus = can.Bus(channel=0, interface="cantact", _testing=True)
+ msg = can.Message(
+ arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True
+ )
+ bus.send(msg)
+ cantact.MockInterface.send.assert_called()
+
+ def test_recv(self):
+ bus = can.Bus(channel=0, interface="cantact", _testing=True)
+ frame = bus.recv(timeout=0.5)
+ cantact.MockInterface.recv.assert_called()
+ self.assertIsInstance(frame, can.Message)
+
+ def test_recv_timeout(self):
+ bus = can.Bus(channel=0, interface="cantact", _testing=True)
+ frame = bus.recv(timeout=0.0)
+ cantact.MockInterface.recv.assert_called()
+ self.assertIsNone(frame)
+
+ def test_shutdown(self):
+ bus = can.Bus(channel=0, interface="cantact", _testing=True)
+ bus.shutdown()
+ cantact.MockInterface.stop.assert_called()
diff --git a/test/test_cli.py b/test/test_cli.py
new file mode 100644
index 000000000..ecc662832
--- /dev/null
+++ b/test/test_cli.py
@@ -0,0 +1,154 @@
+import argparse
+import unittest
+from unittest.mock import patch
+
+from can.cli import add_bus_arguments, create_bus_from_namespace
+
+
+class TestCliUtils(unittest.TestCase):
+ def test_add_bus_arguments(self):
+ parser = argparse.ArgumentParser()
+ add_bus_arguments(parser, filter_arg=True, prefix="test")
+
+ parsed_args = parser.parse_args(
+ [
+ "--test-channel",
+ "0",
+ "--test-interface",
+ "vector",
+ "--test-timing",
+ "f_clock=8000000",
+ "brp=4",
+ "tseg1=11",
+ "tseg2=4",
+ "sjw=2",
+ "nof_samples=3",
+ "--test-filter",
+ "100:7FF",
+ "200~7F0",
+ "--test-bus-kwargs",
+ "app_name=MyApp",
+ "serial=1234",
+ ]
+ )
+
+ self.assertNotIn("channel", parsed_args)
+ self.assertNotIn("test_bitrate", parsed_args)
+ self.assertNotIn("test_data_bitrate", parsed_args)
+ self.assertNotIn("test_fd", parsed_args)
+
+ self.assertEqual(parsed_args.test_channel, "0")
+ self.assertEqual(parsed_args.test_interface, "vector")
+ self.assertEqual(parsed_args.test_timing.f_clock, 8000000)
+ self.assertEqual(parsed_args.test_timing.brp, 4)
+ self.assertEqual(parsed_args.test_timing.tseg1, 11)
+ self.assertEqual(parsed_args.test_timing.tseg2, 4)
+ self.assertEqual(parsed_args.test_timing.sjw, 2)
+ self.assertEqual(parsed_args.test_timing.nof_samples, 3)
+ self.assertEqual(len(parsed_args.test_can_filters), 2)
+ self.assertEqual(parsed_args.test_can_filters[0]["can_id"], 0x100)
+ self.assertEqual(parsed_args.test_can_filters[0]["can_mask"], 0x7FF)
+ self.assertEqual(parsed_args.test_can_filters[1]["can_id"], 0x200 | 0x20000000)
+ self.assertEqual(
+ parsed_args.test_can_filters[1]["can_mask"], 0x7F0 & 0x20000000
+ )
+ self.assertEqual(parsed_args.test_bus_kwargs["app_name"], "MyApp")
+ self.assertEqual(parsed_args.test_bus_kwargs["serial"], 1234)
+
+ def test_add_bus_arguments_no_prefix(self):
+ parser = argparse.ArgumentParser()
+ add_bus_arguments(parser, filter_arg=True)
+
+ parsed_args = parser.parse_args(
+ [
+ "--channel",
+ "0",
+ "--interface",
+ "vector",
+ "--timing",
+ "f_clock=8000000",
+ "brp=4",
+ "tseg1=11",
+ "tseg2=4",
+ "sjw=2",
+ "nof_samples=3",
+ "--filter",
+ "100:7FF",
+ "200~7F0",
+ "--bus-kwargs",
+ "app_name=MyApp",
+ "serial=1234",
+ ]
+ )
+
+ self.assertEqual(parsed_args.channel, "0")
+ self.assertEqual(parsed_args.interface, "vector")
+ self.assertEqual(parsed_args.timing.f_clock, 8000000)
+ self.assertEqual(parsed_args.timing.brp, 4)
+ self.assertEqual(parsed_args.timing.tseg1, 11)
+ self.assertEqual(parsed_args.timing.tseg2, 4)
+ self.assertEqual(parsed_args.timing.sjw, 2)
+ self.assertEqual(parsed_args.timing.nof_samples, 3)
+ self.assertEqual(len(parsed_args.can_filters), 2)
+ self.assertEqual(parsed_args.can_filters[0]["can_id"], 0x100)
+ self.assertEqual(parsed_args.can_filters[0]["can_mask"], 0x7FF)
+ self.assertEqual(parsed_args.can_filters[1]["can_id"], 0x200 | 0x20000000)
+ self.assertEqual(parsed_args.can_filters[1]["can_mask"], 0x7F0 & 0x20000000)
+ self.assertEqual(parsed_args.bus_kwargs["app_name"], "MyApp")
+ self.assertEqual(parsed_args.bus_kwargs["serial"], 1234)
+
+ @patch("can.Bus")
+ def test_create_bus_from_namespace(self, mock_bus):
+ namespace = argparse.Namespace(
+ test_channel="vcan0",
+ test_interface="virtual",
+ test_bitrate=500000,
+ test_data_bitrate=2000000,
+ test_fd=True,
+ test_can_filters=[{"can_id": 0x100, "can_mask": 0x7FF}],
+ test_bus_kwargs={"app_name": "MyApp", "serial": 1234},
+ )
+
+ create_bus_from_namespace(namespace, prefix="test")
+
+ mock_bus.assert_called_once_with(
+ channel="vcan0",
+ interface="virtual",
+ bitrate=500000,
+ data_bitrate=2000000,
+ fd=True,
+ can_filters=[{"can_id": 0x100, "can_mask": 0x7FF}],
+ app_name="MyApp",
+ serial=1234,
+ single_handle=True,
+ )
+
+ @patch("can.Bus")
+ def test_create_bus_from_namespace_no_prefix(self, mock_bus):
+ namespace = argparse.Namespace(
+ channel="vcan0",
+ interface="virtual",
+ bitrate=500000,
+ data_bitrate=2000000,
+ fd=True,
+ can_filters=[{"can_id": 0x100, "can_mask": 0x7FF}],
+ bus_kwargs={"app_name": "MyApp", "serial": 1234},
+ )
+
+ create_bus_from_namespace(namespace)
+
+ mock_bus.assert_called_once_with(
+ channel="vcan0",
+ interface="virtual",
+ bitrate=500000,
+ data_bitrate=2000000,
+ fd=True,
+ can_filters=[{"can_id": 0x100, "can_mask": 0x7FF}],
+ app_name="MyApp",
+ serial=1234,
+ single_handle=True,
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_cyclic_socketcan.py b/test/test_cyclic_socketcan.py
new file mode 100644
index 000000000..f19ce95b9
--- /dev/null
+++ b/test/test_cyclic_socketcan.py
@@ -0,0 +1,622 @@
+#!/usr/bin/env python
+
+"""
+This module tests multiple message cyclic send tasks.
+"""
+import time
+import unittest
+
+import can
+
+from .config import TEST_INTERFACE_SOCKETCAN
+
+
+@unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan")
+class CyclicSocketCan(unittest.TestCase):
+ BITRATE = 500000
+ TIMEOUT = 0.1
+
+ INTERFACE_1 = "socketcan"
+ CHANNEL_1 = "vcan0"
+ INTERFACE_2 = "socketcan"
+ CHANNEL_2 = "vcan0"
+
+ PERIOD = 1.0
+
+ DELTA = 0.01
+
+ def _find_start_index(self, tx_messages, message):
+ """
+ :param tx_messages:
+ The list of messages that were passed to the periodic backend
+ :param message:
+ The message whose data we wish to match and align to
+
+ :returns: start index in the tx_messages
+ """
+ start_index = -1
+ for index, tx_message in enumerate(tx_messages):
+ if tx_message.data == message.data:
+ start_index = index
+ break
+ return start_index
+
+ def setUp(self):
+ self._send_bus = can.Bus(
+ interface=self.INTERFACE_1, channel=self.CHANNEL_1, bitrate=self.BITRATE
+ )
+ self._recv_bus = can.Bus(
+ interface=self.INTERFACE_2, channel=self.CHANNEL_2, bitrate=self.BITRATE
+ )
+
+ def tearDown(self):
+ self._send_bus.shutdown()
+ self._recv_bus.shutdown()
+
+ def test_cyclic_initializer_list(self):
+ messages = []
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+ )
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22],
+ is_extended_id=False,
+ )
+ )
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33],
+ is_extended_id=False,
+ )
+ )
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44],
+ is_extended_id=False,
+ )
+ )
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55],
+ is_extended_id=False,
+ )
+ )
+
+ task = self._send_bus.send_periodic(messages, self.PERIOD)
+ self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC)
+
+ results = []
+ for _ in range(len(messages) * 2):
+ result = self._recv_bus.recv(self.PERIOD * 2)
+ if result:
+ results.append(result)
+
+ task.stop()
+
+ # Find starting index for each
+ start_index = self._find_start_index(messages, results[0])
+ self.assertTrue(start_index != -1)
+
+ # Now go through the partitioned results and assert that they're equal
+ for rx_index, rx_message in enumerate(results):
+ tx_message = messages[start_index]
+
+ self.assertIsNotNone(rx_message)
+ self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id)
+ self.assertEqual(tx_message.dlc, rx_message.dlc)
+ self.assertEqual(tx_message.data, rx_message.data)
+ self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id)
+ self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame)
+ self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame)
+ self.assertEqual(tx_message.is_fd, rx_message.is_fd)
+
+ start_index = (start_index + 1) % len(messages)
+
+ def test_cyclic_initializer_tuple(self):
+ messages = []
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+ )
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22],
+ is_extended_id=False,
+ )
+ )
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33],
+ is_extended_id=False,
+ )
+ )
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44],
+ is_extended_id=False,
+ )
+ )
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55],
+ is_extended_id=False,
+ )
+ )
+ messages = tuple(messages)
+
+ self.assertIsInstance(messages, tuple)
+
+ task = self._send_bus.send_periodic(messages, self.PERIOD)
+ self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC)
+
+ results = []
+ for _ in range(len(messages) * 2):
+ result = self._recv_bus.recv(self.PERIOD * 2)
+ if result:
+ results.append(result)
+
+ task.stop()
+
+ # Find starting index for each
+ start_index = self._find_start_index(messages, results[0])
+ self.assertTrue(start_index != -1)
+
+ # Now go through the partitioned results and assert that they're equal
+ for rx_index, rx_message in enumerate(results):
+ tx_message = messages[start_index]
+
+ self.assertIsNotNone(rx_message)
+ self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id)
+ self.assertEqual(tx_message.dlc, rx_message.dlc)
+ self.assertEqual(tx_message.data, rx_message.data)
+ self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id)
+ self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame)
+ self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame)
+ self.assertEqual(tx_message.is_fd, rx_message.is_fd)
+
+ start_index = (start_index + 1) % len(messages)
+
+ def test_cyclic_initializer_message(self):
+ message = can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+
+ task = self._send_bus.send_periodic(message, self.PERIOD)
+ self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC)
+
+ # Take advantage of kernel's queueing mechanisms
+ time.sleep(4 * self.PERIOD)
+ task.stop()
+
+ for _ in range(4):
+ tx_message = message
+ rx_message = self._recv_bus.recv(self.TIMEOUT)
+
+ self.assertIsNotNone(rx_message)
+ self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id)
+ self.assertEqual(tx_message.dlc, rx_message.dlc)
+ self.assertEqual(tx_message.data, rx_message.data)
+ self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id)
+ self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame)
+ self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame)
+ self.assertEqual(tx_message.is_fd, rx_message.is_fd)
+
+ def test_cyclic_initializer_invalid_none(self):
+ with self.assertRaises(ValueError):
+ task = self._send_bus.send_periodic(None, self.PERIOD)
+
+ def test_cyclic_initializer_invalid_empty_list(self):
+ with self.assertRaises(ValueError):
+ task = self._send_bus.send_periodic([], self.PERIOD)
+
+ def test_cyclic_initializer_different_arbitration_ids(self):
+ messages = []
+ messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+ )
+ messages.append(
+ can.Message(
+ arbitration_id=0x3E1,
+ data=[0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE],
+ is_extended_id=False,
+ )
+ )
+ with self.assertRaises(ValueError):
+ task = self._send_bus.send_periodic(messages, self.PERIOD)
+
+ def test_start_already_started_task(self):
+ messages_a = can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+
+ task_a = self._send_bus.send_periodic(messages_a, self.PERIOD)
+ time.sleep(0.1)
+
+ # Task restarting is permitted as of #1440
+ task_a.start()
+ task_a.stop()
+
+ def test_create_same_id(self):
+ messages_a = can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+
+ messages_b = can.Message(
+ arbitration_id=0x401,
+ data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22],
+ is_extended_id=False,
+ )
+
+ task_a = self._send_bus.send_periodic(messages_a, self.PERIOD)
+ self.assertIsInstance(task_a, can.broadcastmanager.CyclicSendTaskABC)
+ task_b = self._send_bus.send_periodic(messages_b, self.PERIOD)
+ self.assertIsInstance(task_b, can.broadcastmanager.CyclicSendTaskABC)
+
+ time.sleep(self.PERIOD * 4)
+
+ task_a.stop()
+ task_b.stop()
+
+ msgs = []
+ for _ in range(4):
+ msg = self._recv_bus.recv(self.PERIOD * 2)
+ self.assertIsNotNone(msg)
+
+ msgs.append(msg)
+
+ self.assertTrue(len(msgs) >= 4)
+
+ # Both messages should be recevied on the bus,
+ # even with the same arbitration id
+ msg_a_data_present = msg_b_data_present = False
+ for rx_message in msgs:
+ self.assertTrue(
+ rx_message.arbitration_id
+ == messages_a.arbitration_id
+ == messages_b.arbitration_id
+ )
+ if rx_message.data == messages_a.data:
+ msg_a_data_present = True
+ if rx_message.data == messages_b.data:
+ msg_b_data_present = True
+
+ self.assertTrue(msg_a_data_present)
+ self.assertTrue(msg_b_data_present)
+
+ def test_modify_data_list(self):
+ messages_odd = []
+ messages_odd.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+ )
+ messages_odd.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33],
+ is_extended_id=False,
+ )
+ )
+ messages_odd.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55],
+ is_extended_id=False,
+ )
+ )
+ messages_even = []
+ messages_even.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22],
+ is_extended_id=False,
+ )
+ )
+ messages_even.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44],
+ is_extended_id=False,
+ )
+ )
+ messages_even.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x66, 0x66, 0x66, 0x66, 0x66, 0x66],
+ is_extended_id=False,
+ )
+ )
+
+ task = self._send_bus.send_periodic(messages_odd, self.PERIOD)
+ self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC)
+
+ results_odd = []
+ results_even = []
+ for _ in range(len(messages_odd) * 2):
+ result = self._recv_bus.recv(self.PERIOD * 2)
+ if result:
+ results_odd.append(result)
+
+ task.modify_data(messages_even)
+ for _ in range(len(messages_even) * 2):
+ result = self._recv_bus.recv(self.PERIOD * 2)
+ if result:
+ results_even.append(result)
+
+ task.stop()
+
+ # Make sure we received some messages
+ self.assertTrue(len(results_even) != 0)
+ self.assertTrue(len(results_odd) != 0)
+
+ # Find starting index for each
+ start_index_even = self._find_start_index(messages_even, results_even[0])
+ self.assertTrue(start_index_even != -1)
+
+ start_index_odd = self._find_start_index(messages_odd, results_odd[0])
+ self.assertTrue(start_index_odd != -1)
+
+ # Now go through the partitioned results and assert that they're equal
+ for rx_index, rx_message in enumerate(results_even):
+ tx_message = messages_even[start_index_even]
+
+ self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id)
+ self.assertEqual(tx_message.dlc, rx_message.dlc)
+ self.assertEqual(tx_message.data, rx_message.data)
+ self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id)
+ self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame)
+ self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame)
+ self.assertEqual(tx_message.is_fd, rx_message.is_fd)
+
+ start_index_even = (start_index_even + 1) % len(messages_even)
+
+ if rx_index != 0:
+ prev_rx_message = results_even[rx_index - 1]
+ # Assert timestamps are within the expected period
+ self.assertTrue(
+ abs(
+ (rx_message.timestamp - prev_rx_message.timestamp) - self.PERIOD
+ )
+ <= self.DELTA
+ )
+
+ for rx_index, rx_message in enumerate(results_odd):
+ tx_message = messages_odd[start_index_odd]
+
+ self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id)
+ self.assertEqual(tx_message.dlc, rx_message.dlc)
+ self.assertEqual(tx_message.data, rx_message.data)
+ self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id)
+ self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame)
+ self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame)
+ self.assertEqual(tx_message.is_fd, rx_message.is_fd)
+
+ start_index_odd = (start_index_odd + 1) % len(messages_odd)
+
+ if rx_index != 0:
+ prev_rx_message = results_odd[rx_index - 1]
+ # Assert timestamps are within the expected period
+ self.assertTrue(
+ abs(
+ (rx_message.timestamp - prev_rx_message.timestamp) - self.PERIOD
+ )
+ <= self.DELTA
+ )
+
+ def test_modify_data_message(self):
+ message_odd = can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+ message_even = can.Message(
+ arbitration_id=0x401,
+ data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22],
+ is_extended_id=False,
+ )
+ task = self._send_bus.send_periodic(message_odd, self.PERIOD)
+ self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC)
+
+ results_odd = []
+ results_even = []
+ for _ in range(1 * 4):
+ result = self._recv_bus.recv(self.PERIOD * 2)
+ if result:
+ results_odd.append(result)
+
+ task.modify_data(message_even)
+ for _ in range(1 * 4):
+ result = self._recv_bus.recv(self.PERIOD * 2)
+ if result:
+ results_even.append(result)
+
+ task.stop()
+
+ # Now go through the partitioned results and assert that they're equal
+ for rx_index, rx_message in enumerate(results_even):
+ tx_message = message_even
+
+ self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id)
+ self.assertEqual(tx_message.dlc, rx_message.dlc)
+ self.assertEqual(tx_message.data, rx_message.data)
+ self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id)
+ self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame)
+ self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame)
+ self.assertEqual(tx_message.is_fd, rx_message.is_fd)
+
+ if rx_index != 0:
+ prev_rx_message = results_even[rx_index - 1]
+ # Assert timestamps are within the expected period
+ self.assertTrue(
+ abs(
+ (rx_message.timestamp - prev_rx_message.timestamp) - self.PERIOD
+ )
+ <= self.DELTA
+ )
+
+ for rx_index, rx_message in enumerate(results_odd):
+ tx_message = message_odd
+
+ self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id)
+ self.assertEqual(tx_message.dlc, rx_message.dlc)
+ self.assertEqual(tx_message.data, rx_message.data)
+ self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id)
+ self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame)
+ self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame)
+ self.assertEqual(tx_message.is_fd, rx_message.is_fd)
+
+ if rx_index != 0:
+ prev_rx_message = results_odd[rx_index - 1]
+ # Assert timestamps are within the expected period
+ self.assertTrue(
+ abs(
+ (rx_message.timestamp - prev_rx_message.timestamp) - self.PERIOD
+ )
+ <= self.DELTA
+ )
+
+ def test_modify_data_invalid(self):
+ message = can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+ task = self._send_bus.send_periodic(message, self.PERIOD)
+ self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC)
+
+ time.sleep(2 * self.PERIOD)
+
+ with self.assertRaises(ValueError):
+ task.modify_data(None)
+
+ def test_modify_data_unequal_lengths(self):
+ message = can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+ new_messages = []
+ new_messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+ )
+ new_messages.append(
+ can.Message(
+ arbitration_id=0x401,
+ data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22],
+ is_extended_id=False,
+ )
+ )
+
+ task = self._send_bus.send_periodic(message, self.PERIOD)
+ self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC)
+
+ time.sleep(2 * self.PERIOD)
+
+ with self.assertRaises(ValueError):
+ task.modify_data(new_messages)
+
+ def test_modify_data_different_arbitration_id_than_original(self):
+ old_message = can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+ new_message = can.Message(
+ arbitration_id=0x3E1,
+ data=[0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE],
+ is_extended_id=False,
+ )
+
+ task = self._send_bus.send_periodic(old_message, self.PERIOD)
+ self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC)
+
+ time.sleep(2 * self.PERIOD)
+
+ with self.assertRaises(ValueError):
+ task.modify_data(new_message)
+
+ def test_stop_all_periodic_tasks_and_remove_task(self):
+ message_a = can.Message(
+ arbitration_id=0x401,
+ data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11],
+ is_extended_id=False,
+ )
+ message_b = can.Message(
+ arbitration_id=0x402,
+ data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22],
+ is_extended_id=False,
+ )
+ message_c = can.Message(
+ arbitration_id=0x403,
+ data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33],
+ is_extended_id=False,
+ )
+
+ # Start Tasks
+ task_a = self._send_bus.send_periodic(message_a, self.PERIOD)
+ task_b = self._send_bus.send_periodic(message_b, self.PERIOD)
+ task_c = self._send_bus.send_periodic(message_c, self.PERIOD)
+
+ self.assertIsInstance(task_a, can.broadcastmanager.ModifiableCyclicTaskABC)
+ self.assertIsInstance(task_b, can.broadcastmanager.ModifiableCyclicTaskABC)
+ self.assertIsInstance(task_c, can.broadcastmanager.ModifiableCyclicTaskABC)
+
+ for _ in range(6):
+ _ = self._recv_bus.recv(self.PERIOD)
+
+ # Stop all tasks and delete
+ self._send_bus.stop_all_periodic_tasks(remove_tasks=True)
+
+ # Now wait for a few periods, after which we should definitely not
+ # receive any CAN messages
+ time.sleep(4 * self.PERIOD)
+
+ # If we successfully deleted everything, then we will eventually read
+ # 0 messages.
+ successfully_stopped = False
+ for _ in range(6):
+ rx_message = self._recv_bus.recv(self.PERIOD)
+
+ if rx_message is None:
+ successfully_stopped = True
+ break
+ self.assertTrue(successfully_stopped, "Still received messages after stopping")
+
+ # None of the tasks should still be associated with the bus
+ self.assertEqual(0, len(self._send_bus._periodic_tasks))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_detect_available_configs.py b/test/test_detect_available_configs.py
index 417a3eb3e..f6590c276 100644
--- a/test/test_detect_available_configs.py
+++ b/test/test_detect_available_configs.py
@@ -1,58 +1,67 @@
#!/usr/bin/env python
-# coding: utf-8
"""
This module tests :meth:`can.BusABC._detect_available_configs` and
:meth:`can.BusABC.detect_available_configs`.
"""
-from __future__ import absolute_import
-
-import sys
import unittest
-if sys.version_info.major > 2:
- basestring = str
from can import detect_available_configs
-from .config import IS_LINUX
+from .config import IS_CI, IS_UNIX, TEST_INTERFACE_SOCKETCAN
class TestDetectAvailableConfigs(unittest.TestCase):
-
def test_count_returned(self):
# At least virtual has to always return at least one interface
- self.assertGreaterEqual (len(detect_available_configs() ), 1)
- self.assertEqual (len(detect_available_configs(interfaces=[]) ), 0)
- self.assertGreaterEqual (len(detect_available_configs(interfaces='virtual') ), 1)
- self.assertGreaterEqual (len(detect_available_configs(interfaces=['virtual']) ), 1)
- self.assertGreaterEqual (len(detect_available_configs(interfaces=None) ), 1)
+ self.assertGreaterEqual(len(detect_available_configs()), 1)
+ self.assertEqual(len(detect_available_configs(interfaces=[])), 0)
+ self.assertGreaterEqual(len(detect_available_configs(interfaces="virtual")), 1)
+ self.assertGreaterEqual(
+ len(detect_available_configs(interfaces=["virtual"])), 1
+ )
+ self.assertGreaterEqual(len(detect_available_configs(interfaces=None)), 1)
def test_general_values(self):
configs = detect_available_configs()
for config in configs:
- self.assertIn('interface', config)
- self.assertIn('channel', config)
- self.assertIsInstance(config['interface'], basestring)
+ self.assertIn("interface", config)
+ self.assertIn("channel", config)
def test_content_virtual(self):
- configs = detect_available_configs(interfaces='virtual')
+ configs = detect_available_configs(interfaces="virtual")
+ self.assertGreaterEqual(len(configs), 1)
for config in configs:
- self.assertEqual(config['interface'], 'virtual')
+ self.assertEqual(config["interface"], "virtual")
+
+ def test_content_udp_multicast(self):
+ configs = detect_available_configs(interfaces="udp_multicast")
+ for config in configs:
+ self.assertEqual(config["interface"], "udp_multicast")
def test_content_socketcan(self):
- configs = detect_available_configs(interfaces='socketcan')
+ configs = detect_available_configs(interfaces="socketcan")
for config in configs:
- self.assertEqual(config['interface'], 'socketcan')
+ self.assertEqual(config["interface"], "socketcan")
+
+ def test_count_udp_multicast(self):
+ configs = detect_available_configs(interfaces="udp_multicast")
+ if IS_UNIX:
+ self.assertGreaterEqual(len(configs), 2)
+ else:
+ self.assertEqual(len(configs), 0)
- @unittest.skipUnless(IS_LINUX, "socketcan is only available on Linux")
+ @unittest.skipUnless(
+ TEST_INTERFACE_SOCKETCAN and IS_CI, "this setup is very specific"
+ )
def test_socketcan_on_ci_server(self):
- configs = detect_available_configs(interfaces='socketcan')
+ configs = detect_available_configs(interfaces="socketcan")
self.assertGreaterEqual(len(configs), 1)
- self.assertIn('vcan0', [config['channel'] for config in configs])
+ self.assertIn("vcan0", [config["channel"] for config in configs])
- # see TestSocketCanHelpers.test_find_available_interfaces()
+ # see TestSocketCanHelpers.test_find_available_interfaces() too
-if __name__ == '__main__':
+if __name__ == "__main__":
unittest.main()
diff --git a/test/test_interface.py b/test/test_interface.py
new file mode 100644
index 000000000..271e90b1b
--- /dev/null
+++ b/test/test_interface.py
@@ -0,0 +1,42 @@
+import importlib
+from unittest.mock import patch
+
+import pytest
+
+import can
+from can.interfaces import BACKENDS
+
+
+@pytest.fixture(params=(BACKENDS.keys()))
+def constructor(request):
+ mod, cls = BACKENDS[request.param]
+
+ try:
+ module = importlib.import_module(mod)
+ constructor = getattr(module, cls)
+ except:
+ pytest.skip("Unable to load interface")
+
+ return constructor
+
+
+@pytest.fixture
+def interface(constructor):
+ class MockInterface(constructor):
+ def __init__(self):
+ pass
+
+ def __del__(self):
+ pass
+
+ return MockInterface()
+
+
+@patch.object(can.bus.BusABC, "shutdown")
+def test_interface_calls_parent_shutdown(mock_shutdown, interface):
+ try:
+ interface.shutdown()
+ except:
+ pass
+ finally:
+ mock_shutdown.assert_called()
diff --git a/test/test_interface_canalystii.py b/test/test_interface_canalystii.py
new file mode 100755
index 000000000..3bdd281d2
--- /dev/null
+++ b/test/test_interface_canalystii.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+
+""" """
+
+import unittest
+from ctypes import c_ubyte
+from unittest.mock import call, patch
+
+import canalystii as driver # low-level driver module, mock out this layer
+
+import can
+from can.interfaces.canalystii import CANalystIIBus
+
+
+def create_mock_device():
+ return patch("canalystii.CanalystDevice")
+
+
+class CanalystIITest(unittest.TestCase):
+ def test_initialize_from_constructor(self):
+ with create_mock_device() as mock_device:
+ instance = mock_device.return_value
+ bus = CANalystIIBus(bitrate=1000000)
+
+ self.assertEqual(bus.protocol, can.CanProtocol.CAN_20)
+
+ instance.init.assert_has_calls(
+ [
+ call(0, bitrate=1000000),
+ call(1, bitrate=1000000),
+ ]
+ )
+
+ def test_initialize_single_channel_only(self):
+ for channel in 0, 1:
+ with create_mock_device() as mock_device:
+ instance = mock_device.return_value
+ bus = CANalystIIBus(channel, bitrate=1000000)
+
+ self.assertEqual(bus.protocol, can.CanProtocol.CAN_20)
+ instance.init.assert_called_once_with(channel, bitrate=1000000)
+
+ def test_initialize_with_timing_registers(self):
+ with create_mock_device() as mock_device:
+ instance = mock_device.return_value
+ timing = can.BitTiming.from_registers(
+ f_clock=8_000_000, btr0=0x03, btr1=0x6F
+ )
+ bus = CANalystIIBus(bitrate=None, timing=timing)
+ self.assertEqual(bus.protocol, can.CanProtocol.CAN_20)
+
+ instance.init.assert_has_calls(
+ [
+ call(0, timing0=0x03, timing1=0x6F),
+ call(1, timing0=0x03, timing1=0x6F),
+ ]
+ )
+
+ def test_missing_bitrate(self):
+ with self.assertRaises(ValueError) as cm:
+ bus = CANalystIIBus(0, bitrate=None, timing=None)
+ self.assertIn("bitrate", str(cm.exception))
+
+ def test_receive_message(self):
+ driver_message = driver.Message(
+ can_id=0x333,
+ timestamp=1000000,
+ time_flag=1,
+ send_type=0,
+ remote=False,
+ extended=False,
+ data_len=8,
+ data=(c_ubyte * 8)(*range(8)),
+ )
+
+ with create_mock_device() as mock_device:
+ instance = mock_device.return_value
+ instance.receive.return_value = [driver_message]
+ bus = CANalystIIBus(bitrate=1000000)
+ msg = bus.recv(0)
+ self.assertEqual(driver_message.can_id, msg.arbitration_id)
+ self.assertEqual(bytearray(driver_message.data), msg.data)
+
+ def test_send_message(self):
+ message = can.Message(arbitration_id=0x123, data=[3] * 8, is_extended_id=False)
+
+ with create_mock_device() as mock_device:
+ instance = mock_device.return_value
+ bus = CANalystIIBus(channel=0, bitrate=5000000)
+ bus.send(message)
+ instance.send.assert_called_once()
+
+ (channel, driver_messages, _timeout), _kwargs = instance.send.call_args
+ self.assertEqual(0, channel)
+
+ self.assertEqual(1, len(driver_messages))
+
+ driver_message = driver_messages[0]
+ self.assertEqual(message.arbitration_id, driver_message.can_id)
+ self.assertEqual(message.data, bytearray(driver_message.data))
+ self.assertEqual(8, driver_message.data_len)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_interface_ixxat.py b/test/test_interface_ixxat.py
new file mode 100644
index 000000000..90b5f7adc
--- /dev/null
+++ b/test/test_interface_ixxat.py
@@ -0,0 +1,77 @@
+#!/usr/bin/env python
+
+"""
+Unittest for ixxat interface.
+
+Run only this test:
+python setup.py test --addopts "--verbose -s test/test_interface_ixxat.py"
+"""
+
+import unittest
+
+import can
+
+
+class SoftwareTestCase(unittest.TestCase):
+ """
+ Test cases that test the software only and do not rely on an existing/connected hardware.
+ """
+
+ def setUp(self):
+ try:
+ bus = can.Bus(interface="ixxat", channel=0)
+ bus.shutdown()
+ except can.CanInterfaceNotImplementedError:
+ raise unittest.SkipTest("not available on this platform")
+
+ def test_bus_creation(self):
+ # channel must be >= 0
+ with self.assertRaises(ValueError):
+ can.Bus(interface="ixxat", channel=-1)
+
+ # rx_fifo_size must be > 0
+ with self.assertRaises(ValueError):
+ can.Bus(interface="ixxat", channel=0, rx_fifo_size=0)
+
+ # tx_fifo_size must be > 0
+ with self.assertRaises(ValueError):
+ can.Bus(interface="ixxat", channel=0, tx_fifo_size=0)
+
+
+class HardwareTestCase(unittest.TestCase):
+ """
+ Test cases that rely on an existing/connected hardware.
+ """
+
+ def setUp(self):
+ try:
+ bus = can.Bus(interface="ixxat", channel=0)
+ bus.shutdown()
+ except can.CanInterfaceNotImplementedError:
+ raise unittest.SkipTest("not available on this platform")
+
+ def test_bus_creation(self):
+ try:
+ configs = can.detect_available_configs("ixxat")
+ if configs:
+ for interface_kwargs in configs:
+ bus = can.Bus(**interface_kwargs)
+ bus.shutdown()
+ else:
+ raise unittest.SkipTest("No adapters were detected")
+ except can.CanInterfaceNotImplementedError:
+ raise unittest.SkipTest("not available on this platform")
+
+ def test_bus_creation_incorrect_channel(self):
+ # non-existent channel -> use arbitrary high value
+ with self.assertRaises(can.CanInitializationError):
+ can.Bus(interface="ixxat", channel=0xFFFF)
+
+ def test_send_after_shutdown(self):
+ with can.Bus(interface="ixxat", channel=0) as bus:
+ with self.assertRaises(can.CanOperationError):
+ bus.send(can.Message(arbitration_id=0x3FF, dlc=0))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_interface_ixxat_fd.py b/test/test_interface_ixxat_fd.py
new file mode 100644
index 000000000..7274498aa
--- /dev/null
+++ b/test/test_interface_ixxat_fd.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+
+"""
+Unittest for ixxat interface using fd option.
+
+Run only this test:
+python setup.py test --addopts "--verbose -s test/test_interface_ixxat_fd.py"
+"""
+
+import unittest
+
+import can
+
+
+class SoftwareTestCase(unittest.TestCase):
+ """
+ Test cases that test the software only and do not rely on an existing/connected hardware.
+ """
+
+ def setUp(self):
+ try:
+ bus = can.Bus(interface="ixxat", fd=True, channel=0)
+ bus.shutdown()
+ except can.CanInterfaceNotImplementedError:
+ raise unittest.SkipTest("not available on this platform")
+
+ def test_bus_creation(self):
+ # channel must be >= 0
+ with self.assertRaises(ValueError):
+ can.Bus(interface="ixxat", fd=True, channel=-1)
+
+ # rx_fifo_size must be > 0
+ with self.assertRaises(ValueError):
+ can.Bus(interface="ixxat", fd=True, channel=0, rx_fifo_size=0)
+
+ # tx_fifo_size must be > 0
+ with self.assertRaises(ValueError):
+ can.Bus(interface="ixxat", fd=True, channel=0, tx_fifo_size=0)
+
+
+class HardwareTestCase(unittest.TestCase):
+ """
+ Test cases that rely on an existing/connected hardware.
+ """
+
+ def setUp(self):
+ try:
+ bus = can.Bus(interface="ixxat", fd=True, channel=0)
+ bus.shutdown()
+ except can.CanInterfaceNotImplementedError:
+ raise unittest.SkipTest("not available on this platform")
+
+ def test_bus_creation(self):
+ # non-existent channel -> use arbitrary high value
+ with self.assertRaises(can.CanInitializationError):
+ can.Bus(interface="ixxat", fd=True, channel=0xFFFF)
+
+ def test_send_after_shutdown(self):
+ with can.Bus(interface="ixxat", fd=True, channel=0) as bus:
+ with self.assertRaises(can.CanOperationError):
+ bus.send(can.Message(arbitration_id=0x3FF, dlc=0))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_interface_virtual.py b/test/test_interface_virtual.py
new file mode 100644
index 000000000..c1d842180
--- /dev/null
+++ b/test/test_interface_virtual.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python
+
+"""
+This module tests :meth:`can.interface.virtual`.
+"""
+
+import unittest
+
+from can import Bus, Message
+
+EXAMPLE_MSG1 = Message(timestamp=1639739471.5565314, arbitration_id=0x481, data=b"\x01")
+
+
+class TestMessageFiltering(unittest.TestCase):
+ def setUp(self):
+ self.node1 = Bus("test", interface="virtual", preserve_timestamps=True)
+ self.node2 = Bus("test", interface="virtual")
+
+ def tearDown(self):
+ self.node1.shutdown()
+ self.node2.shutdown()
+
+ def test_sendmsg(self):
+ self.node2.send(EXAMPLE_MSG1)
+ r = self.node1.recv(0.1)
+ assert r.timestamp != EXAMPLE_MSG1.timestamp
+ assert r.arbitration_id == EXAMPLE_MSG1.arbitration_id
+ assert r.data == EXAMPLE_MSG1.data
+
+ def test_sendmsg_preserve_timestamp(self):
+ self.node1.send(EXAMPLE_MSG1)
+ r = self.node2.recv(0.1)
+ assert r.timestamp == EXAMPLE_MSG1.timestamp
+ assert r.arbitration_id == EXAMPLE_MSG1.arbitration_id
+ assert r.data == EXAMPLE_MSG1.data
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_kvaser.py b/test/test_kvaser.py
index 229381934..1ad035dec 100644
--- a/test/test_kvaser.py
+++ b/test/test_kvaser.py
@@ -1,33 +1,28 @@
#!/usr/bin/env python
-# coding: utf-8
-"""
-"""
+""" """
import ctypes
import time
-import logging
import unittest
-try:
- from unittest.mock import Mock, patch
-except ImportError:
- from mock import patch, Mock
+from unittest.mock import Mock
import pytest
import can
-from can.interfaces.kvaser import canlib
-from can.interfaces.kvaser import constants
+from can.interfaces.kvaser import canlib, constants
class KvaserTest(unittest.TestCase):
-
def setUp(self):
canlib.canGetNumberOfChannels = KvaserTest.canGetNumberOfChannels
canlib.canOpenChannel = Mock(return_value=0)
canlib.canIoCtl = Mock(return_value=0)
+ canlib.canIoCtlInit = Mock(return_value=0)
canlib.kvReadTimer = Mock()
+ canlib.canSetBusParamsC200 = Mock()
canlib.canSetBusParams = Mock()
+ canlib.canSetBusParamsFd = Mock()
canlib.canBusOn = Mock()
canlib.canBusOff = Mock()
canlib.canClose = Mock()
@@ -37,10 +32,12 @@ def setUp(self):
canlib.canWriteSync = Mock()
canlib.canWrite = self.canWrite
canlib.canReadWait = self.canReadWait
+ canlib.canGetBusStatistics = Mock()
+ canlib.canRequestBusStatistics = Mock()
self.msg = {}
self.msg_in_cue = None
- self.bus = can.Bus(channel=0, bustype='kvaser')
+ self.bus = can.Bus(channel=0, interface="kvaser")
def tearDown(self):
if self.bus:
@@ -49,9 +46,30 @@ def tearDown(self):
def test_bus_creation(self):
self.assertIsInstance(self.bus, canlib.KvaserBus)
+ self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20)
self.assertTrue(canlib.canOpenChannel.called)
self.assertTrue(canlib.canBusOn.called)
+ def test_bus_creation_illegal_channel_name(self):
+ # Test if the bus constructor is able to deal with non-ASCII characters
+ def canGetChannelDataMock(
+ channel: ctypes.c_int,
+ param: ctypes.c_int,
+ buf: ctypes.c_void_p,
+ bufsize: ctypes.c_size_t,
+ ):
+ if param == constants.canCHANNELDATA_DEVDESCR_ASCII:
+ buf_char_ptr = ctypes.cast(buf, ctypes.POINTER(ctypes.c_char))
+ for i, char in enumerate(b"hello\x7a\xcb"):
+ buf_char_ptr[i] = char
+
+ canlib.canGetChannelData = canGetChannelDataMock
+ bus = can.Bus(channel=0, interface="kvaser")
+
+ self.assertTrue(bus.channel_info.startswith("hello"))
+
+ bus.shutdown()
+
def test_bus_shutdown(self):
self.bus.shutdown()
self.assertTrue(canlib.canBusOff.called)
@@ -60,67 +78,58 @@ def test_bus_shutdown(self):
def test_filter_setup(self):
# No filter in constructor
expected_args = [
- ((0, 0, 0, 0),), # Disable filtering STD on read handle
- ((0, 0, 0, 1),), # Disable filtering EXT on read handle
- ((0, 0, 0, 0),), # Disable filtering STD on write handle
- ((0, 0, 0, 1),), # Disable filtering EXT on write handle
+ ((0, 0, 0, 0),), # Disable filtering STD on read handle
+ ((0, 0, 0, 1),), # Disable filtering EXT on read handle
+ ((0, 0, 0, 0),), # Disable filtering STD on write handle
+ ((0, 0, 0, 1),), # Disable filtering EXT on write handle
]
- self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list,
- expected_args)
+ self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list, expected_args)
# One filter, will be handled by canlib
canlib.canSetAcceptanceFilter.reset_mock()
- self.bus.set_filters([
- {'can_id': 0x8, 'can_mask': 0xff, 'extended': True}
- ])
+ self.bus.set_filters([{"can_id": 0x8, "can_mask": 0xFF, "extended": True}])
expected_args = [
- ((0, 0x8, 0xff, 1),), # Enable filtering EXT on read handle
- ((0, 0x8, 0xff, 1),), # Enable filtering EXT on write handle
+ ((0, 0x8, 0xFF, 1),), # Enable filtering EXT on read handle
+ ((0, 0x8, 0xFF, 1),), # Enable filtering EXT on write handle
]
- self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list,
- expected_args)
+ self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list, expected_args)
# Multiple filters, will be handled in Python
canlib.canSetAcceptanceFilter.reset_mock()
multiple_filters = [
- {'can_id': 0x8, 'can_mask': 0xff},
- {'can_id': 0x9, 'can_mask': 0xff}
+ {"can_id": 0x8, "can_mask": 0xFF},
+ {"can_id": 0x9, "can_mask": 0xFF},
]
self.bus.set_filters(multiple_filters)
expected_args = [
- ((0, 0, 0, 0),), # Disable filtering STD on read handle
- ((0, 0, 0, 1),), # Disable filtering EXT on read handle
- ((0, 0, 0, 0),), # Disable filtering STD on write handle
- ((0, 0, 0, 1),), # Disable filtering EXT on write handle
+ ((0, 0, 0, 0),), # Disable filtering STD on read handle
+ ((0, 0, 0, 1),), # Disable filtering EXT on read handle
+ ((0, 0, 0, 0),), # Disable filtering STD on write handle
+ ((0, 0, 0, 1),), # Disable filtering EXT on write handle
]
- self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list,
- expected_args)
+ self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list, expected_args)
def test_send_extended(self):
msg = can.Message(
- arbitration_id=0xc0ffee,
- data=[0, 25, 0, 1, 3, 1, 4],
- extended_id=True)
+ arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4], is_extended_id=True
+ )
self.bus.send(msg)
- self.assertEqual(self.msg['arb_id'], 0xc0ffee)
- self.assertEqual(self.msg['dlc'], 7)
- self.assertEqual(self.msg['flags'], constants.canMSG_EXT)
- self.assertSequenceEqual(self.msg['data'], [0, 25, 0, 1, 3, 1, 4])
+ self.assertEqual(self.msg["arb_id"], 0xC0FFEE)
+ self.assertEqual(self.msg["dlc"], 7)
+ self.assertEqual(self.msg["flags"], constants.canMSG_EXT)
+ self.assertSequenceEqual(self.msg["data"], [0, 25, 0, 1, 3, 1, 4])
def test_send_standard(self):
- msg = can.Message(
- arbitration_id=0x321,
- data=[50, 51],
- extended_id=False)
+ msg = can.Message(arbitration_id=0x321, data=[50, 51], is_extended_id=False)
self.bus.send(msg)
- self.assertEqual(self.msg['arb_id'], 0x321)
- self.assertEqual(self.msg['dlc'], 2)
- self.assertEqual(self.msg['flags'], constants.canMSG_STD)
- self.assertSequenceEqual(self.msg['data'], [50, 51])
+ self.assertEqual(self.msg["arb_id"], 0x321)
+ self.assertEqual(self.msg["dlc"], 2)
+ self.assertEqual(self.msg["flags"], constants.canMSG_STD)
+ self.assertSequenceEqual(self.msg["data"], [50, 51])
@pytest.mark.timeout(3.0)
def test_recv_no_message(self):
@@ -128,47 +137,168 @@ def test_recv_no_message(self):
def test_recv_extended(self):
self.msg_in_cue = can.Message(
- arbitration_id=0xc0ffef,
- data=[1, 2, 3, 4, 5, 6, 7, 8],
- extended_id=True)
+ arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True
+ )
now = time.time()
msg = self.bus.recv()
- self.assertEqual(msg.arbitration_id, 0xc0ffef)
+ self.assertEqual(msg.arbitration_id, 0xC0FFEF)
self.assertEqual(msg.dlc, 8)
- self.assertEqual(msg.id_type, True)
+ self.assertEqual(msg.is_extended_id, True)
self.assertSequenceEqual(msg.data, self.msg_in_cue.data)
self.assertTrue(now - 1 < msg.timestamp < now + 1)
def test_recv_standard(self):
self.msg_in_cue = can.Message(
- arbitration_id=0x123,
- data=[100, 101],
- extended_id=False)
+ arbitration_id=0x123, data=[100, 101], is_extended_id=False
+ )
msg = self.bus.recv()
self.assertEqual(msg.arbitration_id, 0x123)
self.assertEqual(msg.dlc, 2)
- self.assertEqual(msg.id_type, False)
+ self.assertEqual(msg.is_extended_id, False)
self.assertSequenceEqual(msg.data, [100, 101])
-
+
def test_available_configs(self):
configs = canlib.KvaserBus._detect_available_configs()
expected = [
- {'interface': 'kvaser', 'channel': 0},
- {'interface': 'kvaser', 'channel': 1}
+ {
+ "interface": "kvaser",
+ "channel": 0,
+ "dongle_channel": 1,
+ "device_name": "",
+ "serial": 0,
+ },
+ {
+ "interface": "kvaser",
+ "channel": 1,
+ "dongle_channel": 1,
+ "device_name": "",
+ "serial": 0,
+ },
]
self.assertListEqual(configs, expected)
+ def test_canfd_default_data_bitrate(self):
+ canlib.canSetBusParams.reset_mock()
+ canlib.canSetBusParamsFd.reset_mock()
+ bus = can.Bus(channel=0, interface="kvaser", fd=True)
+ self.assertEqual(bus.protocol, can.CanProtocol.CAN_FD)
+ canlib.canSetBusParams.assert_called_once_with(
+ 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0
+ )
+ canlib.canSetBusParamsFd.assert_called_once_with(
+ 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0
+ )
+
+ def test_can_timing(self):
+ canlib.canSetBusParams.reset_mock()
+ canlib.canSetBusParamsFd.reset_mock()
+ timing = can.BitTiming.from_bitrate_and_segments(
+ f_clock=16_000_000,
+ bitrate=125_000,
+ tseg1=13,
+ tseg2=2,
+ sjw=1,
+ )
+ can.Bus(channel=0, interface="kvaser", timing=timing)
+ canlib.canSetBusParamsC200.assert_called_once_with(0, timing.btr0, timing.btr1)
+
+ def test_canfd_timing(self):
+ canlib.canSetBusParams.reset_mock()
+ canlib.canSetBusParamsFd.reset_mock()
+ canlib.canOpenChannel.reset_mock()
+ timing = can.BitTimingFd.from_bitrate_and_segments(
+ f_clock=80_000_000,
+ nom_bitrate=500_000,
+ nom_tseg1=68,
+ nom_tseg2=11,
+ nom_sjw=10,
+ data_bitrate=2_000_000,
+ data_tseg1=10,
+ data_tseg2=9,
+ data_sjw=8,
+ )
+ can.Bus(channel=0, interface="kvaser", timing=timing)
+ canlib.canSetBusParams.assert_called_once_with(0, 500_000, 68, 11, 10, 1, 0)
+ canlib.canSetBusParamsFd.assert_called_once_with(0, 2_000_000, 10, 9, 8)
+ canlib.canOpenChannel.assert_called_with(
+ 0, constants.canOPEN_CAN_FD | constants.canOPEN_ACCEPT_VIRTUAL
+ )
+
+ def test_canfd_non_iso(self):
+ canlib.canSetBusParams.reset_mock()
+ canlib.canSetBusParamsFd.reset_mock()
+ canlib.canOpenChannel.reset_mock()
+ timing = can.BitTimingFd.from_bitrate_and_segments(
+ f_clock=80_000_000,
+ nom_bitrate=500_000,
+ nom_tseg1=68,
+ nom_tseg2=11,
+ nom_sjw=10,
+ data_bitrate=2_000_000,
+ data_tseg1=10,
+ data_tseg2=9,
+ data_sjw=8,
+ )
+ bus = can.Bus(channel=0, interface="kvaser", timing=timing, fd_non_iso=True)
+ self.assertEqual(bus.protocol, can.CanProtocol.CAN_FD_NON_ISO)
+ canlib.canSetBusParams.assert_called_once_with(0, 500_000, 68, 11, 10, 1, 0)
+ canlib.canSetBusParamsFd.assert_called_once_with(0, 2_000_000, 10, 9, 8)
+ canlib.canOpenChannel.assert_called_with(
+ 0, constants.canOPEN_CAN_FD_NONISO | constants.canOPEN_ACCEPT_VIRTUAL
+ )
+
+ def test_canfd_nondefault_data_bitrate(self):
+ canlib.canSetBusParams.reset_mock()
+ canlib.canSetBusParamsFd.reset_mock()
+ data_bitrate = 2000000
+ bus = can.Bus(channel=0, interface="kvaser", fd=True, data_bitrate=data_bitrate)
+ self.assertEqual(bus.protocol, can.CanProtocol.CAN_FD)
+ bitrate_constant = canlib.BITRATE_FD[data_bitrate]
+ canlib.canSetBusParams.assert_called_once_with(
+ 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0
+ )
+ canlib.canSetBusParamsFd.assert_called_once_with(0, bitrate_constant, 0, 0, 0)
+
+ def test_canfd_custom_data_bitrate(self):
+ canlib.canSetBusParams.reset_mock()
+ canlib.canSetBusParamsFd.reset_mock()
+ data_bitrate = 123456
+ can.Bus(channel=0, interface="kvaser", fd=True, data_bitrate=data_bitrate)
+ canlib.canSetBusParams.assert_called_once_with(
+ 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0
+ )
+ canlib.canSetBusParamsFd.assert_called_once_with(0, data_bitrate, 0, 0, 0)
+
+ def test_bus_get_stats(self):
+ stats = self.bus.get_stats()
+ self.assertTrue(canlib.canRequestBusStatistics.called)
+ self.assertTrue(canlib.canGetBusStatistics.called)
+ self.assertIsInstance(stats, canlib.structures.BusStatistics)
+
+ def test_bus_no_init_access(self):
+ canlib.canOpenChannel.reset_mock()
+ bus = can.Bus(interface="kvaser", channel=0, no_init_access=True)
+
+ self.assertGreater(canlib.canOpenChannel.call_count, 0)
+ for call in canlib.canOpenChannel.call_args_list:
+ self.assertEqual(
+ call[0][1] & constants.canOPEN_NO_INIT_ACCESS,
+ constants.canOPEN_NO_INIT_ACCESS,
+ )
+
+ bus.shutdown()
+
@staticmethod
def canGetNumberOfChannels(count):
count._obj.value = 2
def canWrite(self, handle, arb_id, buf, dlc, flags):
- self.msg['arb_id'] = arb_id
- self.msg['dlc'] = dlc
- self.msg['flags'] = flags
- self.msg['data'] = bytearray(buf._obj)
+ self.msg["arb_id"] = arb_id
+ self.msg["dlc"] = dlc
+ self.msg["flags"] = flags
+ self.msg["data"] = bytearray(buf._obj)
def canReadWait(self, handle, arb_id, data, dlc, flags, timestamp, timeout):
if not self.msg_in_cue:
@@ -178,7 +308,7 @@ def canReadWait(self, handle, arb_id, data, dlc, flags, timestamp, timeout):
dlc._obj.value = self.msg_in_cue.dlc
data._obj.raw = self.msg_in_cue.data
flags_temp = 0
- if self.msg_in_cue.id_type:
+ if self.msg_in_cue.is_extended_id:
flags_temp |= constants.canMSG_EXT
else:
flags_temp |= constants.canMSG_STD
@@ -191,5 +321,6 @@ def canReadWait(self, handle, arb_id, data, dlc, flags, timestamp, timeout):
return constants.canOK
-if __name__ == '__main__':
+
+if __name__ == "__main__":
unittest.main()
diff --git a/test/test_load_config.py b/test/test_load_config.py
new file mode 100644
index 000000000..3c850a730
--- /dev/null
+++ b/test/test_load_config.py
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+
+import shutil
+import tempfile
+import unittest
+import unittest.mock
+from tempfile import NamedTemporaryFile
+
+import can
+
+
+class LoadConfigTest(unittest.TestCase):
+ configuration_in = {
+ "default": {"interface": "serial", "channel": "0"},
+ "one": {"interface": "kvaser", "channel": "1", "bitrate": 100000},
+ "two": {"channel": "2"},
+ }
+ configuration_out = {
+ "default": {"interface": "serial", "channel": 0},
+ "one": {"interface": "kvaser", "channel": 1, "bitrate": 100000},
+ "two": {"channel": 2},
+ }
+
+ def setUp(self):
+ # Create a temporary directory
+ self.test_dir = tempfile.mkdtemp()
+
+ def tearDown(self):
+ # Remove the directory after the test
+ shutil.rmtree(self.test_dir)
+
+ def _gen_configration_file(self, sections):
+ with NamedTemporaryFile(
+ mode="w", dir=self.test_dir, delete=False
+ ) as tmp_config_file:
+ content = []
+ for section in sections:
+ content.append(f"[{section}]")
+ for k, v in self.configuration_in[section].items():
+ content.append(f"{k} = {v}")
+ tmp_config_file.write("\n".join(content))
+ return tmp_config_file.name
+
+ def _dict_to_env(self, d):
+ return {f"CAN_{k.upper()}": str(v) for k, v in d.items()}
+
+ def test_config_default(self):
+ tmp_config = self._gen_configration_file(["default"])
+ config = can.util.load_config(path=tmp_config)
+ self.assertEqual(config, self.configuration_out["default"])
+
+ def test_config_whole_default(self):
+ tmp_config = self._gen_configration_file(self.configuration_in)
+ config = can.util.load_config(path=tmp_config)
+ self.assertEqual(config, self.configuration_out["default"])
+
+ def test_config_whole_context(self):
+ tmp_config = self._gen_configration_file(self.configuration_in)
+ config = can.util.load_config(path=tmp_config, context="one")
+ self.assertEqual(config, self.configuration_out["one"])
+
+ def test_config_merge_context(self):
+ tmp_config = self._gen_configration_file(self.configuration_in)
+ config = can.util.load_config(path=tmp_config, context="two")
+ expected = self.configuration_out["default"].copy()
+ expected.update(self.configuration_out["two"])
+ self.assertEqual(config, expected)
+
+ def test_config_merge_environment_to_context(self):
+ tmp_config = self._gen_configration_file(self.configuration_in)
+ env_data = {"interface": "serial", "bitrate": 125000}
+ env_dict = self._dict_to_env(env_data)
+ with unittest.mock.patch.dict("os.environ", env_dict):
+ config = can.util.load_config(path=tmp_config, context="one")
+ expected = self.configuration_out["one"].copy()
+ expected.update(env_data)
+ self.assertEqual(config, expected)
+
+ def test_config_whole_environment(self):
+ tmp_config = self._gen_configration_file(self.configuration_in)
+ env_data = {"interface": "socketcan", "channel": "3", "bitrate": 250000}
+ env_dict = self._dict_to_env(env_data)
+ with unittest.mock.patch.dict("os.environ", env_dict):
+ config = can.util.load_config(path=tmp_config, context="one")
+ expected = self.configuration_out["one"].copy()
+ expected.update({"interface": "socketcan", "channel": 3, "bitrate": 250000})
+ self.assertEqual(config, expected)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_load_file_config.py b/test/test_load_file_config.py
index 52a45d734..afedf58d4 100644
--- a/test/test_load_file_config.py
+++ b/test/test_load_file_config.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python
-# coding: utf-8
+
import shutil
import tempfile
import unittest
@@ -10,10 +10,10 @@
class LoadFileConfigTest(unittest.TestCase):
configuration = {
- 'default': {'interface': 'virtual', 'channel': '0'},
- 'one': {'interface': 'virtual', 'channel': '1'},
- 'two': {'channel': '2'},
- 'three': {'extra': 'extra value'},
+ "default": {"interface": "virtual", "channel": "0"},
+ "one": {"interface": "kvaser", "channel": "1"},
+ "two": {"channel": "2"},
+ "three": {"extra": "extra value"},
}
def setUp(self):
@@ -25,65 +25,70 @@ def tearDown(self):
shutil.rmtree(self.test_dir)
def _gen_configration_file(self, sections):
- with NamedTemporaryFile(mode='w', dir=self.test_dir,
- delete=False) as tmp_config_file:
+ with NamedTemporaryFile(
+ mode="w", dir=self.test_dir, delete=False
+ ) as tmp_config_file:
content = []
for section in sections:
- content.append("[{}]".format(section))
+ content.append(f"[{section}]")
for k, v in self.configuration[section].items():
- content.append("{} = {}".format(k, v))
- tmp_config_file.write('\n'.join(content))
+ content.append(f"{k} = {v}")
+ tmp_config_file.write("\n".join(content))
return tmp_config_file.name
def test_config_file_with_default(self):
- tmp_config = self._gen_configration_file(['default'])
+ tmp_config = self._gen_configration_file(["default"])
config = can.util.load_file_config(path=tmp_config)
- self.assertEqual(config, self.configuration['default'])
+ self.assertEqual(config, self.configuration["default"])
def test_config_file_with_default_and_section(self):
- tmp_config = self._gen_configration_file(['default', 'one'])
+ tmp_config = self._gen_configration_file(["default", "one"])
- default = can.util.load_file_config(path=tmp_config)
- self.assertEqual(default, self.configuration['default'])
+ config = can.util.load_file_config(path=tmp_config)
+ self.assertEqual(config, self.configuration["default"])
- one = can.util.load_file_config(path=tmp_config, section='one')
- self.assertEqual(one, self.configuration['one'])
+ config.update(can.util.load_file_config(path=tmp_config, section="one"))
+ self.assertEqual(config, self.configuration["one"])
def test_config_file_with_section_only(self):
- tmp_config = self._gen_configration_file(['one'])
- config = can.util.load_file_config(path=tmp_config, section='one')
- self.assertEqual(config, self.configuration['one'])
+ tmp_config = self._gen_configration_file(["one"])
+ config = can.util.load_file_config(path=tmp_config)
+ config.update(can.util.load_file_config(path=tmp_config, section="one"))
+ self.assertEqual(config, self.configuration["one"])
def test_config_file_with_section_and_key_in_default(self):
- expected = self.configuration['default'].copy()
- expected.update(self.configuration['two'])
+ expected = self.configuration["default"].copy()
+ expected.update(self.configuration["two"])
- tmp_config = self._gen_configration_file(['default', 'two'])
- config = can.util.load_file_config(path=tmp_config, section='two')
+ tmp_config = self._gen_configration_file(["default", "two"])
+ config = can.util.load_file_config(path=tmp_config)
+ config.update(can.util.load_file_config(path=tmp_config, section="two"))
self.assertEqual(config, expected)
def test_config_file_with_section_missing_interface(self):
- expected = self.configuration['two'].copy()
- tmp_config = self._gen_configration_file(['two'])
- config = can.util.load_file_config(path=tmp_config, section='two')
+ expected = self.configuration["two"].copy()
+ tmp_config = self._gen_configration_file(["two"])
+ config = can.util.load_file_config(path=tmp_config)
+ config.update(can.util.load_file_config(path=tmp_config, section="two"))
self.assertEqual(config, expected)
def test_config_file_extra(self):
- expected = self.configuration['default'].copy()
- expected.update(self.configuration['three'])
+ expected = self.configuration["default"].copy()
+ expected.update(self.configuration["three"])
- tmp_config = self._gen_configration_file(['default', 'three'])
- config = can.util.load_file_config(path=tmp_config, section='three')
+ tmp_config = self._gen_configration_file(["default", "three"])
+ config = can.util.load_file_config(path=tmp_config)
+ config.update(can.util.load_file_config(path=tmp_config, section="three"))
self.assertEqual(config, expected)
def test_config_file_with_non_existing_section(self):
- expected = {}
+ expected = self.configuration["default"].copy()
- tmp_config = self._gen_configration_file([
- 'default', 'one', 'two', 'three'])
- config = can.util.load_file_config(path=tmp_config, section='zero')
+ tmp_config = self._gen_configration_file(["default", "one", "two", "three"])
+ config = can.util.load_file_config(path=tmp_config)
+ config.update(can.util.load_file_config(path=tmp_config, section="zero"))
self.assertEqual(config, expected)
-if __name__ == '__main__':
+if __name__ == "__main__":
unittest.main()
diff --git a/test/test_logger.py b/test/test_logger.py
new file mode 100644
index 000000000..41778ab6a
--- /dev/null
+++ b/test/test_logger.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python
+
+"""
+This module tests the functions inside of logger.py
+"""
+
+import gzip
+import os
+import sys
+import unittest
+from unittest import mock
+from unittest.mock import Mock
+
+import pytest
+
+import can
+import can.cli
+import can.logger
+
+
+class TestLoggerScriptModule(unittest.TestCase):
+ def setUp(self) -> None:
+ # Patch VirtualBus object
+ patcher_virtual_bus = mock.patch("can.interfaces.virtual.VirtualBus", spec=True)
+ self.MockVirtualBus = patcher_virtual_bus.start()
+ self.addCleanup(patcher_virtual_bus.stop)
+ self.mock_virtual_bus = self.MockVirtualBus.return_value
+ self.mock_virtual_bus.shutdown = Mock()
+
+ # Patch Logger object
+ patcher_logger = mock.patch("can.logger.Logger", spec=True)
+ self.MockLogger = patcher_logger.start()
+ self.addCleanup(patcher_logger.stop)
+ self.mock_logger = self.MockLogger.return_value
+ self.mock_logger.stop = Mock()
+
+ self.MockLoggerUse = self.MockLogger
+ self.loggerToUse = self.mock_logger
+
+ # Patch SizedRotatingLogger object
+ patcher_logger_sized = mock.patch("can.logger.SizedRotatingLogger", spec=True)
+ self.MockLoggerSized = patcher_logger_sized.start()
+ self.addCleanup(patcher_logger_sized.stop)
+ self.mock_logger_sized = self.MockLoggerSized.return_value
+ self.mock_logger_sized.stop = Mock()
+
+ self.testmsg = can.Message(
+ arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True
+ )
+
+ self.baseargs = [sys.argv[0], "-i", "virtual"]
+
+ def assertSuccessfullCleanup(self):
+ self.MockVirtualBus.assert_called_once()
+ self.mock_virtual_bus.shutdown.assert_called_once()
+
+ self.MockLoggerUse.assert_called_once()
+ self.loggerToUse.stop.assert_called_once()
+
+ def test_log_virtual(self):
+ self.mock_virtual_bus.recv = Mock(side_effect=[self.testmsg, KeyboardInterrupt])
+
+ sys.argv = self.baseargs
+ can.logger.main()
+ self.assertSuccessfullCleanup()
+ self.mock_logger.assert_called_once()
+
+ def test_log_virtual_active(self):
+ self.mock_virtual_bus.recv = Mock(side_effect=[self.testmsg, KeyboardInterrupt])
+
+ sys.argv = self.baseargs + ["--active"]
+ can.logger.main()
+ self.assertSuccessfullCleanup()
+ self.mock_logger.assert_called_once()
+ self.assertEqual(self.mock_virtual_bus.state, can.BusState.ACTIVE)
+
+ def test_log_virtual_passive(self):
+ self.mock_virtual_bus.recv = Mock(side_effect=[self.testmsg, KeyboardInterrupt])
+
+ sys.argv = self.baseargs + ["--passive"]
+ can.logger.main()
+ self.assertSuccessfullCleanup()
+ self.mock_logger.assert_called_once()
+ self.assertEqual(self.mock_virtual_bus.state, can.BusState.PASSIVE)
+
+ def test_log_virtual_with_config(self):
+ self.mock_virtual_bus.recv = Mock(side_effect=[self.testmsg, KeyboardInterrupt])
+
+ sys.argv = self.baseargs + [
+ "--bitrate",
+ "250000",
+ "--fd",
+ "--data-bitrate",
+ "2000000",
+ ]
+ can.logger.main()
+ self.assertSuccessfullCleanup()
+ self.mock_logger.assert_called_once()
+
+ def test_log_virtual_sizedlogger(self):
+ self.mock_virtual_bus.recv = Mock(side_effect=[self.testmsg, KeyboardInterrupt])
+ self.MockLoggerUse = self.MockLoggerSized
+ self.loggerToUse = self.mock_logger_sized
+
+ sys.argv = self.baseargs + ["--file_size", "1000000"]
+ can.logger.main()
+ self.assertSuccessfullCleanup()
+ self.mock_logger_sized.assert_called_once()
+
+ def test_parse_logger_args(self):
+ args = self.baseargs + [
+ "--bitrate",
+ "250000",
+ "--fd",
+ "--data-bitrate",
+ "2000000",
+ "--receive-own-messages=True",
+ ]
+ results, additional_config = can.logger._parse_logger_args(args[1:])
+ assert results.interface == "virtual"
+ assert results.bitrate == 250_000
+ assert results.fd is True
+ assert results.data_bitrate == 2_000_000
+ assert additional_config["receive_own_messages"] is True
+
+ def test_parse_can_filters(self):
+ expected_can_filters = [{"can_id": 0x100, "can_mask": 0x7FC}]
+ results, additional_config = can.logger._parse_logger_args(
+ ["--filter", "100:7FC", "--bitrate", "250000"]
+ )
+ assert results.can_filters == expected_can_filters
+
+ def test_parse_can_filters_list(self):
+ expected_can_filters = [
+ {"can_id": 0x100, "can_mask": 0x7FC},
+ {"can_id": 0x200, "can_mask": 0x7F0},
+ ]
+ results, additional_config = can.logger._parse_logger_args(
+ ["--filter", "100:7FC", "200:7F0", "--bitrate", "250000"]
+ )
+ assert results.can_filters == expected_can_filters
+
+ def test_parse_timing(self) -> None:
+ can20_args = self.baseargs + [
+ "--timing",
+ "f_clock=8_000_000",
+ "tseg1=5",
+ "tseg2=2",
+ "sjw=2",
+ "brp=2",
+ "nof_samples=1",
+ "--app-name=CANalyzer",
+ ]
+ results, additional_config = can.logger._parse_logger_args(can20_args[1:])
+ assert results.timing == can.BitTiming(
+ f_clock=8_000_000, brp=2, tseg1=5, tseg2=2, sjw=2, nof_samples=1
+ )
+ assert additional_config["app_name"] == "CANalyzer"
+
+ canfd_args = self.baseargs + [
+ "--timing",
+ "f_clock=80_000_000",
+ "nom_tseg1=119",
+ "nom_tseg2=40",
+ "nom_sjw=40",
+ "nom_brp=1",
+ "data_tseg1=29",
+ "data_tseg2=10",
+ "data_sjw=10",
+ "data_brp=1",
+ "--app-name=CANalyzer",
+ ]
+ results, additional_config = can.logger._parse_logger_args(canfd_args[1:])
+ assert results.timing == can.BitTimingFd(
+ f_clock=80_000_000,
+ nom_brp=1,
+ nom_tseg1=119,
+ nom_tseg2=40,
+ nom_sjw=40,
+ data_brp=1,
+ data_tseg1=29,
+ data_tseg2=10,
+ data_sjw=10,
+ )
+ assert additional_config["app_name"] == "CANalyzer"
+
+ # remove f_clock parameter, parsing should fail
+ incomplete_args = self.baseargs + [
+ "--timing",
+ "tseg1=5",
+ "tseg2=2",
+ "sjw=2",
+ "brp=2",
+ "nof_samples=1",
+ "--app-name=CANalyzer",
+ ]
+ with self.assertRaises(SystemExit):
+ can.logger._parse_logger_args(incomplete_args[1:])
+
+ def test_parse_additional_config(self):
+ unknown_args = [
+ "--app-name=CANalyzer",
+ "--serial=5555",
+ "--receive-own-messages=True",
+ "--false-boolean=False",
+ "--offset=1.5",
+ "--tseg1-abr=127",
+ ]
+ parsed_args = can.cli._parse_additional_config(unknown_args)
+
+ assert "app_name" in parsed_args
+ assert parsed_args["app_name"] == "CANalyzer"
+
+ assert "serial" in parsed_args
+ assert parsed_args["serial"] == 5555
+
+ assert "receive_own_messages" in parsed_args
+ assert (
+ isinstance(parsed_args["receive_own_messages"], bool)
+ and parsed_args["receive_own_messages"] is True
+ )
+
+ assert "false_boolean" in parsed_args
+ assert (
+ isinstance(parsed_args["false_boolean"], bool)
+ and parsed_args["false_boolean"] is False
+ )
+
+ assert "offset" in parsed_args
+ assert parsed_args["offset"] == 1.5
+
+ assert "tseg1_abr" in parsed_args
+ assert parsed_args["tseg1_abr"] == 127
+
+ with pytest.raises(ValueError):
+ can.cli._parse_additional_config(["--wrong-format"])
+
+ with pytest.raises(ValueError):
+ can.cli._parse_additional_config(["-wrongformat=value"])
+
+ with pytest.raises(ValueError):
+ can.cli._parse_additional_config(["--wrongformat=value1 value2"])
+
+ with pytest.raises(ValueError):
+ can.cli._parse_additional_config(["wrongformat="])
+
+
+class TestLoggerCompressedFile(unittest.TestCase):
+ def setUp(self) -> None:
+ # Patch VirtualBus object
+ self.patcher_virtual_bus = mock.patch(
+ "can.interfaces.virtual.VirtualBus", spec=True
+ )
+ self.MockVirtualBus = self.patcher_virtual_bus.start()
+ self.mock_virtual_bus = self.MockVirtualBus.return_value
+
+ self.testmsg = can.Message(
+ arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True
+ )
+ self.baseargs = [sys.argv[0], "-i", "virtual"]
+
+ self.testfile = open("coffee.log.gz", "w+")
+
+ def test_compressed_logfile(self):
+ """
+ Basic test to verify Logger is able to write gzip files.
+ """
+ self.mock_virtual_bus.recv = Mock(side_effect=[self.testmsg, KeyboardInterrupt])
+ sys.argv = self.baseargs + ["--file_name", self.testfile.name]
+ can.logger.main()
+ with gzip.open(self.testfile.name, "rt") as testlog:
+ last_line = testlog.readlines()[-1]
+
+ self.assertEqual(last_line, "(0.000000) vcan0 00C0FFEE#0019000103010401 R\n")
+
+ def tearDown(self) -> None:
+ self.testfile.close()
+ os.remove(self.testfile.name)
+ self.patcher_virtual_bus.stop()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_message_class.py b/test/test_message_class.py
new file mode 100644
index 000000000..8e2367034
--- /dev/null
+++ b/test/test_message_class.py
@@ -0,0 +1,136 @@
+#!/usr/bin/env python
+
+import pickle
+import sys
+import unittest
+from copy import copy, deepcopy
+from datetime import timedelta
+from math import isinf, isnan
+
+import hypothesis.errors
+import hypothesis.strategies as st
+import pytest
+from hypothesis import HealthCheck, given, settings
+
+from can import Message
+
+from .config import IS_GITHUB_ACTIONS, IS_PYPY, IS_WINDOWS
+from .message_helper import ComparingMessagesTestCase
+
+
+class TestMessageClass(unittest.TestCase):
+ """
+ This test tries many inputs to the message class constructor and then sanity checks
+ all methods and ensures that nothing crashes. It also checks whether Message._check()
+ allows all valid can frames.
+ """
+
+ @given(
+ timestamp=st.floats(min_value=0.0),
+ arbitration_id=st.integers(),
+ is_extended_id=st.booleans(),
+ is_remote_frame=st.booleans(),
+ is_error_frame=st.booleans(),
+ channel=st.one_of(st.text(), st.integers()),
+ dlc=st.integers(min_value=0, max_value=8),
+ data=st.one_of(st.binary(min_size=0, max_size=8), st.none()),
+ is_fd=st.booleans(),
+ bitrate_switch=st.booleans(),
+ error_state_indicator=st.booleans(),
+ )
+ # The first run may take a second on CI runners and will hit the deadline
+ @settings(
+ max_examples=2000,
+ suppress_health_check=[HealthCheck.too_slow],
+ deadline=None if IS_GITHUB_ACTIONS else timedelta(milliseconds=500),
+ )
+ @pytest.mark.xfail(
+ IS_WINDOWS and IS_PYPY,
+ raises=hypothesis.errors.Flaky,
+ reason="Hypothesis generates inconsistent timestamp floats on Windows+PyPy-3.7",
+ )
+ def test_methods(self, **kwargs):
+ is_valid = not (
+ (
+ not kwargs["is_remote_frame"]
+ and (len(kwargs["data"] or []) != kwargs["dlc"])
+ )
+ or (kwargs["arbitration_id"] >= 0x800 and not kwargs["is_extended_id"])
+ or kwargs["arbitration_id"] >= 0x20000000
+ or kwargs["arbitration_id"] < 0
+ or (
+ kwargs["is_remote_frame"]
+ and (kwargs["is_fd"] or kwargs["is_error_frame"])
+ )
+ or (kwargs["is_remote_frame"] and len(kwargs["data"] or []) > 0)
+ or (
+ (kwargs["bitrate_switch"] or kwargs["error_state_indicator"])
+ and not kwargs["is_fd"]
+ )
+ or isnan(kwargs["timestamp"])
+ or isinf(kwargs["timestamp"])
+ )
+
+ # this should return normally and not throw an exception
+ message = Message(check=is_valid, **kwargs)
+
+ if kwargs["data"] is None or kwargs["is_remote_frame"]:
+ kwargs["data"] = bytearray()
+
+ if not is_valid and not kwargs["is_remote_frame"]:
+ with self.assertRaises(ValueError):
+ Message(check=True, **kwargs)
+
+ self.assertGreater(len(str(message)), 0)
+ self.assertGreater(len(message.__repr__()), 0)
+ if is_valid:
+ self.assertEqual(len(message), kwargs["dlc"])
+ self.assertTrue(bool(message))
+ self.assertGreater(len(f"{message}"), 0)
+ _ = f"{message}"
+ with self.assertRaises(Exception):
+ _ = "{somespec}".format(
+ message
+ ) # pylint: disable=missing-format-argument-key
+ if sys.version_info.major > 2:
+ self.assertEqual(bytearray(bytes(message)), kwargs["data"])
+
+ # check copies and equalities
+ if is_valid:
+ self.assertEqual(message, message)
+ normal_copy = copy(message)
+ deep_copy = deepcopy(message)
+ for other in (normal_copy, deep_copy, message):
+ self.assertTrue(message.equals(other, timestamp_delta=None))
+ self.assertTrue(message.equals(other))
+ self.assertTrue(message.equals(other, timestamp_delta=0))
+
+
+class MessageSerialization(unittest.TestCase, ComparingMessagesTestCase):
+ def __init__(self, *args, **kwargs):
+ unittest.TestCase.__init__(self, *args, **kwargs)
+ ComparingMessagesTestCase.__init__(
+ self, allowed_timestamp_delta=0.016, preserves_channel=True
+ )
+
+ def test_serialization(self):
+ message = Message(
+ timestamp=1.0,
+ arbitration_id=0x401,
+ is_extended_id=False,
+ is_remote_frame=False,
+ is_error_frame=False,
+ channel=1,
+ dlc=6,
+ data=bytearray([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]),
+ is_fd=False,
+ )
+
+ serialized = pickle.dumps(message, -1)
+ deserialized = pickle.loads(serialized)
+
+ self.assertMessageEqual(message, deserialized)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_message_filtering.py b/test/test_message_filtering.py
index e08e6acfd..a73e07aa2 100644
--- a/test/test_message_filtering.py
+++ b/test/test_message_filtering.py
@@ -1,39 +1,26 @@
#!/usr/bin/env python
-# coding: utf-8
"""
This module tests :meth:`can.BusABC._matches_filters`.
"""
-from __future__ import absolute_import
-
import unittest
from can import Bus, Message
from .data.example_data import TEST_ALL_MESSAGES
+EXAMPLE_MSG = Message(arbitration_id=0x123, is_extended_id=True)
+HIGHEST_MSG = Message(arbitration_id=0x1FFFFFFF, is_extended_id=True)
-EXAMPLE_MSG = Message(arbitration_id=0x123, extended_id=True)
-HIGHEST_MSG = Message(arbitration_id=0x1FFFFFFF, extended_id=True)
-
-MATCH_EXAMPLE = [{
- "can_id": 0x123,
- "can_mask": 0x1FFFFFFF,
- "extended": True
-}]
+MATCH_EXAMPLE = [{"can_id": 0x123, "can_mask": 0x1FFFFFFF, "extended": True}]
-MATCH_ONLY_HIGHEST = [{
- "can_id": 0xFFFFFFFF,
- "can_mask": 0x1FFFFFFF,
- "extended": True
-}]
+MATCH_ONLY_HIGHEST = [{"can_id": 0xFFFFFFFF, "can_mask": 0x1FFFFFFF, "extended": True}]
class TestMessageFiltering(unittest.TestCase):
-
def setUp(self):
- self.bus = Bus(bustype='virtual', channel='testy')
+ self.bus = Bus(interface="virtual", channel="testy")
def tearDown(self):
self.bus.shutdown()
@@ -60,5 +47,5 @@ def test_match_example_message(self):
self.assertTrue(self.bus._matches_filters(HIGHEST_MSG))
-if __name__ == '__main__':
+if __name__ == "__main__":
unittest.main()
diff --git a/test/test_message_sync.py b/test/test_message_sync.py
new file mode 100644
index 000000000..90cbe372c
--- /dev/null
+++ b/test/test_message_sync.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python
+
+"""
+This module tests :class:`can.MessageSync`.
+"""
+
+import gc
+import time
+import unittest
+from copy import copy
+
+import pytest
+
+from can import Message, MessageSync
+
+from .config import IS_CI, IS_GITHUB_ACTIONS, IS_LINUX, IS_OSX, IS_TRAVIS
+from .data.example_data import TEST_MESSAGES_BASE
+from .message_helper import ComparingMessagesTestCase
+
+TEST_FEWER_MESSAGES = TEST_MESSAGES_BASE[::2]
+
+
+def inc(value):
+ """Makes the test boundaries give some more space when run on the CI server."""
+ if IS_CI:
+ return value * 1.5
+ else:
+ return value
+
+
+skip_on_unreliable_platforms = unittest.skipIf(
+ (IS_TRAVIS and IS_OSX) or (IS_GITHUB_ACTIONS and not IS_LINUX),
+ "this environment's timings are too unpredictable",
+)
+
+
+@skip_on_unreliable_platforms
+class TestMessageSync(unittest.TestCase, ComparingMessagesTestCase):
+ def __init__(self, *args, **kwargs):
+ unittest.TestCase.__init__(self, *args, **kwargs)
+ ComparingMessagesTestCase.__init__(self)
+
+ def setup_method(self, _):
+ # disabling the garbage collector makes the time readings more reliable
+ gc.disable()
+
+ def teardown_method(self, _):
+ # we need to reenable the garbage collector again
+ gc.enable()
+
+ def test_general(self):
+ messages = [
+ Message(timestamp=50.0),
+ Message(timestamp=50.0),
+ Message(timestamp=50.0 + 0.05),
+ Message(timestamp=50.0 + 0.13),
+ Message(timestamp=50.0), # back in time
+ ]
+ sync = MessageSync(messages, gap=0.0, skip=0.0)
+
+ t_start = time.perf_counter()
+ collected = []
+ timings = []
+ for message in sync:
+ t_now = time.perf_counter()
+ collected.append(message)
+ timings.append(t_now - t_start)
+
+ self.assertMessagesEqual(messages, collected)
+ self.assertEqual(len(timings), len(messages), "programming error in test code")
+
+ self.assertTrue(0.0 <= timings[0] < 0.0 + inc(0.02), str(timings[0]))
+ self.assertTrue(0.0 <= timings[1] < 0.0 + inc(0.02), str(timings[1]))
+ self.assertTrue(0.045 <= timings[2] < 0.05 + inc(0.02), str(timings[2]))
+ self.assertTrue(0.125 <= timings[3] < 0.13 + inc(0.02), str(timings[3]))
+ self.assertTrue(0.125 <= timings[4] < 0.13 + inc(0.02), str(timings[4]))
+
+ def test_skip(self):
+ messages = copy(TEST_FEWER_MESSAGES)
+ sync = MessageSync(messages, skip=0.005, gap=0.0)
+
+ before = time.perf_counter()
+ collected = list(sync)
+ after = time.perf_counter()
+ took = after - before
+
+ # the handling of the messages itself also takes some time:
+ # ~0.001 s/message on a ThinkPad T560 laptop (Ubuntu 18.04, i5-6200U)
+ assert 0 < took < inc(len(messages) * (0.005 + 0.003)), f"took: {took}s"
+
+ self.assertMessagesEqual(messages, collected)
+
+
+@skip_on_unreliable_platforms
+@pytest.mark.parametrize(
+ "timestamp_1,timestamp_2", [(0.0, 0.0), (0.0, 0.01), (0.01, 1.5)]
+)
+def test_gap(timestamp_1, timestamp_2):
+ """This method is alone so it can be parameterized."""
+ messages = [
+ Message(arbitration_id=0x1, timestamp=timestamp_1),
+ Message(arbitration_id=0x2, timestamp=timestamp_2),
+ ]
+ sync = MessageSync(messages, timestamps=False, gap=0.1)
+
+ gc.disable()
+ before = time.perf_counter()
+ collected = list(sync)
+ after = time.perf_counter()
+ gc.enable()
+ took = after - before
+
+ assert 0.195 <= took < 0.2 + inc(0.02)
+ assert messages == collected
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_neousys.py b/test/test_neousys.py
new file mode 100644
index 000000000..69c818869
--- /dev/null
+++ b/test/test_neousys.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python
+
+import unittest
+from ctypes import (
+ POINTER,
+ byref,
+ c_ubyte,
+ cast,
+ sizeof,
+)
+from unittest.mock import Mock
+
+import can
+from can.interfaces.neousys import neousys
+
+
+class TestNeousysBus(unittest.TestCase):
+ def setUp(self) -> None:
+ can.interfaces.neousys.neousys.NEOUSYS_CANLIB = Mock()
+ can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_RegisterReceived = Mock(
+ return_value=1
+ )
+ can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_RegisterStatus = Mock(
+ return_value=1
+ )
+ can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Setup = Mock(return_value=1)
+ can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Start = Mock(return_value=1)
+ can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Send = Mock(return_value=1)
+ can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Stop = Mock(return_value=1)
+ self.bus = can.Bus(channel=0, interface="neousys")
+
+ def tearDown(self) -> None:
+ if self.bus:
+ self.bus.shutdown()
+ self.bus = None
+
+ def test_bus_creation(self) -> None:
+ self.assertIsInstance(self.bus, neousys.NeousysBus)
+ self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20)
+ neousys.NEOUSYS_CANLIB.CAN_Setup.assert_called()
+ neousys.NEOUSYS_CANLIB.CAN_Start.assert_called()
+ neousys.NEOUSYS_CANLIB.CAN_RegisterReceived.assert_called()
+ neousys.NEOUSYS_CANLIB.CAN_RegisterStatus.assert_called()
+ neousys.NEOUSYS_CANLIB.CAN_Send.assert_not_called()
+ neousys.NEOUSYS_CANLIB.CAN_Stop.assert_not_called()
+
+ CAN_Start_args = (
+ can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Setup.call_args[0]
+ )
+
+ # sizeof struct should be 16
+ self.assertEqual(CAN_Start_args[0], 0)
+ self.assertEqual(CAN_Start_args[2], 16)
+ NeousysCanSetup_struct = cast(
+ CAN_Start_args[1], POINTER(neousys.NeousysCanSetup)
+ )
+ self.assertEqual(NeousysCanSetup_struct.contents.bitRate, 500000)
+ self.assertEqual(
+ NeousysCanSetup_struct.contents.recvConfig,
+ neousys.NEOUSYS_CAN_MSG_USE_ID_FILTER,
+ )
+
+ def test_bus_creation_bitrate(self) -> None:
+ self.bus = can.Bus(channel=0, interface="neousys", bitrate=200000)
+ self.assertIsInstance(self.bus, neousys.NeousysBus)
+ self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20)
+
+ CAN_Start_args = (
+ can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Setup.call_args[0]
+ )
+
+ # sizeof struct should be 16
+ self.assertEqual(CAN_Start_args[0], 0)
+ self.assertEqual(CAN_Start_args[2], 16)
+ NeousysCanSetup_struct = cast(
+ CAN_Start_args[1], POINTER(neousys.NeousysCanSetup)
+ )
+ self.assertEqual(NeousysCanSetup_struct.contents.bitRate, 200000)
+ self.assertEqual(
+ NeousysCanSetup_struct.contents.recvConfig,
+ neousys.NEOUSYS_CAN_MSG_USE_ID_FILTER,
+ )
+
+ def test_receive(self) -> None:
+ recv_msg = self.bus.recv(timeout=0.05)
+ self.assertEqual(recv_msg, None)
+ msg_data = [0x01, 0x02, 0x03, 0x04, 0x05]
+ NeousysCanMsg_msg = neousys.NeousysCanMsg(
+ 0x01, 0x00, 0x00, 0x05, (c_ubyte * 8)(*msg_data)
+ )
+ self.bus._neousys_recv_cb(byref(NeousysCanMsg_msg), sizeof(NeousysCanMsg_msg))
+ recv_msg = self.bus.recv(timeout=0.05)
+ self.assertEqual(recv_msg.dlc, 5)
+ self.assertSequenceEqual(recv_msg.data, msg_data)
+
+ def test_send(self) -> None:
+ msg = can.Message(
+ arbitration_id=0x01, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=False
+ )
+ self.bus.send(msg)
+ neousys.NEOUSYS_CANLIB.CAN_Send.assert_called()
+
+ def test_shutdown(self) -> None:
+ self.bus.shutdown()
+ neousys.NEOUSYS_CANLIB.CAN_Stop.assert_called()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_neovi.py b/test/test_neovi.py
new file mode 100644
index 000000000..8c816bef2
--- /dev/null
+++ b/test/test_neovi.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+
+""" """
+import pickle
+import unittest
+
+from can.interfaces.ics_neovi import ICSApiError
+
+
+class ICSApiErrorTest(unittest.TestCase):
+ def test_error_pickling(self):
+ iae = ICSApiError(
+ 0xF00,
+ "description_short",
+ "description_long",
+ severity=ICSApiError.ICS_SPY_ERR_CRITICAL,
+ restart_needed=1,
+ )
+ pickled_iae = pickle.dumps(iae)
+ un_pickled_iae = pickle.loads(pickled_iae)
+ assert iae.__dict__ == un_pickled_iae.__dict__
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_pcan.py b/test/test_pcan.py
new file mode 100644
index 000000000..a9c6ea922
--- /dev/null
+++ b/test/test_pcan.py
@@ -0,0 +1,512 @@
+"""
+Test for PCAN Interface
+"""
+
+import ctypes
+import struct
+import unittest
+from unittest import mock
+from unittest.mock import Mock, patch
+
+import pytest
+from parameterized import parameterized
+
+import can
+from can import BusState, CanProtocol
+from can.exceptions import CanInitializationError
+from can.interfaces.pcan import PcanBus, PcanError
+from can.interfaces.pcan.basic import *
+
+
+class TestPCANBus(unittest.TestCase):
+ def setUp(self) -> None:
+ patcher = mock.patch("can.interfaces.pcan.pcan.PCANBasic", spec=True)
+ self.MockPCANBasic = patcher.start()
+ self.addCleanup(patcher.stop)
+ self.mock_pcan = self.MockPCANBasic.return_value
+ self.mock_pcan.Initialize.return_value = PCAN_ERROR_OK
+ self.mock_pcan.InitializeFD = Mock(return_value=PCAN_ERROR_OK)
+ self.mock_pcan.SetValue = Mock(return_value=PCAN_ERROR_OK)
+ self.mock_pcan.GetValue = self._mockGetValue
+ self.PCAN_API_VERSION_SIM = "4.2"
+ self.bus = None
+
+ def tearDown(self) -> None:
+ if self.bus:
+ self.bus.shutdown()
+ self.bus = None
+
+ def _mockGetValue(self, channel, parameter):
+ """
+ This method is used as mock for GetValue method of PCANBasic object.
+ Only a subset of parameters are supported.
+ """
+ if parameter == PCAN_API_VERSION:
+ return PCAN_ERROR_OK, self.PCAN_API_VERSION_SIM.encode("ascii")
+ elif parameter == PCAN_RECEIVE_EVENT:
+ return PCAN_ERROR_OK, int.from_bytes(PCAN_RECEIVE_EVENT, "big")
+ raise NotImplementedError(
+ f"No mock return value specified for parameter {parameter}"
+ )
+
+ def test_bus_creation(self) -> None:
+ self.bus = can.Bus(interface="pcan")
+
+ self.assertIsInstance(self.bus, PcanBus)
+ self.assertEqual(self.bus.protocol, CanProtocol.CAN_20)
+
+ with pytest.deprecated_call():
+ self.assertFalse(self.bus.fd)
+
+ self.MockPCANBasic.assert_called_once()
+ self.mock_pcan.Initialize.assert_called_once()
+ self.mock_pcan.InitializeFD.assert_not_called()
+
+ def test_bus_creation_state_error(self) -> None:
+ with self.assertRaises(ValueError):
+ can.Bus(interface="pcan", state=BusState.ERROR)
+
+ @parameterized.expand([("f_clock", 80_000_000), ("f_clock_mhz", 80)])
+ def test_bus_creation_fd(self, clock_param: str, clock_val: int) -> None:
+ self.bus = can.Bus(
+ interface="pcan",
+ fd=True,
+ nom_brp=1,
+ nom_tseg1=129,
+ nom_tseg2=30,
+ nom_sjw=1,
+ data_brp=1,
+ data_tseg1=9,
+ data_tseg2=6,
+ data_sjw=1,
+ channel="PCAN_USBBUS1",
+ **{clock_param: clock_val},
+ )
+
+ self.assertIsInstance(self.bus, PcanBus)
+ self.assertEqual(self.bus.protocol, CanProtocol.CAN_FD)
+
+ with pytest.deprecated_call():
+ self.assertTrue(self.bus.fd)
+
+ self.MockPCANBasic.assert_called_once()
+ self.mock_pcan.Initialize.assert_not_called()
+ self.mock_pcan.InitializeFD.assert_called_once()
+
+ # Retrieve second argument of first call
+ bitrate_arg = self.mock_pcan.InitializeFD.call_args[0][-1]
+
+ self.assertTrue(f"{clock_param}={clock_val}".encode("ascii") in bitrate_arg)
+ self.assertTrue(b"nom_brp=1" in bitrate_arg)
+ self.assertTrue(b"nom_tseg1=129" in bitrate_arg)
+ self.assertTrue(b"nom_tseg2=30" in bitrate_arg)
+ self.assertTrue(b"nom_sjw=1" in bitrate_arg)
+ self.assertTrue(b"data_brp=1" in bitrate_arg)
+ self.assertTrue(b"data_tseg1=9" in bitrate_arg)
+ self.assertTrue(b"data_tseg2=6" in bitrate_arg)
+ self.assertTrue(b"data_sjw=1" in bitrate_arg)
+
+ def test_api_version_low(self) -> None:
+ self.PCAN_API_VERSION_SIM = "1.0"
+ with self.assertLogs("can.pcan", level="WARNING") as cm:
+ self.bus = can.Bus(interface="pcan")
+ found_version_warning = False
+ for i in cm.output:
+ if "version" in i and "pcan" in i:
+ found_version_warning = True
+ self.assertTrue(
+ found_version_warning,
+ f"No warning was logged for incompatible api version {cm.output}",
+ )
+
+ def test_api_version_read_fail(self) -> None:
+ self.mock_pcan.GetValue = Mock(return_value=(PCAN_ERROR_ILLOPERATION, None))
+ with self.assertRaises(CanInitializationError):
+ self.bus = can.Bus(interface="pcan")
+
+ def test_issue1642(self) -> None:
+ self.PCAN_API_VERSION_SIM = "1, 3, 0, 50"
+ with self.assertLogs("can.pcan", level="WARNING") as cm:
+ self.bus = can.Bus(interface="pcan")
+ found_version_warning = False
+ for i in cm.output:
+ if "version" in i and "pcan" in i:
+ found_version_warning = True
+ self.assertTrue(
+ found_version_warning,
+ f"No warning was logged for incompatible api version {cm.output}",
+ )
+
+ @parameterized.expand(
+ [
+ ("no_error", PCAN_ERROR_OK, PCAN_ERROR_OK, "some ok text 1"),
+ ("one_error", PCAN_ERROR_UNKNOWN, PCAN_ERROR_OK, "some ok text 2"),
+ (
+ "both_errors",
+ PCAN_ERROR_UNKNOWN,
+ PCAN_ERROR_UNKNOWN,
+ "An error occurred. Error-code's text (8h) couldn't be retrieved",
+ ),
+ ]
+ )
+ def test_get_formatted_error(self, name, status1, status2, expected_result: str):
+ with self.subTest(name):
+ self.bus = can.Bus(interface="pcan")
+ self.mock_pcan.GetErrorText = Mock(
+ side_effect=[
+ (status1, expected_result.encode("utf-8", errors="replace")),
+ (status2, expected_result.encode("utf-8", errors="replace")),
+ ]
+ )
+
+ complete_text = self.bus._get_formatted_error(PCAN_ERROR_BUSHEAVY)
+
+ self.assertEqual(complete_text, expected_result)
+
+ def test_status(self) -> None:
+ self.bus = can.Bus(interface="pcan")
+ self.bus.status()
+ self.mock_pcan.GetStatus.assert_called_once_with(PCAN_USBBUS1)
+
+ @parameterized.expand(
+ [("no_error", PCAN_ERROR_OK, True), ("error", PCAN_ERROR_UNKNOWN, False)]
+ )
+ def test_status_is_ok(self, name, status, expected_result) -> None:
+ with self.subTest(name):
+ self.mock_pcan.GetStatus = Mock(return_value=status)
+ self.bus = can.Bus(interface="pcan")
+ self.assertEqual(self.bus.status_is_ok(), expected_result)
+ self.mock_pcan.GetStatus.assert_called_once_with(PCAN_USBBUS1)
+
+ @parameterized.expand(
+ [("no_error", PCAN_ERROR_OK, True), ("error", PCAN_ERROR_UNKNOWN, False)]
+ )
+ def test_reset(self, name, status, expected_result) -> None:
+ with self.subTest(name):
+ self.mock_pcan.Reset = Mock(return_value=status)
+ self.bus = can.Bus(interface="pcan", fd=True)
+ self.assertEqual(self.bus.reset(), expected_result)
+ self.mock_pcan.Reset.assert_called_once_with(PCAN_USBBUS1)
+
+ @parameterized.expand(
+ [("no_error", PCAN_ERROR_OK, 1), ("error", PCAN_ERROR_UNKNOWN, None)]
+ )
+ def test_get_device_number(self, name, status, expected_result) -> None:
+ with self.subTest(name):
+ self.bus = can.Bus(interface="pcan", fd=True)
+ # Mock GetValue after creation of bus to use first mock of
+ # GetValue in constructor
+ self.mock_pcan.GetValue = Mock(return_value=(status, 1))
+
+ self.assertEqual(self.bus.get_device_number(), expected_result)
+ self.mock_pcan.GetValue.assert_called_once_with(
+ PCAN_USBBUS1, PCAN_DEVICE_NUMBER
+ )
+
+ @parameterized.expand(
+ [("no_error", PCAN_ERROR_OK, True), ("error", PCAN_ERROR_UNKNOWN, False)]
+ )
+ def test_set_device_number(self, name, status, expected_result) -> None:
+ with self.subTest(name):
+ self.bus = can.Bus(interface="pcan")
+ self.mock_pcan.SetValue = Mock(return_value=status)
+ self.assertEqual(self.bus.set_device_number(3), expected_result)
+ # check last SetValue call
+ self.assertEqual(
+ self.mock_pcan.SetValue.call_args_list[-1][0],
+ (PCAN_USBBUS1, PCAN_DEVICE_NUMBER, 3),
+ )
+
+ def test_recv(self):
+ data = (ctypes.c_ubyte * 8)(*[x for x in range(8)])
+ msg = TPCANMsg(ID=0xC0FFEF, LEN=8, MSGTYPE=PCAN_MESSAGE_EXTENDED, DATA=data)
+
+ timestamp = TPCANTimestamp()
+ self.mock_pcan.Read = Mock(return_value=(PCAN_ERROR_OK, msg, timestamp))
+ self.bus = can.Bus(interface="pcan")
+
+ recv_msg = self.bus.recv()
+ self.assertEqual(recv_msg.arbitration_id, msg.ID)
+ self.assertEqual(recv_msg.dlc, msg.LEN)
+ self.assertEqual(recv_msg.is_extended_id, True)
+ self.assertEqual(recv_msg.is_fd, False)
+ self.assertSequenceEqual(recv_msg.data, msg.DATA)
+ self.assertEqual(recv_msg.timestamp, 0)
+ self.assertEqual(recv_msg.channel, "PCAN_USBBUS1")
+
+ def test_recv_fd(self):
+ data = (ctypes.c_ubyte * 64)(*[x for x in range(64)])
+ msg = TPCANMsgFD(
+ ID=0xC0FFEF,
+ DLC=64,
+ MSGTYPE=(PCAN_MESSAGE_EXTENDED.value | PCAN_MESSAGE_FD.value),
+ DATA=data,
+ )
+
+ timestamp = TPCANTimestampFD()
+
+ self.mock_pcan.ReadFD = Mock(return_value=(PCAN_ERROR_OK, msg, timestamp))
+
+ self.bus = can.Bus(interface="pcan", fd=True)
+
+ recv_msg = self.bus.recv()
+ self.assertEqual(recv_msg.arbitration_id, msg.ID)
+ self.assertEqual(recv_msg.dlc, msg.DLC)
+ self.assertEqual(recv_msg.is_extended_id, True)
+ self.assertEqual(recv_msg.is_fd, True)
+ self.assertSequenceEqual(recv_msg.data, msg.DATA)
+ self.assertEqual(recv_msg.timestamp, 0)
+ self.assertEqual(recv_msg.channel, "PCAN_USBBUS1")
+
+ @pytest.mark.timeout(3.0)
+ @patch("select.select", return_value=([], [], []))
+ def test_recv_no_message(self, mock_select):
+ self.mock_pcan.Read = Mock(return_value=(PCAN_ERROR_QRCVEMPTY, None, None))
+ self.bus = can.Bus(interface="pcan")
+ self.assertEqual(self.bus.recv(timeout=0.5), None)
+
+ def test_send(self) -> None:
+ self.mock_pcan.Write = Mock(return_value=PCAN_ERROR_OK)
+ self.bus = can.Bus(interface="pcan")
+ msg = can.Message(
+ arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True
+ )
+ self.bus.send(msg)
+ self.mock_pcan.Write.assert_called_once()
+ self.mock_pcan.WriteFD.assert_not_called()
+
+ def test_send_fd(self) -> None:
+ self.mock_pcan.WriteFD = Mock(return_value=PCAN_ERROR_OK)
+ self.bus = can.Bus(interface="pcan", fd=True)
+ msg = can.Message(
+ arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True
+ )
+ self.bus.send(msg)
+ self.mock_pcan.Write.assert_not_called()
+ self.mock_pcan.WriteFD.assert_called_once()
+
+ @parameterized.expand(
+ [
+ (
+ "standart",
+ (False, False, False, False, False, False),
+ PCAN_MESSAGE_STANDARD,
+ ),
+ (
+ "extended",
+ (True, False, False, False, False, False),
+ PCAN_MESSAGE_EXTENDED,
+ ),
+ ("remote", (False, True, False, False, False, False), PCAN_MESSAGE_RTR),
+ ("error", (False, False, True, False, False, False), PCAN_MESSAGE_ERRFRAME),
+ ("fd", (False, False, False, True, False, False), PCAN_MESSAGE_FD),
+ (
+ "bitrate_switch",
+ (False, False, False, False, True, False),
+ PCAN_MESSAGE_BRS,
+ ),
+ (
+ "error_state_indicator",
+ (False, False, False, False, False, True),
+ PCAN_MESSAGE_ESI,
+ ),
+ ]
+ )
+ def test_send_type(self, name, msg_type, expected_value) -> None:
+ with self.subTest(name):
+ (
+ is_extended_id,
+ is_remote_frame,
+ is_error_frame,
+ is_fd,
+ bitrate_switch,
+ error_state_indicator,
+ ) = msg_type
+
+ self.mock_pcan.Write = Mock(return_value=PCAN_ERROR_OK)
+
+ self.bus = can.Bus(interface="pcan")
+ msg = can.Message(
+ arbitration_id=0xC0FFEF,
+ data=[1, 2, 3, 4, 5, 6, 7, 8],
+ is_extended_id=is_extended_id,
+ is_remote_frame=is_remote_frame,
+ is_error_frame=is_error_frame,
+ bitrate_switch=bitrate_switch,
+ error_state_indicator=error_state_indicator,
+ is_fd=is_fd,
+ )
+ self.bus.send(msg)
+ # self.mock_m_objPCANBasic.Write.assert_called_once()
+ CANMsg = self.mock_pcan.Write.call_args_list[0][0][1]
+ self.assertEqual(CANMsg.MSGTYPE, expected_value.value)
+
+ def test_send_error(self) -> None:
+ self.mock_pcan.Write = Mock(return_value=PCAN_ERROR_BUSHEAVY)
+ self.bus = can.Bus(interface="pcan")
+ msg = can.Message(
+ arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True
+ )
+
+ with self.assertRaises(PcanError):
+ self.bus.send(msg)
+
+ @parameterized.expand([("on", True), ("off", False)])
+ def test_flash(self, name, flash) -> None:
+ with self.subTest(name):
+ self.bus = can.Bus(interface="pcan")
+ self.bus.flash(flash)
+ call_list = self.mock_pcan.SetValue.call_args_list
+ last_call_args_list = call_list[-1][0]
+ self.assertEqual(
+ last_call_args_list, (PCAN_USBBUS1, PCAN_CHANNEL_IDENTIFYING, flash)
+ )
+
+ def test_shutdown(self) -> None:
+ self.bus = can.Bus(interface="pcan")
+ self.bus.shutdown()
+ self.mock_pcan.Uninitialize.assert_called_once_with(PCAN_USBBUS1)
+
+ @parameterized.expand(
+ [
+ ("active", BusState.ACTIVE, PCAN_PARAMETER_OFF),
+ ("passive", BusState.PASSIVE, PCAN_PARAMETER_ON),
+ ]
+ )
+ def test_state(self, name, bus_state: BusState, expected_parameter) -> None:
+ with self.subTest(name):
+ self.bus = can.Bus(interface="pcan")
+
+ self.bus.state = bus_state
+ call_list = self.mock_pcan.SetValue.call_args_list
+ last_call_args_list = call_list[-1][0]
+ self.assertEqual(
+ last_call_args_list,
+ (PCAN_USBBUS1, PCAN_LISTEN_ONLY, expected_parameter),
+ )
+
+ def test_state_constructor(self):
+ for state in [BusState.ACTIVE, BusState.PASSIVE]:
+ bus = can.Bus(interface="pcan", state=state)
+ assert bus.state == state
+
+ def test_detect_available_configs(self) -> None:
+ if platform.system() == "Darwin":
+ self.mock_pcan.GetValue = Mock(
+ return_value=(PCAN_ERROR_OK, PCAN_CHANNEL_AVAILABLE)
+ )
+ configs = PcanBus._detect_available_configs()
+ self.assertEqual(len(configs), 50)
+ else:
+ value = (TPCANChannelInformation * 1).from_buffer_copy(
+ struct.pack("HBBI33sII", 81, 5, 0, 1, b"PCAN-USB FD", 1122867, 1)
+ )
+ self.mock_pcan.GetValue = Mock(return_value=(PCAN_ERROR_OK, value))
+ configs = PcanBus._detect_available_configs()
+ assert len(configs) == 1
+ assert configs[0]["interface"] == "pcan"
+ assert configs[0]["channel"] == "PCAN_USBBUS1"
+ assert configs[0]["supports_fd"]
+ assert configs[0]["controller_number"] == 0
+ assert configs[0]["device_features"] == 1
+ assert configs[0]["device_id"] == 1122867
+ assert configs[0]["device_name"] == "PCAN-USB FD"
+ assert configs[0]["device_type"] == 5
+ assert configs[0]["channel_condition"] == 1
+
+ @parameterized.expand([("valid", PCAN_ERROR_OK, "OK"), ("invalid", 0x00005, None)])
+ def test_status_string(self, name, status, expected_result) -> None:
+ with self.subTest(name):
+ self.bus = can.Bus(interface="pcan")
+ self.mock_pcan.GetStatus = Mock(return_value=status)
+ self.assertEqual(self.bus.status_string(), expected_result)
+ self.mock_pcan.GetStatus.assert_called()
+
+ @parameterized.expand([(0x0, "error"), (0x42, "PCAN_USBBUS8")])
+ def test_constructor_with_device_id(self, dev_id, expected_result):
+ def get_value_side_effect(handle, param):
+ if param == PCAN_API_VERSION:
+ return PCAN_ERROR_OK, self.PCAN_API_VERSION_SIM.encode("ascii")
+
+ if handle in (PCAN_USBBUS8, PCAN_USBBUS14):
+ return 0, 0x42
+ else:
+ return PCAN_ERROR_ILLHW, 0x0
+
+ self.mock_pcan.GetValue = Mock(side_effect=get_value_side_effect)
+
+ if expected_result == "error":
+ with self.assertRaises(ValueError):
+ can.Bus(interface="pcan", device_id=dev_id)
+ else:
+ self.bus = can.Bus(interface="pcan", device_id=dev_id)
+ self.assertEqual(expected_result, self.bus.channel_info)
+
+ def test_bus_creation_auto_reset(self):
+ self.bus = can.Bus(interface="pcan", auto_reset=True)
+ self.assertIsInstance(self.bus, PcanBus)
+ self.MockPCANBasic.assert_called_once()
+
+ def test_auto_reset_init_fault(self):
+ self.mock_pcan.SetValue = Mock(return_value=PCAN_ERROR_INITIALIZE)
+ with self.assertRaises(CanInitializationError):
+ self.bus = can.Bus(interface="pcan", auto_reset=True)
+
+ def test_peak_fd_bus_constructor_regression(self):
+ # Tests that the following issue has been fixed:
+ # https://github.com/hardbyte/python-can/issues/1458
+ params = {
+ "interface": "pcan",
+ "fd": True,
+ "f_clock": 80000000,
+ "nom_brp": 1,
+ "nom_tseg1": 129,
+ "nom_tseg2": 30,
+ "nom_sjw": 1,
+ "data_brp": 1,
+ "data_tseg1": 9,
+ "data_tseg2": 6,
+ "data_sjw": 1,
+ "channel": "PCAN_USBBUS1",
+ }
+
+ can.Bus(**params)
+
+ def test_constructor_bit_timing(self):
+ timing = can.BitTiming.from_registers(f_clock=8_000_000, btr0=0x47, btr1=0x2F)
+ bus = can.Bus(interface="pcan", channel="PCAN_USBBUS1", timing=timing)
+
+ bitrate_arg = self.mock_pcan.Initialize.call_args[0][1]
+ self.assertEqual(bitrate_arg.value, 0x472F)
+ self.assertEqual(bus.protocol, CanProtocol.CAN_20)
+
+ def test_constructor_bit_timing_fd(self):
+ timing = can.BitTimingFd(
+ f_clock=40_000_000,
+ nom_brp=1,
+ nom_tseg1=129,
+ nom_tseg2=30,
+ nom_sjw=1,
+ data_brp=1,
+ data_tseg1=9,
+ data_tseg2=6,
+ data_sjw=1,
+ )
+ bus = can.Bus(interface="pcan", channel="PCAN_USBBUS1", timing=timing)
+ self.assertEqual(bus.protocol, CanProtocol.CAN_FD)
+
+ bitrate_arg = self.mock_pcan.InitializeFD.call_args[0][-1]
+
+ self.assertTrue(b"f_clock=40000000" in bitrate_arg)
+ self.assertTrue(b"nom_brp=1" in bitrate_arg)
+ self.assertTrue(b"nom_tseg1=129" in bitrate_arg)
+ self.assertTrue(b"nom_tseg2=30" in bitrate_arg)
+ self.assertTrue(b"nom_sjw=1" in bitrate_arg)
+ self.assertTrue(b"data_brp=1" in bitrate_arg)
+ self.assertTrue(b"data_tseg1=9" in bitrate_arg)
+ self.assertTrue(b"data_tseg2=6" in bitrate_arg)
+ self.assertTrue(b"data_sjw=1" in bitrate_arg)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_player.py b/test/test_player.py
new file mode 100755
index 000000000..c4c3c90ef
--- /dev/null
+++ b/test/test_player.py
@@ -0,0 +1,155 @@
+#!/usr/bin/env python
+
+"""
+This module tests the functions inside of player.py
+"""
+
+import io
+import os
+import sys
+import unittest
+from unittest import mock
+from unittest.mock import Mock
+
+from parameterized import parameterized
+
+import can
+import can.player
+
+
+class TestPlayerScriptModule(unittest.TestCase):
+ logfile = os.path.join(os.path.dirname(__file__), "data", "test_CanMessage.asc")
+
+ def setUp(self) -> None:
+ # Patch VirtualBus object
+ patcher_virtual_bus = mock.patch("can.interfaces.virtual.VirtualBus", spec=True)
+ self.MockVirtualBus = patcher_virtual_bus.start()
+ self.addCleanup(patcher_virtual_bus.stop)
+ self.mock_virtual_bus = self.MockVirtualBus.return_value
+ self.mock_virtual_bus.__enter__ = Mock(return_value=self.mock_virtual_bus)
+
+ # Patch time sleep object
+ patcher_sleep = mock.patch("can.io.player.time.sleep", spec=True)
+ self.MockSleep = patcher_sleep.start()
+ self.addCleanup(patcher_sleep.stop)
+
+ self.baseargs = [sys.argv[0], "-i", "virtual"]
+
+ def assertSuccessfulCleanup(self):
+ self.MockVirtualBus.assert_called_once()
+ self.mock_virtual_bus.__exit__.assert_called_once()
+
+ def test_play_virtual(self):
+ sys.argv = [*self.baseargs, self.logfile]
+ can.player.main()
+ msg1 = can.Message(
+ timestamp=2.501,
+ arbitration_id=0xC8,
+ is_extended_id=False,
+ is_fd=False,
+ is_rx=False,
+ channel=1,
+ dlc=8,
+ data=[0x9, 0x8, 0x7, 0x6, 0x5, 0x4, 0x3, 0x2],
+ )
+ msg2 = can.Message(
+ timestamp=17.876708,
+ arbitration_id=0x6F9,
+ is_extended_id=False,
+ is_fd=False,
+ is_rx=True,
+ channel=0,
+ dlc=8,
+ data=[0x5, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0],
+ )
+ self.assertTrue(msg1.equals(self.mock_virtual_bus.send.mock_calls[0].args[0]))
+ self.assertTrue(msg2.equals(self.mock_virtual_bus.send.mock_calls[1].args[0]))
+ self.assertSuccessfulCleanup()
+
+ def test_play_virtual_verbose(self):
+ sys.argv = [*self.baseargs, "-v", self.logfile]
+ with mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
+ can.player.main()
+ self.assertIn("09 08 07 06 05 04 03 02", mock_stdout.getvalue())
+ self.assertIn("05 0c 00 00 00 00 00 00", mock_stdout.getvalue())
+ self.assertEqual(self.mock_virtual_bus.send.call_count, 2)
+ self.assertSuccessfulCleanup()
+
+ def test_play_virtual_exit(self):
+ self.MockSleep.side_effect = [None, KeyboardInterrupt]
+
+ sys.argv = [*self.baseargs, self.logfile]
+ can.player.main()
+ assert self.mock_virtual_bus.send.call_count <= 2
+ self.assertSuccessfulCleanup()
+
+ def test_play_skip_error_frame(self):
+ logfile = os.path.join(
+ os.path.dirname(__file__), "data", "logfile_errorframes.asc"
+ )
+ sys.argv = [*self.baseargs, "-v", logfile]
+ can.player.main()
+ self.assertEqual(self.mock_virtual_bus.send.call_count, 9)
+ self.assertSuccessfulCleanup()
+
+ def test_play_error_frame(self):
+ logfile = os.path.join(
+ os.path.dirname(__file__), "data", "logfile_errorframes.asc"
+ )
+ sys.argv = [*self.baseargs, "-v", "--error-frames", logfile]
+ can.player.main()
+ self.assertEqual(self.mock_virtual_bus.send.call_count, 12)
+ self.assertSuccessfulCleanup()
+
+ @parameterized.expand([0, 1, 2, 3])
+ def test_play_loop(self, loop_val):
+ sys.argv = [*self.baseargs, "--loop", str(loop_val), self.logfile]
+ can.player.main()
+ msg1 = can.Message(
+ timestamp=2.501,
+ arbitration_id=0xC8,
+ is_extended_id=False,
+ is_fd=False,
+ is_rx=False,
+ channel=1,
+ dlc=8,
+ data=[0x9, 0x8, 0x7, 0x6, 0x5, 0x4, 0x3, 0x2],
+ )
+ msg2 = can.Message(
+ timestamp=17.876708,
+ arbitration_id=0x6F9,
+ is_extended_id=False,
+ is_fd=False,
+ is_rx=True,
+ channel=0,
+ dlc=8,
+ data=[0x5, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0],
+ )
+ for i in range(loop_val):
+ self.assertTrue(
+ msg1.equals(self.mock_virtual_bus.send.mock_calls[2 * i + 0].args[0])
+ )
+ self.assertTrue(
+ msg2.equals(self.mock_virtual_bus.send.mock_calls[2 * i + 1].args[0])
+ )
+ self.assertEqual(self.mock_virtual_bus.send.call_count, 2 * loop_val)
+ self.assertSuccessfulCleanup()
+
+ def test_play_loop_infinite(self):
+ self.mock_virtual_bus.send.side_effect = [None] * 99 + [KeyboardInterrupt]
+ sys.argv = [*self.baseargs, "-l", "i", self.logfile]
+ can.player.main()
+ self.assertEqual(self.mock_virtual_bus.send.call_count, 100)
+ self.assertSuccessfulCleanup()
+
+
+class TestPlayerCompressedFile(TestPlayerScriptModule):
+ """
+ Re-run tests using a compressed file.
+ """
+
+ logfile = os.path.join(os.path.dirname(__file__), "data", "test_CanMessage.asc.gz")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_robotell.py b/test/test_robotell.py
new file mode 100644
index 000000000..f95139917
--- /dev/null
+++ b/test/test_robotell.py
@@ -0,0 +1,953 @@
+#!/usr/bin/env python
+
+import unittest
+
+import can
+
+
+class robotellTestCase(unittest.TestCase):
+ def setUp(self):
+ # will log timeout messages since we are not feeding ack messages to the serial port at this stage
+ self.bus = can.Bus("loop://", interface="robotell")
+ self.serial = self.bus.serialPortOrig
+ self.serial.read(self.serial.in_waiting)
+
+ def tearDown(self):
+ self.bus.shutdown()
+
+ def test_protocol(self):
+ self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20)
+
+ def test_recv_extended(self):
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0x56,
+ 0x34,
+ 0x12,
+ 0x00,
+ 0xA5,
+ 0xAA,
+ 0xA5,
+ 0xA5,
+ 0xA5,
+ 0x55,
+ 0xA5,
+ 0x55,
+ 0xA5,
+ 0xA5,
+ 0xA5,
+ 0xAA,
+ 0x00,
+ 0x00,
+ 0x06,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0xEB,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ msg = self.bus.recv(1)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 0x123456)
+ self.assertEqual(msg.is_extended_id, True)
+ self.assertEqual(msg.is_remote_frame, False)
+ self.assertEqual(msg.dlc, 6)
+ self.assertSequenceEqual(msg.data, [0xAA, 0xA5, 0x55, 0x55, 0xA5, 0xAA])
+ data = self.serial.read(self.serial.in_waiting)
+
+ def test_send_extended(self):
+ msg = can.Message(
+ arbitration_id=0x123456,
+ is_extended_id=True,
+ data=[0xAA, 0xA5, 0x55, 0x55, 0xA5, 0xAA],
+ )
+ self.bus.send(msg)
+ data = self.serial.read(self.serial.in_waiting)
+ self.assertEqual(
+ data,
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0x56,
+ 0x34,
+ 0x12,
+ 0x00,
+ 0xA5,
+ 0xAA,
+ 0xA5,
+ 0xA5,
+ 0xA5,
+ 0x55,
+ 0xA5,
+ 0x55,
+ 0xA5,
+ 0xA5,
+ 0xA5,
+ 0xAA,
+ 0x00,
+ 0x00,
+ 0x06,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0xEB,
+ 0x55,
+ 0x55,
+ ]
+ ),
+ )
+
+ def test_recv_standard(self):
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0x7B,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x48,
+ 0x65,
+ 0x6C,
+ 0x6C,
+ 0x6F,
+ 0x31,
+ 0x32,
+ 0x33,
+ 0x08,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x0D,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ msg = self.bus.recv(1)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 123)
+ self.assertEqual(msg.is_extended_id, False)
+ self.assertEqual(msg.is_remote_frame, False)
+ self.assertEqual(msg.dlc, 8)
+ self.assertSequenceEqual(
+ msg.data, [0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x31, 0x32, 0x33]
+ )
+ data = self.serial.read(self.serial.in_waiting)
+
+ def test_send_standard(self):
+ msg = can.Message(
+ arbitration_id=123,
+ is_extended_id=False,
+ data=[0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x31, 0x32, 0x33],
+ )
+ self.bus.send(msg)
+ data = self.serial.read(self.serial.in_waiting)
+ self.assertEqual(
+ data,
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0x7B,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x48,
+ 0x65,
+ 0x6C,
+ 0x6C,
+ 0x6F,
+ 0x31,
+ 0x32,
+ 0x33,
+ 0x08,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x0D,
+ 0x55,
+ 0x55,
+ ]
+ ),
+ )
+
+ def test_recv_extended_remote(self):
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0x56,
+ 0x34,
+ 0x12,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x07,
+ 0x00,
+ 0x01,
+ 0x01,
+ 0xA5,
+ 0xA5,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ msg = self.bus.recv(1)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 0x123456)
+ self.assertEqual(msg.is_extended_id, True)
+ self.assertEqual(msg.is_remote_frame, True)
+ self.assertEqual(msg.dlc, 7)
+ data = self.serial.read(self.serial.in_waiting)
+
+ def test_send_extended_remote(self):
+ msg = can.Message(
+ arbitration_id=0x123456, is_extended_id=True, is_remote_frame=True, dlc=7
+ )
+ self.bus.send(msg)
+ data = self.serial.read(self.serial.in_waiting)
+ self.assertEqual(
+ data,
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0x56,
+ 0x34,
+ 0x12,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x07,
+ 0x00,
+ 0x01,
+ 0x01,
+ 0xA5,
+ 0xA5,
+ 0x55,
+ 0x55,
+ ]
+ ),
+ )
+
+ def test_partial_recv(self):
+ # write some junk data and then start of message
+ self.serial.write(
+ bytearray([0x11, 0x22, 0x33, 0xAA, 0xAA, 0x7B, 0x00, 0x00, 0x00, 0x48])
+ )
+ msg = self.bus.recv(1)
+ self.assertIsNone(msg)
+
+ # write rest of first message, and then a second message
+ self.serial.write(
+ bytearray(
+ [
+ 0x65,
+ 0x6C,
+ 0x6C,
+ 0x6F,
+ 0x31,
+ 0x32,
+ 0x33,
+ 0x08,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x0D,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0x56,
+ 0x34,
+ 0x12,
+ 0x00,
+ 0xA5,
+ 0xAA,
+ 0xA5,
+ 0xA5,
+ 0xA5,
+ 0x55,
+ 0xA5,
+ 0x55,
+ 0xA5,
+ 0xA5,
+ 0xA5,
+ 0xAA,
+ 0x00,
+ 0x00,
+ 0x06,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0xEB,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ msg = self.bus.recv(1)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 123)
+ self.assertEqual(msg.is_extended_id, False)
+ self.assertEqual(msg.is_remote_frame, False)
+ self.assertEqual(msg.dlc, 8)
+ self.assertSequenceEqual(
+ msg.data, [0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x31, 0x32, 0x33]
+ )
+
+ # now try to also receive 2nd message
+ msg = self.bus.recv(1)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 0x123456)
+ self.assertEqual(msg.is_extended_id, True)
+ self.assertEqual(msg.is_remote_frame, False)
+ self.assertEqual(msg.dlc, 6)
+ self.assertSequenceEqual(msg.data, [0xAA, 0xA5, 0x55, 0x55, 0xA5, 0xAA])
+
+ # test nothing more left
+ msg = self.bus.recv(1)
+ self.assertIsNone(msg)
+ data = self.serial.read(self.serial.in_waiting)
+
+ def test_serial_number(self):
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xF0,
+ 0xFF,
+ 0xFF,
+ 0x01,
+ 0x53,
+ 0xFF,
+ 0x6A,
+ 0x06,
+ 0x49,
+ 0x72,
+ 0x48,
+ 0xA5,
+ 0x55,
+ 0x08,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x11,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xF1,
+ 0xFF,
+ 0xFF,
+ 0x01,
+ 0x40,
+ 0x60,
+ 0x17,
+ 0x87,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x08,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x36,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ sn = self.bus.get_serial_number(1)
+ self.assertEqual(sn, "53FF-6A06-4972-4855-4060-1787")
+ data = self.serial.read(self.serial.in_waiting)
+ self.assertEqual(
+ data,
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xF0,
+ 0xFF,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x08,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0xF8,
+ 0x55,
+ 0x55,
+ 0xAA,
+ 0xAA,
+ 0xF1,
+ 0xFF,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x08,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0xF9,
+ 0x55,
+ 0x55,
+ ]
+ ),
+ )
+
+ sn = self.bus.get_serial_number(0)
+ self.assertIsNone(sn)
+ data = self.serial.read(self.serial.in_waiting)
+
+ def test_set_bitrate(self):
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xD0,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x40,
+ 0x42,
+ 0x0F,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x04,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0x64,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ self.bus.set_bitrate(1000000)
+ data = self.serial.read(self.serial.in_waiting)
+ self.assertEqual(
+ data,
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xD0,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x40,
+ 0x42,
+ 0x0F,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x04,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x63,
+ 0x55,
+ 0x55,
+ ]
+ ),
+ )
+
+ def test_set_auto_retransmit(self):
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xA0,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0xA1,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xA0,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0xA0,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ self.bus.set_auto_retransmit(True)
+ self.bus.set_auto_retransmit(False)
+ data = self.serial.read(self.serial.in_waiting)
+ self.assertEqual(
+ data,
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xA0,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0xA0,
+ 0x55,
+ 0x55,
+ 0xAA,
+ 0xAA,
+ 0xA0,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x9F,
+ 0x55,
+ 0x55,
+ ]
+ ),
+ )
+
+ def test_set_auto_bus_management(self):
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xB0,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0xB1,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xB0,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0xB0,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ self.bus.set_auto_bus_management(True)
+ self.bus.set_auto_bus_management(False)
+ data = self.serial.read(self.serial.in_waiting)
+ self.assertEqual(
+ data,
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xB0,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0xB0,
+ 0x55,
+ 0x55,
+ 0xAA,
+ 0xAA,
+ 0xB0,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0xAF,
+ 0x55,
+ 0x55,
+ ]
+ ),
+ )
+
+ def test_set_serial_rate(self):
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0x90,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0xC2,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x04,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0x56,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ self.bus.set_serial_rate(115200)
+ data = self.serial.read(self.serial.in_waiting)
+ self.assertEqual(
+ data,
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0x90,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0xC2,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x04,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0xA5,
+ 0x55,
+ 0x55,
+ 0x55,
+ ]
+ ),
+ )
+
+ def test_set_hw_filter(self):
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xE0,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x80,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x08,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0x67,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xE1,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0xC0,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x08,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0xA8,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ self.serial.write(
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xE2,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0xF0,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0xF0,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x08,
+ 0xFF,
+ 0x01,
+ 0x01,
+ 0xCB,
+ 0x55,
+ 0x55,
+ ]
+ )
+ )
+ self.bus.set_hw_filter(1, True, 0, 0, False)
+ self.bus.set_hw_filter(2, True, 0, 0, True)
+ self.bus.set_hw_filter(3, False, 0x1F0, 0x1F0, False)
+ data = self.serial.read(self.serial.in_waiting)
+ self.assertEqual(
+ data,
+ bytearray(
+ [
+ 0xAA,
+ 0xAA,
+ 0xE0,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x80,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x08,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x66,
+ 0x55,
+ 0x55,
+ 0xAA,
+ 0xAA,
+ 0xE1,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0xC0,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x08,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0xA7,
+ 0x55,
+ 0x55,
+ 0xAA,
+ 0xAA,
+ 0xE2,
+ 0xFE,
+ 0xFF,
+ 0x01,
+ 0xF0,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0xF0,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x08,
+ 0xFF,
+ 0x01,
+ 0x00,
+ 0xCA,
+ 0x55,
+ 0x55,
+ ]
+ ),
+ )
+
+ def test_when_no_fileno(self):
+ with self.assertRaises(NotImplementedError):
+ self.bus.fileno()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_rotating_loggers.py b/test/test_rotating_loggers.py
new file mode 100644
index 000000000..77a2c7f5d
--- /dev/null
+++ b/test/test_rotating_loggers.py
@@ -0,0 +1,252 @@
+#!/usr/bin/env python
+
+"""
+Test rotating loggers
+"""
+
+import os
+from pathlib import Path
+from typing import cast
+from unittest.mock import Mock
+
+import can
+from can.io.generic import FileIOMessageWriter
+from can.typechecking import StringPathLike
+
+from .data.example_data import generate_message
+
+
+class TestBaseRotatingLogger:
+ @staticmethod
+ def _get_instance(file: StringPathLike) -> can.io.BaseRotatingLogger:
+ class SubClass(can.io.BaseRotatingLogger):
+ """Subclass that implements abstract methods for testing."""
+
+ _supported_formats = {".asc", ".blf", ".csv", ".log", ".txt"}
+
+ def __init__(self, file: StringPathLike, **kwargs) -> None:
+ super().__init__(**kwargs)
+ suffix = Path(file).suffix.lower()
+ if suffix not in self._supported_formats:
+ raise ValueError(f"Unsupported file format: {suffix}")
+ self._writer = can.Logger(filename=file)
+
+ @property
+ def writer(self) -> FileIOMessageWriter:
+ return cast(FileIOMessageWriter, self._writer)
+
+ def should_rollover(self, msg: can.Message) -> bool:
+ return False
+
+ def do_rollover(self): ...
+
+ return SubClass(file=file)
+
+ def test_import(self):
+ assert hasattr(can.io, "BaseRotatingLogger")
+
+ def test_attributes(self):
+ assert issubclass(can.io.BaseRotatingLogger, can.Listener)
+ assert hasattr(can.io.BaseRotatingLogger, "namer")
+ assert hasattr(can.io.BaseRotatingLogger, "rotator")
+ assert hasattr(can.io.BaseRotatingLogger, "rollover_count")
+ assert hasattr(can.io.BaseRotatingLogger, "writer")
+ assert hasattr(can.io.BaseRotatingLogger, "rotation_filename")
+ assert hasattr(can.io.BaseRotatingLogger, "rotate")
+ assert hasattr(can.io.BaseRotatingLogger, "on_message_received")
+ assert hasattr(can.io.BaseRotatingLogger, "stop")
+ assert hasattr(can.io.BaseRotatingLogger, "should_rollover")
+ assert hasattr(can.io.BaseRotatingLogger, "do_rollover")
+
+ def test_get_new_writer(self, tmp_path):
+ with self._get_instance(tmp_path / "file.ASC") as logger_instance:
+ assert isinstance(logger_instance.writer, can.ASCWriter)
+
+ with self._get_instance(tmp_path / "file.BLF") as logger_instance:
+ assert isinstance(logger_instance.writer, can.BLFWriter)
+
+ with self._get_instance(tmp_path / "file.CSV") as logger_instance:
+ assert isinstance(logger_instance.writer, can.CSVWriter)
+
+ with self._get_instance(tmp_path / "file.LOG") as logger_instance:
+ assert isinstance(logger_instance.writer, can.CanutilsLogWriter)
+
+ with self._get_instance(tmp_path / "file.TXT") as logger_instance:
+ assert isinstance(logger_instance.writer, can.Printer)
+
+ def test_rotation_filename(self, tmp_path):
+ with self._get_instance(tmp_path / "__unused.txt") as logger_instance:
+ default_name = "default"
+ assert logger_instance.rotation_filename(default_name) == "default"
+
+ logger_instance.namer = lambda x: x + "_by_namer"
+ assert logger_instance.rotation_filename(default_name) == "default_by_namer"
+
+ def test_rotate_without_rotator(self, tmp_path):
+ source = str(tmp_path / "source.txt")
+ dest = str(tmp_path / "dest.txt")
+
+ assert os.path.exists(source) is False
+ assert os.path.exists(dest) is False
+
+ with self._get_instance(source) as logger_instance:
+ # use context manager to create `source` file and close it
+ pass
+
+ assert os.path.exists(source) is True
+ assert os.path.exists(dest) is False
+
+ logger_instance.rotate(source, dest)
+
+ assert os.path.exists(source) is False
+ assert os.path.exists(dest) is True
+
+ def test_rotate_with_rotator(self, tmp_path):
+ source = str(tmp_path / "source.txt")
+ dest = str(tmp_path / "dest.txt")
+
+ assert os.path.exists(source) is False
+ assert os.path.exists(dest) is False
+
+ with self._get_instance(source) as logger_instance:
+ # use context manager to create `source` file and close it
+ pass
+
+ rotator_func = Mock()
+ logger_instance.rotator = rotator_func
+ logger_instance._writer = logger_instance._get_new_writer(source)
+ logger_instance.stop()
+
+ assert os.path.exists(source) is True
+ assert os.path.exists(dest) is False
+
+ logger_instance.rotate(source, dest)
+ rotator_func.assert_called_with(source, dest)
+
+ # assert that no rotation was performed since rotator_func
+ # does not do anything
+ assert os.path.exists(source) is True
+ assert os.path.exists(dest) is False
+
+ def test_stop(self, tmp_path):
+ """Test if stop() method of writer is called."""
+ with self._get_instance(tmp_path / "file.ASC") as logger_instance:
+ # replace stop method of writer with Mock
+ mock_stop = Mock(side_effect=logger_instance.writer.stop)
+ logger_instance.writer.stop = mock_stop
+
+ logger_instance.stop()
+ mock_stop.assert_called()
+
+ def test_on_message_received(self, tmp_path):
+ with self._get_instance(tmp_path / "file.ASC") as logger_instance:
+ # Test without rollover
+ should_rollover = Mock(return_value=False)
+ do_rollover = Mock()
+ writers_on_message_received = Mock()
+
+ logger_instance.should_rollover = should_rollover
+ logger_instance.do_rollover = do_rollover
+ logger_instance.writer.on_message_received = writers_on_message_received
+
+ msg = generate_message(0x123)
+ logger_instance.on_message_received(msg)
+
+ should_rollover.assert_called_with(msg)
+ do_rollover.assert_not_called()
+ writers_on_message_received.assert_called_with(msg)
+
+ # Test with rollover
+ should_rollover = Mock(return_value=True)
+ do_rollover = Mock()
+ writers_on_message_received = Mock()
+
+ logger_instance.should_rollover = should_rollover
+ logger_instance.do_rollover = do_rollover
+ logger_instance.writer.on_message_received = writers_on_message_received
+
+ msg = generate_message(0x123)
+ logger_instance.on_message_received(msg)
+
+ should_rollover.assert_called_with(msg)
+ do_rollover.assert_called()
+ writers_on_message_received.assert_called_with(msg)
+
+ def test_issue_1792(self, tmp_path):
+ filepath = tmp_path / "2017_Jeep_Grand_Cherokee_3.6L_V6.log"
+ with self._get_instance(filepath) as logger_instance:
+ assert isinstance(logger_instance.writer, can.CanutilsLogWriter)
+
+
+class TestSizedRotatingLogger:
+ def test_import(self):
+ assert hasattr(can.io, "SizedRotatingLogger")
+ assert hasattr(can, "SizedRotatingLogger")
+
+ def test_attributes(self):
+ assert issubclass(can.SizedRotatingLogger, can.io.BaseRotatingLogger)
+ assert hasattr(can.SizedRotatingLogger, "namer")
+ assert hasattr(can.SizedRotatingLogger, "rotator")
+ assert hasattr(can.SizedRotatingLogger, "should_rollover")
+ assert hasattr(can.SizedRotatingLogger, "do_rollover")
+
+ def test_create_instance(self, tmp_path):
+ base_filename = "mylogfile.ASC"
+ max_bytes = 512
+
+ with can.SizedRotatingLogger(
+ base_filename=tmp_path / base_filename, max_bytes=max_bytes
+ ) as logger_instance:
+ assert Path(logger_instance.base_filename).name == base_filename
+ assert logger_instance.max_bytes == max_bytes
+ assert logger_instance.rollover_count == 0
+ assert isinstance(logger_instance.writer, can.ASCWriter)
+
+ def test_should_rollover(self, tmp_path):
+ base_filename = "mylogfile.ASC"
+ max_bytes = 512
+
+ with can.SizedRotatingLogger(
+ base_filename=tmp_path / base_filename, max_bytes=max_bytes
+ ) as logger_instance:
+ msg = generate_message(0x123)
+ do_rollover = Mock()
+ logger_instance.do_rollover = do_rollover
+
+ logger_instance.writer.file.tell = Mock(return_value=511)
+ assert logger_instance.should_rollover(msg) is False
+ logger_instance.on_message_received(msg)
+ do_rollover.assert_not_called()
+
+ logger_instance.writer.file.tell = Mock(return_value=512)
+ assert logger_instance.should_rollover(msg) is True
+ logger_instance.on_message_received(msg)
+ do_rollover.assert_called()
+
+ def test_logfile_size(self, tmp_path):
+ base_filename = "mylogfile.ASC"
+ max_bytes = 1024
+ msg = generate_message(0x123)
+
+ with can.SizedRotatingLogger(
+ base_filename=tmp_path / base_filename, max_bytes=max_bytes
+ ) as logger_instance:
+ for _ in range(128):
+ logger_instance.on_message_received(msg)
+
+ for file_path in os.listdir(tmp_path):
+ assert os.path.getsize(tmp_path / file_path) <= 1100
+
+ def test_logfile_size_context_manager(self, tmp_path):
+ base_filename = "mylogfile.ASC"
+ max_bytes = 1024
+ msg = generate_message(0x123)
+
+ with can.SizedRotatingLogger(
+ base_filename=tmp_path / base_filename, max_bytes=max_bytes
+ ) as logger_instance:
+ for _ in range(128):
+ logger_instance.on_message_received(msg)
+
+ for file_path in os.listdir(tmp_path):
+ assert os.path.getsize(os.path.join(tmp_path, file_path)) <= 1100
diff --git a/test/test_scripts.py b/test/test_scripts.py
index 90687ccd7..c1a6c082d 100644
--- a/test/test_scripts.py
+++ b/test/test_scripts.py
@@ -1,41 +1,38 @@
#!/usr/bin/env python
-# coding: utf-8
"""
This module tests that the scripts are all callable.
"""
-from __future__ import absolute_import
-
+import errno
import subprocess
-import unittest
import sys
-import errno
+import unittest
from abc import ABCMeta, abstractmethod
from .config import *
-class CanScriptTest(unittest.TestCase):
+class CanScriptTest(unittest.TestCase, metaclass=ABCMeta):
@classmethod
def setUpClass(cls):
# clean up the argument list so the call to the main() functions
# in test_does_not_crash() succeeds
sys.argv = sys.argv[:1]
- #: this is overridden by the subclasses
- __test__ = False
-
- __metaclass__ = ABCMeta
-
def test_do_commands_exist(self):
- """This test calls each scripts once and veifies that the help
+ """This test calls each scripts once and verifies that the help
can be read without any other errors, like the script not being
found.
"""
for command in self._commands():
try:
- subprocess.check_output(command.split(), stderr=subprocess.STDOUT)
+ subprocess.check_output(
+ command.split(),
+ stderr=subprocess.STDOUT,
+ encoding="utf-8",
+ shell=IS_WINDOWS,
+ )
except subprocess.CalledProcessError as e:
return_code = e.returncode
output = e.output
@@ -44,9 +41,13 @@ def test_do_commands_exist(self):
output = "-- NO OUTPUT --"
allowed = [0, errno.EINVAL]
- self.assertIn(return_code, allowed,
- 'Calling "{}" failed (exit code was {} and not SUCCESS/0 or EINVAL/22):\n{}'
- .format(command, return_code, output))
+ self.assertIn(
+ return_code,
+ allowed,
+ 'Calling "{}" failed (exit code was {} and not SUCCESS/0 or EINVAL/22):\n{}'.format(
+ command, return_code, output
+ ),
+ )
def test_does_not_crash(self):
# test import
@@ -54,8 +55,7 @@ def test_does_not_crash(self):
# test main method
with self.assertRaises(SystemExit) as cm:
module.main()
- self.assertEqual(cm.exception.code, errno.EINVAL,
- 'Calling main failed:\n{}'.format(command, e.output))
+ self.assertEqual(cm.exception.code, errno.EINVAL)
@abstractmethod
def _commands(self):
@@ -66,49 +66,72 @@ def _commands(self):
@abstractmethod
def _import(self):
- """Returns the modue of the script that has a main() function.
- """
+ """Returns the modue of the script that has a main() function."""
pass
class TestLoggerScript(CanScriptTest):
-
- __test__ = True
-
def _commands(self):
commands = [
"python -m can.logger --help",
- "python scripts/can_logger.py --help"
+ "can_logger --help",
]
- if IS_UNIX:
- commands += ["can_logger.py --help"]
return commands
def _import(self):
import can.logger as module
+
return module
class TestPlayerScript(CanScriptTest):
-
- __test__ = True
-
def _commands(self):
commands = [
"python -m can.player --help",
- "python scripts/can_player.py --help"
+ "can_player --help",
]
- if IS_UNIX:
- commands += ["can_player.py --help"]
return commands
def _import(self):
import can.player as module
+
+ return module
+
+
+class TestBridgeScript(CanScriptTest):
+ def _commands(self):
+ commands = [
+ "python -m can.bridge --help",
+ "can_bridge --help",
+ ]
+ return commands
+
+ def _import(self):
+ import can.bridge as module
+
+ return module
+
+
+class TestLogconvertScript(CanScriptTest):
+ def _commands(self):
+ commands = [
+ "python -m can.logconvert --help",
+ "can_logconvert --help",
+ ]
+ return commands
+
+ def _import(self):
+ import can.logconvert as module
+
return module
# TODO add #390
-if __name__ == '__main__':
+# this excludes the base class from being executed as a test case itself
+del CanScriptTest
+
+
+if __name__ == "__main__":
unittest.main()
diff --git a/test/test_slcan.py b/test/test_slcan.py
new file mode 100644
index 000000000..b757ad04d
--- /dev/null
+++ b/test/test_slcan.py
@@ -0,0 +1,479 @@
+#!/usr/bin/env python
+
+import unittest.mock
+from typing import cast, Optional
+
+from serial.serialutil import SerialBase
+
+import can.interfaces.slcan
+
+from .config import IS_PYPY
+
+"""
+Mentioned in #1010 & #1490
+
+> PyPy works best with pure Python applications. Whenever you use a C extension module,
+> it runs much slower than in CPython. The reason is that PyPy can't optimize C extension modules since they're not fully supported.
+> In addition, PyPy has to emulate reference counting for that part of the code, making it even slower.
+
+https://realpython.com/pypy-faster-python/#it-doesnt-work-well-with-c-extensions
+"""
+TIMEOUT = 0.5 if IS_PYPY else 0.01 # 0.001 is the default set in slcanBus
+
+
+class SerialMock(SerialBase):
+ def __init__(self, *args, **kwargs) -> None:
+ super().__init__(*args, **kwargs)
+
+ self._input_buffer = b""
+ self._output_buffer = b""
+
+ def open(self) -> None:
+ self.is_open = True
+
+ def close(self) -> None:
+ self.is_open = False
+ self._input_buffer = b""
+ self._output_buffer = b""
+
+ def read(self, size: int = -1, /) -> bytes:
+ if size > 0:
+ data = self._input_buffer[:size]
+ self._input_buffer = self._input_buffer[size:]
+ return data
+ return b""
+
+ def write(self, b: bytes, /) -> Optional[int]:
+ self._output_buffer = b
+ if b == b"N\r":
+ self.set_input_buffer(b"NA123\r")
+ elif b == b"V\r":
+ self.set_input_buffer(b"V1013\r")
+ return len(b)
+
+ def set_input_buffer(self, expected: bytes) -> None:
+ self._input_buffer = expected
+
+ def get_output_buffer(self) -> bytes:
+ return self._output_buffer
+
+ def reset_input_buffer(self) -> None:
+ self._input_buffer = b""
+
+ @property
+ def in_waiting(self) -> int:
+ return len(self._input_buffer)
+
+ @classmethod
+ def serial_for_url(cls, *args, **kwargs) -> SerialBase:
+ return cls(*args, **kwargs)
+
+
+class slcanTestCase(unittest.TestCase):
+ @unittest.mock.patch("serial.serial_for_url", SerialMock.serial_for_url)
+ def setUp(self):
+ self.bus = cast(
+ can.interfaces.slcan.slcanBus,
+ can.Bus(
+ "loop://",
+ interface="slcan",
+ sleep_after_open=0,
+ timeout=TIMEOUT,
+ bitrate=500000,
+ ),
+ )
+ self.serial = cast(SerialMock, self.bus.serialPortOrig)
+ self.serial.reset_input_buffer()
+
+ def tearDown(self):
+ self.bus.shutdown()
+
+ def test_recv_extended(self):
+ self.serial.set_input_buffer(b"T12ABCDEF2AA55\r")
+ msg = self.bus.recv(TIMEOUT)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 0x12ABCDEF)
+ self.assertEqual(msg.is_extended_id, True)
+ self.assertEqual(msg.is_remote_frame, False)
+ self.assertEqual(msg.dlc, 2)
+ self.assertSequenceEqual(msg.data, [0xAA, 0x55])
+
+ # Ewert Energy Systems CANDapter specific
+ self.serial.set_input_buffer(b"x12ABCDEF2AA55\r")
+ msg = self.bus.recv(TIMEOUT)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 0x12ABCDEF)
+ self.assertEqual(msg.is_extended_id, True)
+ self.assertEqual(msg.is_remote_frame, False)
+ self.assertEqual(msg.dlc, 2)
+ self.assertSequenceEqual(msg.data, [0xAA, 0x55])
+
+ def test_send_extended(self):
+ payload = b"T12ABCDEF2AA55\r"
+ msg = can.Message(
+ arbitration_id=0x12ABCDEF, is_extended_id=True, data=[0xAA, 0x55]
+ )
+ self.bus.send(msg)
+ self.assertEqual(payload, self.serial.get_output_buffer())
+
+ self.serial.set_input_buffer(payload)
+ rx_msg = self.bus.recv(TIMEOUT)
+ self.assertTrue(msg.equals(rx_msg, timestamp_delta=None))
+
+ def test_recv_standard(self):
+ self.serial.set_input_buffer(b"t4563112233\r")
+ msg = self.bus.recv(TIMEOUT)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 0x456)
+ self.assertEqual(msg.is_extended_id, False)
+ self.assertEqual(msg.is_remote_frame, False)
+ self.assertEqual(msg.dlc, 3)
+ self.assertSequenceEqual(msg.data, [0x11, 0x22, 0x33])
+
+ def test_send_standard(self):
+ payload = b"t4563112233\r"
+ msg = can.Message(
+ arbitration_id=0x456, is_extended_id=False, data=[0x11, 0x22, 0x33]
+ )
+ self.bus.send(msg)
+ self.assertEqual(payload, self.serial.get_output_buffer())
+
+ self.serial.set_input_buffer(payload)
+ rx_msg = self.bus.recv(TIMEOUT)
+ self.assertTrue(msg.equals(rx_msg, timestamp_delta=None))
+
+ def test_recv_standard_remote(self):
+ self.serial.set_input_buffer(b"r1238\r")
+ msg = self.bus.recv(TIMEOUT)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 0x123)
+ self.assertEqual(msg.is_extended_id, False)
+ self.assertEqual(msg.is_remote_frame, True)
+ self.assertEqual(msg.dlc, 8)
+
+ def test_send_standard_remote(self):
+ payload = b"r1238\r"
+ msg = can.Message(
+ arbitration_id=0x123, is_extended_id=False, is_remote_frame=True, dlc=8
+ )
+ self.bus.send(msg)
+ self.assertEqual(payload, self.serial.get_output_buffer())
+
+ self.serial.set_input_buffer(payload)
+ rx_msg = self.bus.recv(TIMEOUT)
+ self.assertTrue(msg.equals(rx_msg, timestamp_delta=None))
+
+ def test_recv_extended_remote(self):
+ self.serial.set_input_buffer(b"R12ABCDEF6\r")
+ msg = self.bus.recv(TIMEOUT)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 0x12ABCDEF)
+ self.assertEqual(msg.is_extended_id, True)
+ self.assertEqual(msg.is_remote_frame, True)
+ self.assertEqual(msg.dlc, 6)
+
+ def test_send_extended_remote(self):
+ payload = b"R12ABCDEF6\r"
+ msg = can.Message(
+ arbitration_id=0x12ABCDEF, is_extended_id=True, is_remote_frame=True, dlc=6
+ )
+ self.bus.send(msg)
+ self.assertEqual(payload, self.serial.get_output_buffer())
+
+ self.serial.set_input_buffer(payload)
+ rx_msg = self.bus.recv(TIMEOUT)
+ self.assertTrue(msg.equals(rx_msg, timestamp_delta=None))
+
+ def test_recv_fd(self):
+ self.serial.set_input_buffer(b"d123A303132333435363738393a3b3c3d3e3f\r")
+ msg = self.bus.recv(TIMEOUT)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 0x123)
+ self.assertEqual(msg.is_extended_id, False)
+ self.assertEqual(msg.is_remote_frame, False)
+ self.assertEqual(msg.is_fd, True)
+ self.assertEqual(msg.bitrate_switch, False)
+ self.assertEqual(msg.dlc, 16)
+ self.assertSequenceEqual(
+ msg.data,
+ [
+ 0x30,
+ 0x31,
+ 0x32,
+ 0x33,
+ 0x34,
+ 0x35,
+ 0x36,
+ 0x37,
+ 0x38,
+ 0x39,
+ 0x3A,
+ 0x3B,
+ 0x3C,
+ 0x3D,
+ 0x3E,
+ 0x3F,
+ ],
+ )
+
+ def test_send_fd(self):
+ payload = b"d123A303132333435363738393A3B3C3D3E3F\r"
+ msg = can.Message(
+ arbitration_id=0x123,
+ is_extended_id=False,
+ is_fd=True,
+ data=[
+ 0x30,
+ 0x31,
+ 0x32,
+ 0x33,
+ 0x34,
+ 0x35,
+ 0x36,
+ 0x37,
+ 0x38,
+ 0x39,
+ 0x3A,
+ 0x3B,
+ 0x3C,
+ 0x3D,
+ 0x3E,
+ 0x3F,
+ ],
+ )
+ self.bus.send(msg)
+ self.assertEqual(payload, self.serial.get_output_buffer())
+
+ self.serial.set_input_buffer(payload)
+ rx_msg = self.bus.recv(TIMEOUT)
+ self.assertTrue(msg.equals(rx_msg, timestamp_delta=None))
+
+ def test_recv_fd_extended(self):
+ self.serial.set_input_buffer(b"D12ABCDEFA303132333435363738393A3B3C3D3E3F\r")
+ msg = self.bus.recv(TIMEOUT)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 0x12ABCDEF)
+ self.assertEqual(msg.is_extended_id, True)
+ self.assertEqual(msg.is_remote_frame, False)
+ self.assertEqual(msg.dlc, 16)
+ self.assertEqual(msg.bitrate_switch, False)
+ self.assertTrue(msg.is_fd)
+ self.assertSequenceEqual(
+ msg.data,
+ [
+ 0x30,
+ 0x31,
+ 0x32,
+ 0x33,
+ 0x34,
+ 0x35,
+ 0x36,
+ 0x37,
+ 0x38,
+ 0x39,
+ 0x3A,
+ 0x3B,
+ 0x3C,
+ 0x3D,
+ 0x3E,
+ 0x3F,
+ ],
+ )
+
+ def test_send_fd_extended(self):
+ payload = b"D12ABCDEFA303132333435363738393A3B3C3D3E3F\r"
+ msg = can.Message(
+ arbitration_id=0x12ABCDEF,
+ is_extended_id=True,
+ is_fd=True,
+ data=[
+ 0x30,
+ 0x31,
+ 0x32,
+ 0x33,
+ 0x34,
+ 0x35,
+ 0x36,
+ 0x37,
+ 0x38,
+ 0x39,
+ 0x3A,
+ 0x3B,
+ 0x3C,
+ 0x3D,
+ 0x3E,
+ 0x3F,
+ ],
+ )
+ self.bus.send(msg)
+ self.assertEqual(payload, self.serial.get_output_buffer())
+
+ self.serial.set_input_buffer(payload)
+ rx_msg = self.bus.recv(TIMEOUT)
+ self.assertTrue(msg.equals(rx_msg, timestamp_delta=None))
+
+ def test_recv_fd_brs(self):
+ self.serial.set_input_buffer(b"b123A303132333435363738393a3b3c3d3e3f\r")
+ msg = self.bus.recv(TIMEOUT)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 0x123)
+ self.assertEqual(msg.is_extended_id, False)
+ self.assertEqual(msg.is_remote_frame, False)
+ self.assertEqual(msg.is_fd, True)
+ self.assertEqual(msg.bitrate_switch, True)
+ self.assertEqual(msg.dlc, 16)
+ self.assertSequenceEqual(
+ msg.data,
+ [
+ 0x30,
+ 0x31,
+ 0x32,
+ 0x33,
+ 0x34,
+ 0x35,
+ 0x36,
+ 0x37,
+ 0x38,
+ 0x39,
+ 0x3A,
+ 0x3B,
+ 0x3C,
+ 0x3D,
+ 0x3E,
+ 0x3F,
+ ],
+ )
+
+ def test_send_fd_brs(self):
+ payload = b"b123A303132333435363738393A3B3C3D3E3F\r"
+ msg = can.Message(
+ arbitration_id=0x123,
+ is_extended_id=False,
+ is_fd=True,
+ bitrate_switch=True,
+ data=[
+ 0x30,
+ 0x31,
+ 0x32,
+ 0x33,
+ 0x34,
+ 0x35,
+ 0x36,
+ 0x37,
+ 0x38,
+ 0x39,
+ 0x3A,
+ 0x3B,
+ 0x3C,
+ 0x3D,
+ 0x3E,
+ 0x3F,
+ ],
+ )
+ self.bus.send(msg)
+ self.assertEqual(payload, self.serial.get_output_buffer())
+
+ self.serial.set_input_buffer(payload)
+ rx_msg = self.bus.recv(TIMEOUT)
+ self.assertTrue(msg.equals(rx_msg, timestamp_delta=None))
+
+ def test_recv_fd_brs_extended(self):
+ self.serial.set_input_buffer(b"B12ABCDEFA303132333435363738393A3B3C3D3E3F\r")
+ msg = self.bus.recv(TIMEOUT)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 0x12ABCDEF)
+ self.assertEqual(msg.is_extended_id, True)
+ self.assertEqual(msg.is_remote_frame, False)
+ self.assertEqual(msg.dlc, 16)
+ self.assertEqual(msg.bitrate_switch, True)
+ self.assertTrue(msg.is_fd)
+ self.assertSequenceEqual(
+ msg.data,
+ [
+ 0x30,
+ 0x31,
+ 0x32,
+ 0x33,
+ 0x34,
+ 0x35,
+ 0x36,
+ 0x37,
+ 0x38,
+ 0x39,
+ 0x3A,
+ 0x3B,
+ 0x3C,
+ 0x3D,
+ 0x3E,
+ 0x3F,
+ ],
+ )
+
+ def test_send_fd_brs_extended(self):
+ payload = b"B12ABCDEFA303132333435363738393A3B3C3D3E3F\r"
+ msg = can.Message(
+ arbitration_id=0x12ABCDEF,
+ is_extended_id=True,
+ is_fd=True,
+ bitrate_switch=True,
+ data=[
+ 0x30,
+ 0x31,
+ 0x32,
+ 0x33,
+ 0x34,
+ 0x35,
+ 0x36,
+ 0x37,
+ 0x38,
+ 0x39,
+ 0x3A,
+ 0x3B,
+ 0x3C,
+ 0x3D,
+ 0x3E,
+ 0x3F,
+ ],
+ )
+ self.bus.send(msg)
+ self.assertEqual(payload, self.serial.get_output_buffer())
+
+ self.serial.set_input_buffer(payload)
+ rx_msg = self.bus.recv(TIMEOUT)
+ self.assertTrue(msg.equals(rx_msg, timestamp_delta=None))
+
+ def test_partial_recv(self):
+ self.serial.set_input_buffer(b"T12ABCDEF")
+ msg = self.bus.recv(TIMEOUT)
+ self.assertIsNone(msg)
+
+ self.serial.set_input_buffer(b"2AA55\rT12")
+ msg = self.bus.recv(TIMEOUT)
+ self.assertIsNotNone(msg)
+ self.assertEqual(msg.arbitration_id, 0x12ABCDEF)
+ self.assertEqual(msg.is_extended_id, True)
+ self.assertEqual(msg.is_remote_frame, False)
+ self.assertEqual(msg.dlc, 2)
+ self.assertSequenceEqual(msg.data, [0xAA, 0x55])
+
+ msg = self.bus.recv(TIMEOUT)
+ self.assertIsNone(msg)
+
+ self.serial.set_input_buffer(b"ABCDEF2AA55\r")
+ msg = self.bus.recv(TIMEOUT)
+ self.assertIsNotNone(msg)
+
+ def test_version(self):
+ hw_ver, sw_ver = self.bus.get_version(0)
+ self.assertEqual(b"V\r", self.serial.get_output_buffer())
+ self.assertEqual(hw_ver, 10)
+ self.assertEqual(sw_ver, 13)
+
+ def test_serial_number(self):
+ sn = self.bus.get_serial_number(0)
+ self.assertEqual(b"N\r", self.serial.get_output_buffer())
+ self.assertEqual(sn, "A123")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_socketcan.py b/test/test_socketcan.py
new file mode 100644
index 000000000..534ee2a61
--- /dev/null
+++ b/test/test_socketcan.py
@@ -0,0 +1,394 @@
+#!/usr/bin/env python
+
+"""
+Test functions in `can.interfaces.socketcan.socketcan`.
+"""
+import ctypes
+import struct
+import sys
+import unittest
+import warnings
+from unittest.mock import patch
+
+import can
+from can.interfaces.socketcan.constants import (
+ CAN_BCM_TX_DELETE,
+ CAN_BCM_TX_SETUP,
+ SETTIMER,
+ STARTTIMER,
+ TX_COUNTEVT,
+)
+from can.interfaces.socketcan.socketcan import (
+ BcmMsgHead,
+ bcm_header_factory,
+ build_bcm_header,
+ build_bcm_transmit_header,
+ build_bcm_tx_delete_header,
+ build_bcm_update_header,
+)
+
+from .config import IS_LINUX, IS_PYPY, TEST_INTERFACE_SOCKETCAN
+
+
+class SocketCANTest(unittest.TestCase):
+ def setUp(self):
+ self._ctypes_sizeof = ctypes.sizeof
+ self._ctypes_alignment = ctypes.alignment
+
+ @unittest.skipIf(sys.version_info >= (3, 14), "Fails on Python 3.14 or newer")
+ @patch("ctypes.sizeof")
+ @patch("ctypes.alignment")
+ def test_bcm_header_factory_32_bit_sizeof_long_4_alignof_long_4(
+ self, ctypes_sizeof, ctypes_alignment
+ ):
+ """This tests a 32-bit platform (ex. Debian Stretch on i386), where:
+
+ * sizeof(long) == 4
+ * sizeof(long long) == 8
+ * alignof(long) == 4
+ * alignof(long long) == 4
+ """
+
+ def side_effect_ctypes_sizeof(value):
+ type_to_size = {
+ ctypes.c_longlong: 8,
+ ctypes.c_long: 4,
+ ctypes.c_uint8: 1,
+ ctypes.c_uint16: 2,
+ ctypes.c_uint32: 4,
+ ctypes.c_uint64: 8,
+ }
+ return type_to_size[value]
+
+ def side_effect_ctypes_alignment(value):
+ type_to_alignment = {
+ ctypes.c_longlong: 4,
+ ctypes.c_long: 4,
+ ctypes.c_uint8: 1,
+ ctypes.c_uint16: 2,
+ ctypes.c_uint32: 4,
+ ctypes.c_uint64: 4,
+ }
+ return type_to_alignment[value]
+
+ ctypes_sizeof.side_effect = side_effect_ctypes_sizeof
+ ctypes_alignment.side_effect = side_effect_ctypes_alignment
+
+ fields = [
+ ("opcode", ctypes.c_uint32),
+ ("flags", ctypes.c_uint32),
+ ("count", ctypes.c_uint32),
+ ("ival1_tv_sec", ctypes.c_long),
+ ("ival1_tv_usec", ctypes.c_long),
+ ("ival2_tv_sec", ctypes.c_long),
+ ("ival2_tv_usec", ctypes.c_long),
+ ("can_id", ctypes.c_uint32),
+ ("nframes", ctypes.c_uint32),
+ ]
+ BcmMsgHead = bcm_header_factory(fields)
+
+ expected_fields = [
+ ("opcode", ctypes.c_uint32),
+ ("flags", ctypes.c_uint32),
+ ("count", ctypes.c_uint32),
+ ("ival1_tv_sec", ctypes.c_long),
+ ("ival1_tv_usec", ctypes.c_long),
+ ("ival2_tv_sec", ctypes.c_long),
+ ("ival2_tv_usec", ctypes.c_long),
+ ("can_id", ctypes.c_uint32),
+ ("nframes", ctypes.c_uint32),
+ # We expect 4 bytes of padding
+ ("pad_0", ctypes.c_uint8),
+ ("pad_1", ctypes.c_uint8),
+ ("pad_2", ctypes.c_uint8),
+ ("pad_3", ctypes.c_uint8),
+ ]
+ self.assertEqual(expected_fields, BcmMsgHead._fields_)
+
+ @unittest.skipIf(sys.version_info >= (3, 14), "Fails on Python 3.14 or newer")
+ @patch("ctypes.sizeof")
+ @patch("ctypes.alignment")
+ def test_bcm_header_factory_32_bit_sizeof_long_4_alignof_long_long_8(
+ self, ctypes_sizeof, ctypes_alignment
+ ):
+ """This tests a 32-bit platform (ex. Raspbian Stretch on armv7l), where:
+
+ * sizeof(long) == 4
+ * sizeof(long long) == 8
+ * alignof(long) == 4
+ * alignof(long long) == 8
+ """
+
+ def side_effect_ctypes_sizeof(value):
+ type_to_size = {
+ ctypes.c_longlong: 8,
+ ctypes.c_long: 4,
+ ctypes.c_uint8: 1,
+ ctypes.c_uint16: 2,
+ ctypes.c_uint32: 4,
+ ctypes.c_uint64: 8,
+ }
+ return type_to_size[value]
+
+ def side_effect_ctypes_alignment(value):
+ type_to_alignment = {
+ ctypes.c_longlong: 8,
+ ctypes.c_long: 4,
+ ctypes.c_uint8: 1,
+ ctypes.c_uint16: 2,
+ ctypes.c_uint32: 4,
+ ctypes.c_uint64: 8,
+ }
+ return type_to_alignment[value]
+
+ ctypes_sizeof.side_effect = side_effect_ctypes_sizeof
+ ctypes_alignment.side_effect = side_effect_ctypes_alignment
+
+ fields = [
+ ("opcode", ctypes.c_uint32),
+ ("flags", ctypes.c_uint32),
+ ("count", ctypes.c_uint32),
+ ("ival1_tv_sec", ctypes.c_long),
+ ("ival1_tv_usec", ctypes.c_long),
+ ("ival2_tv_sec", ctypes.c_long),
+ ("ival2_tv_usec", ctypes.c_long),
+ ("can_id", ctypes.c_uint32),
+ ("nframes", ctypes.c_uint32),
+ ]
+ BcmMsgHead = bcm_header_factory(fields)
+
+ expected_fields = [
+ ("opcode", ctypes.c_uint32),
+ ("flags", ctypes.c_uint32),
+ ("count", ctypes.c_uint32),
+ ("ival1_tv_sec", ctypes.c_long),
+ ("ival1_tv_usec", ctypes.c_long),
+ ("ival2_tv_sec", ctypes.c_long),
+ ("ival2_tv_usec", ctypes.c_long),
+ ("can_id", ctypes.c_uint32),
+ ("nframes", ctypes.c_uint32),
+ # We expect 4 bytes of padding
+ ("pad_0", ctypes.c_uint8),
+ ("pad_1", ctypes.c_uint8),
+ ("pad_2", ctypes.c_uint8),
+ ("pad_3", ctypes.c_uint8),
+ ]
+ self.assertEqual(expected_fields, BcmMsgHead._fields_)
+
+ @unittest.skipIf(sys.version_info >= (3, 14), "Fails on Python 3.14 or newer")
+ @patch("ctypes.sizeof")
+ @patch("ctypes.alignment")
+ def test_bcm_header_factory_64_bit_sizeof_long_8_alignof_long_8(
+ self, ctypes_sizeof, ctypes_alignment
+ ):
+ """This tests a 64-bit platform (ex. Ubuntu 18.04 on x86_64), where:
+
+ * sizeof(long) == 8
+ * sizeof(long long) == 8
+ * alignof(long) == 8
+ * alignof(long long) == 8
+ """
+
+ def side_effect_ctypes_sizeof(value):
+ type_to_size = {
+ ctypes.c_longlong: 8,
+ ctypes.c_long: 8,
+ ctypes.c_uint8: 1,
+ ctypes.c_uint16: 2,
+ ctypes.c_uint32: 4,
+ ctypes.c_uint64: 8,
+ }
+ return type_to_size[value]
+
+ def side_effect_ctypes_alignment(value):
+ type_to_alignment = {
+ ctypes.c_longlong: 8,
+ ctypes.c_long: 8,
+ ctypes.c_uint8: 1,
+ ctypes.c_uint16: 2,
+ ctypes.c_uint32: 4,
+ ctypes.c_uint64: 8,
+ }
+ return type_to_alignment[value]
+
+ ctypes_sizeof.side_effect = side_effect_ctypes_sizeof
+ ctypes_alignment.side_effect = side_effect_ctypes_alignment
+
+ fields = [
+ ("opcode", ctypes.c_uint32),
+ ("flags", ctypes.c_uint32),
+ ("count", ctypes.c_uint32),
+ ("ival1_tv_sec", ctypes.c_long),
+ ("ival1_tv_usec", ctypes.c_long),
+ ("ival2_tv_sec", ctypes.c_long),
+ ("ival2_tv_usec", ctypes.c_long),
+ ("can_id", ctypes.c_uint32),
+ ("nframes", ctypes.c_uint32),
+ ]
+ BcmMsgHead = bcm_header_factory(fields)
+
+ expected_fields = [
+ ("opcode", ctypes.c_uint32),
+ ("flags", ctypes.c_uint32),
+ ("count", ctypes.c_uint32),
+ # We expect 4 bytes of padding
+ ("pad_0", ctypes.c_uint8),
+ ("pad_1", ctypes.c_uint8),
+ ("pad_2", ctypes.c_uint8),
+ ("pad_3", ctypes.c_uint8),
+ ("ival1_tv_sec", ctypes.c_long),
+ ("ival1_tv_usec", ctypes.c_long),
+ ("ival2_tv_sec", ctypes.c_long),
+ ("ival2_tv_usec", ctypes.c_long),
+ ("can_id", ctypes.c_uint32),
+ ("nframes", ctypes.c_uint32),
+ ]
+ self.assertEqual(expected_fields, BcmMsgHead._fields_)
+
+ def test_build_bcm_header(self):
+ def _find_u32_fmt_char() -> str:
+ for _fmt in ("H", "I", "L", "Q"):
+ if struct.calcsize(_fmt) == 4:
+ return _fmt
+
+ def _standard_size_little_endian_to_native(data: bytes) -> bytes:
+ std_le_fmt = "
+ ascii_msg = "< frame 123 1680000000.0 01020304 >"
+ msg = socketcand.convert_ascii_message_to_can_message(ascii_msg)
+ self.assertIsInstance(msg, can.Message)
+ self.assertEqual(msg.arbitration_id, 0x123)
+ self.assertEqual(msg.timestamp, 1680000000.0)
+ self.assertEqual(msg.data, bytearray([1, 2, 3, 4]))
+ self.assertEqual(msg.dlc, 4)
+ self.assertFalse(msg.is_extended_id)
+ self.assertTrue(msg.is_rx)
+
+ def test_valid_error_message(self):
+ # Example: < error 1ABCDEF0 1680000001.0 >
+ ascii_msg = "< error 1ABCDEF0 1680000001.0 >"
+ msg = socketcand.convert_ascii_message_to_can_message(ascii_msg)
+ self.assertIsInstance(msg, can.Message)
+ self.assertEqual(msg.arbitration_id, 0x1ABCDEF0)
+ self.assertEqual(msg.timestamp, 1680000001.0)
+ self.assertEqual(msg.data, bytearray([0]))
+ self.assertEqual(msg.dlc, 1)
+ self.assertTrue(msg.is_extended_id)
+ self.assertTrue(msg.is_error_frame)
+ self.assertTrue(msg.is_rx)
+
+ def test_invalid_message(self):
+ ascii_msg = "< unknown 123 0.0 >"
+ msg = socketcand.convert_ascii_message_to_can_message(ascii_msg)
+ self.assertIsNone(msg)
+
+ def test_missing_ending_character(self):
+ ascii_msg = "< frame 123 1680000000.0 01020304"
+ msg = socketcand.convert_ascii_message_to_can_message(ascii_msg)
+ self.assertIsNone(msg)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_systec.py b/test/test_systec.py
new file mode 100644
index 000000000..86ed31362
--- /dev/null
+++ b/test/test_systec.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python
+
+import unittest
+from unittest.mock import Mock, patch
+
+import can
+from can.interfaces.systec import ucan, ucanbus
+from can.interfaces.systec.ucan import *
+
+
+class SystecTest(unittest.TestCase):
+ def compare_message(self, first, second, msg):
+ if (
+ first.arbitration_id != second.arbitration_id
+ or first.data != second.data
+ or first.is_extended_id != second.is_extended_id
+ ):
+ raise self.failureException(msg)
+
+ def setUp(self):
+ # add equality function for can.Message
+ self.addTypeEqualityFunc(can.Message, self.compare_message)
+
+ ucan.UcanInitHwConnectControlEx = Mock()
+ ucan.UcanInitHardwareEx = Mock()
+ ucan.UcanInitHardwareEx2 = Mock()
+ ucan.UcanInitCanEx2 = Mock()
+ ucan.UcanGetHardwareInfoEx2 = Mock()
+ ucan.UcanSetAcceptanceEx = Mock()
+ ucan.UcanDeinitCanEx = Mock()
+ ucan.UcanDeinitHardware = Mock()
+ ucan.UcanWriteCanMsgEx = Mock()
+ ucan.UcanResetCanEx = Mock()
+ ucan._UCAN_INITIALIZED = True # Fake this
+ self.bus = can.Bus(interface="systec", channel=0, bitrate=125000)
+
+ def test_bus_creation(self):
+ self.assertIsInstance(self.bus, ucanbus.UcanBus)
+ self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20)
+
+ self.assertTrue(ucan.UcanInitHwConnectControlEx.called)
+ self.assertTrue(
+ ucan.UcanInitHardwareEx.called or ucan.UcanInitHardwareEx2.called
+ )
+ self.assertTrue(ucan.UcanInitCanEx2.called)
+ self.assertTrue(ucan.UcanGetHardwareInfoEx2.called)
+ self.assertTrue(ucan.UcanSetAcceptanceEx.called)
+
+ def test_bus_shutdown(self):
+ self.bus.shutdown()
+ self.assertTrue(ucan.UcanDeinitCanEx.called)
+ self.assertTrue(ucan.UcanDeinitHardware.called)
+
+ def test_filter_setup(self):
+ # no filter in the constructor
+ expected_args = ((self.bus._ucan._handle, 0, AMR_ALL, ACR_ALL),)
+ self.assertEqual(ucan.UcanSetAcceptanceEx.call_args, expected_args)
+
+ # one filter is handled by the driver
+ ucan.UcanSetAcceptanceEx.reset_mock()
+ can_filter = (True, 0x123, 0x123, False, False)
+ self.bus.set_filters(ucanbus.UcanBus.create_filter(*can_filter))
+ expected_args = (
+ (
+ self.bus._ucan._handle,
+ 0,
+ ucan.UcanServer.calculate_amr(*can_filter),
+ ucan.UcanServer.calculate_acr(*can_filter),
+ ),
+ )
+ self.assertEqual(ucan.UcanSetAcceptanceEx.call_args, expected_args)
+
+ # multiple filters are handled by the bus
+ ucan.UcanSetAcceptanceEx.reset_mock()
+ can_filter = ((False, 0x8, 0x8, False, False), (False, 0x9, 0x9, False, False))
+ self.bus.set_filters(
+ ucanbus.UcanBus.create_filter(*can_filter[0])
+ + ucanbus.UcanBus.create_filter(*can_filter[1])
+ )
+ expected_args = ((self.bus._ucan._handle, 0, AMR_ALL, ACR_ALL),)
+ self.assertEqual(ucan.UcanSetAcceptanceEx.call_args, expected_args)
+
+ @patch("can.interfaces.systec.ucan.UcanServer.write_can_msg")
+ def test_send_extended(self, mock_write_can_msg):
+ msg = can.Message(
+ arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4], is_extended_id=True
+ )
+ self.bus.send(msg)
+
+ expected_args = (
+ (0, [CanMsg(msg.arbitration_id, MsgFrameFormat.MSG_FF_EXT, msg.data)]),
+ )
+ self.assertEqual(mock_write_can_msg.call_args, expected_args)
+
+ @patch("can.interfaces.systec.ucan.UcanServer.write_can_msg")
+ def test_send_standard(self, mock_write_can_msg):
+ msg = can.Message(arbitration_id=0x321, data=[50, 51], is_extended_id=False)
+ self.bus.send(msg)
+
+ expected_args = (
+ (0, [CanMsg(msg.arbitration_id, MsgFrameFormat.MSG_FF_STD, msg.data)]),
+ )
+ self.assertEqual(mock_write_can_msg.call_args, expected_args)
+
+ @patch("can.interfaces.systec.ucan.UcanServer.get_msg_pending")
+ def test_recv_no_message(self, mock_get_msg_pending):
+ mock_get_msg_pending.return_value = 0
+ self.assertEqual(self.bus.recv(timeout=0.5), None)
+
+ @patch("can.interfaces.systec.ucan.UcanServer.get_msg_pending")
+ @patch("can.interfaces.systec.ucan.UcanServer.read_can_msg")
+ def test_recv_extended(self, mock_read_can_msg, mock_get_msg_pending):
+ mock_read_can_msg.return_value = (
+ [CanMsg(0xC0FFEF, MsgFrameFormat.MSG_FF_EXT, [1, 2, 3, 4, 5, 6, 7, 8])],
+ 0,
+ )
+ mock_get_msg_pending.return_value = 1
+
+ msg = can.Message(
+ arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True
+ )
+ can_msg = self.bus.recv()
+ self.assertEqual(can_msg, msg)
+
+ @patch("can.interfaces.systec.ucan.UcanServer.get_msg_pending")
+ @patch("can.interfaces.systec.ucan.UcanServer.read_can_msg")
+ def test_recv_standard(self, mock_read_can_msg, mock_get_msg_pending):
+ mock_read_can_msg.return_value = (
+ [CanMsg(0x321, MsgFrameFormat.MSG_FF_STD, [50, 51])],
+ 0,
+ )
+ mock_get_msg_pending.return_value = 1
+
+ msg = can.Message(arbitration_id=0x321, data=[50, 51], is_extended_id=False)
+ can_msg = self.bus.recv()
+ self.assertEqual(can_msg, msg)
+
+ @staticmethod
+ def test_bus_defaults():
+ ucan.UcanInitCanEx2.reset_mock()
+ bus = can.Bus(interface="systec", channel=0)
+ ucan.UcanInitCanEx2.assert_called_once_with(
+ bus._ucan._handle,
+ 0,
+ InitCanParam(
+ Mode.MODE_NORMAL,
+ Baudrate.BAUD_500kBit,
+ OutputControl.OCR_DEFAULT,
+ AMR_ALL,
+ ACR_ALL,
+ BaudrateEx.BAUDEX_USE_BTR01,
+ DEFAULT_BUFFER_ENTRIES,
+ DEFAULT_BUFFER_ENTRIES,
+ ),
+ )
+
+ @staticmethod
+ def test_bus_channel():
+ ucan.UcanInitCanEx2.reset_mock()
+ bus = can.Bus(interface="systec", channel=1)
+ ucan.UcanInitCanEx2.assert_called_once_with(
+ bus._ucan._handle,
+ 1,
+ InitCanParam(
+ Mode.MODE_NORMAL,
+ Baudrate.BAUD_500kBit,
+ OutputControl.OCR_DEFAULT,
+ AMR_ALL,
+ ACR_ALL,
+ BaudrateEx.BAUDEX_USE_BTR01,
+ DEFAULT_BUFFER_ENTRIES,
+ DEFAULT_BUFFER_ENTRIES,
+ ),
+ )
+
+ @staticmethod
+ def test_bus_bitrate():
+ ucan.UcanInitCanEx2.reset_mock()
+ bus = can.Bus(interface="systec", channel=0, bitrate=125000)
+ ucan.UcanInitCanEx2.assert_called_once_with(
+ bus._ucan._handle,
+ 0,
+ InitCanParam(
+ Mode.MODE_NORMAL,
+ Baudrate.BAUD_125kBit,
+ OutputControl.OCR_DEFAULT,
+ AMR_ALL,
+ ACR_ALL,
+ BaudrateEx.BAUDEX_USE_BTR01,
+ DEFAULT_BUFFER_ENTRIES,
+ DEFAULT_BUFFER_ENTRIES,
+ ),
+ )
+
+ def test_bus_custom_bitrate(self):
+ with self.assertRaises(ValueError):
+ can.Bus(interface="systec", channel=0, bitrate=123456)
+
+ @staticmethod
+ def test_receive_own_messages():
+ ucan.UcanInitCanEx2.reset_mock()
+ bus = can.Bus(interface="systec", channel=0, receive_own_messages=True)
+ ucan.UcanInitCanEx2.assert_called_once_with(
+ bus._ucan._handle,
+ 0,
+ InitCanParam(
+ Mode.MODE_TX_ECHO,
+ Baudrate.BAUD_500kBit,
+ OutputControl.OCR_DEFAULT,
+ AMR_ALL,
+ ACR_ALL,
+ BaudrateEx.BAUDEX_USE_BTR01,
+ DEFAULT_BUFFER_ENTRIES,
+ DEFAULT_BUFFER_ENTRIES,
+ ),
+ )
+
+ @staticmethod
+ def test_bus_passive_state():
+ ucan.UcanInitCanEx2.reset_mock()
+ bus = can.Bus(interface="systec", channel=0, state=can.BusState.PASSIVE)
+ ucan.UcanInitCanEx2.assert_called_once_with(
+ bus._ucan._handle,
+ 0,
+ InitCanParam(
+ Mode.MODE_LISTEN_ONLY,
+ Baudrate.BAUD_500kBit,
+ OutputControl.OCR_DEFAULT,
+ AMR_ALL,
+ ACR_ALL,
+ BaudrateEx.BAUDEX_USE_BTR01,
+ DEFAULT_BUFFER_ENTRIES,
+ DEFAULT_BUFFER_ENTRIES,
+ ),
+ )
+
+ @staticmethod
+ def test_rx_buffer_entries():
+ ucan.UcanInitCanEx2.reset_mock()
+ bus = can.Bus(interface="systec", channel=0, rx_buffer_entries=1024)
+ ucan.UcanInitCanEx2.assert_called_once_with(
+ bus._ucan._handle,
+ 0,
+ InitCanParam(
+ Mode.MODE_NORMAL,
+ Baudrate.BAUD_500kBit,
+ OutputControl.OCR_DEFAULT,
+ AMR_ALL,
+ ACR_ALL,
+ BaudrateEx.BAUDEX_USE_BTR01,
+ 1024,
+ DEFAULT_BUFFER_ENTRIES,
+ ),
+ )
+
+ @staticmethod
+ def test_tx_buffer_entries():
+ ucan.UcanInitCanEx2.reset_mock()
+ bus = can.Bus(interface="systec", channel=0, tx_buffer_entries=1024)
+ ucan.UcanInitCanEx2.assert_called_once_with(
+ bus._ucan._handle,
+ 0,
+ InitCanParam(
+ Mode.MODE_NORMAL,
+ Baudrate.BAUD_500kBit,
+ OutputControl.OCR_DEFAULT,
+ AMR_ALL,
+ ACR_ALL,
+ BaudrateEx.BAUDEX_USE_BTR01,
+ DEFAULT_BUFFER_ENTRIES,
+ 1024,
+ ),
+ )
+
+ def test_flush_tx_buffer(self):
+ self.bus.flush_tx_buffer()
+ ucan.UcanResetCanEx.assert_called_once_with(
+ self.bus._ucan._handle, 0, ResetFlags.RESET_ONLY_TX_BUFF
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/test/test_util.py b/test/test_util.py
new file mode 100644
index 000000000..53a9c973b
--- /dev/null
+++ b/test/test_util.py
@@ -0,0 +1,359 @@
+#!/usr/bin/env python
+
+import unittest
+import warnings
+
+import pytest
+
+import can
+from can import BitTiming, BitTimingFd
+from can.exceptions import CanInitializationError
+from can.util import (
+ _create_bus_config,
+ _rename_kwargs,
+ cast_from_string,
+ channel2int,
+ check_or_adjust_timing_clock,
+ deprecated_args_alias,
+)
+
+
+class RenameKwargsTest(unittest.TestCase):
+ expected_kwargs = dict(a=1, b=2, c=3, d=4)
+
+ def _test(self, start: str, end: str, kwargs, aliases):
+ # Test that we do get the DeprecationWarning when called with deprecated kwargs
+ with self.assertWarnsRegex(
+ DeprecationWarning, "is deprecated.*?" + start + ".*?" + end
+ ):
+ _rename_kwargs("unit_test", start, end, kwargs, aliases)
+
+ # Test that the aliases contains the deprecated values and
+ # the obsolete kwargs have been removed
+ assert kwargs == self.expected_kwargs
+
+ # Test that we do not get a DeprecationWarning when we call
+ # without deprecated kwargs
+
+ # Cause all warnings to always be triggered.
+ warnings.simplefilter("error", DeprecationWarning)
+ try:
+ _rename_kwargs("unit_test", start, end, kwargs, aliases)
+ finally:
+ warnings.resetwarnings()
+
+ def test_rename(self):
+ kwargs = dict(old_a=1, old_b=2, c=3, d=4)
+ aliases = {"old_a": "a", "old_b": "b"}
+ self._test("1.0", "2.0", kwargs, aliases)
+
+ def test_obsolete(self):
+ kwargs = dict(a=1, b=2, c=3, d=4, z=10)
+ aliases = {"z": None}
+ self._test("1.0", "2.0", kwargs, aliases)
+
+ def test_rename_and_obsolete(self):
+ kwargs = dict(old_a=1, old_b=2, c=3, d=4, z=10)
+ aliases = {"old_a": "a", "old_b": "b", "z": None}
+ self._test("1.0", "2.0", kwargs, aliases)
+
+ def test_with_new_and_alias_present(self):
+ kwargs = dict(old_a=1, a=1, b=2, c=3, d=4, z=10)
+ aliases = {"old_a": "a", "old_b": "b", "z": None}
+ with self.assertRaises(TypeError):
+ self._test("1.0", "2.0", kwargs, aliases)
+
+
+class DeprecatedArgsAliasTest(unittest.TestCase):
+ def test_decorator(self):
+ @deprecated_args_alias("1.0.0", old_a="a")
+ def _test_func1(a):
+ pass
+
+ with pytest.warns(DeprecationWarning) as record:
+ _test_func1(old_a=1)
+ assert len(record) == 1
+ assert (
+ record[0].message.args[0]
+ == "The 'old_a' argument is deprecated since python-can v1.0.0. Use 'a' instead."
+ )
+
+ @deprecated_args_alias("1.6.0", "3.4.0", old_a="a", old_b=None)
+ def _test_func2(a):
+ pass
+
+ with pytest.warns(DeprecationWarning) as record:
+ _test_func2(old_a=1, old_b=2)
+ assert len(record) == 2
+ assert record[0].message.args[0] == (
+ "The 'old_a' argument is deprecated since python-can v1.6.0, and scheduled for "
+ "removal in python-can v3.4.0. Use 'a' instead."
+ )
+ assert record[1].message.args[0] == (
+ "The 'old_b' argument is deprecated since python-can v1.6.0, and scheduled for "
+ "removal in python-can v3.4.0."
+ )
+
+ @deprecated_args_alias("1.6.0", "3.4.0", old_a="a")
+ @deprecated_args_alias("2.0.0", "4.0.0", old_b=None)
+ def _test_func3(a):
+ pass
+
+ with pytest.warns(DeprecationWarning) as record:
+ _test_func3(old_a=1, old_b=2)
+ assert len(record) == 2
+ assert record[0].message.args[0] == (
+ "The 'old_a' argument is deprecated since python-can v1.6.0, and scheduled "
+ "for removal in python-can v3.4.0. Use 'a' instead."
+ )
+ assert record[1].message.args[0] == (
+ "The 'old_b' argument is deprecated since python-can v2.0.0, and scheduled "
+ "for removal in python-can v4.0.0."
+ )
+
+ with pytest.warns(DeprecationWarning) as record:
+ _test_func3(old_a=1)
+ assert len(record) == 1
+ assert record[0].message.args[0] == (
+ "The 'old_a' argument is deprecated since python-can v1.6.0, and scheduled "
+ "for removal in python-can v3.4.0. Use 'a' instead."
+ )
+
+ with pytest.warns(DeprecationWarning) as record:
+ _test_func3(a=1, old_b=2)
+ assert len(record) == 1
+ assert record[0].message.args[0] == (
+ "The 'old_b' argument is deprecated since python-can v2.0.0, and scheduled "
+ "for removal in python-can v4.0.0."
+ )
+
+ with warnings.catch_warnings():
+ warnings.simplefilter("error")
+ _test_func3(a=1)
+
+
+class TestBusConfig(unittest.TestCase):
+ base_config = {"interface": "socketcan", "bitrate": 500_000}
+
+ def test_timing_can_use_int(self):
+ """
+ Test that an exception is not raised when using
+ integers for timing values in config.
+ """
+ timing_conf = dict(tseg1=5, tseg2=10, sjw=25)
+ try:
+ _create_bus_config({**self.base_config, **timing_conf})
+ except TypeError as e:
+ self.fail(e)
+
+ def test_port_datatype(self):
+ self.assertRaises(
+ ValueError, _create_bus_config, {**self.base_config, "port": "fail123"}
+ )
+ self.assertRaises(
+ ValueError, _create_bus_config, {**self.base_config, "port": "999999"}
+ )
+ self.assertRaises(
+ TypeError, _create_bus_config, {**self.base_config, "port": (1234,)}
+ )
+
+ try:
+ _create_bus_config({**self.base_config, "port": "1234"})
+ except TypeError as e:
+ self.fail(e)
+
+ def test_bit_timing_cfg(self):
+ can_cfg = _create_bus_config(
+ {
+ **self.base_config,
+ "f_clock": "8000000",
+ "brp": "1",
+ "tseg1": "5",
+ "tseg2": "2",
+ "sjw": "1",
+ "nof_samples": "1",
+ }
+ )
+ timing = can_cfg["timing"]
+ assert isinstance(timing, can.BitTiming)
+ assert timing.f_clock == 8_000_000
+ assert timing.brp == 1
+ assert timing.tseg1 == 5
+ assert timing.tseg2 == 2
+ assert timing.sjw == 1
+
+ def test_bit_timing_fd_cfg(self):
+ canfd_cfg = _create_bus_config(
+ {
+ **self.base_config,
+ "f_clock": "80000000",
+ "nom_brp": "1",
+ "nom_tseg1": "119",
+ "nom_tseg2": "40",
+ "nom_sjw": "40",
+ "data_brp": "1",
+ "data_tseg1": "29",
+ "data_tseg2": "10",
+ "data_sjw": "10",
+ }
+ )
+ timing = canfd_cfg["timing"]
+ assert isinstance(timing, can.BitTimingFd)
+ assert timing.f_clock == 80_000_000
+ assert timing.nom_brp == 1
+ assert timing.nom_tseg1 == 119
+ assert timing.nom_tseg2 == 40
+ assert timing.nom_sjw == 40
+ assert timing.data_brp == 1
+ assert timing.data_tseg1 == 29
+ assert timing.data_tseg2 == 10
+ assert timing.data_sjw == 10
+
+ def test_state_with_str(self):
+ can_cfg = _create_bus_config(
+ {
+ **self.base_config,
+ "state": "PASSIVE",
+ }
+ )
+ state = can_cfg["state"]
+ assert isinstance(state, can.BusState)
+ assert state is can.BusState.PASSIVE
+
+ def test_state_with_enum(self):
+ expected_state = can.BusState.PASSIVE
+ can_cfg = _create_bus_config(
+ {
+ **self.base_config,
+ "state": expected_state,
+ }
+ )
+ state = can_cfg["state"]
+ assert isinstance(state, can.BusState)
+ assert state is expected_state
+
+
+class TestChannel2Int(unittest.TestCase):
+ def test_channel2int(self) -> None:
+ self.assertEqual(0, channel2int("can0"))
+ self.assertEqual(0, channel2int("vcan0"))
+ self.assertEqual(1, channel2int("vcan1"))
+ self.assertEqual(12, channel2int("vcan12"))
+ self.assertEqual(3, channel2int(3))
+ self.assertEqual(42, channel2int("42"))
+ self.assertEqual(None, channel2int("can"))
+ self.assertEqual(None, channel2int("can0a"))
+
+
+class TestCheckAdjustTimingClock(unittest.TestCase):
+ def test_adjust_timing(self):
+ timing = BitTiming(f_clock=80_000_000, brp=10, tseg1=13, tseg2=2, sjw=1)
+
+ # Check identity case
+ new_timing = check_or_adjust_timing_clock(timing, valid_clocks=[80_000_000])
+ assert timing == new_timing
+
+ with pytest.warns(UserWarning) as record:
+ new_timing = check_or_adjust_timing_clock(
+ timing, valid_clocks=[8_000_000, 24_000_000]
+ )
+ assert len(record) == 1
+ assert (
+ record[0].message.args[0]
+ == "Adjusted f_clock in BitTiming from 80000000 to 8000000"
+ )
+ assert new_timing.__class__ == BitTiming
+ assert new_timing.f_clock == 8_000_000
+ assert new_timing.bitrate == timing.bitrate
+ assert new_timing.tseg1 == timing.tseg1
+ assert new_timing.tseg2 == timing.tseg2
+ assert new_timing.sjw == timing.sjw
+
+ # Check that order is preserved
+ with pytest.warns(UserWarning) as record:
+ new_timing = check_or_adjust_timing_clock(
+ timing, valid_clocks=[24_000_000, 8_000_000]
+ )
+ assert new_timing.f_clock == 24_000_000
+ assert len(record) == 1
+ assert (
+ record[0].message.args[0]
+ == "Adjusted f_clock in BitTiming from 80000000 to 24000000"
+ )
+
+ # Check that order is preserved for all valid clock rates
+ with pytest.warns(UserWarning) as record:
+ new_timing = check_or_adjust_timing_clock(
+ timing, valid_clocks=[8_000, 24_000_000, 8_000_000]
+ )
+ assert new_timing.f_clock == 24_000_000
+ assert len(record) == 1
+ assert (
+ record[0].message.args[0]
+ == "Adjusted f_clock in BitTiming from 80000000 to 24000000"
+ )
+
+ with pytest.raises(CanInitializationError):
+ check_or_adjust_timing_clock(timing, valid_clocks=[8_000, 16_000])
+
+ def test_adjust_timing_fd(self):
+ timing = BitTimingFd(
+ f_clock=160_000_000,
+ nom_brp=2,
+ nom_tseg1=119,
+ nom_tseg2=40,
+ nom_sjw=40,
+ data_brp=2,
+ data_tseg1=29,
+ data_tseg2=10,
+ data_sjw=10,
+ )
+
+ # Check identity case
+ new_timing = check_or_adjust_timing_clock(timing, valid_clocks=[160_000_000])
+ assert timing == new_timing
+
+ with pytest.warns(UserWarning) as record:
+ new_timing = check_or_adjust_timing_clock(
+ timing, valid_clocks=[8_000, 80_000_000]
+ )
+ assert len(record) == 1, "; ".join(
+ [record[i].message.args[0] for i in range(len(record))]
+ ) # print all warnings, if more than one warning is present
+ assert (
+ record[0].message.args[0]
+ == "Adjusted f_clock in BitTimingFd from 160000000 to 80000000"
+ )
+ assert new_timing.__class__ == BitTimingFd
+ assert new_timing.f_clock == 80_000_000
+ assert new_timing.nom_bitrate == timing.nom_bitrate
+ assert new_timing.nom_sample_point == timing.nom_sample_point
+ assert new_timing.data_bitrate == timing.data_bitrate
+ assert new_timing.data_sample_point == timing.data_sample_point
+
+ with pytest.raises(CanInitializationError):
+ check_or_adjust_timing_clock(timing, valid_clocks=[8_000, 16_000])
+
+
+class TestCastFromString(unittest.TestCase):
+ def test_cast_from_string(self) -> None:
+ self.assertEqual(1, cast_from_string("1"))
+ self.assertEqual(-1, cast_from_string("-1"))
+ self.assertEqual(0, cast_from_string("-0"))
+ self.assertEqual(1.1, cast_from_string("1.1"))
+ self.assertEqual(-1.1, cast_from_string("-1.1"))
+ self.assertEqual(0.1, cast_from_string(".1"))
+ self.assertEqual(10.0, cast_from_string(".1e2"))
+ self.assertEqual(0.001, cast_from_string(".1e-2"))
+ self.assertEqual(-0.001, cast_from_string("-.1e-2"))
+ self.assertEqual("text", cast_from_string("text"))
+ self.assertEqual("", cast_from_string(""))
+ self.assertEqual("can0", cast_from_string("can0"))
+ self.assertEqual("0can", cast_from_string("0can"))
+ self.assertEqual(False, cast_from_string("false"))
+ self.assertEqual(False, cast_from_string("False"))
+ self.assertEqual(True, cast_from_string("true"))
+ self.assertEqual(True, cast_from_string("True"))
+
+ with self.assertRaises(TypeError):
+ cast_from_string(None)
diff --git a/test/test_vector.py b/test/test_vector.py
new file mode 100644
index 000000000..5d074f614
--- /dev/null
+++ b/test/test_vector.py
@@ -0,0 +1,1297 @@
+#!/usr/bin/env python
+
+"""
+Test for Vector Interface
+"""
+
+import ctypes
+import functools
+import pickle
+import sys
+import time
+from test.config import IS_WINDOWS
+from unittest.mock import Mock
+
+import pytest
+
+import can
+from can.interfaces.vector import (
+ VectorBusParams,
+ VectorCanFdParams,
+ VectorCanParams,
+ VectorChannelConfig,
+ VectorError,
+ VectorInitializationError,
+ VectorOperationError,
+ canlib,
+ xlclass,
+ xldefine,
+)
+
+XLDRIVER_FOUND = canlib.xldriver is not None
+
+
+@pytest.fixture()
+def mock_xldriver() -> None:
+ # basic mock for XLDriver
+ xldriver_mock = Mock()
+
+ # bus creation functions
+ xldriver_mock.xlOpenDriver = Mock()
+ xldriver_mock.xlGetApplConfig = Mock(side_effect=xlGetApplConfig)
+ xldriver_mock.xlGetChannelIndex = Mock(side_effect=xlGetChannelIndex)
+ xldriver_mock.xlOpenPort = Mock(side_effect=xlOpenPort)
+ xldriver_mock.xlCanFdSetConfiguration = Mock(return_value=0)
+ xldriver_mock.xlCanSetChannelMode = Mock(return_value=0)
+ xldriver_mock.xlActivateChannel = Mock(return_value=0)
+ xldriver_mock.xlGetSyncTime = Mock(side_effect=xlGetSyncTime)
+ xldriver_mock.xlCanSetChannelAcceptance = Mock(return_value=0)
+ xldriver_mock.xlCanSetChannelBitrate = Mock(return_value=0)
+ xldriver_mock.xlSetNotification = Mock(side_effect=xlSetNotification)
+ xldriver_mock.xlCanSetChannelOutput = Mock(return_value=0)
+
+ # bus deactivation functions
+ xldriver_mock.xlDeactivateChannel = Mock(return_value=0)
+ xldriver_mock.xlClosePort = Mock(return_value=0)
+ xldriver_mock.xlCloseDriver = Mock()
+
+ # sender functions
+ xldriver_mock.xlCanTransmit = Mock(return_value=0)
+ xldriver_mock.xlCanTransmitEx = Mock(return_value=0)
+
+ # various functions
+ xldriver_mock.xlCanFlushTransmitQueue = Mock()
+
+ # backup unmodified values
+ real_xldriver = canlib.xldriver
+ real_waitforsingleobject = canlib.WaitForSingleObject
+ real_has_events = canlib.HAS_EVENTS
+
+ # set mock
+ canlib.xldriver = xldriver_mock
+ canlib.HAS_EVENTS = False
+
+ yield
+
+ # cleanup
+ canlib.xldriver = real_xldriver
+ canlib.WaitForSingleObject = real_waitforsingleobject
+ canlib.HAS_EVENTS = real_has_events
+
+
+def test_listen_only_mocked(mock_xldriver) -> None:
+ bus = can.Bus(channel=0, interface="vector", listen_only=True, _testing=True)
+ assert isinstance(bus, canlib.VectorBus)
+ assert bus.protocol == can.CanProtocol.CAN_20
+
+ can.interfaces.vector.canlib.xldriver.xlCanSetChannelOutput.assert_called()
+ xlCanSetChannelOutput_args = (
+ can.interfaces.vector.canlib.xldriver.xlCanSetChannelOutput.call_args[0]
+ )
+ assert xlCanSetChannelOutput_args[2] == xldefine.XL_OutputMode.XL_OUTPUT_MODE_SILENT
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_listen_only() -> None:
+ bus = can.Bus(
+ channel=0,
+ serial=_find_virtual_can_serial(),
+ interface="vector",
+ receive_own_messages=True,
+ listen_only=True,
+ )
+ assert isinstance(bus, canlib.VectorBus)
+ assert bus.protocol == can.CanProtocol.CAN_20
+
+ msg = can.Message(
+ arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True
+ )
+
+ bus.send(msg)
+
+ received_msg = bus.recv()
+
+ assert received_msg.arbitration_id == msg.arbitration_id
+ assert received_msg.data == msg.data
+
+ bus.shutdown()
+
+
+def test_bus_creation_mocked(mock_xldriver) -> None:
+ bus = can.Bus(channel=0, interface="vector", _testing=True)
+ assert isinstance(bus, canlib.VectorBus)
+ assert bus.protocol == can.CanProtocol.CAN_20
+
+ can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called()
+
+ can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called()
+ xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0]
+ assert xlOpenPort_args[5] == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION.value
+ assert xlOpenPort_args[6] == xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value
+
+ can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_not_called()
+ can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_not_called()
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_bus_creation() -> None:
+ bus = can.Bus(channel=0, serial=_find_virtual_can_serial(), interface="vector")
+ assert isinstance(bus, canlib.VectorBus)
+ assert bus.protocol == can.CanProtocol.CAN_20
+
+ bus.shutdown()
+
+ xl_channel_config = _find_xl_channel_config(
+ serial=_find_virtual_can_serial(), channel=0
+ )
+ assert bus.channel_masks[0] == xl_channel_config.channelMask
+ assert (
+ xl_channel_config.busParams.data.can.canOpMode
+ & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CAN20
+ )
+
+ bus = canlib.VectorBus(channel=0, serial=_find_virtual_can_serial())
+ assert isinstance(bus, canlib.VectorBus)
+ assert bus.protocol == can.CanProtocol.CAN_20
+ bus.shutdown()
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_bus_creation_channel_index() -> None:
+ channel_index = 1
+ bus = can.Bus(
+ channel=0,
+ serial=_find_virtual_can_serial(),
+ channel_index=channel_index,
+ interface="vector",
+ )
+ assert isinstance(bus, canlib.VectorBus)
+ assert bus.protocol == can.CanProtocol.CAN_20
+ assert bus.channel_masks[0] == 1 << channel_index
+
+ bus.shutdown()
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_bus_creation_multiple_channels() -> None:
+ bus = can.Bus(
+ channel="0, 1",
+ bitrate=1_000_000,
+ serial=_find_virtual_can_serial(),
+ interface="vector",
+ )
+ assert isinstance(bus, canlib.VectorBus)
+ assert bus.protocol == can.CanProtocol.CAN_20
+ assert len(bus.channels) == 2
+ assert bus.mask == 3
+
+ xl_channel_config_0 = _find_xl_channel_config(
+ serial=_find_virtual_can_serial(), channel=0
+ )
+ assert xl_channel_config_0.busParams.data.can.bitRate == 1_000_000
+
+ xl_channel_config_1 = _find_xl_channel_config(
+ serial=_find_virtual_can_serial(), channel=1
+ )
+ assert xl_channel_config_1.busParams.data.can.bitRate == 1_000_000
+
+ bus.shutdown()
+
+
+def test_bus_creation_bitrate_mocked(mock_xldriver) -> None:
+ bus = can.Bus(channel=0, interface="vector", bitrate=200_000, _testing=True)
+ assert isinstance(bus, canlib.VectorBus)
+ assert bus.protocol == can.CanProtocol.CAN_20
+
+ can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called()
+
+ can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called()
+ xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0]
+ assert xlOpenPort_args[5] == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION.value
+ assert xlOpenPort_args[6] == xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value
+
+ can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_not_called()
+ can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_called()
+ xlCanSetChannelBitrate_args = (
+ can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.call_args[0]
+ )
+ assert xlCanSetChannelBitrate_args[2] == 200_000
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_bus_creation_bitrate() -> None:
+ bus = can.Bus(
+ channel=0,
+ serial=_find_virtual_can_serial(),
+ interface="vector",
+ bitrate=200_000,
+ )
+ assert isinstance(bus, canlib.VectorBus)
+ assert bus.protocol == can.CanProtocol.CAN_20
+
+ xl_channel_config = _find_xl_channel_config(
+ serial=_find_virtual_can_serial(), channel=0
+ )
+ assert xl_channel_config.busParams.data.can.bitRate == 200_000
+
+ bus.shutdown()
+
+
+def test_bus_creation_fd_mocked(mock_xldriver) -> None:
+ bus = can.Bus(channel=0, interface="vector", fd=True, _testing=True)
+ assert isinstance(bus, canlib.VectorBus)
+ assert bus.protocol == can.CanProtocol.CAN_FD
+
+ can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called()
+
+ can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called()
+ xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0]
+ assert (
+ xlOpenPort_args[5] == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4.value
+ )
+ assert xlOpenPort_args[6] == xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value
+
+ can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_not_called()
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_bus_creation_fd() -> None:
+ bus = can.Bus(
+ channel=0, serial=_find_virtual_can_serial(), interface="vector", fd=True
+ )
+ assert isinstance(bus, canlib.VectorBus)
+ assert bus.protocol == can.CanProtocol.CAN_FD
+
+ xl_channel_config = _find_xl_channel_config(
+ serial=_find_virtual_can_serial(), channel=0
+ )
+ assert (
+ xl_channel_config.interfaceVersion
+ == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4
+ )
+ assert (
+ xl_channel_config.busParams.data.canFD.canOpMode
+ & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CANFD
+ )
+ bus.shutdown()
+
+
+def test_bus_creation_fd_bitrate_timings_mocked(mock_xldriver) -> None:
+ bus = can.Bus(
+ channel=0,
+ interface="vector",
+ fd=True,
+ bitrate=500_000,
+ data_bitrate=2_000_000,
+ sjw_abr=16,
+ tseg1_abr=127,
+ tseg2_abr=32,
+ sjw_dbr=6,
+ tseg1_dbr=27,
+ tseg2_dbr=12,
+ _testing=True,
+ )
+ assert isinstance(bus, canlib.VectorBus)
+ assert bus.protocol == can.CanProtocol.CAN_FD
+
+ can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called()
+
+ can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called()
+ xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0]
+ assert (
+ xlOpenPort_args[5] == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4.value
+ )
+
+ assert xlOpenPort_args[6] == xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value
+
+ can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_not_called()
+
+ xlCanFdSetConfiguration_args = (
+ can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.call_args[0]
+ )
+ canFdConf = xlCanFdSetConfiguration_args[2]
+ assert canFdConf.arbitrationBitRate == 500000
+ assert canFdConf.dataBitRate == 2000000
+ assert canFdConf.sjwAbr == 16
+ assert canFdConf.tseg1Abr == 127
+ assert canFdConf.tseg2Abr == 32
+ assert canFdConf.sjwDbr == 6
+ assert canFdConf.tseg1Dbr == 27
+ assert canFdConf.tseg2Dbr == 12
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_bus_creation_fd_bitrate_timings() -> None:
+ bus = can.Bus(
+ channel=0,
+ serial=_find_virtual_can_serial(),
+ interface="vector",
+ fd=True,
+ bitrate=500_000,
+ data_bitrate=2_000_000,
+ sjw_abr=16,
+ tseg1_abr=127,
+ tseg2_abr=32,
+ sjw_dbr=6,
+ tseg1_dbr=27,
+ tseg2_dbr=12,
+ )
+
+ xl_channel_config = _find_xl_channel_config(
+ serial=_find_virtual_can_serial(), channel=0
+ )
+ assert (
+ xl_channel_config.interfaceVersion
+ == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4
+ )
+ assert (
+ xl_channel_config.busParams.data.canFD.canOpMode
+ & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CANFD
+ )
+ assert xl_channel_config.busParams.data.canFD.arbitrationBitRate == 500_000
+ assert xl_channel_config.busParams.data.canFD.sjwAbr == 16
+ assert xl_channel_config.busParams.data.canFD.tseg1Abr == 127
+ assert xl_channel_config.busParams.data.canFD.tseg2Abr == 32
+ assert xl_channel_config.busParams.data.canFD.sjwDbr == 6
+ assert xl_channel_config.busParams.data.canFD.tseg1Dbr == 27
+ assert xl_channel_config.busParams.data.canFD.tseg2Dbr == 12
+ assert xl_channel_config.busParams.data.canFD.dataBitRate == 2_000_000
+
+ bus.shutdown()
+
+
+def test_bus_creation_timing_8mhz_mocked(mock_xldriver) -> None:
+ timing = can.BitTiming.from_bitrate_and_segments(
+ f_clock=8_000_000,
+ bitrate=125_000,
+ tseg1=13,
+ tseg2=2,
+ sjw=1,
+ )
+ bus = can.Bus(channel=0, interface="vector", timing=timing, _testing=True)
+ assert isinstance(bus, canlib.VectorBus)
+ can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called()
+
+ can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called()
+ xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0]
+ assert xlOpenPort_args[5] == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION.value
+ assert xlOpenPort_args[6] == xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value
+
+ can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_not_called()
+ can.interfaces.vector.canlib.xldriver.xlCanSetChannelParamsC200.assert_called()
+ btr0, btr1 = (
+ can.interfaces.vector.canlib.xldriver.xlCanSetChannelParamsC200.call_args[0]
+ )[2:]
+ assert btr0 == timing.btr0
+ assert btr1 == timing.btr1
+
+
+def test_bus_creation_timing_16mhz_mocked(mock_xldriver) -> None:
+ timing = can.BitTiming.from_bitrate_and_segments(
+ f_clock=16_000_000,
+ bitrate=125_000,
+ tseg1=13,
+ tseg2=2,
+ sjw=1,
+ )
+ bus = can.Bus(channel=0, interface="vector", timing=timing, _testing=True)
+ assert isinstance(bus, canlib.VectorBus)
+ can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called()
+
+ can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called()
+ xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0]
+ assert xlOpenPort_args[5] == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION.value
+ assert xlOpenPort_args[6] == xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value
+
+ can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_not_called()
+ can.interfaces.vector.canlib.xldriver.xlCanSetChannelParams.assert_called()
+ chip_params = (
+ can.interfaces.vector.canlib.xldriver.xlCanSetChannelParams.call_args[0]
+ )[2]
+ assert chip_params.bitRate == 125_000
+ assert chip_params.sjw == 1
+ assert chip_params.tseg1 == 13
+ assert chip_params.tseg2 == 2
+ assert chip_params.sam == 1
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_bus_creation_timing() -> None:
+ for f_clock in [8_000_000, 16_000_000]:
+ timing = can.BitTiming.from_bitrate_and_segments(
+ f_clock=f_clock,
+ bitrate=125_000,
+ tseg1=13,
+ tseg2=2,
+ sjw=1,
+ )
+ bus = can.Bus(
+ channel=0,
+ serial=_find_virtual_can_serial(),
+ interface="vector",
+ timing=timing,
+ )
+ assert isinstance(bus, canlib.VectorBus)
+ assert bus.protocol == can.CanProtocol.CAN_20
+
+ xl_channel_config = _find_xl_channel_config(
+ serial=_find_virtual_can_serial(), channel=0
+ )
+ assert xl_channel_config.busParams.data.can.bitRate == 125_000
+ assert xl_channel_config.busParams.data.can.sjw == 1
+ assert xl_channel_config.busParams.data.can.tseg1 == 13
+ assert xl_channel_config.busParams.data.can.tseg2 == 2
+
+ bus.shutdown()
+
+
+def test_bus_creation_timingfd_mocked(mock_xldriver) -> None:
+ timing = can.BitTimingFd.from_bitrate_and_segments(
+ f_clock=80_000_000,
+ nom_bitrate=500_000,
+ nom_tseg1=68,
+ nom_tseg2=11,
+ nom_sjw=10,
+ data_bitrate=2_000_000,
+ data_tseg1=10,
+ data_tseg2=9,
+ data_sjw=8,
+ )
+ bus = can.Bus(
+ channel=0,
+ interface="vector",
+ timing=timing,
+ _testing=True,
+ )
+ assert isinstance(bus, canlib.VectorBus)
+ assert bus.protocol == can.CanProtocol.CAN_FD
+
+ can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called()
+
+ can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called()
+ xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0]
+ assert (
+ xlOpenPort_args[5] == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4.value
+ )
+
+ assert xlOpenPort_args[6] == xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value
+
+ can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_not_called()
+
+ xlCanFdSetConfiguration_args = (
+ can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.call_args[0]
+ )
+ canFdConf = xlCanFdSetConfiguration_args[2]
+ assert canFdConf.arbitrationBitRate == 500_000
+ assert canFdConf.dataBitRate == 2_000_000
+ assert canFdConf.sjwAbr == 10
+ assert canFdConf.tseg1Abr == 68
+ assert canFdConf.tseg2Abr == 11
+ assert canFdConf.sjwDbr == 8
+ assert canFdConf.tseg1Dbr == 10
+ assert canFdConf.tseg2Dbr == 9
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_bus_creation_timingfd() -> None:
+ timing = can.BitTimingFd.from_bitrate_and_segments(
+ f_clock=80_000_000,
+ nom_bitrate=500_000,
+ nom_tseg1=68,
+ nom_tseg2=11,
+ nom_sjw=10,
+ data_bitrate=2_000_000,
+ data_tseg1=10,
+ data_tseg2=9,
+ data_sjw=8,
+ )
+ bus = can.Bus(
+ channel=0,
+ serial=_find_virtual_can_serial(),
+ interface="vector",
+ timing=timing,
+ )
+
+ assert bus.protocol == can.CanProtocol.CAN_FD
+
+ xl_channel_config = _find_xl_channel_config(
+ serial=_find_virtual_can_serial(), channel=0
+ )
+ assert (
+ xl_channel_config.interfaceVersion
+ == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4
+ )
+ assert (
+ xl_channel_config.busParams.data.canFD.canOpMode
+ & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CANFD
+ )
+ assert xl_channel_config.busParams.data.canFD.arbitrationBitRate == 500_000
+ assert xl_channel_config.busParams.data.canFD.sjwAbr == 10
+ assert xl_channel_config.busParams.data.canFD.tseg1Abr == 68
+ assert xl_channel_config.busParams.data.canFD.tseg2Abr == 11
+ assert xl_channel_config.busParams.data.canFD.sjwDbr == 8
+ assert xl_channel_config.busParams.data.canFD.tseg1Dbr == 10
+ assert xl_channel_config.busParams.data.canFD.tseg2Dbr == 9
+ assert xl_channel_config.busParams.data.canFD.dataBitRate == 2_000_000
+
+ bus.shutdown()
+
+
+def test_send_mocked(mock_xldriver) -> None:
+ bus = can.Bus(channel=0, interface="vector", _testing=True)
+ msg = can.Message(
+ arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True
+ )
+ bus.send(msg)
+ can.interfaces.vector.canlib.xldriver.xlCanTransmit.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlCanTransmitEx.assert_not_called()
+
+
+def test_send_fd_mocked(mock_xldriver) -> None:
+ bus = can.Bus(channel=0, interface="vector", fd=True, _testing=True)
+ msg = can.Message(
+ arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True
+ )
+ bus.send(msg)
+ can.interfaces.vector.canlib.xldriver.xlCanTransmit.assert_not_called()
+ can.interfaces.vector.canlib.xldriver.xlCanTransmitEx.assert_called()
+
+
+def test_receive_mocked(mock_xldriver) -> None:
+ can.interfaces.vector.canlib.xldriver.xlReceive = Mock(side_effect=xlReceive)
+ bus = can.Bus(channel=0, interface="vector", _testing=True)
+ bus.recv(timeout=0.05)
+ can.interfaces.vector.canlib.xldriver.xlReceive.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlCanReceive.assert_not_called()
+
+
+def test_receive_fd_mocked(mock_xldriver) -> None:
+ can.interfaces.vector.canlib.xldriver.xlCanReceive = Mock(side_effect=xlCanReceive)
+ bus = can.Bus(channel=0, interface="vector", fd=True, _testing=True)
+ bus.recv(timeout=0.05)
+ can.interfaces.vector.canlib.xldriver.xlReceive.assert_not_called()
+ can.interfaces.vector.canlib.xldriver.xlCanReceive.assert_called()
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_send_and_receive() -> None:
+ bus1 = can.Bus(channel=0, serial=_find_virtual_can_serial(), interface="vector")
+ bus2 = can.Bus(channel=0, serial=_find_virtual_can_serial(), interface="vector")
+
+ msg_std = can.Message(
+ channel=0, arbitration_id=0xFF, data=list(range(8)), is_extended_id=False
+ )
+ msg_ext = can.Message(
+ channel=0, arbitration_id=0xFFFFFF, data=list(range(8)), is_extended_id=True
+ )
+
+ bus1.send(msg_std)
+ msg_std_recv = bus2.recv(None)
+ assert msg_std.equals(msg_std_recv, timestamp_delta=None)
+
+ bus1.send(msg_ext)
+ msg_ext_recv = bus2.recv(None)
+ assert msg_ext.equals(msg_ext_recv, timestamp_delta=None)
+
+ bus1.shutdown()
+ bus2.shutdown()
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_send_and_receive_fd() -> None:
+ bus1 = can.Bus(
+ channel=0, serial=_find_virtual_can_serial(), fd=True, interface="vector"
+ )
+ bus2 = can.Bus(
+ channel=0, serial=_find_virtual_can_serial(), fd=True, interface="vector"
+ )
+
+ msg_std = can.Message(
+ channel=0,
+ arbitration_id=0xFF,
+ data=list(range(64)),
+ is_extended_id=False,
+ is_fd=True,
+ )
+ msg_ext = can.Message(
+ channel=0,
+ arbitration_id=0xFFFFFF,
+ data=list(range(64)),
+ is_extended_id=True,
+ is_fd=True,
+ )
+
+ bus1.send(msg_std)
+ msg_std_recv = bus2.recv(None)
+ assert msg_std.equals(msg_std_recv, timestamp_delta=None)
+
+ bus1.send(msg_ext)
+ msg_ext_recv = bus2.recv(None)
+ assert msg_ext.equals(msg_ext_recv, timestamp_delta=None)
+
+ bus1.shutdown()
+ bus2.shutdown()
+
+
+def test_receive_non_msg_event_mocked(mock_xldriver) -> None:
+ can.interfaces.vector.canlib.xldriver.xlReceive = Mock(
+ side_effect=xlReceive_chipstate
+ )
+ bus = can.Bus(channel=0, interface="vector", _testing=True)
+ bus.handle_can_event = Mock()
+ bus.recv(timeout=0.05)
+ can.interfaces.vector.canlib.xldriver.xlReceive.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlCanReceive.assert_not_called()
+ bus.handle_can_event.assert_called()
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_receive_non_msg_event() -> None:
+ bus = canlib.VectorBus(
+ channel=0, serial=_find_virtual_can_serial(), interface="vector"
+ )
+ bus.handle_can_event = Mock()
+ bus.xldriver.xlCanRequestChipState(bus.port_handle, bus.channel_masks[0])
+ bus.recv(timeout=0.5)
+ bus.handle_can_event.assert_called()
+ bus.shutdown()
+
+
+def test_receive_fd_non_msg_event_mocked(mock_xldriver) -> None:
+ can.interfaces.vector.canlib.xldriver.xlCanReceive = Mock(
+ side_effect=xlCanReceive_chipstate
+ )
+ bus = can.Bus(channel=0, interface="vector", fd=True, _testing=True)
+ bus.handle_canfd_event = Mock()
+ bus.recv(timeout=0.05)
+ can.interfaces.vector.canlib.xldriver.xlReceive.assert_not_called()
+ can.interfaces.vector.canlib.xldriver.xlCanReceive.assert_called()
+ bus.handle_canfd_event.assert_called()
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_receive_fd_non_msg_event() -> None:
+ bus = canlib.VectorBus(
+ channel=0, serial=_find_virtual_can_serial(), fd=True, interface="vector"
+ )
+ bus.handle_canfd_event = Mock()
+ bus.xldriver.xlCanRequestChipState(bus.port_handle, bus.channel_masks[0])
+ bus.recv(timeout=0.5)
+ bus.handle_canfd_event.assert_called()
+ bus.shutdown()
+
+
+def test_flush_tx_buffer_mocked(mock_xldriver) -> None:
+ bus = can.Bus(channel=0, interface="vector", _testing=True)
+ bus.flush_tx_buffer()
+ transmit_args = can.interfaces.vector.canlib.xldriver.xlCanTransmit.call_args[0]
+
+ num_msg = transmit_args[2]
+ assert num_msg.value == ctypes.c_uint(1).value
+
+ event = transmit_args[3]
+ assert isinstance(event, xlclass.XLevent)
+ assert event.tag & xldefine.XL_EventTags.XL_TRANSMIT_MSG
+ assert event.tagData.msg.flags & (
+ xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_OVERRUN
+ | xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_WAKEUP
+ )
+
+
+def test_flush_tx_buffer_fd_mocked(mock_xldriver) -> None:
+ bus = can.Bus(channel=0, interface="vector", fd=True, _testing=True)
+ bus.flush_tx_buffer()
+ transmit_args = can.interfaces.vector.canlib.xldriver.xlCanTransmitEx.call_args[0]
+
+ num_msg = transmit_args[2]
+ assert num_msg.value == ctypes.c_uint(1).value
+
+ num_msg_sent = transmit_args[3]
+ assert num_msg_sent.value == ctypes.c_uint(0).value
+
+ event = transmit_args[4]
+ assert isinstance(event, xlclass.XLcanTxEvent)
+ assert event.tag & xldefine.XL_CANFD_TX_EventTags.XL_CAN_EV_TAG_TX_MSG
+ assert (
+ event.tagData.canMsg.msgFlags
+ & xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_HIGHPRIO
+ )
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_flush_tx_buffer() -> None:
+ bus = can.Bus(channel=0, serial=_find_virtual_can_serial(), interface="vector")
+ bus.flush_tx_buffer()
+ bus.shutdown()
+
+
+def test_shutdown_mocked(mock_xldriver) -> None:
+ bus = can.Bus(channel=0, interface="vector", _testing=True)
+ bus.shutdown()
+ can.interfaces.vector.canlib.xldriver.xlDeactivateChannel.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlClosePort.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlCloseDriver.assert_called()
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_shutdown() -> None:
+ bus = can.Bus(channel=0, serial=_find_virtual_can_serial(), interface="vector")
+
+ xl_channel_config = _find_xl_channel_config(
+ serial=_find_virtual_can_serial(), channel=0
+ )
+ assert xl_channel_config.isOnBus != 0
+ bus.shutdown()
+
+ xl_channel_config = _find_xl_channel_config(
+ serial=_find_virtual_can_serial(), channel=0
+ )
+ assert xl_channel_config.isOnBus == 0
+
+
+def test_reset_mocked(mock_xldriver) -> None:
+ bus = canlib.VectorBus(channel=0, interface="vector", _testing=True)
+ bus.reset()
+ can.interfaces.vector.canlib.xldriver.xlDeactivateChannel.assert_called()
+ can.interfaces.vector.canlib.xldriver.xlActivateChannel.assert_called()
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_reset() -> None:
+ bus = canlib.VectorBus(
+ channel=0, serial=_find_virtual_can_serial(), interface="vector"
+ )
+ bus.reset()
+ bus.shutdown()
+
+
+def test_popup_hw_cfg_mocked(mock_xldriver) -> None:
+ canlib.xldriver.xlPopupHwConfig = Mock()
+ canlib.VectorBus.popup_vector_hw_configuration(10)
+ assert canlib.xldriver.xlPopupHwConfig.called
+ args, kwargs = canlib.xldriver.xlPopupHwConfig.call_args
+ assert isinstance(args[0], ctypes.c_char_p)
+ assert isinstance(args[1], ctypes.c_uint)
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_popup_hw_cfg() -> None:
+ with pytest.raises(VectorOperationError):
+ canlib.VectorBus.popup_vector_hw_configuration(1)
+
+
+def test_get_application_config_mocked(mock_xldriver) -> None:
+ canlib.xldriver.xlGetApplConfig = Mock()
+ canlib.VectorBus.get_application_config(app_name="CANalyzer", app_channel=0)
+ assert canlib.xldriver.xlGetApplConfig.called
+
+
+def test_set_application_config_mocked(mock_xldriver) -> None:
+ canlib.xldriver.xlSetApplConfig = Mock()
+ canlib.VectorBus.set_application_config(
+ app_name="CANalyzer",
+ app_channel=0,
+ hw_type=xldefine.XL_HardwareType.XL_HWTYPE_VN1610,
+ hw_index=0,
+ hw_channel=0,
+ )
+ assert canlib.xldriver.xlSetApplConfig.called
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_set_and_get_application_config() -> None:
+ xl_channel_config = _find_xl_channel_config(
+ serial=_find_virtual_can_serial(), channel=1
+ )
+ canlib.VectorBus.set_application_config(
+ app_name="python-can::test_vector",
+ app_channel=5,
+ hw_channel=xl_channel_config.hwChannel,
+ hw_index=xl_channel_config.hwIndex,
+ hw_type=xldefine.XL_HardwareType(xl_channel_config.hwType),
+ )
+ hw_type, hw_index, hw_channel = canlib.VectorBus.get_application_config(
+ app_name="python-can::test_vector",
+ app_channel=5,
+ )
+ assert hw_type == xldefine.XL_HardwareType(xl_channel_config.hwType)
+ assert hw_index == xl_channel_config.hwIndex
+ assert hw_channel == xl_channel_config.hwChannel
+
+
+def test_set_timer_mocked(mock_xldriver) -> None:
+ canlib.xldriver.xlSetTimerRate = Mock()
+ bus = canlib.VectorBus(channel=0, interface="vector", fd=True, _testing=True)
+ bus.set_timer_rate(timer_rate_ms=1)
+ assert canlib.xldriver.xlSetTimerRate.called
+
+
+@pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable")
+def test_set_timer() -> None:
+ bus = canlib.VectorBus(
+ channel=0, serial=_find_virtual_can_serial(), interface="vector"
+ )
+ bus.handle_can_event = Mock()
+ bus.set_timer_rate(timer_rate_ms=1)
+ t0 = time.perf_counter()
+ while time.perf_counter() - t0 < 0.5:
+ bus.recv(timeout=-1)
+
+ # call_count is incorrect when using virtual bus
+ # assert bus.handle_can_event.call_count > 498
+ # assert bus.handle_can_event.call_count < 502
+
+
+@pytest.mark.skipif(IS_WINDOWS, reason="Not relevant for Windows.")
+def test_called_without_testing_argument() -> None:
+ """This tests if an exception is thrown when we are not running on Windows."""
+ with pytest.raises(can.CanInterfaceNotImplementedError):
+ # do not set the _testing argument, since it would suppress the exception
+ can.Bus(channel=0, interface="vector")
+
+
+def test_vector_error_pickle() -> None:
+ for error_type in [
+ VectorError,
+ VectorInitializationError,
+ VectorOperationError,
+ ]:
+ error_code = 118
+ error_string = "XL_ERROR"
+ function = "function_name"
+
+ exc = error_type(error_code, error_string, function)
+
+ # pickle and unpickle
+ p = pickle.dumps(exc)
+ exc_unpickled: VectorError = pickle.loads(p)
+
+ assert str(exc) == str(exc_unpickled)
+ assert error_code == exc_unpickled.error_code
+
+ with pytest.raises(error_type):
+ raise exc_unpickled
+
+
+def test_vector_subtype_error_from_generic() -> None:
+ for error_type in [VectorInitializationError, VectorOperationError]:
+ error_code = 118
+ error_string = "XL_ERROR"
+ function = "function_name"
+
+ generic = VectorError(error_code, error_string, function)
+
+ # pickle and unpickle
+ specific: VectorError = error_type.from_generic(generic)
+
+ assert str(generic) == str(specific)
+ assert error_code == specific.error_code
+
+ with pytest.raises(error_type):
+ raise specific
+
+
+def test_iterate_channel_index() -> None:
+ channel_mask = 0x23 # 100011
+ channels = list(canlib._iterate_channel_index(channel_mask))
+ assert channels == [0, 1, 5]
+
+
+@pytest.mark.skipif(
+ sys.byteorder != "little", reason="Test relies on little endian data."
+)
+def test_get_channel_configs() -> None:
+ _original_func = canlib._get_xl_driver_config
+ canlib._get_xl_driver_config = _get_predefined_xl_driver_config
+
+ channel_configs = canlib.get_channel_configs()
+ assert len(channel_configs) == 12
+
+ canlib._get_xl_driver_config = _original_func
+
+
+@pytest.mark.skipif(
+ sys.byteorder != "little", reason="Test relies on little endian data."
+)
+def test_detect_available_configs() -> None:
+ _original_func = canlib._get_xl_driver_config
+ canlib._get_xl_driver_config = _get_predefined_xl_driver_config
+
+ available_configs = canlib.VectorBus._detect_available_configs()
+
+ assert len(available_configs) == 5
+
+ assert available_configs[0]["interface"] == "vector"
+ assert available_configs[0]["channel"] == 2
+ assert available_configs[0]["serial"] == 1001
+ assert available_configs[0]["channel_index"] == 2
+ assert available_configs[0]["hw_type"] == xldefine.XL_HardwareType.XL_HWTYPE_VN8900
+ assert available_configs[0]["hw_index"] == 0
+ assert available_configs[0]["supports_fd"] is True
+ assert isinstance(
+ available_configs[0]["vector_channel_config"], VectorChannelConfig
+ )
+
+ canlib._get_xl_driver_config = _original_func
+
+
+@pytest.mark.skipif(not IS_WINDOWS, reason="Windows specific test")
+def test_winapi_availability() -> None:
+ assert canlib.WaitForSingleObject is not None
+ assert canlib.INFINITE is not None
+
+
+def test_vector_channel_config_attributes():
+ assert hasattr(VectorChannelConfig, "name")
+ assert hasattr(VectorChannelConfig, "hw_type")
+ assert hasattr(VectorChannelConfig, "hw_index")
+ assert hasattr(VectorChannelConfig, "hw_channel")
+ assert hasattr(VectorChannelConfig, "channel_index")
+ assert hasattr(VectorChannelConfig, "channel_mask")
+ assert hasattr(VectorChannelConfig, "channel_capabilities")
+ assert hasattr(VectorChannelConfig, "channel_bus_capabilities")
+ assert hasattr(VectorChannelConfig, "is_on_bus")
+ assert hasattr(VectorChannelConfig, "bus_params")
+ assert hasattr(VectorChannelConfig, "connected_bus_type")
+ assert hasattr(VectorChannelConfig, "serial_number")
+ assert hasattr(VectorChannelConfig, "article_number")
+ assert hasattr(VectorChannelConfig, "transceiver_name")
+
+
+def test_vector_bus_params_attributes():
+ assert hasattr(VectorBusParams, "bus_type")
+ assert hasattr(VectorBusParams, "can")
+ assert hasattr(VectorBusParams, "canfd")
+
+
+def test_vector_can_params_attributes():
+ assert hasattr(VectorCanParams, "bitrate")
+ assert hasattr(VectorCanParams, "sjw")
+ assert hasattr(VectorCanParams, "tseg1")
+ assert hasattr(VectorCanParams, "tseg2")
+ assert hasattr(VectorCanParams, "sam")
+ assert hasattr(VectorCanParams, "output_mode")
+ assert hasattr(VectorCanParams, "can_op_mode")
+
+
+def test_vector_canfd_params_attributes():
+ assert hasattr(VectorCanFdParams, "bitrate")
+ assert hasattr(VectorCanFdParams, "data_bitrate")
+ assert hasattr(VectorCanFdParams, "sjw_abr")
+ assert hasattr(VectorCanFdParams, "tseg1_abr")
+ assert hasattr(VectorCanFdParams, "tseg2_abr")
+ assert hasattr(VectorCanFdParams, "sam_abr")
+ assert hasattr(VectorCanFdParams, "sjw_dbr")
+ assert hasattr(VectorCanFdParams, "tseg1_dbr")
+ assert hasattr(VectorCanFdParams, "tseg2_dbr")
+ assert hasattr(VectorCanFdParams, "output_mode")
+ assert hasattr(VectorCanFdParams, "can_op_mode")
+
+
+# *****************************************************************************
+# Utility functions
+# *****************************************************************************
+
+
+def _find_xl_channel_config(serial: int, channel: int) -> xlclass.XLchannelConfig:
+ """Helper function"""
+ xl_driver_config = xlclass.XLdriverConfig()
+ canlib.xldriver.xlOpenDriver()
+ canlib.xldriver.xlGetDriverConfig(xl_driver_config)
+ canlib.xldriver.xlCloseDriver()
+
+ for i in range(xl_driver_config.channelCount):
+ xl_channel_config: xlclass.XLchannelConfig = xl_driver_config.channel[i]
+
+ if xl_channel_config.serialNumber != serial:
+ continue
+
+ if xl_channel_config.hwChannel != channel:
+ continue
+
+ return xl_channel_config
+
+ raise LookupError("XLchannelConfig not found.")
+
+
+@functools.lru_cache
+def _find_virtual_can_serial() -> int:
+ """Serial number might be 0 or 100 depending on driver version."""
+ xl_driver_config = xlclass.XLdriverConfig()
+ canlib.xldriver.xlOpenDriver()
+ canlib.xldriver.xlGetDriverConfig(xl_driver_config)
+ canlib.xldriver.xlCloseDriver()
+
+ for i in range(xl_driver_config.channelCount):
+ xl_channel_config: xlclass.XLchannelConfig = xl_driver_config.channel[i]
+
+ if "Virtual CAN" in xl_channel_config.transceiverName.decode():
+ return xl_channel_config.serialNumber
+
+ raise LookupError("Vector virtual CAN not found")
+
+
+XL_DRIVER_CONFIG_EXAMPLE = (
+ b"\x0e\x00\x1e\x14\x0c\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e\x6e"
+ b"\x65\x6c\x20\x53\x74\x72\x65\x61\x6d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x2d\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x04"
+ b"\x0a\x40\x00\x02\x00\x02\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x04\x00\x00\x00\x00\x00\x00\x00\x8e"
+ b"\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00\x08"
+ b"\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31"
+ b"\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x31\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x01\x03\x02\x00\x00\x00\x00\x01\x02\x00\x00"
+ b"\x00\x00\x00\x00\x00\x02\x10\x00\x08\x07\x01\x04\x00\x00\x00\x00\x00\x00\x04\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x04\x00"
+ b"\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x46\x52\x70\x69\x67\x67\x79\x20\x31\x30"
+ b"\x38\x30\x41\x6d\x61\x67\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x05\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x32\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x02\x3c\x01\x00"
+ b"\x00\x00\x00\x02\x04\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\xa2\x03\x05\x01\x00"
+ b"\x00\x00\x04\x00\x00\x01\x00\x00\x00\x20\xa1\x07\x00\x01\x04\x03\x01\x01\x00\x00"
+ b"\x00\x00\x00\x00\x00\x01\x80\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x0c\x00\x02\x0a\x04\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00\x00"
+ b"\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x4f\x6e\x20"
+ b"\x62\x6f\x61\x72\x64\x20\x43\x41\x4e\x20\x31\x30\x35\x31\x63\x61\x70\x28\x48\x69"
+ b"\x67\x68\x73\x70\x65\x65\x64\x29\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03"
+ b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e"
+ b"\x6e\x65\x6c\x20\x33\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x2d\x00\x03\x3c\x01\x00\x00\x00\x00\x03\x08\x00\x00\x00\x00\x00\x00\x00\x12"
+ b"\x00\x00\xa2\x03\x09\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xa1\x07\x00"
+ b"\x01\x04\x03\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x9b\x00\x00\x00\x68\x89\x09"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x04\x00\x00\x00\x00\x00\x00\x00"
+ b"\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00"
+ b"\x08\x1c\x00\x00\x4f\x6e\x20\x62\x6f\x61\x72\x64\x20\x43\x41\x4e\x20\x31\x30\x35"
+ b"\x31\x63\x61\x70\x28\x48\x69\x67\x68\x73\x70\x65\x65\x64\x29\x00\x04\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39"
+ b"\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x34\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x04\x33\x01\x00\x00\x00\x00\x04\x10\x00"
+ b"\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x09\x02\x08\x00\x00\x00\x00\x00\x02"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x03"
+ b"\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x4c\x49\x4e\x70\x69\x67\x67\x79\x20"
+ b"\x37\x32\x36\x39\x6d\x61\x67\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x07\x00\x00\x00\x70\x17\x00\x00\x0c\x09\x03\x04\x58\x02\x10\x0e\x30"
+ b"\x57\x05\x00\x00\x00\x00\x00\x88\x13\x88\x13\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x35\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x05\x00\x00"
+ b"\x00\x00\x02\x00\x05\x20\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x0c\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00"
+ b"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61"
+ b"\x6e\x6e\x65\x6c\x20\x36\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x2d\x00\x06\x00\x00\x00\x00\x02\x00\x06\x40\x00\x00\x00\x00\x00\x00\x00"
+ b"\x02\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00"
+ b"\x00\x08\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38"
+ b"\x39\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x37\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x07\x00\x00\x00\x00\x02\x00\x07\x80"
+ b"\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x38"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2d\x00\x08\x3c"
+ b"\x01\x00\x00\x00\x00\x08\x00\x01\x00\x00\x00\x00\x00\x00\x12\x00\x00\xa2\x01\x00"
+ b"\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xa1\x07\x00\x01\x04\x03\x01\x01"
+ b"\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x0c\x00\x02\x0a\x04\x00\x00\x00\x00\x00\x00\x00\x8e\x00\x02\x0a\x00"
+ b"\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03\x00\x00\x08\x1c\x00\x00\x4f"
+ b"\x6e\x20\x62\x6f\x61\x72\x64\x20\x43\x41\x4e\x20\x31\x30\x35\x31\x63\x61\x70\x28"
+ b"\x48\x69\x67\x68\x73\x70\x65\x65\x64\x29\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4e\x38\x39\x31\x34\x20\x43\x68"
+ b"\x61\x6e\x6e\x65\x6c\x20\x39\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x2d\x00\x09\x80\x02\x00\x00\x00\x00\x09\x00\x02\x00\x00\x00\x00\x00"
+ b"\x00\x02\x00\x00\x00\x40\x00\x40\x00\x00\x00\x00\x00\x00\x40\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x0a\x03\x00\x00\x00\x00\x00"
+ b"\x00\x00\x8e\x00\x02\x0a\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe9\x03"
+ b"\x00\x00\x08\x1c\x00\x00\x44\x2f\x41\x20\x49\x4f\x70\x69\x67\x67\x79\x20\x38\x36"
+ b"\x34\x32\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x69"
+ b"\x72\x74\x75\x61\x6c\x20\x43\x68\x61\x6e\x6e\x65\x6c\x20\x31\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x16\x00\x00\x00\x00\x00\x0a"
+ b"\x00\x04\x00\x00\x00\x00\x00\x00\x07\x00\x00\xa0\x01\x00\x01\x00\x00\x00\x00\x00"
+ b"\x00\x01\x00\x00\x00\x20\xa1\x07\x00\x01\x04\x03\x01\x01\x00\x00\x00\x00\x00\x00"
+ b"\x00\x01\x00\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x1e"
+ b"\x14\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x69\x72\x74\x75\x61\x6c"
+ b"\x20\x43\x41\x4e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x56\x69\x72\x74\x75\x61\x6c\x20\x43\x68\x61\x6e\x6e\x65\x6c"
+ b"\x20\x32\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01"
+ b"\x16\x00\x00\x00\x00\x00\x0b\x00\x08\x00\x00\x00\x00\x00\x00\x07\x00\x00\xa0\x01"
+ b"\x00\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xa1\x07\x00\x01\x04\x03\x01"
+ b"\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x10\x00\x1e\x14\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x56\x69\x72\x74\x75\x61\x6c\x20\x43\x41\x4e\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x02" + 11832 * b"\x00"
+)
+
+
+def _get_predefined_xl_driver_config() -> xlclass.XLdriverConfig:
+ return xlclass.XLdriverConfig.from_buffer_copy(XL_DRIVER_CONFIG_EXAMPLE)
+
+
+# *****************************************************************************
+# Mock functions/side effects
+# *****************************************************************************
+
+
+def xlGetApplConfig(
+ app_name_p: ctypes.c_char_p,
+ app_channel: ctypes.c_uint,
+ hw_type: ctypes.POINTER(ctypes.c_uint),
+ hw_index: ctypes.POINTER(ctypes.c_uint),
+ hw_channel: ctypes.POINTER(ctypes.c_uint),
+ bus_type: ctypes.c_uint,
+) -> int:
+ hw_type.value = 1
+ hw_channel.value = 0
+ return 0
+
+
+def xlGetChannelIndex(
+ hw_type: ctypes.c_int, hw_index: ctypes.c_int, hw_channel: ctypes.c_int
+) -> int:
+ return hw_channel
+
+
+def xlOpenPort(
+ port_handle_p: ctypes.POINTER(xlclass.XLportHandle),
+ app_name_p: ctypes.c_char_p,
+ access_mask: int,
+ permission_mask: xlclass.XLaccess,
+ rx_queue_size: ctypes.c_uint,
+ xl_interface_version: ctypes.c_uint,
+ bus_type: ctypes.c_uint,
+) -> int:
+ port_handle_p.value = 0
+ permission_mask.value = access_mask
+ return 0
+
+
+def xlGetSyncTime(
+ port_handle: xlclass.XLportHandle, time_p: ctypes.POINTER(xlclass.XLuint64)
+) -> int:
+ time_p.value = 544219859027581
+ return 0
+
+
+def xlSetNotification(
+ port_handle: xlclass.XLportHandle,
+ event_handle: ctypes.POINTER(xlclass.XLhandle),
+ queue_level: ctypes.c_int,
+) -> int:
+ event_handle.value = 520
+ return 0
+
+
+def xlReceive(
+ port_handle: xlclass.XLportHandle,
+ event_count_p: ctypes.POINTER(ctypes.c_uint),
+ event: ctypes.POINTER(xlclass.XLevent),
+) -> int:
+ event.tag = xldefine.XL_EventTags.XL_RECEIVE_MSG.value
+ event.tagData.msg.id = 0x123
+ event.tagData.msg.dlc = 8
+ event.tagData.msg.flags = 0
+ event.timeStamp = 0
+ event.chanIndex = 0
+ for idx, value in enumerate([1, 2, 3, 4, 5, 6, 7, 8]):
+ event.tagData.msg.data[idx] = value
+ return 0
+
+
+def xlCanReceive(
+ port_handle: xlclass.XLportHandle, event: ctypes.POINTER(xlclass.XLcanRxEvent)
+) -> int:
+ event.tag = xldefine.XL_CANFD_RX_EventTags.XL_CAN_EV_TAG_RX_OK.value
+ event.tagData.canRxOkMsg.canId = 0x123
+ event.tagData.canRxOkMsg.dlc = 8
+ event.tagData.canRxOkMsg.msgFlags = 0
+ event.timeStamp = 0
+ event.chanIndex = 0
+ for idx, value in enumerate([1, 2, 3, 4, 5, 6, 7, 8]):
+ event.tagData.canRxOkMsg.data[idx] = value
+ return 0
+
+
+def xlReceive_chipstate(
+ port_handle: xlclass.XLportHandle,
+ event_count_p: ctypes.POINTER(ctypes.c_uint),
+ event: ctypes.POINTER(xlclass.XLevent),
+) -> int:
+ event.tag = xldefine.XL_EventTags.XL_CHIP_STATE.value
+ event.tagData.chipState.busStatus = 8
+ event.tagData.chipState.rxErrorCounter = 0
+ event.tagData.chipState.txErrorCounter = 0
+ event.timeStamp = 0
+ event.chanIndex = 2
+ return 0
+
+
+def xlCanReceive_chipstate(
+ port_handle: xlclass.XLportHandle, event: ctypes.POINTER(xlclass.XLcanRxEvent)
+) -> int:
+ event.tag = xldefine.XL_CANFD_RX_EventTags.XL_CAN_EV_TAG_CHIP_STATE.value
+ event.tagData.canChipState.busStatus = 8
+ event.tagData.canChipState.rxErrorCounter = 0
+ event.tagData.canChipState.txErrorCounter = 0
+ event.timeStamp = 0
+ event.chanIndex = 2
+ return 0
diff --git a/test/test_viewer.py b/test/test_viewer.py
index d978f12ed..e71d06dc8 100644
--- a/test/test_viewer.py
+++ b/test/test_viewer.py
@@ -1,7 +1,20 @@
-#!/usr/bin/python
-# coding: utf-8
+#!/usr/bin/env python
#
-# Copyright (C) 2018 Kristian Sloth Lauszus. All rights reserved.
+# Copyright (C) 2018 Kristian Sloth Lauszus.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 3 of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Contact information
# -------------------
@@ -9,37 +22,37 @@
# Web : http://www.lauszus.com
# e-mail : lauszus@gmail.com
-from __future__ import absolute_import
-
import argparse
-import can
-import curses
import math
-import pytest
+import os
import random
import struct
import time
import unittest
-import os
-import six
+from collections import defaultdict
+from test.config import IS_CI
+from unittest.mock import patch
-from typing import Dict, Tuple, Union
+import pytest
+import can
+from can.viewer import CanViewer, _parse_viewer_args
+
+# Allow the curses module to be missing (e.g. on PyPy on Windows)
try:
- # noinspection PyCompatibility
- from unittest.mock import Mock, patch
-except ImportError:
- # noinspection PyPackageRequirements
- from mock import Mock, patch
+ import curses
-from can.viewer import KEY_ESC, KEY_SPACE, CanViewer, parse_args
+ CURSES_AVAILABLE = True
+except ImportError:
+ curses = None # type: ignore
+ CURSES_AVAILABLE = False
# noinspection SpellCheckingInspection,PyUnusedLocal
class StdscrDummy:
-
def __init__(self):
self.key_counter = 0
+ self.draw_buffer = defaultdict(dict)
@staticmethod
def clear():
@@ -52,15 +65,20 @@ def erase():
@staticmethod
def getmaxyx():
# Set y-value, so scrolling gets tested
- return 1, 1
+ # Set x-value, so the text will fit in the window
+ return 1, 100
- @staticmethod
- def addstr(row, col, txt, *args):
+ def addstr(self, row, col, txt, *args):
assert row >= 0
assert col >= 0
assert txt is not None
+
+ # Save the text written into the buffer
+ for i, t in enumerate(txt):
+ self.draw_buffer[row][col + i] = t
+
# Raise an exception 50 % of the time, so we can make sure the code handles it
- if random.random() < .5:
+ if random.random() < 0.5:
raise curses.error
@staticmethod
@@ -68,19 +86,26 @@ def nodelay(_bool):
pass
def getch(self):
+ assert curses is not None
+
self.key_counter += 1
if self.key_counter == 1:
# Send invalid key
return -1
elif self.key_counter == 2:
- return ord('c') # Clear
+ return ord("c") # Clear
elif self.key_counter == 3:
- return KEY_SPACE # Pause
+ return curses.ascii.SP # Pause
elif self.key_counter == 4:
- return KEY_SPACE # Unpause
+ return curses.ascii.SP # Unpause
elif self.key_counter == 5:
- return ord('s') # Sort
-
+ return ord("s") # Sort
+ # Turn on byte highlighting (toggle)
+ elif self.key_counter == 6:
+ return ord("h")
+ # Turn off byte highlighting (toggle)
+ elif self.key_counter == 7:
+ return ord("h")
# Keep scrolling until it exceeds the number of messages
elif self.key_counter <= 100:
return curses.KEY_DOWN
@@ -88,49 +113,49 @@ def getch(self):
elif self.key_counter <= 200:
return curses.KEY_UP
- return KEY_ESC
+ return curses.ascii.ESC
+@unittest.skipUnless(CURSES_AVAILABLE, "curses might be missing on some platforms")
class CanViewerTest(unittest.TestCase):
-
@classmethod
def setUpClass(cls):
# Set seed, so the tests are not affected
random.seed(0)
def setUp(self):
- stdscr = StdscrDummy()
- config = {'interface': 'virtual', 'receive_own_messages': True}
+ self.stdscr_dummy = StdscrDummy()
+ config = {"interface": "virtual", "receive_own_messages": True}
bus = can.Bus(**config)
data_structs = None
- patch_curs_set = patch('curses.curs_set')
+ patch_curs_set = patch("curses.curs_set")
patch_curs_set.start()
self.addCleanup(patch_curs_set.stop)
- patch_use_default_colors = patch('curses.use_default_colors')
+ patch_use_default_colors = patch("curses.use_default_colors")
patch_use_default_colors.start()
self.addCleanup(patch_use_default_colors.stop)
- patch_init_pair = patch('curses.init_pair')
+ patch_init_pair = patch("curses.init_pair")
patch_init_pair.start()
self.addCleanup(patch_init_pair.stop)
- patch_color_pair = patch('curses.color_pair')
+ patch_color_pair = patch("curses.color_pair")
patch_color_pair.start()
self.addCleanup(patch_color_pair.stop)
- patch_is_term_resized = patch('curses.is_term_resized')
+ patch_is_term_resized = patch("curses.is_term_resized")
mock_is_term_resized = patch_is_term_resized.start()
- mock_is_term_resized.return_value = True if random.random() < .5 else False
+ mock_is_term_resized.return_value = True if random.random() < 0.5 else False
self.addCleanup(patch_is_term_resized.stop)
- if hasattr(curses, 'resizeterm'):
- patch_resizeterm = patch('curses.resizeterm')
+ if hasattr(curses, "resizeterm"):
+ patch_resizeterm = patch("curses.resizeterm")
patch_resizeterm.start()
self.addCleanup(patch_resizeterm.stop)
- self.can_viewer = CanViewer(stdscr, bus, data_structs, testing=True)
+ self.can_viewer = CanViewer(self.stdscr_dummy, bus, data_structs, testing=True)
def tearDown(self):
# Run the viewer after the test, this is done, so we can receive the CAN-Bus messages and make sure that they
@@ -140,36 +165,46 @@ def tearDown(self):
def test_send(self):
# CANopen EMCY
data = [1, 2, 3, 4, 5, 6, 7] # Wrong length
- msg = can.Message(arbitration_id=0x080 + 1, data=data, extended_id=False)
+ msg = can.Message(arbitration_id=0x080 + 1, data=data, is_extended_id=False)
self.can_viewer.bus.send(msg)
data = [1, 2, 3, 4, 5, 6, 7, 8]
- msg = can.Message(arbitration_id=0x080 + 1, data=data, extended_id=False)
+ msg = can.Message(arbitration_id=0x080 + 1, data=data, is_extended_id=False)
self.can_viewer.bus.send(msg)
# CANopen HEARTBEAT
data = [0x05] # Operational
- msg = can.Message(arbitration_id=0x700 + 0x7F, data=data, extended_id=False)
+ msg = can.Message(arbitration_id=0x700 + 0x7F, data=data, is_extended_id=False)
self.can_viewer.bus.send(msg)
# Send non-CANopen message
data = [1, 2, 3, 4, 5, 6, 7, 8]
- msg = can.Message(arbitration_id=0x101, data=data, extended_id=False)
+ msg = can.Message(arbitration_id=0x101, data=data, is_extended_id=False)
self.can_viewer.bus.send(msg)
# Send the same command, but with another data length
data = [1, 2, 3, 4, 5, 6]
- msg = can.Message(arbitration_id=0x101, data=data, extended_id=False)
+ msg = can.Message(arbitration_id=0x101, data=data, is_extended_id=False)
+ self.can_viewer.bus.send(msg)
+
+ # Send non-CANopen message with long parsed data length
+ data = [255, 255]
+ msg = can.Message(arbitration_id=0x102, data=data, is_extended_id=False)
+ self.can_viewer.bus.send(msg)
+
+ # Send the same command, but with shorter parsed data length
+ data = [0, 0]
+ msg = can.Message(arbitration_id=0x102, data=data, is_extended_id=False)
self.can_viewer.bus.send(msg)
# Message with extended id
data = [1, 2, 3, 4, 5, 6, 7, 8]
- msg = can.Message(arbitration_id=0x123456, data=data, extended_id=True)
+ msg = can.Message(arbitration_id=0x123456, data=data, is_extended_id=True)
self.can_viewer.bus.send(msg)
# self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None))
# Send the same message again to make sure that resending works and dt is correct
- time.sleep(.1)
+ time.sleep(0.1)
self.can_viewer.bus.send(msg)
# Send error message
@@ -182,41 +217,59 @@ def test_receive(self):
data_structs = {
# For converting the EMCY and HEARTBEAT messages
- 0x080 + 0x01: struct.Struct('ff'),
+ 0x123456: struct.Struct(">ff"),
}
# Receive the messages we just sent in 'test_canopen'
while 1:
msg = self.can_viewer.bus.recv(timeout=0)
if msg is not None:
- self.can_viewer.data_structs = data_structs if msg.arbitration_id != 0x101 else None
+ self.can_viewer.data_structs = (
+ data_structs if msg.arbitration_id != 0x101 else None
+ )
_id = self.can_viewer.draw_can_bus_message(msg)
- if _id['msg'].arbitration_id == 0x101:
+ if _id["msg"].arbitration_id == 0x101:
# Check if the counter is reset when the length has changed
- self.assertEqual(_id['count'], 1)
- elif _id['msg'].arbitration_id == 0x123456:
+ self.assertEqual(_id["count"], 1)
+
+ # Make sure the line has been cleared after the shorted message was send
+ for col, v in self.stdscr_dummy.draw_buffer[_id["row"]].items():
+ if col >= 52 + _id["msg"].dlc * 3:
+ self.assertEqual(v, " ")
+ elif _id["msg"].arbitration_id == 0x102:
+ # Make sure the parsed values have been cleared after the shorted message was send
+ for col, v in self.stdscr_dummy.draw_buffer[_id["row"]].items():
+ if col >= 77 + _id["values_string_length"]:
+ self.assertEqual(v, " ")
+ elif _id["msg"].arbitration_id == 0x123456:
# Check if the counter is incremented
- if _id['dt'] == 0:
- self.assertEqual(_id['count'], 1)
+ if _id["dt"] == 0:
+ self.assertEqual(_id["count"], 1)
else:
- self.assertTrue(pytest.approx(_id['dt'], 0.1)) # dt should be ~0.1 s
- self.assertEqual(_id['count'], 2)
+ if not IS_CI: # do not test timing in CI
+ assert _id["dt"] == pytest.approx(
+ 0.1, abs=5e-2
+ ) # dt should be ~0.1 s
+ self.assertEqual(_id["count"], 2)
else:
# Make sure dt is 0
- if _id['count'] == 1:
- self.assertEqual(_id['dt'], 0)
+ if _id["count"] == 1:
+ self.assertEqual(_id["dt"], 0)
else:
break
# Convert it into raw integer values and then pack the data
@staticmethod
- def pack_data(cmd, cmd_to_struct, *args): # type: (int, Dict, Union[*float, *int]) -> bytes
+ def pack_data(
+ cmd, cmd_to_struct, *args
+ ): # type: (int, Dict, Union[*float, *int]) -> bytes
if not cmd_to_struct or len(args) == 0:
# If no arguments are given, then the message does not contain a data package
- return b''
+ return b""
for key in cmd_to_struct.keys():
if cmd == key if isinstance(key, int) else cmd in key:
@@ -227,17 +280,17 @@ def pack_data(cmd, cmd_to_struct, *args): # type: (int, Dict, Union[*float, *in
# The conversion from SI-units to raw values are given in the rest of the tuple
fmt = struct_t.format
- if isinstance(fmt, six.string_types): # pragma: no cover
+ if isinstance(fmt, str): # pragma: no cover
# Needed for Python 3.7
- fmt = six.b(fmt)
+ fmt = fmt.encode()
# Make sure the endian is given as the first argument
- assert six.byte2int(fmt) == ord('<') or six.byte2int(fmt) == ord('>')
+ assert fmt[0] == ord("<") or fmt[0] == ord(">")
# Disable rounding if the format is a float
data = []
- for c, arg, val in zip(six.iterbytes(fmt[1:]), args, value[1:]):
- if c == ord('f'):
+ for c, arg, val in zip(fmt[1:], args, value[1:]):
+ if c == ord("f"):
data.append(arg * val)
else:
data.append(round(arg * val))
@@ -248,7 +301,7 @@ def pack_data(cmd, cmd_to_struct, *args): # type: (int, Dict, Union[*float, *in
return struct_t.pack(*data)
else:
- raise ValueError('Unknown command: 0x{:02X}'.format(cmd))
+ raise ValueError(f"Unknown command: 0x{cmd:02X}")
def test_pack_unpack(self):
CANOPEN_TPDO1 = 0x180
@@ -276,34 +329,41 @@ def test_pack_unpack(self):
# are divided by the value in order to convert from real units to raw integer values.
data_structs = {
# CANopen node 1
- CANOPEN_TPDO1 + 1: struct.Struct('lL'),
- (CANOPEN_TPDO3 + 2, CANOPEN_TPDO4 + 2): struct.Struct('>LL'),
+ CANOPEN_TPDO2 + 2: struct.Struct(">lL"),
+ (CANOPEN_TPDO3 + 2, CANOPEN_TPDO4 + 2): struct.Struct(">LL"),
} # type: Dict[Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None]]
- raw_data = self.pack_data(CANOPEN_TPDO1 + 1, data_structs, -7, 13, -1024, 2048, 0xFFFF)
+ raw_data = self.pack_data(
+ CANOPEN_TPDO1 + 1, data_structs, -7, 13, -1024, 2048, 0xFFFF
+ )
parsed_data = CanViewer.unpack_data(CANOPEN_TPDO1 + 1, data_structs, raw_data)
self.assertListEqual(parsed_data, [-7, 13, -1024, 2048, 0xFFFF])
self.assertTrue(all(isinstance(d, int) for d in parsed_data))
raw_data = self.pack_data(CANOPEN_TPDO2 + 1, data_structs, 12.34, 4.5, 6)
parsed_data = CanViewer.unpack_data(CANOPEN_TPDO2 + 1, data_structs, raw_data)
- self.assertTrue(pytest.approx(parsed_data, [12.34, 4.5, 6]))
- self.assertTrue(isinstance(parsed_data[0], float) and isinstance(parsed_data[1], float) and
- isinstance(parsed_data[2], int))
+ assert parsed_data == pytest.approx([12.34, 4.5, 6])
+ self.assertTrue(
+ isinstance(parsed_data[0], float)
+ and isinstance(parsed_data[1], float)
+ and isinstance(parsed_data[2], int)
+ )
raw_data = self.pack_data(CANOPEN_TPDO3 + 1, data_structs, 123.45, 67.89)
parsed_data = CanViewer.unpack_data(CANOPEN_TPDO3 + 1, data_structs, raw_data)
- self.assertTrue(pytest.approx(parsed_data, [123.45, 67.89]))
+ assert parsed_data == pytest.approx([123.45, 67.89])
self.assertTrue(all(isinstance(d, float) for d in parsed_data))
- raw_data = self.pack_data(CANOPEN_TPDO4 + 1, data_structs, math.pi / 2., math.pi)
+ raw_data = self.pack_data(
+ CANOPEN_TPDO4 + 1, data_structs, math.pi / 2.0, math.pi
+ )
parsed_data = CanViewer.unpack_data(CANOPEN_TPDO4 + 1, data_structs, raw_data)
- self.assertTrue(pytest.approx(parsed_data, [math.pi / 2., math.pi]))
+ assert parsed_data == pytest.approx([math.pi / 2.0, math.pi])
self.assertTrue(all(isinstance(d, float) for d in parsed_data))
raw_data = self.pack_data(CANOPEN_TPDO1 + 2, data_structs)
@@ -311,7 +371,9 @@ def test_pack_unpack(self):
self.assertListEqual(parsed_data, [])
self.assertIsInstance(parsed_data, list)
- raw_data = self.pack_data(CANOPEN_TPDO2 + 2, data_structs, -2147483648, 0xFFFFFFFF)
+ raw_data = self.pack_data(
+ CANOPEN_TPDO2 + 2, data_structs, -2147483648, 0xFFFFFFFF
+ )
parsed_data = CanViewer.unpack_data(CANOPEN_TPDO2 + 2, data_structs, raw_data)
self.assertListEqual(parsed_data, [-2147483648, 0xFFFFFFFF])
@@ -323,59 +385,63 @@ def test_pack_unpack(self):
parsed_data = CanViewer.unpack_data(CANOPEN_TPDO4 + 2, data_structs, raw_data)
self.assertListEqual(parsed_data, [0xFFFFFF, 0xFFFFFFFF])
- # See: http://python-future.org/compatible_idioms.html#long-integers
- from past.builtins import long
- self.assertTrue(all(isinstance(d, (int, long)) for d in parsed_data))
+ self.assertTrue(all(isinstance(d, int) for d in parsed_data))
# Make sure that the ValueError exception is raised
with self.assertRaises(ValueError):
self.pack_data(0x101, data_structs, 1, 2, 3, 4)
with self.assertRaises(ValueError):
- CanViewer.unpack_data(0x102, data_structs, b'\x01\x02\x03\x04\x05\x06\x07\x08')
+ CanViewer.unpack_data(
+ 0x102, data_structs, b"\x01\x02\x03\x04\x05\x06\x07\x08"
+ )
def test_parse_args(self):
- parsed_args, _, _ = parse_args(['-b', '250000'])
+ parsed_args, _ = _parse_viewer_args(["-b", "250000"])
self.assertEqual(parsed_args.bitrate, 250000)
- parsed_args, _, _ = parse_args(['--bitrate', '500000'])
+ parsed_args, _ = _parse_viewer_args(["--bitrate", "500000"])
self.assertEqual(parsed_args.bitrate, 500000)
- parsed_args, _, _ = parse_args(['-c', 'can0'])
- self.assertEqual(parsed_args.channel, 'can0')
+ parsed_args, _ = _parse_viewer_args(["-c", "can0"])
+ self.assertEqual(parsed_args.channel, "can0")
- parsed_args, _, _ = parse_args(['--channel', 'PCAN_USBBUS1'])
- self.assertEqual(parsed_args.channel, 'PCAN_USBBUS1')
+ parsed_args, _ = _parse_viewer_args(["--channel", "PCAN_USBBUS1"])
+ self.assertEqual(parsed_args.channel, "PCAN_USBBUS1")
- parsed_args, _, data_structs = parse_args(['-d', '100: