diff -pruN 4.5.0-1/CHANGELOG.md 4.6.1-1/CHANGELOG.md
--- 4.5.0-1/CHANGELOG.md	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/CHANGELOG.md	2025-08-12 07:32:01.000000000 +0000
@@ -1,8 +1,67 @@
-Version 4.5.0
-=============
+# Changelog
 
-Features
---------
+<!--
+All notable changes to this project will be documented in this file.
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the changes for the upcoming release can be found in <https://github.com/hardbyte/python-can/tree/main/doc/changelog.d/>.
+-->
+
+<!-- towncrier release notes start -->
+
+## 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
@@ -13,8 +72,7 @@ Features
 * 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
----------
+### 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
@@ -25,8 +83,7 @@ Bug Fixes
 * Resolve AttributeError within NicanError by @vijaysubbiah20 in https://github.com/hardbyte/python-can/pull/1806
 
 
-Miscellaneous
--------------
+### 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
@@ -43,11 +100,10 @@ Miscellaneous
 * Add zlgcan to docs by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1839
 
 
-Version 4.4.2
-=============
+## Version 4.4.2
+
+### Bug Fixes
 
-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)
@@ -56,8 +112,7 @@ Bug Fixes
 * socketcan: Do not log exception on non-linux platforms (#1800)
 * vector, kvaser: Activate channels after CAN filters were applied (#1413, #1708, #1796)
 
-Features
---------
+### Features
 
 * kvaser: Add support for non-ISO CAN FD (#1752)
 * neovi: Return timestamps relative to epoch (#1789)
@@ -66,11 +121,9 @@ Features
 * vector: Add support for `listen_only` mode (#1764)
 
 
-Version 4.4.0
-=============
+## Version 4.4.0
 
-Features
---------
+### 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).
@@ -80,17 +133,14 @@ Features
 * 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
----------
+### 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
--------------
+### Miscellaneous
 
 * Invert default value logic for BusABC._is_shutdown. (#1774)
 * Implemented various logging enhancements to provide more detailed and useful operational insights (#1703).
@@ -100,29 +150,27 @@ Miscellaneous
 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
-=============
+## Version 4.3.1
+
+### Bug Fixes
 
-Bug Fixes
----------
 * Fix socketcand erroneously discarding frames (#1700)
 * Fix initialization order in EtasBus (#1693, #1704)
 
-Documentation
--------------
+### Documentation
+
 * Fix install instructions for neovi (#1694, #1697)
 
 
-Version 4.3.0
-=============
+## Version 4.3.0
+
+### Breaking Changes
 
-Breaking Changes
-----------------
 * Raise Minimum Python Version to 3.8 (#1597)
 * Do not stop notifier if exception was handled (#1645)
 
-Bug Fixes
----------
+### 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)
@@ -133,20 +181,22 @@ Bug Fixes
 * 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
---------
+### Features
+
+#### API
 
-### 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
+#### IO
+
 * Can Player compatibility with interfaces that use additional configuration (#1610)
 
-### Interface Improvements
+#### 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)
@@ -156,7 +206,8 @@ Features
 * Kvaser: add parameter exclusive and `override_exclusive` (#1660)
 * socketcand: Add parameter `tcp_tune` to reduce latency (#1683)
 
-### Miscellaneous
+#### Miscellaneous
+
 * Distinguish Text/Binary-IO for Reader/Writer classes. (#1585)
 * Convert setup.py to pyproject.toml (#1592)
 * activate ruff pycodestyle checks (#1602)
@@ -170,32 +221,29 @@ Features
 * Add Python 3.12 Support / Test Python 3.12 (#1673)
 
 
-Version 4.2.2
-=============
+## Version 4.2.2
+
+### Bug Fixes
 
-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
-=============
+## Version 4.2.1
+
+### Bug Fixes
 
-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
-=============
+## Version 4.2.0
+
+### Breaking Changes
 
-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 
@@ -210,17 +258,16 @@ Breaking Changes
   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
---------
+### Features
 
-### IO
+#### IO
 * Add support for MF4 files (#1289).
 * Add support for version 2 TRC files and other TRC file enhancements (#1530).
 
-### Type Annotations
+#### Type Annotations
 * Export symbols to satisfy type checkers (#1547, #1551, #1558, #1568).
 
-### Interface Improvements
+#### 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).
@@ -232,21 +279,21 @@ Features
 * vector: Only check sample point instead of tseg & sjw (#1486).
 * vector: add VN5611 hwtype (#1501).
 
-Documentation
--------------
+### Documentation
+
 * Add new section about related tools to documentation. Add a list of
   plugin interface packages (#1457).
 
-Bug Fixes
----------
+### 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
-------------
+### 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 
@@ -258,8 +305,8 @@ Deprecations
 * The ``brs`` and ``log_errors`` parameters of `` NiXNETcanBus`` are deprecated 
   and will be removed in version 5.0. (#1520).
 
-Miscellaneous
--------------
+### Miscellaneous
+
 * Use high resolution timer on Windows to improve 
   timing precision for BroadcastManager (#1449).
 * Improve ThreadBasedCyclicSendTask timing (#1539).
@@ -271,11 +318,9 @@ Miscellaneous
 * Add deprecation period to utility function ``deprecated_args_alias`` (#1477).
 * Add `ruff` to the CI system (#1551)
 
-Version 4.1.0
-=============
+## Version 4.1.0
 
-Breaking Changes
-----------------
+### Breaking Changes
 
 * ``windows-curses`` was moved to optional dependencies (#1395). 
   Use ``pip install python-can[viewer]`` if you are using the ``can.viewer`` 
@@ -284,11 +329,9 @@ Breaking Changes
   from camelCase to snake_case (#1422).
 
 
-Features
---------
-
-### IO
+### Features
 
+#### IO
 * The canutils logger preserves message direction (#1244) 
   and uses common interface names (e.g. can0) instead of just 
   channel numbers (#1271).
@@ -299,11 +342,11 @@ Features
   and player initialisation (#1366).
 * Initial support for TRC files (#1217)
 
-### Type Annotations
+#### Type Annotations
 * python-can now includes the ``py.typed`` marker to support type checking 
   according to PEP 561 (#1344).
 
-### Interface Improvements
+#### 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).
@@ -317,8 +360,7 @@ Features
   be applied according to the arguments of ``VectorBus.__init__`` (#1426).
 * Ixxat bus now implements BusState api and detects errors (#1141)
 
-Bug Fixes
----------
+### Bug Fixes
 
 * Improve robustness of USB2CAN serial number detection (#1129).
 * Fix channel2int conversion (#1268, #1269).
@@ -340,8 +382,7 @@ Bug Fixes
 * Raise ValueError if gzip is used with incompatible log formats (#1429).
 * Allow restarting of transmission tasks for socketcan (#1440)
 
-Miscellaneous
--------------
+### Miscellaneous
 
 * Allow ICSApiError to be pickled and un-pickled (#1341)
 * Sort interface names in CLI API to make documentation reproducible (#1342)
@@ -351,8 +392,7 @@ Miscellaneous
 * 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
-====
+## Version 4.0.0
 
 TL;DR: This release includes a ton of improvements from 2.5 years of development! 🎉 Test thoroughly after switching.
 
@@ -366,8 +406,7 @@ Therefore, users are strongly advised to
 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
---------------
+### 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)
@@ -375,8 +414,7 @@ Major features
 * [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
---------------
+### New interfaces
 
 * udp_multicast (#644)
 * robotell (#731)
@@ -387,8 +425,7 @@ New interfaces
 * socketcand (#1140)
 * etas (#1144)
 
-Improved interfaces
--------------------
+### Improved interfaces
 
 * socketcan
   * Support for multiple Cyclic Messages in Tasks (#610)
@@ -465,8 +502,7 @@ Improved interfaces
   * Fix transmitting onto a busy bus (#1114)
   * Replace binary library with python driver (#726, #1127)
 
-Other API changes and improvements
-----------------------------------
+### Other API changes and improvements
 
 * CAN FD frame support is pretty complete (#963)
   * ASCWriter (#604) and ASCReader (#741)
@@ -497,8 +533,7 @@ Other API changes and improvements
 * Add changed byte highlighting to viewer.py (#1159)
 * Change DLC to DL in Message.\_\_str\_\_() (#1212)
 
-Other Bugfixes
---------------
+### Other Bugfixes
 
 * BLF PDU padding (#459)
 * stop_all_periodic_tasks skipping every other task (#634, #637, #645)
@@ -524,8 +559,7 @@ Other Bugfixes
 * 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
--------------------------------------
+### 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
@@ -543,16 +577,13 @@ Behind the scenes & Quality assurance
   * [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
-====
+## 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
-====
-
+## Version 3.3.3
 Backported fixes from 4.x development branch which targets Python 3.
 
 * #798 Backport caching msg.data value in neovi interface.
@@ -570,37 +601,30 @@ Backported fixes from 4.x development br
 * #605 Socketcan BCM status fix.
 
 
-Version 3.3.2
-====
+## Version 3.3.2
 
 Minor bug fix release addressing issue in PCAN RTR.
 
-Version 3.3.1
-====
+## Version 3.3.1
 
 Minor fix to setup.py to only require pytest-runner when necessary.
 
-Version 3.3.0
-====
+## 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
-====
+## 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
-====
-
+## Version 3.2.0
 
-Major features
---------------
+### Major features
 
 * FD support added for Pcan by @bmeisels with input from
   @markuspi, @christiansandberg & @felixdivo in PR #537
@@ -608,8 +632,7 @@ Major features
   and Python 3.5. Support has been removed for Python 3.4 in this
   release in PR #532
 
-Other notable changes
----------------------
+### Other notable changes
 
 * #533 BusState is now an enum.
 * #535 This release should automatically be published to PyPi by travis.
@@ -624,26 +647,21 @@ https://github.com/hardbyte/python-can/m
 
 Pulls: #522, #526, #527, #536, #540, #546, #547, #548, #533, #559, #569, #571, #572, #575
 
-Backend Specific Changes
-------------------------
+### Backend Specific Changes
 
-pcan
-~~~~
+#### pcan
 
 * FD
 
-slcan
-~~~~
+#### slcan
 
 * ability to set custom can speed instead of using predefined speed values. #553
 
-socketcan
-~~~~
+#### socketcan
 
 * Bug fix to properly support 32bit systems. #573
 
-usb2can
-~~~~
+#### usb2can
 
 * slightly better error handling
 * multiple serial devices can be found
@@ -651,25 +669,20 @@ usb2can
 
 Pulls #511, #535
 
-vector
-~~~~
+#### vector
 
 * handle `app_name`. #525
 
-Version 3.1.1
-====
+## Version 3.1.1
 
-Major features
---------------
+### 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
----------------------
+### Other notable changes
 
 * #477 The kvaser interface now supports bus statistics via a custom bus method.
 * #434 neovi now supports receiving own messages
@@ -686,18 +699,15 @@ Other notable changes
 * #455 Fix to `Message` initializer
 * Small bugfixes and improvements
 
-Version 3.1.0
-====
+## 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
-====
+## Version 3.0.0
 
-Major features
---------------
+### 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
@@ -710,8 +720,7 @@ Major features
   by calling the bus's new `stop_all_periodic_tasks` method. #412
 
 
-Breaking changes
-----------------
+### 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
@@ -721,8 +730,7 @@ Breaking changes
   read/writer constructors from `filename` to `file`.
 
 
-Other notable changes
----------------------
+### Other notable changes
 
 * can.Message class updated #413
     - Addition of a `Message.equals` method.
@@ -754,59 +762,48 @@ Other notable changes
 
 General fixes, cleanup and docs changes: (#347, #348, #367, #368, #370, #371, #373, #420, #417, #419, #432)
 
-Backend Specific Changes
-------------------------
+### Backend Specific Changes
 
-3rd party interfaces
-~~~~~~~~~~~~~~~~~~~~
+#### 3rd party interfaces
 
 * Deprecated `python_can.interface` entry point instead use `can.interface`. #389
 
-neovi
-~~~~~
+#### neovi
 
 * Added support for CAN-FD #408
 * Fix issues checking if bus is open. #381
 * Adding multiple channels support. #415
 
-nican
-~~~~~
+#### nican
 
 * implements reset instead of custom `flush_tx_buffer`. #364
 
-pcan
-~~~~
+#### pcan
 
 * now supported on OSX. #365
 
-
-serial
-~~~~~~
+#### 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
 
 * socketcan tasks now reuse a bcm socket #404, #425, #426,
 * socketcan bugfix to receive error frames #384
 
-vector
-~~~~~~
+#### 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)
-=====
+## 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)
-=====
+## 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`),
@@ -817,8 +814,7 @@ Version 2.2.0 (2018-06-30)
 * 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)
-=====
+## 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.
@@ -830,8 +826,7 @@ Version 2.1.0 (2018-02-17)
 * Other misc improvements and bug fixes
 
 
-Version 2.0.0 (2018-01-05
-=====
+## Version 2.0.0 (2018-01-05)
 
 After an extended baking period we have finally tagged version 2.0.0!
 
diff -pruN 4.5.0-1/CONTRIBUTING.md 4.6.1-1/CONTRIBUTING.md
--- 4.5.0-1/CONTRIBUTING.md	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/CONTRIBUTING.md	2025-08-12 07:32:01.000000000 +0000
@@ -1 +1 @@
-Please read the [Development - Contributing](https://python-can.readthedocs.io/en/stable/development.html#contributing) guidelines in the documentation site.
+Please read the [Development - Contributing](https://python-can.readthedocs.io/en/main/development.html#contributing) guidelines in the documentation site.
diff -pruN 4.5.0-1/README.rst 4.6.1-1/README.rst
--- 4.5.0-1/README.rst	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/README.rst	2025-08-12 07:32:01.000000000 +0000
@@ -3,7 +3,7 @@ python-can
 
 |pypi| |conda| |python_implementation| |downloads| |downloads_monthly|
 
-|docs| |github-actions| |coverage| |mergify| |formatter|
+|docs| |github-actions| |coverage| |formatter|
 
 .. |pypi| image:: https://img.shields.io/pypi/v/python-can.svg
    :target: https://pypi.python.org/pypi/python-can/
@@ -37,14 +37,10 @@ python-can
    :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=develop
-   :target: https://coveralls.io/github/hardbyte/python-can?branch=develop
+.. |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
 
-.. |mergify| image:: https://img.shields.io/endpoint.svg?url=https://api.mergify.com/v1/badges/hardbyte/python-can&style=flat
-   :target: https://mergify.io
-   :alt: Mergify Status
-
 The **C**\ ontroller **A**\ rea **N**\ etwork is a bus standard designed
 to allow microcontrollers and devices to communicate with each other. It
 has priority based bus arbitration and reliable deterministic
@@ -64,6 +60,7 @@ Library Version                 Python
   3.x                           2.7+, 3.5+
   4.0+                          3.7+
   4.3+                          3.8+
+  4.6+                          3.9+
 ==============================  ===========
 
 
diff -pruN 4.5.0-1/can/__init__.py 4.6.1-1/can/__init__.py
--- 4.5.0-1/can/__init__.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/__init__.py	2025-08-12 07:32:01.000000000 +0000
@@ -8,20 +8,23 @@ messages on a can bus.
 import contextlib
 import logging
 from importlib.metadata import PackageNotFoundError, version
-from typing import Any, Dict
+from typing import Any
 
 __all__ = [
+    "VALID_INTERFACES",
     "ASCReader",
     "ASCWriter",
     "AsyncBufferedReader",
-    "BitTiming",
-    "BitTimingFd",
     "BLFReader",
     "BLFWriter",
+    "BitTiming",
+    "BitTimingFd",
     "BufferedReader",
     "Bus",
     "BusABC",
     "BusState",
+    "CSVReader",
+    "CSVWriter",
     "CanError",
     "CanInitializationError",
     "CanInterfaceNotImplementedError",
@@ -30,18 +33,16 @@ __all__ = [
     "CanTimeoutError",
     "CanutilsLogReader",
     "CanutilsLogWriter",
-    "CSVReader",
-    "CSVWriter",
     "CyclicSendTaskABC",
     "LimitedDurationCyclicSendTaskABC",
     "Listener",
-    "Logger",
     "LogReader",
-    "ModifiableCyclicTaskABC",
-    "Message",
-    "MessageSync",
+    "Logger",
     "MF4Reader",
     "MF4Writer",
+    "Message",
+    "MessageSync",
+    "ModifiableCyclicTaskABC",
     "Notifier",
     "Printer",
     "RedirectReader",
@@ -49,11 +50,10 @@ __all__ = [
     "SizedRotatingLogger",
     "SqliteReader",
     "SqliteWriter",
-    "ThreadSafeBus",
     "TRCFileVersion",
     "TRCReader",
     "TRCWriter",
-    "VALID_INTERFACES",
+    "ThreadSafeBus",
     "bit_timing",
     "broadcastmanager",
     "bus",
@@ -64,8 +64,8 @@ __all__ = [
     "interfaces",
     "io",
     "listener",
-    "logconvert",
     "log",
+    "logconvert",
     "logger",
     "message",
     "notifier",
@@ -130,4 +130,4 @@ with contextlib.suppress(PackageNotFound
 
 log = logging.getLogger("can")
 
-rc: Dict[str, Any] = {}
+rc: dict[str, Any] = {}
diff -pruN 4.5.0-1/can/_entry_points.py 4.6.1-1/can/_entry_points.py
--- 4.5.0-1/can/_entry_points.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/_entry_points.py	2025-08-12 07:32:01.000000000 +0000
@@ -2,7 +2,7 @@ import importlib
 import sys
 from dataclasses import dataclass
 from importlib.metadata import entry_points
-from typing import Any, List
+from typing import Any
 
 
 @dataclass
@@ -20,15 +20,15 @@ class _EntryPoint:
 # "Compatibility Note".
 if sys.version_info >= (3, 10):
 
-    def read_entry_points(group: str) -> List[_EntryPoint]:
+    def read_entry_points(group: str) -> list[_EntryPoint]:
         return [
             _EntryPoint(ep.name, ep.module, ep.attr) for ep in entry_points(group=group)
         ]
 
 else:
 
-    def read_entry_points(group: str) -> List[_EntryPoint]:
+    def read_entry_points(group: str) -> list[_EntryPoint]:
         return [
             _EntryPoint(ep.name, *ep.value.split(":", maxsplit=1))
-            for ep in entry_points().get(group, [])
+            for ep in entry_points().get(group, [])  # pylint: disable=no-member
         ]
diff -pruN 4.5.0-1/can/bit_timing.py 4.6.1-1/can/bit_timing.py
--- 4.5.0-1/can/bit_timing.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/bit_timing.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,12 +1,13 @@
 # pylint: disable=too-many-lines
 import math
-from typing import TYPE_CHECKING, Iterator, List, Mapping, cast
+from collections.abc import Iterator, Mapping
+from typing import TYPE_CHECKING, cast
 
 if TYPE_CHECKING:
     from can.typechecking import BitTimingDict, BitTimingFdDict
 
 
-class BitTiming(Mapping):
+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
@@ -155,7 +156,7 @@ class BitTiming(Mapping):
             if the arguments are invalid.
         """
         try:
-            brp = int(round(f_clock / (bitrate * (1 + tseg1 + tseg2))))
+            brp = round(f_clock / (bitrate * (1 + tseg1 + tseg2)))
         except ZeroDivisionError:
             raise ValueError("Invalid inputs") from None
 
@@ -232,7 +233,7 @@ class BitTiming(Mapping):
             raise ValueError(f"sample_point (={sample_point}) must not be below 50%.")
 
         for brp in range(1, 65):
-            nbt = round(int(f_clock / (bitrate * brp)))
+            nbt = int(f_clock / (bitrate * brp))
             if nbt < 8:
                 break
 
@@ -240,7 +241,7 @@ class BitTiming(Mapping):
             if abs(effective_bitrate - bitrate) > bitrate / 256:
                 continue
 
-            tseg1 = int(round(sample_point / 100 * nbt)) - 1
+            tseg1 = round(sample_point / 100 * nbt) - 1
             # limit tseg1, so tseg2 is at least 1 TQ
             tseg1 = min(tseg1, nbt - 2)
 
@@ -286,7 +287,7 @@ class BitTiming(Mapping):
         if sample_point < 50.0:
             raise ValueError(f"sample_point (={sample_point}) must not be below 50%.")
 
-        possible_solutions: List[BitTiming] = list(
+        possible_solutions: list[BitTiming] = list(
             cls.iterate_from_sample_point(f_clock, bitrate, sample_point)
         )
 
@@ -312,7 +313,7 @@ class BitTiming(Mapping):
     @property
     def bitrate(self) -> int:
         """Bitrate in bits/s."""
-        return int(round(self.f_clock / (self.nbt * self.brp)))
+        return round(self.f_clock / (self.nbt * self.brp))
 
     @property
     def brp(self) -> int:
@@ -322,7 +323,7 @@ class BitTiming(Mapping):
     @property
     def tq(self) -> int:
         """Time quantum in nanoseconds"""
-        return int(round(self.brp / self.f_clock * 1e9))
+        return round(self.brp / self.f_clock * 1e9)
 
     @property
     def nbt(self) -> int:
@@ -433,7 +434,7 @@ class BitTiming(Mapping):
                 "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 = int(round(self.sjw / self.nbt * bt.nbt))
+        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
@@ -458,7 +459,7 @@ class BitTiming(Mapping):
         return f"can.{self.__class__.__name__}({args})"
 
     def __getitem__(self, key: str) -> int:
-        return cast(int, self._data.__getitem__(key))
+        return cast("int", self._data.__getitem__(key))
 
     def __len__(self) -> int:
         return self._data.__len__()
@@ -476,7 +477,7 @@ class BitTiming(Mapping):
         return tuple(self._data.values()).__hash__()
 
 
-class BitTimingFd(Mapping):
+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
@@ -716,10 +717,8 @@ class BitTimingFd(Mapping):
             if the arguments are invalid.
         """
         try:
-            nom_brp = int(round(f_clock / (nom_bitrate * (1 + nom_tseg1 + nom_tseg2))))
-            data_brp = int(
-                round(f_clock / (data_bitrate * (1 + data_tseg1 + data_tseg2)))
-            )
+            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
 
@@ -787,7 +786,7 @@ class BitTimingFd(Mapping):
         sync_seg = 1
 
         for nom_brp in range(1, 257):
-            nbt = round(int(f_clock / (nom_bitrate * nom_brp)))
+            nbt = int(f_clock / (nom_bitrate * nom_brp))
             if nbt < 1:
                 break
 
@@ -795,7 +794,7 @@ class BitTimingFd(Mapping):
             if abs(effective_nom_bitrate - nom_bitrate) > nom_bitrate / 256:
                 continue
 
-            nom_tseg1 = int(round(nom_sample_point / 100 * nbt)) - 1
+            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
@@ -811,7 +810,7 @@ class BitTimingFd(Mapping):
                 if abs(effective_data_bitrate - data_bitrate) > data_bitrate / 256:
                     continue
 
-                data_tseg1 = int(round(data_sample_point / 100 * dbt)) - 1
+                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
@@ -876,7 +875,7 @@ class BitTimingFd(Mapping):
                 f"data_sample_point (={data_sample_point}) must not be below 50%."
             )
 
-        possible_solutions: List[BitTimingFd] = list(
+        possible_solutions: list[BitTimingFd] = list(
             cls.iterate_from_sample_point(
                 f_clock,
                 nom_bitrate,
@@ -923,7 +922,7 @@ class BitTimingFd(Mapping):
     @property
     def nom_bitrate(self) -> int:
         """Nominal (arbitration phase) bitrate."""
-        return int(round(self.f_clock / (self.nbt * self.nom_brp)))
+        return round(self.f_clock / (self.nbt * self.nom_brp))
 
     @property
     def nom_brp(self) -> int:
@@ -933,7 +932,7 @@ class BitTimingFd(Mapping):
     @property
     def nom_tq(self) -> int:
         """Nominal time quantum in nanoseconds"""
-        return int(round(self.nom_brp / self.f_clock * 1e9))
+        return round(self.nom_brp / self.f_clock * 1e9)
 
     @property
     def nbt(self) -> int:
@@ -969,7 +968,7 @@ class BitTimingFd(Mapping):
     @property
     def data_bitrate(self) -> int:
         """Bitrate of the data phase in bit/s."""
-        return int(round(self.f_clock / (self.dbt * self.data_brp)))
+        return round(self.f_clock / (self.dbt * self.data_brp))
 
     @property
     def data_brp(self) -> int:
@@ -979,7 +978,7 @@ class BitTimingFd(Mapping):
     @property
     def data_tq(self) -> int:
         """Data time quantum in nanoseconds"""
-        return int(round(self.data_brp / self.f_clock * 1e9))
+        return round(self.data_brp / self.f_clock * 1e9)
 
     @property
     def dbt(self) -> int:
@@ -1106,10 +1105,10 @@ class BitTimingFd(Mapping):
                 "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 = int(round(self.nom_sjw / self.nbt * bt.nbt))
+        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 = int(round(self.data_sjw / self.dbt * bt.dbt))
+        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
@@ -1138,7 +1137,7 @@ class BitTimingFd(Mapping):
         return f"can.{self.__class__.__name__}({args})"
 
     def __getitem__(self, key: str) -> int:
-        return cast(int, self._data.__getitem__(key))
+        return cast("int", self._data.__getitem__(key))
 
     def __len__(self) -> int:
         return self._data.__len__()
diff -pruN 4.5.0-1/can/bridge.py 4.6.1-1/can/bridge.py
--- 4.5.0-1/can/bridge.py	1970-01-01 00:00:00.000000000 +0000
+++ 4.6.1-1/can/bridge.py	2025-08-12 07:32:01.000000000 +0000
@@ -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 -pruN 4.5.0-1/can/broadcastmanager.py 4.6.1-1/can/broadcastmanager.py
--- 4.5.0-1/can/broadcastmanager.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/broadcastmanager.py	2025-08-12 07:32:01.000000000 +0000
@@ -12,13 +12,12 @@ import sys
 import threading
 import time
 import warnings
+from collections.abc import Sequence
 from typing import (
     TYPE_CHECKING,
     Callable,
     Final,
     Optional,
-    Sequence,
-    Tuple,
     Union,
     cast,
 )
@@ -40,8 +39,8 @@ class _Pywin32Event:
 
 class _Pywin32:
     def __init__(self) -> None:
-        import pywintypes  # pylint: disable=import-outside-toplevel,import-error
-        import win32event  # pylint: disable=import-outside-toplevel,import-error
+        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
@@ -61,7 +60,7 @@ class _Pywin32:
         ):
             event = self.win32event.CreateWaitableTimer(None, False, None)
 
-        return cast(_Pywin32Event, event)
+        return cast("_Pywin32Event", event)
 
     def set_timer(self, event: _Pywin32Event, period_ms: int) -> None:
         self.win32event.SetWaitableTimer(event.handle, 0, period_ms, None, None, False)
@@ -121,13 +120,13 @@ class CyclicSendTaskABC(CyclicTask, abc.
         # Take the Arbitration ID of the first element
         self.arbitration_id = messages[0].arbitration_id
         self.period = period
-        self.period_ns = int(round(period * 1e9))
+        self.period_ns = round(period * 1e9)
         self.messages = messages
 
     @staticmethod
     def _check_and_convert_messages(
-        messages: Union[Sequence[Message], Message]
-    ) -> Tuple[Message, ...]:
+        messages: Union[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.
 
@@ -194,7 +193,7 @@ class RestartableCyclicTaskABC(CyclicSen
 
 
 class ModifiableCyclicTaskABC(CyclicSendTaskABC, abc.ABC):
-    def _check_modified_messages(self, messages: Tuple[Message, ...]) -> None:
+    def _check_modified_messages(self, messages: tuple[Message, ...]) -> None:
         """Helper function to perform error checking when modifying the data in
         the cyclic task.
 
diff -pruN 4.5.0-1/can/bus.py 4.6.1-1/can/bus.py
--- 4.5.0-1/can/bus.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/bus.py	2025-08-12 07:32:01.000000000 +0000
@@ -5,26 +5,20 @@ Contains the ABC bus implementation and
 import contextlib
 import logging
 import threading
-from abc import ABC, ABCMeta, abstractmethod
+from abc import ABC, abstractmethod
+from collections.abc import Iterator, Sequence
 from enum import Enum, auto
 from time import time
 from types import TracebackType
 from typing import (
-    Any,
     Callable,
-    Iterator,
-    List,
     Optional,
-    Sequence,
-    Tuple,
-    Type,
     Union,
     cast,
 )
 
 from typing_extensions import Self
 
-import can
 import can.typechecking
 from can.broadcastmanager import CyclicSendTaskABC, ThreadBasedCyclicSendTask
 from can.message import Message
@@ -49,7 +43,7 @@ class CanProtocol(Enum):
     CAN_XL = auto()
 
 
-class BusABC(metaclass=ABCMeta):
+class BusABC(ABC):
     """The CAN Bus Abstract Base Class that serves as the basis
     for all concrete interfaces.
 
@@ -73,7 +67,7 @@ class BusABC(metaclass=ABCMeta):
     @abstractmethod
     def __init__(
         self,
-        channel: Any,
+        channel: can.typechecking.Channel,
         can_filters: Optional[can.typechecking.CanFilters] = None,
         **kwargs: object,
     ):
@@ -97,7 +91,7 @@ class BusABC(metaclass=ABCMeta):
         :raises ~can.exceptions.CanInitializationError:
             If the bus cannot be initialized
         """
-        self._periodic_tasks: List[_SelfRemovingCyclicTask] = []
+        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,
@@ -147,7 +141,7 @@ class BusABC(metaclass=ABCMeta):
 
     def _recv_internal(
         self, timeout: Optional[float]
-    ) -> Tuple[Optional[Message], bool]:
+    ) -> tuple[Optional[Message], bool]:
         """
         Read a message from the bus and tell whether it was filtered.
         This methods may be called by :meth:`~can.BusABC.recv`
@@ -276,7 +270,7 @@ class BusABC(metaclass=ABCMeta):
 
         # Create a backend specific task; will be patched to a _SelfRemovingCyclicTask later
         task = cast(
-            _SelfRemovingCyclicTask,
+            "_SelfRemovingCyclicTask",
             self._send_periodic_internal(
                 msgs, period, duration, autostart, modifier_callback
             ),
@@ -452,7 +446,6 @@ class BusABC(metaclass=ABCMeta):
         for _filter in self._filters:
             # check if this filter even applies to the message
             if "extended" in _filter:
-                _filter = cast(can.typechecking.CanFilterExtended, _filter)
                 if _filter["extended"] != msg.is_extended_id:
                     continue
 
@@ -491,7 +484,7 @@ class BusABC(metaclass=ABCMeta):
 
     def __exit__(
         self,
-        exc_type: Optional[Type[BaseException]],
+        exc_type: Optional[type[BaseException]],
         exc_value: Optional[BaseException],
         traceback: Optional[TracebackType],
     ) -> None:
@@ -529,7 +522,7 @@ class BusABC(metaclass=ABCMeta):
         return self._can_protocol
 
     @staticmethod
-    def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]:
+    def _detect_available_configs() -> Sequence[can.typechecking.AutoDetectedConfig]:
         """Detect all configurations/channels that this interface could
         currently connect with.
 
diff -pruN 4.5.0-1/can/cli.py 4.6.1-1/can/cli.py
--- 4.5.0-1/can/cli.py	1970-01-01 00:00:00.000000000 +0000
+++ 4.6.1-1/can/cli.py	2025-08-12 07:32:01.000000000 +0000
@@ -0,0 +1,320 @@
+import argparse
+import re
+from collections.abc import Sequence
+from typing import Any, Optional, Union
+
+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: Optional[str] = None,
+    group_title: Optional[str] = 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="{<can_id>:<can_mask>,<can_id>~<can_mask>}",
+            help="R|Space separated CAN filters for the given CAN interface:"
+            "\n      <can_id>:<can_mask> (matches when <received_can_id> & mask =="
+            " can_id & mask)"
+            "\n      <can_id>~<can_mask> (matches when <received_can_id> & 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: Optional[str] = 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: Union[str, Sequence[Any], None],
+        option_string: Optional[str] = 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: Union[str, Sequence[Any], None],
+        option_string: Optional[str] = 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: Union[str, Sequence[Any], None],
+        option_string: Optional[str] = None,
+    ) -> None:
+        if not isinstance(values, list):
+            raise argparse.ArgumentError(self, "Invalid --bus-kwargs argument")
+
+        bus_kwargs: dict[str, Union[str, int, float, bool]] = {}
+
+        for arg in values:
+            try:
+                match = re.match(
+                    r"^(?P<name>[_a-zA-Z][_a-zA-Z0-9]*)=(?P<value>\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: Union[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, Union[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 -pruN 4.5.0-1/can/ctypesutil.py 4.6.1-1/can/ctypesutil.py
--- 4.5.0-1/can/ctypesutil.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/ctypesutil.py	2025-08-12 07:32:01.000000000 +0000
@@ -5,11 +5,11 @@ This module contains common `ctypes` uti
 import ctypes
 import logging
 import sys
-from typing import Any, Callable, Optional, Tuple, Union
+from typing import Any, Callable, Optional, Union
 
 log = logging.getLogger("can.ctypesutil")
 
-__all__ = ["CLibrary", "HANDLE", "PHANDLE", "HRESULT"]
+__all__ = ["HANDLE", "HRESULT", "PHANDLE", "CLibrary"]
 
 if sys.platform == "win32":
     _LibBase = ctypes.WinDLL
@@ -32,7 +32,7 @@ class CLibrary(_LibBase):
         self,
         func_name: str,
         restype: Any = None,
-        argtypes: Tuple[Any, ...] = (),
+        argtypes: tuple[Any, ...] = (),
         errcheck: Optional[Callable[..., Any]] = None,
     ) -> Any:
         """
diff -pruN 4.5.0-1/can/exceptions.py 4.6.1-1/can/exceptions.py
--- 4.5.0-1/can/exceptions.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/exceptions.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,6 +1,6 @@
 """
 There are several specific :class:`Exception` classes to allow user
-code to react to specific scenarios related to CAN busses::
+code to react to specific scenarios related to CAN buses::
 
     Exception (Python standard library)
      +-- ...
@@ -15,14 +15,9 @@ For example, validating typical argument
 :class:`ValueError`. This should always be documented for the function at hand.
 """
 
-import sys
+from collections.abc import Generator
 from contextlib import contextmanager
-from typing import Optional, Type
-
-if sys.version_info >= (3, 9):
-    from collections.abc import Generator
-else:
-    from typing import Generator
+from typing import Optional
 
 
 class CanError(Exception):
@@ -114,7 +109,7 @@ class CanTimeoutError(CanError, TimeoutE
 @contextmanager
 def error_check(
     error_message: Optional[str] = None,
-    exception_type: Type[CanError] = CanOperationError,
+    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:
diff -pruN 4.5.0-1/can/interface.py 4.6.1-1/can/interface.py
--- 4.5.0-1/can/interface.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interface.py	2025-08-12 07:32:01.000000000 +0000
@@ -4,9 +4,11 @@ as a list of all available backends and
 CyclicSendTasks.
 """
 
+import concurrent.futures.thread
 import importlib
 import logging
-from typing import Any, Iterable, List, Optional, Type, Union, cast
+from collections.abc import Callable, Iterable, Sequence
+from typing import Any, Optional, Union, cast
 
 from . import util
 from .bus import BusABC
@@ -18,7 +20,7 @@ log = logging.getLogger("can.interface")
 log_autodetect = log.getChild("detect_available_configs")
 
 
-def _get_class_for_interface(interface: str) -> Type[BusABC]:
+def _get_class_for_interface(interface: str) -> type[BusABC]:
     """
     Returns the main bus class for the given interface.
 
@@ -52,7 +54,7 @@ def _get_class_for_interface(interface:
             f"'{interface}': {e}"
         ) from None
 
-    return cast(Type[BusABC], bus_class)
+    return cast("type[BusABC]", bus_class)
 
 
 @util.deprecated_args_alias(
@@ -138,8 +140,9 @@ def Bus(  # noqa: N802
 
 
 def detect_available_configs(
-    interfaces: Union[None, str, Iterable[str]] = None
-) -> List[AutoDetectedConfig]:
+    interfaces: Union[None, str, Iterable[str]] = None,
+    timeout: float = 5.0,
+) -> Sequence[AutoDetectedConfig]:
     """Detect all configurations/channels that the interfaces could
     currently connect with.
 
@@ -147,59 +150,84 @@ def detect_available_configs(
 
     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.BusABC`.
+             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:
         interfaces = BACKENDS
     elif isinstance(interfaces, str):
         interfaces = (interfaces,)
-    # else it is supposed to be an iterable of strings
+    # otherwise assume iterable of strings
 
-    result = []
-    for interface in interfaces:
+    # Collect detection callbacks
+    callbacks: dict[str, Callable[[], Sequence[AutoDetectedConfig]]] = {}
+    for interface_keyword in interfaces:
         try:
-            bus_class = _get_class_for_interface(interface)
+            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,
+                interface_keyword,
             )
-            continue
 
-        # get available channels
-        try:
-            available = list(
-                bus_class._detect_available_configs()  # pylint: disable=protected-access
-            )
-        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
+    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 -pruN 4.5.0-1/can/interfaces/__init__.py 4.6.1-1/can/interfaces/__init__.py
--- 4.5.0-1/can/interfaces/__init__.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/__init__.py	2025-08-12 07:32:01.000000000 +0000
@@ -2,8 +2,6 @@
 Interfaces contain low level implementations that interact with CAN hardware.
 """
 
-from typing import Dict, Tuple
-
 from can._entry_points import read_entry_points
 
 __all__ = [
@@ -35,7 +33,7 @@ __all__ = [
 ]
 
 # interface_name => (module, classname)
-BACKENDS: Dict[str, Tuple[str, str]] = {
+BACKENDS: dict[str, tuple[str, str]] = {
     "kvaser": ("can.interfaces.kvaser", "KvaserBus"),
     "socketcan": ("can.interfaces.socketcan", "SocketcanBus"),
     "serial": ("can.interfaces.serial.serial_can", "SerialBus"),
diff -pruN 4.5.0-1/can/interfaces/canalystii.py 4.6.1-1/can/interfaces/canalystii.py
--- 4.5.0-1/can/interfaces/canalystii.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/canalystii.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,8 +1,9 @@
 import logging
 import time
 from collections import deque
+from collections.abc import Sequence
 from ctypes import c_ubyte
-from typing import Any, Deque, Dict, Optional, Sequence, Tuple, Union
+from typing import Any, Optional, Union
 
 import canalystii as driver
 
@@ -26,7 +27,7 @@ class CANalystIIBus(BusABC):
         timing: Optional[Union[BitTiming, BitTimingFd]] = None,
         can_filters: Optional[CanFilters] = None,
         rx_queue_size: Optional[int] = None,
-        **kwargs: Dict[str, Any],
+        **kwargs: dict[str, Any],
     ):
         """
 
@@ -68,7 +69,7 @@ class CANalystIIBus(BusABC):
             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.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
 
@@ -129,7 +130,7 @@ class CANalystIIBus(BusABC):
         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]:
+    def _recv_from_queue(self) -> tuple[Message, bool]:
         """Return a message from the internal receive queue"""
         channel, raw_msg = self.rx_queue.popleft()
 
@@ -166,7 +167,7 @@ class CANalystIIBus(BusABC):
 
     def _recv_internal(
         self, timeout: Optional[float] = None
-    ) -> Tuple[Optional[Message], bool]:
+    ) -> tuple[Optional[Message], bool]:
         """
 
         :param timeout: float in seconds
diff -pruN 4.5.0-1/can/interfaces/cantact.py 4.6.1-1/can/interfaces/cantact.py
--- 4.5.0-1/can/interfaces/cantact.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/cantact.py	2025-08-12 07:32:01.000000000 +0000
@@ -4,6 +4,7 @@ Interface for CANtact devices from Linkl
 
 import logging
 import time
+from collections.abc import Sequence
 from typing import Any, Optional, Union
 from unittest.mock import Mock
 
@@ -14,6 +15,7 @@ from ..exceptions import (
     CanInterfaceNotImplementedError,
     error_check,
 )
+from ..typechecking import AutoDetectedConfig
 from ..util import check_or_adjust_timing_clock, deprecated_args_alias
 
 logger = logging.getLogger(__name__)
@@ -31,7 +33,7 @@ class CantactBus(BusABC):
     """CANtact interface"""
 
     @staticmethod
-    def _detect_available_configs():
+    def _detect_available_configs() -> Sequence[AutoDetectedConfig]:
         try:
             interface = cantact.Interface()
         except (NameError, SystemError, AttributeError):
@@ -40,7 +42,7 @@ class CantactBus(BusABC):
             )
             return []
 
-        channels = []
+        channels: list[AutoDetectedConfig] = []
         for i in range(0, interface.channel_count()):
             channels.append({"interface": "cantact", "channel": f"ch:{i}"})
         return channels
@@ -121,7 +123,14 @@ class CantactBus(BusABC):
             **kwargs,
         )
 
-    def _recv_internal(self, timeout):
+    def _recv_internal(
+        self, timeout: Optional[float]
+    ) -> tuple[Optional[Message], 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:
@@ -140,7 +149,7 @@ class CantactBus(BusABC):
         )
         return msg, False
 
-    def send(self, msg, timeout=None):
+    def send(self, msg: Message, timeout: Optional[float] = None) -> None:
         with error_check("Cannot send message"):
             self.interface.send(
                 self.channel,
@@ -151,13 +160,13 @@ class CantactBus(BusABC):
                 msg.data,
             )
 
-    def shutdown(self):
+    def shutdown(self) -> None:
         super().shutdown()
         with error_check("Cannot shutdown interface"):
             self.interface.stop()
 
 
-def mock_recv(timeout):
+def mock_recv(timeout: int) -> Optional[dict[str, Any]]:
     if timeout > 0:
         return {
             "id": 0x123,
diff -pruN 4.5.0-1/can/interfaces/etas/__init__.py 4.6.1-1/can/interfaces/etas/__init__.py
--- 4.5.0-1/can/interfaces/etas/__init__.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/etas/__init__.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,5 +1,5 @@
 import time
-from typing import Dict, List, Optional, Tuple
+from typing import Optional
 
 import can
 from can.exceptions import CanInitializationError
@@ -16,7 +16,7 @@ class EtasBus(can.BusABC):
         bitrate: int = 1000000,
         fd: bool = True,
         data_bitrate: int = 2000000,
-        **kwargs: Dict[str, any],
+        **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
@@ -122,7 +122,7 @@ class EtasBus(can.BusABC):
 
     def _recv_internal(
         self, timeout: Optional[float]
-    ) -> Tuple[Optional[can.Message], bool]:
+    ) -> tuple[Optional[can.Message], bool]:
         ociMsgs = (ctypes.POINTER(OCI_CANMessageEx) * 1)()
         ociMsg = OCI_CANMessageEx()
         ociMsgs[0] = ctypes.pointer(ociMsg)
@@ -295,12 +295,12 @@ class EtasBus(can.BusABC):
         raise NotImplementedError("Setting state is not implemented.")
 
     @staticmethod
-    def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]:
+    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]] = []
+        nodes: list[dict[str, str]] = []
 
         def _findNodes(tree, prefix):
             uri = f"{prefix}/{tree.contents.item.uriName.decode()}"
diff -pruN 4.5.0-1/can/interfaces/gs_usb.py 4.6.1-1/can/interfaces/gs_usb.py
--- 4.5.0-1/can/interfaces/gs_usb.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/gs_usb.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,5 +1,5 @@
 import logging
-from typing import Optional, Tuple
+from typing import Optional
 
 import usb
 from gs_usb.constants import CAN_EFF_FLAG, CAN_ERR_FLAG, CAN_MAX_DLC, CAN_RTR_FLAG
@@ -119,7 +119,7 @@ class GsUsbBus(can.BusABC):
 
     def _recv_internal(
         self, timeout: Optional[float]
-    ) -> Tuple[Optional[can.Message], bool]:
+    ) -> tuple[Optional[can.Message], bool]:
         """
         Read a message from the bus and tell whether it was filtered.
         This methods may be called by :meth:`~can.BusABC.recv`
diff -pruN 4.5.0-1/can/interfaces/ics_neovi/__init__.py 4.6.1-1/can/interfaces/ics_neovi/__init__.py
--- 4.5.0-1/can/interfaces/ics_neovi/__init__.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/ics_neovi/__init__.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,5 +1,4 @@
-"""
-"""
+""" """
 
 __all__ = [
     "ICSApiError",
diff -pruN 4.5.0-1/can/interfaces/iscan.py 4.6.1-1/can/interfaces/iscan.py
--- 4.5.0-1/can/interfaces/iscan.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/iscan.py	2025-08-12 07:32:01.000000000 +0000
@@ -5,7 +5,7 @@ Interface for isCAN from *Thorsis Techno
 import ctypes
 import logging
 import time
-from typing import Optional, Tuple, Union
+from typing import Optional, Union
 
 from can import (
     BusABC,
@@ -117,7 +117,7 @@ class IscanBus(BusABC):
 
     def _recv_internal(
         self, timeout: Optional[float]
-    ) -> Tuple[Optional[Message], bool]:
+    ) -> tuple[Optional[Message], bool]:
         raw_msg = MessageExStruct()
         end_time = time.time() + timeout if timeout is not None else None
         while True:
diff -pruN 4.5.0-1/can/interfaces/ixxat/canlib.py 4.6.1-1/can/interfaces/ixxat/canlib.py
--- 4.5.0-1/can/interfaces/ixxat/canlib.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/ixxat/canlib.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,4 +1,5 @@
-from typing import Callable, List, Optional, Sequence, Union
+from collections.abc import Sequence
+from typing import Callable, Optional, Union
 
 import can.interfaces.ixxat.canlib_vcinpl as vcinpl
 import can.interfaces.ixxat.canlib_vcinpl2 as vcinpl2
@@ -8,7 +9,6 @@ from can import (
     CyclicSendTaskABC,
     Message,
 )
-from can.typechecking import AutoDetectedConfig
 
 
 class IXXATBus(BusABC):
@@ -174,5 +174,5 @@ class IXXATBus(BusABC):
         return self.bus.state
 
     @staticmethod
-    def _detect_available_configs() -> List[AutoDetectedConfig]:
+    def _detect_available_configs() -> Sequence[vcinpl.AutoDetectedIxxatConfig]:
         return vcinpl._detect_available_configs()
diff -pruN 4.5.0-1/can/interfaces/ixxat/canlib_vcinpl.py 4.6.1-1/can/interfaces/ixxat/canlib_vcinpl.py
--- 4.5.0-1/can/interfaces/ixxat/canlib_vcinpl.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/ixxat/canlib_vcinpl.py	2025-08-12 07:32:01.000000000 +0000
@@ -13,8 +13,10 @@ import ctypes
 import functools
 import logging
 import sys
+import time
 import warnings
-from typing import Callable, List, Optional, Sequence, Tuple, Union
+from collections.abc import Sequence
+from typing import Callable, Optional, Union
 
 from can import (
     BusABC,
@@ -35,11 +37,11 @@ from . import constants, structures
 from .exceptions import *
 
 __all__ = [
-    "VCITimeout",
-    "VCIError",
+    "IXXATBus",
     "VCIBusOffError",
     "VCIDeviceNotFoundError",
-    "IXXATBus",
+    "VCIError",
+    "VCITimeout",
     "vciFormatError",
 ]
 
@@ -64,7 +66,7 @@ else:
 
 
 def __vciFormatErrorExtended(
-    library_instance: CLibrary, function: Callable, vret: int, args: Tuple
+    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:
@@ -619,7 +621,15 @@ class IXXATBus(BusABC):
                 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
@@ -692,6 +702,9 @@ class IXXATBus(BusABC):
                         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:
                     log.warning("CAN error: data overrun")
@@ -708,7 +721,8 @@ class IXXATBus(BusABC):
                         self._message.uMsgInfo.Bytes.bFlags,
                     )
             elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR:
-                pass
+                # 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",
@@ -740,11 +754,12 @@ class IXXATBus(BusABC):
             # 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
+            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,
@@ -890,7 +905,7 @@ class CyclicSendTask(LimitedDurationCycl
         self._count = int(duration / period) if duration else 0
 
         self._msg = structures.CANCYCLICTXMSG()
-        self._msg.wCycleTime = int(round(period * resolution))
+        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
@@ -961,8 +976,8 @@ def get_ixxat_hwids():
     return hwids
 
 
-def _detect_available_configs() -> List[AutoDetectedConfig]:
-    config_list = []  # list in wich to store the resulting bus kwargs
+def _detect_available_configs() -> Sequence["AutoDetectedIxxatConfig"]:
+    config_list = []  # list in which to store the resulting bus kwargs
 
     # used to detect HWID
     device_handle = HANDLE()
@@ -1011,3 +1026,7 @@ def _detect_available_configs() -> List[
         pass  # _canlib is None in the CI tests -> return a blank list
 
     return config_list
+
+
+class AutoDetectedIxxatConfig(AutoDetectedConfig):
+    unique_hardware_id: int
diff -pruN 4.5.0-1/can/interfaces/ixxat/canlib_vcinpl2.py 4.6.1-1/can/interfaces/ixxat/canlib_vcinpl2.py
--- 4.5.0-1/can/interfaces/ixxat/canlib_vcinpl2.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/ixxat/canlib_vcinpl2.py	2025-08-12 07:32:01.000000000 +0000
@@ -15,7 +15,8 @@ import logging
 import sys
 import time
 import warnings
-from typing import Callable, Optional, Sequence, Tuple, Union
+from collections.abc import Sequence
+from typing import Callable, Optional, Union
 
 from can import (
     BusABC,
@@ -34,11 +35,11 @@ from . import constants, structures
 from .exceptions import *
 
 __all__ = [
-    "VCITimeout",
-    "VCIError",
+    "IXXATBus",
     "VCIBusOffError",
     "VCIDeviceNotFoundError",
-    "IXXATBus",
+    "VCIError",
+    "VCITimeout",
     "vciFormatError",
 ]
 
@@ -62,7 +63,7 @@ else:
 
 
 def __vciFormatErrorExtended(
-    library_instance: CLibrary, function: Callable, vret: int, args: Tuple
+    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:
@@ -726,7 +727,15 @@ class IXXATBus(BusABC):
                 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
@@ -831,7 +840,9 @@ class IXXATBus(BusABC):
                                 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
                     ):
@@ -853,7 +864,8 @@ class IXXATBus(BusABC):
                         self._message.uMsgInfo.Bits.type
                         == constants.CAN_MSGTYPE_TIMEOVR
                     ):
-                        pass
+                        # Add the number of timestamp overruns to the high word
+                        self._overrunticks += self._message.dwMsgId << 32
                     else:
                         log.warning("Unexpected message info type")
 
@@ -867,11 +879,12 @@ class IXXATBus(BusABC):
             return None, True
 
         data_len = dlc2len(self._message.uMsgInfo.Bits.dlc)
-        # 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
+            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,
@@ -1010,7 +1023,7 @@ class CyclicSendTask(LimitedDurationCycl
         self._count = int(duration / period) if duration else 0
 
         self._msg = structures.CANCYCLICTXMSG2()
-        self._msg.wCycleTime = int(round(period * resolution))
+        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
diff -pruN 4.5.0-1/can/interfaces/ixxat/exceptions.py 4.6.1-1/can/interfaces/ixxat/exceptions.py
--- 4.5.0-1/can/interfaces/ixxat/exceptions.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/ixxat/exceptions.py	2025-08-12 07:32:01.000000000 +0000
@@ -12,11 +12,11 @@ from can import (
 )
 
 __all__ = [
-    "VCITimeout",
-    "VCIError",
-    "VCIRxQueueEmptyError",
     "VCIBusOffError",
     "VCIDeviceNotFoundError",
+    "VCIError",
+    "VCIRxQueueEmptyError",
+    "VCITimeout",
 ]
 
 
diff -pruN 4.5.0-1/can/interfaces/ixxat/structures.py 4.6.1-1/can/interfaces/ixxat/structures.py
--- 4.5.0-1/can/interfaces/ixxat/structures.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/ixxat/structures.py	2025-08-12 07:32:01.000000000 +0000
@@ -201,13 +201,13 @@ class CANBTP(ctypes.Structure):
     ]
 
     def __str__(self):
-        return "dwMode=%d, dwBPS=%d, wTS1=%d,  wTS2=%d, wSJW=%d, wTDO=%d" % (
-            self.dwMode,
-            self.dwBPS,
-            self.wTS1,
-            self.wTS2,
-            self.wSJW,
-            self.wTDO,
+        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}"
         )
 
 
diff -pruN 4.5.0-1/can/interfaces/kvaser/__init__.py 4.6.1-1/can/interfaces/kvaser/__init__.py
--- 4.5.0-1/can/interfaces/kvaser/__init__.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/kvaser/__init__.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,5 +1,4 @@
-"""
-"""
+""" """
 
 __all__ = [
     "CANLIBInitializationError",
diff -pruN 4.5.0-1/can/interfaces/kvaser/canlib.py 4.6.1-1/can/interfaces/kvaser/canlib.py
--- 4.5.0-1/can/interfaces/kvaser/canlib.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/kvaser/canlib.py	2025-08-12 07:32:01.000000000 +0000
@@ -439,7 +439,8 @@ class KvaserBus(BusABC):
         :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(f"CAN Filters: {can_filters}")
@@ -455,6 +456,7 @@ class KvaserBus(BusABC):
         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)
@@ -491,6 +493,8 @@ class KvaserBus(BusABC):
             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:
             if fd_non_iso:
                 flags |= canstat.canOPEN_CAN_FD_NONISO
@@ -554,6 +558,15 @@ class KvaserBus(BusABC):
             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
@@ -671,6 +684,7 @@ class KvaserBus(BusABC):
             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
@@ -682,6 +696,7 @@ class KvaserBus(BusABC):
                 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,
diff -pruN 4.5.0-1/can/interfaces/kvaser/constants.py 4.6.1-1/can/interfaces/kvaser/constants.py
--- 4.5.0-1/can/interfaces/kvaser/constants.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/kvaser/constants.py	2025-08-12 07:32:01.000000000 +0000
@@ -63,6 +63,7 @@ canMSG_NERR = 0x0010
 canMSG_ERROR_FRAME = 0x0020
 canMSG_TXACK = 0x0040
 canMSG_TXRQ = 0x0080
+canMSG_LOCAL_TXACK = 0x1000_0000
 
 canFDMSG_FDF = 0x010000
 canFDMSG_BRS = 0x020000
@@ -195,6 +196,7 @@ canIOCTL_SET_USB_THROTTLE = 28
 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 -pruN 4.5.0-1/can/interfaces/neousys/__init__.py 4.6.1-1/can/interfaces/neousys/__init__.py
--- 4.5.0-1/can/interfaces/neousys/__init__.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/neousys/__init__.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,4 +1,4 @@
-""" Neousys CAN bus driver """
+"""Neousys CAN bus driver"""
 
 __all__ = [
     "NeousysBus",
diff -pruN 4.5.0-1/can/interfaces/neousys/neousys.py 4.6.1-1/can/interfaces/neousys/neousys.py
--- 4.5.0-1/can/interfaces/neousys/neousys.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/neousys/neousys.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,4 +1,4 @@
-""" Neousys CAN bus driver """
+"""Neousys CAN bus driver"""
 
 #
 # This kind of interface can be found for example on Neousys POC-551VTC
diff -pruN 4.5.0-1/can/interfaces/nican.py 4.6.1-1/can/interfaces/nican.py
--- 4.5.0-1/can/interfaces/nican.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/nican.py	2025-08-12 07:32:01.000000000 +0000
@@ -16,7 +16,7 @@ TODO: We could implement this interface
 import ctypes
 import logging
 import sys
-from typing import Optional, Tuple, Type
+from typing import Optional
 
 import can.typechecking
 from can import (
@@ -112,7 +112,7 @@ def check_status(
     result: int,
     function,
     arguments,
-    error_class: Type[NicanError] = NicanOperationError,
+    error_class: type[NicanError] = NicanOperationError,
 ) -> int:
     if result > 0:
         logger.warning(get_error_message(result))
@@ -281,7 +281,7 @@ class NicanBus(BusABC):
 
     def _recv_internal(
         self, timeout: Optional[float]
-    ) -> Tuple[Optional[Message], bool]:
+    ) -> tuple[Optional[Message], bool]:
         """
         Read a message from a NI-CAN bus.
 
diff -pruN 4.5.0-1/can/interfaces/nixnet.py 4.6.1-1/can/interfaces/nixnet.py
--- 4.5.0-1/can/interfaces/nixnet.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/nixnet.py	2025-08-12 07:32:01.000000000 +0000
@@ -14,7 +14,7 @@ import time
 import warnings
 from queue import SimpleQueue
 from types import ModuleType
-from typing import Any, List, Optional, Tuple, Union
+from typing import Any, Optional, Union
 
 import can.typechecking
 from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message
@@ -203,7 +203,7 @@ class NiXNETcanBus(BusABC):
 
     def _recv_internal(
         self, timeout: Optional[float]
-    ) -> Tuple[Optional[Message], bool]:
+    ) -> tuple[Optional[Message], bool]:
         end_time = time.perf_counter() + timeout if timeout is not None else None
 
         while True:
@@ -327,7 +327,7 @@ class NiXNETcanBus(BusABC):
             self._session_receive.close()
 
     @staticmethod
-    def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]:
+    def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]:
         configs = []
 
         try:
diff -pruN 4.5.0-1/can/interfaces/pcan/__init__.py 4.6.1-1/can/interfaces/pcan/__init__.py
--- 4.5.0-1/can/interfaces/pcan/__init__.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/pcan/__init__.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,5 +1,4 @@
-"""
-"""
+""" """
 
 __all__ = [
     "PcanBus",
diff -pruN 4.5.0-1/can/interfaces/pcan/pcan.py 4.6.1-1/can/interfaces/pcan/pcan.py
--- 4.5.0-1/can/interfaces/pcan/pcan.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/pcan/pcan.py	2025-08-12 07:32:01.000000000 +0000
@@ -6,7 +6,7 @@ import logging
 import platform
 import time
 import warnings
-from typing import Any, List, Optional, Tuple, Union
+from typing import Any, Optional, Union
 
 from packaging import version
 
@@ -502,7 +502,7 @@ class PcanBus(BusABC):
 
     def _recv_internal(
         self, timeout: Optional[float]
-    ) -> Tuple[Optional[Message], bool]:
+    ) -> tuple[Optional[Message], bool]:
         end_time = time.time() + timeout if timeout is not None else None
 
         while True:
@@ -590,6 +590,7 @@ class PcanBus(BusABC):
             )
 
         rx_msg = Message(
+            channel=self.channel_info,
             timestamp=timestamp,
             arbitration_id=pcan_msg.ID,
             is_extended_id=is_extended_id,
@@ -726,7 +727,7 @@ class PcanBus(BusABC):
             res, value = library_handle.GetValue(PCAN_NONEBUS, PCAN_ATTACHED_CHANNELS)
             if res != PCAN_ERROR_OK:
                 return interfaces
-            channel_information: List[TPCANChannelInformation] = list(value)
+            channel_information: list[TPCANChannelInformation] = list(value)
             for channel in channel_information:
                 # find channel name in PCAN_CHANNEL_NAMES by value
                 channel_name = next(
diff -pruN 4.5.0-1/can/interfaces/seeedstudio/__init__.py 4.6.1-1/can/interfaces/seeedstudio/__init__.py
--- 4.5.0-1/can/interfaces/seeedstudio/__init__.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/seeedstudio/__init__.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,6 +1,3 @@
-"""
-"""
-
 __all__ = [
     "SeeedBus",
     "seeedstudio",
diff -pruN 4.5.0-1/can/interfaces/serial/__init__.py 4.6.1-1/can/interfaces/serial/__init__.py
--- 4.5.0-1/can/interfaces/serial/__init__.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/serial/__init__.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,5 +1,4 @@
-"""
-"""
+""" """
 
 __all__ = [
     "SerialBus",
diff -pruN 4.5.0-1/can/interfaces/serial/serial_can.py 4.6.1-1/can/interfaces/serial/serial_can.py
--- 4.5.0-1/can/interfaces/serial/serial_can.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/serial/serial_can.py	2025-08-12 07:32:01.000000000 +0000
@@ -10,7 +10,8 @@ See the interface documentation for the
 import io
 import logging
 import struct
-from typing import Any, List, Optional, Tuple
+from collections.abc import Sequence
+from typing import Any, Optional, cast
 
 from can import (
     BusABC,
@@ -26,20 +27,20 @@ from can.typechecking import AutoDetecte
 logger = logging.getLogger("can.serial")
 
 try:
-    import serial
+    import serial.tools.list_ports
 except ImportError:
     logger.warning(
         "You won't be able to use the serial can backend without "
-        "the serial module installed!"
+        "the `pyserial` package installed!"
     )
     serial = None
 
-try:
-    from serial.tools.list_ports import comports as list_comports
-except ImportError:
-    # If unavailable on some platform, just return nothing
-    def list_comports() -> List[Any]:
-        return []
+
+CAN_ERR_FLAG = 0x20000000
+CAN_RTR_FLAG = 0x40000000
+CAN_EFF_FLAG = 0x80000000
+CAN_ID_MASK_EXT = 0x1FFFFFFF
+CAN_ID_MASK_STD = 0x7FF
 
 
 class SerialBus(BusABC):
@@ -56,8 +57,7 @@ class SerialBus(BusABC):
         baudrate: int = 115200,
         timeout: float = 0.1,
         rtscts: bool = False,
-        *args,
-        **kwargs,
+        **kwargs: Any,
     ) -> None:
         """
         :param channel:
@@ -100,7 +100,7 @@ class SerialBus(BusABC):
                 "could not create the serial device"
             ) from error
 
-        super().__init__(channel, *args, **kwargs)
+        super().__init__(channel, **kwargs)
 
     def shutdown(self) -> None:
         """
@@ -116,9 +116,6 @@ class SerialBus(BusABC):
         :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.
 
@@ -134,19 +131,25 @@ class SerialBus(BusABC):
             raise ValueError(f"Timestamp is out of range: {msg.timestamp}") from None
 
         # Pack arbitration ID
-        try:
-            arbitration_id = struct.pack("<I", msg.arbitration_id)
-        except struct.error:
-            raise ValueError(
-                f"Arbitration ID is out of range: {msg.arbitration_id}"
-            ) from None
+        if msg.is_extended_id:
+            arbitration_id = msg.arbitration_id & CAN_ID_MASK_EXT
+            arbitration_id |= CAN_EFF_FLAG
+        else:
+            arbitration_id = msg.arbitration_id & CAN_ID_MASK_STD
+
+        if msg.is_error_frame:
+            arbitration_id |= CAN_ERR_FLAG
+        if msg.is_remote_frame:
+            arbitration_id |= CAN_RTR_FLAG
+
+        arbitration_id_bytes = struct.pack("<I", arbitration_id)
 
         # Assemble message
         byte_msg = bytearray()
         byte_msg.append(0xAA)
         byte_msg += timestamp
         byte_msg.append(msg.dlc)
-        byte_msg += arbitration_id
+        byte_msg += arbitration_id_bytes
         byte_msg += msg.data
         byte_msg.append(0xBB)
 
@@ -160,7 +163,7 @@ class SerialBus(BusABC):
 
     def _recv_internal(
         self, timeout: Optional[float]
-    ) -> Tuple[Optional[Message], bool]:
+    ) -> tuple[Optional[Message], bool]:
         """
         Read a message from the serial device.
 
@@ -171,11 +174,6 @@ class SerialBus(BusABC):
 
         :returns:
             Received message and :obj:`False` (because no filtering as taken place).
-
-            .. warning::
-                Flags like ``is_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.
         """
         try:
             rx_byte = self._ser.read()
@@ -188,10 +186,14 @@ class SerialBus(BusABC):
 
                 s = self._ser.read(4)
                 arbitration_id = struct.unpack("<I", s)[0]
-                if arbitration_id >= 0x20000000:
-                    raise ValueError(
-                        "received arbitration id may not exceed 2^29 (0x20000000)"
-                    )
+                is_extended_id = bool(arbitration_id & CAN_EFF_FLAG)
+                is_error_frame = bool(arbitration_id & CAN_ERR_FLAG)
+                is_remote_frame = bool(arbitration_id & CAN_RTR_FLAG)
+
+                if is_extended_id:
+                    arbitration_id = arbitration_id & CAN_ID_MASK_EXT
+                else:
+                    arbitration_id = arbitration_id & CAN_ID_MASK_STD
 
                 data = self._ser.read(dlc)
 
@@ -204,6 +206,9 @@ class SerialBus(BusABC):
                         arbitration_id=arbitration_id,
                         dlc=dlc,
                         data=data,
+                        is_extended_id=is_extended_id,
+                        is_error_frame=is_error_frame,
+                        is_remote_frame=is_remote_frame,
                     )
                     return msg, False
 
@@ -220,7 +225,7 @@ class SerialBus(BusABC):
 
     def fileno(self) -> int:
         try:
-            return self._ser.fileno()
+            return cast("int", self._ser.fileno())
         except io.UnsupportedOperation:
             raise NotImplementedError(
                 "fileno is not implemented using current CAN bus on this platform"
@@ -229,7 +234,11 @@ class SerialBus(BusABC):
             raise CanOperationError("Cannot fetch fileno") from exception
 
     @staticmethod
-    def _detect_available_configs() -> List[AutoDetectedConfig]:
-        return [
-            {"interface": "serial", "channel": port.device} for port in list_comports()
-        ]
+    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 -pruN 4.5.0-1/can/interfaces/slcan.py 4.6.1-1/can/interfaces/slcan.py
--- 4.5.0-1/can/interfaces/slcan.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/slcan.py	2025-08-12 07:32:01.000000000 +0000
@@ -6,7 +6,8 @@ import io
 import logging
 import time
 import warnings
-from typing import Any, Optional, Tuple, Union
+from queue import SimpleQueue
+from typing import Any, Optional, Union, cast
 
 from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message, typechecking
 from can.exceptions import (
@@ -15,7 +16,12 @@ from can.exceptions import (
     CanOperationError,
     error_check,
 )
-from can.util import check_or_adjust_timing_clock, deprecated_args_alias
+from can.util import (
+    CAN_FD_DLC,
+    check_or_adjust_timing_clock,
+    deprecated_args_alias,
+    len2dlc,
+)
 
 logger = logging.getLogger(__name__)
 
@@ -47,6 +53,11 @@ class slcanBus(BusABC):
         1000000: "S8",
         83300: "S9",
     }
+    _DATA_BITRATES = {
+        0: "",
+        2000000: "Y2",
+        5000000: "Y5",
+    }
 
     _SLEEP_AFTER_SERIAL_OPEN = 2  # in seconds
 
@@ -85,7 +96,8 @@ class slcanBus(BusABC):
             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 are not supported.
+            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 sleep_after_open:
@@ -131,6 +143,7 @@ class slcanBus(BusABC):
                 timeout=timeout,
             )
 
+        self._queue: SimpleQueue[str] = SimpleQueue()
         self._buffer = bytearray()
         self._can_protocol = CanProtocol.CAN_20
 
@@ -141,9 +154,7 @@ class slcanBus(BusABC):
                 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):
-                raise NotImplementedError(
-                    f"CAN FD is not supported by {self.__class__.__name__}."
-                )
+                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.")
@@ -155,10 +166,12 @@ class slcanBus(BusABC):
 
         super().__init__(channel, **kwargs)
 
-    def set_bitrate(self, bitrate: int) -> None:
+    def set_bitrate(self, bitrate: int, data_bitrate: Optional[int] = None) -> None:
         """
         :param bitrate:
             Bitrate in bit/s
+        :param data_bitrate:
+            Data Bitrate in bit/s for FD frames
 
         :raise ValueError: if ``bitrate`` is not among the possible values
         """
@@ -168,8 +181,19 @@ class slcanBus(BusABC):
             bitrates = ", ".join(str(k) for k in self._BITRATES.keys())
             raise ValueError(f"Invalid bitrate, choose one of {bitrates}.")
 
+        # If data_bitrate is None, we set it to 0 which means no data bitrate
+        if data_bitrate is None:
+            data_bitrate = 0
+
+        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}.")
+
         self.close()
         self._write(bitrate_code)
+        self._write(dbitrate_code)
         self.open()
 
     def set_bitrate_reg(self, btr: str) -> None:
@@ -196,7 +220,7 @@ class slcanBus(BusABC):
                 # 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(size=1)
+                    new_byte = self.serialPortOrig.read(1)
                     if new_byte:
                         self._buffer.extend(new_byte)
                     else:
@@ -228,13 +252,18 @@ class slcanBus(BusABC):
 
     def _recv_internal(
         self, timeout: Optional[float]
-    ) -> Tuple[Optional[Message], bool]:
+    ) -> tuple[Optional[Message], bool]:
         canId = None
         remote = False
         extended = False
         data = None
+        isFd = False
+        fdBrs = False
 
-        string = self._read(timeout)
+        if self._queue.qsize():
+            string: Optional[str] = self._queue.get_nowait()
+        else:
+            string = self._read(timeout)
 
         if not string:
             pass
@@ -263,6 +292,34 @@ class slcanBus(BusABC):
             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(
@@ -270,7 +327,9 @@ class slcanBus(BusABC):
                 is_extended_id=extended,
                 timestamp=time.time(),  # Better than nothing...
                 is_remote_frame=remote,
-                dlc=dlc,
+                is_fd=isFd,
+                bitrate_switch=fdBrs,
+                dlc=CAN_FD_DLC[dlc],
                 data=data,
             )
             return msg, False
@@ -284,6 +343,20 @@ class slcanBus(BusABC):
                 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:
+                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 = f"T{msg.arbitration_id:08X}{msg.dlc:d}"
@@ -300,7 +373,7 @@ class slcanBus(BusABC):
 
     def fileno(self) -> int:
         try:
-            return self.serialPortOrig.fileno()
+            return cast("int", self.serialPortOrig.fileno())
         except io.UnsupportedOperation:
             raise NotImplementedError(
                 "fileno is not implemented using current CAN bus on this platform"
@@ -310,7 +383,7 @@ class slcanBus(BusABC):
 
     def get_version(
         self, timeout: Optional[float]
-    ) -> Tuple[Optional[int], Optional[int]]:
+    ) -> tuple[Optional[int], Optional[int]]:
         """Get HW and SW version of the slcan interface.
 
         :param timeout:
@@ -321,19 +394,21 @@ class slcanBus(BusABC):
             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)
 
-        string = self._read(timeout)
-
-        if not string:
-            pass
-        elif string[0] == cmd and len(string) == 6:
-            # convert ASCII coded version
-            hw_version = int(string[1:3])
-            sw_version = int(string[3:5])
-            return hw_version, sw_version
-
+        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: Optional[float]) -> Optional[str]:
@@ -345,15 +420,17 @@ class slcanBus(BusABC):
         :return:
             :obj:`None` on timeout or a :class:`str` object.
         """
+        _timeout = serial.Timeout(timeout)
         cmd = "N"
         self._write(cmd)
 
-        string = self._read(timeout)
-
-        if not string:
-            pass
-        elif string[0] == cmd and len(string) == 6:
-            serial_number = string[1:-1]
-            return serial_number
-
+        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 -pruN 4.5.0-1/can/interfaces/socketcan/constants.py 4.6.1-1/can/interfaces/socketcan/constants.py
--- 4.5.0-1/can/interfaces/socketcan/constants.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/socketcan/constants.py	2025-08-12 07:32:01.000000000 +0000
@@ -53,8 +53,18 @@ SIOCGIFINDEX = 0x8933
 SIOCGSTAMP = 0x8906
 EXTFLG = 0x0004
 
-CANFD_BRS = 0x01
-CANFD_ESI = 0x02
+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
+
+# CAN payload length and DLC definitions according to ISO 11898-1
+CAN_MAX_DLC = 8
+CAN_MAX_RAW_DLC = 15
+CAN_MAX_DLEN = 8
+
+# CAN FD payload length and DLC definitions according to ISO 11898-7
+CANFD_MAX_DLC = 15
+CANFD_MAX_DLEN = 64
 
 CANFD_MTU = 72
 
diff -pruN 4.5.0-1/can/interfaces/socketcan/socketcan.py 4.6.1-1/can/interfaces/socketcan/socketcan.py
--- 4.5.0-1/can/interfaces/socketcan/socketcan.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/socketcan/socketcan.py	2025-08-12 07:32:01.000000000 +0000
@@ -15,7 +15,8 @@ import struct
 import threading
 import time
 import warnings
-from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
+from collections.abc import Sequence
+from typing import Callable, Optional, Union
 
 import can
 from can import BusABC, CanProtocol, Message
@@ -50,13 +51,13 @@ RECEIVED_ANCILLARY_BUFFER_SIZE = (
 
 # Setup BCM struct
 def bcm_header_factory(
-    fields: List[Tuple[str, Union[Type[ctypes.c_uint32], Type[ctypes.c_long]]]],
+    fields: list[tuple[str, Union[type[ctypes.c_uint32], type[ctypes.c_long]]]],
     alignment: int = 8,
 ):
     curr_stride = 0
-    results: List[
-        Tuple[
-            str, Union[Type[ctypes.c_uint8], Type[ctypes.c_uint32], Type[ctypes.c_long]]
+    results: list[
+        tuple[
+            str, Union[type[ctypes.c_uint8], type[ctypes.c_uint32], type[ctypes.c_long]]
         ]
     ] = []
     pad_index = 0
@@ -139,23 +140,68 @@ BcmMsgHead = bcm_header_factory(
 # 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: Message) -> bytes:
     """CAN frame packing/unpacking (see 'struct can_frame' in <linux/can.h>)
     /**
-     * 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
@@ -175,14 +221,30 @@ def build_can_frame(msg: Message) -> byt
     };
     """
     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 |= constants.CANFD_BRS
     if msg.error_state_indicator:
         flags |= constants.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
+
+    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(
@@ -231,7 +293,7 @@ def build_bcm_transmit_header(
         # Note `TX_COUNTEVT` creates the message TX_EXPIRED when count expires
         flags |= constants.TX_COUNTEVT
 
-    def split_time(value: float) -> Tuple[int, int]:
+    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))
@@ -259,12 +321,31 @@ def build_bcm_update_header(can_id: int,
     )
 
 
-def dissect_can_frame(frame: bytes) -> Tuple[int, int, int, bytes]:
-    can_id, can_dlc, flags = CAN_FRAME_HEADER_STRUCT.unpack_from(frame)
-    if len(frame) != constants.CANFD_MTU:
+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 dissect_can_frame(frame: bytes) -> tuple[int, int, int, bytes]:
+    can_id, data_len, flags, len8_dlc = CAN_FRAME_HEADER_STRUCT.unpack_from(frame)
+
+    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: str) -> socket.socket:
@@ -659,7 +740,7 @@ class SocketcanBus(BusABC):  # pylint: d
         self.socket = create_socket()
         self.channel = channel
         self.channel_info = f"socketcan channel '{channel}'"
-        self._bcm_sockets: Dict[str, socket.socket] = {}
+        self._bcm_sockets: dict[str, socket.socket] = {}
         self._is_filtered = False
         self._task_id = 0
         self._task_id_guard = threading.Lock()
@@ -739,7 +820,7 @@ class SocketcanBus(BusABC):  # pylint: d
 
     def _recv_internal(
         self, timeout: Optional[float]
-    ) -> Tuple[Optional[Message], bool]:
+    ) -> tuple[Optional[Message], 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)
@@ -912,7 +993,7 @@ class SocketcanBus(BusABC):  # pylint: d
         return self.socket.fileno()
 
     @staticmethod
-    def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]:
+    def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]:
         return [
             {"interface": "socketcan", "channel": channel}
             for channel in find_available_interfaces()
diff -pruN 4.5.0-1/can/interfaces/socketcan/utils.py 4.6.1-1/can/interfaces/socketcan/utils.py
--- 4.5.0-1/can/interfaces/socketcan/utils.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/socketcan/utils.py	2025-08-12 07:32:01.000000000 +0000
@@ -9,7 +9,7 @@ import os
 import struct
 import subprocess
 import sys
-from typing import List, Optional, cast
+from typing import Optional
 
 from can import typechecking
 from can.interfaces.socketcan.constants import CAN_EFF_FLAG
@@ -28,7 +28,6 @@ def pack_filters(can_filters: Optional[t
         can_id = can_filter["can_id"]
         can_mask = can_filter["can_mask"]
         if "extended" in can_filter:
-            can_filter = cast(typechecking.CanFilterExtended, can_filter)
             # Match on either 11-bit OR 29-bit messages instead of both
             can_mask |= CAN_EFF_FLAG
             if can_filter["extended"]:
@@ -39,7 +38,7 @@ def pack_filters(can_filters: Optional[t
     return struct.pack(can_filter_fmt, *filter_data)
 
 
-def find_available_interfaces() -> List[str]:
+def find_available_interfaces() -> list[str]:
     """Returns the names of all open can/vcan interfaces
 
     The function calls the ``ip link list`` command. If the lookup fails, an error
diff -pruN 4.5.0-1/can/interfaces/socketcand/socketcand.py 4.6.1-1/can/interfaces/socketcand/socketcand.py
--- 4.5.0-1/can/interfaces/socketcand/socketcand.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/socketcand/socketcand.py	2025-08-12 07:32:01.000000000 +0000
@@ -17,7 +17,6 @@ import traceback
 import urllib.parse as urlparselib
 import xml.etree.ElementTree as ET
 from collections import deque
-from typing import List
 
 import can
 
@@ -27,7 +26,7 @@ DEFAULT_SOCKETCAND_DISCOVERY_ADDRESS = "
 DEFAULT_SOCKETCAND_DISCOVERY_PORT = 42000
 
 
-def detect_beacon(timeout_ms: int = 3100) -> List[can.typechecking.AutoDetectedConfig]:
+def detect_beacon(timeout_ms: int = 3100) -> list[can.typechecking.AutoDetectedConfig]:
     """
     Detects socketcand servers
 
@@ -125,10 +124,11 @@ def detect_beacon(timeout_ms: int = 3100
 
 
 def convert_ascii_message_to_can_message(ascii_msg: str) -> can.Message:
-    if not ascii_msg.startswith("< frame ") or not ascii_msg.endswith(" >"):
-        log.warning(f"Could not parse ascii message: {ascii_msg}")
+    if not ascii_msg.endswith(" >"):
+        log.warning(f"Missing ending character in ascii message: {ascii_msg}")
         return None
-    else:
+
+    if ascii_msg.startswith("< frame "):
         # frame_string = ascii_msg.removeprefix("< frame ").removesuffix(" >")
         frame_string = ascii_msg[8:-2]
         parts = frame_string.split(" ", 3)
@@ -147,6 +147,31 @@ def convert_ascii_message_to_can_message
         )
         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
@@ -340,7 +365,7 @@ class SocketCanDaemonBus(can.BusABC):
         self.__socket.close()
 
     @staticmethod
-    def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]:
+    def _detect_available_configs() -> list[can.typechecking.AutoDetectedConfig]:
         try:
             return detect_beacon()
         except Exception as e:
diff -pruN 4.5.0-1/can/interfaces/systec/exceptions.py 4.6.1-1/can/interfaces/systec/exceptions.py
--- 4.5.0-1/can/interfaces/systec/exceptions.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/systec/exceptions.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,5 +1,4 @@
 from abc import ABC, abstractmethod
-from typing import Dict
 
 from can import CanError
 
@@ -22,7 +21,7 @@ class UcanException(CanError, ABC):
 
     @property
     @abstractmethod
-    def _error_message_mapping(self) -> Dict[ReturnCode, str]: ...
+    def _error_message_mapping(self) -> dict[ReturnCode, str]: ...
 
 
 class UcanError(UcanException):
@@ -51,7 +50,7 @@ class UcanError(UcanException):
     }
 
     @property
-    def _error_message_mapping(self) -> Dict[ReturnCode, str]:
+    def _error_message_mapping(self) -> dict[ReturnCode, str]:
         return UcanError._ERROR_MESSAGES
 
 
@@ -77,7 +76,7 @@ class UcanCmdError(UcanException):
     }
 
     @property
-    def _error_message_mapping(self) -> Dict[ReturnCode, str]:
+    def _error_message_mapping(self) -> dict[ReturnCode, str]:
         return UcanCmdError._ERROR_MESSAGES
 
 
@@ -102,5 +101,5 @@ class UcanWarning(UcanException):
     }
 
     @property
-    def _error_message_mapping(self) -> Dict[ReturnCode, str]:
+    def _error_message_mapping(self) -> dict[ReturnCode, str]:
         return UcanWarning._ERROR_MESSAGES
diff -pruN 4.5.0-1/can/interfaces/systec/structures.py 4.6.1-1/can/interfaces/systec/structures.py
--- 4.5.0-1/can/interfaces/systec/structures.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/systec/structures.py	2025-08-12 07:32:01.000000000 +0000
@@ -51,6 +51,7 @@ class CanMsg(Structure):
             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
@@ -116,6 +117,7 @@ class Status(Structure):
         ("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):
@@ -171,6 +173,7 @@ class InitCanParam(Structure):
             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
@@ -277,6 +280,7 @@ class HardwareInfoEx(Structure):
         ("m_dwUniqueId3", DWORD),
         ("m_dwFlags", DWORD),  # additional flags
     ]
+    __hash__ = Structure.__hash__
 
     def __init__(self):
         super().__init__(sizeof(HardwareInfoEx))
@@ -389,6 +393,7 @@ class ChannelInfo(Structure):
             WORD,
         ),  # CAN status (same as received by method :meth:`UcanServer.get_status`)
     ]
+    __hash__ = Structure.__hash__
 
     def __init__(self):
         super().__init__(sizeof(ChannelInfo))
diff -pruN 4.5.0-1/can/interfaces/udp_multicast/bus.py 4.6.1-1/can/interfaces/udp_multicast/bus.py
--- 4.5.0-1/can/interfaces/udp_multicast/bus.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/udp_multicast/bus.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,35 +1,38 @@
 import errno
 import logging
+import platform
 import select
 import socket
 import struct
+import time
 import warnings
-from typing import List, Optional, Tuple, Union
+from typing import Any, Optional, Union
 
 import can
-from can import BusABC, CanProtocol
+from can import BusABC, CanProtocol, Message
 from can.typechecking import AutoDetectedConfig
 
-from .utils import check_msgpack_installed, pack_message, unpack_message
+from .utils import is_msgpack_installed, pack_message, unpack_message
 
-try:
+is_linux = platform.system() == "Linux"
+if is_linux:
     from fcntl import ioctl
-except ModuleNotFoundError:  # Missing on Windows
-    pass
-
 
 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
+IPv4_ADDRESS_INFO = tuple[str, int]  # address, port
+IPv6_ADDRESS_INFO = tuple[str, int, int, int]  # address, port, flowinfo, scope_id
 IP_ADDRESS_INFO = Union[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.
@@ -95,9 +98,9 @@ class UdpMulticastBus(BusABC):
         hop_limit: int = 1,
         receive_own_messages: bool = False,
         fd: bool = True,
-        **kwargs,
+        **kwargs: Any,
     ) -> None:
-        check_msgpack_installed()
+        is_msgpack_installed()
 
         if receive_own_messages:
             raise can.CanInterfaceNotImplementedError(
@@ -123,7 +126,9 @@ class UdpMulticastBus(BusABC):
         )
         return self._can_protocol is CanProtocol.CAN_FD
 
-    def _recv_internal(self, timeout: Optional[float]):
+    def _recv_internal(
+        self, timeout: Optional[float]
+    ) -> tuple[Optional[Message], bool]:
         result = self._multicast.recv(timeout)
         if not result:
             return None, False
@@ -165,7 +170,7 @@ class UdpMulticastBus(BusABC):
         self._multicast.shutdown()
 
     @staticmethod
-    def _detect_available_configs() -> List[AutoDetectedConfig]:
+    def _detect_available_configs() -> list[AutoDetectedConfig]:
         if hasattr(socket, "CMSG_SPACE"):
             return [
                 {
@@ -201,7 +206,7 @@ class GeneralPurposeUdpMulticastBus:
 
         # 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(  # type: ignore
+        connection_candidates = socket.getaddrinfo(
             group, self.port, type=socket.SOCK_DGRAM
         )
         sock = None
@@ -268,11 +273,19 @@ class GeneralPurposeUdpMulticastBus:
             # 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:  # It is unavailable on macOS
+                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
@@ -330,7 +343,7 @@ class GeneralPurposeUdpMulticastBus:
 
     def recv(
         self, timeout: Optional[float] = None
-    ) -> Optional[Tuple[bytes, IP_ADDRESS_INFO, float]]:
+    ) -> Optional[tuple[bytes, IP_ADDRESS_INFO, float]]:
         """
         Receive up to **max_buffer** bytes.
 
@@ -353,18 +366,18 @@ class GeneralPurposeUdpMulticastBus:
             ) from exc
 
         if ready_receive_sockets:  # not empty
-            # fetch data & source address
-            (
-                raw_message_data,
-                ancillary_data,
-                _,  # flags
-                sender_address,
-            ) = self._socket.recvmsg(
-                self.max_buffer, self.received_ancillary_buffer_size
-            )
-
             # 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(
@@ -385,14 +398,29 @@ class GeneralPurposeUdpMulticastBus:
                     )
                 timestamp = seconds + nanoseconds * 1.0e-9
             else:
-                result_buffer = ioctl(
-                    self._socket.fileno(),
-                    SIOCGSTAMP,
-                    bytes(self.received_timestamp_struct_size),
-                )
-                seconds, microseconds = struct.unpack(
-                    self.received_timestamp_struct, result_buffer
+                # 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"
diff -pruN 4.5.0-1/can/interfaces/udp_multicast/utils.py 4.6.1-1/can/interfaces/udp_multicast/utils.py
--- 4.5.0-1/can/interfaces/udp_multicast/utils.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/udp_multicast/utils.py	2025-08-12 07:32:01.000000000 +0000
@@ -2,7 +2,7 @@
 Defines common functions.
 """
 
-from typing import Any, Dict, Optional
+from typing import Any, Optional, cast
 
 from can import CanInterfaceNotImplementedError, Message
 from can.typechecking import ReadableBytesLike
@@ -13,10 +13,22 @@ except ImportError:
     msgpack = None
 
 
-def check_msgpack_installed() -> None:
-    """Raises a :class:`can.CanInterfaceNotImplementedError` if `msgpack` is not installed."""
+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:
-        raise CanInterfaceNotImplementedError("msgpack not installed")
+        if raise_exception:
+            raise CanInterfaceNotImplementedError("msgpack not installed")
+        return False
+    return True
 
 
 def pack_message(message: Message) -> bytes:
@@ -25,7 +37,7 @@ def pack_message(message: Message) -> by
 
     :param message: the message to be packed
     """
-    check_msgpack_installed()
+    is_msgpack_installed()
     as_dict = {
         "timestamp": message.timestamp,
         "arbitration_id": message.arbitration_id,
@@ -39,12 +51,12 @@ def pack_message(message: Message) -> by
         "bitrate_switch": message.bitrate_switch,
         "error_state_indicator": message.error_state_indicator,
     }
-    return msgpack.packb(as_dict, use_bin_type=True)
+    return cast("bytes", msgpack.packb(as_dict, use_bin_type=True))
 
 
 def unpack_message(
     data: ReadableBytesLike,
-    replace: Optional[Dict[str, Any]] = None,
+    replace: Optional[dict[str, Any]] = None,
     check: bool = False,
 ) -> Message:
     """Unpack a can.Message from a msgpack byte blob.
@@ -58,7 +70,7 @@ def unpack_message(
     :raise ValueError: if `check` is true and the message metadata is invalid in some way
     :raise Exception: if there was another problem while unpacking
     """
-    check_msgpack_installed()
+    is_msgpack_installed()
     as_dict = msgpack.unpackb(data, raw=False)
     if replace is not None:
         as_dict.update(replace)
diff -pruN 4.5.0-1/can/interfaces/usb2can/__init__.py 4.6.1-1/can/interfaces/usb2can/__init__.py
--- 4.5.0-1/can/interfaces/usb2can/__init__.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/usb2can/__init__.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,12 +1,11 @@
-"""
-"""
+""" """
 
 __all__ = [
     "Usb2CanAbstractionLayer",
     "Usb2canBus",
     "serial_selector",
-    "usb2canabstractionlayer",
     "usb2canInterface",
+    "usb2canabstractionlayer",
 ]
 
 from .usb2canabstractionlayer import Usb2CanAbstractionLayer
diff -pruN 4.5.0-1/can/interfaces/usb2can/serial_selector.py 4.6.1-1/can/interfaces/usb2can/serial_selector.py
--- 4.5.0-1/can/interfaces/usb2can/serial_selector.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/usb2can/serial_selector.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,12 +1,11 @@
-"""
-"""
+""" """
 
 import logging
-from typing import List
 
 log = logging.getLogger("can.usb2can")
 
 try:
+    import pythoncom
     import win32com.client
 except ImportError:
     log.warning(
@@ -44,7 +43,7 @@ def WMIDateStringToDate(dtmDate) -> str:
     return strDateTime
 
 
-def find_serial_devices(serial_matcher: str = "") -> List[str]:
+def find_serial_devices(serial_matcher: str = "") -> list[str]:
     """
     Finds a list of USB devices where the serial number (partially) matches the given string.
 
@@ -52,6 +51,7 @@ def find_serial_devices(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)
diff -pruN 4.5.0-1/can/interfaces/vector/__init__.py 4.6.1-1/can/interfaces/vector/__init__.py
--- 4.5.0-1/can/interfaces/vector/__init__.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/vector/__init__.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,5 +1,4 @@
-"""
-"""
+""" """
 
 __all__ = [
     "VectorBus",
diff -pruN 4.5.0-1/can/interfaces/vector/canlib.py 4.6.1-1/can/interfaces/vector/canlib.py
--- 4.5.0-1/can/interfaces/vector/canlib.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/vector/canlib.py	2025-08-12 07:32:01.000000000 +0000
@@ -10,17 +10,13 @@ import logging
 import os
 import time
 import warnings
+from collections.abc import Iterator, Sequence
 from types import ModuleType
 from typing import (
     Any,
     Callable,
-    Dict,
-    Iterator,
-    List,
     NamedTuple,
     Optional,
-    Sequence,
-    Tuple,
     Union,
     cast,
 )
@@ -204,8 +200,8 @@ class VectorBus(BusABC):
         is_fd = isinstance(timing, BitTimingFd) if timing else fd
 
         self.mask = 0
-        self.channel_masks: Dict[int, int] = {}
-        self.index_to_channel: Dict[int, int] = {}
+        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
@@ -220,7 +216,7 @@ class VectorBus(BusABC):
                 # 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)
+                channel_index = cast("int", _channel_index)
             else:
                 channel_index = self._find_global_channel_idx(
                     channel=channel,
@@ -383,7 +379,7 @@ class VectorBus(BusABC):
         channel: int,
         serial: Optional[int],
         app_name: Optional[str],
-        channel_configs: List["VectorChannelConfig"],
+        channel_configs: list["VectorChannelConfig"],
     ) -> int:
         if serial is not None:
             serial_found = False
@@ -410,7 +406,7 @@ class VectorBus(BusABC):
                 app_name, channel
             )
             idx = cast(
-                int, self.xldriver.xlGetChannelIndex(hw_type, hw_index, hw_channel)
+                "int", self.xldriver.xlGetChannelIndex(hw_type, hw_index, hw_channel)
             )
             if idx < 0:
                 # Undocumented behavior! See issue #353.
@@ -439,7 +435,7 @@ class VectorBus(BusABC):
         return bool(self.permission_mask & self.channel_masks[channel])
 
     def _read_bus_params(
-        self, channel_index: int, vcc_list: List["VectorChannelConfig"]
+        self, channel_index: int, vcc_list: list["VectorChannelConfig"]
     ) -> "VectorBusParams":
         for vcc in vcc_list:
             if vcc.channel_index == channel_index:
@@ -712,7 +708,7 @@ class VectorBus(BusABC):
 
     def _recv_internal(
         self, timeout: Optional[float]
-    ) -> Tuple[Optional[Message], bool]:
+    ) -> tuple[Optional[Message], bool]:
         end_time = time.time() + timeout if timeout is not None else None
 
         while True:
@@ -986,8 +982,8 @@ class VectorBus(BusABC):
         )
 
     @staticmethod
-    def _detect_available_configs() -> List[AutoDetectedConfig]:
-        configs = []
+    def _detect_available_configs() -> Sequence["AutoDetectedVectorConfig"]:
+        configs: list[AutoDetectedVectorConfig] = []
         channel_configs = get_channel_configs()
         LOG.info("Found %d channels", len(channel_configs))
         for channel_config in channel_configs:
@@ -1003,16 +999,13 @@ class VectorBus(BusABC):
             )
             configs.append(
                 {
-                    # data for use in VectorBus.__init__():
                     "interface": "vector",
                     "channel": channel_config.hw_channel,
                     "serial": channel_config.serial_number,
                     "channel_index": channel_config.channel_index,
-                    # data for use in VectorBus.set_application_config():
                     "hw_type": channel_config.hw_type,
                     "hw_index": channel_config.hw_index,
                     "hw_channel": channel_config.hw_channel,
-                    # additional information:
                     "supports_fd": bool(
                         channel_config.channel_capabilities
                         & xldefine.XL_ChannelCapabilities.XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT
@@ -1020,7 +1013,7 @@ class VectorBus(BusABC):
                     "vector_channel_config": channel_config,
                 }
             )
-        return configs  # type: ignore
+        return configs
 
     @staticmethod
     def popup_vector_hw_configuration(wait_for_finish: int = 0) -> None:
@@ -1037,7 +1030,7 @@ class VectorBus(BusABC):
     @staticmethod
     def get_application_config(
         app_name: str, app_channel: int
-    ) -> Tuple[Union[int, xldefine.XL_HardwareType], int, int]:
+    ) -> tuple[Union[int, xldefine.XL_HardwareType], int, int]:
         """Retrieve information for an application in Vector Hardware Configuration.
 
         :param app_name:
@@ -1192,6 +1185,19 @@ class VectorChannelConfig(NamedTuple):
     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(
@@ -1243,14 +1249,14 @@ def _read_bus_params_from_c_struct(
     )
 
 
-def get_channel_configs() -> List[VectorChannelConfig]:
+def get_channel_configs() -> list[VectorChannelConfig]:
     """Read channel properties from Vector XL API."""
     try:
         driver_config = _get_xl_driver_config()
     except VectorError:
         return []
 
-    channel_list: List[VectorChannelConfig] = []
+    channel_list: list[VectorChannelConfig] = []
     for i in range(driver_config.channelCount):
         xlcc: xlclass.XLchannelConfig = driver_config.channel[i]
         vcc = VectorChannelConfig(
diff -pruN 4.5.0-1/can/interfaces/vector/exceptions.py 4.6.1-1/can/interfaces/vector/exceptions.py
--- 4.5.0-1/can/interfaces/vector/exceptions.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/vector/exceptions.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,10 +1,14 @@
 """Exception/error declarations for the vector interface."""
 
+from typing import Any, Optional, Union
+
 from can import CanError, CanInitializationError, CanOperationError
 
 
 class VectorError(CanError):
-    def __init__(self, error_code, error_string, function):
+    def __init__(
+        self, error_code: Optional[int], error_string: str, function: str
+    ) -> None:
         super().__init__(
             message=f"{function} failed ({error_string})", error_code=error_code
         )
@@ -12,7 +16,7 @@ class VectorError(CanError):
         # keep reference to args for pickling
         self._args = error_code, error_string, function
 
-    def __reduce__(self):
+    def __reduce__(self) -> Union[str, tuple[Any, ...]]:
         return type(self), self._args, {}
 
 
diff -pruN 4.5.0-1/can/interfaces/virtual.py 4.6.1-1/can/interfaces/virtual.py
--- 4.5.0-1/can/interfaces/virtual.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/interfaces/virtual.py	2025-08-12 07:32:01.000000000 +0000
@@ -12,23 +12,18 @@ import time
 from copy import deepcopy
 from random import randint
 from threading import RLock
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
+from typing import Any, Final, Optional
 
 from can import CanOperationError
 from can.bus import BusABC, CanProtocol
 from can.message import Message
-from can.typechecking import AutoDetectedConfig
+from can.typechecking import AutoDetectedConfig, Channel
 
 logger = logging.getLogger(__name__)
 
-
 # Channels are lists of queues, one for each connection
-if TYPE_CHECKING:
-    # https://mypy.readthedocs.io/en/stable/runtime_troubles.html#using-classes-that-are-generic-in-stubs-but-not-at-runtime
-    channels: Dict[Optional[Any], List[queue.Queue[Message]]] = {}
-else:
-    channels = {}
-channels_lock = RLock()
+channels: Final[dict[Channel, list[queue.Queue[Message]]]] = {}
+channels_lock: Final = RLock()
 
 
 class VirtualBus(BusABC):
@@ -58,7 +53,7 @@ class VirtualBus(BusABC):
 
     def __init__(
         self,
-        channel: Any = None,
+        channel: Channel = "channel-0",
         receive_own_messages: bool = False,
         rx_queue_size: int = 0,
         preserve_timestamps: bool = False,
@@ -71,9 +66,9 @@ class VirtualBus(BusABC):
         bus by virtual instances constructed with the same channel identifier.
 
         :param channel: The channel identifier. This parameter can be an
-            arbitrary value. The bus instance will be able to see messages
-            from other virtual bus instances that were created with the same
-            value.
+            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
@@ -125,7 +120,7 @@ class VirtualBus(BusABC):
 
     def _recv_internal(
         self, timeout: Optional[float]
-    ) -> Tuple[Optional[Message], bool]:
+    ) -> tuple[Optional[Message], bool]:
         self._check_if_open()
         try:
             msg = self.queue.get(block=True, timeout=timeout)
@@ -168,7 +163,7 @@ class VirtualBus(BusABC):
                     del channels[self.channel_id]
 
     @staticmethod
-    def _detect_available_configs() -> List[AutoDetectedConfig]:
+    def _detect_available_configs() -> list[AutoDetectedConfig]:
         """
         Returns all currently used channels as well as
         one other currently unused channel.
@@ -183,7 +178,7 @@ class VirtualBus(BusABC):
             available_channels = list(channels.keys())
 
         # find a currently unused channel
-        def get_extra():
+        def get_extra() -> str:
             return f"channel-{randint(0, 9999)}"
 
         extra = get_extra()
diff -pruN 4.5.0-1/can/io/__init__.py 4.6.1-1/can/io/__init__.py
--- 4.5.0-1/can/io/__init__.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/io/__init__.py	2025-08-12 07:32:01.000000000 +0000
@@ -4,22 +4,22 @@ and Writers based off the file extension
 """
 
 __all__ = [
+    "MESSAGE_READERS",
+    "MESSAGE_WRITERS",
     "ASCReader",
     "ASCWriter",
-    "BaseRotatingLogger",
     "BLFReader",
     "BLFWriter",
-    "CanutilsLogReader",
-    "CanutilsLogWriter",
+    "BaseRotatingLogger",
     "CSVReader",
     "CSVWriter",
-    "Logger",
+    "CanutilsLogReader",
+    "CanutilsLogWriter",
     "LogReader",
-    "MESSAGE_READERS",
-    "MESSAGE_WRITERS",
-    "MessageSync",
+    "Logger",
     "MF4Reader",
     "MF4Writer",
+    "MessageSync",
     "Printer",
     "SizedRotatingLogger",
     "SqliteReader",
diff -pruN 4.5.0-1/can/io/asc.py 4.6.1-1/can/io/asc.py
--- 4.5.0-1/can/io/asc.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/io/asc.py	2025-08-12 07:32:01.000000000 +0000
@@ -8,8 +8,9 @@ Example .asc files:
 
 import logging
 import re
+from collections.abc import Generator
 from datetime import datetime
-from typing import Any, Dict, Final, Generator, Optional, TextIO, Union
+from typing import Any, Final, Optional, TextIO, Union
 
 from ..message import Message
 from ..typechecking import StringPathLike
@@ -38,8 +39,6 @@ class ASCReader(TextIOMessageReader):
     bus statistics, J1939 Transport Protocol messages) is ignored.
     """
 
-    file: TextIO
-
     def __init__(
         self,
         file: Union[StringPathLike, TextIO],
@@ -153,7 +152,7 @@ class ASCReader(TextIOMessageReader):
 
         raise ValueError(f"Incompatible datetime string {datetime_string}")
 
-    def _extract_can_id(self, str_can_id: str, msg_kwargs: Dict[str, Any]) -> None:
+    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)
@@ -169,7 +168,7 @@ class ASCReader(TextIOMessageReader):
         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]
+        self, data_str: str, data_length: int, msg_kwargs: dict[str, Any]
     ) -> None:
         frame = bytearray()
         data = data_str.split()
@@ -178,7 +177,7 @@ class ASCReader(TextIOMessageReader):
         msg_kwargs["data"] = frame
 
     def _process_classic_can_frame(
-        self, line: str, msg_kwargs: Dict[str, Any]
+        self, line: str, msg_kwargs: dict[str, Any]
     ) -> Message:
         # CAN error frame
         if line.strip()[0:10].lower() == "errorframe":
@@ -213,7 +212,7 @@ class ASCReader(TextIOMessageReader):
 
         return Message(**msg_kwargs)
 
-    def _process_fd_can_frame(self, line: str, msg_kwargs: Dict[str, Any]) -> Message:
+    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
@@ -285,7 +284,7 @@ class ASCReader(TextIOMessageReader):
                 # J1939 message or some other unsupported event
                 continue
 
-            msg_kwargs: Dict[str, Union[float, bool, int]] = {}
+            msg_kwargs: dict[str, Union[float, bool, int]] = {}
             try:
                 _timestamp, channel, rest_of_message = line.split(None, 2)
                 timestamp = float(_timestamp) + self.start_time
@@ -321,8 +320,6 @@ class ASCWriter(TextIOMessageWriter):
     It the first message does not have a timestamp, it is set to zero.
     """
 
-    file: TextIO
-
     FORMAT_MESSAGE = "{channel}  {id:<15} {dir:<4} {dtype} {data}"
     FORMAT_MESSAGE_FD = " ".join(
         [
diff -pruN 4.5.0-1/can/io/blf.py 4.6.1-1/can/io/blf.py
--- 4.5.0-1/can/io/blf.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/io/blf.py	2025-08-12 07:32:01.000000000 +0000
@@ -17,14 +17,16 @@ import logging
 import struct
 import time
 import zlib
-from typing import Any, BinaryIO, Generator, List, Optional, Tuple, Union, cast
+from collections.abc import Generator, Iterator
+from decimal import Decimal
+from typing import Any, BinaryIO, Optional, Union, cast
 
 from ..message import Message
 from ..typechecking import StringPathLike
 from ..util import channel2int, dlc2len, len2dlc
-from .generic import BinaryIOMessageReader, FileIOMessageWriter
+from .generic import BinaryIOMessageReader, BinaryIOMessageWriter
 
-TSystemTime = Tuple[int, int, int, int, int, int, int, int]
+TSystemTime = tuple[int, int, int, int, int, int, int, int]
 
 
 class BLFParseError(Exception):
@@ -98,12 +100,15 @@ DIR = 0x1
 TIME_TEN_MICS = 0x00000001
 TIME_ONE_NANS = 0x00000002
 
+TIME_TEN_MICS_FACTOR = Decimal("1e-5")
+TIME_ONE_NANS_FACTOR = Decimal("1e-9")
 
-def timestamp_to_systemtime(timestamp: float) -> TSystemTime:
+
+def timestamp_to_systemtime(timestamp: Optional[float]) -> 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(round(timestamp, 3))
+    t = datetime.datetime.fromtimestamp(round(timestamp, 3), tz=datetime.timezone.utc)
     return (
         t.year,
         t.month,
@@ -126,6 +131,7 @@ def systemtime_to_timestamp(systemtime:
             systemtime[5],
             systemtime[6],
             systemtime[7] * 1000,
+            tzinfo=datetime.timezone.utc,
         )
         return t.timestamp()
     except ValueError:
@@ -140,8 +146,6 @@ class BLFReader(BinaryIOMessageReader):
     silently ignored.
     """
 
-    file: BinaryIO
-
     def __init__(
         self,
         file: Union[StringPathLike, BinaryIO],
@@ -160,8 +164,12 @@ class BLFReader(BinaryIOMessageReader):
         self.file_size = header[10]
         self.uncompressed_size = header[11]
         self.object_count = header[12]
-        self.start_timestamp = systemtime_to_timestamp(cast(TSystemTime, header[14:22]))
-        self.stop_timestamp = systemtime_to_timestamp(cast(TSystemTime, 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""
@@ -196,7 +204,7 @@ class BLFReader(BinaryIOMessageReader):
                 yield from self._parse_container(data)
         self.stop()
 
-    def _parse_container(self, data):
+    def _parse_container(self, data: bytes) -> Iterator[Message]:
         if self._tail:
             data = b"".join((self._tail, data))
         try:
@@ -207,7 +215,7 @@ class BLFReader(BinaryIOMessageReader):
         # Save the remaining data that could not be processed
         self._tail = data[self._pos :]
 
-    def _parse_data(self, data):
+    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
@@ -239,7 +247,7 @@ class BLFReader(BinaryIOMessageReader):
                 raise BLFParseError("Could not find next object") from None
             header = unpack_obj_header_base(data, pos)
             # print(header)
-            signature, _, header_version, obj_size, obj_type = header
+            signature, header_size, header_version, obj_size, obj_type = header
             if signature != b"LOBJ":
                 raise BLFParseError()
 
@@ -263,8 +271,8 @@ class BLFReader(BinaryIOMessageReader):
                 continue
 
             # Calculate absolute timestamp in seconds
-            factor = 1e-5 if flags == 1 else 1e-9
-            timestamp = timestamp * factor + start_timestamp
+            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)
@@ -334,10 +342,20 @@ class BLFReader(BinaryIOMessageReader):
                     _,
                     _,
                     direction,
-                    _,
+                    ext_data_offset,
                     _,
                 ) = unpack_can_fd_64_msg(data, pos)
-                pos += can_fd_64_msg_size
+
+                # :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,
@@ -348,20 +366,18 @@ class BLFReader(BinaryIOMessageReader):
                     bitrate_switch=bool(fd_flags & 0x2000),
                     error_state_indicator=bool(fd_flags & 0x4000),
                     dlc=dlc2len(dlc),
-                    data=data[pos : pos + valid_bytes],
+                    data=msg_data,
                     channel=channel - 1,
                 )
 
             pos = next_pos
 
 
-class BLFWriter(FileIOMessageWriter):
+class BLFWriter(BinaryIOMessageWriter):
     """
     Logs CAN data to a Binary Logging File compatible with Vector's tools.
     """
 
-    file: BinaryIO
-
     #: Max log container size of uncompressed data
     max_container_size = 128 * 1024
 
@@ -392,18 +408,16 @@ class BLFWriter(FileIOMessageWriter):
             Z_DEFAULT_COMPRESSION represents a default compromise between
             speed and compression (currently equivalent to level 6).
         """
-        mode = "rb+" if append else "wb"
         try:
-            super().__init__(file, mode=mode)
+            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
-            mode = "wb"
-            super().__init__(file, mode=mode)
+            super().__init__(file, mode="wb")
         assert self.file is not None
         self.channel = channel
         self.compression_level = compression_level
-        self._buffer: List[bytes] = []
+        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):
@@ -417,10 +431,10 @@ class BLFWriter(FileIOMessageWriter):
             self.uncompressed_size = header[11]
             self.object_count = header[12]
             self.start_timestamp: Optional[float] = systemtime_to_timestamp(
-                cast(TSystemTime, header[14:22])
+                cast("TSystemTime", header[14:22])
             )
             self.stop_timestamp: Optional[float] = systemtime_to_timestamp(
-                cast(TSystemTime, header[22:30])
+                cast("TSystemTime", header[22:30])
             )
             # Jump to the end of the file
             self.file.seek(0, 2)
@@ -432,7 +446,7 @@ class BLFWriter(FileIOMessageWriter):
             # Write a default header which will be updated when stopped
             self._write_header(FILE_HEADER_SIZE)
 
-    def _write_header(self, filesize):
+    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])
@@ -442,7 +456,7 @@ class BLFWriter(FileIOMessageWriter):
         # Pad to header size
         self.file.write(b"\x00" * (FILE_HEADER_SIZE - FILE_HEADER_STRUCT.size))
 
-    def on_message_received(self, msg):
+    def on_message_received(self, msg: Message) -> None:
         channel = channel2int(msg.channel)
         if channel is None:
             channel = self.channel
@@ -494,7 +508,7 @@ class BLFWriter(FileIOMessageWriter):
             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: Optional[float] = None) -> None:
         """Add an arbitrary message to the log file as a global marker.
 
         :param str text:
@@ -505,21 +519,26 @@ class BLFWriter(FileIOMessageWriter):
         """
         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)
+            0, 0xFFFFFF, 0xFF3300, 0, len(encoded), len(marker), len(comment)
         )
-        self._add_object(GLOBAL_MARKER, data + text + marker + comment, timestamp)
+        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: Optional[float] = 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
@@ -541,7 +560,7 @@ class BLFWriter(FileIOMessageWriter):
         if self._buffer_size >= self.max_container_size:
             self._flush()
 
-    def _flush(self):
+    def _flush(self) -> None:
         """Compresses and writes data in the buffer to file."""
         if self.file.closed:
             return
@@ -555,7 +574,7 @@ class BLFWriter(FileIOMessageWriter):
         self._buffer = [tail]
         self._buffer_size = len(tail)
         if not self.compression_level:
-            data = uncompressed_data
+            data: "Union[bytes, memoryview[int]]" = uncompressed_data  # noqa: UP037
             method = NO_COMPRESSION
         else:
             data = zlib.compress(uncompressed_data, self.compression_level)
@@ -578,7 +597,7 @@ class BLFWriter(FileIOMessageWriter):
         """Return an estimate of the current file size in bytes."""
         return self.file.tell() + self._buffer_size
 
-    def stop(self):
+    def stop(self) -> None:
         """Stops logging and closes the file."""
         self._flush()
         if self.file.seekable():
diff -pruN 4.5.0-1/can/io/canutils.py 4.6.1-1/can/io/canutils.py
--- 4.5.0-1/can/io/canutils.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/io/canutils.py	2025-08-12 07:32:01.000000000 +0000
@@ -5,7 +5,8 @@ It is is compatible with "candump -L" fr
 """
 
 import logging
-from typing import Any, Generator, TextIO, Union
+from collections.abc import Generator
+from typing import Any, Optional, TextIO, Union
 
 from can.message import Message
 
@@ -33,8 +34,6 @@ class CanutilsLogReader(TextIOMessageRea
         ``(0.0) vcan0 001#8d00100100820100``
     """
 
-    file: TextIO
-
     def __init__(
         self,
         file: Union[StringPathLike, TextIO],
@@ -147,13 +146,12 @@ class CanutilsLogWriter(TextIOMessageWri
         :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().__init__(file, mode=mode)
+        super().__init__(file, mode="a" if append else "w")
 
         self.channel = channel
-        self.last_timestamp = None
+        self.last_timestamp: Optional[float] = 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
@@ -165,7 +163,7 @@ class CanutilsLogWriter(TextIOMessageWri
             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():
+        if isinstance(channel, int) or (isinstance(channel, str) and channel.isdigit()):
             channel = f"can{channel}"
 
         framestr = f"({timestamp:f}) {channel}"
diff -pruN 4.5.0-1/can/io/csv.py 4.6.1-1/can/io/csv.py
--- 4.5.0-1/can/io/csv.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/io/csv.py	2025-08-12 07:32:01.000000000 +0000
@@ -10,7 +10,8 @@ TODO: This module could use https://docs
 """
 
 from base64 import b64decode, b64encode
-from typing import Any, Generator, TextIO, Union
+from collections.abc import Generator
+from typing import Any, TextIO, Union
 
 from can.message import Message
 
@@ -27,8 +28,6 @@ class CSVReader(TextIOMessageReader):
     Any line separator is accepted.
     """
 
-    file: TextIO
-
     def __init__(
         self,
         file: Union[StringPathLike, TextIO],
@@ -88,8 +87,6 @@ class CSVWriter(TextIOMessageWriter):
     Each line is terminated with a platform specific line separator.
     """
 
-    file: TextIO
-
     def __init__(
         self,
         file: Union[StringPathLike, TextIO],
@@ -105,8 +102,7 @@ class CSVWriter(TextIOMessageWriter):
                             the file is truncated and starts with a newly
                             written header line
         """
-        mode = "a" if append else "w"
-        super().__init__(file, mode=mode)
+        super().__init__(file, mode="a" if append else "w")
 
         # Write a header row
         if not append:
diff -pruN 4.5.0-1/can/io/generic.py 4.6.1-1/can/io/generic.py
--- 4.5.0-1/can/io/generic.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/io/generic.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,131 +1,260 @@
-"""Contains generic base classes for file IO."""
+"""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.
+"""
 
-import gzip
 import locale
-from abc import ABCMeta
+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,
-    ContextManager,
-    Iterable,
+    Generic,
     Literal,
     Optional,
     TextIO,
-    Type,
+    TypeVar,
     Union,
-    cast,
 )
 
 from typing_extensions import Self
 
-from .. import typechecking
 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,
+    )
 
-class BaseIOHandler(ContextManager, metaclass=ABCMeta):
-    """A generic file handler that can be used for reading and writing.
 
-    Can be used as a context manager.
+#: type parameter used in generic classes :class:`MessageReader` and :class:`MessageWriter`
+_IoTypeVar = TypeVar("_IoTypeVar", bound=FileLike)
 
-    :attr file:
-        the file-like object that is kept internally, or `None` if none
-        was opened
-    """
 
-    file: Optional[typechecking.FileLike]
+class MessageWriter(AbstractContextManager["MessageWriter"], Listener, ABC):
+    """Abstract base class for all CAN message writers.
 
-    def __init__(
-        self,
-        file: Optional[typechecking.AcceptedIOType],
-        mode: str = "rt",
-        **kwargs: Any,
-    ) -> None:
-        """
-        :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 mode: the mode that should be used to open the file, see
-                     :func:`open`, ignored if *file* is `None`
-        """
-        if file is None or (hasattr(file, "read") and hasattr(file, "write")):
-            # file is None or some file-like object
-            self.file = cast(Optional[typechecking.FileLike], file)
-        else:
-            encoding: Optional[str] = (
-                None
-                if "b" in mode
-                else kwargs.get("encoding", locale.getpreferredencoding(False))
-            )
-            # pylint: disable=consider-using-with
-            # file is some path-like object
-            self.file = cast(
-                typechecking.FileLike,
-                open(cast(typechecking.StringPathLike, file), mode, encoding=encoding),
-            )
+    This class serves as a foundation for implementing different message writer formats.
+    It combines context manager capabilities with the message listener interface.
 
-        # for multiple inheritance
-        super().__init__()
+    :param file: Path-like object or string representing the output file location
+    :param kwargs: Additional keyword arguments for specific writer implementations
+    """
+
+    @abstractmethod
+    def __init__(self, file: StringPathLike, **kwargs: Any) -> None:
+        pass
+
+    @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: Optional[Type[BaseException]],
-        exc_val: Optional[BaseException],
-        exc_tb: Optional[TracebackType],
+        exc_type: Optional[type[BaseException]],
+        exc_value: Optional[BaseException],
+        traceback: Optional[TracebackType],
     ) -> 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
+        """
+
+
+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: Union[StringPathLike, _IoTypeVar], **kwargs: Any) -> None:
+        pass
+
     def stop(self) -> None:
-        """Closes the underlying file-like object and flushes it, if it was opened in write mode."""
-        if self.file is not None:
-            # this also implies a flush()
-            self.file.close()
+        """Close the file and stop writing."""
+        self.file.close()
 
+    def file_size(self) -> int:
+        """Get the current file size."""
+        return self.file.tell()
 
-class MessageWriter(BaseIOHandler, Listener, metaclass=ABCMeta):
-    """The base class for all writers."""
 
-    file: Optional[typechecking.FileLike]
+class TextIOMessageWriter(FileIOMessageWriter[Union[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: Union[StringPathLike, TextIO, TextIOWrapper],
+        mode: "Union[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 FileIOMessageWriter(MessageWriter, metaclass=ABCMeta):
-    """A specialized base class for all writers with file descriptors."""
 
-    file: typechecking.FileLike
+class BinaryIOMessageWriter(FileIOMessageWriter[Union[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__(
-        self, file: typechecking.AcceptedIOType, mode: str = "wt", **kwargs: Any
+        self,
+        file: Union[StringPathLike, BinaryIO, BufferedIOBase],
+        mode: "Union[OpenBinaryModeUpdating, OpenBinaryModeWriting]" = "wb",
+        **kwargs: Any,
     ) -> None:
-        # Not possible with the type signature, but be verbose for user-friendliness
-        if file is None:
-            raise ValueError("The given file cannot be 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
 
-        super().__init__(file, mode, **kwargs)
 
-    def file_size(self) -> int:
-        """Return an estimate of the current file size in bytes."""
-        return self.file.tell()
+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.
 
-class TextIOMessageWriter(FileIOMessageWriter, metaclass=ABCMeta):
-    file: TextIO
+    :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
 
-class BinaryIOMessageWriter(FileIOMessageWriter, metaclass=ABCMeta):
-    file: Union[BinaryIO, gzip.GzipFile]
+    @abstractmethod
+    def stop(self) -> None:
+        """Stop reading messages and cleanup any resources."""
 
+    def __enter__(self) -> Self:
+        return self
 
-class MessageReader(BaseIOHandler, Iterable[Message], metaclass=ABCMeta):
-    """The base class for all readers."""
+    def __exit__(
+        self,
+        exc_type: Optional[type[BaseException]],
+        exc_value: Optional[BaseException],
+        traceback: Optional[TracebackType],
+    ) -> 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.
 
-class TextIOMessageReader(MessageReader, metaclass=ABCMeta):
-    file: TextIO
+    :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
+    """
+
+    file: _IoTypeVar
+
+    @abstractmethod
+    def __init__(self, file: Union[StringPathLike, _IoTypeVar], **kwargs: Any) -> None:
+        pass
+
+    def stop(self) -> None:
+        self.file.close()
 
-class BinaryIOMessageReader(MessageReader, metaclass=ABCMeta):
-    file: Union[BinaryIO, gzip.GzipFile]
+
+class TextIOMessageReader(FileIOMessageReader[Union[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: Union[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[Union[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__(
+        self,
+        file: Union[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 -pruN 4.5.0-1/can/io/logger.py 4.6.1-1/can/io/logger.py
--- 4.5.0-1/can/io/logger.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/io/logger.py	2025-08-12 07:32:01.000000000 +0000
@@ -12,21 +12,16 @@ from typing import (
     Any,
     Callable,
     ClassVar,
-    Dict,
     Final,
     Literal,
     Optional,
-    Set,
-    Tuple,
-    Type,
-    cast,
 )
 
 from typing_extensions import Self
 
 from .._entry_points import read_entry_points
 from ..message import Message
-from ..typechecking import AcceptedIOType, FileLike, StringPathLike
+from ..typechecking import StringPathLike
 from .asc import ASCWriter
 from .blf import BLFWriter
 from .canutils import CanutilsLogWriter
@@ -35,6 +30,8 @@ from .generic import (
     BinaryIOMessageWriter,
     FileIOMessageWriter,
     MessageWriter,
+    SizedMessageWriter,
+    TextIOMessageWriter,
 )
 from .mf4 import MF4Writer
 from .printer import Printer
@@ -43,7 +40,7 @@ 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]]] = {
+MESSAGE_WRITERS: Final[dict[str, type[MessageWriter]]] = {
     ".asc": ASCWriter,
     ".blf": BLFWriter,
     ".csv": CSVWriter,
@@ -66,7 +63,7 @@ def _update_writer_plugins() -> None:
             MESSAGE_WRITERS[entry_point.key] = writer_class
 
 
-def _get_logger_for_suffix(suffix: str) -> Type[MessageWriter]:
+def _get_logger_for_suffix(suffix: str) -> type[MessageWriter]:
     try:
         return MESSAGE_WRITERS[suffix]
     except KeyError:
@@ -75,9 +72,7 @@ def _get_logger_for_suffix(suffix: str)
         ) from None
 
 
-def _compress(
-    filename: StringPathLike, **kwargs: Any
-) -> Tuple[Type[MessageWriter], FileLike]:
+def _compress(filename: StringPathLike, **kwargs: Any) -> FileIOMessageWriter[Any]:
     """
     Return the suffix and io object of the decompressed file.
     File will automatically recompress upon close.
@@ -97,11 +92,18 @@ def _compress(
     append = kwargs.get("append", False)
 
     if issubclass(logger_type, BinaryIOMessageWriter):
-        mode = "ab" if append else "wb"
-    else:
-        mode = "at" if append else "wt"
+        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
+        )
 
-    return logger_type, gzip.open(filename, mode)
+    raise ValueError(
+        f"The file type {real_suffix} is currently incompatible with gzip."
+    )
 
 
 def Logger(  # noqa: N802
@@ -147,12 +149,11 @@ def Logger(  # noqa: N802
     _update_writer_plugins()
 
     suffix = pathlib.PurePath(filename).suffix.lower()
-    file_or_filename: AcceptedIOType = filename
     if suffix == ".gz":
-        logger_type, file_or_filename = _compress(filename, **kwargs)
-    else:
-        logger_type = _get_logger_for_suffix(suffix)
-    return logger_type(file=file_or_filename, **kwargs)
+        return _compress(filename, **kwargs)
+
+    logger_type = _get_logger_for_suffix(suffix)
+    return logger_type(file=filename, **kwargs)
 
 
 class BaseRotatingLogger(MessageWriter, ABC):
@@ -171,7 +172,7 @@ class BaseRotatingLogger(MessageWriter,
     Subclasses must set the `_writer` attribute upon initialization.
     """
 
-    _supported_formats: ClassVar[Set[str]] = set()
+    _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
@@ -187,13 +188,11 @@ class BaseRotatingLogger(MessageWriter,
     rollover_count: int = 0
 
     def __init__(self, **kwargs: Any) -> None:
-        super().__init__(**{**kwargs, "file": None})
-
         self.writer_kwargs = kwargs
 
     @property
     @abstractmethod
-    def writer(self) -> FileIOMessageWriter:
+    def writer(self) -> MessageWriter:
         """This attribute holds an instance of a writer class which manages the actual file IO."""
         raise NotImplementedError
 
@@ -247,7 +246,7 @@ class BaseRotatingLogger(MessageWriter,
 
         self.writer.on_message_received(msg)
 
-    def _get_new_writer(self, filename: StringPathLike) -> FileIOMessageWriter:
+    def _get_new_writer(self, filename: StringPathLike) -> MessageWriter:
         """Instantiate a new writer.
 
         .. note::
@@ -265,10 +264,7 @@ class BaseRotatingLogger(MessageWriter,
             if suffix not in self._supported_formats:
                 continue
             logger = Logger(filename=filename, **self.writer_kwargs)
-            if isinstance(logger, FileIOMessageWriter):
-                return logger
-            elif isinstance(logger, Printer) and logger.file is not None:
-                return cast(FileIOMessageWriter, logger)
+            return logger
 
         raise ValueError(
             f'The log format of "{pathlib.Path(filename).name}" '
@@ -290,11 +286,12 @@ class BaseRotatingLogger(MessageWriter,
 
     def __exit__(
         self,
-        exc_type: Optional[Type[BaseException]],
+        exc_type: Optional[type[BaseException]],
         exc_val: Optional[BaseException],
         exc_tb: Optional[TracebackType],
     ) -> Literal[False]:
-        return self.writer.__exit__(exc_type, exc_val, exc_tb)
+        self.stop()
+        return False
 
     @abstractmethod
     def should_rollover(self, msg: Message) -> bool:
@@ -347,7 +344,7 @@ class SizedRotatingLogger(BaseRotatingLo
     :meth:`~can.Listener.stop` is called.
     """
 
-    _supported_formats: ClassVar[Set[str]] = {".asc", ".blf", ".csv", ".log", ".txt"}
+    _supported_formats: ClassVar[set[str]] = {".asc", ".blf", ".csv", ".log", ".txt"}
 
     def __init__(
         self,
@@ -370,18 +367,25 @@ class SizedRotatingLogger(BaseRotatingLo
 
         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) -> FileIOMessageWriter:
+    def writer(self) -> SizedMessageWriter:
         return self._writer
 
     def should_rollover(self, msg: Message) -> bool:
         if self.max_bytes <= 0:
             return False
 
-        if self.writer.file_size() >= self.max_bytes:
-            return True
+        file_size = self.writer.file_size()
+        if file_size is None:
+            return False
 
-        return False
+        return file_size >= self.max_bytes
 
     def do_rollover(self) -> None:
         if self.writer:
diff -pruN 4.5.0-1/can/io/mf4.py 4.6.1-1/can/io/mf4.py
--- 4.5.0-1/can/io/mf4.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/io/mf4.py	2025-08-12 07:32:01.000000000 +0000
@@ -8,11 +8,12 @@ the ASAM MDF standard (see https://www.a
 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, Dict, Generator, Iterator, List, Optional, Union, cast
+from typing import Any, BinaryIO, Optional, Union, cast
 
 from ..message import Message
 from ..typechecking import StringPathLike
@@ -124,7 +125,7 @@ class MF4Writer(BinaryIOMessageWriter):
 
         super().__init__(file, mode="w+b")
         now = datetime.now()
-        self._mdf = cast(MDF4, MDF(version="4.10"))
+        self._mdf = cast("MDF4", MDF(version="4.10"))
         self._mdf.header.start_time = now
         self.last_timestamp = self._start_time = now.timestamp()
 
@@ -184,7 +185,10 @@ class MF4Writer(BinaryIOMessageWriter):
     def file_size(self) -> 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
+        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)
@@ -270,7 +274,7 @@ class MF4Writer(BinaryIOMessageWriter):
         self._rtr_buffer = np.zeros(1, dtype=RTR_DTYPE)
 
 
-class FrameIterator(metaclass=abc.ABCMeta):
+class FrameIterator(abc.ABC):
     """
     Iterator helper class for common handling among CAN DataFrames, ErrorFrames and RemoteFrames.
     """
@@ -337,7 +341,7 @@ class MF4Reader(BinaryIOMessageReader):
                 for i in range(len(data)):
                     data_length = int(data["CAN_DataFrame.DataLength"][i])
 
-                    kv: Dict[str, Any] = {
+                    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][
@@ -348,7 +352,10 @@ class MF4Reader(BinaryIOMessageReader):
                     if "CAN_DataFrame.BusChannel" in names:
                         kv["channel"] = int(data["CAN_DataFrame.BusChannel"][i])
                     if "CAN_DataFrame.Dir" in names:
-                        kv["is_rx"] = int(data["CAN_DataFrame.Dir"][i]) == 0
+                        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:
@@ -375,7 +382,7 @@ class MF4Reader(BinaryIOMessageReader):
                 names = data.samples[0].dtype.names
 
                 for i in range(len(data)):
-                    kv: Dict[str, Any] = {
+                    kv: dict[str, Any] = {
                         "timestamp": float(data.timestamps[i]) + self._start_timestamp,
                         "is_error_frame": True,
                     }
@@ -383,7 +390,10 @@ class MF4Reader(BinaryIOMessageReader):
                     if "CAN_ErrorFrame.BusChannel" in names:
                         kv["channel"] = int(data["CAN_ErrorFrame.BusChannel"][i])
                     if "CAN_ErrorFrame.Dir" in names:
-                        kv["is_rx"] = int(data["CAN_ErrorFrame.Dir"][i]) == 0
+                        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
@@ -426,7 +436,7 @@ class MF4Reader(BinaryIOMessageReader):
                 names = data.samples[0].dtype.names
 
                 for i in range(len(data)):
-                    kv: Dict[str, Any] = {
+                    kv: dict[str, Any] = {
                         "timestamp": float(data.timestamps[i]) + self._start_timestamp,
                         "arbitration_id": int(data["CAN_RemoteFrame.ID"][i])
                         & 0x1FFFFFFF,
@@ -437,7 +447,10 @@ class MF4Reader(BinaryIOMessageReader):
                     if "CAN_RemoteFrame.BusChannel" in names:
                         kv["channel"] = int(data["CAN_RemoteFrame.BusChannel"][i])
                     if "CAN_RemoteFrame.Dir" in names:
-                        kv["is_rx"] = int(data["CAN_RemoteFrame.Dir"][i]) == 0
+                        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])
 
@@ -463,16 +476,16 @@ class MF4Reader(BinaryIOMessageReader):
 
         self._mdf: MDF4
         if isinstance(file, BufferedIOBase):
-            self._mdf = cast(MDF4, MDF(BytesIO(file.read())))
+            self._mdf = cast("MDF4", MDF(BytesIO(file.read())))
         else:
-            self._mdf = cast(MDF4, MDF(file))
+            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] = []
+        iterators: list[FrameIterator] = []
         for group_index, group in enumerate(self._mdf.groups):
             channel_group: ChannelGroup = group.channel_group
 
diff -pruN 4.5.0-1/can/io/player.py 4.6.1-1/can/io/player.py
--- 4.5.0-1/can/io/player.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/io/player.py	2025-08-12 07:32:01.000000000 +0000
@@ -7,32 +7,27 @@ in the recorded order and time intervals
 import gzip
 import pathlib
 import time
+from collections.abc import Generator, Iterable
 from typing import (
     Any,
-    Dict,
     Final,
-    Generator,
-    Iterable,
-    Tuple,
-    Type,
-    Union,
 )
 
 from .._entry_points import read_entry_points
 from ..message import Message
-from ..typechecking import AcceptedIOType, FileLike, StringPathLike
+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
+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]]] = {
+MESSAGE_READERS: Final[dict[str, type[MessageReader]]] = {
     ".asc": ASCReader,
     ".blf": BLFReader,
     ".csv": CSVReader,
@@ -54,7 +49,7 @@ def _update_reader_plugins() -> None:
             MESSAGE_READERS[entry_point.key] = reader_class
 
 
-def _get_logger_for_suffix(suffix: str) -> Type[MessageReader]:
+def _get_logger_for_suffix(suffix: str) -> type[MessageReader]:
     """Find MessageReader class for given suffix."""
     try:
         return MESSAGE_READERS[suffix]
@@ -62,24 +57,25 @@ def _get_logger_for_suffix(suffix: str)
         raise ValueError(f'No read support for unknown log format "{suffix}"') from None
 
 
-def _decompress(
-    filename: StringPathLike,
-) -> Tuple[Type[MessageReader], Union[str, FileLike]]:
+def _decompress(filename: StringPathLike, **kwargs: Any) -> MessageReader:
     """
     Return the suffix and io object of the decompressed file.
     """
     suffixes = pathlib.Path(filename).suffixes
     if len(suffixes) != 2:
         raise ValueError(
-            f"No write support for unknown log format \"{''.join(suffixes)}\""
-        ) from None
+            f"No read support for unknown log format \"{''.join(suffixes)}\""
+        )
 
     real_suffix = suffixes[-2].lower()
     reader_type = _get_logger_for_suffix(real_suffix)
 
-    mode = "rb" if issubclass(reader_type, BinaryIOMessageReader) else "rt"
+    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)
 
-    return reader_type, gzip.open(filename, mode)
+    raise ValueError(f"No read support for unknown log format \"{''.join(suffixes)}\"")
 
 
 def LogReader(filename: StringPathLike, **kwargs: Any) -> MessageReader:  # noqa: N802
@@ -122,12 +118,11 @@ def LogReader(filename: StringPathLike,
     _update_reader_plugins()
 
     suffix = pathlib.PurePath(filename).suffix.lower()
-    file_or_filename: AcceptedIOType = filename
     if suffix == ".gz":
-        reader_type, file_or_filename = _decompress(filename)
-    else:
-        reader_type = _get_logger_for_suffix(suffix)
-    return reader_type(file=file_or_filename, **kwargs)
+        return _decompress(filename)
+
+    reader_type = _get_logger_for_suffix(suffix)
+    return reader_type(file=filename, **kwargs)
 
 
 class MessageSync:
diff -pruN 4.5.0-1/can/io/printer.py 4.6.1-1/can/io/printer.py
--- 4.5.0-1/can/io/printer.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/io/printer.py	2025-08-12 07:32:01.000000000 +0000
@@ -3,16 +3,18 @@ This Listener simply prints to stdout /
 """
 
 import logging
-from typing import Any, Optional, TextIO, Union, cast
+import sys
+from io import TextIOWrapper
+from typing import Any, TextIO, Union
 
 from ..message import Message
 from ..typechecking import StringPathLike
-from .generic import MessageWriter
+from .generic import TextIOMessageWriter
 
 log = logging.getLogger("can.io.printer")
 
 
-class Printer(MessageWriter):
+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 turned into a
@@ -22,11 +24,9 @@ class Printer(MessageWriter):
                          standard out
     """
 
-    file: Optional[TextIO]
-
     def __init__(
         self,
-        file: Optional[Union[StringPathLike, TextIO]] = None,
+        file: Union[StringPathLike, TextIO, TextIOWrapper] = sys.stdout,
         append: bool = False,
         **kwargs: Any,
     ) -> None:
@@ -38,18 +38,17 @@ class Printer(MessageWriter):
         :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
-        mode = "a" if append else "w"
-        super().__init__(file, mode=mode)
+        super().__init__(file, mode="a" if append else "w")
 
     def on_message_received(self, msg: Message) -> None:
-        if self.write_to_file:
-            cast(TextIO, self.file).write(str(msg) + "\n")
-        else:
-            print(msg)  # noqa: T201
+        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 None:
+        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 -pruN 4.5.0-1/can/io/sqlite.py 4.6.1-1/can/io/sqlite.py
--- 4.5.0-1/can/io/sqlite.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/io/sqlite.py	2025-08-12 07:32:01.000000000 +0000
@@ -8,7 +8,10 @@ import logging
 import sqlite3
 import threading
 import time
-from typing import Any, Generator
+from collections.abc import Generator, Iterator
+from typing import Any
+
+from typing_extensions import TypeAlias
 
 from can.listener import BufferedReader
 from can.message import Message
@@ -18,6 +21,8 @@ from .generic import MessageReader, Mess
 
 log = logging.getLogger("can.io.sqlite")
 
+_MessageTuple: TypeAlias = "tuple[float, int, bool, bool, bool, int, memoryview[int]]"
+
 
 class SqliteReader(MessageReader):
     """
@@ -48,7 +53,6 @@ class SqliteReader(MessageReader):
                      do not accept file-like objects as the `file` parameter.
                      It also runs in ``append=True`` mode all the time.
         """
-        super().__init__(file=None)
         self._conn = sqlite3.connect(file)
         self._cursor = self._conn.cursor()
         self.table_name = table_name
@@ -58,7 +62,7 @@ class SqliteReader(MessageReader):
             yield SqliteReader._assemble_message(frame_data)
 
     @staticmethod
-    def _assemble_message(frame_data):
+    def _assemble_message(frame_data: _MessageTuple) -> Message:
         timestamp, can_id, is_extended, is_remote, is_error, dlc, data = frame_data
         return Message(
             timestamp=timestamp,
@@ -70,12 +74,12 @@ class SqliteReader(MessageReader):
             data=data,
         )
 
-    def __len__(self):
+    def __len__(self) -> int:
         # this might not run in constant time
         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]
@@ -83,9 +87,8 @@ class SqliteReader(MessageReader):
         result = self._cursor.execute(f"SELECT * FROM {self.table_name}").fetchall()
         return (SqliteReader._assemble_message(frame) for frame in result)
 
-    def stop(self):
+    def stop(self) -> None:
         """Closes the connection to the database."""
-        super().stop()
         self._conn.close()
 
 
@@ -153,11 +156,10 @@ class SqliteWriter(MessageWriter, Buffer
                 f"The append argument should not be used in "
                 f"conjunction with the {self.__class__.__name__}."
             )
-        super().__init__(file=None)
+        BufferedReader.__init__(self)
         self.table_name = table_name
         self._db_filename = file
         self._stop_running_event = threading.Event()
-        self._conn = None
         self._writer_thread = threading.Thread(target=self._db_writer_thread)
         self._writer_thread.start()
         self.num_frames = 0
@@ -166,7 +168,8 @@ class SqliteWriter(MessageWriter, Buffer
             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::
@@ -174,11 +177,11 @@ class SqliteWriter(MessageWriter, Buffer
             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(
-            f"""CREATE TABLE IF NOT EXISTS {self.table_name}
+        conn.cursor().execute(
+            f"""CREATE TABLE IF NOT EXISTS {table_name}
             (
               ts REAL,
               arbitration_id INTEGER,
@@ -189,14 +192,16 @@ class SqliteWriter(MessageWriter, Buffer
               data BLOB
             )"""
         )
-        self._conn.commit()
+        conn.commit()
+
+        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:
@@ -225,10 +230,10 @@ class SqliteWriter(MessageWriter, Buffer
 
                 count = len(messages)
                 if count > 0:
-                    with self._conn:
+                    with 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
+                        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()
 
@@ -237,14 +242,13 @@ class SqliteWriter(MessageWriter, Buffer
                     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 and block.
         """
         BufferedReader.stop(self)
         self._stop_running_event.set()
         self._writer_thread.join()
-        MessageReader.stop(self)
diff -pruN 4.5.0-1/can/io/trc.py 4.6.1-1/can/io/trc.py
--- 4.5.0-1/can/io/trc.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/io/trc.py	2025-08-12 07:32:01.000000000 +0000
@@ -9,9 +9,11 @@ Version 1.1 will be implemented as it is
 
 import logging
 import os
+from collections.abc import Generator
 from datetime import datetime, timedelta, timezone
 from enum import Enum
-from typing import Any, Callable, Dict, Generator, Optional, TextIO, Tuple, Union
+from io import TextIOWrapper
+from typing import Any, Callable, Optional, TextIO, Union
 
 from ..message import Message
 from ..typechecking import StringPathLike
@@ -30,8 +32,8 @@ class TRCFileVersion(Enum):
     V2_0 = 200
     V2_1 = 201
 
-    def __ge__(self, other):
-        if self.__class__ is other.__class__:
+    def __ge__(self, other: Any) -> bool:
+        if isinstance(other, TRCFileVersion):
             return self.value >= other.value
         return NotImplemented
 
@@ -41,8 +43,6 @@ class TRCReader(TextIOMessageReader):
     Iterator of CAN messages from a TRC logging file.
     """
 
-    file: TextIO
-
     def __init__(
         self,
         file: Union[StringPathLike, TextIO],
@@ -56,13 +56,13 @@ class TRCReader(TextIOMessageReader):
         super().__init__(file, mode="r")
         self.file_version = TRCFileVersion.UNKNOWN
         self._start_time: float = 0
-        self.columns: Dict[str, int] = {}
+        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, ...]], Optional[Message]] = (
+        self._parse_cols: Callable[[tuple[str, ...]], Optional[Message]] = (
             lambda x: None
         )
 
@@ -72,7 +72,7 @@ class TRCReader(TextIOMessageReader):
             return datetime.fromtimestamp(self._start_time, timezone.utc)
         return None
 
-    def _extract_header(self):
+    def _extract_header(self) -> str:
         line = ""
         for _line in self.file:
             line = _line.strip()
@@ -140,7 +140,7 @@ class TRCReader(TextIOMessageReader):
 
         return line
 
-    def _parse_msg_v1_0(self, cols: Tuple[str, ...]) -> Optional[Message]:
+    def _parse_msg_v1_0(self, cols: tuple[str, ...]) -> Optional[Message]:
         arbit_id = cols[2]
         if arbit_id == "FFFFFFFF":
             logger.info("TRCReader: Dropping bus info line")
@@ -152,10 +152,13 @@ class TRCReader(TextIOMessageReader):
         msg.is_extended_id = len(arbit_id) > 4
         msg.channel = 1
         msg.dlc = int(cols[3])
-        msg.data = bytearray([int(cols[i + 4], 16) for i in range(msg.dlc)])
+        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, ...]) -> Optional[Message]:
+    def _parse_msg_v1_1(self, cols: tuple[str, ...]) -> Optional[Message]:
         arbit_id = cols[3]
 
         msg = Message()
@@ -164,11 +167,14 @@ class TRCReader(TextIOMessageReader):
         msg.is_extended_id = len(arbit_id) > 4
         msg.channel = 1
         msg.dlc = int(cols[4])
-        msg.data = bytearray([int(cols[i + 5], 16) for i in range(msg.dlc)])
+        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, ...]) -> Optional[Message]:
+    def _parse_msg_v1_3(self, cols: tuple[str, ...]) -> Optional[Message]:
         arbit_id = cols[4]
 
         msg = Message()
@@ -177,11 +183,14 @@ class TRCReader(TextIOMessageReader):
         msg.is_extended_id = len(arbit_id) > 4
         msg.channel = int(cols[2])
         msg.dlc = int(cols[6])
-        msg.data = bytearray([int(cols[i + 7], 16) for i in range(msg.dlc)])
+        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, ...]) -> Optional[Message]:
+    def _parse_msg_v2_x(self, cols: tuple[str, ...]) -> Optional[Message]:
         type_ = cols[self.columns["T"]]
         bus = self.columns.get("B", None)
 
@@ -199,7 +208,8 @@ class TRCReader(TextIOMessageReader):
         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
-        if 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"}
@@ -208,7 +218,7 @@ class TRCReader(TextIOMessageReader):
 
         return msg
 
-    def _parse_cols_v1_1(self, cols: Tuple[str, ...]) -> Optional[Message]:
+    def _parse_cols_v1_1(self, cols: tuple[str, ...]) -> Optional[Message]:
         dtype = cols[2]
         if dtype in ("Tx", "Rx"):
             return self._parse_msg_v1_1(cols)
@@ -216,7 +226,7 @@ class TRCReader(TextIOMessageReader):
             logger.info("TRCReader: Unsupported type '%s'", dtype)
             return None
 
-    def _parse_cols_v1_3(self, cols: Tuple[str, ...]) -> Optional[Message]:
+    def _parse_cols_v1_3(self, cols: tuple[str, ...]) -> Optional[Message]:
         dtype = cols[3]
         if dtype in ("Tx", "Rx"):
             return self._parse_msg_v1_3(cols)
@@ -224,9 +234,9 @@ class TRCReader(TextIOMessageReader):
             logger.info("TRCReader: Unsupported type '%s'", dtype)
             return None
 
-    def _parse_cols_v2_x(self, cols: Tuple[str, ...]) -> Optional[Message]:
+    def _parse_cols_v2_x(self, cols: tuple[str, ...]) -> Optional[Message]:
         dtype = cols[self.columns["T"]]
-        if dtype in {"DT", "FD", "FB", "FE", "BI"}:
+        if dtype in {"DT", "FD", "FB", "FE", "BI", "RR"}:
             return self._parse_msg_v2_x(cols)
         else:
             logger.info("TRCReader: Unsupported type '%s'", dtype)
@@ -275,9 +285,6 @@ class TRCWriter(TextIOMessageWriter):
     If the first message does not have a timestamp, it is set to zero.
     """
 
-    file: TextIO
-    first_timestamp: Optional[float]
-
     FORMAT_MESSAGE = (
         "{msgnr:>7} {time:13.3f} DT {channel:>2} {id:>8} {dir:>2} -  {dlc:<4} {data}"
     )
@@ -285,7 +292,7 @@ class TRCWriter(TextIOMessageWriter):
 
     def __init__(
         self,
-        file: Union[StringPathLike, TextIO],
+        file: Union[StringPathLike, TextIO, TextIOWrapper],
         channel: int = 1,
         **kwargs: Any,
     ) -> None:
@@ -307,7 +314,7 @@ class TRCWriter(TextIOMessageWriter):
         self.filepath = os.path.abspath(self.file.name)
         self.header_written = False
         self.msgnr = 0
-        self.first_timestamp = None
+        self.first_timestamp: Optional[float] = None
         self.file_version = TRCFileVersion.V2_1
         self._msg_fmt_string = self.FORMAT_MESSAGE_V1_0
         self._format_message = self._format_message_init
@@ -359,7 +366,7 @@ class TRCWriter(TextIOMessageWriter):
         ]
         self.file.writelines(line + "\n" for line in lines)
 
-    def _format_message_by_format(self, msg, channel):
+    def _format_message_by_format(self, msg: Message, channel: int) -> str:
         if msg.is_extended_id:
             arb_id = f"{msg.arbitration_id:07X}"
         else:
@@ -367,6 +374,8 @@ class TRCWriter(TextIOMessageWriter):
 
         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,
@@ -378,7 +387,7 @@ class TRCWriter(TextIOMessageWriter):
         )
         return serialized
 
-    def _format_message_init(self, msg, channel):
+    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
diff -pruN 4.5.0-1/can/listener.py 4.6.1-1/can/listener.py
--- 4.5.0-1/can/listener.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/listener.py	2025-08-12 07:32:01.000000000 +0000
@@ -5,15 +5,16 @@ This module contains the implementation
 import asyncio
 import sys
 import warnings
-from abc import ABCMeta, abstractmethod
+from abc import ABC, abstractmethod
+from collections.abc import AsyncIterator
 from queue import Empty, SimpleQueue
-from typing import Any, AsyncIterator, Optional
+from typing import Any, Optional
 
 from can.bus import BusABC
 from can.message import Message
 
 
-class Listener(metaclass=ABCMeta):
+class Listener(ABC):
     """The basic listener that can be called directly to handle some
     CAN message::
 
@@ -135,6 +136,7 @@ class AsyncBufferedReader(
     """
 
     def __init__(self, **kwargs: Any) -> None:
+        self._is_stopped: bool = False
         self.buffer: asyncio.Queue[Message]
 
         if "loop" in kwargs:
@@ -145,11 +147,12 @@ class AsyncBufferedReader(
                 stacklevel=2,
             )
             if sys.version_info < (3, 10):
-                self.buffer = asyncio.Queue(loop=kwargs["loop"])
+                self.buffer = asyncio.Queue(  # pylint: disable=unexpected-keyword-arg
+                    loop=kwargs["loop"]
+                )
                 return
 
         self.buffer = asyncio.Queue()
-        self._is_stopped: bool = False
 
     def on_message_received(self, msg: Message) -> None:
         """Append a message to the buffer.
diff -pruN 4.5.0-1/can/logconvert.py 4.6.1-1/can/logconvert.py
--- 4.5.0-1/can/logconvert.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/logconvert.py	2025-08-12 07:32:01.000000000 +0000
@@ -5,17 +5,21 @@ Convert a log file from one format to an
 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):
+    def error(self, message: str) -> NoReturn:
         self.print_help(sys.stderr)
         self.exit(errno.EINVAL, f"{self.prog}: error: {message}\n")
 
 
-def main():
+def main() -> None:
     parser = ArgumentParser(
         description="Convert a log file from one format to another.",
     )
@@ -47,7 +51,7 @@ def main():
 
     with LogReader(args.input) as reader:
         if args.file_size:
-            logger = SizedRotatingLogger(
+            logger: MessageWriter = SizedRotatingLogger(
                 base_filename=args.output, max_bytes=args.file_size
             )
         else:
diff -pruN 4.5.0-1/can/logger.py 4.6.1-1/can/logger.py
--- 4.5.0-1/can/logger.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/logger.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,211 +1,30 @@
 import argparse
 import errno
-import re
 import sys
 from datetime import datetime
 from typing import (
     TYPE_CHECKING,
-    Any,
-    Dict,
-    List,
-    Optional,
-    Sequence,
-    Tuple,
     Union,
 )
 
-import can
-from can import Bus, BusState, Logger, SizedRotatingLogger
+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
-from can.util import _dict2timing, cast_from_string
 
 if TYPE_CHECKING:
     from can.io import BaseRotatingLogger
     from can.io.generic import MessageWriter
-    from can.typechecking import CanFilter
-
-
-def _create_base_argument_parser(parser: argparse.ArgumentParser) -> None:
-    """Adds common options to an argument parser."""
-
-    parser.add_argument(
-        "-c",
-        "--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".',
-    )
-
-    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=sorted(can.VALID_INTERFACES),
-    )
-
-    parser.add_argument(
-        "-b", "--bitrate", type=int, help="Bitrate to use for the CAN bus."
-    )
-
-    parser.add_argument("--fd", help="Activate CAN-FD support", action="store_true")
-
-    parser.add_argument(
-        "--data_bitrate",
-        type=int,
-        help="Bitrate to use for the data phase in case of CAN-FD.",
-    )
-
-    parser.add_argument(
-        "--timing",
-        action=_BitTimingAction,
-        nargs=argparse.ONE_OR_MORE,
-        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.",
-        metavar="TIMING_ARG",
-    )
-
-    parser.add_argument(
-        "extra_args",
-        nargs=argparse.REMAINDER,
-        help="The remaining arguments will be used for the interface and "
-        "logger/player initialisation. "
-        "For example, `-i vector -c 1 --app-name=MyCanApp` is the equivalent "
-        "to opening the bus with `Bus('vector', channel=1, app_name='MyCanApp')",
-    )
-
-
-def _append_filter_argument(
-    parser: Union[argparse.ArgumentParser, argparse._ArgumentGroup],
-    *args: str,
-    **kwargs: Any,
-) -> None:
-    """Adds the ``filter`` option to an argument parser."""
-
-    parser.add_argument(
-        *args,
-        "--filter",
-        help="R|Space separated CAN filters for the given CAN interface:"
-        "\n      <can_id>:<can_mask> (matches when <received_can_id> & mask =="
-        " can_id & mask)"
-        "\n      <can_id>~<can_mask> (matches when <received_can_id> & 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",
-        metavar="{<can_id>:<can_mask>,<can_id>~<can_mask>}",
-        nargs=argparse.ONE_OR_MORE,
-        action=_CanFilterAction,
-        dest="can_filters",
-        **kwargs,
-    )
-
-
-def _create_bus(parsed_args: argparse.Namespace, **kwargs: Any) -> can.BusABC:
-    logging_level_names = ["critical", "error", "warning", "info", "debug", "subdebug"]
-    can.set_logging_level(logging_level_names[min(5, parsed_args.verbosity)])
-
-    config: Dict[str, Any] = {"single_handle": True, **kwargs}
-    if parsed_args.interface:
-        config["interface"] = parsed_args.interface
-    if parsed_args.bitrate:
-        config["bitrate"] = parsed_args.bitrate
-    if parsed_args.fd:
-        config["fd"] = True
-    if parsed_args.data_bitrate:
-        config["data_bitrate"] = parsed_args.data_bitrate
-    if getattr(parsed_args, "can_filters", None):
-        config["can_filters"] = parsed_args.can_filters
-    if parsed_args.timing:
-        config["timing"] = parsed_args.timing
-
-    return Bus(parsed_args.channel, **config)
-
-
-class _CanFilterAction(argparse.Action):
-    def __call__(
-        self,
-        parser: argparse.ArgumentParser,
-        namespace: argparse.Namespace,
-        values: Union[str, Sequence[Any], None],
-        option_string: Optional[str] = None,
-    ) -> None:
-        if not isinstance(values, list):
-            raise argparse.ArgumentError(None, "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(None, "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: Union[str, Sequence[Any], None],
-        option_string: Optional[str] = None,
-    ) -> None:
-        if not isinstance(values, list):
-            raise argparse.ArgumentError(None, "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(
-                    None, f"Invalid timing argument: {arg}"
-                ) from None
-
-        if not (timing := _dict2timing(timing_dict)):
-            err_msg = "Invalid --timing argument. Incomplete parameters."
-            raise argparse.ArgumentError(None, err_msg)
-
-        setattr(namespace, self.dest, timing)
-        print(timing)
-
-
-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, Union[str, int, float, bool]] = {}
-    for key, string_val in map(_split_arg, unknown_args):
-        args[key] = cast_from_string(string_val)
-    return args
 
 
 def _parse_logger_args(
-    args: List[str],
-) -> Tuple[argparse.Namespace, TAdditionalCliArgs]:
+    args: list[str],
+) -> tuple[argparse.Namespace, TAdditionalCliArgs]:
     """Parse command line arguments for logger script."""
 
     parser = argparse.ArgumentParser(
@@ -213,11 +32,9 @@ def _parse_logger_args(
         "given file.",
     )
 
-    # Generate the standard arguments:
-    # Channel, bitrate, data_bitrate, interface, app_name, CAN-FD support
-    _create_base_argument_parser(parser)
+    logger_group = parser.add_argument_group("logger arguments")
 
-    parser.add_argument(
+    logger_group.add_argument(
         "-f",
         "--file_name",
         dest="log_file",
@@ -225,7 +42,7 @@ def _parse_logger_args(
         default=None,
     )
 
-    parser.add_argument(
+    logger_group.add_argument(
         "-a",
         "--append",
         dest="append",
@@ -233,7 +50,7 @@ def _parse_logger_args(
         action="store_true",
     )
 
-    parser.add_argument(
+    logger_group.add_argument(
         "-s",
         "--file_size",
         dest="file_size",
@@ -245,7 +62,7 @@ def _parse_logger_args(
         default=None,
     )
 
-    parser.add_argument(
+    logger_group.add_argument(
         "-v",
         action="count",
         dest="verbosity",
@@ -254,9 +71,7 @@ def _parse_logger_args(
         default=2,
     )
 
-    _append_filter_argument(parser)
-
-    state_group = parser.add_mutually_exclusive_group(required=False)
+    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.",
@@ -266,6 +81,12 @@ def _parse_logger_args(
         "--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)
@@ -278,7 +99,8 @@ def _parse_logger_args(
 
 def main() -> None:
     results, additional_config = _parse_logger_args(sys.argv[1:])
-    bus = _create_bus(results, **additional_config)
+    bus = create_bus_from_namespace(results)
+    _set_logging_level_from_namespace(results)
 
     if results.active:
         bus.state = BusState.ACTIVE
diff -pruN 4.5.0-1/can/message.py 4.6.1-1/can/message.py
--- 4.5.0-1/can/message.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/message.py	2025-08-12 07:32:01.000000000 +0000
@@ -8,7 +8,7 @@ This module contains the implementation
 
 from copy import deepcopy
 from math import isinf, isnan
-from typing import Optional
+from typing import Any, Optional
 
 from . import typechecking
 
@@ -32,19 +32,19 @@ class Message:  # pylint: disable=too-ma
     """
 
     __slots__ = (
-        "timestamp",
+        "__weakref__",  # support weak references to messages
         "arbitration_id",
-        "is_extended_id",
-        "is_remote_frame",
-        "is_error_frame",
+        "bitrate_switch",
         "channel",
-        "dlc",
         "data",
+        "dlc",
+        "error_state_indicator",
+        "is_error_frame",
+        "is_extended_id",
         "is_fd",
+        "is_remote_frame",
         "is_rx",
-        "bitrate_switch",
-        "error_state_indicator",
-        "__weakref__",  # support weak references to messages
+        "timestamp",
     )
 
     def __init__(  # pylint: disable=too-many-locals, too-many-arguments
@@ -210,7 +210,7 @@ class Message:  # pylint: disable=too-ma
             error_state_indicator=self.error_state_indicator,
         )
 
-    def __deepcopy__(self, memo: dict) -> "Message":
+    def __deepcopy__(self, memo: Optional[dict[int, Any]]) -> "Message":
         return Message(
             timestamp=self.timestamp,
             arbitration_id=self.arbitration_id,
diff -pruN 4.5.0-1/can/notifier.py 4.6.1-1/can/notifier.py
--- 4.5.0-1/can/notifier.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/notifier.py	2025-08-12 07:32:01.000000000 +0000
@@ -7,7 +7,17 @@ import functools
 import logging
 import threading
 import time
-from typing import Any, Awaitable, Callable, Iterable, List, Optional, Union
+from collections.abc import Awaitable, Iterable
+from contextlib import AbstractContextManager
+from types import TracebackType
+from typing import (
+    Any,
+    Callable,
+    Final,
+    NamedTuple,
+    Optional,
+    Union,
+)
 
 from can.bus import BusABC
 from can.listener import Listener
@@ -18,10 +28,88 @@ logger = logging.getLogger("can.Notifier
 MessageRecipient = Union[Listener, Callable[[Message], Union[Awaitable[None], None]]]
 
 
-class Notifier:
+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)
+
+
+class Notifier(AbstractContextManager["Notifier"]):
+
+    _registry: Final = _NotifierRegistry()
+
     def __init__(
         self,
-        bus: Union[BusABC, List[BusABC]],
+        bus: Union[BusABC, list[BusABC]],
         listeners: Iterable[MessageRecipient],
         timeout: float = 1.0,
         loop: Optional[asyncio.AbstractEventLoop] = None,
@@ -32,61 +120,81 @@ class Notifier:
 
         .. Note::
 
-            Remember to call `stop()` after all messages are received as
+            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 listen to.
+        :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.
+        :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: List[MessageRecipient] = list(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: Optional[Exception] = None
 
-        self._running = True
+        self._stopped = False
         self._lock = threading.Lock()
 
-        self._readers: List[Union[int, threading.Thread]] = []
-        buses = self.bus if isinstance(self.bus, list) else [self.bus]
-        for each_bus in buses:
+        self._readers: list[Union[int, threading.Thread]] = []
+        _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) -> Union[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: BusABC) -> None:
         """Add a bus for notification.
 
         :param bus:
             CAN bus instance.
+        :raises ValueError:
+            If the *bus* is already assigned to an active :class:`~can.Notifier`.
         """
-        reader: int = -1
+        # 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:
-            reader = bus.fileno()
+            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 reader >= 0:
+        if self._loop is not None and file_descriptor >= 0:
             # Use bus file descriptor to watch for messages
-            self._loop.add_reader(reader, self._on_message_available, bus)
-            self._readers.append(reader)
+            self._loop.add_reader(file_descriptor, self._on_message_available, bus)
+            self._readers.append(file_descriptor)
         else:
             reader_thread = threading.Thread(
                 target=self._rx_thread,
                 args=(bus,),
-                name=f'can.notifier for bus "{bus.channel_info}"',
+                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: float = 5) -> None:
+    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.
 
@@ -94,7 +202,7 @@ class Notifier:
             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):
@@ -108,6 +216,10 @@ class Notifier:
             if hasattr(listener, "stop"):
                 listener.stop()
 
+        # 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:
@@ -118,7 +230,7 @@ class Notifier:
         else:
             handle_message = self._on_message_received
 
-        while self._running:
+        while not self._stopped:
             try:
                 if msg := bus.recv(self.timeout):
                     with self._lock:
@@ -183,3 +295,33 @@ class Notifier:
         :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: Optional[type[BaseException]],
+        exc_value: Optional[BaseException],
+        traceback: Optional[TracebackType],
+    ) -> None:
+        if not self._stopped:
+            self.stop()
diff -pruN 4.5.0-1/can/player.py 4.6.1-1/can/player.py
--- 4.5.0-1/can/player.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/player.py	2025-08-12 07:32:01.000000000 +0000
@@ -9,19 +9,29 @@ import argparse
 import errno
 import sys
 from datetime import datetime
-from typing import Iterable, cast
+from typing import TYPE_CHECKING, cast
 
-from can import LogReader, Message, MessageSync
+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,
+)
 
-from .logger import _create_base_argument_parser, _create_bus, _parse_additional_config
+if TYPE_CHECKING:
+    from collections.abc import Iterable
+
+    from can import Message
 
 
 def main() -> None:
     parser = argparse.ArgumentParser(description="Replay CAN traffic.")
 
-    _create_base_argument_parser(parser)
+    player_group = parser.add_argument_group("Player arguments")
 
-    parser.add_argument(
+    player_group.add_argument(
         "-f",
         "--file_name",
         dest="log_file",
@@ -29,7 +39,7 @@ def main() -> None:
         default=None,
     )
 
-    parser.add_argument(
+    player_group.add_argument(
         "-v",
         action="count",
         dest="verbosity",
@@ -38,27 +48,27 @@ def main() -> None:
         default=2,
     )
 
-    parser.add_argument(
+    player_group.add_argument(
         "--ignore-timestamps",
         dest="timestamps",
         help="""Ignore timestamps (send all frames immediately with minimum gap between frames)""",
         action="store_false",
     )
 
-    parser.add_argument(
+    player_group.add_argument(
         "--error-frames",
         help="Also send error frames to the interface.",
         action="store_true",
     )
 
-    parser.add_argument(
+    player_group.add_argument(
         "-g",
         "--gap",
         type=float,
         help="<s> minimum time between replayed frames",
         default=0.0001,
     )
-    parser.add_argument(
+    player_group.add_argument(
         "-s",
         "--skip",
         type=float,
@@ -66,13 +76,19 @@ def main() -> None:
         help="<s> skip gaps greater than 's' seconds",
     )
 
-    parser.add_argument(
+    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)
@@ -81,14 +97,15 @@ def main() -> None:
     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
 
     error_frames = results.error_frames
 
-    with _create_bus(results, **additional_config) as bus:
+    with create_bus_from_namespace(results) as bus:
         with LogReader(results.infile, **additional_config) as reader:
             in_sync = MessageSync(
-                cast(Iterable[Message], reader),
+                cast("Iterable[Message]", reader),
                 timestamps=results.timestamps,
                 gap=results.gap,
                 skip=results.skip,
diff -pruN 4.5.0-1/can/thread_safe_bus.py 4.6.1-1/can/thread_safe_bus.py
--- 4.5.0-1/can/thread_safe_bus.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/thread_safe_bus.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,4 +1,12 @@
+from contextlib import nullcontext
 from threading import RLock
+from typing import Any, Optional
+
+from can import typechecking
+from can.bus import BusABC, BusState, CanProtocol
+from can.message import Message
+
+from .interface import Bus
 
 try:
     # Only raise an exception on instantiation but allow module
@@ -10,10 +18,6 @@ except ImportError as exc:
     ObjectProxy = object
     import_exc = exc
 
-from contextlib import nullcontext
-
-from .interface import Bus
-
 
 class ThreadSafeBus(ObjectProxy):  # pylint: disable=abstract-method
     """
@@ -32,65 +36,81 @@ class ThreadSafeBus(ObjectProxy):  # pyl
         instead of :meth:`~can.BusABC.recv` directly.
     """
 
-    def __init__(self, *args, **kwargs):
+    __wrapped__: BusABC
+
+    def __init__(
+        self,
+        channel: Optional[typechecking.Channel] = None,
+        interface: Optional[str] = None,
+        config_context: Optional[str] = None,
+        ignore_config: bool = False,
+        **kwargs: Any,
+    ) -> None:
         if import_exc is not None:
             raise import_exc
 
-        super().__init__(Bus(*args, **kwargs))
+        super().__init__(
+            Bus(
+                channel=channel,
+                interface=interface,
+                config_context=config_context,
+                ignore_config=ignore_config,
+                **kwargs,
+            )
+        )
 
         # 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.__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
-    ):  # pylint: disable=keyword-arg-before-vararg
+    def recv(self, timeout: Optional[float] = None) -> Optional[Message]:
         with self._lock_recv:
-            return self.__wrapped__.recv(timeout=timeout, *args, **kwargs)
+            return self.__wrapped__.recv(timeout=timeout)
 
-    def send(
-        self, msg, timeout=None, *args, **kwargs
-    ):  # pylint: disable=keyword-arg-before-vararg
+    def send(self, msg: Message, timeout: Optional[float] = None) -> None:
         with self._lock_send:
-            return self.__wrapped__.send(msg, timeout=timeout, *args, **kwargs)
+            return self.__wrapped__.send(msg=msg, timeout=timeout)
 
     # send_periodic does not need a lock, since the underlying
     # `send` method is already synchronized
 
     @property
-    def filters(self):
+    def filters(self) -> Optional[typechecking.CanFilters]:
         with self._lock_recv:
             return self.__wrapped__.filters
 
     @filters.setter
-    def filters(self, filters):
+    def filters(self, filters: Optional[typechecking.CanFilters]) -> None:
         with self._lock_recv:
             self.__wrapped__.filters = filters
 
-    def set_filters(
-        self, filters=None, *args, **kwargs
-    ):  # pylint: disable=keyword-arg-before-vararg
+    def set_filters(self, filters: Optional[typechecking.CanFilters] = None) -> None:
         with self._lock_recv:
-            return self.__wrapped__.set_filters(filters=filters, *args, **kwargs)
+            return self.__wrapped__.set_filters(filters=filters)
 
-    def flush_tx_buffer(self, *args, **kwargs):
+    def flush_tx_buffer(self) -> None:
         with self._lock_send:
-            return self.__wrapped__.flush_tx_buffer(*args, **kwargs)
+            return self.__wrapped__.flush_tx_buffer()
 
-    def shutdown(self, *args, **kwargs):
+    def shutdown(self) -> None:
         with self._lock_send, self._lock_recv:
-            return self.__wrapped__.shutdown(*args, **kwargs)
+            return self.__wrapped__.shutdown()
 
     @property
-    def state(self):
+    def state(self) -> BusState:
         with self._lock_send, self._lock_recv:
             return self.__wrapped__.state
 
     @state.setter
-    def state(self, new_state):
+    def state(self, new_state: BusState) -> None:
         with self._lock_send, self._lock_recv:
             self.__wrapped__.state = new_state
+
+    @property
+    def protocol(self) -> CanProtocol:
+        with self._lock_send, self._lock_recv:
+            return self.__wrapped__.protocol
diff -pruN 4.5.0-1/can/typechecking.py 4.6.1-1/can/typechecking.py
--- 4.5.0-1/can/typechecking.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/typechecking.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,10 +1,9 @@
-"""Types for mypy type-checking
-"""
+"""Types for mypy type-checking"""
 
-import gzip
-import struct
+import io
 import sys
-import typing
+from collections.abc import Iterable, Sequence
+from typing import IO, TYPE_CHECKING, Any, NewType, Union
 
 if sys.version_info >= (3, 10):
     from typing import TypeAlias
@@ -17,47 +16,45 @@ else:
     from typing_extensions import TypedDict
 
 
-if typing.TYPE_CHECKING:
+if TYPE_CHECKING:
     import os
+    import struct
 
 
-class CanFilter(TypedDict):
+class _CanFilterBase(TypedDict):
     can_id: int
     can_mask: int
 
 
-class CanFilterExtended(TypedDict):
-    can_id: int
-    can_mask: int
+class CanFilter(_CanFilterBase, total=False):
     extended: bool
 
 
-CanFilters = typing.Sequence[typing.Union[CanFilter, CanFilterExtended]]
+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 = typing.Union[bytes, bytearray, int, typing.Iterable[int]]
+CanData = Union[bytes, bytearray, int, Iterable[int]]
 
 # Used for the Abstract Base Class
 ChannelStr = str
 ChannelInt = int
-Channel = typing.Union[ChannelInt, ChannelStr]
+Channel = Union[ChannelInt, ChannelStr, Sequence[ChannelInt]]
 
 # Used by the IO module
-FileLike = typing.Union[typing.TextIO, typing.BinaryIO, gzip.GzipFile]
-StringPathLike = typing.Union[str, "os.PathLike[str]"]
-AcceptedIOType = typing.Union[FileLike, StringPathLike]
+FileLike = Union[IO[Any], io.TextIOWrapper, io.BufferedIOBase]
+StringPathLike = Union[str, "os.PathLike[str]"]
 
-BusConfig = typing.NewType("BusConfig", typing.Dict[str, typing.Any])
+BusConfig = NewType("BusConfig", dict[str, Any])
 
 # Used by CLI scripts
-TAdditionalCliArgs: TypeAlias = typing.Dict[str, typing.Union[str, int, float, bool]]
-TDataStructs: TypeAlias = typing.Dict[
-    typing.Union[int, typing.Tuple[int, ...]],
-    typing.Union[struct.Struct, typing.Tuple, None],
+TAdditionalCliArgs: TypeAlias = dict[str, Union[str, int, float, bool]]
+TDataStructs: TypeAlias = dict[
+    Union[int, tuple[int, ...]],
+    "Union[struct.Struct, tuple[struct.Struct, *tuple[float, ...]]]",
 ]
 
 
@@ -66,7 +63,7 @@ class AutoDetectedConfig(TypedDict):
     channel: Channel
 
 
-ReadableBytesLike = typing.Union[bytes, bytearray, memoryview]
+ReadableBytesLike = Union[bytes, bytearray, memoryview]
 
 
 class BitTimingDict(TypedDict):
diff -pruN 4.5.0-1/can/util.py 4.6.1-1/can/util.py
--- 4.5.0-1/can/util.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/util.py	2025-08-12 07:32:01.000000000 +0000
@@ -12,15 +12,13 @@ import os.path
 import platform
 import re
 import warnings
+from collections.abc import Iterable
 from configparser import ConfigParser
 from time import get_clock_info, perf_counter, time
 from typing import (
     Any,
     Callable,
-    Dict,
-    Iterable,
     Optional,
-    Tuple,
     TypeVar,
     Union,
     cast,
@@ -52,8 +50,8 @@ elif platform.system() == "Windows" or p
 
 
 def load_file_config(
-    path: Optional[typechecking.AcceptedIOType] = None, section: str = "default"
-) -> Dict[str, str]:
+    path: Optional[typechecking.StringPathLike] = None, section: str = "default"
+) -> dict[str, str]:
     """
     Loads configuration from file with following content::
 
@@ -77,7 +75,7 @@ def load_file_config(
     else:
         config.read(path)
 
-    _config: Dict[str, str] = {}
+    _config: dict[str, str] = {}
 
     if config.has_section(section):
         _config.update(config.items(section))
@@ -85,7 +83,7 @@ def load_file_config(
     return _config
 
 
-def load_environment_config(context: Optional[str] = None) -> Dict[str, str]:
+def load_environment_config(context: Optional[str] = None) -> dict[str, str]:
     """
     Loads config dict from environmental variables (if set):
 
@@ -111,7 +109,7 @@ def load_environment_config(context: Opt
 
     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, "{}"))
+    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)
@@ -122,8 +120,8 @@ def load_environment_config(context: Opt
 
 
 def load_config(
-    path: Optional[typechecking.AcceptedIOType] = None,
-    config: Optional[Dict[str, Any]] = None,
+    path: Optional[typechecking.StringPathLike] = None,
+    config: Optional[dict[str, Any]] = None,
     context: Optional[str] = None,
 ) -> typechecking.BusConfig:
     """
@@ -178,7 +176,7 @@ def load_config(
 
     # Use the given dict for default values
     config_sources = cast(
-        Iterable[Union[Dict[str, Any], Callable[[Any], Dict[str, Any]]]],
+        "Iterable[Union[dict[str, Any], Callable[[Any], dict[str, Any]]]]",
         [
             given_config,
             can.rc,
@@ -212,7 +210,7 @@ def load_config(
     return bus_config
 
 
-def _create_bus_config(config: Dict[str, Any]) -> typechecking.BusConfig:
+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).
 
@@ -251,10 +249,16 @@ def _create_bus_config(config: Dict[str,
     if "fd" in config:
         config["fd"] = config["fd"] not in (0, False)
 
-    return cast(typechecking.BusConfig, config)
+    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]) -> Union[BitTiming, BitTimingFd, None]:
+def _dict2timing(data: dict[str, Any]) -> Union[BitTiming, BitTimingFd, None]:
     """Try to instantiate a :class:`~can.BitTiming` or :class:`~can.BitTimingFd` from
     a dictionary. Return `None` if not possible."""
 
@@ -396,8 +400,8 @@ def _rename_kwargs(
     func_name: str,
     start: str,
     end: Optional[str],
-    kwargs: P1.kwargs,
-    aliases: Dict[str, Optional[str]],
+    kwargs: dict[str, Any],
+    aliases: dict[str, Optional[str]],
 ) -> None:
     """Helper function for `deprecated_args_alias`"""
     for alias, new in aliases.items():
@@ -468,7 +472,7 @@ def check_or_adjust_timing_clock(timing:
     ) from None
 
 
-def time_perfcounter_correlation() -> Tuple[float, float]:
+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
diff -pruN 4.5.0-1/can/viewer.py 4.6.1-1/can/viewer.py
--- 4.5.0-1/can/viewer.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/can/viewer.py	2025-08-12 07:32:01.000000000 +0000
@@ -27,16 +27,14 @@ import os
 import struct
 import sys
 import time
-from typing import Dict, List, Tuple
 
 from can import __version__
-from can.logger import (
-    _append_filter_argument,
-    _create_base_argument_parser,
-    _create_bus,
-    _parse_additional_config,
+from can.cli import (
+    _set_logging_level_from_namespace,
+    add_bus_arguments,
+    create_bus_from_namespace,
 )
-from can.typechecking import TAdditionalCliArgs, TDataStructs
+from can.typechecking import TDataStructs
 
 logger = logging.getLogger("can.viewer")
 
@@ -161,14 +159,13 @@ class CanViewer:  # pylint: disable=too-
 
     # Unpack the data and then convert it into SI-units
     @staticmethod
-    def unpack_data(cmd: int, cmd_to_struct: Dict, data: bytes) -> List[float]:
+    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:
+        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: struct.Struct = value[0]
@@ -391,8 +388,8 @@ class SmartFormatter(argparse.HelpFormat
 
 
 def _parse_viewer_args(
-    args: List[str],
-) -> Tuple[argparse.Namespace, TDataStructs, TAdditionalCliArgs]:
+    args: list[str],
+) -> tuple[argparse.Namespace, TDataStructs]:
     # Parse command line arguments
     parser = argparse.ArgumentParser(
         "python -m can.viewer",
@@ -413,9 +410,8 @@ def _parse_viewer_args(
         allow_abbrev=False,
     )
 
-    # Generate the standard arguments:
-    # Channel, bitrate, data_bitrate, interface, app_name, CAN-FD support
-    _create_base_argument_parser(parser)
+    # add bus options group
+    add_bus_arguments(parser, filter_arg=True, group_title="Bus arguments")
 
     optional = parser.add_argument_group("Optional arguments")
 
@@ -472,8 +468,6 @@ def _parse_viewer_args(
         default="",
     )
 
-    _append_filter_argument(optional, "-f")
-
     optional.add_argument(
         "-v",
         action="count",
@@ -489,6 +483,8 @@ def _parse_viewer_args(
         raise SystemExit(errno.EINVAL)
 
     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.
@@ -525,7 +521,7 @@ def _parse_viewer_args(
             key, fmt = int(tmp[0], base=16), tmp[1]
 
             # The scaling
-            scaling: List[float] = []
+            scaling: list[float] = []
             for t in tmp[2:]:
                 # First try to convert to int, if that fails, then convert to a float
                 try:
@@ -538,15 +534,13 @@ def _parse_viewer_args(
             else:
                 data_structs[key] = struct.Struct(fmt)
 
-    additional_config = _parse_additional_config(
-        [*parsed_args.extra_args, *unknown_args]
-    )
-    return parsed_args, data_structs, additional_config
+    return parsed_args, data_structs
 
 
 def main() -> None:
-    parsed_args, data_structs, additional_config = _parse_viewer_args(sys.argv[1:])
-    bus = _create_bus(parsed_args, **additional_config)
+    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]
 
 
diff -pruN 4.5.0-1/debian/can_bridge.1 4.6.1-1/debian/can_bridge.1
--- 4.5.0-1/debian/can_bridge.1	1970-01-01 00:00:00.000000000 +0000
+++ 4.6.1-1/debian/can_bridge.1	2025-10-01 07:34:51.000000000 +0000
@@ -0,0 +1,97 @@
+.\" DO NOT MODIFY THIS FILE!  It was generated by help2man 1.49.3.
+.TH CAN_BRIDGE "1" "October 2025" "can_bridge 4.6.1" "User Commands"
+.SH NAME
+can_bridge \- bridge two CAN buses
+.SH DESCRIPTION
+usage: can_bridge [\-h] [\-\-bus1\-channel CHANNEL]
+.IP
+[\-\-bus1\-interface INTERFACE]
+[\-\-bus1\-bitrate BITRATE] [\-\-bus1\-fd]
+[\-\-bus1\-data\-bitrate DATA_BITRATE]
+[\-\-bus1\-timing TIMING_ARG [TIMING_ARG ...]]
+[\-\-bus1\-bus\-kwargs BUS_KWARG [BUS_KWARG ...]]
+[\-\-bus2\-channel CHANNEL]
+[\-\-bus2\-interface INTERFACE]
+[\-\-bus2\-bitrate BITRATE] [\-\-bus2\-fd]
+[\-\-bus2\-data\-bitrate DATA_BITRATE]
+[\-\-bus2\-timing TIMING_ARG [TIMING_ARG ...]]
+[\-\-bus2\-bus\-kwargs BUS_KWARG [BUS_KWARG ...]]
+.PP
+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.
+.SS "options:"
+.TP
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.SS "Bus 1 arguments:"
+.TP
+\fB\-\-bus1\-channel\fR 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 interface valid channel examples include:
+"can0", "vcan0".
+.TP
+\fB\-\-bus1\-interface\fR {canalystii,cantact,etas,gs_usb,iscan,ixxat,kvaser,neousys,neovi,nican,nixnet,pcan,robotell,seeedstudio,serial,slcan,socketcan,socketcand,systec,udp_multicast,usb2can,vector,virtual}
+Specify the backend CAN interface to use. If left
+blank, fall back to reading from configuration files.
+.TP
+\fB\-\-bus1\-bitrate\fR BITRATE
+Bitrate to use for the CAN bus.
+.TP
+\fB\-\-bus1\-fd\fR
+Activate CAN\-FD support
+.TP
+\fB\-\-bus1\-data\-bitrate\fR DATA_BITRATE
+Bitrate to use for the data phase in case of CAN\-FD.
+.TP
+\fB\-\-bus1\-timing\fR TIMING_ARG [TIMING_ARG ...]
+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 pythoncan documentation to verify whether your CAN interface
+supports the `timing` argument.
+.TP
+\fB\-\-bus1\-bus\-kwargs\fR BUS_KWARG [BUS_KWARG ...]
+Pass keyword arguments down to the instantiation of
+the bus class. For example, `\-i vector \fB\-c\fR 1 \fB\-\-buskwargs\fR app_name=MyCanApp serial=1234` is equivalent to
+opening the bus with `can.Bus('vector', channel=1,
+app_name='MyCanApp', serial=1234)
+.SS "Bus 2 arguments:"
+.TP
+\fB\-\-bus2\-channel\fR 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 interface valid channel examples include:
+"can0", "vcan0".
+.TP
+\fB\-\-bus2\-interface\fR {canalystii,cantact,etas,gs_usb,iscan,ixxat,kvaser,neousys,neovi,nican,nixnet,pcan,robotell,seeedstudio,serial,slcan,socketcan,socketcand,systec,udp_multicast,usb2can,vector,virtual}
+Specify the backend CAN interface to use. If left
+blank, fall back to reading from configuration files.
+.TP
+\fB\-\-bus2\-bitrate\fR BITRATE
+Bitrate to use for the CAN bus.
+.TP
+\fB\-\-bus2\-fd\fR
+Activate CAN\-FD support
+.TP
+\fB\-\-bus2\-data\-bitrate\fR DATA_BITRATE
+Bitrate to use for the data phase in case of CAN\-FD.
+.TP
+\fB\-\-bus2\-timing\fR TIMING_ARG [TIMING_ARG ...]
+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 pythoncan documentation to verify whether your CAN interface
+supports the `timing` argument.
+.TP
+\fB\-\-bus2\-bus\-kwargs\fR BUS_KWARG [BUS_KWARG ...]
+Pass keyword arguments down to the instantiation of
+the bus class. For example, `\-i vector \fB\-c\fR 1 \fB\-\-buskwargs\fR app_name=MyCanApp serial=1234` is equivalent to
+opening the bus with `can.Bus('vector', channel=1,
+app_name='MyCanApp', serial=1234)
diff -pruN 4.5.0-1/debian/can_logconvert.1 4.6.1-1/debian/can_logconvert.1
--- 4.5.0-1/debian/can_logconvert.1	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/can_logconvert.1	2025-10-01 07:34:51.000000000 +0000
@@ -1,9 +1,9 @@
 .\" DO NOT MODIFY THIS FILE!  It was generated by help2man 1.49.3.
-.TH CAN_LOGCONVERT "1" "December 2023" "can_logconvert 4.3.1" "User Commands"
+.TH CAN_LOGCONVERT "1" "October 2025" "can_logconvert 4.6.1" "User Commands"
 .SH NAME
-can_logconvert \- CAN data converter
+can_logconvert \- convert a log file from one format to another
 .SH DESCRIPTION
-usage: logconvert.py [\-h] [\-s FILE_SIZE] INFILE OUTFILE
+usage: can_logconvert [\-h] [\-s FILE_SIZE] INFILE OUTFILE
 .PP
 Convert a log file from one format to another.
 .SS "positional arguments:"
@@ -20,6 +20,6 @@ see can.Logger.
 \fB\-h\fR, \fB\-\-help\fR
 show this help message and exit
 .TP
-\fB\-s\fR FILE_SIZE, \fB\-\-file_size\fR FILE_SIZE
+\fB\-s\fR, \fB\-\-file_size\fR FILE_SIZE
 Maximum file size in bytes. Rotate log file when size
 threshold is reached.
diff -pruN 4.5.0-1/debian/can_logger.1 4.6.1-1/debian/can_logger.1
--- 4.5.0-1/debian/can_logger.1	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/can_logger.1	2025-10-01 07:34:51.000000000 +0000
@@ -1,68 +1,85 @@
 .\" DO NOT MODIFY THIS FILE!  It was generated by help2man 1.49.3.
-.TH CAN_LOGGER "1" "December 2023" "can_logger 4.3.1" "User Commands"
+.TH CAN_LOGGER "1" "October 2025" "can_logger 4.6.1" "User Commands"
 .SH NAME
-can_logger \- CAN data logger
+can_logger \- log CAN traffic, printing messages to stdout or to a given file
 .SH DESCRIPTION
-usage: logger.py [\-h] [\-c CHANNEL]
+usage: can_logger [\-h] [\-f LOG_FILE] [\-a] [\-s FILE_SIZE] [\-v] [\-\-active |
 .IP
-[\-i {canalystii,cantact,etas,gs_usb,iscan,ixxat,kvaser,neousys,neovi,nican,nixnet,pcan,robotell,seeedstudio,serial,slcan,socketcan,socketcand,systec,udp_multicast,usb2can,vector,virtual}]
-[\-b BITRATE] [\-\-fd] [\-\-data_bitrate DATA_BITRATE]
-[\-f LOG_FILE] [\-a] [\-s FILE_SIZE] [\-v]
+\fB\-\-passive]\fR [\-c CHANNEL]
+[\-i INTERFACE]
+[\-b BITRATE] [\-\-fd] [\-\-data\-bitrate DATA_BITRATE]
+[\-\-timing TIMING_ARG [TIMING_ARG ...]]
 [\-\-filter {<can_id>:<can_mask>,<can_id>~<can_mask>} [{<can_id>:<can_mask>,<can_id>~<can_mask>} ...]]
-[\-\-active | \fB\-\-passive]\fR
+[\-\-bus\-kwargs BUS_KWARG [BUS_KWARG ...]]
 \&...
 .PP
 Log CAN traffic, printing messages to stdout or to a given file.
-.SS "positional arguments:"
-.TP
-extra_args
-The remaining arguments will be used for the interface
-and logger/player initialisation. For example, `\-i
-vector \fB\-c\fR 1 \fB\-\-app\-name\fR=\fI\,MyCanApp\/\fR` is the equivalent to
-opening the bus with `Bus('vector', channel=1,
-app_name='MyCanApp')
 .SS "options:"
 .TP
 \fB\-h\fR, \fB\-\-help\fR
 show this help message and exit
+.SS "logger arguments:"
+.TP
+\fB\-f\fR, \fB\-\-file_name\fR LOG_FILE
+Path and base log filename, for supported types see
+can.Logger.
+.TP
+\fB\-a\fR, \fB\-\-append\fR
+Append to the log file if it already exists.
+.TP
+\fB\-s\fR, \fB\-\-file_size\fR FILE_SIZE
+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.)
+.TP
+\fB\-v\fR
+How much information do you want to see at the command
+line? You can add several of these e.g., \fB\-vv\fR is DEBUG
+.TP
+\fB\-\-active\fR
+Start the bus as active, this is applied by default.
+.TP
+\fB\-\-passive\fR
+Start the bus as passive.
+.TP
+extra_args
+The remaining arguments will be used for logger/player
+initialisation. For example, `can_logger \fB\-i\fR virtual \fB\-c\fR
+test \fB\-f\fR logfile.blf \fB\-\-compression\-level\fR=\fI\,9\/\fR` passes the
+keyword argument `compression_level=9` to the
+BlfWriter.
+.SS "bus arguments:"
 .TP
-\fB\-c\fR CHANNEL, \fB\-\-channel\fR CHANNEL
+\fB\-c\fR, \fB\-\-channel\fR 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 interface valid channel examples include:
 "can0", "vcan0".
 .TP
-\fB\-i\fR {canalystii,cantact,etas,gs_usb,iscan,ixxat,kvaser,neousys,neovi,nican,nixnet,pcan,robotell,seeedstudio,serial,slcan,socketcan,socketcand,systec,udp_multicast,usb2can,vector,virtual}, \fB\-\-interface\fR {canalystii,cantact,etas,gs_usb,iscan,ixxat,kvaser,neousys,neovi,nican,nixnet,pcan,robotell,seeedstudio,serial,slcan,socketcan,socketcand,systec,udp_multicast,usb2can,vector,virtual}
+\fB\-i\fR, \fB\-\-interface\fR {canalystii,cantact,etas,gs_usb,iscan,ixxat,kvaser,neousys,neovi,nican,nixnet,pcan,robotell,seeedstudio,serial,slcan,socketcan,socketcand,systec,udp_multicast,usb2can,vector,virtual}
 Specify the backend CAN interface to use. If left
 blank, fall back to reading from configuration files.
 .TP
-\fB\-b\fR BITRATE, \fB\-\-bitrate\fR BITRATE
+\fB\-b\fR, \fB\-\-bitrate\fR BITRATE
 Bitrate to use for the CAN bus.
 .TP
 \fB\-\-fd\fR
 Activate CAN\-FD support
 .TP
-\fB\-\-data_bitrate\fR DATA_BITRATE
+\fB\-\-data\-bitrate\fR DATA_BITRATE
 Bitrate to use for the data phase in case of CAN\-FD.
 .TP
-\fB\-f\fR LOG_FILE, \fB\-\-file_name\fR LOG_FILE
-Path and base log filename, for supported types see
-can.Logger.
-.TP
-\fB\-a\fR, \fB\-\-append\fR
-Append to the log file if it already exists.
-.TP
-\fB\-s\fR FILE_SIZE, \fB\-\-file_size\fR FILE_SIZE
-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.)
-.TP
-\fB\-v\fR
-How much information do you want to see at the command
-line? You can add several of these e.g., \fB\-vv\fR is DEBUG
+\fB\-\-timing\fR TIMING_ARG [TIMING_ARG ...]
+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 pythoncan documentation to verify whether your CAN interface
+supports the `timing` argument.
 .TP
 \fB\-\-filter\fR {<can_id>:<can_mask>,<can_id>~<can_mask>} [{<can_id>:<can_mask>,<can_id>~<can_mask>} ...]
 R|Space separated CAN filters for the given CAN
@@ -71,11 +88,11 @@ interface: <can_id>:<can_mask> (matches
 <can_id>~<can_mask> (matches when <received_can_id> &
 mask != can_id & mask) Fx to show only frames with ID
 0x100 to 0x103 and 0x200 to 0x20F: python3 \fB\-m\fR
-can.viewer \fB\-f\fR 100:7FC 200:7F0 Note that the ID and
-mask are always interpreted as hex values
+can.viewer \fB\-\-filter\fR 100:7FC 200:7F0 Note that the ID
+and mask are always interpreted as hex values
 .TP
-\fB\-\-active\fR
-Start the bus as active, this is applied by default.
-.TP
-\fB\-\-passive\fR
-Start the bus as passive.
+\fB\-\-bus\-kwargs\fR BUS_KWARG [BUS_KWARG ...]
+Pass keyword arguments down to the instantiation of
+the bus class. For example, `\-i vector \fB\-c\fR 1 \fB\-\-buskwargs\fR app_name=MyCanApp serial=1234` is equivalent to
+opening the bus with `can.Bus('vector', channel=1,
+app_name='MyCanApp', serial=1234)
diff -pruN 4.5.0-1/debian/can_player.1 4.6.1-1/debian/can_player.1
--- 4.5.0-1/debian/can_player.1	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/can_player.1	2025-10-01 07:34:51.000000000 +0000
@@ -1,71 +1,88 @@
 .\" DO NOT MODIFY THIS FILE!  It was generated by help2man 1.49.3.
-.TH CAN_PLAYER "1" "December 2023" "can_player 4.3.1" "User Commands"
+.TH CAN_PLAYER "1" "October 2025" "can_player 4.6.1" "User Commands"
 .SH NAME
-can_player \- CAN data player
+can_player \- replay CAN traffic
 .SH DESCRIPTION
-usage: player.py [\-h] [\-c CHANNEL]
+usage: can_player [\-h] [\-f LOG_FILE] [\-v] [\-\-ignore\-timestamps]
 .IP
-[\-i {canalystii,cantact,etas,gs_usb,iscan,ixxat,kvaser,neousys,neovi,nican,nixnet,pcan,robotell,seeedstudio,serial,slcan,socketcan,socketcand,systec,udp_multicast,usb2can,vector,virtual}]
-[\-b BITRATE] [\-\-fd] [\-\-data_bitrate DATA_BITRATE]
-[\-f LOG_FILE] [\-v] [\-\-ignore\-timestamps] [\-\-error\-frames]
-[\-g GAP] [\-s SKIP]
-\&... input\-file
+[\-\-error\-frames] [\-g GAP] [\-s SKIP] [\-c CHANNEL]
+[\-i INTERFACE]
+[\-b BITRATE] [\-\-fd] [\-\-data\-bitrate DATA_BITRATE]
+[\-\-timing TIMING_ARG [TIMING_ARG ...]]
+[\-\-bus\-kwargs BUS_KWARG [BUS_KWARG ...]]
+input\-file ...
 .PP
 Replay CAN traffic.
-.SS "positional arguments:"
+.SS "options:"
 .TP
-extra_args
-The remaining arguments will be used for the interface
-and logger/player initialisation. For example, `\-i
-vector \fB\-c\fR 1 \fB\-\-app\-name\fR=\fI\,MyCanApp\/\fR` is the equivalent to
-opening the bus with `Bus('vector', channel=1,
-app_name='MyCanApp')
+\fB\-h\fR, \fB\-\-help\fR
+show this help message and exit
+.SS "Player arguments:"
+.TP
+\fB\-f\fR, \fB\-\-file_name\fR LOG_FILE
+Path and base log filename, for supported types see
+can.LogReader.
+.TP
+\fB\-v\fR
+Also print can frames to stdout. You can add several
+of these to enable debugging
+.TP
+\fB\-\-ignore\-timestamps\fR
+Ignore timestamps (send all frames immediately with
+minimum gap between frames)
+.TP
+\fB\-\-error\-frames\fR
+Also send error frames to the interface.
+.TP
+\fB\-g\fR, \fB\-\-gap\fR GAP
+<s> minimum time between replayed frames
+.TP
+\fB\-s\fR, \fB\-\-skip\fR SKIP
+<s> skip gaps greater than 's' seconds
 .TP
 input\-file
 The file to replay. For supported types see
 can.LogReader.
-.SS "options:"
 .TP
-\fB\-h\fR, \fB\-\-help\fR
-show this help message and exit
+extra_args
+The remaining arguments will be used for logger/player
+initialisation. For example, `can_logger \fB\-i\fR virtual \fB\-c\fR
+test \fB\-f\fR logfile.blf \fB\-\-compression\-level\fR=\fI\,9\/\fR` passes the
+keyword argument `compression_level=9` to the
+BlfWriter.
+.SS "bus arguments:"
 .TP
-\fB\-c\fR CHANNEL, \fB\-\-channel\fR CHANNEL
+\fB\-c\fR, \fB\-\-channel\fR 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 interface valid channel examples include:
 "can0", "vcan0".
 .TP
-\fB\-i\fR {canalystii,cantact,etas,gs_usb,iscan,ixxat,kvaser,neousys,neovi,nican,nixnet,pcan,robotell,seeedstudio,serial,slcan,socketcan,socketcand,systec,udp_multicast,usb2can,vector,virtual}, \fB\-\-interface\fR {canalystii,cantact,etas,gs_usb,iscan,ixxat,kvaser,neousys,neovi,nican,nixnet,pcan,robotell,seeedstudio,serial,slcan,socketcan,socketcand,systec,udp_multicast,usb2can,vector,virtual}
+\fB\-i\fR, \fB\-\-interface\fR {canalystii,cantact,etas,gs_usb,iscan,ixxat,kvaser,neousys,neovi,nican,nixnet,pcan,robotell,seeedstudio,serial,slcan,socketcan,socketcand,systec,udp_multicast,usb2can,vector,virtual}
 Specify the backend CAN interface to use. If left
 blank, fall back to reading from configuration files.
 .TP
-\fB\-b\fR BITRATE, \fB\-\-bitrate\fR BITRATE
+\fB\-b\fR, \fB\-\-bitrate\fR BITRATE
 Bitrate to use for the CAN bus.
 .TP
 \fB\-\-fd\fR
 Activate CAN\-FD support
 .TP
-\fB\-\-data_bitrate\fR DATA_BITRATE
+\fB\-\-data\-bitrate\fR DATA_BITRATE
 Bitrate to use for the data phase in case of CAN\-FD.
 .TP
-\fB\-f\fR LOG_FILE, \fB\-\-file_name\fR LOG_FILE
-Path and base log filename, for supported types see
-can.LogReader.
-.TP
-\fB\-v\fR
-Also print can frames to stdout. You can add several
-of these to enable debugging
-.TP
-\fB\-\-ignore\-timestamps\fR
-Ignore timestamps (send all frames immediately with
-minimum gap between frames)
-.TP
-\fB\-\-error\-frames\fR
-Also send error frames to the interface.
-.TP
-\fB\-g\fR GAP, \fB\-\-gap\fR GAP
-<s> minimum time between replayed frames
-.TP
-\fB\-s\fR SKIP, \fB\-\-skip\fR SKIP
-<s> skip gaps greater than 's' seconds
+\fB\-\-timing\fR TIMING_ARG [TIMING_ARG ...]
+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 pythoncan documentation to verify whether your CAN interface
+supports the `timing` argument.
+.TP
+\fB\-\-bus\-kwargs\fR BUS_KWARG [BUS_KWARG ...]
+Pass keyword arguments down to the instantiation of
+the bus class. For example, `\-i vector \fB\-c\fR 1 \fB\-\-buskwargs\fR app_name=MyCanApp serial=1234` is equivalent to
+opening the bus with `can.Bus('vector', channel=1,
+app_name='MyCanApp', serial=1234)
diff -pruN 4.5.0-1/debian/can_viewer.1 4.6.1-1/debian/can_viewer.1
--- 4.5.0-1/debian/can_viewer.1	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/can_viewer.1	2025-10-01 07:34:51.000000000 +0000
@@ -1,30 +1,22 @@
 .\" DO NOT MODIFY THIS FILE!  It was generated by help2man 1.49.3.
-.TH CAN_VIEWER "1" "December 2023" "can_viewer 4.3.1" "User Commands"
+.TH CAN_VIEWER "1" "October 2025" "can_viewer 4.6.1" "User Commands"
 .SH NAME
-can_viewer \- CAN data viewer
+can_viewer \- interactive terminal viewer for live CAN bus traffic
 .SH SYNOPSIS
 .B python3
 \fI\,-m can.viewer \/\fR[\fI\,-c CHANNEL\/\fR]
 .SH DESCRIPTION
 .TP
-[\-i {canalystii,cantact,etas,gs_usb,iscan,ixxat,kvaser,neousys,neovi,nican,nixnet,pcan,robotell,seeedstudio,serial,slcan,socketcan,socketcand,systec,udp_multicast,usb2can,vector,virtual}]
-[\-b BITRATE] [\-\-fd] [\-\-data_bitrate DATA_BITRATE]
-[\-h] [\-\-version]
+[\-i INTERFACE]
+[\-b BITRATE] [\-\-fd] [\-\-data\-bitrate DATA_BITRATE]
+[\-\-timing ('TIMING_ARG',)]
+[\-\-filter ('{<can_id>:<can_mask>,<can_id>~<can_mask>}',)]
+[\-\-bus\-kwargs ('BUS_KWARG',)] [\-h] [\-\-version]
 [\-d ('{<id>:<format>,<id>:<format>:<scaling1>:...:<scalingN>,file.txt}',)]
-[\-f ('{<can_id>:<can_mask>,<can_id>~<can_mask>}',)]
 [\-v]
-('extra_args',)
 .PP
 A simple CAN viewer terminal application written in Python
-.SS "positional arguments:"
-.TP
-extra_args
-The remaining arguments will be used for the interface
-and logger/player initialisation. For example, `\-i
-vector \fB\-c\fR 1 \fB\-\-app\-name\fR=\fI\,MyCanApp\/\fR` is the equivalent to
-opening the bus with `Bus('vector', channel=1,
-app_name='MyCanApp')
-.SS "options:"
+.SS "Bus arguments:"
 .TP
 \fB\-c\fR, \fB\-\-channel\fR CHANNEL
 Most backend interfaces require some sort of channel.
@@ -43,8 +35,34 @@ Bitrate to use for the CAN bus.
 \fB\-\-fd\fR
 Activate CAN\-FD support
 .TP
-\fB\-\-data_bitrate\fR DATA_BITRATE
+\fB\-\-data\-bitrate\fR DATA_BITRATE
 Bitrate to use for the data phase in case of CAN\-FD.
+.TP
+\fB\-\-timing\fR ('TIMING_ARG',)
+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 pythoncan documentation to verify whether your CAN interface
+supports the `timing` argument.
+.TP
+\fB\-\-filter\fR ('{<can_id>:<can_mask>,<can_id>~<can_mask>}',)
+Space separated CAN filters for the given CAN interface:
+.TP
+<can_id>:<can_mask> (matches when <received_can_id> & mask == can_id & mask)
+<can_id>~<can_mask> (matches when <received_can_id> & mask != can_id & mask)
+.TP
+Fx to show only frames with ID 0x100 to 0x103 and 0x200 to 0x20F:
+python3 \fB\-m\fR can.viewer \fB\-\-filter\fR 100:7FC 200:7F0
+.IP
+Note that the ID and mask are always interpreted as hex values
+.TP
+\fB\-\-bus\-kwargs\fR ('BUS_KWARG',)
+Pass keyword arguments down to the instantiation of
+the bus class. For example, `\-i vector \fB\-c\fR 1 \fB\-\-buskwargs\fR app_name=MyCanApp serial=1234` is equivalent to
+opening the bus with `can.Bus('vector', channel=1,
+app_name='MyCanApp', serial=1234)
 .SS "Optional arguments:"
 .TP
 \fB\-h\fR, \fB\-\-help\fR
@@ -98,17 +116,6 @@ can be given as input:
 .IP
 \f(CW$ python3 -m can.viewer -d file.txt\fR
 .TP
-\fB\-f\fR, \fB\-\-filter\fR ('{<can_id>:<can_mask>,<can_id>~<can_mask>}',)
-Space separated CAN filters for the given CAN interface:
-.TP
-<can_id>:<can_mask> (matches when <received_can_id> & mask == can_id & mask)
-<can_id>~<can_mask> (matches when <received_can_id> & mask != can_id & mask)
-.TP
-Fx to show only frames with ID 0x100 to 0x103 and 0x200 to 0x20F:
-python3 \fB\-m\fR can.viewer \fB\-f\fR 100:7FC 200:7F0
-.IP
-Note that the ID and mask are always interpreted as hex values
-.TP
 \fB\-v\fR
 How much information do you want to see at the command
 line? You can add several of these e.g., \fB\-vv\fR is DEBUG
diff -pruN 4.5.0-1/debian/changelog 4.6.1-1/debian/changelog
--- 4.5.0-1/debian/changelog	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/changelog	2025-10-01 07:34:51.000000000 +0000
@@ -1,3 +1,24 @@
+python-can (4.6.1-1) unstable; urgency=medium
+
+  * New upstream version 4.6.1
+  * Adopt package (Closes: #1014518)
+  * d/watch: migrate to version 5 format
+  * d/control:
+    - Bump Standards-Version to 4.7.2 (no changes needed)
+    - Switch Testsuite to autopkgtest-pkg-pybuild
+    - Add python3-doc <!nodoc> to Build-Depends
+  * d/gbp.conf: set debian/master as default branch
+  * d/manpages:
+    - Refresh can_logger.1, can_player.1, can_viewer.1, can_logconvert.1
+    - Add new manpage: can_bridge.1
+   * d/patches:
+    - Refresh python3.patch, examples.patch.
+    - Add use-local-python-objects-inv.patch to avoid network use
+  * d/copyright:
+    - Updated licenses and years
+
+ -- Carl Keinath <carl.keinath@gmail.com>  Wed, 01 Oct 2025 09:34:51 +0200
+
 python-can (4.5.0-1) unstable; urgency=medium
 
   * New upstream version 4.5.0
@@ -25,7 +46,7 @@ python-can (4.4.0-1) unstable; urgency=m
   * Update copyright information
     + Update copyright dates
     + Re-generate d/copyright_hints
-  * Fix capitalisation in changelog entry for 4.3.1-2 
+  * Fix capitalisation in changelog entry for 4.3.1-2
   * Bump standards version to 4.7.0
 
  -- IOhannes m zmölnig (Debian/GNU) <umlaeute@debian.org>  Mon, 17 Jun 2024 18:43:09 +0200
diff -pruN 4.5.0-1/debian/control 4.6.1-1/debian/control
--- 4.5.0-1/debian/control	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/control	2025-10-01 07:34:51.000000000 +0000
@@ -3,13 +3,14 @@ Section: python
 Priority: optional
 Maintainer: Debian Python Team <team+python@tracker.debian.org>
 Uploaders:
- IOhannes m zmölnig (Debian/GNU) <umlaeute@debian.org>,
  Alexandre Detiste <tchet@debian.org>,
+ Carl Keinath <carl.keinath@gmail.com>,
 Build-Depends:
  debhelper-compat (= 13),
  dh-sequence-python3,
  dh-sequence-sphinxdoc <!nodoc>,
  furo <!nodoc>,
+ python3-doc <!nodoc>,
  pybuild-plugin-pyproject,
  python3-all,
  python3-hypothesis <!nocheck>,
@@ -28,8 +29,8 @@ Build-Depends:
  python3-sphinxcontrib.programoutput <!nodoc>,
  python3-typing-extensions,
  python3-wrapt,
-Testsuite: autopkgtest-pkg-python
-Standards-Version: 4.7.0
+Testsuite: autopkgtest-pkg-pybuild
+Standards-Version: 4.7.2
 Rules-Requires-Root: no
 Homepage: https://github.com/hardbyte/python-can/
 Vcs-Git: https://salsa.debian.org/python-team/packages/python-can.git
@@ -62,8 +63,6 @@ Section: doc
 Depends:
  ${misc:Depends},
  ${sphinxdoc:Depends},
-Built-Using:
- ${sphinxdoc:Built-Using},
 Description: Controller Area Network (CAN) interface module - API documentation
  The Controller Area Network (CAN, aka "CAN bus") is a bus standard designed
  to allow microcontrollers and devices to communicate with each other. It
diff -pruN 4.5.0-1/debian/copyright 4.6.1-1/debian/copyright
--- 4.5.0-1/debian/copyright	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/copyright	2025-10-01 07:34:51.000000000 +0000
@@ -4,7 +4,17 @@ Upstream-Contact: Brian Thorne <hardbyte
 Source: https://github.com/hardbyte/python-can/
 
 Files: *
-Copyright: 2010-2024 Brian Thorne <hardbyte@gmail.com> et al.
+Copyright: 2010-2025 Brian Thorne <hardbyte@gmail.com> et al.
+License: LGPL-3
+
+Files: can/interfaces/ixxat/*
+Copyright: 2016-2021 Giuseppe Corbelli <giuseppe.corbelli@weightpack.com>
+License: LGPL-3
+
+Files: can/interfaces/ixxat/exceptions.py
+Copyright:
+ 2016 Giuseppe Corbelli <giuseppe.corbelli@weightpack.com>
+ 2019 Marcel Kanter <marcel.kanter@googlemail.com>
 License: LGPL-3
 
 Files: can/interfaces/kvaser/*
@@ -12,31 +22,39 @@ Files: can/interfaces/kvaser/*
 Copyright: 2010 Dynamic Controls
 License: LGPL-3
 
-Files: can/interfaces/ixxat/*
-Copyright: 2016-2021 Giuseppe Corbelli <giuseppe.corbelli@weightpack.com>
+Files: can/interfaces/pcan/basic.py
+Copyright: 1999-2022, PEAK-System Technik
 License: LGPL-3
 
 Files: can/interfaces/socketcand/*
 Copyright: 2021, DOMOLOGIC GmbH
 License: LGPL-3
 
-Files: can/interfaces/pcan/basic.py
-Copyright: 1999-2022, PEAK-System Technik
-License: LGPL-3
-
 Files: can/viewer.py
  test/test_viewer.py
-Copyright: 2018, Kristian Sloth Lauszus.
+Copyright: 2018 Kristian Sloth Lauszus
 License: LGPL-3+
 
+Files:
+ can/io/blf.py
+Copyright:
+ 2023 Tobias Lorenz <tobias.lorenz@gmx.net>
+License: GPL-3
+
 Files: test/serial_test.py
-Copyright: 2017, Boris Wenzlaff
+Copyright: 2017 Boris Wenzlaff
 License: LGPL-3
 
 Files: debian/*
-Copyright: 2016-2023 IOhannes m zmölnig (Debian/GNU) <umlaeute@debian.org>
+Copyright:
+ 2016-2024 IOhannes m zmölnig (Debian/GNU) <umlaeute@debian.org>
+ 2025 Carl Keinath <carl.keinath@gmail.com>
 License: LGPL-3
 
+License: GPL-3
+ On Debian systems, the complete text of the GNU General Public License 
+ version 3 can be found in "/usr/share/common-licenses/GPL-3".
+
 License: LGPL-3
  On Debian systems, the complete text of the GNU Lesser General
  Public License version 3 can be found in "/usr/share/common-licenses/LGPL-3".
diff -pruN 4.5.0-1/debian/copyright_hints 4.6.1-1/debian/copyright_hints
--- 4.5.0-1/debian/copyright_hints	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/copyright_hints	1970-01-01 00:00:00.000000000 +0000
@@ -1,282 +0,0 @@
-Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
-Upstream-Name: FIXME
-Upstream-Contact: FIXME
-Source: FIXME
-Disclaimer: Autogenerated by licensecheck
-
-Files: CHANGELOG.md
- CONTRIBUTING.md
- CONTRIBUTORS.txt
- MANIFEST.in
- README.rst
- can/__init__.py
- can/_entry_points.py
- can/bit_timing.py
- can/broadcastmanager.py
- can/bus.py
- can/ctypesutil.py
- can/exceptions.py
- can/interface.py
- can/interfaces/__init__.py
- can/interfaces/canalystii.py
- can/interfaces/cantact.py
- can/interfaces/etas/__init__.py
- can/interfaces/etas/boa.py
- can/interfaces/gs_usb.py
- can/interfaces/ics_neovi/__init__.py
- can/interfaces/ics_neovi/neovi_bus.py
- can/interfaces/iscan.py
- can/interfaces/ixxat/canlib.py
- can/interfaces/ixxat/canlib_vcinpl.py
- can/interfaces/ixxat/canlib_vcinpl2.py
- can/interfaces/kvaser/__init__.py
- can/interfaces/kvaser/structures.py
- can/interfaces/neousys/__init__.py
- can/interfaces/neousys/neousys.py
- can/interfaces/nican.py
- can/interfaces/nixnet.py
- can/interfaces/pcan/__init__.py
- can/interfaces/pcan/pcan.py
- can/interfaces/robotell.py
- can/interfaces/seeedstudio/__init__.py
- can/interfaces/seeedstudio/seeedstudio.py
- can/interfaces/serial/__init__.py
- can/interfaces/serial/serial_can.py
- can/interfaces/slcan.py
- can/interfaces/socketcan/__init__.py
- can/interfaces/socketcan/constants.py
- can/interfaces/socketcan/socketcan.py
- can/interfaces/socketcan/utils.py
- can/interfaces/systec/__init__.py
- can/interfaces/systec/constants.py
- can/interfaces/systec/exceptions.py
- can/interfaces/systec/structures.py
- can/interfaces/systec/ucan.py
- can/interfaces/systec/ucanbus.py
- can/interfaces/udp_multicast/__init__.py
- can/interfaces/udp_multicast/bus.py
- can/interfaces/udp_multicast/utils.py
- can/interfaces/usb2can/__init__.py
- can/interfaces/usb2can/serial_selector.py
- can/interfaces/usb2can/usb2canInterface.py
- can/interfaces/usb2can/usb2canabstractionlayer.py
- can/interfaces/vector/__init__.py
- can/interfaces/vector/canlib.py
- can/interfaces/vector/exceptions.py
- can/interfaces/vector/xlclass.py
- can/interfaces/vector/xldefine.py
- can/interfaces/vector/xldriver.py
- can/interfaces/virtual.py
- can/io/__init__.py
- can/io/asc.py
- can/io/canutils.py
- can/io/csv.py
- can/io/generic.py
- can/io/logger.py
- can/io/mf4.py
- can/io/player.py
- can/io/printer.py
- can/io/sqlite.py
- can/io/trc.py
- can/listener.py
- can/logconvert.py
- can/logger.py
- can/message.py
- can/notifier.py
- can/player.py
- can/thread_safe_bus.py
- can/typechecking.py
- can/util.py
- doc/api.rst
- doc/asyncio.rst
- doc/bcm.rst
- doc/bit_timing.rst
- doc/bus.rst
- doc/conf.py
- doc/configuration.rst
- doc/development.rst
- doc/doc-requirements.txt
- doc/errors.rst
- doc/file_io.rst
- doc/history.rst
- doc/images/bit_timing_dark.svg
- doc/images/bit_timing_light.svg
- doc/index.rst
- doc/installation.rst
- doc/interfaces.rst
- doc/interfaces/canalystii.rst
- doc/interfaces/cantact.rst
- doc/interfaces/etas.rst
- doc/interfaces/gs_usb.rst
- doc/interfaces/iscan.rst
- doc/interfaces/ixxat.rst
- doc/interfaces/kvaser.rst
- doc/interfaces/neousys.rst
- doc/interfaces/neovi.rst
- doc/interfaces/nican.rst
- doc/interfaces/nixnet.rst
- doc/interfaces/pcan.rst
- doc/interfaces/robotell.rst
- doc/interfaces/seeedstudio.rst
- doc/interfaces/serial.rst
- doc/interfaces/slcan.rst
- doc/interfaces/socketcan.rst
- doc/interfaces/socketcand.rst
- doc/interfaces/systec.rst
- doc/interfaces/udp_multicast.rst
- doc/interfaces/usb2can.rst
- doc/interfaces/vector.rst
- doc/interfaces/virtual.rst
- doc/internal-api.rst
- doc/message.rst
- doc/notifier.rst
- doc/other-tools.rst
- doc/plugin-interface.rst
- doc/pycanlib.pml
- doc/scripts.rst
- doc/utils.rst
- doc/virtual-interfaces.rst
- examples/asyncio_demo.py
- examples/crc.py
- examples/cyclic.py
- examples/cyclic_checksum.py
- examples/cyclic_multiple.py
- examples/print_notifier.py
- examples/receive_all.py
- examples/send_multiple.py
- examples/send_one.py
- examples/serial_com.py
- examples/simple_log_converter.py
- examples/vcan_filtered.py
- test/__init__.py
- test/back2back_test.py
- test/config.py
- test/contextmanager_test.py
- test/data/__init__.py
- test/data/example_data.py
- test/data/ip_link_list.json
- test/data/issue_1256.asc
- test/data/issue_1299.asc
- test/data/logfile.asc
- test/data/logfile_errorframes.asc
- test/data/single_frame.asc
- test/data/single_frame_us_locale.asc
- test/data/test_CanErrorFrameExt.blf
- test/data/test_CanErrorFrames.asc
- test/data/test_CanFdMessage.asc
- test/data/test_CanFdMessage.blf
- test/data/test_CanFdMessage64.asc
- test/data/test_CanFdMessage64.blf
- test/data/test_CanFdRemoteMessage.asc
- test/data/test_CanMessage.asc
- test/data/test_CanMessage.asc.gz
- test/data/test_CanMessage.blf
- test/data/test_CanMessage.trc
- test/data/test_CanMessage2.blf
- test/data/test_CanMessage_V1_0_BUS1.trc
- test/data/test_CanMessage_V1_1.trc
- test/data/test_CanMessage_V1_3.trc
- test/data/test_CanMessage_V2_0_BUS1.trc
- test/data/test_CanMessage_V2_1.trc
- test/data/test_CanRemoteMessage.asc
- test/listener_test.py
- test/logformats_test.py
- test/message_helper.py
- test/network_test.py
- test/notifier_test.py
- test/open_vcan.sh
- test/simplecyclic_test.py
- test/test_bit_timing.py
- test/test_bus.py
- test/test_cantact.py
- test/test_cyclic_socketcan.py
- test/test_detect_available_configs.py
- test/test_interface.py
- test/test_interface_canalystii.py
- test/test_interface_ixxat.py
- test/test_interface_ixxat_fd.py
- test/test_interface_virtual.py
- test/test_kvaser.py
- test/test_load_config.py
- test/test_load_file_config.py
- test/test_logger.py
- test/test_message_class.py
- test/test_message_filtering.py
- test/test_message_sync.py
- test/test_neousys.py
- test/test_neovi.py
- test/test_pcan.py
- test/test_player.py
- test/test_robotell.py
- test/test_rotating_loggers.py
- test/test_scripts.py
- test/test_slcan.py
- test/test_socketcan.py
- test/test_socketcan_helpers.py
- test/test_socketcan_loopback.py
- test/test_systec.py
- test/test_util.py
- test/test_vector.py
- test/zero_dlc_test.py
- tox.ini
-Copyright: NONE
-License: UNKNOWN
- FIXME
-
-Files: can/interfaces/ixxat/__init__.py
- can/interfaces/ixxat/constants.py
- can/interfaces/ixxat/structures.py
-Copyright: 2016, Giuseppe Corbelli <giuseppe.corbelli@weightpack.com>
-  2016-2021, Giuseppe Corbelli <giuseppe.corbelli@weightpack.com>
-License: UNKNOWN
- FIXME
-
-Files: can/viewer.py
- test/test_viewer.py
-Copyright: 2018, Kristian Sloth Lauszus.
-License: LGPL-3+
- FIXME
-
-Files: can/interfaces/socketcand/__init__.py
- can/interfaces/socketcand/socketcand.py
-Copyright: 2021, DOMOLOGIC GmbH
-License: UNKNOWN
- FIXME
-
-Files: can/interfaces/kvaser/canlib.py
- can/interfaces/kvaser/constants.py
-Copyright: 2010, Dynamic Controls
-License: UNKNOWN
- FIXME
-
-Files: can/io/blf.py
-Copyright: NONE
-License: GPL-3
- FIXME
-
-Files: pyproject.toml
-Copyright: NONE
-License: LGPL-3
- FIXME
-
-Files: LICENSE.txt
-Copyright: 2007, Free Software Foundation, Inc. <http://fsf.org/>
-License: LGPL-3
- FIXME
-
-Files: test/serial_test.py
-Copyright: 2017, Boris Wenzlaff
-License: UNKNOWN
- FIXME
-
-Files: can/interfaces/ixxat/exceptions.py
-Copyright: 2016, Giuseppe Corbelli <giuseppe.corbelli@weightpack.com>
-  2019, Marcel Kanter <marcel.kanter@googlemail.com>
-License: UNKNOWN
- FIXME
-
-Files: can/interfaces/pcan/basic.py
-Copyright: 1999-2022, PEAK-System Technik GmbH, Darmstadt
-License: UNKNOWN
- FIXME
-
diff -pruN 4.5.0-1/debian/gbp.conf 4.6.1-1/debian/gbp.conf
--- 4.5.0-1/debian/gbp.conf	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/gbp.conf	2025-10-01 07:34:51.000000000 +0000
@@ -1,6 +1,7 @@
 # Configuration file for git-buildpackage and friends
 
 [DEFAULT]
+debian-branch=debian/master
 pristine-tar = True
 sign-tags = True
 filter = */.git*
diff -pruN 4.5.0-1/debian/patches/canalystii.patch 4.6.1-1/debian/patches/canalystii.patch
--- 4.5.0-1/debian/patches/canalystii.patch	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/patches/canalystii.patch	1970-01-01 00:00:00.000000000 +0000
@@ -1,127 +0,0 @@
-From: =?utf-8?q?IOhannes_m_zm=C3=B6lnig?= <umlaeute@debian.org>
-Date: Tue, 2 May 2023 10:42:26 +0200
-Subject: Drop 'canalystii' depending tests,...
-
-Origin: Debian
-Forwarded: not-needed
-Last-Update: 2022-07-06
-
-Once python-canalystii is packaged, consider re-adding them
-Last-Update: 2022-07-06
----
- test/test_interface_canalystii.py | 106 --------------------------------------
- 1 file changed, 106 deletions(-)
- delete mode 100755 test/test_interface_canalystii.py
-
-diff --git a/test/test_interface_canalystii.py b/test/test_interface_canalystii.py
-deleted file mode 100755
-index 65d9ee7..0000000
---- a/test/test_interface_canalystii.py
-+++ /dev/null
-@@ -1,106 +0,0 @@
--#!/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 -pruN 4.5.0-1/debian/patches/examples.patch 4.6.1-1/debian/patches/examples.patch
--- 4.5.0-1/debian/patches/examples.patch	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/patches/examples.patch	2025-10-01 07:34:51.000000000 +0000
@@ -11,6 +11,7 @@ Last-Update: 2022-11-29
  examples/asyncio_demo.py         | 2 +-
  examples/crc.py                  | 2 +-
  examples/cyclic.py               | 2 +-
+ examples/cyclic_checksum.py      | 2 +-
  examples/cyclic_multiple.py      | 2 +-
  examples/print_notifier.py       | 2 +-
  examples/receive_all.py          | 2 +-
@@ -19,10 +20,10 @@ Last-Update: 2022-11-29
  examples/serial_com.py           | 2 +-
  examples/simple_log_converter.py | 2 +-
  examples/vcan_filtered.py        | 2 +-
- 11 files changed, 11 insertions(+), 11 deletions(-)
+ 12 files changed, 12 insertions(+), 12 deletions(-)
 
 diff --git a/examples/asyncio_demo.py b/examples/asyncio_demo.py
-index d29f03b..ad9932b 100755
+index 6befbe7..64d5389 100755
 --- a/examples/asyncio_demo.py
 +++ b/examples/asyncio_demo.py
 @@ -1,4 +1,4 @@
@@ -51,6 +52,16 @@ index bdd69ee..22e261c 100755
  
  """
  This example exercises the periodic sending capabilities.
+diff --git a/examples/cyclic_checksum.py b/examples/cyclic_checksum.py
+index 763fcd7..91c7bf8 100644
+--- a/examples/cyclic_checksum.py
++++ b/examples/cyclic_checksum.py
+@@ -1,4 +1,4 @@
+-#!/usr/bin/env python
++#!/usr/bin/env python3
+ 
+ """
+ This example demonstrates how to send a periodic message containing
 diff --git a/examples/cyclic_multiple.py b/examples/cyclic_multiple.py
 index 43dc0cd..1af7c80 100755
 --- a/examples/cyclic_multiple.py
@@ -62,7 +73,7 @@ index 43dc0cd..1af7c80 100755
  """
  This example exercises the periodic task's multiple message sending capabilities
 diff --git a/examples/print_notifier.py b/examples/print_notifier.py
-index 8d55ca1..080bebe 100755
+index e6e11db..1995473 100755
 --- a/examples/print_notifier.py
 +++ b/examples/print_notifier.py
 @@ -1,4 +1,4 @@
@@ -82,7 +93,7 @@ index e9410e4..596ee99 100755
  """
  Shows how to receive messages via polling.
 diff --git a/examples/send_multiple.py b/examples/send_multiple.py
-index fdcaa5b..49af43f 100755
+index 9123e1b..b8d8137 100755
 --- a/examples/send_multiple.py
 +++ b/examples/send_multiple.py
 @@ -1,4 +1,4 @@
@@ -102,7 +113,7 @@ index 41b3a3c..6ec9bab 100755
  """
  This example shows how sending a single message works.
 diff --git a/examples/serial_com.py b/examples/serial_com.py
-index 538c8d1..50d171e 100755
+index 9f203b2..ef721e9 100755
 --- a/examples/serial_com.py
 +++ b/examples/serial_com.py
 @@ -1,4 +1,4 @@
@@ -122,7 +133,7 @@ index 20e8fba..374b7b8 100755
  """
  Use this to convert .can/.asc files to .log files.
 diff --git a/examples/vcan_filtered.py b/examples/vcan_filtered.py
-index 9c67390..d8b0968 100755
+index 22bca70..52fca5c 100755
 --- a/examples/vcan_filtered.py
 +++ b/examples/vcan_filtered.py
 @@ -1,4 +1,4 @@
diff -pruN 4.5.0-1/debian/patches/python3.patch 4.6.1-1/debian/patches/python3.patch
--- 4.5.0-1/debian/patches/python3.patch	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/patches/python3.patch	2025-10-01 07:34:51.000000000 +0000
@@ -10,31 +10,17 @@ Last-Update: 2019-09-10
 also in the documentation
 Last-Update: 2019-09-10
 ---
- can/logger.py        |  2 +-
  can/viewer.py        | 10 +++++-----
  doc/scripts.rst      | 10 +++++-----
- test/test_scripts.py |  6 +++---
- 4 files changed, 14 insertions(+), 14 deletions(-)
+ test/test_scripts.py |  8 ++++----
+ 3 files changed, 14 insertions(+), 14 deletions(-)
 
-diff --git a/can/logger.py b/can/logger.py
-index 4167558..d6ec188 100644
---- a/can/logger.py
-+++ b/can/logger.py
-@@ -97,7 +97,7 @@ def _append_filter_argument(
-         "\n      <can_id>~<can_mask> (matches when <received_can_id> & 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"
-+        "\n      python3 -m can.viewer --filter 100:7FC 200:7F0"
-         "\nNote that the ID and mask are always interpreted as hex values",
-         metavar="{<can_id>:<can_mask>,<can_id>~<can_mask>}",
-         nargs=argparse.ONE_OR_MORE,
 diff --git a/can/viewer.py b/can/viewer.py
-index 45c313b..fcac1cf 100644
+index 97bda16..7971167 100644
 --- a/can/viewer.py
 +++ b/can/viewer.py
-@@ -395,7 +395,7 @@ def _parse_viewer_args(
- ) -> Tuple[argparse.Namespace, TDataStructs, TAdditionalCliArgs]:
+@@ -392,7 +392,7 @@ def _parse_viewer_args(
+ ) -> tuple[argparse.Namespace, TDataStructs]:
      # Parse command line arguments
      parser = argparse.ArgumentParser(
 -        "python -m can.viewer",
@@ -42,7 +28,7 @@ index 45c313b..fcac1cf 100644
          description="A simple CAN viewer terminal application written in Python",
          epilog="R|Shortcuts: "
          "\n        +---------+-------------------------------+"
-@@ -448,7 +448,7 @@ def _parse_viewer_args(
+@@ -444,7 +444,7 @@ def _parse_viewer_args(
          "\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:"
@@ -51,7 +37,7 @@ index 45c313b..fcac1cf 100644
          "\nNote that the IDs are always interpreted as hex values."
          "\nAn optional conversion from integers to real units can be given"
          "\nas additional arguments. In order to convert from raw integer"
-@@ -457,16 +457,16 @@ def _parse_viewer_args(
+@@ -453,16 +453,16 @@ def _parse_viewer_args(
          "\nto convert from real units to raw integer values."
          "\nFx lets say the uint8_t needs no conversion, but the uint16 and the uint32_t"
          "\nneeds to be divided by 10 and 100 respectively:"
@@ -72,7 +58,7 @@ index 45c313b..fcac1cf 100644
          nargs=argparse.ONE_OR_MORE,
          default="",
 diff --git a/doc/scripts.rst b/doc/scripts.rst
-index 2d59b75..ce211ac 100644
+index 1d730a7..dfc14b0 100644
 --- a/doc/scripts.rst
 +++ b/doc/scripts.rst
 @@ -3,7 +3,7 @@ Command Line Tools
@@ -101,7 +87,7 @@ index 2d59b75..ce211ac 100644
      :shell:
  
  
-@@ -53,12 +53,12 @@ By default the ``can.viewer`` uses the :doc:`/interfaces/socketcan` interface. A
+@@ -53,7 +53,7 @@ By default the ``can.viewer`` uses the :doc:`/interfaces/socketcan` interface. A
  
  The full usage page can be seen below:
  
@@ -110,6 +96,7 @@ index 2d59b75..ce211ac 100644
      :shell:
  
  
+@@ -69,5 +69,5 @@ A small application that can be used to connect two can buses:
  can.logconvert
  --------------
  
@@ -117,7 +104,7 @@ index 2d59b75..ce211ac 100644
 +.. command-output:: python3 -m can.logconvert -h
      :shell:
 diff --git a/test/test_scripts.py b/test/test_scripts.py
-index 9d8c059..42b4d8c 100644
+index c1a6c08..a9d4269 100644
 --- a/test/test_scripts.py
 +++ b/test/test_scripts.py
 @@ -73,7 +73,7 @@ class CanScriptTest(unittest.TestCase, metaclass=ABCMeta):
@@ -139,6 +126,15 @@ index 9d8c059..42b4d8c 100644
          ]
          return commands
 @@ -101,7 +101,7 @@ class TestPlayerScript(CanScriptTest):
+ class TestBridgeScript(CanScriptTest):
+     def _commands(self):
+         commands = [
+-            "python -m can.bridge --help",
++            "python3 -m can.bridge --help",
+             "can_bridge --help",
+         ]
+         return commands
+@@ -115,7 +115,7 @@ class TestBridgeScript(CanScriptTest):
  class TestLogconvertScript(CanScriptTest):
      def _commands(self):
          commands = [
diff -pruN 4.5.0-1/debian/patches/remove-broken-pytest-config.patch 4.6.1-1/debian/patches/remove-broken-pytest-config.patch
--- 4.5.0-1/debian/patches/remove-broken-pytest-config.patch	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/patches/remove-broken-pytest-config.patch	2025-10-01 07:34:51.000000000 +0000
@@ -13,15 +13,15 @@ Last-Update: 2022-07-06
  1 file changed, 1 insertion(+), 1 deletion(-)
 
 diff --git a/tox.ini b/tox.ini
-index e112c22..ccddb87 100644
+index 5f393cb..945320b 100644
 --- a/tox.ini
 +++ b/tox.ini
-@@ -44,7 +44,7 @@ commands =
+@@ -77,7 +77,7 @@ commands =
  
  [pytest]
  testpaths = test
--addopts = -v --timeout=300 --cov=can --cov-config=tox.ini --cov-report=lcov --cov-report=term
+-addopts = -v --timeout=300 --cov=can --cov-config=tox.ini --cov-report=lcov --cov-report=term --color=yes
 +addopts = -v --timeout=300
  
- 
  [coverage:run]
+ # we could also use branch coverage
diff -pruN 4.5.0-1/debian/patches/series 4.6.1-1/debian/patches/series
--- 4.5.0-1/debian/patches/series	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/patches/series	2025-10-01 07:34:51.000000000 +0000
@@ -1,4 +1,4 @@
 remove-broken-pytest-config.patch
 python3.patch
 examples.patch
-canalystii.patch
+use-local-python-objects-inv.patch
diff -pruN 4.5.0-1/debian/patches/use-local-python-objects-inv.patch 4.6.1-1/debian/patches/use-local-python-objects-inv.patch
--- 4.5.0-1/debian/patches/use-local-python-objects-inv.patch	1970-01-01 00:00:00.000000000 +0000
+++ 4.6.1-1/debian/patches/use-local-python-objects-inv.patch	2025-10-01 07:34:51.000000000 +0000
@@ -0,0 +1,23 @@
+From: Carl Keinath <carl.keinath@gmail.com>
+Date: Wed, 1 Oct 2025 21:23:30 +0200
+Subject: use-local-python-objects-inv
+
+Origin: Debian
+Forwarded: not-needed
+---
+ doc/conf.py | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/doc/conf.py b/doc/conf.py
+index 5e41336..f7f7ee5 100755
+--- a/doc/conf.py
++++ b/doc/conf.py
+@@ -56,7 +56,7 @@ extensions = [
+ # 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 #%s")}
+ 
+-intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)}
++intersphinx_mapping = {"python": ("/usr/share/doc/python3/html", None)}
+ 
+ # If this is True, todo and todolist produce output, else they produce nothing.
+ # The default is False.
diff -pruN 4.5.0-1/debian/python3-can.manpages 4.6.1-1/debian/python3-can.manpages
--- 4.5.0-1/debian/python3-can.manpages	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/python3-can.manpages	2025-10-01 07:34:51.000000000 +0000
@@ -1,3 +1,4 @@
+debian/can_bridge.1
 debian/can_logconvert.1
 debian/can_logger.1
 debian/can_player.1
diff -pruN 4.5.0-1/debian/rules 4.6.1-1/debian/rules
--- 4.5.0-1/debian/rules	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/rules	2025-10-01 07:34:51.000000000 +0000
@@ -1,7 +1,11 @@
 #! /usr/bin/make -f
 
+export PYTHONWARNINGS=d
 export PYBUILD_NAME=can
-export PYBUILD_TEST_ARGS=-k 'not BasicTestUdpMulticastBusIP'
+
+export PYTEST_ADDOPTS=--ignore=test/test_interface_canalystii.py \
+	-k 'not BasicTestUdpMulticastBusIP' \
+	--tb=short
 
 include /usr/share/dpkg/default.mk
 export DEB_VERSION_UPSTREAM
@@ -28,7 +32,7 @@ override_dh_installdocs:
 	dh_installdocs --doc-main-package=python3-can -p python-can-doc
 	dh_installdocs --remaining-packages
 
-manpages=debian/can_logger.1 debian/can_player.1 debian/can_viewer.1 debian/can_logconvert.1
+manpages=debian/can_logger.1 debian/can_player.1 debian/can_viewer.1 debian/can_logconvert.1 debian/can_bridge.1
 
 $(manpages): debian/helper2man
 	cp $^ $(@:.1=)
@@ -37,16 +41,3 @@ $(manpages): debian/helper2man
 
 .PHONY: manpages
 manpages: $(manpages)
-
-
-DEB_COPYRIGHT_CHECK_IGNORE_REGEX = \
-        debian/.*|doc/images/.*\.png
-# licensecheck v1
-.PHONY: licensecheck
-licensecheck:
-	LANG=C.UTF-8 licensecheck \
-		-i "^($(DEB_COPYRIGHT_CHECK_IGNORE_REGEX))$$" \
-		--check '.*' --recursive --deb-machine --lines 0 * \
-		> debian/copyright_newhints
-	cmp debian/copyright_hints debian/copyright_newhints \
-		&& rm debian/copyright_newhints
diff -pruN 4.5.0-1/debian/tests/control 4.6.1-1/debian/tests/control
--- 4.5.0-1/debian/tests/control	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/tests/control	1970-01-01 00:00:00.000000000 +0000
@@ -1,10 +0,0 @@
-Tests:
- upstream-tests,
-Depends:
- python3-can,
- python3-hypothesis,
- python3-parameterized,
- python3-pytest,
- python3-pytest-timeout,
-Restrictions:
- allow-stderr,
diff -pruN 4.5.0-1/debian/tests/upstream-tests 4.6.1-1/debian/tests/upstream-tests
--- 4.5.0-1/debian/tests/upstream-tests	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/tests/upstream-tests	1970-01-01 00:00:00.000000000 +0000
@@ -1,8 +0,0 @@
-#!/bin/sh
-
-set -e
-
-mv can can_
-
-cd test
-pytest .
diff -pruN 4.5.0-1/debian/watch 4.6.1-1/debian/watch
--- 4.5.0-1/debian/watch	2024-11-29 19:42:59.000000000 +0000
+++ 4.6.1-1/debian/watch	2025-10-01 07:34:51.000000000 +0000
@@ -1,6 +1,5 @@
-# process this file with uscan(1)
-version=4
-opts=dversionmangle=auto;s/~github//,uversionmangle=s/(-?(alpha|beta|rc|a|b|c|dev|pre)(\.)?(\d+)?)$/~\\1/,repacksuffix=~github \
-  https://github.com/hardbyte/python-can/tags .*/v?(\d\S*)\.tar\.gz
+Version: 5
 
-#https://pypi.python.org/packages/source/p/python-can/ python-can-@ANY_VERSION@@ARCHIVE_EXT@ debian uupdate
+Template: Github
+Owner: hardbyte
+Project: python-can
diff -pruN 4.5.0-1/doc/conf.py 4.6.1-1/doc/conf.py
--- 4.5.0-1/doc/conf.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/conf.py	2025-08-12 07:32:01.000000000 +0000
@@ -122,6 +122,12 @@ 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"),
@@ -132,11 +138,11 @@ nitpick_ignore = [
     ("py:class", "~P1"),
     # intersphinx fails to reference some builtins
     ("py:class", "asyncio.events.AbstractEventLoop"),
-    ("py:class", "_thread.allocate_lock"),
+    ("py:class", "_thread.lock"),
 ]
 
 # mock windows specific attributes
-autodoc_mock_imports = ["win32com"]
+autodoc_mock_imports = ["win32com", "pythoncom"]
 ctypes.windll = MagicMock()
 ctypesutil.HRESULT = ctypes.c_long
 
diff -pruN 4.5.0-1/doc/development.rst 4.6.1-1/doc/development.rst
--- 4.5.0-1/doc/development.rst	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/development.rst	2025-08-12 07:32:01.000000000 +0000
@@ -1,133 +1,281 @@
 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
+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 <https://github.com/hardbyte/python-can>`_.
+If you have questions, feel free to open an issue or start a discussion on GitHub.
 
-Note that the latest released version on PyPi may be significantly behind the
-``main`` branch. Please open any feature requests against the ``main`` branch
+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.
 
-There is also a `python-can <https://groups.google.com/forum/#!forum/python-can>`__
-mailing list for development discussion.
+Code Structure
+^^^^^^^^^^^^^^
 
-Some more information about the internals of this library can be found
-in the chapter :ref:`internalapi`.
-There is also additional information on extending the ``can.io`` module.
+The modules in ``python-can`` are:
 
++---------------------------------+------------------------------------------------------+
+|Module                           | Description                                          |
++=================================+======================================================+
+|:doc:`interfaces <interfaces>`   | Contains interface dependent code.                   |
++---------------------------------+------------------------------------------------------+
+|:doc:`bus <bus>`                 | Contains the interface independent Bus object.       |
++---------------------------------+------------------------------------------------------+
+|:doc:`message <message>`         | Contains the interface independent Message object.   |
++---------------------------------+------------------------------------------------------+
+|:doc:`io <file_io>`              | Contains a range of file readers and writers.        |
++---------------------------------+------------------------------------------------------+
+|:doc:`broadcastmanager <bcm>`    | Contains interface independent broadcast manager     |
+|                                 | code.                                                |
++---------------------------------+------------------------------------------------------+
 
-Pre-releases
-------------
+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/<your-username>/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 <https://docs.astral.sh/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
 
-The latest pre-release can be installed with::
+   **Modern method (uv):**
 
-    pip install --upgrade --pre python-can
+   With `uv`, you can run your script directly:
 
+   .. code-block:: shell
 
+      uv run snippet.py
 
-Building & Installing
----------------------
+   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.
 
-The following assumes that the commands are executed from the root of the repository:
+3. **Make Your Changes**
 
-The project can be built with::
+   * 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.
 
-    pipx run build
-    pipx run twine check dist/*
+4. **Test Your Changes**
 
-The project can be installed in editable mode with::
+   This project uses `tox <https://tox.wiki/en/latest/>`__ to automate all checks (tests, lint, type, docs). 
+   Tox will set up isolated environments and run the right tools for you.
 
-    pip install -e .
+   To run all checks:
 
-The unit tests can be run with::
+   .. code-block:: shell
 
-    pipx run tox -e py
+      tox
 
-The documentation can be built with::
+   To run a specific check, use:
 
-    pipx run tox -e docs
+   .. code-block:: shell
 
-The linters can be run with::
+      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)
 
-    pip install -e .[lint]
-    black --check can
-    mypy can
-    ruff check can
-    pylint can/**.py can/io doc/conf.py examples/**.py can/interfaces/socketcan
+   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 <https://towncrier.readthedocs.io/>`__ 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 ``<issue-or-description>.<type>.md``, where ``<type>`` 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``
-- Implement the central part of the backend: the bus class that extends
+* 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
+* 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.
-  To get started, have a look at ``back2back_test.py``:
-  Simply add a test case like ``BasicTestSocketCan`` and some basic tests will be executed for the new interface.
+* 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.
 
-.. attention::
-    We strongly recommend using the :ref:`plugin interface` to extend python-can.
-    Publish a python package that contains your :class:`can.BusABC` subclass and use
-    it within the python-can API. We will mention your package inside this documentation
-    and add it as an optional dependency.
+Creating a new Release
+----------------------
 
-Code Structure
---------------
+Releases are automated via GitHub Actions. To create a new release:
 
-The modules in ``python-can`` are:
+* Build the changelog with towncrier:
 
-+---------------------------------+------------------------------------------------------+
-|Module                           | Description                                          |
-+=================================+======================================================+
-|:doc:`interfaces <interfaces>`   | Contains interface dependent code.                   |
-+---------------------------------+------------------------------------------------------+
-|:doc:`bus <bus>`                 | Contains the interface independent Bus object.       |
-+---------------------------------+------------------------------------------------------+
-|:doc:`message <message>`         | Contains the interface independent Message object.   |
-+---------------------------------+------------------------------------------------------+
-|:doc:`io <file_io>`              | Contains a range of file readers and writers.        |
-+---------------------------------+------------------------------------------------------+
-|:doc:`broadcastmanager <bcm>`    | Contains interface independent broadcast manager     |
-|                                 | code.                                                |
-+---------------------------------+------------------------------------------------------+
 
+  * Collect all news fragments and update ``CHANGELOG.md`` by running:
 
-Creating a new Release
-----------------------
+    .. code-block:: shell
 
-- Release from the ``main`` branch (except for pre-releases).
-- Check if any deprecations are pending.
-- 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.
-- 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 <https://pypi.org/project/python-can/#history>`__,
-  `Read the Docs <https://readthedocs.org/projects/python-can/versions/>`__ and
-  `GitHub <https://github.com/hardbyte/python-can/releases>`__.
+       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.
 
-Manual release steps (deprecated)
----------------------------------
+* 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.
 
-- Create a temporary virtual environment.
-- Create a new tag in the repository. Use `semantic versioning <http://semver.org>`__.
-- Build with  ``pipx run build``
-- 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*``.
+* You can monitor the release status on:
+  `PyPi <https://pypi.org/project/python-can/#history>`__,
+  `Read the Docs <https://readthedocs.org/projects/python-can/versions/>`__ and
+  `GitHub Releases <https://github.com/hardbyte/python-can/releases>`__.
diff -pruN 4.5.0-1/doc/doc-requirements.txt 4.6.1-1/doc/doc-requirements.txt
--- 4.5.0-1/doc/doc-requirements.txt	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/doc-requirements.txt	1970-01-01 00:00:00.000000000 +0000
@@ -1,5 +0,0 @@
-sphinx>=5.2.3
-sphinxcontrib-programoutput
-sphinx-inline-tabs
-sphinx-copybutton
-furo
diff -pruN 4.5.0-1/doc/file_io.rst 4.6.1-1/doc/file_io.rst
--- 4.5.0-1/doc/file_io.rst	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/file_io.rst	2025-08-12 07:32:01.000000000 +0000
@@ -94,7 +94,7 @@ as further references can-utils can be u
 Log (.log can-utils Logging format)
 -----------------------------------
 
-CanutilsLogWriter logs CAN data to an ASCII log file compatible with `can-utils <https://github.com/linux-can/can-utils>`
+CanutilsLogWriter logs CAN data to an ASCII log file compatible with `can-utils <https://github.com/linux-can/can-utils>`_
 As specification following references can-utils can be used: 
 `asc2log <https://github.com/linux-can/can-utils/blob/master/asc2log.c>`_,
 `log2asc <https://github.com/linux-can/can-utils/blob/master/log2asc.c>`_.
diff -pruN 4.5.0-1/doc/installation.rst 4.6.1-1/doc/installation.rst
--- 4.5.0-1/doc/installation.rst	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/installation.rst	2025-08-12 07:32:01.000000000 +0000
@@ -21,6 +21,13 @@ Install the ``can`` package from PyPi wi
        $ pip install python-can[serial]
 
 
+Pre-releases
+------------
+
+The latest pre-release can be installed with::
+
+    pip install --upgrade --pre python-can
+
 
 GNU/Linux dependencies
 ----------------------
diff -pruN 4.5.0-1/doc/interfaces/gs_usb.rst 4.6.1-1/doc/interfaces/gs_usb.rst
--- 4.5.0-1/doc/interfaces/gs_usb.rst	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/interfaces/gs_usb.rst	2025-08-12 07:32:01.000000000 +0000
@@ -6,7 +6,7 @@ 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]"``
+Install: ``pip install "python-can[gs-usb]"``
 
 Usage: pass device ``index`` or ``channel`` (starting from 0) if using automatic device detection:
 
diff -pruN 4.5.0-1/doc/interfaces/pcan.rst 4.6.1-1/doc/interfaces/pcan.rst
--- 4.5.0-1/doc/interfaces/pcan.rst	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/interfaces/pcan.rst	2025-08-12 07:32:01.000000000 +0000
@@ -15,7 +15,7 @@ Here is an example configuration file fo
     [default]
     interface = pcan
     channel = PCAN_USBBUS1
-    state = can.bus.BusState.PASSIVE
+    state = PASSIVE
     bitrate = 500000
 
 ``channel`` (default ``"PCAN_USBBUS1"``)
diff -pruN 4.5.0-1/doc/interfaces/serial.rst 4.6.1-1/doc/interfaces/serial.rst
--- 4.5.0-1/doc/interfaces/serial.rst	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/interfaces/serial.rst	2025-08-12 07:32:01.000000000 +0000
@@ -30,7 +30,9 @@ six parts. The start and the stop byte f
 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
 ^^^^^^^^^^^^^^^^^^^
@@ -102,3 +104,21 @@ Examples of serial frames
 +================+=====================+======+=====================+==============+
 | 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 -pruN 4.5.0-1/doc/interfaces/udp_multicast.rst 4.6.1-1/doc/interfaces/udp_multicast.rst
--- 4.5.0-1/doc/interfaces/udp_multicast.rst	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/interfaces/udp_multicast.rst	2025-08-12 07:32:01.000000000 +0000
@@ -22,6 +22,15 @@ sufficiently reliable for 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
 -------------------
 
@@ -53,6 +62,9 @@ from ``bus_1`` to ``bus_2``:
             # give the notifier enough time to get triggered by the second bus
             time.sleep(2.0)
 
+            # clean-up
+            notifier.stop()
+
 
 Bus Class Documentation
 -----------------------
diff -pruN 4.5.0-1/doc/internal-api.rst 4.6.1-1/doc/internal-api.rst
--- 4.5.0-1/doc/internal-api.rst	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/internal-api.rst	2025-08-12 07:32:01.000000000 +0000
@@ -90,8 +90,8 @@ 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 usually extends :class:`can.io.generic.BaseIOHandler`, while
-the writer often additionally extends :class:`can.Listener`,
+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`.
 
 
@@ -104,9 +104,9 @@ Ideally add both reading and writing sup
 
 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 often extends :class:`can.io.generic.BaseIOHandler`, but does not have to).
+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 often extends :class:`can.io.generic.BaseIOHandler` and :class:`can.Listener`, but does not have to).
+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.
@@ -126,7 +126,9 @@ IO Utilities
 
 
 .. automodule:: can.io.generic
+    :show-inheritance:
     :members:
+    :private-members:
     :member-order: bysource
 
 
diff -pruN 4.5.0-1/doc/notifier.rst 4.6.1-1/doc/notifier.rst
--- 4.5.0-1/doc/notifier.rst	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/notifier.rst	2025-08-12 07:32:01.000000000 +0000
@@ -58,7 +58,8 @@ readers are also documented here.
     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 :class:`can.io.generic.BaseIOHandler` concrete implementation.
+    is a concrete implementation of :class:`~can.io.generic.MessageReader` or
+    :class:`~can.io.generic.MessageWriter`.
 
     ::
 
diff -pruN 4.5.0-1/doc/other-tools.rst 4.6.1-1/doc/other-tools.rst
--- 4.5.0-1/doc/other-tools.rst	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/other-tools.rst	2025-08-12 07:32:01.000000000 +0000
@@ -47,7 +47,7 @@ CAN Frame Parsing tools etc. (implemente
 
 #. CAN Message / Database scripting
     * The `cantools`_ package provides multiple methods for interacting with can message database
-      files, and using these files to monitor live busses with a command line monitor tool.
+      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.).
diff -pruN 4.5.0-1/doc/plugin-interface.rst 4.6.1-1/doc/plugin-interface.rst
--- 4.5.0-1/doc/plugin-interface.rst	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/plugin-interface.rst	2025-08-12 07:32:01.000000000 +0000
@@ -73,15 +73,18 @@ The table below lists interface drivers
 +----------------------------+-------------------------------------------------------+
 | `python-can-sontheim`_     | CAN Driver for Sontheim CAN interfaces (e.g. CANfox)  |
 +----------------------------+-------------------------------------------------------+
-| `zlgcan-driver-py`_        | Python wrapper for zlgcan-driver-rs                   |
+| `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-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-driver-py: https://github.com/zhuyu4839/zlgcan-driver
+.. _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
 
diff -pruN 4.5.0-1/doc/scripts.rst 4.6.1-1/doc/scripts.rst
--- 4.5.0-1/doc/scripts.rst	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/scripts.rst	2025-08-12 07:32:01.000000000 +0000
@@ -57,6 +57,15 @@ The full usage page can be seen below:
     :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
 --------------
 
diff -pruN 4.5.0-1/doc/utils.rst 4.6.1-1/doc/utils.rst
--- 4.5.0-1/doc/utils.rst	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/doc/utils.rst	2025-08-12 07:32:01.000000000 +0000
@@ -4,4 +4,7 @@ Utilities
 
 .. autofunction:: can.detect_available_configs
 
+.. autofunction:: can.cli.add_bus_arguments
+
+.. autofunction:: can.cli.create_bus_from_namespace
 
diff -pruN 4.5.0-1/examples/asyncio_demo.py 4.6.1-1/examples/asyncio_demo.py
--- 4.5.0-1/examples/asyncio_demo.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/examples/asyncio_demo.py	2025-08-12 07:32:01.000000000 +0000
@@ -5,10 +5,12 @@ This example demonstrates how to use asy
 """
 
 import asyncio
-from typing import List
+from typing import TYPE_CHECKING
 
 import can
-from can.notifier import MessageRecipient
+
+if TYPE_CHECKING:
+    from can.notifier import MessageRecipient
 
 
 def print_message(msg: can.Message) -> None:
@@ -25,32 +27,28 @@ async def main() -> None:
         reader = can.AsyncBufferedReader()
         logger = can.Logger("logfile.asc")
 
-        listeners: List[MessageRecipient] = [
+        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
-        loop = asyncio.get_running_loop()
-        notifier = can.Notifier(bus, listeners, loop=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!")
-
-        # Clean-up
-        notifier.stop()
+        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__":
diff -pruN 4.5.0-1/examples/cyclic_checksum.py 4.6.1-1/examples/cyclic_checksum.py
--- 4.5.0-1/examples/cyclic_checksum.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/examples/cyclic_checksum.py	2025-08-12 07:32:01.000000000 +0000
@@ -59,6 +59,5 @@ def compute_xbr_checksum(message: can.Me
 
 if __name__ == "__main__":
     with can.Bus(channel=0, interface="virtual", receive_own_messages=True) as _bus:
-        notifier = can.Notifier(bus=_bus, listeners=[print])
-        cyclic_checksum_send(_bus)
-        notifier.stop()
+        with can.Notifier(bus=_bus, listeners=[print]):
+            cyclic_checksum_send(_bus)
diff -pruN 4.5.0-1/examples/print_notifier.py 4.6.1-1/examples/print_notifier.py
--- 4.5.0-1/examples/print_notifier.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/examples/print_notifier.py	2025-08-12 07:32:01.000000000 +0000
@@ -8,14 +8,13 @@ import can
 def main():
     with can.Bus(interface="virtual", receive_own_messages=True) as bus:
         print_listener = can.Printer()
-        notifier = can.Notifier(bus, [print_listener])
-
-        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)
-        notifier.stop()
+        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__":
diff -pruN 4.5.0-1/examples/send_multiple.py 4.6.1-1/examples/send_multiple.py
--- 4.5.0-1/examples/send_multiple.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/examples/send_multiple.py	2025-08-12 07:32:01.000000000 +0000
@@ -4,8 +4,8 @@
 This demo creates multiple processes of producers to spam a socketcan bus.
 """
 
-from time import sleep
 from concurrent.futures import ProcessPoolExecutor
+from time import sleep
 
 import can
 
diff -pruN 4.5.0-1/examples/serial_com.py 4.6.1-1/examples/serial_com.py
--- 4.5.0-1/examples/serial_com.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/examples/serial_com.py	2025-08-12 07:32:01.000000000 +0000
@@ -18,8 +18,8 @@ Expects two serial ports (/dev/ttyS10 an
         com0com: http://com0com.sourceforge.net/
 """
 
-import time
 import threading
+import time
 
 import can
 
diff -pruN 4.5.0-1/examples/vcan_filtered.py 4.6.1-1/examples/vcan_filtered.py
--- 4.5.0-1/examples/vcan_filtered.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/examples/vcan_filtered.py	2025-08-12 07:32:01.000000000 +0000
@@ -18,14 +18,11 @@ def main():
         # 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
-        notifier = 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)
-        notifier.stop()
+        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__":
diff -pruN 4.5.0-1/pyproject.toml 4.6.1-1/pyproject.toml
--- 4.5.0-1/pyproject.toml	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/pyproject.toml	2025-08-12 07:32:01.000000000 +0000
@@ -1,5 +1,5 @@
 [build-system]
-requires = ["setuptools >= 67.7", "setuptools_scm>=8"]
+requires = ["setuptools >= 77.0", "setuptools_scm>=8"]
 build-backend = "setuptools.build_meta"
 
 [project]
@@ -11,10 +11,9 @@ dependencies = [
     "wrapt~=1.10",
     "packaging >= 23.1",
     "typing_extensions>=3.10.0.0",
-    "msgpack~=1.1.0; platform_system != 'Windows'",
 ]
-requires-python = ">=3.8"
-license = { text = "LGPL v3" }
+requires-python = ">=3.9"
+license = "LGPL-3.0-only"
 classifiers = [
     "Development Status :: 5 - Production/Stable",
     "Environment :: Console",
@@ -23,18 +22,17 @@ classifiers = [
     "Intended Audience :: Information Technology",
     "Intended Audience :: Manufacturing",
     "Intended Audience :: Telecommunications Industry",
-    "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
     "Natural Language :: English",
     "Operating System :: MacOS",
     "Operating System :: Microsoft :: Windows",
     "Operating System :: POSIX :: Linux",
     "Programming Language :: Python",
-    "Programming Language :: Python :: 3.8",
     "Programming Language :: Python :: 3.9",
     "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",
@@ -51,6 +49,7 @@ 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"
@@ -59,30 +58,56 @@ repository = "https://github.com/hardbyt
 changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md"
 
 [project.optional-dependencies]
-lint = [
-    "pylint==3.2.*",
-    "ruff==0.7.0",
-    "black==24.10.*",
-    "mypy==1.12.*",
-]
-pywin32 = ["pywin32>=305"]
+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"]
+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-driver-py"]
+zlgcan = ["zlgcan"]
+candle = ["python-can-candle>=1.2.2"]
 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==3.3.*",
+    "ruff==0.12.8",
+    "black==25.1.*",
+    "mypy==1.17.*",
+]
+test = [
+    "pytest==8.4.*",
+    "pytest-timeout==2.4.*",
+    "pytest-modern==0.7.*;platform_system!='Windows'",
+    "coveralls==4.0.*",
+    "pytest-cov==6.2.*",
+    "coverage==7.10.*",
+    "hypothesis>=6.136,<6.138",
+    "parameterized==0.9.*",
+]
+dev = [
+    {include-group = "docs"},
+    {include-group = "lint"},
+    {include-group = "test"},
+]
 
 [tool.setuptools.dynamic]
 readme = { file = "README.rst" }
@@ -107,11 +132,8 @@ disallow_incomplete_defs = true
 warn_redundant_casts = true
 warn_unused_ignores = true
 exclude = [
-    "venv",
     "^doc/conf.py$",
-    "^build",
     "^test",
-    "^can/interfaces/__init__.py",
     "^can/interfaces/etas",
     "^can/interfaces/gs_usb",
     "^can/interfaces/ics_neovi",
@@ -121,20 +143,16 @@ exclude = [
     "^can/interfaces/nican",
     "^can/interfaces/neousys",
     "^can/interfaces/pcan",
-    "^can/interfaces/serial",
-    "^can/interfaces/slcan",
     "^can/interfaces/socketcan",
     "^can/interfaces/systec",
-    "^can/interfaces/udp_multicast",
     "^can/interfaces/usb2can",
-    "^can/interfaces/virtual",
 ]
 
 [tool.ruff]
 line-length = 100
 
 [tool.ruff.lint]
-select = [
+extend-select = [
     "A",  # flake8-builtins
     "B",  # flake8-bugbear
     "C4",  # flake8-comprehensions
@@ -165,15 +183,19 @@ ignore = [
     "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 = [
-    "c-extension-no-member",
     "cyclic-import",
     "duplicate-code",
     "fixme",
@@ -188,6 +210,45 @@ disable = [
     "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 = "<!-- towncrier release notes start -->\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 -pruN 4.5.0-1/test/back2back_test.py 4.6.1-1/test/back2back_test.py
--- 4.5.0-1/test/back2back_test.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/back2back_test.py	2025-08-12 07:32:01.000000000 +0000
@@ -14,13 +14,12 @@ 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 (
     IS_CI,
     IS_OSX,
     IS_PYPY,
-    IS_TRAVIS,
-    IS_UNIX,
     TEST_CAN_FD,
     TEST_INTERFACE_SOCKETCAN,
 )
@@ -273,23 +272,21 @@ class Back2BackTestCase(unittest.TestCas
         self.bus2.recv(0)
         self.bus2.recv(0)
 
-    @unittest.skipIf(IS_CI, "fails randomly when run on CI server")
     def test_send_periodic_duration(self):
         """
         Verify that send_periodic only transmits for the specified duration.
 
         Regression test for #1713.
         """
-        for params in [(0.01, 0.003), (0.1, 0.011), (1, 0.4)]:
-            duration, period = params
+        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 * 1.25)) is not None:
+            while (msg := self.bus1.recv(period + self.TIMEOUT)) is not None:
                 messages.append(msg)
 
-            delta_t = round(messages[-1].timestamp - messages[0].timestamp, 2)
-            assert delta_t <= duration
+            delta_t = messages[-1].timestamp - messages[0].timestamp
+            assert delta_t < duration + 0.05
 
 
 @unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan")
@@ -302,9 +299,13 @@ class BasicTestSocketCan(Back2BackTestCa
 
 # 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_UNIX and not (IS_CI and IS_OSX),
-    "only supported on Unix systems (but not on macOS at Travis CI and GitHub Actions)",
+    is_msgpack_installed(raise_exception=False),
+    "msgpack not installed",
 )
 class BasicTestUdpMulticastBusIPv4(Back2BackTestCase):
     INTERFACE_1 = "udp_multicast"
@@ -319,9 +320,13 @@ class BasicTestUdpMulticastBusIPv4(Back2
 
 # 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_UNIX and not (IS_TRAVIS or (IS_CI and IS_OSX)),
-    "only supported on Unix systems (but not on Travis CI; and not on macOS at GitHub Actions)",
+    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"
diff -pruN 4.5.0-1/test/contextmanager_test.py 4.6.1-1/test/contextmanager_test.py
--- 4.5.0-1/test/contextmanager_test.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/contextmanager_test.py	2025-08-12 07:32:01.000000000 +0000
@@ -17,9 +17,10 @@ class ContextManagerTest(unittest.TestCa
         )
 
     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()
 
@@ -27,9 +28,10 @@ class ContextManagerTest(unittest.TestCa
             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
diff -pruN 4.5.0-1/test/data/example_data.py 4.6.1-1/test/data/example_data.py
--- 4.5.0-1/test/data/example_data.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/data/example_data.py	2025-08-12 07:32:01.000000000 +0000
@@ -160,7 +160,7 @@ TEST_MESSAGES_REMOTE_FRAMES = sort_messa
 
 TEST_MESSAGES_ERROR_FRAMES = sort_messages(
     [
-        Message(is_error_frame=True),
+        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),
     ]
Binary files 4.5.0-1/test/data/issue_1905.blf and 4.6.1-1/test/data/issue_1905.blf differ
diff -pruN 4.5.0-1/test/data/test_CanMessage_V1_0_BUS1.trc 4.6.1-1/test/data/test_CanMessage_V1_0_BUS1.trc
--- 4.5.0-1/test/data/test_CanMessage_V1_0_BUS1.trc	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/data/test_CanMessage_V1_0_BUS1.trc	2025-08-12 07:32:01.000000000 +0000
@@ -1,10 +1,10 @@
 ;##########################################################################
-;   C:\Users\User\Desktop\python-can\test\data\test_CanMessage_V1_0_BUS1.trc
+;   C:\NewFileName_BUS1.trc
 ;
-;    CAN activities imported from C:\Users\User\Desktop\python-can\test\data\test_CanMessage_V1_1.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 2.2.4.136
+;    Generated by PEAK-Converter Version 3.0.4.594
 ;
 ;    Columns description:
 ;    ~~~~~~~~~~~~~~~~~~~~~
@@ -26,3 +26,4 @@
      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 -pruN 4.5.0-1/test/data/test_CanMessage_V1_1.trc 4.6.1-1/test/data/test_CanMessage_V1_1.trc
--- 4.5.0-1/test/data/test_CanMessage_V1_1.trc	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/data/test_CanMessage_V1_1.trc	2025-08-12 07:32:01.000000000 +0000
@@ -22,4 +22,5 @@
      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 
+    11)     21097.1  Tx     00000100  8  00 00 00 00 00 00 00 00
+    12)     48937.6  Rx         0704  1  RTR
diff -pruN 4.5.0-1/test/data/test_CanMessage_V1_3.trc 4.6.1-1/test/data/test_CanMessage_V1_3.trc
--- 4.5.0-1/test/data/test_CanMessage_V1_3.trc	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/data/test_CanMessage_V1_3.trc	2025-08-12 07:32:01.000000000 +0000
@@ -1,13 +1,14 @@
 ;$FILEVERSION=1.3
 ;$STARTTIME=44548.6028595139
+;   C:\NewFileName_V1_3.trc
 ;
-;   C:\test.trc
-;   Start time: 18.12.2021 14:28:07.062.0
-;   Generated by PCAN-Explorer v5.4.0
+;   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  Bit rate
-;   1    PCAN   Untitled@pcan_usb          CAN       500 kbit/s
-;   2    PTCAN  PCANLight_USB_16@pcan_usb  CAN
+;   Bus  Name  Connection  Protocol
+;   N/A  N/A   N/A         CAN
 ;-------------------------------------------------------------------------------
 ;   Message Number
 ;   |         Time Offset (ms)
@@ -20,13 +21,15 @@
 ;   |         |       |    |       |    |   |    |
 ;   |         |       |    |       |    |   |    |
 ;---+-- ------+------ +- --+-- ----+--- +- -+-- -+ -- -- -- -- -- -- --
-     1)        17535.4 1  Tx    00000100 -  8    00 00 00 00 00 00 00 00
-     2)        17700.3 1  Tx    00000100 -  8    00 00 00 00 00 00 00 00
-     3)        17873.8 1  Tx    00000100 -  8    00 00 00 00 00 00 00 00
-     4)        19295.4 1  Tx        0000 -  8    00 00 00 00 00 00 00 00
-     5)        19500.6 1  Tx        0000 -  8    00 00 00 00 00 00 00 00
-     6)        19705.2 1  Tx        0000 -  8    00 00 00 00 00 00 00 00
-     7)        20592.7 1  Tx    00000100 -  8    00 00 00 00 00 00 00 00
-     8)        20798.6 1  Tx    00000100 -  8    00 00 00 00 00 00 00 00
-     9)        20956.0 1  Tx    00000100 -  8    00 00 00 00 00 00 00 00
-    10)        21097.1 1  Tx    00000100 -  8    00 00 00 00 00 00 00 00
+     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 -pruN 4.5.0-1/test/data/test_CanMessage_V2_0_BUS1.trc 4.6.1-1/test/data/test_CanMessage_V2_0_BUS1.trc
--- 4.5.0-1/test/data/test_CanMessage_V2_0_BUS1.trc	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/data/test_CanMessage_V2_0_BUS1.trc	2025-08-12 07:32:01.000000000 +0000
@@ -2,18 +2,18 @@
 ;$STARTTIME=44548.6028595139
 ;$COLUMNS=N,O,T,I,d,l,D
 ;
-;   C:\Users\User\Desktop\python-can\test\data\test_CanMessage_V2_0_BUS1.trc
+;   C:\test_CanMessage_V2_0_BUS1.trc
 ;   Start time: 18.12.2021 14:28:07.062.001
-;   Generated by PEAK-Converter Version 2.2.4.136
-;   Data imported from C:\Users\User\Desktop\python-can\test\data\test_CanMessage_V1_1.trc
+;   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      
+;   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
@@ -26,3 +26,4 @@
       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 -pruN 4.5.0-1/test/data/test_CanMessage_V2_1.trc 4.6.1-1/test/data/test_CanMessage_V2_1.trc
--- 4.5.0-1/test/data/test_CanMessage_V2_1.trc	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/data/test_CanMessage_V2_1.trc	2025-08-12 07:32:01.000000000 +0000
@@ -2,19 +2,19 @@
 ;$STARTTIME=44548.6028595139
 ;$COLUMNS=N,O,T,B,I,d,R,L,D
 ;
-;   C:\Users\User\Desktop\python-can\test\data\test_CanMessage_V2_1.trc
+;   C:\test_CanMessage_V2_1.trc
 ;   Start time: 18.12.2021 14:28:07.062.001
-;   Generated by PEAK-Converter Version 2.2.4.136
-;   Data imported from C:\Users\User\Desktop\python-can\test\data\test_CanMessage_V1_1.trc
+;   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      
+;   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
@@ -27,3 +27,4 @@
       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 -pruN 4.5.0-1/test/listener_test.py 4.6.1-1/test/listener_test.py
--- 4.5.0-1/test/listener_test.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/listener_test.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,7 +1,6 @@
 #!/usr/bin/env python
 
-"""
-"""
+""" """
 import asyncio
 import logging
 import os
@@ -160,8 +159,13 @@ class ListenerTest(BusTest):
 
 
 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=asyncio.get_event_loop())
+    can.AsyncBufferedReader(loop=loop)
     assert len(recwarn) > 0
     assert recwarn.pop(DeprecationWarning)
     recwarn.clear()
diff -pruN 4.5.0-1/test/logformats_test.py 4.6.1-1/test/logformats_test.py
--- 4.5.0-1/test/logformats_test.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/logformats_test.py	2025-08-12 07:32:01.000000000 +0000
@@ -137,6 +137,7 @@ class ReaderWriterTest(unittest.TestCase
         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
@@ -187,6 +188,7 @@ class ReaderWriterTest(unittest.TestCase
         self.reader_constructor = reader_constructor
         self.binary_file = binary_file
         self.test_append_enabled = test_append
+        self.assert_file_closed = assert_file_closed
 
         ComparingMessagesTestCase.__init__(
             self,
@@ -212,7 +214,7 @@ class ReaderWriterTest(unittest.TestCase
         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")
@@ -220,7 +222,7 @@ class ReaderWriterTest(unittest.TestCase
         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
@@ -243,7 +245,7 @@ class ReaderWriterTest(unittest.TestCase
             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
@@ -251,7 +253,7 @@ class ReaderWriterTest(unittest.TestCase
         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;
@@ -274,7 +276,7 @@ class ReaderWriterTest(unittest.TestCase
         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")
@@ -283,7 +285,7 @@ class ReaderWriterTest(unittest.TestCase
         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
@@ -307,7 +309,7 @@ class ReaderWriterTest(unittest.TestCase
             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
@@ -316,7 +318,7 @@ class ReaderWriterTest(unittest.TestCase
         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;
@@ -380,7 +382,7 @@ class ReaderWriterTest(unittest.TestCase
                 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())
 
@@ -694,7 +696,6 @@ class TestBlfFileFormat(ReaderWriterTest
             check_fd=True,
             check_comments=False,
             test_append=True,
-            allowed_timestamp_delta=1.0e-6,
             preserves_channel=False,
             adds_default_channel=0,
         )
@@ -789,6 +790,30 @@ class TestBlfFileFormat(ReaderWriterTest
             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"""
@@ -848,6 +873,7 @@ class TestSqliteDatabaseFormat(ReaderWri
             check_comments=False,
             preserves_channel=False,
             adds_default_channel=None,
+            assert_file_closed=False,
         )
 
     @unittest.skip("not implemented")
@@ -967,6 +993,7 @@ class TestTrcFileFormatGen(TestTrcFileFo
             ("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),
         ]
     )
@@ -1006,6 +1033,20 @@ class TestTrcFileFormatGen(TestTrcFileFo
                     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),
@@ -1017,6 +1058,7 @@ class TestTrcFileFormatGen(TestTrcFileFo
                 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)
diff -pruN 4.5.0-1/test/notifier_test.py 4.6.1-1/test/notifier_test.py
--- 4.5.0-1/test/notifier_test.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/notifier_test.py	2025-08-12 07:32:01.000000000 +0000
@@ -12,16 +12,19 @@ class NotifierTest(unittest.TestCase):
         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):
         with can.Bus(0, interface="virtual", receive_own_messages=True) as bus1:
             with 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)
@@ -33,6 +36,39 @@ class NotifierTest(unittest.TestCase):
                 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)
+
+    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,))
+
+            # 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):
diff -pruN 4.5.0-1/test/serial_test.py 4.6.1-1/test/serial_test.py
--- 4.5.0-1/test/serial_test.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/serial_test.py	2025-08-12 07:32:01.000000000 +0000
@@ -84,20 +84,38 @@ class SimpleSerialTestBase(ComparingMess
         msg_receive = self.bus.recv()
         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.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.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)
@@ -137,6 +155,28 @@ class SimpleSerialTestBase(ComparingMess
         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
diff -pruN 4.5.0-1/test/test_bridge.py 4.6.1-1/test/test_bridge.py
--- 4.5.0-1/test/test_bridge.py	1970-01-01 00:00:00.000000000 +0000
+++ 4.6.1-1/test/test_bridge.py	2025-08-12 07:32:01.000000000 +0000
@@ -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 -pruN 4.5.0-1/test/test_cli.py 4.6.1-1/test/test_cli.py
--- 4.5.0-1/test/test_cli.py	1970-01-01 00:00:00.000000000 +0000
+++ 4.6.1-1/test/test_cli.py	2025-08-12 07:32:01.000000000 +0000
@@ -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 -pruN 4.5.0-1/test/test_interface_canalystii.py 4.6.1-1/test/test_interface_canalystii.py
--- 4.5.0-1/test/test_interface_canalystii.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/test_interface_canalystii.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,7 +1,6 @@
 #!/usr/bin/env python
 
-"""
-"""
+""" """
 
 import unittest
 from ctypes import c_ubyte
diff -pruN 4.5.0-1/test/test_kvaser.py 4.6.1-1/test/test_kvaser.py
--- 4.5.0-1/test/test_kvaser.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/test_kvaser.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,7 +1,6 @@
 #!/usr/bin/env python
 
-"""
-"""
+""" """
 
 import ctypes
 import time
@@ -278,6 +277,19 @@ class KvaserTest(unittest.TestCase):
         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
diff -pruN 4.5.0-1/test/test_logger.py 4.6.1-1/test/test_logger.py
--- 4.5.0-1/test/test_logger.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/test_logger.py	2025-08-12 07:32:01.000000000 +0000
@@ -14,6 +14,7 @@ from unittest.mock import Mock
 import pytest
 
 import can
+import can.cli
 import can.logger
 
 
@@ -89,7 +90,7 @@ class TestLoggerScriptModule(unittest.Te
             "--bitrate",
             "250000",
             "--fd",
-            "--data_bitrate",
+            "--data-bitrate",
             "2000000",
         ]
         can.logger.main()
@@ -111,7 +112,7 @@ class TestLoggerScriptModule(unittest.Te
             "--bitrate",
             "250000",
             "--fd",
-            "--data_bitrate",
+            "--data-bitrate",
             "2000000",
             "--receive-own-messages=True",
         ]
@@ -205,7 +206,7 @@ class TestLoggerScriptModule(unittest.Te
             "--offset=1.5",
             "--tseg1-abr=127",
         ]
-        parsed_args = can.logger._parse_additional_config(unknown_args)
+        parsed_args = can.cli._parse_additional_config(unknown_args)
 
         assert "app_name" in parsed_args
         assert parsed_args["app_name"] == "CANalyzer"
@@ -232,16 +233,16 @@ class TestLoggerScriptModule(unittest.Te
         assert parsed_args["tseg1_abr"] == 127
 
         with pytest.raises(ValueError):
-            can.logger._parse_additional_config(["--wrong-format"])
+            can.cli._parse_additional_config(["--wrong-format"])
 
         with pytest.raises(ValueError):
-            can.logger._parse_additional_config(["-wrongformat=value"])
+            can.cli._parse_additional_config(["-wrongformat=value"])
 
         with pytest.raises(ValueError):
-            can.logger._parse_additional_config(["--wrongformat=value1 value2"])
+            can.cli._parse_additional_config(["--wrongformat=value1 value2"])
 
         with pytest.raises(ValueError):
-            can.logger._parse_additional_config(["wrongformat="])
+            can.cli._parse_additional_config(["wrongformat="])
 
 
 class TestLoggerCompressedFile(unittest.TestCase):
diff -pruN 4.5.0-1/test/test_neovi.py 4.6.1-1/test/test_neovi.py
--- 4.5.0-1/test/test_neovi.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/test_neovi.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,7 +1,6 @@
 #!/usr/bin/env python
 
-"""
-"""
+""" """
 import pickle
 import unittest
 
diff -pruN 4.5.0-1/test/test_pcan.py 4.6.1-1/test/test_pcan.py
--- 4.5.0-1/test/test_pcan.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/test_pcan.py	2025-08-12 07:32:01.000000000 +0000
@@ -232,6 +232,7 @@ class TestPCANBus(unittest.TestCase):
         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)])
@@ -255,6 +256,7 @@ class TestPCANBus(unittest.TestCase):
         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=([], [], []))
diff -pruN 4.5.0-1/test/test_rotating_loggers.py 4.6.1-1/test/test_rotating_loggers.py
--- 4.5.0-1/test/test_rotating_loggers.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/test_rotating_loggers.py	2025-08-12 07:32:01.000000000 +0000
@@ -29,7 +29,7 @@ class TestBaseRotatingLogger:
                 suffix = Path(file).suffix.lower()
                 if suffix not in self._supported_formats:
                     raise ValueError(f"Unsupported file format: {suffix}")
-                self._writer = can.Printer(file=file)
+                self._writer = can.Logger(filename=file)
 
             @property
             def writer(self) -> FileIOMessageWriter:
@@ -59,26 +59,20 @@ class TestBaseRotatingLogger:
         assert hasattr(can.io.BaseRotatingLogger, "do_rollover")
 
     def test_get_new_writer(self, tmp_path):
-        with self._get_instance(tmp_path / "__unused.txt") as logger_instance:
-            writer = logger_instance._get_new_writer(tmp_path / "file.ASC")
-            assert isinstance(writer, can.ASCWriter)
-            writer.stop()
-
-            writer = logger_instance._get_new_writer(tmp_path / "file.BLF")
-            assert isinstance(writer, can.BLFWriter)
-            writer.stop()
-
-            writer = logger_instance._get_new_writer(tmp_path / "file.CSV")
-            assert isinstance(writer, can.CSVWriter)
-            writer.stop()
-
-            writer = logger_instance._get_new_writer(tmp_path / "file.LOG")
-            assert isinstance(writer, can.CanutilsLogWriter)
-            writer.stop()
-
-            writer = logger_instance._get_new_writer(tmp_path / "file.TXT")
-            assert isinstance(writer, can.Printer)
-            writer.stop()
+        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:
@@ -89,63 +83,61 @@ class TestBaseRotatingLogger:
             assert logger_instance.rotation_filename(default_name) == "default_by_namer"
 
     def test_rotate_without_rotator(self, tmp_path):
-        with self._get_instance(tmp_path / "__unused.txt") as logger_instance:
-            source = str(tmp_path / "source.txt")
-            dest = str(tmp_path / "dest.txt")
+        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
+        assert os.path.exists(source) is False
+        assert os.path.exists(dest) is False
 
-            logger_instance._writer = logger_instance._get_new_writer(source)
-            logger_instance.stop()
+        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
+        assert os.path.exists(source) is True
+        assert os.path.exists(dest) is False
 
-            logger_instance.rotate(source, dest)
+        logger_instance.rotate(source, dest)
 
-            assert os.path.exists(source) is False
-            assert os.path.exists(dest) is True
+        assert os.path.exists(source) is False
+        assert os.path.exists(dest) is True
 
     def test_rotate_with_rotator(self, tmp_path):
-        with self._get_instance(tmp_path / "__unused.txt") as logger_instance:
-            rotator_func = Mock()
-            logger_instance.rotator = rotator_func
-
-            source = str(tmp_path / "source.txt")
-            dest = str(tmp_path / "dest.txt")
+        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
-
-            logger_instance._writer = logger_instance._get_new_writer(source)
-            logger_instance.stop()
+        assert os.path.exists(source) is False
+        assert os.path.exists(dest) is False
 
-            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
+        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
-            original_stop = logger_instance.writer.stop
-            mock_stop = Mock()
+            mock_stop = Mock(side_effect=logger_instance.writer.stop)
             logger_instance.writer.stop = mock_stop
 
             logger_instance.stop()
             mock_stop.assert_called()
 
-            # close file.ASC to enable cleanup of temp_dir
-            original_stop()
-
     def test_on_message_received(self, tmp_path):
         with self._get_instance(tmp_path / "file.ASC") as logger_instance:
             # Test without rollover
@@ -181,12 +173,9 @@ class TestBaseRotatingLogger:
             writers_on_message_received.assert_called_with(msg)
 
     def test_issue_1792(self, tmp_path):
-        with self._get_instance(tmp_path / "__unused.log") as logger_instance:
-            writer = logger_instance._get_new_writer(
-                tmp_path / "2017_Jeep_Grand_Cherokee_3.6L_V6.log"
-            )
-            assert isinstance(writer, can.CanutilsLogWriter)
-            writer.stop()
+        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:
diff -pruN 4.5.0-1/test/test_scripts.py 4.6.1-1/test/test_scripts.py
--- 4.5.0-1/test/test_scripts.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/test_scripts.py	2025-08-12 07:32:01.000000000 +0000
@@ -98,6 +98,20 @@ class TestPlayerScript(CanScriptTest):
         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 = [
diff -pruN 4.5.0-1/test/test_slcan.py 4.6.1-1/test/test_slcan.py
--- 4.5.0-1/test/test_slcan.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/test_slcan.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,9 +1,9 @@
 #!/usr/bin/env python
 
-import unittest
-from typing import cast
+import unittest.mock
+from typing import cast, Optional
 
-import serial
+from serial.serialutil import SerialBase
 
 import can.interfaces.slcan
 
@@ -21,20 +21,75 @@ https://realpython.com/pypy-faster-pytho
 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),
+            can.Bus(
+                "loop://",
+                interface="slcan",
+                sleep_after_open=0,
+                timeout=TIMEOUT,
+                bitrate=500000,
+            ),
         )
-        self.serial = cast(serial.Serial, self.bus.serialPortOrig)
+        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.write(b"T12ABCDEF2AA55\r")
+        self.serial.set_input_buffer(b"T12ABCDEF2AA55\r")
         msg = self.bus.recv(TIMEOUT)
         self.assertIsNotNone(msg)
         self.assertEqual(msg.arbitration_id, 0x12ABCDEF)
@@ -44,7 +99,7 @@ class slcanTestCase(unittest.TestCase):
         self.assertSequenceEqual(msg.data, [0xAA, 0x55])
 
         # Ewert Energy Systems CANDapter specific
-        self.serial.write(b"x12ABCDEF2AA55\r")
+        self.serial.set_input_buffer(b"x12ABCDEF2AA55\r")
         msg = self.bus.recv(TIMEOUT)
         self.assertIsNotNone(msg)
         self.assertEqual(msg.arbitration_id, 0x12ABCDEF)
@@ -54,15 +109,19 @@ class slcanTestCase(unittest.TestCase):
         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.write(b"t4563112233\r")
+        self.serial.set_input_buffer(b"t4563112233\r")
         msg = self.bus.recv(TIMEOUT)
         self.assertIsNotNone(msg)
         self.assertEqual(msg.arbitration_id, 0x456)
@@ -72,15 +131,19 @@ class slcanTestCase(unittest.TestCase):
         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.write(b"r1238\r")
+        self.serial.set_input_buffer(b"r1238\r")
         msg = self.bus.recv(TIMEOUT)
         self.assertIsNotNone(msg)
         self.assertEqual(msg.arbitration_id, 0x123)
@@ -89,15 +152,19 @@ class slcanTestCase(unittest.TestCase):
         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.write(b"R12ABCDEF6\r")
+        self.serial.set_input_buffer(b"R12ABCDEF6\r")
         msg = self.bus.recv(TIMEOUT)
         self.assertIsNotNone(msg)
         self.assertEqual(msg.arbitration_id, 0x12ABCDEF)
@@ -106,19 +173,281 @@ class slcanTestCase(unittest.TestCase):
         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.write(b"T12ABCDEF")
+        self.serial.set_input_buffer(b"T12ABCDEF")
         msg = self.bus.recv(TIMEOUT)
         self.assertIsNone(msg)
 
-        self.serial.write(b"2AA55\rT12")
+        self.serial.set_input_buffer(b"2AA55\rT12")
         msg = self.bus.recv(TIMEOUT)
         self.assertIsNotNone(msg)
         self.assertEqual(msg.arbitration_id, 0x12ABCDEF)
@@ -130,28 +459,21 @@ class slcanTestCase(unittest.TestCase):
         msg = self.bus.recv(TIMEOUT)
         self.assertIsNone(msg)
 
-        self.serial.write(b"ABCDEF2AA55\r")
+        self.serial.set_input_buffer(b"ABCDEF2AA55\r")
         msg = self.bus.recv(TIMEOUT)
         self.assertIsNotNone(msg)
 
     def test_version(self):
-        self.serial.write(b"V1013\r")
         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)
 
-        hw_ver, sw_ver = self.bus.get_version(0)
-        self.assertIsNone(hw_ver)
-        self.assertIsNone(sw_ver)
-
     def test_serial_number(self):
-        self.serial.write(b"NA123\r")
         sn = self.bus.get_serial_number(0)
+        self.assertEqual(b"N\r", self.serial.get_output_buffer())
         self.assertEqual(sn, "A123")
 
-        sn = self.bus.get_serial_number(0)
-        self.assertIsNone(sn)
-
 
 if __name__ == "__main__":
     unittest.main()
diff -pruN 4.5.0-1/test/test_socketcan.py 4.6.1-1/test/test_socketcan.py
--- 4.5.0-1/test/test_socketcan.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/test_socketcan.py	2025-08-12 07:32:01.000000000 +0000
@@ -5,6 +5,7 @@ Test functions in `can.interfaces.socket
 """
 import ctypes
 import struct
+import sys
 import unittest
 import warnings
 from unittest.mock import patch
@@ -34,6 +35,7 @@ class SocketCANTest(unittest.TestCase):
         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(
@@ -103,6 +105,7 @@ class SocketCANTest(unittest.TestCase):
         ]
         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(
@@ -172,6 +175,7 @@ class SocketCANTest(unittest.TestCase):
         ]
         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(
@@ -373,7 +377,7 @@ class SocketCANTest(unittest.TestCase):
 
         This test shall document raw CAN socket support under PyPy. Once this test fails, it is likely that PyPy
         either implemented raw CAN socket support or at least changed the error that is thrown.
-        https://foss.heptapod.net/pypy/pypy/-/issues/3809
+        https://github.com/pypy/pypy/issues/3808
         https://github.com/hardbyte/python-can/issues/1479
         """
         try:
@@ -382,7 +386,7 @@ class SocketCANTest(unittest.TestCase):
             if "unknown address family" not in str(e):
                 warnings.warn(
                     "Please check if PyPy has implemented raw CAN socket support! "
-                    "See: https://foss.heptapod.net/pypy/pypy/-/issues/3809"
+                    "See: https://github.com/pypy/pypy/issues/3808"
                 )
 
 
diff -pruN 4.5.0-1/test/test_socketcand.py 4.6.1-1/test/test_socketcand.py
--- 4.5.0-1/test/test_socketcand.py	1970-01-01 00:00:00.000000000 +0000
+++ 4.6.1-1/test/test_socketcand.py	2025-08-12 07:32:01.000000000 +0000
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+
+import unittest
+import can
+from can.interfaces.socketcand import socketcand
+
+
+class TestConvertAsciiMessageToCanMessage(unittest.TestCase):
+    def test_valid_frame_message(self):
+        # Example: < frame 123 1680000000.0 01020304 >
+        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 -pruN 4.5.0-1/test/test_util.py 4.6.1-1/test/test_util.py
--- 4.5.0-1/test/test_util.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/test_util.py	2025-08-12 07:32:01.000000000 +0000
@@ -209,6 +209,29 @@ class TestBusConfig(unittest.TestCase):
         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:
diff -pruN 4.5.0-1/test/test_vector.py 4.6.1-1/test/test_vector.py
--- 4.5.0-1/test/test_vector.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/test_vector.py	2025-08-12 07:32:01.000000000 +0000
@@ -1042,142 +1042,142 @@ def _find_virtual_can_serial() -> int:
 
 
 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"\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\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\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\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\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\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\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"\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"\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\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\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"\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\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\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\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"\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\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\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\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\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\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\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"\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\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"\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"\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\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"\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"\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\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"\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"
 )
diff -pruN 4.5.0-1/test/test_viewer.py 4.6.1-1/test/test_viewer.py
--- 4.5.0-1/test/test_viewer.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/test_viewer.py	2025-08-12 07:32:01.000000000 +0000
@@ -397,19 +397,19 @@ class CanViewerTest(unittest.TestCase):
             )
 
     def test_parse_args(self):
-        parsed_args, _, _ = _parse_viewer_args(["-b", "250000"])
+        parsed_args, _ = _parse_viewer_args(["-b", "250000"])
         self.assertEqual(parsed_args.bitrate, 250000)
 
-        parsed_args, _, _ = _parse_viewer_args(["--bitrate", "500000"])
+        parsed_args, _ = _parse_viewer_args(["--bitrate", "500000"])
         self.assertEqual(parsed_args.bitrate, 500000)
 
-        parsed_args, _, _ = _parse_viewer_args(["-c", "can0"])
+        parsed_args, _ = _parse_viewer_args(["-c", "can0"])
         self.assertEqual(parsed_args.channel, "can0")
 
-        parsed_args, _, _ = _parse_viewer_args(["--channel", "PCAN_USBBUS1"])
+        parsed_args, _ = _parse_viewer_args(["--channel", "PCAN_USBBUS1"])
         self.assertEqual(parsed_args.channel, "PCAN_USBBUS1")
 
-        parsed_args, data_structs, _ = _parse_viewer_args(["-d", "100:<L"])
+        parsed_args, data_structs = _parse_viewer_args(["-d", "100:<L"])
         self.assertEqual(parsed_args.decode, ["100:<L"])
 
         self.assertIsInstance(data_structs, dict)
@@ -422,7 +422,7 @@ class CanViewerTest(unittest.TestCase):
         f = open("test.txt", "w")
         f.write("100:<BB\n101:<HH\n")
         f.close()
-        parsed_args, data_structs, _ = _parse_viewer_args(["-d", "test.txt"])
+        parsed_args, data_structs = _parse_viewer_args(["-d", "test.txt"])
 
         self.assertIsInstance(data_structs, dict)
         self.assertEqual(len(data_structs), 2)
@@ -436,7 +436,7 @@ class CanViewerTest(unittest.TestCase):
         self.assertEqual(data_structs[0x101].size, 4)
         os.remove("test.txt")
 
-        parsed_args, data_structs, _ = _parse_viewer_args(
+        parsed_args, data_structs = _parse_viewer_args(
             ["--decode", "100:<LH:10.:100.", "101:<ff", "102:<Bf:1:57.3"]
         )
         self.assertEqual(
@@ -469,13 +469,13 @@ class CanViewerTest(unittest.TestCase):
         self.assertEqual(data_structs[0x102][1], 1)
         self.assertAlmostEqual(data_structs[0x102][2], 57.3)
 
-        parsed_args, _, _ = _parse_viewer_args(["-f", "100:7FF"])
+        parsed_args, _ = _parse_viewer_args(["--filter", "100:7FF"])
         self.assertIsInstance(parsed_args.can_filters, list)
         self.assertIsInstance(parsed_args.can_filters[0], dict)
         self.assertEqual(parsed_args.can_filters[0]["can_id"], 0x100)
         self.assertEqual(parsed_args.can_filters[0]["can_mask"], 0x7FF)
 
-        parsed_args, _, _ = _parse_viewer_args(["-f", "101:7FF", "102:7FC"])
+        parsed_args, _ = _parse_viewer_args(["--filter", "101:7FF", "102:7FC"])
         self.assertIsInstance(parsed_args.can_filters, list)
         self.assertIsInstance(parsed_args.can_filters[0], dict)
         self.assertIsInstance(parsed_args.can_filters[1], dict)
@@ -485,18 +485,18 @@ class CanViewerTest(unittest.TestCase):
         self.assertEqual(parsed_args.can_filters[1]["can_mask"], 0x7FC)
 
         with self.assertRaises(SystemExit):
-            _parse_viewer_args(["-f", "101,7FF"])
+            _parse_viewer_args(["--filter", "101,7FF"])
 
-        parsed_args, _, _ = _parse_viewer_args(["--filter", "100~7FF"])
+        parsed_args, _ = _parse_viewer_args(["--filter", "100~7FF"])
         self.assertIsInstance(parsed_args.can_filters, list)
         self.assertIsInstance(parsed_args.can_filters[0], dict)
         self.assertEqual(parsed_args.can_filters[0]["can_id"], 0x100 | 0x20000000)
         self.assertEqual(parsed_args.can_filters[0]["can_mask"], 0x7FF & 0x20000000)
 
-        parsed_args, _, _ = _parse_viewer_args(["-i", "socketcan"])
+        parsed_args, _ = _parse_viewer_args(["-i", "socketcan"])
         self.assertEqual(parsed_args.interface, "socketcan")
 
-        parsed_args, _, _ = _parse_viewer_args(["--interface", "pcan"])
+        parsed_args, _ = _parse_viewer_args(["--interface", "pcan"])
         self.assertEqual(parsed_args.interface, "pcan")
 
         # Make sure it exits with the correct error code when displaying the help page
diff -pruN 4.5.0-1/test/zero_dlc_test.py 4.6.1-1/test/zero_dlc_test.py
--- 4.5.0-1/test/zero_dlc_test.py	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/test/zero_dlc_test.py	2025-08-12 07:32:01.000000000 +0000
@@ -1,7 +1,6 @@
 #!/usr/bin/env python
 
-"""
-"""
+""" """
 
 import logging
 import unittest
diff -pruN 4.5.0-1/tox.ini 4.6.1-1/tox.ini
--- 4.5.0-1/tox.ini	2024-11-28 05:59:18.000000000 +0000
+++ 4.6.1-1/tox.ini	2025-08-12 07:32:01.000000000 +0000
@@ -1,51 +1,83 @@
+# https://tox.wiki/en/latest/config.html
 [tox]
+min_version = 4.26
+env_list = py,lint,type,docs
 
 [testenv]
-deps =
-    pytest==8.3.*
-    pytest-timeout==2.1.*
-    coveralls==3.3.1
-    pytest-cov==4.0.0
-    coverage==6.5.0
-    hypothesis~=6.35.0
-    pyserial~=3.5
-    parameterized~=0.8
-    asammdf>=6.0; platform_python_implementation=="CPython" and python_version<"3.13"
-    pywin32>=305; platform_system=="Windows" and platform_python_implementation=="CPython" and python_version<"3.14"
-
-commands =
-    pytest {posargs}
-
-extras =
-    canalystii
-
-[testenv:gh]
 passenv =
     CI
     GITHUB_*
     COVERALLS_*
     PY_COLORS
     TEST_SOCKETCAN
+dependency_groups =
+    test
+extras =
+    canalystii
+    mf4
+    multicast
+    pywin32
+    serial
+    viewer
+commands =
+    pytest {posargs}
 
-[testenv:docs]
-description = Build and test the documentation
-basepython = py312
-deps =
-    -r doc/doc-requirements.txt
-    gs-usb
+[testenv:py314]
+extras =
+    canalystii
+    serial
+    pywin32
 
+[testenv:{py313t,py314t,pypy310,pypy311}]
 extras =
     canalystii
+    serial
 
+[testenv:docs]
+description = Build and test the documentation
+basepython = py313
+dependency_groups =
+    docs
+extras =
+    canalystii
+    gs-usb
 commands =
     python -m sphinx -b html    -Wan --keep-going doc build
     python -m sphinx -b doctest -W   --keep-going doc build
 
+[testenv:lint]
+description = Run linters
+basepython = py313
+dependency_groups =
+    lint
+extras =
+    viewer
+commands =
+    black --check .
+    ruff check can examples doc
+    pylint \
+        can/**.py \
+        can/io \
+        doc/conf.py \
+        examples/**.py \
+        can/interfaces/socketcan
+
+[testenv:type]
+description = Run type checker
+basepython = py313
+dependency_groups =
+    lint
+extras =
+commands =
+    mypy --python-version 3.9  .
+    mypy --python-version 3.10 .
+    mypy --python-version 3.11 .
+    mypy --python-version 3.12 .
+    mypy --python-version 3.13 .
 
 [pytest]
 testpaths = test
-addopts = -v --timeout=300 --cov=can --cov-config=tox.ini --cov-report=lcov --cov-report=term
-
+addopts = -v --timeout=300 --cov=can --cov-config=tox.ini --cov-report=lcov --cov-report=term --color=yes
 
 [coverage:run]
 # we could also use branch coverage
