pax_global_header00006660000000000000000000000064147220032660014514gustar00rootroot0000000000000052 comment=654a02ae24bfc50bf1bb1fad7aab4aa88763d302 python-can-4.5.0/000077500000000000000000000000001472200326600136025ustar00rootroot00000000000000python-can-4.5.0/CHANGELOG.md000066400000000000000000001102551472200326600154170ustar00rootroot00000000000000Version 4.5.0 ============= Features -------- * gs_usb command-line support (and documentation updates and stability fixes) by @BenGardiner in https://github.com/hardbyte/python-can/pull/1790 * Faster and more general MF4 support by @cssedev in https://github.com/hardbyte/python-can/pull/1892 * ASCWriter speed improvement by @pierreluctg in https://github.com/hardbyte/python-can/pull/1856 * Faster Message string representation by @pierreluctg in https://github.com/hardbyte/python-can/pull/1858 * Added Netronic's CANdo and CANdoISO adapters interface by @belliriccardo in https://github.com/hardbyte/python-can/pull/1887 * Add autostart option to BusABC.send_periodic() to fix issue #1848 by @SWolfSchunk in https://github.com/hardbyte/python-can/pull/1853 * Improve TestBusConfig by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1804 * Improve speed of TRCReader by @lebuni in https://github.com/hardbyte/python-can/pull/1893 Bug Fixes --------- * Fix Kvaser timestamp by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1878 * Set end_time in ThreadBasedCyclicSendTask.start() by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1871 * Fix regex in _parse_additional_config() by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1868 * Fix for #1849 (PCAN fails when PCAN_ERROR_ILLDATA is read via ReadFD) by @bures in https://github.com/hardbyte/python-can/pull/1850 * Period must be >= 1ms for BCM using Win32 API by @pierreluctg in https://github.com/hardbyte/python-can/pull/1847 * Fix ASCReader Crash on "Start of Measurement" Line by @RitheeshBaradwaj in https://github.com/hardbyte/python-can/pull/1811 * Resolve AttributeError within NicanError by @vijaysubbiah20 in https://github.com/hardbyte/python-can/pull/1806 Miscellaneous ------------- * Fix CI by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1889 * Update msgpack dependency by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1875 * Add tox environment for doctest by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1870 * Use typing_extensions.TypedDict on python < 3.12 for pydantic support by @NickCao in https://github.com/hardbyte/python-can/pull/1845 * Replace PyPy3.8 with PyPy3.10 by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1838 * Fix slcan tests by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1834 * Test on Python 3.13 by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1833 * Stop notifier in examples by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1814 * Use setuptools_scm by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1810 * Added extra info for Kvaser dongles by @FedericoSpada in https://github.com/hardbyte/python-can/pull/1797 * Socketcand: show actual response as well as expected in error by @liamkinne in https://github.com/hardbyte/python-can/pull/1807 * Refactor CLI filter parsing, add tests by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1805 * Add zlgcan to docs by @zariiii9003 in https://github.com/hardbyte/python-can/pull/1839 Version 4.4.2 ============= Bug Fixes --------- * Remove `abstractmethod` decorator from `Listener.stop()` (#1770, #1795) * Fix `SizedRotatingLogger` file suffix bug (#1792, #1793) * gs_usb: Use `BitTiming` class internally to configure bitrate (#1747, #1748) * pcan: Fix unpack error in `PcanBus._detect_available_configs()` (#1767) * socketcan: Improve error handling in `SocketcanBus.__init__()` (#1771) * socketcan: Do not log exception on non-linux platforms (#1800) * vector, kvaser: Activate channels after CAN filters were applied (#1413, #1708, #1796) Features -------- * kvaser: Add support for non-ISO CAN FD (#1752) * neovi: Return timestamps relative to epoch (#1789) * slcan: Support CANdapter extended length arbitration ID (#1506, #1528) * slcan: Add support for `listen_only` mode (#1496) * vector: Add support for `listen_only` mode (#1764) Version 4.4.0 ============= Features -------- * TRC 1.3 Support: Added support for .trc log files as generated by PCAN Explorer v5 and other tools, expanding compatibility with common log file formats (#1753). * ASCReader refactor: improved the ASCReader code (#1717). * SYSTEC Interface Enhancements: Added the ability to pass an explicit DLC value to the send() method when using the SYSTEC interface, enhancing flexibility for message definitions (#1756). * Socketcand Beacon Detection: Introduced a feature for detecting socketcand beacons, facilitating easier connection and configuration with socketcand servers (#1687). * PCAN Driver Echo Frames: Enabled echo frames in the PCAN driver when receive_own_messages is set, improving feedback for message transmissions (#1723). * CAN FD Bus Connection for VectorBus: Enabled connecting to CAN FD buses without specifying bus timings, simplifying the connection process for users (#1716). * Neousys Configs Detection: Updated the detection mechanism for available Neousys configurations, ensuring more accurate and comprehensive configuration discovery (#1744). Bug Fixes --------- * Send Periodic Messages: Fixed an issue where fixed-duration periodic messages were sent one extra time beyond their intended count (#1713). * Vector Interface on Windows 11: Addressed compatibility issues with the Vector interface on Windows 11, ensuring stable operation across the latest OS version (#1731). * ASCWriter Millisecond Handling: Corrected the handling of milliseconds in ASCWriter, ensuring accurate time representation in log files (#1734). * Various minor bug fixes: Addressed several minor bugs to improve overall stability and performance. Miscellaneous ------------- * Invert default value logic for BusABC._is_shutdown. (#1774) * Implemented various logging enhancements to provide more detailed and useful operational insights (#1703). * Updated CI to use OIDC for connecting GitHub Actions to PyPi, improving security and access control for CI workflows. * Fix CI to work for MacOS (#1772). * The release also includes various other minor enhancements and bug fixes aimed at improving the reliability and performance of the software. Version 4.3.1 ============= Bug Fixes --------- * Fix socketcand erroneously discarding frames (#1700) * Fix initialization order in EtasBus (#1693, #1704) Documentation ------------- * Fix install instructions for neovi (#1694, #1697) Version 4.3.0 ============= Breaking Changes ---------------- * Raise Minimum Python Version to 3.8 (#1597) * Do not stop notifier if exception was handled (#1645) Bug Fixes --------- * Vector: channel detection fails, if there is an active flexray channel (#1634) * ixxat: Fix exception in 'state' property on bus coupling errors (#1647) * NeoVi: Fixed serial number range (#1650) * PCAN: Fix timestamp offset due to timezone (#1651) * Catch `pywintypes.error` in broadcast manager (#1659) * Fix BLFReader error for incomplete or truncated stream (#1662) * PCAN: remove Windows registry check to fix 32bit compatibility (#1672) * Vector: Skip the `can_op_mode check` if the device reports `can_op_mode=0` (#1678) * Vector: using the config from `detect_available_configs` might raise XL_ERR_INVALID_CHANNEL_MASK error (#1681) Features -------- ### API * Add `modifier_callback` parameter to `BusABC.send_periodic` for auto-modifying cyclic tasks (#703) * Add `protocol` property to BusABC to determine active CAN Protocol (#1532) * Change Bus constructor implementation and typing (#1557) * Add optional `strict` parameter to relax BitTiming & BitTimingFd Validation (#1618) * Add `BitTiming.iterate_from_sample_point` static methods (#1671) ### IO * Can Player compatibility with interfaces that use additional configuration (#1610) ### Interface Improvements * Kvaser: Add BitTiming/BitTimingFd support to KvaserBus (#1510) * Ixxat: Implement `detect_available_configs` for the Ixxat bus. (#1607) * NeoVi: Enable send and receive on network ID above 255 (#1627) * Vector: Send HighPriority Message to flush Tx buffer (#1636) * PCAN: Optimize send performance (#1640) * PCAN: Support version string of older PCAN basic API (#1644) * Kvaser: add parameter exclusive and `override_exclusive` (#1660) * socketcand: Add parameter `tcp_tune` to reduce latency (#1683) ### Miscellaneous * Distinguish Text/Binary-IO for Reader/Writer classes. (#1585) * Convert setup.py to pyproject.toml (#1592) * activate ruff pycodestyle checks (#1602) * Update linter instructions in development.rst (#1603) * remove unnecessary script files (#1604) * BigEndian test fixes (#1625) * align `ID:` in can.Message string (#1635) * Use same configuration file as Linux on macOS (#1657) * We do not need to account for drift when we `USE_WINDOWS_EVENTS` (#1666, #1679) * Update linters, activate more ruff rules (#1669) * Add Python 3.12 Support / Test Python 3.12 (#1673) Version 4.2.2 ============= Bug Fixes --------- * Fix socketcan KeyError (#1598, #1599). * Fix IXXAT not properly shutdown message (#1606). * Fix Mf4Reader and TRCReader incompatibility with extra CLI args (#1610). * Fix decoding error in Kvaser constructor for non-ASCII product name (#1613). Version 4.2.1 ============= Bug Fixes --------- * The ASCWriter now logs the correct channel for error frames (#1578, #1583). * Fix PCAN library detection (#1579, #1580). * On Windows, the first two periodic frames were sent without delay (#1590). Version 4.2.0 ============= Breaking Changes ---------------- * The ``can.BitTiming`` class was replaced with the new ``can.BitTiming`` and `can.BitTimingFd` classes (#1468, #1515). Early adopters of ``can.BitTiming`` will need to update their code. Check the [documentation](https://python-can.readthedocs.io/en/develop/bit_timing.html) for more information. Currently, the following interfaces support the new classes: * canalystii (#1468) * cantact (#1468) * nixnet (#1520) * pcan (#1514) * vector (#1470, #1516) There are open pull requests for kvaser (#1510), slcan (#1512) and usb2can (#1511). Testing and reviewing of these open PRs would be most appreciated. Features -------- ### IO * Add support for MF4 files (#1289). * Add support for version 2 TRC files and other TRC file enhancements (#1530). ### Type Annotations * Export symbols to satisfy type checkers (#1547, #1551, #1558, #1568). ### Interface Improvements * Add ``__del__`` method to ``can.BusABC`` to automatically release resources (#1489, #1564). * pcan: Update PCAN Basic to 4.6.2.753 (#1481). * pcan: Use select instead of polling on Linux (#1410). * socketcan: Use ip link JSON output in ``find_available_interfaces`` (#1478). * socketcan: Enable SocketCAN interface tests in GitHub CI (#1484). * slcan: improve receiving performance (#1490). * usb2can: Stop using root logger (#1483). * usb2can: Faster channel detection on Windows (#1480). * vector: Only check sample point instead of tseg & sjw (#1486). * vector: add VN5611 hwtype (#1501). Documentation ------------- * Add new section about related tools to documentation. Add a list of plugin interface packages (#1457). Bug Fixes --------- * Automatic type conversion for config values (#1498, #1499). * pcan: Fix ``Bus.__new__`` for CAN-FD interfaces (#1458, #1460). * pcan: Fix Detection of Library on Windows on ARM (#1463). * socketcand: extended ID bug fixes (#1504, #1508). * vector: improve robustness against unknown HardwareType values (#1500, #1502). Deprecations ------------ * The ``bustype`` parameter of ``can.Bus`` is deprecated and will be removed in version 5.0, use ``interface`` instead. (#1462). * The ``context`` parameter of ``can.Bus`` is deprecated and will be removed in version 5.0, use ``config_context`` instead. (#1474). * The ``bit_timing`` parameter of ``CantactBus`` is deprecated and will be removed in version 5.0, use ``timing`` instead. (#1468). * The ``bit_timing`` parameter of ``CANalystIIBus`` is deprecated and will be removed in version 5.0, use ``timing`` instead. (#1468). * The ``brs`` and ``log_errors`` parameters of `` NiXNETcanBus`` are deprecated and will be removed in version 5.0. (#1520). Miscellaneous ------------- * Use high resolution timer on Windows to improve timing precision for BroadcastManager (#1449). * Improve ThreadBasedCyclicSendTask timing (#1539). * Make code examples executable on Linux (#1452). * Fix CanFilter type annotation (#1456). * Fix ``The entry_points().get`` deprecation warning and improve type annotation of ``can.interfaces.BACKENDS`` (#1465). * Add ``ignore_config`` parameter to ``can.Bus`` (#1474). * Add deprecation period to utility function ``deprecated_args_alias`` (#1477). * Add `ruff` to the CI system (#1551) Version 4.1.0 ============= Breaking Changes ---------------- * ``windows-curses`` was moved to optional dependencies (#1395). Use ``pip install python-can[viewer]`` if you are using the ``can.viewer`` script on Windows. * The attributes of ``can.interfaces.vector.VectorChannelConfig`` were renamed from camelCase to snake_case (#1422). Features -------- ### IO * The canutils logger preserves message direction (#1244) and uses common interface names (e.g. can0) instead of just channel numbers (#1271). * The ``can.logger`` script accepts the ``-a, --append`` option to add new data to an existing log file (#1326, #1327, #1361). Currently only the blf-, canutils- and csv-formats are supported. * All CLI ``extra_args`` are passed to the bus, logger and player initialisation (#1366). * Initial support for TRC files (#1217) ### Type Annotations * python-can now includes the ``py.typed`` marker to support type checking according to PEP 561 (#1344). ### Interface Improvements * The gs_usb interface can be selected by device index instead of USB bus/address. Loopback frames are now correctly marked with the ``is_rx`` flag (#1270). * The PCAN interface can be selected by its device ID instead of just the channel name (#1346). * The PCAN Bus implementation supports auto bus-off reset (#1345). * SocketCAN: Make ``find_available_interfaces()`` find slcanX interfaces (#1369). * Vector: Add xlGetReceiveQueueLevel, xlGenerateSyncPulse and xlFlushReceiveQueue to xldriver (#1387). * Vector: Raise a CanInitializationError, if the CAN settings can not be applied according to the arguments of ``VectorBus.__init__`` (#1426). * Ixxat bus now implements BusState api and detects errors (#1141) Bug Fixes --------- * Improve robustness of USB2CAN serial number detection (#1129). * Fix channel2int conversion (#1268, #1269). * Fix BLF timestamp conversion (#1266, #1273). * Fix timestamp handling in udp_multicast on macOS (#1275, #1278). * Fix failure to initiate the Neousys DLL (#1281). * Fix AttributeError in IscanError (#1292, #1293). * Add missing vector devices (#1296). * Fix error for DLC > 8 in ASCReader (#1299, #1301). * Set default mode for FileIOMessageWriter to wt instead of rt (#1303). * Fix conversion for port number from config file (#1309). * Fix fileno error on Windows (#1312, #1313, #1333). * Remove redundant ``writer.stop()`` call that throws error (#1316, #1317). * Detect and cast types of CLI ``extra_args`` (#1280, #1328). * Fix ASC/CANoe incompatibility due to timestamp format (#1315, #1362). * Fix MessageSync timings (#1372, #1374). * Fix file name for compressed files in SizedRotatingLogger (#1382, #1683). * Fix memory leak in neoVI bus where message_receipts grows with no limit (#1427). * Raise ValueError if gzip is used with incompatible log formats (#1429). * Allow restarting of transmission tasks for socketcan (#1440) Miscellaneous ------------- * Allow ICSApiError to be pickled and un-pickled (#1341) * Sort interface names in CLI API to make documentation reproducible (#1342) * Exclude repository-configuration from git-archive (#1343) * Improve documentation (#1397, #1401, #1405, #1420, #1421, #1434) * Officially support Python 3.11 (#1423) * Migrate code coverage reporting from Codecov to Coveralls (#1430) * Migrate building docs and publishing releases to PyPi from Travis-CI to GitHub Actions (#1433) Version 4.0.0 ==== TL;DR: This release includes a ton of improvements from 2.5 years of development! 🎉 Test thoroughly after switching. For more than two years, there was no major release of *python-can*. However, development was very much active over most of this time, and many parts were switched out and improved. Over this time, over 530 issues and PRs have been resolved or merged, and discussions took place in even more. Statistics of the final diff: About 200 files changed due to ~22k additions and ~7k deletions from more than thirty contributors. This changelog diligently lists the major changes but does not promise to be the complete list of changes. Therefore, users are strongly advised to thoroughly test their programs against this new version. Re-reading the documentation for your interfaces might be helpful too as limitations and capabilities might have changed or are more explicit. While we did try to avoid breaking changes, in some cases it was not feasible and in particular, many implementation details have changed. Major features -------------- * Type hints for the core library and some interfaces (#652 and many others) * Support for Python 3.7-3.10+ only (dropped support for Python 2.* and 3.5-3.6) (#528 and many others) * [Granular and unified exceptions](https://python-can.readthedocs.io/en/develop/api.html#errors) (#356, #562, #1025; overview in #1046) * [Support for automatic configuration detection](https://python-can.readthedocs.io/en/develop/api.html#can.detect_available_configs) in most interfaces (#303, #640, #641, #811, #1077, #1085) * Better alignment of interfaces and IO to common conventions and semantics New interfaces -------------- * udp_multicast (#644) * robotell (#731) * cantact (#853) * gs_usb (#905) * nixnet (#968, #1154) * neousys (#980, #1076) * socketcand (#1140) * etas (#1144) Improved interfaces ------------------- * socketcan * Support for multiple Cyclic Messages in Tasks (#610) * Socketcan crash when attempting to stop CyclicSendTask with same arbitration ID (#605, #638, #720) * Relax restriction of arbitration ID uniqueness for CyclicSendTask (#721, #785, #930) * Add nanosecond resolution time stamping to socketcan (#938, #1015) * Add support for changing the loopback flag (#960) * Socketcan timestamps are missing sub-second precision (#1021, #1029) * Add parameter to ignore CAN error frames (#1128) * socketcan_ctypes * Removed and replaced by socketcan after deprecation period * socketcan_native * Removed and replaced by socketcan after deprecation period * vector * Add chip state API (#635) * Add methods to handle non message events (#708) * Implement XLbusParams (#718) * Add support for VN8900 xlGetChannelTime function (#732, #733) * Add vector hardware config popup (#774) * Fix Vector CANlib treatment of empty app name (#796, #814) * Make VectorError pickleable (#848) * Add methods get_application_config(), set_application_config() and set_timer_rate() to VectorBus (#849) * Interface arguments are now lowercase (#858) * Fix errors using multiple Vector devices (#898, #971, #977) * Add more interface information to channel config (#917) * Improve timestamp accuracy on Windows (#934, #936) * Fix error with VN8900 (#1184) * Add static typing (#1229) * PCAN * Do not incorrectly reset CANMsg.MSGTYPE on remote frame (#659, #681) * Add support for error frames (#711) * Added keycheck for windows platform for better error message (#724) * Added status_string method to return simple status strings (#725) * Fix timestamp timezone offset (#777, #778) * Add [Cygwin](https://www.cygwin.com/) support (#840) * Update PCAN basic Python file to February 7, 2020 (#929) * Fix compatibility with the latest macOS SDK (#947, #948, #957, #976) * Allow numerical channel specifier (#981, #982) * macOS: Try to find libPCBUSB.dylib before loading it (#983, #984) * Disable command PCAN_ALLOW_ERROR_FRAMES on macOS (#985) * Force english error messages (#986, #993, #994) * Add set/get device number (#987) * Timestamps are silently incorrect on Windows without uptime installed (#1053, #1093) * Implement check for minimum version of pcan library (#1065, #1188) * Handle case where uptime is imported successfully but returns None (#1102, #1103) * slcan * Fix bitrate setting (#691) * Fix fileno crash on Windows (#924) * ics_neovi * Filter out Tx error messages (#854) * Adding support for send timeout (#855) * Raising more precise API error when set bitrate fails (#865) * Avoid flooding the logger with many errors when they are the same (#1125) * Omit the transmit exception cause for brevity (#1086) * Raise ValueError if message data is over max frame length (#1177, #1181) * Setting is_error_frame message property (#1189) * ixxat * Raise exception on busoff in recv() (#856) * Add support for 666 kbit/s bitrate (#911) * Add function to list hwids of available devices (#926) * Add CAN FD support (#1119) * seeed * Fix fileno crash on Windows (#902) * kvaser * Improve timestamp accuracy on Windows (#934, #936) * usb2can * Fix "Error 8" on Windows and provide better error messages (#989) * Fix crash on initialization (#1248, #1249) * Pass flags instead of flags_t type upon initialization (#1252) * serial * Fix "TypeError: cannot unpack non-iterable NoneType" and more robust error handling (#1000, #1010) * canalystii * Fix is_extended_id (#1006) * Fix transmitting onto a busy bus (#1114) * Replace binary library with python driver (#726, #1127) Other API changes and improvements ---------------------------------- * CAN FD frame support is pretty complete (#963) * ASCWriter (#604) and ASCReader (#741) * Canutils reader and writer (#1042) * Logger, viewer and player tools can handle CAN FD (#632) * Many bugfixes and more testing coverage * IO * [Log rotation](https://python-can.readthedocs.io/en/develop/listeners.html#can.SizedRotatingLogger) (#648, #874, #881, #1147) * Transparent (de)compression of [gzip](https://docs.python.org/3/library/gzip.html) files for all formats (#1221) * Add [plugin support to can.io Reader/Writer](https://python-can.readthedocs.io/en/develop/listeners.html#listener) (#783) * ASCReader/Writer enhancements like increased robustness (#820, #1223, #1256, #1257) * Adding absolute timestamps to ASC reader (#761) * Support other base number (radix) at ASCReader (#764) * Add [logconvert script](https://python-can.readthedocs.io/en/develop/scripts.html#can-logconvert) (#1072, #1194) * Adding support for gzipped ASC logging file (.asc.gz) (#1138) * Improve [IO class hierarchy](https://python-can.readthedocs.io/en/develop/internal-api.html#module-can.io.generic) (#1147) * An [overview over various "virtual" interfaces](https://python-can.readthedocs.io/en/develop/interfaces/virtual.html#other-virtual-interfaces) (#644) * Make ThreadBasedCyclicSendTask event based & improve timing accuracy (#656) * Ignore error frames in can.player by default, add --error-frames option (#690) * Add an error callback to ThreadBasedCyclicSendTask (#743, #781) * Add direction to CAN messages (#773, #779, #780, #852, #966) * Notifier no longer raises handled exceptions in rx_thread (#775, #789) but does so if no listener handles them (#1039, #1040) * Changes to serial device number decoding (#869) * Add a default fileno function to the BusABC (#877) * Disallow Messages to simultaneously be "FD" and "remote" (#1049) * Speed up interface plugin imports by avoiding pkg_resources (#1110) * Allowing for extra config arguments in can.logger (#1142, #1170) * Add changed byte highlighting to viewer.py (#1159) * Change DLC to DL in Message.\_\_str\_\_() (#1212) Other Bugfixes -------------- * BLF PDU padding (#459) * stop_all_periodic_tasks skipping every other task (#634, #637, #645) * Preserve capitalization when reading config files (#702, #1062) * ASCReader: Skip J1939Tp messages (#701) * Fix crash in Canutils Log Reader when parsing RTR frames (#713) * Various problems with the installation of the library * ASCWriter: Fix date format to show correct day of month (#754) * Fixes that some BLF files can't be read ( #763, #765) * Seek for start of object instead of calculating it (#786, #803, #806) * Only import winreg when on Windows (#800, #802) * Find the correct Reader/Writer independently of the file extension case (#895) * RecursionError when unpickling message object (#804, #885, #904) * Move "filelock" to neovi dependencies (#943) * Bus() with "fd" parameter as type bool always resolved to fd-enabled configuration (#954, #956) * Asyncio code hits error due to deprecated loop parameter (#1005, #1013) * Catch time before 1970 in ASCReader (#1034) * Fix a bug where error handlers were not called correctly (#1116) * Improved user interface of viewer script (#1118) * Correct app_name argument in logger (#1151) * Calling stop_all_periodic_tasks() in BusABC.shutdown() and all interfaces call it on shutdown (#1174) * Timing configurations do not allow int (#1175) * Some smaller bugfixes are not listed here since the problems were never part of a proper release * ASCReader & ASCWriter using DLC as data length (#1245, #1246) Behind the scenes & Quality assurance ------------------------------------- * We publish both source distributions (`sdist`) and binary wheels (`bdist_wheel`) (#1059, #1071) * Many interfaces were partly rewritten to modernize the code or to better handle errors * Performance improvements * Dependencies have changed * Derive type information in Sphinx docs directly from type hints (#654) * Better documentation in many, many places; This includes the examples, README and python-can developer resources * Add issue templates (#1008, #1017, #1018, #1178) * Many continuous integration (CI) discussions & improvements (for example: #951, #940, #1032) * Use the [mypy](https://github.com/python/mypy) static type checker (#598, #651) * Use [tox](https://tox.wiki/en/latest/) for testing (#582, #833, #870) * Use [Mergify](https://mergify.com/) (#821, #835, #937) * Switch between various CI providers, abandoned [AppVeyor](https://www.appveyor.com/) (#1009) and partly [Travis CI](https://travis-ci.org/), ended up with mostly [GitHub Actions](https://docs.github.com/en/actions) (#827, #1224) * Use the [black](https://black.readthedocs.io/en/stable/) auto-formatter (#950) * [Good test coverage](https://app.codecov.io/gh/hardbyte/python-can/branch/develop) for all but the interfaces * Testing: Many of the new features directly added tests, and coverage of existing code was improved too (for example: #1031, #581, #585, #586, #942, #1196, #1198) Version 3.3.4 ==== Last call for Python2 support. * #850 Fix socket.error is a deprecated alias of OSError used on Python versions lower than 3.3. Version 3.3.3 ==== Backported fixes from 4.x development branch which targets Python 3. * #798 Backport caching msg.data value in neovi interface. * #796 Fix Vector CANlib treatment of empty app name. * #771 Handle empty CSV file. * #741 ASCII reader can now handle FD frames. * #740 Exclude test packages from distribution. * #713 RTR crash fix in canutils log reader parsing RTR frames. * #701 Skip J1939 messages in ASC Reader. * #690 Exposes a configuration option to allow the CAN message player to send error frames (and sets the default to not send error frames). * #638 Fixes the semantics provided by periodic tasks in SocketCAN interface. * #628 Avoid padding CAN_FD_MESSAGE_64 objects to 4 bytes. * #617 Fixes the broken CANalyst-II interface. * #605 Socketcan BCM status fix. Version 3.3.2 ==== Minor bug fix release addressing issue in PCAN RTR. Version 3.3.1 ==== Minor fix to setup.py to only require pytest-runner when necessary. Version 3.3.0 ==== * Adding CAN FD 64 frame support to blf reader * Updates to installation instructions * Clean up bits generator in PCAN interface #588 * Minor fix to use latest tools when building wheels on travis. Version 3.2.1 ==== * CAN FD 64 frame support to blf reader * Minor fix to use latest tools when building wheels on travis. * Updates links in documentation. Version 3.2.0 ==== Major features -------------- * FD support added for Pcan by @bmeisels with input from @markuspi, @christiansandberg & @felixdivo in PR #537 * This is the last version of python-can which will support Python 2.7 and Python 3.5. Support has been removed for Python 3.4 in this release in PR #532 Other notable changes --------------------- * #533 BusState is now an enum. * #535 This release should automatically be published to PyPi by travis. * #577 Travis-ci now uses stages. * #548 A guide has been added for new io formats. * #550 Finish moving from nose to pytest. * #558 Fix installation on Windows. * #561 Tests for MessageSync added. General fixes, cleanup and docs changes can be found on the GitHub milestone https://github.com/hardbyte/python-can/milestone/7?closed=1 Pulls: #522, #526, #527, #536, #540, #546, #547, #548, #533, #559, #569, #571, #572, #575 Backend Specific Changes ------------------------ pcan ~~~~ * FD slcan ~~~~ * ability to set custom can speed instead of using predefined speed values. #553 socketcan ~~~~ * Bug fix to properly support 32bit systems. #573 usb2can ~~~~ * slightly better error handling * multiple serial devices can be found * support for the `_detect_available_configs()` API Pulls #511, #535 vector ~~~~ * handle `app_name`. #525 Version 3.1.1 ==== Major features -------------- Two new interfaces this release: - SYSTEC contributed by @idaniel86 in PR #466 - CANalyst-II contributed by @smeng9 in PR #476 Other notable changes --------------------- * #477 The kvaser interface now supports bus statistics via a custom bus method. * #434 neovi now supports receiving own messages * #490 Adding option to override the neovi library name * #488 Allow simultaneous access to IXXAT cards * #447 Improvements to serial interface: * to allow receiving partial messages * to fix issue with DLC of remote frames * addition of unit tests * #497 Small API changes to `Message` and added unit tests * #471 Fix CAN FD issue in kvaser interface * #462 Fix `Notifier` issue with asyncio * #481 Fix PCAN support on OSX * #455 Fix to `Message` initializer * Small bugfixes and improvements Version 3.1.0 ==== Version 3.1.0 was built with old wheel and/or setuptools packages and was replaced with v3.1.1 after an installation but was discovered. Version 3.0.0 ==== Major features -------------- * Adds support for developing `asyncio` applications with `python-can` more easily. This can be useful when implementing protocols that handles simultaneous connections to many nodes since you can write synchronous looking code without handling multiple threads and locking mechanisms. #388 * New can viewer terminal application. (`python -m can.viewer`) #390 * More formally adds task management responsibility to the `Bus`. By default tasks created with `bus.send_periodic` will have a reference held by the bus - this means in many cases the user doesn't need to keep the task in scope for their periodic messages to continue being sent. If this behavior isn't desired pass `store_task=False` to the `send_periodic` method. Stop all tasks by calling the bus's new `stop_all_periodic_tasks` method. #412 Breaking changes ---------------- * Interfaces should no longer override `send_periodic` and instead implement `_send_periodic_internal` to allow the Bus base class to manage tasks. #426 * writing to closed writers is not supported any more (it was supported only for some) * the file in the reader/writer is now always stored in the attribute uniformly called `file`, and not in something like `fp`, `log_file` or `output_file`. Changed the name of the first parameter of the read/writer constructors from `filename` to `file`. Other notable changes --------------------- * can.Message class updated #413 - Addition of a `Message.equals` method. - Deprecate id_type in favor of is_extended_id - Initializer parameter extended_id deprecated in favor of is_extended_id - documentation, testing and example updates - Addition of support for various builtins: __repr__, __slots__, __copy__ * IO module updates to bring consistency to the different CAN message writers and readers. #348 - context manager support for all readers and writers - they share a common super class called `BaseIOHandler` - all file handles can now be closed with the `stop()` method - the table name in `SqliteReader`/`SqliteWriter` can be adjusted - append mode added in `CSVWriter` and `CanutilsLogWriter` - [file-like](https://docs.python.org/3/glossary.html#term-file-like-object) and [path-like](https://docs.python.org/3/glossary.html#term-path-like-object) objects can now be passed to the readers and writers (except to the Sqlite handlers) - add a `__ne__()` method to the `Message` class (this was required by the tests) - added a `stop()` method for `BufferedReader` - `SqliteWriter`: this now guarantees that all messages are being written, exposes some previously internal metrics and only buffers messages up to a certain limit before writing/committing to the database. - the unused `header_line` attribute from `CSVReader` has been removed - privatized some attributes that are only to be used internally in the classes - the method `Listener.on_message_received()` is now abstract (using `@abc.abstractmethod`) * Start testing against Python 3.7 #380 * All scripts have been moved into `can/scripts`. #370, #406 * Added support for additional sections to the config #338 * Code coverage reports added. #346, #374 * Bug fix to thread safe bus. #397 General fixes, cleanup and docs changes: (#347, #348, #367, #368, #370, #371, #373, #420, #417, #419, #432) Backend Specific Changes ------------------------ 3rd party interfaces ~~~~~~~~~~~~~~~~~~~~ * Deprecated `python_can.interface` entry point instead use `can.interface`. #389 neovi ~~~~~ * Added support for CAN-FD #408 * Fix issues checking if bus is open. #381 * Adding multiple channels support. #415 nican ~~~~~ * implements reset instead of custom `flush_tx_buffer`. #364 pcan ~~~~ * now supported on OSX. #365 serial ~~~~~~ * Removed TextIOWrapper from serial. #383 * switch to `serial_for_url` enabling using remote ports via `loop://`, ``socket://` and `rfc2217://` URLs. #393 * hardware handshake using `rtscts` kwarg #402 socketcan ~~~~~~~~~ * socketcan tasks now reuse a bcm socket #404, #425, #426, * socketcan bugfix to receive error frames #384 vector ~~~~~~ * Vector interface now implements `_detect_available_configs`. #362 * Added support to select device by serial number. #387 Version 2.2.1 (2018-07-12) ===== * Fix errors and warnings when importing library on Windows * Fix Vector backend raising ValueError when hardware is not connected Version 2.2.0 (2018-06-30) ===== * Fallback message filtering implemented in Python for interfaces that don't offer better accelerated mechanism. * SocketCAN interfaces have been merged (Now use `socketcan` instead of either `socketcan_native` and `socketcan_ctypes`), this is now completely transparent for the library user. * automatic detection of available configs/channels in supported interfaces. * Added synchronized (thread-safe) Bus variant. * context manager support for the Bus class. * Dropped support for Python 3.3 (officially reached end-of-life in Sept. 2017) * Deprecated the old `CAN` module, please use the newer `can` entry point (will be removed in an upcoming major version) Version 2.1.0 (2018-02-17) ===== * Support for out of tree can interfaces with pluggy. * Initial support for CAN-FD for socketcan_native and kvaser interfaces. * Neovi interface now uses Intrepid Control Systems's own interface library. * Improvements and new documentation for SQL reader/writer. * Fix bug in neovi serial number decoding. * Add testing on OSX to TravisCI * Fix non english decoding error on pcan * Other misc improvements and bug fixes Version 2.0.0 (2018-01-05 ===== After an extended baking period we have finally tagged version 2.0.0! Quite a few major changes from v1.x: * New interfaces: * Vector * NI-CAN * isCAN * neoVI * Simplified periodic send API with initial support for SocketCAN * Protocols module including J1939 support removed * Logger script moved to module `can.logger` * New `can.player` script to replay log files * BLF, ASC log file support added in new `can.io` module You can install from [PyPi](https://pypi.python.org/pypi/python-can/2.0.0) with pip: ``` pip install python-can==2.0.0 ``` The documentation for v2.0.0 is available at http://python-can.readthedocs.io/en/2.0.0/ python-can-4.5.0/CONTRIBUTING.md000066400000000000000000000002361472200326600160340ustar00rootroot00000000000000Please read the [Development - Contributing](https://python-can.readthedocs.io/en/stable/development.html#contributing) guidelines in the documentation site. python-can-4.5.0/CONTRIBUTORS.txt000066400000000000000000000023021472200326600162750ustar00rootroot00000000000000https://github.com/hardbyte/python-can/graphs/contributors Ben Powell Brian Thorne Geert Linders Mark Catley Phillip Dixon Rose Lu Karl van Workum Albert Bloomfield Sam Bristow Ethan Zonca Robert Kaye Andrew Beal Jonas Frid Tynan McAuley Bruno Pennati Jack Jester-Weinstein Joshua Villyard Giuseppe Corbelli Christian Sandberg Eduard Bröcker Boris Wenzlaff Pierre-Luc Tessier Gagné Felix Divo Kristian Sloth Lauszus Shaoyu Meng Alexander Mueller Jan Goeteyn ykzheng Lear Corporation Nick Black Francisco Javier Burgos Macia Felix Nieuwenhuizen @marcel-kanter @bessman @koberbe @tamenol @deonvdw @ptoews @chrisoro @sou1hacker @auden-rovellequartz @typecprint @tysonite @Joey5337 @Aajn @josko7452 @leventerevesz @ericevenchick @ixygo @Gussy @altendky @philsc @rliebscher @jxltom @kdschlosser @tojoh511 @s0kr4t3s @jaesc @NiallDarwin @sdorre @gordon-epc @willson556 @jjguti @wiboticalex @illuusio @cperkulator @simontegelid @DawidRosinski @fabiocrestani @ChrisSweetKT @ausserlesh @wbarnha @projectgus @samsmith94 @alexey @MattWoodhead @nbusser @domologic @fjburgos @pkess @felixn @Tbruno25 @RitheeshBaradwaj @vijaysubbiah20 @liamkinne @RitheeshBaradwaj @BenGardiner @bures @NickCao @SWolfSchunk @belliriccardo @cssedev python-can-4.5.0/LICENSE.txt000066400000000000000000000167431472200326600154400ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. python-can-4.5.0/MANIFEST.in000066400000000000000000000001221472200326600153330ustar00rootroot00000000000000include *.txt include test/*.py include test/data/*.* recursive-include doc *.rst python-can-4.5.0/README.rst000066400000000000000000000121261472200326600152730ustar00rootroot00000000000000python-can ========== |pypi| |conda| |python_implementation| |downloads| |downloads_monthly| |docs| |github-actions| |coverage| |mergify| |formatter| .. |pypi| image:: https://img.shields.io/pypi/v/python-can.svg :target: https://pypi.python.org/pypi/python-can/ :alt: Latest Version on PyPi .. |conda| image:: https://img.shields.io/conda/v/conda-forge/python-can :target: https://github.com/conda-forge/python-can-feedstock :alt: Latest Version on conda-forge .. |python_implementation| image:: https://img.shields.io/pypi/implementation/python-can :target: https://pypi.python.org/pypi/python-can/ :alt: Supported Python implementations .. |downloads| image:: https://static.pepy.tech/badge/python-can :target: https://pepy.tech/project/python-can :alt: Downloads on PePy .. |downloads_monthly| image:: https://static.pepy.tech/badge/python-can/month :target: https://pepy.tech/project/python-can :alt: Monthly downloads on PePy .. |formatter| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/python/black :alt: This project uses the black formatter. .. |docs| image:: https://readthedocs.org/projects/python-can/badge/?version=stable :target: https://python-can.readthedocs.io/en/stable/ :alt: Documentation .. |github-actions| image:: https://github.com/hardbyte/python-can/actions/workflows/ci.yml/badge.svg :target: https://github.com/hardbyte/python-can/actions/workflows/ci.yml :alt: Github Actions workflow status .. |coverage| image:: https://coveralls.io/repos/github/hardbyte/python-can/badge.svg?branch=develop :target: https://coveralls.io/github/hardbyte/python-can?branch=develop :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 communication. It is used in cars, trucks, boats, wheelchairs and more. The ``can`` package provides controller area network support for Python developers; providing common abstractions to different hardware devices, and a suite of utilities for sending and receiving messages on a can bus. The library currently supports CPython as well as PyPy and runs on Mac, Linux and Windows. ============================== =========== Library Version Python ------------------------------ ----------- 2.x 2.6+, 3.4+ 3.x 2.7+, 3.5+ 4.0+ 3.7+ 4.3+ 3.8+ ============================== =========== Features -------- - common abstractions for CAN communication - support for many different backends (see the `docs `__) - receiving, sending, and periodically sending messages - normal and extended arbitration IDs - `CAN FD `__ support - many different loggers and readers supporting playback: ASC (CANalyzer format), BLF (Binary Logging Format by Vector), MF4 (Measurement Data Format v4 by ASAM), TRC, CSV, SQLite, and Canutils log - efficient in-kernel or in-hardware filtering of messages on supported interfaces - bus configuration reading from a file or from environment variables - command line tools for working with CAN buses (see the `docs `__) - more Example usage ------------- ``pip install python-can`` .. code:: python # import the library import can # create a bus instance using 'with' statement, # this will cause bus.shutdown() to be called on the block exit; # many other interfaces are supported as well (see documentation) with can.Bus(interface='socketcan', channel='vcan0', receive_own_messages=True) as bus: # send a message message = can.Message(arbitration_id=123, is_extended_id=True, data=[0x11, 0x22, 0x33]) bus.send(message, timeout=0.2) # iterate over received messages for msg in bus: print(f"{msg.arbitration_id:X}: {msg.data}") # or use an asynchronous notifier notifier = can.Notifier(bus, [can.Logger("recorded.log"), can.Printer()]) You can find more information in the documentation, online at `python-can.readthedocs.org `__. Discussion ---------- If you run into bugs, you can file them in our `issue tracker `__ on GitHub. `Stackoverflow `__ has several questions and answers tagged with ``python+can``. Wherever we interact, we strive to follow the `Python Community Code of Conduct `__. Contributing ------------ See `doc/development.rst `__ for getting started. python-can-4.5.0/can/000077500000000000000000000000001472200326600143435ustar00rootroot00000000000000python-can-4.5.0/can/__init__.py000066400000000000000000000056151472200326600164630ustar00rootroot00000000000000""" The ``can`` package provides controller area network support for Python developers; providing common abstractions to different hardware devices, and a suite of utilities for sending and receiving messages on a can bus. """ import contextlib import logging from importlib.metadata import PackageNotFoundError, version from typing import Any, Dict __all__ = [ "ASCReader", "ASCWriter", "AsyncBufferedReader", "BitTiming", "BitTimingFd", "BLFReader", "BLFWriter", "BufferedReader", "Bus", "BusABC", "BusState", "CanError", "CanInitializationError", "CanInterfaceNotImplementedError", "CanOperationError", "CanProtocol", "CanTimeoutError", "CanutilsLogReader", "CanutilsLogWriter", "CSVReader", "CSVWriter", "CyclicSendTaskABC", "LimitedDurationCyclicSendTaskABC", "Listener", "Logger", "LogReader", "ModifiableCyclicTaskABC", "Message", "MessageSync", "MF4Reader", "MF4Writer", "Notifier", "Printer", "RedirectReader", "RestartableCyclicTaskABC", "SizedRotatingLogger", "SqliteReader", "SqliteWriter", "ThreadSafeBus", "TRCFileVersion", "TRCReader", "TRCWriter", "VALID_INTERFACES", "bit_timing", "broadcastmanager", "bus", "ctypesutil", "detect_available_configs", "exceptions", "interface", "interfaces", "io", "listener", "logconvert", "log", "logger", "message", "notifier", "player", "set_logging_level", "thread_safe_bus", "typechecking", "util", "viewer", ] from . import typechecking # isort:skip from . import util # isort:skip from . import broadcastmanager, interface from .bit_timing import BitTiming, BitTimingFd from .broadcastmanager import ( CyclicSendTaskABC, LimitedDurationCyclicSendTaskABC, ModifiableCyclicTaskABC, RestartableCyclicTaskABC, ) from .bus import BusABC, BusState, CanProtocol from .exceptions import ( CanError, CanInitializationError, CanInterfaceNotImplementedError, CanOperationError, CanTimeoutError, ) from .interface import Bus, detect_available_configs from .interfaces import VALID_INTERFACES from .io import ( ASCReader, ASCWriter, BLFReader, BLFWriter, CanutilsLogReader, CanutilsLogWriter, CSVReader, CSVWriter, Logger, LogReader, MessageSync, MF4Reader, MF4Writer, Printer, SizedRotatingLogger, SqliteReader, SqliteWriter, TRCFileVersion, TRCReader, TRCWriter, ) from .listener import AsyncBufferedReader, BufferedReader, Listener, RedirectReader from .message import Message from .notifier import Notifier from .thread_safe_bus import ThreadSafeBus from .util import set_logging_level with contextlib.suppress(PackageNotFoundError): __version__ = version("python-can") log = logging.getLogger("can") rc: Dict[str, Any] = {} python-can-4.5.0/can/_entry_points.py000066400000000000000000000015631472200326600176160ustar00rootroot00000000000000import importlib import sys from dataclasses import dataclass from importlib.metadata import entry_points from typing import Any, List @dataclass class _EntryPoint: key: str module_name: str class_name: str def load(self) -> Any: module = importlib.import_module(self.module_name) return getattr(module, self.class_name) # See https://docs.python.org/3/library/importlib.metadata.html#entry-points, # "Compatibility Note". if sys.version_info >= (3, 10): 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]: return [ _EntryPoint(ep.name, *ep.value.split(":", maxsplit=1)) for ep in entry_points().get(group, []) ] python-can-4.5.0/can/bit_timing.py000066400000000000000000001230261472200326600170460ustar00rootroot00000000000000# pylint: disable=too-many-lines import math from typing import TYPE_CHECKING, Iterator, List, Mapping, cast if TYPE_CHECKING: from can.typechecking import BitTimingDict, BitTimingFdDict class BitTiming(Mapping): """Representation of a bit timing configuration for a CAN 2.0 bus. The class can be constructed in multiple ways, depending on the information available. The preferred way is using CAN clock frequency, prescaler, tseg1, tseg2 and sjw:: can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=1, sjw=1) Alternatively you can set the bitrate instead of the bit rate prescaler:: can.BitTiming.from_bitrate_and_segments( f_clock=8_000_000, bitrate=1_000_000, tseg1=5, tseg2=1, sjw=1 ) It is also possible to specify BTR registers:: can.BitTiming.from_registers(f_clock=8_000_000, btr0=0x00, btr1=0x14) or to calculate the timings for a given sample point:: can.BitTiming.from_sample_point(f_clock=8_000_000, bitrate=1_000_000, sample_point=75.0) """ def __init__( self, f_clock: int, brp: int, tseg1: int, tseg2: int, sjw: int, nof_samples: int = 1, strict: bool = False, ) -> None: """ :param int f_clock: The CAN system clock frequency in Hz. :param int brp: Bit rate prescaler. :param int tseg1: Time segment 1, that is, the number of quanta from (but not including) the Sync Segment to the sampling point. :param int tseg2: Time segment 2, that is, the number of quanta from the sampling point to the end of the bit. :param int sjw: The Synchronization Jump Width. Decides the maximum number of time quanta that the controller can resynchronize every bit. :param int nof_samples: Either 1 or 3. Some CAN controllers can also sample each bit three times. In this case, the bit will be sampled three quanta in a row, with the last sample being taken in the edge between TSEG1 and TSEG2. Three samples should only be used for relatively slow baudrates. :param bool strict: If True, restrict bit timings to the minimum required range as defined in ISO 11898. This can be used to ensure compatibility across a wide variety of CAN hardware. :raises ValueError: if the arguments are invalid. """ self._data: BitTimingDict = { "f_clock": f_clock, "brp": brp, "tseg1": tseg1, "tseg2": tseg2, "sjw": sjw, "nof_samples": nof_samples, } self._validate() if strict: self._restrict_to_minimum_range() def _validate(self) -> None: if not 1 <= self.brp <= 64: raise ValueError(f"bitrate prescaler (={self.brp}) must be in [1...64].") if not 1 <= self.tseg1 <= 16: raise ValueError(f"tseg1 (={self.tseg1}) must be in [1...16].") if not 1 <= self.tseg2 <= 8: raise ValueError(f"tseg2 (={self.tseg2}) must be in [1...8].") if not 1 <= self.sjw <= 4: raise ValueError(f"sjw (={self.sjw}) must be in [1...4].") if self.sjw > self.tseg2: raise ValueError( f"sjw (={self.sjw}) must not be greater than tseg2 (={self.tseg2})." ) if self.sample_point < 50.0: raise ValueError( f"The sample point must be greater than or equal to 50% " f"(sample_point={self.sample_point:.2f}%)." ) if self.nof_samples not in (1, 3): raise ValueError("nof_samples must be 1 or 3") def _restrict_to_minimum_range(self) -> None: if not 8 <= self.nbt <= 25: raise ValueError(f"nominal bit time (={self.nbt}) must be in [8...25].") if not 1 <= self.brp <= 32: raise ValueError(f"bitrate prescaler (={self.brp}) must be in [1...32].") if not 5_000 <= self.bitrate <= 1_000_000: raise ValueError( f"bitrate (={self.bitrate}) must be in [5,000...1,000,000]." ) @classmethod def from_bitrate_and_segments( cls, f_clock: int, bitrate: int, tseg1: int, tseg2: int, sjw: int, nof_samples: int = 1, strict: bool = False, ) -> "BitTiming": """Create a :class:`~can.BitTiming` instance from bitrate and segment lengths. :param int f_clock: The CAN system clock frequency in Hz. :param int bitrate: Bitrate in bit/s. :param int tseg1: Time segment 1, that is, the number of quanta from (but not including) the Sync Segment to the sampling point. :param int tseg2: Time segment 2, that is, the number of quanta from the sampling point to the end of the bit. :param int sjw: The Synchronization Jump Width. Decides the maximum number of time quanta that the controller can resynchronize every bit. :param int nof_samples: Either 1 or 3. Some CAN controllers can also sample each bit three times. In this case, the bit will be sampled three quanta in a row, with the last sample being taken in the edge between TSEG1 and TSEG2. Three samples should only be used for relatively slow baudrates. :param bool strict: If True, restrict bit timings to the minimum required range as defined in ISO 11898. This can be used to ensure compatibility across a wide variety of CAN hardware. :raises ValueError: if the arguments are invalid. """ try: brp = int(round(f_clock / (bitrate * (1 + tseg1 + tseg2)))) except ZeroDivisionError: raise ValueError("Invalid inputs") from None bt = cls( f_clock=f_clock, brp=brp, tseg1=tseg1, tseg2=tseg2, sjw=sjw, nof_samples=nof_samples, strict=strict, ) if abs(bt.bitrate - bitrate) > bitrate / 256: raise ValueError( f"the effective bitrate (={bt.bitrate}) diverges " f"from the requested bitrate (={bitrate})" ) return bt @classmethod def from_registers( cls, f_clock: int, btr0: int, btr1: int, ) -> "BitTiming": """Create a :class:`~can.BitTiming` instance from registers btr0 and btr1. :param int f_clock: The CAN system clock frequency in Hz. :param int btr0: The BTR0 register value used by many CAN controllers. :param int btr1: The BTR1 register value used by many CAN controllers. :raises ValueError: if the arguments are invalid. """ if not 0 <= btr0 < 2**16: raise ValueError(f"Invalid btr0 value. ({btr0})") if not 0 <= btr1 < 2**16: raise ValueError(f"Invalid btr1 value. ({btr1})") brp = (btr0 & 0x3F) + 1 sjw = (btr0 >> 6) + 1 tseg1 = (btr1 & 0xF) + 1 tseg2 = ((btr1 >> 4) & 0x7) + 1 nof_samples = 3 if btr1 & 0x80 else 1 return cls( brp=brp, f_clock=f_clock, tseg1=tseg1, tseg2=tseg2, sjw=sjw, nof_samples=nof_samples, ) @classmethod def iterate_from_sample_point( cls, f_clock: int, bitrate: int, sample_point: float = 69.0 ) -> Iterator["BitTiming"]: """Create a :class:`~can.BitTiming` iterator with all the solutions for a sample point. :param int f_clock: The CAN system clock frequency in Hz. :param int bitrate: Bitrate in bit/s. :param int sample_point: The sample point value in percent. :raises ValueError: if the arguments are invalid. """ if sample_point < 50.0: raise ValueError(f"sample_point (={sample_point}) must not be below 50%.") for brp in range(1, 65): nbt = round(int(f_clock / (bitrate * brp))) if nbt < 8: break effective_bitrate = f_clock / (nbt * brp) if abs(effective_bitrate - bitrate) > bitrate / 256: continue tseg1 = int(round(sample_point / 100 * nbt)) - 1 # limit tseg1, so tseg2 is at least 1 TQ tseg1 = min(tseg1, nbt - 2) tseg2 = nbt - tseg1 - 1 sjw = min(tseg2, 4) try: bt = BitTiming( f_clock=f_clock, brp=brp, tseg1=tseg1, tseg2=tseg2, sjw=sjw, strict=True, ) yield bt except ValueError: continue @classmethod def from_sample_point( cls, f_clock: int, bitrate: int, sample_point: float = 69.0 ) -> "BitTiming": """Create a :class:`~can.BitTiming` instance for a sample point. This function tries to find bit timings, which are close to the requested sample point. It does not take physical bus properties into account, so the calculated bus timings might not work properly for you. The :func:`oscillator_tolerance` function might be helpful to evaluate the bus timings. :param int f_clock: The CAN system clock frequency in Hz. :param int bitrate: Bitrate in bit/s. :param int sample_point: The sample point value in percent. :raises ValueError: if the arguments are invalid. """ if sample_point < 50.0: raise ValueError(f"sample_point (={sample_point}) must not be below 50%.") possible_solutions: List[BitTiming] = list( cls.iterate_from_sample_point(f_clock, bitrate, sample_point) ) if not possible_solutions: raise ValueError("No suitable bit timings found.") # sort solutions for key, reverse in ( # prefer low prescaler (lambda x: x.brp, False), # prefer low sample point deviation from requested values (lambda x: abs(x.sample_point - sample_point), False), ): possible_solutions.sort(key=key, reverse=reverse) return possible_solutions[0] @property def f_clock(self) -> int: """The CAN system clock frequency in Hz.""" return self._data["f_clock"] @property def bitrate(self) -> int: """Bitrate in bits/s.""" return int(round(self.f_clock / (self.nbt * self.brp))) @property def brp(self) -> int: """Bit Rate Prescaler.""" return self._data["brp"] @property def tq(self) -> int: """Time quantum in nanoseconds""" return int(round(self.brp / self.f_clock * 1e9)) @property def nbt(self) -> int: """Nominal Bit Time.""" return 1 + self.tseg1 + self.tseg2 @property def tseg1(self) -> int: """Time segment 1. The number of quanta from (but not including) the Sync Segment to the sampling point. """ return self._data["tseg1"] @property def tseg2(self) -> int: """Time segment 2. The number of quanta from the sampling point to the end of the bit. """ return self._data["tseg2"] @property def sjw(self) -> int: """Synchronization Jump Width.""" return self._data["sjw"] @property def nof_samples(self) -> int: """Number of samples (1 or 3).""" return self._data["nof_samples"] @property def sample_point(self) -> float: """Sample point in percent.""" return 100.0 * (1 + self.tseg1) / (1 + self.tseg1 + self.tseg2) @property def btr0(self) -> int: """Bit timing register 0 for SJA1000.""" return (self.sjw - 1) << 6 | self.brp - 1 @property def btr1(self) -> int: """Bit timing register 1 for SJA1000.""" sam = 1 if self.nof_samples == 3 else 0 return sam << 7 | (self.tseg2 - 1) << 4 | self.tseg1 - 1 def oscillator_tolerance( self, node_loop_delay_ns: float = 250.0, bus_length_m: float = 10.0, ) -> float: """Oscillator tolerance in percent according to ISO 11898-1. :param float node_loop_delay_ns: Transceiver loop delay in nanoseconds. :param float bus_length_m: Bus length in meters. """ delay_per_meter = 5 bidirectional_propagation_delay_ns = 2 * ( node_loop_delay_ns + delay_per_meter * bus_length_m ) prop_seg = math.ceil(bidirectional_propagation_delay_ns / self.tq) nom_phase_seg1 = self.tseg1 - prop_seg nom_phase_seg2 = self.tseg2 df_clock_list = [ _oscillator_tolerance_condition_1(nom_sjw=self.sjw, nbt=self.nbt), _oscillator_tolerance_condition_2( nbt=self.nbt, nom_phase_seg1=nom_phase_seg1, nom_phase_seg2=nom_phase_seg2, ), ] return max(0.0, min(df_clock_list) * 100) def recreate_with_f_clock(self, f_clock: int) -> "BitTiming": """Return a new :class:`~can.BitTiming` instance with the given *f_clock* but the same bit rate and sample point. :param int f_clock: The CAN system clock frequency in Hz. :raises ValueError: if no suitable bit timings were found. """ # try the most simple solution first: another bitrate prescaler try: return BitTiming.from_bitrate_and_segments( f_clock=f_clock, bitrate=self.bitrate, tseg1=self.tseg1, tseg2=self.tseg2, sjw=self.sjw, nof_samples=self.nof_samples, strict=True, ) except ValueError: pass # create a new timing instance with the same sample point bt = BitTiming.from_sample_point( f_clock=f_clock, bitrate=self.bitrate, sample_point=self.sample_point ) if abs(bt.sample_point - self.sample_point) > 1.0: raise ValueError( "f_clock change failed because of sample point discrepancy." ) # adapt synchronization jump width, so it has the same size relative to bit time as self sjw = int(round(self.sjw / self.nbt * bt.nbt)) sjw = max(1, min(4, bt.tseg2, sjw)) bt._data["sjw"] = sjw # pylint: disable=protected-access bt._data["nof_samples"] = self.nof_samples # pylint: disable=protected-access bt._validate() # pylint: disable=protected-access return bt def __str__(self) -> str: segments = [ f"BR: {self.bitrate:_} bit/s", f"SP: {self.sample_point:.2f}%", f"BRP: {self.brp}", f"TSEG1: {self.tseg1}", f"TSEG2: {self.tseg2}", f"SJW: {self.sjw}", f"BTR: {self.btr0:02X}{self.btr1:02X}h", f"CLK: {self.f_clock / 1e6:.0f}MHz", ] return ", ".join(segments) def __repr__(self) -> str: args = ", ".join(f"{key}={value}" for key, value in self.items()) return f"can.{self.__class__.__name__}({args})" def __getitem__(self, key: str) -> int: return cast(int, self._data.__getitem__(key)) def __len__(self) -> int: return self._data.__len__() def __iter__(self) -> Iterator[str]: return self._data.__iter__() def __eq__(self, other: object) -> bool: if not isinstance(other, BitTiming): return False return self._data == other._data def __hash__(self) -> int: return tuple(self._data.values()).__hash__() class BitTimingFd(Mapping): """Representation of a bit timing configuration for a CAN FD bus. The class can be constructed in multiple ways, depending on the information available. The preferred way is using CAN clock frequency, bit rate prescaler, tseg1, tseg2 and sjw for both the arbitration (nominal) and data phase:: can.BitTimingFd( f_clock=80_000_000, nom_brp=1, nom_tseg1=59, nom_tseg2=20, nom_sjw=10, data_brp=1, data_tseg1=6, data_tseg2=3, data_sjw=2, ) Alternatively you can set the bit rates instead of the bit rate prescalers:: can.BitTimingFd.from_bitrate_and_segments( f_clock=80_000_000, nom_bitrate=1_000_000, nom_tseg1=59, nom_tseg2=20, nom_sjw=10, data_bitrate=8_000_000, data_tseg1=6, data_tseg2=3, data_sjw=2, ) It is also possible to calculate the timings for a given pair of arbitration and data sample points:: can.BitTimingFd.from_sample_point( f_clock=80_000_000, nom_bitrate=1_000_000, nom_sample_point=75.0, data_bitrate=8_000_000, data_sample_point=70.0, ) """ def __init__( # pylint: disable=too-many-arguments self, f_clock: int, nom_brp: int, nom_tseg1: int, nom_tseg2: int, nom_sjw: int, data_brp: int, data_tseg1: int, data_tseg2: int, data_sjw: int, strict: bool = False, ) -> None: """ Initialize a BitTimingFd instance with the specified parameters. :param int f_clock: The CAN system clock frequency in Hz. :param int nom_brp: Nominal (arbitration) phase bitrate prescaler. :param int nom_tseg1: Nominal phase Time segment 1, that is, the number of quanta from (but not including) the Sync Segment to the sampling point. :param int nom_tseg2: Nominal phase Time segment 2, that is, the number of quanta from the sampling point to the end of the bit. :param int nom_sjw: The Synchronization Jump Width for the nominal phase. This value determines the maximum number of time quanta that the controller can resynchronize every bit. :param int data_brp: Data phase bitrate prescaler. :param int data_tseg1: Data phase Time segment 1, that is, the number of quanta from (but not including) the Sync Segment to the sampling point. :param int data_tseg2: Data phase Time segment 2, that is, the number of quanta from the sampling point to the end of the bit. :param int data_sjw: The Synchronization Jump Width for the data phase. This value determines the maximum number of time quanta that the controller can resynchronize every bit. :param bool strict: If True, restrict bit timings to the minimum required range as defined in ISO 11898. This can be used to ensure compatibility across a wide variety of CAN hardware. :raises ValueError: if the arguments are invalid. """ self._data: BitTimingFdDict = { "f_clock": f_clock, "nom_brp": nom_brp, "nom_tseg1": nom_tseg1, "nom_tseg2": nom_tseg2, "nom_sjw": nom_sjw, "data_brp": data_brp, "data_tseg1": data_tseg1, "data_tseg2": data_tseg2, "data_sjw": data_sjw, } self._validate() if strict: self._restrict_to_minimum_range() def _validate(self) -> None: for param, value in self._data.items(): if value < 0: # type: ignore[operator] err_msg = f"'{param}' (={value}) must not be negative." raise ValueError(err_msg) if self.nom_brp < 1: raise ValueError( f"nominal bitrate prescaler (={self.nom_brp}) must be at least 1." ) if self.data_brp < 1: raise ValueError( f"data bitrate prescaler (={self.data_brp}) must be at least 1." ) if self.data_bitrate < self.nom_bitrate: raise ValueError( f"data_bitrate (={self.data_bitrate}) must be greater than or " f"equal to nom_bitrate (={self.nom_bitrate})" ) if self.nom_sjw > self.nom_tseg2: raise ValueError( f"nom_sjw (={self.nom_sjw}) must not be " f"greater than nom_tseg2 (={self.nom_tseg2})." ) if self.data_sjw > self.data_tseg2: raise ValueError( f"data_sjw (={self.data_sjw}) must not be " f"greater than data_tseg2 (={self.data_tseg2})." ) if self.nom_sample_point < 50.0: raise ValueError( f"The arbitration sample point must be greater than or equal to 50% " f"(nom_sample_point={self.nom_sample_point:.2f}%)." ) if self.data_sample_point < 50.0: raise ValueError( f"The data sample point must be greater than or equal to 50% " f"(data_sample_point={self.data_sample_point:.2f}%)." ) def _restrict_to_minimum_range(self) -> None: # restrict to minimum required range as defined in ISO 11898 if not 8 <= self.nbt <= 80: raise ValueError(f"Nominal bit time (={self.nbt}) must be in [8...80]") if not 5 <= self.dbt <= 25: raise ValueError(f"Nominal bit time (={self.dbt}) must be in [5...25]") if not 1 <= self.data_tseg1 <= 16: raise ValueError(f"data_tseg1 (={self.data_tseg1}) must be in [1...16].") if not 2 <= self.data_tseg2 <= 8: raise ValueError(f"data_tseg2 (={self.data_tseg2}) must be in [2...8].") if not 1 <= self.data_sjw <= 8: raise ValueError(f"data_sjw (={self.data_sjw}) must be in [1...8].") if self.nom_brp == self.data_brp: # shared prescaler if not 2 <= self.nom_tseg1 <= 128: raise ValueError(f"nom_tseg1 (={self.nom_tseg1}) must be in [2...128].") if not 2 <= self.nom_tseg2 <= 32: raise ValueError(f"nom_tseg2 (={self.nom_tseg2}) must be in [2...32].") if not 1 <= self.nom_sjw <= 32: raise ValueError(f"nom_sjw (={self.nom_sjw}) must be in [1...32].") else: # separate prescaler if not 2 <= self.nom_tseg1 <= 64: raise ValueError(f"nom_tseg1 (={self.nom_tseg1}) must be in [2...64].") if not 2 <= self.nom_tseg2 <= 16: raise ValueError(f"nom_tseg2 (={self.nom_tseg2}) must be in [2...16].") if not 1 <= self.nom_sjw <= 16: raise ValueError(f"nom_sjw (={self.nom_sjw}) must be in [1...16].") @classmethod def from_bitrate_and_segments( # pylint: disable=too-many-arguments cls, f_clock: int, nom_bitrate: int, nom_tseg1: int, nom_tseg2: int, nom_sjw: int, data_bitrate: int, data_tseg1: int, data_tseg2: int, data_sjw: int, strict: bool = False, ) -> "BitTimingFd": """ Create a :class:`~can.BitTimingFd` instance with the bitrates and segments lengths. :param int f_clock: The CAN system clock frequency in Hz. :param int nom_bitrate: Nominal (arbitration) phase bitrate in bit/s. :param int nom_tseg1: Nominal phase Time segment 1, that is, the number of quanta from (but not including) the Sync Segment to the sampling point. :param int nom_tseg2: Nominal phase Time segment 2, that is, the number of quanta from the sampling point to the end of the bit. :param int nom_sjw: The Synchronization Jump Width for the nominal phase. This value determines the maximum number of time quanta that the controller can resynchronize every bit. :param int data_bitrate: Data phase bitrate in bit/s. :param int data_tseg1: Data phase Time segment 1, that is, the number of quanta from (but not including) the Sync Segment to the sampling point. :param int data_tseg2: Data phase Time segment 2, that is, the number of quanta from the sampling point to the end of the bit. :param int data_sjw: The Synchronization Jump Width for the data phase. This value determines the maximum number of time quanta that the controller can resynchronize every bit. :param bool strict: If True, restrict bit timings to the minimum required range as defined in ISO 11898. This can be used to ensure compatibility across a wide variety of CAN hardware. :raises ValueError: if the arguments are invalid. """ try: nom_brp = int(round(f_clock / (nom_bitrate * (1 + nom_tseg1 + nom_tseg2)))) data_brp = int( round(f_clock / (data_bitrate * (1 + data_tseg1 + data_tseg2))) ) except ZeroDivisionError: raise ValueError("Invalid inputs.") from None bt = cls( f_clock=f_clock, nom_brp=nom_brp, nom_tseg1=nom_tseg1, nom_tseg2=nom_tseg2, nom_sjw=nom_sjw, data_brp=data_brp, data_tseg1=data_tseg1, data_tseg2=data_tseg2, data_sjw=data_sjw, strict=strict, ) if abs(bt.nom_bitrate - nom_bitrate) > nom_bitrate / 256: raise ValueError( f"the effective nom. bitrate (={bt.nom_bitrate}) diverges " f"from the requested nom. bitrate (={nom_bitrate})" ) if abs(bt.data_bitrate - data_bitrate) > data_bitrate / 256: raise ValueError( f"the effective data bitrate (={bt.data_bitrate}) diverges " f"from the requested data bitrate (={data_bitrate})" ) return bt @classmethod def iterate_from_sample_point( cls, f_clock: int, nom_bitrate: int, nom_sample_point: float, data_bitrate: int, data_sample_point: float, ) -> Iterator["BitTimingFd"]: """Create an :class:`~can.BitTimingFd` iterator with all the solutions for a sample point. :param int f_clock: The CAN system clock frequency in Hz. :param int nom_bitrate: Nominal bitrate in bit/s. :param int nom_sample_point: The sample point value of the arbitration phase in percent. :param int data_bitrate: Data bitrate in bit/s. :param int data_sample_point: The sample point value of the data phase in percent. :raises ValueError: if the arguments are invalid. """ if nom_sample_point < 50.0: raise ValueError( f"nom_sample_point (={nom_sample_point}) must not be below 50%." ) if data_sample_point < 50.0: raise ValueError( f"data_sample_point (={data_sample_point}) must not be below 50%." ) sync_seg = 1 for nom_brp in range(1, 257): nbt = round(int(f_clock / (nom_bitrate * nom_brp))) if nbt < 1: break effective_nom_bitrate = f_clock / (nbt * nom_brp) if abs(effective_nom_bitrate - nom_bitrate) > nom_bitrate / 256: continue nom_tseg1 = int(round(nom_sample_point / 100 * nbt)) - 1 # limit tseg1, so tseg2 is at least 2 TQ nom_tseg1 = min(nom_tseg1, nbt - sync_seg - 2) nom_tseg2 = nbt - nom_tseg1 - 1 nom_sjw = min(nom_tseg2, 128) for data_brp in range(1, 257): dbt = round(int(f_clock / (data_bitrate * data_brp))) if dbt < 1: break effective_data_bitrate = f_clock / (dbt * data_brp) if abs(effective_data_bitrate - data_bitrate) > data_bitrate / 256: continue data_tseg1 = int(round(data_sample_point / 100 * dbt)) - 1 # limit tseg1, so tseg2 is at least 2 TQ data_tseg1 = min(data_tseg1, dbt - sync_seg - 2) data_tseg2 = dbt - data_tseg1 - 1 data_sjw = min(data_tseg2, 16) try: bt = BitTimingFd( f_clock=f_clock, nom_brp=nom_brp, nom_tseg1=nom_tseg1, nom_tseg2=nom_tseg2, nom_sjw=nom_sjw, data_brp=data_brp, data_tseg1=data_tseg1, data_tseg2=data_tseg2, data_sjw=data_sjw, strict=True, ) yield bt except ValueError: continue @classmethod def from_sample_point( cls, f_clock: int, nom_bitrate: int, nom_sample_point: float, data_bitrate: int, data_sample_point: float, ) -> "BitTimingFd": """Create a :class:`~can.BitTimingFd` instance for a sample point. This function tries to find bit timings, which are close to the requested sample points. It does not take physical bus properties into account, so the calculated bus timings might not work properly for you. The :func:`oscillator_tolerance` function might be helpful to evaluate the bus timings. :param int f_clock: The CAN system clock frequency in Hz. :param int nom_bitrate: Nominal bitrate in bit/s. :param int nom_sample_point: The sample point value of the arbitration phase in percent. :param int data_bitrate: Data bitrate in bit/s. :param int data_sample_point: The sample point value of the data phase in percent. :raises ValueError: if the arguments are invalid. """ if nom_sample_point < 50.0: raise ValueError( f"nom_sample_point (={nom_sample_point}) must not be below 50%." ) if data_sample_point < 50.0: raise ValueError( f"data_sample_point (={data_sample_point}) must not be below 50%." ) possible_solutions: List[BitTimingFd] = list( cls.iterate_from_sample_point( f_clock, nom_bitrate, nom_sample_point, data_bitrate, data_sample_point, ) ) if not possible_solutions: raise ValueError("No suitable bit timings found.") # prefer using the same prescaler for arbitration and data phase same_prescaler = list( filter(lambda x: x.nom_brp == x.data_brp, possible_solutions) ) if same_prescaler: possible_solutions = same_prescaler # sort solutions for key, reverse in ( # prefer low prescaler (lambda x: x.nom_brp + x.data_brp, False), # prefer same prescaler for arbitration and data (lambda x: abs(x.nom_brp - x.data_brp), False), # prefer low sample point deviation from requested values ( lambda x: ( abs(x.nom_sample_point - nom_sample_point) + abs(x.data_sample_point - data_sample_point) ), False, ), ): possible_solutions.sort(key=key, reverse=reverse) return possible_solutions[0] @property def f_clock(self) -> int: """The CAN system clock frequency in Hz.""" return self._data["f_clock"] @property def nom_bitrate(self) -> int: """Nominal (arbitration phase) bitrate.""" return int(round(self.f_clock / (self.nbt * self.nom_brp))) @property def nom_brp(self) -> int: """Prescaler value for the arbitration phase.""" return self._data["nom_brp"] @property def nom_tq(self) -> int: """Nominal time quantum in nanoseconds""" return int(round(self.nom_brp / self.f_clock * 1e9)) @property def nbt(self) -> int: """Number of time quanta in a bit of the arbitration phase.""" return 1 + self.nom_tseg1 + self.nom_tseg2 @property def nom_tseg1(self) -> int: """Time segment 1 value of the arbitration phase. This is the sum of the propagation time segment and the phase buffer segment 1. """ return self._data["nom_tseg1"] @property def nom_tseg2(self) -> int: """Time segment 2 value of the arbitration phase. Also known as phase buffer segment 2.""" return self._data["nom_tseg2"] @property def nom_sjw(self) -> int: """Synchronization jump width of the arbitration phase. The phase buffer segments may be shortened or lengthened by this value. """ return self._data["nom_sjw"] @property def nom_sample_point(self) -> float: """Sample point of the arbitration phase in percent.""" return 100.0 * (1 + self.nom_tseg1) / (1 + self.nom_tseg1 + self.nom_tseg2) @property def data_bitrate(self) -> int: """Bitrate of the data phase in bit/s.""" return int(round(self.f_clock / (self.dbt * self.data_brp))) @property def data_brp(self) -> int: """Prescaler value for the data phase.""" return self._data["data_brp"] @property def data_tq(self) -> int: """Data time quantum in nanoseconds""" return int(round(self.data_brp / self.f_clock * 1e9)) @property def dbt(self) -> int: """Number of time quanta in a bit of the data phase.""" return 1 + self.data_tseg1 + self.data_tseg2 @property def data_tseg1(self) -> int: """TSEG1 value of the data phase. This is the sum of the propagation time segment and the phase buffer segment 1. """ return self._data["data_tseg1"] @property def data_tseg2(self) -> int: """TSEG2 value of the data phase. Also known as phase buffer segment 2.""" return self._data["data_tseg2"] @property def data_sjw(self) -> int: """Synchronization jump width of the data phase. The phase buffer segments may be shortened or lengthened by this value. """ return self._data["data_sjw"] @property def data_sample_point(self) -> float: """Sample point of the data phase in percent.""" return 100.0 * (1 + self.data_tseg1) / (1 + self.data_tseg1 + self.data_tseg2) def oscillator_tolerance( self, node_loop_delay_ns: float = 250.0, bus_length_m: float = 10.0, ) -> float: """Oscillator tolerance in percent according to ISO 11898-1. :param float node_loop_delay_ns: Transceiver loop delay in nanoseconds. :param float bus_length_m: Bus length in meters. """ delay_per_meter = 5 bidirectional_propagation_delay_ns = 2 * ( node_loop_delay_ns + delay_per_meter * bus_length_m ) prop_seg = math.ceil(bidirectional_propagation_delay_ns / self.nom_tq) nom_phase_seg1 = self.nom_tseg1 - prop_seg nom_phase_seg2 = self.nom_tseg2 data_phase_seg2 = self.data_tseg2 df_clock_list = [ _oscillator_tolerance_condition_1(nom_sjw=self.nom_sjw, nbt=self.nbt), _oscillator_tolerance_condition_2( nbt=self.nbt, nom_phase_seg1=nom_phase_seg1, nom_phase_seg2=nom_phase_seg2, ), _oscillator_tolerance_condition_3(data_sjw=self.data_sjw, dbt=self.dbt), _oscillator_tolerance_condition_4( nom_phase_seg1=nom_phase_seg1, nom_phase_seg2=nom_phase_seg2, data_phase_seg2=data_phase_seg2, nbt=self.nbt, dbt=self.dbt, data_brp=self.data_brp, nom_brp=self.nom_brp, ), _oscillator_tolerance_condition_5( data_sjw=self.data_sjw, data_brp=self.data_brp, nom_brp=self.nom_brp, data_phase_seg2=data_phase_seg2, nom_phase_seg2=nom_phase_seg2, nbt=self.nbt, dbt=self.dbt, ), ] return max(0.0, min(df_clock_list) * 100) def recreate_with_f_clock(self, f_clock: int) -> "BitTimingFd": """Return a new :class:`~can.BitTimingFd` instance with the given *f_clock* but the same bit rates and sample points. :param int f_clock: The CAN system clock frequency in Hz. :raises ValueError: if no suitable bit timings were found. """ # try the most simple solution first: another bitrate prescaler try: return BitTimingFd.from_bitrate_and_segments( f_clock=f_clock, nom_bitrate=self.nom_bitrate, nom_tseg1=self.nom_tseg1, nom_tseg2=self.nom_tseg2, nom_sjw=self.nom_sjw, data_bitrate=self.data_bitrate, data_tseg1=self.data_tseg1, data_tseg2=self.data_tseg2, data_sjw=self.data_sjw, strict=True, ) except ValueError: pass # create a new timing instance with the same sample points bt = BitTimingFd.from_sample_point( f_clock=f_clock, nom_bitrate=self.nom_bitrate, nom_sample_point=self.nom_sample_point, data_bitrate=self.data_bitrate, data_sample_point=self.data_sample_point, ) if ( abs(bt.nom_sample_point - self.nom_sample_point) > 1.0 or abs(bt.data_sample_point - self.data_sample_point) > 1.0 ): raise ValueError( "f_clock change failed because of sample point discrepancy." ) # adapt synchronization jump width, so it has the same size relative to bit time as self nom_sjw = int(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 = max(1, min(bt.data_tseg2, data_sjw)) bt._data["data_sjw"] = data_sjw # pylint: disable=protected-access bt._validate() # pylint: disable=protected-access return bt def __str__(self) -> str: segments = [ f"NBR: {self.nom_bitrate:_} bit/s", f"NSP: {self.nom_sample_point:.2f}%", f"NBRP: {self.nom_brp}", f"NTSEG1: {self.nom_tseg1}", f"NTSEG2: {self.nom_tseg2}", f"NSJW: {self.nom_sjw}", f"DBR: {self.data_bitrate:_} bit/s", f"DSP: {self.data_sample_point:.2f}%", f"DBRP: {self.data_brp}", f"DTSEG1: {self.data_tseg1}", f"DTSEG2: {self.data_tseg2}", f"DSJW: {self.data_sjw}", f"CLK: {self.f_clock / 1e6:.0f}MHz", ] return ", ".join(segments) def __repr__(self) -> str: args = ", ".join(f"{key}={value}" for key, value in self.items()) return f"can.{self.__class__.__name__}({args})" def __getitem__(self, key: str) -> int: return cast(int, self._data.__getitem__(key)) def __len__(self) -> int: return self._data.__len__() def __iter__(self) -> Iterator[str]: return self._data.__iter__() def __eq__(self, other: object) -> bool: if not isinstance(other, BitTimingFd): return False return self._data == other._data def __hash__(self) -> int: return tuple(self._data.values()).__hash__() def _oscillator_tolerance_condition_1(nom_sjw: int, nbt: int) -> float: """Arbitration phase - resynchronization""" return nom_sjw / (2 * 10 * nbt) def _oscillator_tolerance_condition_2( nbt: int, nom_phase_seg1: int, nom_phase_seg2: int ) -> float: """Arbitration phase - sampling of bit after error flag""" return min(nom_phase_seg1, nom_phase_seg2) / (2 * (13 * nbt - nom_phase_seg2)) def _oscillator_tolerance_condition_3(data_sjw: int, dbt: int) -> float: """Data phase - resynchronization""" return data_sjw / (2 * 10 * dbt) def _oscillator_tolerance_condition_4( nom_phase_seg1: int, nom_phase_seg2: int, data_phase_seg2: int, nbt: int, dbt: int, data_brp: int, nom_brp: int, ) -> float: """Data phase - sampling of bit after error flag""" return min(nom_phase_seg1, nom_phase_seg2) / ( 2 * ((6 * dbt - data_phase_seg2) * data_brp / nom_brp + 7 * nbt) ) def _oscillator_tolerance_condition_5( data_sjw: int, data_brp: int, nom_brp: int, nom_phase_seg2: int, data_phase_seg2: int, nbt: int, dbt: int, ) -> float: """Data phase - bit rate switch""" max_correctable_phase_shift = data_sjw - max(0.0, nom_brp / data_brp - 1) time_between_resync = 2 * ( (2 * nbt - nom_phase_seg2) * nom_brp / data_brp + data_phase_seg2 + 4 * dbt ) return max_correctable_phase_shift / time_between_resync python-can-4.5.0/can/broadcastmanager.py000066400000000000000000000316621472200326600202220ustar00rootroot00000000000000""" Exposes several methods for transmitting cyclic messages. The main entry point to these classes should be through :meth:`can.BusABC.send_periodic`. """ import abc import logging import platform import sys import threading import time import warnings from typing import ( TYPE_CHECKING, Callable, Final, Optional, Sequence, Tuple, Union, cast, ) from can import typechecking from can.message import Message if TYPE_CHECKING: from can.bus import BusABC log = logging.getLogger("can.bcm") NANOSECONDS_IN_SECOND: Final[int] = 1_000_000_000 class _Pywin32Event: handle: int class _Pywin32: def __init__(self) -> None: import pywintypes # pylint: disable=import-outside-toplevel,import-error import win32event # pylint: disable=import-outside-toplevel,import-error self.pywintypes = pywintypes self.win32event = win32event def create_timer(self) -> _Pywin32Event: try: event = self.win32event.CreateWaitableTimerEx( None, None, self.win32event.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, self.win32event.TIMER_ALL_ACCESS, ) except ( AttributeError, OSError, self.pywintypes.error, # pylint: disable=no-member ): event = self.win32event.CreateWaitableTimer(None, False, None) return cast(_Pywin32Event, event) def set_timer(self, event: _Pywin32Event, period_ms: int) -> None: self.win32event.SetWaitableTimer(event.handle, 0, period_ms, None, None, False) def stop_timer(self, event: _Pywin32Event) -> None: self.win32event.SetWaitableTimer(event.handle, 0, 0, None, None, False) def wait_0(self, event: _Pywin32Event) -> None: self.win32event.WaitForSingleObject(event.handle, 0) def wait_inf(self, event: _Pywin32Event) -> None: self.win32event.WaitForSingleObject( event.handle, self.win32event.INFINITE, ) PYWIN32: Optional[_Pywin32] = None if sys.platform == "win32" and sys.version_info < (3, 11): try: PYWIN32 = _Pywin32() except ImportError: pass class CyclicTask(abc.ABC): """ Abstract Base for all cyclic tasks. """ @abc.abstractmethod def stop(self) -> None: """Cancel this periodic task. :raises ~can.exceptions.CanError: If stop is called on an already stopped task. """ class CyclicSendTaskABC(CyclicTask, abc.ABC): """ Message send task with defined period """ def __init__( self, messages: Union[Sequence[Message], Message], period: float ) -> None: """ :param messages: The messages to be sent periodically. :param period: The rate in seconds at which to send the messages. :raises ValueError: If the given messages are invalid """ messages = self._check_and_convert_messages(messages) # Take the Arbitration ID of the first element self.arbitration_id = messages[0].arbitration_id self.period = period self.period_ns = int(round(period * 1e9)) self.messages = messages @staticmethod def _check_and_convert_messages( 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. Performs error checking to ensure that all Messages have the same arbitration ID and channel. Should be called when the cyclic task is initialized. :raises ValueError: If the given messages are invalid """ if not isinstance(messages, (list, tuple)): if isinstance(messages, Message): messages = [messages] else: raise ValueError("Must be either a list, tuple, or a Message") if not messages: raise ValueError("Must be at least a list or tuple of length 1") messages = tuple(messages) all_same_id = all( message.arbitration_id == messages[0].arbitration_id for message in messages ) if not all_same_id: raise ValueError("All Arbitration IDs should be the same") all_same_channel = all( message.channel == messages[0].channel for message in messages ) if not all_same_channel: raise ValueError("All Channel IDs should be the same") return messages class LimitedDurationCyclicSendTaskABC(CyclicSendTaskABC, abc.ABC): def __init__( self, messages: Union[Sequence[Message], Message], period: float, duration: Optional[float], ) -> None: """Message send task with a defined duration and period. :param messages: The messages to be sent periodically. :param period: The rate in seconds at which to send the messages. :param duration: Approximate duration in seconds to continue sending messages. If no duration is provided, the task will continue indefinitely. :raises ValueError: If the given messages are invalid """ super().__init__(messages, period) self.duration = duration self.end_time: Optional[float] = None class RestartableCyclicTaskABC(CyclicSendTaskABC, abc.ABC): """Adds support for restarting a stopped cyclic task""" @abc.abstractmethod def start(self) -> None: """Restart a stopped periodic task.""" class ModifiableCyclicTaskABC(CyclicSendTaskABC, abc.ABC): def _check_modified_messages(self, messages: Tuple[Message, ...]) -> None: """Helper function to perform error checking when modifying the data in the cyclic task. Performs error checking to ensure the arbitration ID and the number of cyclic messages hasn't changed. Should be called when modify_data is called in the cyclic task. :raises ValueError: If the given messages are invalid """ if len(self.messages) != len(messages): raise ValueError( "The number of new cyclic messages to be sent must be equal to " "the number of messages originally specified for this task" ) if self.arbitration_id != messages[0].arbitration_id: raise ValueError( "The arbitration ID of new cyclic messages cannot be changed " "from when the task was created" ) def modify_data(self, messages: Union[Sequence[Message], Message]) -> None: """Update the contents of the periodically sent messages, without altering the timing. :param messages: The messages with the new :attr:`Message.data`. Note: The arbitration ID cannot be changed. Note: The number of new cyclic messages to be sent must be equal to the original number of messages originally specified for this task. :raises ValueError: If the given messages are invalid """ messages = self._check_and_convert_messages(messages) self._check_modified_messages(messages) self.messages = messages class MultiRateCyclicSendTaskABC(CyclicSendTaskABC, abc.ABC): """A Cyclic send task that supports switches send frequency after a set time.""" def __init__( self, channel: typechecking.Channel, messages: Union[Sequence[Message], Message], count: int, # pylint: disable=unused-argument initial_period: float, # pylint: disable=unused-argument subsequent_period: float, ) -> None: """ Transmits a message `count` times at `initial_period` then continues to transmit messages at `subsequent_period`. :param channel: See interface specific documentation. :param messages: :param count: :param initial_period: :param subsequent_period: :raises ValueError: If the given messages are invalid """ super().__init__(messages, subsequent_period) self._channel = channel class ThreadBasedCyclicSendTask( LimitedDurationCyclicSendTaskABC, ModifiableCyclicTaskABC, RestartableCyclicTaskABC ): """Fallback cyclic send task using daemon thread.""" def __init__( self, bus: "BusABC", lock: threading.Lock, messages: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, on_error: Optional[Callable[[Exception], bool]] = None, autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> None: """Transmits `messages` with a `period` seconds for `duration` seconds on a `bus`. The `on_error` is called if any error happens on `bus` while sending `messages`. If `on_error` present, and returns ``False`` when invoked, thread is stopped immediately, otherwise, thread continuously tries to send `messages` ignoring errors on a `bus`. Absence of `on_error` means that thread exits immediately on error. :param on_error: The callable that accepts an exception if any error happened on a `bus` while sending `messages`, it shall return either ``True`` or ``False`` depending on desired behaviour of `ThreadBasedCyclicSendTask`. :raises ValueError: If the given messages are invalid """ super().__init__(messages, period, duration) self.bus = bus self.send_lock = lock self.stopped = True self.thread: Optional[threading.Thread] = None self.on_error = on_error self.modifier_callback = modifier_callback self.period_ms = int(round(period * 1000, 0)) self.event: Optional[_Pywin32Event] = None if PYWIN32: if self.period_ms == 0: # A period of 0 would mean that the timer is signaled only once raise ValueError("The period cannot be smaller than 0.001 (1 ms)") self.event = PYWIN32.create_timer() elif ( sys.platform == "win32" and sys.version_info < (3, 11) and platform.python_implementation() == "CPython" ): warnings.warn( f"{self.__class__.__name__} may achieve better timing accuracy " f"if the 'pywin32' package is installed.", RuntimeWarning, stacklevel=1, ) if autostart: self.start() def stop(self) -> None: self.stopped = True if self.event and PYWIN32: # Reset and signal any pending wait by setting the timer to 0 PYWIN32.stop_timer(self.event) def start(self) -> None: self.stopped = False if self.thread is None or not self.thread.is_alive(): name = f"Cyclic send task for 0x{self.messages[0].arbitration_id:X}" self.thread = threading.Thread(target=self._run, name=name) self.thread.daemon = True self.end_time: Optional[float] = ( time.perf_counter() + self.duration if self.duration else None ) if self.event and PYWIN32: PYWIN32.set_timer(self.event, self.period_ms) self.thread.start() def _run(self) -> None: msg_index = 0 msg_due_time_ns = time.perf_counter_ns() if self.event and PYWIN32: # Make sure the timer is non-signaled before entering the loop PYWIN32.wait_0(self.event) while not self.stopped: if self.end_time is not None and time.perf_counter() >= self.end_time: self.stop() break try: if self.modifier_callback is not None: self.modifier_callback(self.messages[msg_index]) with self.send_lock: # Prevent calling bus.send from multiple threads self.bus.send(self.messages[msg_index]) except Exception as exc: # pylint: disable=broad-except log.exception(exc) # stop if `on_error` callback was not given if self.on_error is None: self.stop() raise exc # stop if `on_error` returns False if not self.on_error(exc): self.stop() break if not self.event: msg_due_time_ns += self.period_ns msg_index = (msg_index + 1) % len(self.messages) if self.event and PYWIN32: PYWIN32.wait_inf(self.event) else: # Compensate for the time it takes to send the message delay_ns = msg_due_time_ns - time.perf_counter_ns() if delay_ns > 0: time.sleep(delay_ns / NANOSECONDS_IN_SECOND) python-can-4.5.0/can/bus.py000066400000000000000000000461401472200326600155130ustar00rootroot00000000000000""" Contains the ABC bus implementation and its documentation. """ import contextlib import logging import threading from abc import ABC, ABCMeta, abstractmethod 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 LOG = logging.getLogger(__name__) class BusState(Enum): """The state in which a :class:`can.BusABC` can be.""" ACTIVE = auto() PASSIVE = auto() ERROR = auto() class CanProtocol(Enum): """The CAN protocol type supported by a :class:`can.BusABC` instance""" CAN_20 = auto() CAN_FD = auto() # ISO Mode CAN_FD_NON_ISO = auto() CAN_XL = auto() class BusABC(metaclass=ABCMeta): """The CAN Bus Abstract Base Class that serves as the basis for all concrete interfaces. This class may be used as an iterator over the received messages and as a context manager for auto-closing the bus when done using it. Please refer to :ref:`errors` for possible exceptions that may be thrown by certain operations on this bus. """ #: a string describing the underlying bus and/or channel channel_info = "unknown" #: Log level for received messages RECV_LOGGING_LEVEL = 9 #: Assume that no cleanup is needed until something was initialized _is_shutdown: bool = True _can_protocol: CanProtocol = CanProtocol.CAN_20 @abstractmethod def __init__( self, channel: Any, can_filters: Optional[can.typechecking.CanFilters] = None, **kwargs: object, ): """Construct and open a CAN bus instance of the specified type. Subclasses should call though this method with all given parameters as it handles generic tasks like applying filters. :param channel: The can interface identifier. Expected type is backend dependent. :param can_filters: See :meth:`~can.BusABC.set_filters` for details. :param dict kwargs: Any backend dependent configurations are passed in this dictionary :raises ValueError: If parameters are out of range :raises ~can.exceptions.CanInterfaceNotImplementedError: If the driver cannot be accessed :raises ~can.exceptions.CanInitializationError: If the bus cannot be initialized """ self._periodic_tasks: List[_SelfRemovingCyclicTask] = [] self.set_filters(can_filters) # Flip the class default value when the constructor finishes. That # usually means the derived class constructor was also successful, # since it calls this parent constructor last. self._is_shutdown: bool = False def __str__(self) -> str: return self.channel_info def recv(self, timeout: Optional[float] = None) -> Optional[Message]: """Block waiting for a message from the Bus. :param timeout: seconds to wait for a message or None to wait indefinitely :return: :obj:`None` on timeout or a :class:`~can.Message` object. :raises ~can.exceptions.CanOperationError: If an error occurred while reading """ start = time() time_left = timeout while True: # try to get a message msg, already_filtered = self._recv_internal(timeout=time_left) # return it, if it matches if msg and (already_filtered or self._matches_filters(msg)): LOG.log(self.RECV_LOGGING_LEVEL, "Received: %s", msg) return msg # if not, and timeout is None, try indefinitely elif timeout is None: continue # try next one only if there still is time, and with # reduced timeout else: time_left = timeout - (time() - start) if time_left > 0: continue return None def _recv_internal( self, timeout: Optional[float] ) -> 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` to read a message multiple times if the filters set by :meth:`~can.BusABC.set_filters` do not match and the call has not yet timed out. New implementations should always override this method instead of :meth:`~can.BusABC.recv`, to be able to take advantage of the software based filtering provided by :meth:`~can.BusABC.recv` as a fallback. This method should never be called directly. .. note:: This method is not an `@abstractmethod` (for now) to allow older external implementations to continue using their existing :meth:`~can.BusABC.recv` implementation. .. note:: The second return value (whether filtering was already done) may change over time for some interfaces, like for example in the Kvaser interface. Thus it cannot be simplified to a constant value. :param float timeout: seconds to wait for a message, see :meth:`~can.BusABC.send` :return: 1. a message that was read or None on timeout 2. a bool that is True if message filtering has already been done and else False :raises ~can.exceptions.CanOperationError: If an error occurred while reading :raises NotImplementedError: if the bus provides it's own :meth:`~can.BusABC.recv` implementation (legacy implementation) """ raise NotImplementedError("Trying to read from a write only bus?") @abstractmethod def send(self, msg: Message, timeout: Optional[float] = None) -> None: """Transmit a message to the CAN bus. Override this method to enable the transmit path. :param Message msg: A message object. :param timeout: If > 0, wait up to this many seconds for message to be ACK'ed or for transmit queue to be ready depending on driver implementation. If timeout is exceeded, an exception will be raised. Might not be supported by all interfaces. None blocks indefinitely. :raises ~can.exceptions.CanOperationError: If an error occurred while sending """ raise NotImplementedError("Trying to write to a readonly bus?") def send_periodic( self, msgs: Union[Message, Sequence[Message]], period: float, duration: Optional[float] = None, store_task: bool = True, autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> can.broadcastmanager.CyclicSendTaskABC: """Start sending messages at a given period on this bus. The task will be active until one of the following conditions are met: - the (optional) duration expires - the Bus instance goes out of scope - the Bus instance is shutdown - :meth:`stop_all_periodic_tasks` is called - the task's :meth:`~can.broadcastmanager.CyclicTask.stop` method is called. :param msgs: Message(s) to transmit :param period: Period in seconds between each message :param duration: Approximate duration in seconds to continue sending messages. If no duration is provided, the task will continue indefinitely. :param store_task: If True (the default) the task will be attached to this Bus instance. Disable to instead manage tasks manually. :param autostart: If True (the default) the sending task will immediately start after creation. Otherwise, the task has to be started by calling the tasks :meth:`~can.RestartableCyclicTaskABC.start` method on it. :param modifier_callback: Function which should be used to modify each message's data before sending. The callback modifies the :attr:`~can.Message.data` of the message and returns ``None``. :return: A started task instance. Note the task can be stopped (and depending on the backend modified) by calling the task's :meth:`~can.broadcastmanager.CyclicTask.stop` method. .. note:: Note the duration before the messages stop being sent may not be exactly the same as the duration specified by the user. In general the message will be sent at the given rate until at least **duration** seconds. .. note:: For extremely long-running Bus instances with many short-lived tasks the default api with ``store_task==True`` may not be appropriate as the stopped tasks are still taking up memory as they are associated with the Bus instance. """ if isinstance(msgs, Message): msgs = [msgs] elif isinstance(msgs, Sequence): # A Sequence does not necessarily provide __bool__ we need to use len() if len(msgs) == 0: raise ValueError("Must be a sequence at least of length 1") else: raise ValueError("Must be either a message or a sequence of messages") # Create a backend specific task; will be patched to a _SelfRemovingCyclicTask later task = cast( _SelfRemovingCyclicTask, self._send_periodic_internal( msgs, period, duration, autostart, modifier_callback ), ) # we wrap the task's stop method to also remove it from the Bus's list of tasks periodic_tasks = self._periodic_tasks original_stop_method = task.stop def wrapped_stop_method(remove_task: bool = True) -> None: nonlocal task, periodic_tasks, original_stop_method if remove_task: try: periodic_tasks.remove(task) except ValueError: pass # allow the task to be already removed original_stop_method() task.stop = wrapped_stop_method # type: ignore[method-assign] if store_task: self._periodic_tasks.append(task) return task def _send_periodic_internal( self, msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> can.broadcastmanager.CyclicSendTaskABC: """Default implementation of periodic message sending using threading. Override this method to enable a more efficient backend specific approach. :param msgs: Messages to transmit :param period: Period in seconds between each message :param duration: The duration between sending each message at the given rate. If no duration is provided, the task will continue indefinitely. :param autostart: If True (the default) the sending task will immediately start after creation. Otherwise, the task has to be started by calling the tasks :meth:`~can.RestartableCyclicTaskABC.start` method on it. :return: A started task instance. Note the task can be stopped (and depending on the backend modified) by calling the :meth:`~can.broadcastmanager.CyclicTask.stop` method. """ if not hasattr(self, "_lock_send_periodic"): # Create a send lock for this bus, but not for buses which override this method self._lock_send_periodic = ( # pylint: disable=attribute-defined-outside-init threading.Lock() ) task = ThreadBasedCyclicSendTask( bus=self, lock=self._lock_send_periodic, messages=msgs, period=period, duration=duration, autostart=autostart, modifier_callback=modifier_callback, ) return task def stop_all_periodic_tasks(self, remove_tasks: bool = True) -> None: """Stop sending any messages that were started using :meth:`send_periodic`. .. note:: The result is undefined if a single task throws an exception while being stopped. :param remove_tasks: Stop tracking the stopped tasks. """ if not hasattr(self, "_periodic_tasks"): # avoid AttributeError for partially initialized BusABC instance return for task in self._periodic_tasks: # we cannot let `task.stop()` modify `self._periodic_tasks` while we are # iterating over it (#634) task.stop(remove_task=False) if remove_tasks: self._periodic_tasks.clear() def __iter__(self) -> Iterator[Message]: """Allow iteration on messages as they are received. .. code-block:: python for msg in bus: print(msg) :yields: :class:`Message` msg objects. """ while True: msg = self.recv(timeout=1.0) if msg is not None: yield msg @property def filters(self) -> Optional[can.typechecking.CanFilters]: """ Modify the filters of this bus. See :meth:`~can.BusABC.set_filters` for details. """ return self._filters @filters.setter def filters(self, filters: Optional[can.typechecking.CanFilters]) -> None: self.set_filters(filters) def set_filters( self, filters: Optional[can.typechecking.CanFilters] = None ) -> None: """Apply filtering to all messages received by this Bus. All messages that match at least one filter are returned. If `filters` is `None` or a zero length sequence, all messages are matched. Calling without passing any filters will reset the applied filters to ``None``. :param filters: A iterable of dictionaries each containing a "can_id", a "can_mask", and an optional "extended" key:: [{"can_id": 0x11, "can_mask": 0x21, "extended": False}] A filter matches, when `` & can_mask == can_id & can_mask``. If ``extended`` is set as well, it only matches messages where `` == extended``. Else it matches every messages based only on the arbitration ID and mask. """ self._filters = filters or None with contextlib.suppress(NotImplementedError): self._apply_filters(self._filters) def _apply_filters(self, filters: Optional[can.typechecking.CanFilters]) -> None: """ Hook for applying the filters to the underlying kernel or hardware if supported/implemented by the interface. :param filters: See :meth:`~can.BusABC.set_filters` for details. """ raise NotImplementedError def _matches_filters(self, msg: Message) -> bool: """Checks whether the given message matches at least one of the current filters. See :meth:`~can.BusABC.set_filters` for details on how the filters work. This method should not be overridden. :param msg: the message to check if matching :return: whether the given message matches at least one filter """ # if no filters are set, all messages are matched if self._filters is None: return True 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 # then check for the mask and id can_id = _filter["can_id"] can_mask = _filter["can_mask"] # basically, we compute # `msg.arbitration_id & can_mask == can_id & can_mask` # by using the shorter, but equivalent from below: if (can_id ^ msg.arbitration_id) & can_mask == 0: return True # nothing matched return False def flush_tx_buffer(self) -> None: """Discard every message that may be queued in the output buffer(s).""" raise NotImplementedError def shutdown(self) -> None: """ Called to carry out any interface specific cleanup required in shutting down a bus. This method can be safely called multiple times. """ if self._is_shutdown: LOG.debug("%s is already shut down", self.__class__) return self._is_shutdown = True self.stop_all_periodic_tasks() def __enter__(self) -> Self: return self def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: self.shutdown() def __del__(self) -> None: if not self._is_shutdown: LOG.warning("%s was not properly shut down", self.__class__.__name__) # We do some best-effort cleanup if the user # forgot to properly close the bus instance with contextlib.suppress(AttributeError): self.shutdown() @property def state(self) -> BusState: """ Return the current state of the hardware """ return BusState.ACTIVE @state.setter def state(self, new_state: BusState) -> None: """ Set the new state of the hardware """ raise NotImplementedError("Property is not implemented.") @property def protocol(self) -> CanProtocol: """Return the CAN protocol used by this bus instance. This value is set at initialization time and does not change during the lifetime of a bus instance. """ return self._can_protocol @staticmethod def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: """Detect all configurations/channels that this interface could currently connect with. This might be quite time consuming. May not to be implemented by every interface on every platform. :return: an iterable of dicts, each being a configuration suitable for usage in the interface's bus constructor. """ raise NotImplementedError() def fileno(self) -> int: raise NotImplementedError("fileno is not implemented using current CAN bus") class _SelfRemovingCyclicTask(CyclicSendTaskABC, ABC): """Removes itself from a bus. Only needed for typing :meth:`Bus._periodic_tasks`. Do not instantiate. """ def stop(self, remove_task: bool = True) -> None: raise NotImplementedError() python-can-4.5.0/can/ctypesutil.py000066400000000000000000000046371472200326600171340ustar00rootroot00000000000000""" This module contains common `ctypes` utils. """ import ctypes import logging import sys from typing import Any, Callable, Optional, Tuple, Union log = logging.getLogger("can.ctypesutil") __all__ = ["CLibrary", "HANDLE", "PHANDLE", "HRESULT"] if sys.platform == "win32": _LibBase = ctypes.WinDLL _FUNCTION_TYPE = ctypes.WINFUNCTYPE else: _LibBase = ctypes.CDLL _FUNCTION_TYPE = ctypes.CFUNCTYPE class CLibrary(_LibBase): def __init__(self, library_or_path: Union[str, ctypes.CDLL]) -> None: self.func_name: Any if isinstance(library_or_path, str): super().__init__(library_or_path) else: super().__init__(library_or_path._name, library_or_path._handle) def map_symbol( self, func_name: str, restype: Any = None, argtypes: Tuple[Any, ...] = (), errcheck: Optional[Callable[..., Any]] = None, ) -> Any: """ Map and return a symbol (function) from a C library. A reference to the mapped symbol is also held in the instance :param func_name: symbol_name :param ctypes.c_* restype: function result type (i.e. ctypes.c_ulong...), defaults to void :param tuple(ctypes.c_* ... ) argtypes: argument types, defaults to no args :param callable errcheck: optional error checking function, see ctypes docs for _FuncPtr """ if argtypes: prototype = _FUNCTION_TYPE(restype, *argtypes) else: prototype = _FUNCTION_TYPE(restype) try: func = prototype((func_name, self)) except AttributeError: raise ImportError( f'Could not map function "{func_name}" from library {self._name}' ) from None func._name = func_name # type: ignore[attr-defined] # pylint: disable=protected-access log.debug( 'Wrapped function "%s", result type: %s, error_check %s', func_name, type(restype), errcheck, ) if errcheck is not None: func.errcheck = errcheck setattr(self, func_name, func) return func if sys.platform == "win32": HRESULT = ctypes.HRESULT elif sys.platform == "cygwin": class HRESULT(ctypes.c_long): pass # Common win32 definitions class HANDLE(ctypes.c_void_p): pass PHANDLE = ctypes.POINTER(HANDLE) python-can-4.5.0/can/exceptions.py000066400000000000000000000100011472200326600170660ustar00rootroot00000000000000""" There are several specific :class:`Exception` classes to allow user code to react to specific scenarios related to CAN busses:: Exception (Python standard library) +-- ... +-- CanError (python-can) +-- CanInterfaceNotImplementedError +-- CanInitializationError +-- CanOperationError +-- CanTimeoutError Keep in mind that some functions and methods may raise different exceptions. For example, validating typical arguments and parameters might result in a :class:`ValueError`. This should always be documented for the function at hand. """ import sys 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 class CanError(Exception): """Base class for all CAN related exceptions. If specified, the error code is automatically appended to the message: .. testsetup:: canerror from can import CanError, CanOperationError .. doctest:: canerror >>> # With an error code (it also works with a specific error): >>> error = CanOperationError(message="Failed to do the thing", error_code=42) >>> str(error) 'Failed to do the thing [Error Code 42]' >>> >>> # Missing the error code: >>> plain_error = CanError(message="Something went wrong ...") >>> str(plain_error) 'Something went wrong ...' :param error_code: An optional error code to narrow down the cause of the fault :arg error_code: An optional error code to narrow down the cause of the fault """ def __init__( self, message: str = "", error_code: Optional[int] = None, ) -> None: self.error_code = error_code super().__init__( message if error_code is None else f"{message} [Error Code {error_code}]" ) class CanInterfaceNotImplementedError(CanError, NotImplementedError): """Indicates that the interface is not supported on the current platform. Example scenarios: - No interface with that name exists - The interface is unsupported on the current operating system or interpreter - The driver could not be found or has the wrong version """ class CanInitializationError(CanError): """Indicates an error the occurred while initializing a :class:`can.BusABC`. If initialization fails due to a driver or platform missing/being unsupported, a :exc:`~can.exceptions.CanInterfaceNotImplementedError` is raised instead. If initialization fails due to a value being out of range, a :class:`ValueError` is raised. Example scenarios: - Try to open a non-existent device and/or channel - Try to use an invalid setting, which is ok by value, but not ok for the interface - The device or other resources are already used """ class CanOperationError(CanError): """Indicates an error while in operation. Example scenarios: - A call to a library function results in an unexpected return value - An invalid message was received - The driver rejected a message that was meant to be sent - Cyclic redundancy check (CRC) failed - A message remained unacknowledged - A buffer is full """ class CanTimeoutError(CanError, TimeoutError): """Indicates the timeout of an operation. Example scenarios: - Some message could not be sent after the timeout elapsed - No message was read within the given time """ @contextmanager def error_check( error_message: Optional[str] = None, exception_type: Type[CanError] = CanOperationError, ) -> Generator[None, None, None]: """Catches any exceptions and turns them into the new type while preserving the stack trace.""" try: yield except Exception as error: # pylint: disable=broad-except if error_message is None: raise exception_type(str(error)) from error else: raise exception_type(error_message) from error python-can-4.5.0/can/interface.py000066400000000000000000000153771472200326600166720ustar00rootroot00000000000000""" This module contains the base implementation of :class:`can.BusABC` as well as a list of all available backends and some implemented CyclicSendTasks. """ import importlib import logging from typing import Any, Iterable, List, Optional, Type, Union, cast from . import util from .bus import BusABC from .exceptions import CanInterfaceNotImplementedError from .interfaces import BACKENDS from .typechecking import AutoDetectedConfig, Channel log = logging.getLogger("can.interface") log_autodetect = log.getChild("detect_available_configs") def _get_class_for_interface(interface: str) -> Type[BusABC]: """ Returns the main bus class for the given interface. :raises: NotImplementedError if the interface is not known :raises CanInterfaceNotImplementedError: if there was a problem while importing the interface or the bus class within that """ # Find the correct backend try: module_name, class_name = BACKENDS[interface] except KeyError: raise NotImplementedError( f"CAN interface '{interface}' not supported" ) from None # Import the correct interface module try: module = importlib.import_module(module_name) except Exception as e: raise CanInterfaceNotImplementedError( f"Cannot import module {module_name} for CAN interface '{interface}': {e}" ) from None # Get the correct class try: bus_class = getattr(module, class_name) except Exception as e: raise CanInterfaceNotImplementedError( f"Cannot import class {class_name} from module {module_name} for CAN interface " f"'{interface}': {e}" ) from None return cast(Type[BusABC], bus_class) @util.deprecated_args_alias( deprecation_start="4.2.0", deprecation_end="5.0.0", bustype="interface", context="config_context", ) def Bus( # noqa: N802 channel: Optional[Channel] = None, interface: Optional[str] = None, config_context: Optional[str] = None, ignore_config: bool = False, **kwargs: Any, ) -> BusABC: """Create a new bus instance with configuration loading. Instantiates a CAN Bus of the given ``interface``, falls back to reading a configuration file from default locations. .. note:: Please note that while the arguments provided to this class take precedence over any existing values from configuration, it is possible that other parameters from the configuration may be added to the bus instantiation. This could potentially have unintended consequences. To prevent this, you may use the *ignore_config* parameter to ignore any existing configurations. :param channel: Channel identification. Expected type is backend dependent. Set to ``None`` to let it be resolved automatically from the default :ref:`configuration`. :param interface: See :ref:`interface names` for a list of supported interfaces. Set to ``None`` to let it be resolved automatically from the default :ref:`configuration`. :param config_context: Extra 'context', that is passed to config sources. This can be used to select a section other than 'default' in the configuration file. :param ignore_config: If ``True``, only the given arguments will be used for the bus instantiation. Existing configuration sources will be ignored. :param kwargs: ``interface`` specific keyword arguments. :raises ~can.exceptions.CanInterfaceNotImplementedError: if the ``interface`` isn't recognized or cannot be loaded :raises ~can.exceptions.CanInitializationError: if the bus cannot be instantiated :raises ValueError: if the ``channel`` could not be determined """ # figure out the rest of the configuration; this might raise an error if interface is not None: kwargs["interface"] = interface if channel is not None: kwargs["channel"] = channel if not ignore_config: kwargs = util.load_config(config=kwargs, context=config_context) # resolve the bus class to use for that interface cls = _get_class_for_interface(kwargs["interface"]) # remove the "interface" key, so it doesn't get passed to the backend del kwargs["interface"] # make sure the bus can handle this config format channel = kwargs.pop("channel", channel) if channel is None: # Use the default channel for the backend bus = cls(**kwargs) else: bus = cls(channel, **kwargs) return bus def detect_available_configs( interfaces: Union[None, str, Iterable[str]] = None ) -> List[AutoDetectedConfig]: """Detect all configurations/channels that the interfaces could currently connect with. This might be quite time-consuming. Automated configuration detection may not be implemented by every interface on every platform. This method will not raise an error in that case, but with rather return an empty list 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. :rtype: list[dict] :return: an iterable of dicts, each suitable for usage in the constructor of :class:`can.BusABC`. """ # Figure out where to search if interfaces is None: interfaces = BACKENDS elif isinstance(interfaces, str): interfaces = (interfaces,) # else it is supposed to be an iterable of strings result = [] for interface in interfaces: try: bus_class = _get_class_for_interface(interface) except CanInterfaceNotImplementedError: log_autodetect.debug( 'interface "%s" cannot be loaded for detection of available configurations', interface, ) 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 return result python-can-4.5.0/can/interfaces/000077500000000000000000000000001472200326600164665ustar00rootroot00000000000000python-can-4.5.0/can/interfaces/__init__.py000066400000000000000000000042161472200326600206020ustar00rootroot00000000000000""" Interfaces contain low level implementations that interact with CAN hardware. """ from typing import Dict, Tuple from can._entry_points import read_entry_points __all__ = [ "BACKENDS", "VALID_INTERFACES", "canalystii", "cantact", "etas", "gs_usb", "ics_neovi", "iscan", "ixxat", "kvaser", "neousys", "nican", "nixnet", "pcan", "robotell", "seeedstudio", "serial", "slcan", "socketcan", "socketcand", "systec", "udp_multicast", "usb2can", "vector", "virtual", ] # interface_name => (module, classname) BACKENDS: Dict[str, Tuple[str, str]] = { "kvaser": ("can.interfaces.kvaser", "KvaserBus"), "socketcan": ("can.interfaces.socketcan", "SocketcanBus"), "serial": ("can.interfaces.serial.serial_can", "SerialBus"), "pcan": ("can.interfaces.pcan", "PcanBus"), "usb2can": ("can.interfaces.usb2can", "Usb2canBus"), "ixxat": ("can.interfaces.ixxat", "IXXATBus"), "nican": ("can.interfaces.nican", "NicanBus"), "iscan": ("can.interfaces.iscan", "IscanBus"), "virtual": ("can.interfaces.virtual", "VirtualBus"), "udp_multicast": ("can.interfaces.udp_multicast", "UdpMulticastBus"), "neovi": ("can.interfaces.ics_neovi", "NeoViBus"), "vector": ("can.interfaces.vector", "VectorBus"), "slcan": ("can.interfaces.slcan", "slcanBus"), "robotell": ("can.interfaces.robotell", "robotellBus"), "canalystii": ("can.interfaces.canalystii", "CANalystIIBus"), "systec": ("can.interfaces.systec", "UcanBus"), "seeedstudio": ("can.interfaces.seeedstudio", "SeeedBus"), "cantact": ("can.interfaces.cantact", "CantactBus"), "gs_usb": ("can.interfaces.gs_usb", "GsUsbBus"), "nixnet": ("can.interfaces.nixnet", "NiXNETcanBus"), "neousys": ("can.interfaces.neousys", "NeousysBus"), "etas": ("can.interfaces.etas", "EtasBus"), "socketcand": ("can.interfaces.socketcand", "SocketCanDaemonBus"), } BACKENDS.update( { interface.key: (interface.module_name, interface.class_name) for interface in read_entry_points(group="can.interface") } ) VALID_INTERFACES = frozenset(sorted(BACKENDS.keys())) python-can-4.5.0/can/interfaces/canalystii.py000066400000000000000000000176011472200326600212050ustar00rootroot00000000000000import logging import time from collections import deque from ctypes import c_ubyte from typing import Any, Deque, Dict, Optional, Sequence, Tuple, Union import canalystii as driver from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message from can.exceptions import CanTimeoutError from can.typechecking import CanFilters from can.util import check_or_adjust_timing_clock, deprecated_args_alias logger = logging.getLogger(__name__) class CANalystIIBus(BusABC): @deprecated_args_alias( deprecation_start="4.2.0", deprecation_end="5.0.0", bit_timing="timing" ) def __init__( self, channel: Union[int, Sequence[int], str] = (0, 1), device: int = 0, bitrate: Optional[int] = None, timing: Optional[Union[BitTiming, BitTimingFd]] = None, can_filters: Optional[CanFilters] = None, rx_queue_size: Optional[int] = None, **kwargs: Dict[str, Any], ): """ :param channel: Optional channel number, list/tuple of multiple channels, or comma separated string of channels. Default is to configure both channels. :param device: Optional USB device number. Default is 0 (first device found). :param bitrate: CAN bitrate in bits/second. Required unless the bit_timing argument is set. :param timing: Optional :class:`~can.BitTiming` instance to use for custom bit timing setting. If this argument is set then it overrides the bitrate argument. The `f_clock` value of the timing instance must be set to 8_000_000 (8MHz) for standard CAN. CAN FD and the :class:`~can.BitTimingFd` class are not supported. :param can_filters: Optional filters for received CAN messages. :param rx_queue_size: If set, software received message queue can only grow to this many messages (for all channels) before older messages are dropped """ if not (bitrate or timing): raise ValueError("Either bitrate or timing argument is required") # Do this after the error handling super().__init__( channel=channel, can_filters=can_filters, **kwargs, ) if isinstance(channel, str): # Assume comma separated string of channels self.channels = [int(ch.strip()) for ch in channel.split(",")] elif isinstance(channel, int): self.channels = [channel] else: # Sequence[int] self.channels = list(channel) self.channel_info = f"CANalyst-II: device {device}, channels {self.channels}" self.rx_queue: Deque[Tuple[int, driver.Message]] = deque(maxlen=rx_queue_size) self.device = driver.CanalystDevice(device_index=device) self._can_protocol = CanProtocol.CAN_20 for single_channel in self.channels: if isinstance(timing, BitTiming): timing = check_or_adjust_timing_clock(timing, valid_clocks=[8_000_000]) self.device.init( single_channel, timing0=timing.btr0, timing1=timing.btr1 ) elif isinstance(timing, BitTimingFd): raise NotImplementedError( f"CAN FD is not supported by {self.__class__.__name__}." ) else: self.device.init(single_channel, bitrate=bitrate) # Delay to use between each poll for new messages # # The timeout is deliberately kept low to avoid the possibility of # a hardware buffer overflow. This value was determined # experimentally, but the ideal value will depend on the specific # system. RX_POLL_DELAY = 0.020 def send(self, msg: Message, timeout: Optional[float] = None) -> None: """Send a CAN message to the bus :param msg: message to send :param timeout: timeout (in seconds) to wait for the TX queue to clear. If set to ``None`` (default) the function returns immediately. Note: Due to limitations in the device firmware and protocol, the timeout will not trigger if there are problems with CAN arbitration, but only if the device is overloaded with a backlog of too many messages to send. """ raw_message = driver.Message( msg.arbitration_id, 0, # timestamp 1, # time_flag 0, # send_type msg.is_remote_frame, msg.is_extended_id, msg.dlc, (c_ubyte * 8)(*msg.data), ) if msg.channel is not None: channel = msg.channel elif len(self.channels) == 1: channel = self.channels[0] else: raise ValueError( "Message channel must be set when using multiple channels." ) send_result = self.device.send(channel, [raw_message], timeout) if timeout is not None and not send_result: raise CanTimeoutError(f"Send timed out after {timeout} seconds") def _recv_from_queue(self) -> Tuple[Message, bool]: """Return a message from the internal receive queue""" channel, raw_msg = self.rx_queue.popleft() # Protocol timestamps are in units of 100us, convert to seconds as # float timestamp = raw_msg.timestamp * 100e-6 return ( Message( channel=channel, timestamp=timestamp, arbitration_id=raw_msg.can_id, is_extended_id=raw_msg.extended, is_remote_frame=raw_msg.remote, dlc=raw_msg.data_len, data=bytes(raw_msg.data), ), False, ) def poll_received_messages(self) -> None: """Poll new messages from the device into the rx queue but don't return any message to the caller Calling this function isn't necessary as polling the device is done automatically when calling recv(). This function is for the situation where an application needs to empty the hardware receive buffer without consuming any message. """ for channel in self.channels: self.rx_queue.extend( (channel, raw_msg) for raw_msg in self.device.receive(channel) ) def _recv_internal( self, timeout: Optional[float] = None ) -> Tuple[Optional[Message], bool]: """ :param timeout: float in seconds :return: """ if self.rx_queue: return self._recv_from_queue() deadline = None while deadline is None or time.time() < deadline: if deadline is None and timeout is not None: deadline = time.time() + timeout self.poll_received_messages() if self.rx_queue: return self._recv_from_queue() # If blocking on a timeout, add a sleep before we loop again # to reduce CPU usage. if deadline is None or deadline - time.time() > 0.050: time.sleep(self.RX_POLL_DELAY) return (None, False) def flush_tx_buffer(self, channel: Optional[int] = None) -> None: """Flush the TX buffer of the device. :param channel: Optional channel number to flush. If set to None, all initialized channels are flushed. Note that because of protocol limitations this function returning doesn't mean that messages have been sent, it may also mean they failed to send. """ if channel: self.device.flush_tx_buffer(channel, float("infinity")) else: for ch in self.channels: self.device.flush_tx_buffer(ch, float("infinity")) def shutdown(self) -> None: super().shutdown() for channel in self.channels: self.device.stop(channel) self.device = None python-can-4.5.0/can/interfaces/cantact.py000066400000000000000000000135241472200326600204620ustar00rootroot00000000000000""" Interface for CANtact devices from Linklayer Labs """ import logging import time from typing import Any, Optional, Union from unittest.mock import Mock from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message from ..exceptions import ( CanInitializationError, CanInterfaceNotImplementedError, error_check, ) from ..util import check_or_adjust_timing_clock, deprecated_args_alias logger = logging.getLogger(__name__) try: import cantact except ImportError: cantact = None logger.warning( "The CANtact module is not installed. Install it using `pip install cantact`" ) class CantactBus(BusABC): """CANtact interface""" @staticmethod def _detect_available_configs(): try: interface = cantact.Interface() except (NameError, SystemError, AttributeError): logger.debug( "Could not import or instantiate cantact, so no configurations are available" ) return [] channels = [] for i in range(0, interface.channel_count()): channels.append({"interface": "cantact", "channel": f"ch:{i}"}) return channels @deprecated_args_alias( deprecation_start="4.2.0", deprecation_end="5.0.0", bit_timing="timing" ) def __init__( self, channel: int, bitrate: int = 500_000, poll_interval: float = 0.01, monitor: bool = False, timing: Optional[Union[BitTiming, BitTimingFd]] = None, **kwargs: Any, ) -> None: """ :param int channel: Channel number (zero indexed, labeled on multi-channel devices) :param int bitrate: Bitrate in bits/s :param bool monitor: If true, operate in listen-only monitoring mode :param timing: Optional :class:`~can.BitTiming` instance to use for custom bit timing setting. If this argument is set then it overrides the bitrate argument. The `f_clock` value of the timing instance must be set to 24_000_000 (24MHz) for standard CAN. CAN FD and the :class:`~can.BitTimingFd` class are not supported. """ if kwargs.get("_testing", False): self.interface = MockInterface() else: if cantact is None: raise CanInterfaceNotImplementedError( "The CANtact module is not installed. " "Install it using `python -m pip install cantact`" ) with error_check( "Cannot create the cantact.Interface", CanInitializationError ): self.interface = cantact.Interface() self.channel = int(channel) self.channel_info = f"CANtact: ch:{channel}" self._can_protocol = CanProtocol.CAN_20 # Configure the interface with error_check("Cannot setup the cantact.Interface", CanInitializationError): if isinstance(timing, BitTiming): timing = check_or_adjust_timing_clock(timing, valid_clocks=[24_000_000]) # use custom bit timing self.interface.set_bit_timing( int(channel), int(timing.brp), int(timing.tseg1), int(timing.tseg2), int(timing.sjw), ) elif isinstance(timing, BitTimingFd): raise NotImplementedError( f"CAN FD is not supported by {self.__class__.__name__}." ) else: # use bitrate self.interface.set_bitrate(int(channel), int(bitrate)) self.interface.set_enabled(int(channel), True) self.interface.set_monitor(int(channel), monitor) self.interface.start() super().__init__( channel=channel, bitrate=bitrate, poll_interval=poll_interval, **kwargs, ) def _recv_internal(self, timeout): with error_check("Cannot receive message"): frame = self.interface.recv(int(timeout * 1000)) if frame is None: # timeout occurred return None, False msg = Message( arbitration_id=frame["id"], is_extended_id=frame["extended"], timestamp=frame["timestamp"], is_remote_frame=frame["rtr"], dlc=frame["dlc"], data=frame["data"][: frame["dlc"]], channel=frame["channel"], is_rx=(not frame["loopback"]), # received if not loopback frame ) return msg, False def send(self, msg, timeout=None): with error_check("Cannot send message"): self.interface.send( self.channel, msg.arbitration_id, bool(msg.is_extended_id), bool(msg.is_remote_frame), msg.dlc, msg.data, ) def shutdown(self): super().shutdown() with error_check("Cannot shutdown interface"): self.interface.stop() def mock_recv(timeout): if timeout > 0: return { "id": 0x123, "extended": False, "timestamp": time.time(), "loopback": False, "rtr": False, "dlc": 8, "data": [1, 2, 3, 4, 5, 6, 7, 8], "channel": 0, } else: # simulate timeout when timeout = 0 return None class MockInterface: """ Mock interface to replace real interface when testing. This allows for tests to run without actual hardware. """ start = Mock() set_bitrate = Mock() set_bit_timing = Mock() set_enabled = Mock() set_monitor = Mock() stop = Mock() send = Mock() channel_count = Mock(return_value=1) recv = Mock(side_effect=mock_recv) python-can-4.5.0/can/interfaces/etas/000077500000000000000000000000001472200326600174225ustar00rootroot00000000000000python-can-4.5.0/can/interfaces/etas/__init__.py000066400000000000000000000274751472200326600215520ustar00rootroot00000000000000import time from typing import Dict, List, Optional, Tuple import can from can.exceptions import CanInitializationError from .boa import * class EtasBus(can.BusABC): def __init__( self, channel: str, can_filters: Optional[can.typechecking.CanFilters] = None, receive_own_messages: bool = False, bitrate: int = 1000000, fd: bool = True, data_bitrate: int = 2000000, **kwargs: Dict[str, any], ): self.receive_own_messages = receive_own_messages self._can_protocol = can.CanProtocol.CAN_FD if fd else can.CanProtocol.CAN_20 nodeRange = CSI_NodeRange(CSI_NODE_MIN, CSI_NODE_MAX) self.tree = ctypes.POINTER(CSI_Tree)() CSI_CreateProtocolTree(ctypes.c_char_p(b""), nodeRange, ctypes.byref(self.tree)) oci_can_v = BOA_Version(1, 4, 0, 0) self.ctrl = OCI_ControllerHandle() OCI_CreateCANControllerNoSearch( channel.encode(), ctypes.byref(oci_can_v), self.tree, ctypes.byref(self.ctrl), ) ctrlConf = OCI_CANConfiguration() ctrlConf.baudrate = bitrate ctrlConf.samplePoint = 80 ctrlConf.samplesPerBit = OCI_CAN_THREE_SAMPLES_PER_BIT ctrlConf.BTL_Cycles = 10 ctrlConf.SJW = 1 ctrlConf.syncEdge = OCI_CAN_SINGLE_SYNC_EDGE ctrlConf.physicalMedia = OCI_CAN_MEDIA_HIGH_SPEED if receive_own_messages: ctrlConf.selfReceptionMode = OCI_SELF_RECEPTION_ON else: ctrlConf.selfReceptionMode = OCI_SELF_RECEPTION_OFF ctrlConf.busParticipationMode = OCI_BUSMODE_ACTIVE if fd: ctrlConf.canFDEnabled = True ctrlConf.canFDConfig.dataBitRate = data_bitrate ctrlConf.canFDConfig.dataBTL_Cycles = 10 ctrlConf.canFDConfig.dataSamplePoint = 80 ctrlConf.canFDConfig.dataSJW = 1 ctrlConf.canFDConfig.flags = 0 ctrlConf.canFDConfig.canFdTxConfig = OCI_CANFDTX_USE_CAN_AND_CANFD_FRAMES ctrlConf.canFDConfig.canFdRxConfig.canRxMode = ( OCI_CAN_RXMODE_CAN_FRAMES_USING_CAN_MESSAGE ) ctrlConf.canFDConfig.canFdRxConfig.canFdRxMode = ( OCI_CANFDRXMODE_CANFD_FRAMES_USING_CANFD_MESSAGE ) ctrlProp = OCI_CANControllerProperties() ctrlProp.mode = OCI_CONTROLLER_MODE_RUNNING ec = OCI_OpenCANController( self.ctrl, ctypes.byref(ctrlConf), ctypes.byref(ctrlProp) ) if ec != 0x0 and ec != 0x40004000: # accept BOA_WARN_PARAM_ADAPTED raise CanInitializationError( f"OCI_OpenCANController failed with error 0x{ec:X}" ) # RX rxQConf = OCI_CANRxQueueConfiguration() rxQConf.onFrame.function = ctypes.cast(None, OCI_CANRxCallbackFunctionSingleMsg) rxQConf.onFrame.userData = None rxQConf.onEvent.function = ctypes.cast(None, OCI_CANRxCallbackFunctionSingleMsg) rxQConf.onEvent.userData = None if receive_own_messages: rxQConf.selfReceptionMode = OCI_SELF_RECEPTION_ON else: rxQConf.selfReceptionMode = OCI_SELF_RECEPTION_OFF self.rxQueue = OCI_QueueHandle() OCI_CreateCANRxQueue( self.ctrl, ctypes.byref(rxQConf), ctypes.byref(self.rxQueue) ) self._oci_filters = None self.filters = can_filters # TX txQConf = OCI_CANTxQueueConfiguration() txQConf.reserved = 0 self.txQueue = OCI_QueueHandle() OCI_CreateCANTxQueue( self.ctrl, ctypes.byref(txQConf), ctypes.byref(self.txQueue) ) # Common timerCapabilities = OCI_TimerCapabilities() OCI_GetTimerCapabilities(self.ctrl, ctypes.byref(timerCapabilities)) self.tickFrequency = timerCapabilities.tickFrequency # clock ticks per second # all timestamps are hardware timestamps relative to the CAN device powerup # calculate an offset to make them relative to epoch now = OCI_Time() OCI_GetTimerValue(self.ctrl, ctypes.byref(now)) self.timeOffset = time.time() - (float(now.value) / self.tickFrequency) self.channel_info = channel # Super call must be after child init since super calls set_filters super().__init__(channel=channel, **kwargs) def _recv_internal( self, timeout: Optional[float] ) -> Tuple[Optional[can.Message], bool]: ociMsgs = (ctypes.POINTER(OCI_CANMessageEx) * 1)() ociMsg = OCI_CANMessageEx() ociMsgs[0] = ctypes.pointer(ociMsg) count = ctypes.c_uint32() if timeout is not None: # wait for specified time t = OCI_Time(round(timeout * self.tickFrequency)) else: # wait indefinitely t = OCI_NO_TIME OCI_ReadCANDataEx( self.rxQueue, t, ociMsgs, 1, ctypes.byref(count), None, ) msg = None if count.value != 0: if ociMsg.type == OCI_CANFDRX_MESSAGE.value: ociRxMsg = ociMsg.data.canFDRxMessage msg = can.Message( timestamp=float(ociRxMsg.timeStamp) / self.tickFrequency + self.timeOffset, arbitration_id=ociRxMsg.frameID, is_extended_id=bool(ociRxMsg.flags & OCI_CAN_MSG_FLAG_EXTENDED), is_remote_frame=bool( ociRxMsg.flags & OCI_CAN_MSG_FLAG_REMOTE_FRAME ), # is_error_frame=False, # channel=None, dlc=ociRxMsg.size, data=ociRxMsg.data[0 : ociRxMsg.size], is_fd=True, is_rx=not bool(ociRxMsg.flags & OCI_CAN_MSG_FLAG_SELFRECEPTION), bitrate_switch=bool( ociRxMsg.flags & OCI_CAN_MSG_FLAG_FD_DATA_BIT_RATE ), # error_state_indicator=False, # check=False, ) elif ociMsg.type == OCI_CAN_RX_MESSAGE.value: ociRxMsg = ociMsg.data.rxMessage msg = can.Message( timestamp=float(ociRxMsg.timeStamp) / self.tickFrequency + self.timeOffset, arbitration_id=ociRxMsg.frameID, is_extended_id=bool(ociRxMsg.flags & OCI_CAN_MSG_FLAG_EXTENDED), is_remote_frame=bool( ociRxMsg.flags & OCI_CAN_MSG_FLAG_REMOTE_FRAME ), # is_error_frame=False, # channel=None, dlc=ociRxMsg.dlc, data=ociRxMsg.data[0 : ociRxMsg.dlc], # is_fd=False, is_rx=not bool(ociRxMsg.flags & OCI_CAN_MSG_FLAG_SELFRECEPTION), # bitrate_switch=False, # error_state_indicator=False, # check=False, ) return (msg, True) def send(self, msg: can.Message, timeout: Optional[float] = None) -> None: ociMsgs = (ctypes.POINTER(OCI_CANMessageEx) * 1)() ociMsg = OCI_CANMessageEx() ociMsgs[0] = ctypes.pointer(ociMsg) if msg.is_fd: ociMsg.type = OCI_CANFDTX_MESSAGE ociTxMsg = ociMsg.data.canFDTxMessage ociTxMsg.size = msg.dlc else: ociMsg.type = OCI_CAN_TX_MESSAGE ociTxMsg = ociMsg.data.txMessage ociTxMsg.dlc = msg.dlc # set fields common to CAN / CAN-FD ociTxMsg.frameID = msg.arbitration_id ociTxMsg.flags = 0 if msg.is_extended_id: ociTxMsg.flags |= OCI_CAN_MSG_FLAG_EXTENDED if msg.is_remote_frame: ociTxMsg.flags |= OCI_CAN_MSG_FLAG_REMOTE_FRAME ociTxMsg.data = tuple(msg.data) if msg.is_fd: ociTxMsg.flags |= OCI_CAN_MSG_FLAG_FD_DATA if msg.bitrate_switch: ociTxMsg.flags |= OCI_CAN_MSG_FLAG_FD_DATA_BIT_RATE OCI_WriteCANDataEx(self.txQueue, OCI_NO_TIME, ociMsgs, 1, None) def _apply_filters(self, filters: Optional[can.typechecking.CanFilters]) -> None: if self._oci_filters: OCI_RemoveCANFrameFilterEx(self.rxQueue, self._oci_filters, 1) # "accept all" filter if filters is None: filters = [{"can_id": 0x0, "can_mask": 0x0}] self._oci_filters = (ctypes.POINTER(OCI_CANRxFilterEx) * len(filters))() for i, filter_ in enumerate(filters): f = OCI_CANRxFilterEx() f.frameIDValue = filter_["can_id"] f.frameIDMask = filter_["can_mask"] f.tag = 0 f.flagsValue = 0 if self.receive_own_messages: # mask out the SR bit, i.e. ignore the bit -> receive all f.flagsMask = 0 else: # enable the SR bit in the mask. since the bit is 0 in flagsValue -> do not self-receive f.flagsMask = OCI_CAN_MSG_FLAG_SELFRECEPTION if filter_.get("extended"): f.flagsValue |= OCI_CAN_MSG_FLAG_EXTENDED f.flagsMask |= OCI_CAN_MSG_FLAG_EXTENDED self._oci_filters[i].contents = f OCI_AddCANFrameFilterEx(self.rxQueue, self._oci_filters, len(self._oci_filters)) def flush_tx_buffer(self) -> None: OCI_ResetQueue(self.txQueue) def shutdown(self) -> None: super().shutdown() # Cleanup TX if self.txQueue: OCI_DestroyCANTxQueue(self.txQueue) self.txQueue = None # Cleanup RX if self.rxQueue: OCI_DestroyCANRxQueue(self.rxQueue) self.rxQueue = None # Cleanup common if self.ctrl: OCI_CloseCANController(self.ctrl) OCI_DestroyCANController(self.ctrl) self.ctrl = None if self.tree: CSI_DestroyProtocolTree(self.tree) self.tree = None @property def state(self) -> can.BusState: status = OCI_CANControllerStatus() OCI_GetCANControllerStatus(self.ctrl, ctypes.byref(status)) if status.stateCode & OCI_CAN_STATE_ACTIVE: return can.BusState.ACTIVE elif status.stateCode & OCI_CAN_STATE_PASSIVE: return can.BusState.PASSIVE @state.setter def state(self, new_state: can.BusState) -> None: # disabled, OCI_AdaptCANConfiguration does not allow changing the bus mode # if new_state == can.BusState.ACTIVE: # self.ctrlConf.busParticipationMode = OCI_BUSMODE_ACTIVE # else: # self.ctrlConf.busParticipationMode = OCI_BUSMODE_PASSIVE # ec = OCI_AdaptCANConfiguration(self.ctrl, ctypes.byref(self.ctrlConf)) # if ec != 0x0: # raise CanOperationError(f"OCI_AdaptCANConfiguration failed with error 0x{ec:X}") raise NotImplementedError("Setting state is not implemented.") @staticmethod def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: nodeRange = CSI_NodeRange(CSI_NODE_MIN, CSI_NODE_MAX) tree = ctypes.POINTER(CSI_Tree)() CSI_CreateProtocolTree(ctypes.c_char_p(b""), nodeRange, ctypes.byref(tree)) nodes: List[Dict[str, str]] = [] def _findNodes(tree, prefix): uri = f"{prefix}/{tree.contents.item.uriName.decode()}" if "CAN:" in uri: nodes.append({"interface": "etas", "channel": uri}) elif tree.contents.child: _findNodes( tree.contents.child, f"{prefix}/{tree.contents.item.uriName.decode()}", ) if tree.contents.sibling: _findNodes(tree.contents.sibling, prefix) _findNodes(tree, "ETAS:/") CSI_DestroyProtocolTree(tree) return nodes python-can-4.5.0/can/interfaces/etas/boa.py000066400000000000000000000502441472200326600205420ustar00rootroot00000000000000import ctypes from ...exceptions import CanInitializationError, CanOperationError try: # try to load libraries from the system default paths _csi = ctypes.windll.LoadLibrary("dll-csiBind") _oci = ctypes.windll.LoadLibrary("dll-ocdProxy") except FileNotFoundError: # try to load libraries with hardcoded paths if ctypes.sizeof(ctypes.c_voidp) == 4: # 32 bit path = "C:/Program Files (x86)/ETAS/BOA_V2/Bin/Win32/Dll/Framework/" elif ctypes.sizeof(ctypes.c_voidp) == 8: # 64 bit path = "C:/Program Files/ETAS/BOA_V2/Bin/x64/Dll/Framework/" _csi = ctypes.windll.LoadLibrary(path + "dll-csiBind") _oci = ctypes.windll.LoadLibrary(path + "dll-ocdProxy") # define helper functions to use with errcheck def errcheck_init(result, func, _arguments): # unfortunately, we can't use OCI_GetError here # because we don't always have a handle to use # text = ctypes.create_string_buffer(500) # OCI_GetError(self.ctrl, ec, text, 500) if result != 0x0: raise CanInitializationError(f"{func.__name__} failed with error 0x{result:X}") return result def errcheck_oper(result, func, _arguments): if result != 0x0: raise CanOperationError(f"{func.__name__} failed with error 0x{result:X}") return result # Common (BOA) BOA_ResultCode = ctypes.c_uint32 BOA_Handle = ctypes.c_int32 BOA_Time = ctypes.c_int64 BOA_NO_VALUE = -1 BOA_NO_HANDLE = BOA_Handle(BOA_NO_VALUE) BOA_NO_TIME = BOA_Time(BOA_NO_VALUE) class BOA_UuidBin(ctypes.Structure): _fields_ = [("data", ctypes.c_uint8 * 16)] class BOA_Version(ctypes.Structure): _fields_ = [ ("majorVersion", ctypes.c_uint8), ("minorVersion", ctypes.c_uint8), ("bugfix", ctypes.c_uint8), ("build", ctypes.c_uint8), ] class BOA_UuidVersion(ctypes.Structure): _fields_ = [("uuid", BOA_UuidBin), ("version", BOA_Version)] class BOA_ServiceId(ctypes.Structure): _fields_ = [("api", BOA_UuidVersion), ("access", BOA_UuidVersion)] class BOA_ServiceIdParam(ctypes.Structure): _fields_ = [ ("id", BOA_ServiceId), ("count", ctypes.c_uint32), ("accessParam", ctypes.c_char * 128), ] # Connection Service Interface (CSI) # CSI - Search For Service (SFS) CSI_NodeType = ctypes.c_uint32 CSI_NODE_MIN = CSI_NodeType(0) CSI_NODE_MAX = CSI_NodeType(0x7FFF) class CSI_NodeRange(ctypes.Structure): _fields_ = [("min", CSI_NodeType), ("max", CSI_NodeType)] class CSI_SubItem(ctypes.Structure): _fields_ = [ ("server", BOA_ServiceIdParam), ("nodeType", CSI_NodeType), ("uriName", ctypes.c_char * 128), ("visibleName", ctypes.c_char * 4), ("version", BOA_Version), ("reserved2", ctypes.c_char * 88), ("serverAffinity", BOA_UuidBin), ("requiredAffinity0", BOA_UuidBin), ("reserved", ctypes.c_int32 * 4), ("requiredAffinity1", BOA_UuidBin), ("count", ctypes.c_int32), ("requiredAPI", BOA_ServiceId * 4), ] class CSI_Tree(ctypes.Structure): pass CSI_Tree._fields_ = [ ("item", CSI_SubItem), ("sibling", ctypes.POINTER(CSI_Tree)), ("child", ctypes.POINTER(CSI_Tree)), ("childrenProbed", ctypes.c_int), ] CSI_CreateProtocolTree = _csi.CSI_CreateProtocolTree CSI_CreateProtocolTree.argtypes = [ ctypes.c_char_p, CSI_NodeRange, ctypes.POINTER(ctypes.POINTER(CSI_Tree)), ] CSI_CreateProtocolTree.restype = BOA_ResultCode CSI_CreateProtocolTree.errcheck = errcheck_init CSI_DestroyProtocolTree = _csi.CSI_DestroyProtocolTree CSI_DestroyProtocolTree.argtypes = [ ctypes.POINTER(CSI_Tree), ] CSI_DestroyProtocolTree.restype = BOA_ResultCode CSI_DestroyProtocolTree.errcheck = errcheck_oper # Open Controller Interface (OCI) # OCI Common - Global Types OCI_NO_VALUE = BOA_NO_VALUE OCI_NO_HANDLE = BOA_NO_HANDLE OCI_Handle = BOA_Handle OCI_Time = BOA_Time # OCI Common - Controller Handling OCI_ControllerHandle = OCI_Handle OCI_ControllerPropertiesMode = ctypes.c_uint32 OCI_CONTROLLER_MODE_RUNNING = OCI_ControllerPropertiesMode(0) OCI_CONTROLLER_MODE_SUSPENDED = OCI_ControllerPropertiesMode(1) OCI_SelfReceptionMode = ctypes.c_uint32 OCI_SELF_RECEPTION_OFF = OCI_SelfReceptionMode(0) OCI_SELF_RECEPTION_ON = OCI_SelfReceptionMode(1) # OCI Common - Event Handling # OCI Common - Error Management OCI_ErrorCode = BOA_ResultCode OCI_InternalErrorEvent = ctypes.c_uint32 OCI_INTERNAL_GENERAL_ERROR = OCI_InternalErrorEvent(0) class OCI_InternalErrorEventMessage(ctypes.Structure): _fields_ = [ ("timeStamp", OCI_Time), ("tag", ctypes.c_uint32), ("eventCode", OCI_InternalErrorEvent), ("errorCode", OCI_ErrorCode), ] OCI_GetError = _oci.OCI_GetError OCI_GetError.argtypes = [ OCI_Handle, OCI_ErrorCode, ctypes.c_char_p, ctypes.c_uint32, ] OCI_GetError.restype = OCI_ErrorCode OCI_GetError.errcheck = errcheck_oper # OCI Common - Queue Handling OCI_QueueHandle = OCI_Handle OCI_QueueEvent = ctypes.c_uint32 OCI_QUEUE_UNDERRUN = OCI_QueueEvent(0) OCI_QUEUE_EMPTY = OCI_QueueEvent(1) OCI_QUEUE_NOT_EMPTY = OCI_QueueEvent(2) OCI_QUEUE_LOW_WATERMARK = OCI_QueueEvent(3) OCI_QUEUE_HIGH_WATERMARK = OCI_QueueEvent(4) OCI_QUEUE_FULL = OCI_QueueEvent(5) OCI_QUEUE_OVERFLOW = OCI_QueueEvent(6) class OCI_QueueEventMessage(ctypes.Structure): _fields_ = [ ("timeStamp", OCI_Time), ("tag", ctypes.c_uint32), ("eventCode", OCI_QueueEvent), ("destination", ctypes.c_uint32), ] OCI_ResetQueue = _oci.OCI_ResetQueue OCI_ResetQueue.argtypes = [OCI_QueueHandle] OCI_ResetQueue.restype = OCI_ErrorCode OCI_ResetQueue.errcheck = errcheck_oper # OCI Common - Timer Handling OCI_NO_TIME = BOA_NO_TIME OCI_TimeReferenceScale = ctypes.c_uint32 OCI_TimeReferenceScaleUnknown = OCI_TimeReferenceScale(0) OCI_TimeReferenceScaleTAI = OCI_TimeReferenceScale(1) OCI_TimeReferenceScaleUTC = OCI_TimeReferenceScale(2) OCI_TimerEvent = ctypes.c_uint32 OCI_TIMER_EVENT_SYNC_LOCK = OCI_TimerEvent(0) OCI_TIMER_EVENT_SYNC_LOSS = OCI_TimerEvent(1) OCI_TIMER_EVENT_LEAP_SECOND = OCI_TimerEvent(2) class OCI_TimerCapabilities(ctypes.Structure): _fields_ = [ ("localClockID", ctypes.c_char * 40), ("format", ctypes.c_uint32), ("tickFrequency", ctypes.c_uint32), ("ticksPerIncrement", ctypes.c_uint32), ("localStratumLevel", ctypes.c_uint32), ("localReferenceScale", OCI_TimeReferenceScale), ("localTimeOriginIso8601", ctypes.c_char * 40), ("syncSlave", ctypes.c_uint32), ("syncMaster", ctypes.c_uint32), ] class OCI_TimerEventMessage(ctypes.Structure): _fields_ = [ ("timeStamp", OCI_Time), ("tag", ctypes.c_uint32), ("eventCode", OCI_TimerEvent), ("destination", ctypes.c_uint32), ] OCI_GetTimerCapabilities = _oci.OCI_GetTimerCapabilities OCI_GetTimerCapabilities.argtypes = [ OCI_ControllerHandle, ctypes.POINTER(OCI_TimerCapabilities), ] OCI_GetTimerCapabilities.restype = OCI_ErrorCode OCI_GetTimerCapabilities.errcheck = errcheck_init OCI_GetTimerValue = _oci.OCI_GetTimerValue OCI_GetTimerValue.argtypes = [ OCI_ControllerHandle, ctypes.POINTER(OCI_Time), ] OCI_GetTimerValue.restype = OCI_ErrorCode OCI_GetTimerValue.errcheck = errcheck_oper # OCI CAN # OCI CAN - CAN-FD OCI_CANFDRxMode = ctypes.c_uint32 OCI_CANFDRXMODE_CANFD_FRAMES_IGNORED = OCI_CANFDRxMode(1) OCI_CANFDRXMODE_CANFD_FRAMES_USING_CAN_MESSAGE = OCI_CANFDRxMode(2) OCI_CANFDRXMODE_CANFD_FRAMES_USING_CANFD_MESSAGE = OCI_CANFDRxMode(4) OCI_CANFDRXMODE_CANFD_FRAMES_USING_CANFD_MESSAGE_PADDING = OCI_CANFDRxMode(8) OCI_CANRxMode = ctypes.c_uint32 OCI_CAN_RXMODE_CAN_FRAMES_IGNORED = OCI_CANRxMode(1) OCI_CAN_RXMODE_CAN_FRAMES_USING_CAN_MESSAGE = OCI_CANRxMode(2) OCI_CANFDTxConfig = ctypes.c_uint32 OCI_CANFDTX_USE_CAN_FRAMES_ONLY = OCI_CANFDTxConfig(1) OCI_CANFDTX_USE_CANFD_FRAMES_ONLY = OCI_CANFDTxConfig(2) OCI_CANFDTX_USE_CAN_AND_CANFD_FRAMES = OCI_CANFDTxConfig(4) class OCI_CANFDRxConfig(ctypes.Structure): _fields_ = [ ("canRxMode", OCI_CANRxMode), ("canFdRxMode", OCI_CANFDRxMode), ] class OCI_CANFDConfiguration(ctypes.Structure): _fields_ = [ ("dataBitRate", ctypes.c_uint32), ("dataSamplePoint", ctypes.c_uint32), ("dataBTL_Cycles", ctypes.c_uint32), ("dataSJW", ctypes.c_uint32), ("flags", ctypes.c_uint32), ("txSecondarySamplePointOffset", ctypes.c_uint32), ("canFdRxConfig", OCI_CANFDRxConfig), ("canFdTxConfig", OCI_CANFDTxConfig), ("txSecondarySamplePointFilterWindow", ctypes.c_uint16), ("reserved", ctypes.c_uint16), ] class OCI_CANFDRxMessage(ctypes.Structure): _fields_ = [ ("timeStamp", OCI_Time), ("tag", ctypes.c_uint32), ("frameID", ctypes.c_uint32), ("flags", ctypes.c_uint16), ("res", ctypes.c_uint8), ("size", ctypes.c_uint8), ("res1", ctypes.c_uint8 * 4), ("data", ctypes.c_uint8 * 64), ] class OCI_CANFDTxMessage(ctypes.Structure): _fields_ = [ ("frameID", ctypes.c_uint32), ("flags", ctypes.c_uint16), ("res", ctypes.c_uint8), ("size", ctypes.c_uint8), ("data", ctypes.c_uint8 * 64), ] # OCI CAN - Initialization OCI_CAN_THREE_SAMPLES_PER_BIT = 2 OCI_CAN_SINGLE_SYNC_EDGE = 1 OCI_CAN_MEDIA_HIGH_SPEED = 1 OCI_CAN_STATE_ACTIVE = 0x00000001 OCI_CAN_STATE_PASSIVE = 0x00000002 OCI_CAN_STATE_ERRLIMIT = 0x00000004 OCI_CAN_STATE_BUSOFF = 0x00000008 OCI_CANBusParticipationMode = ctypes.c_uint32 OCI_BUSMODE_PASSIVE = OCI_CANBusParticipationMode(1) OCI_BUSMODE_ACTIVE = OCI_CANBusParticipationMode(2) OCI_CANBusTransmissionPolicies = ctypes.c_uint32 OCI_CANTX_UNDEFINED = OCI_CANBusTransmissionPolicies(0) OCI_CANTX_DONTCARE = OCI_CANBusTransmissionPolicies(0) OCI_CANTX_FIFO = OCI_CANBusTransmissionPolicies(1) OCI_CANTX_BESTEFFORT = OCI_CANBusTransmissionPolicies(2) class OCI_CANConfiguration(ctypes.Structure): _fields_ = [ ("baudrate", ctypes.c_uint32), ("samplePoint", ctypes.c_uint32), ("samplesPerBit", ctypes.c_uint32), ("BTL_Cycles", ctypes.c_uint32), ("SJW", ctypes.c_uint32), ("syncEdge", ctypes.c_uint32), ("physicalMedia", ctypes.c_uint32), ("selfReceptionMode", OCI_SelfReceptionMode), ("busParticipationMode", OCI_CANBusParticipationMode), ("canFDEnabled", ctypes.c_uint32), ("canFDConfig", OCI_CANFDConfiguration), ("canTxPolicy", OCI_CANBusTransmissionPolicies), ] class OCI_CANControllerProperties(ctypes.Structure): _fields_ = [ ("mode", OCI_ControllerPropertiesMode), ] class OCI_CANControllerCapabilities(ctypes.Structure): _fields_ = [ ("samplesPerBit", ctypes.c_uint32), ("syncEdge", ctypes.c_uint32), ("physicalMedia", ctypes.c_uint32), ("reserved", ctypes.c_uint32), ("busEvents", ctypes.c_uint32), ("errorFrames", ctypes.c_uint32), ("messageFlags", ctypes.c_uint32), ("canFDSupport", ctypes.c_uint32), ("canFDMaxDataSize", ctypes.c_uint32), ("canFDMaxQualifiedDataRate", ctypes.c_uint32), ("canFDMaxDataRate", ctypes.c_uint32), ("canFDRxConfig_CANMode", ctypes.c_uint32), ("canFDRxConfig_CANFDMode", ctypes.c_uint32), ("canFDTxConfig_Mode", ctypes.c_uint32), ("canBusParticipationMode", ctypes.c_uint32), ("canTxPolicies", ctypes.c_uint32), ] class OCI_CANControllerStatus(ctypes.Structure): _fields_ = [ ("reserved", ctypes.c_uint32), ("stateCode", ctypes.c_uint32), ] OCI_CreateCANControllerNoSearch = _oci.OCI_CreateCANControllerNoSearch OCI_CreateCANControllerNoSearch.argtypes = [ ctypes.c_char_p, ctypes.POINTER(BOA_Version), ctypes.POINTER(CSI_Tree), ctypes.POINTER(OCI_ControllerHandle), ] OCI_CreateCANControllerNoSearch.restype = OCI_ErrorCode OCI_CreateCANControllerNoSearch.errcheck = errcheck_init OCI_OpenCANController = _oci.OCI_OpenCANController OCI_OpenCANController.argtypes = [ OCI_ControllerHandle, ctypes.POINTER(OCI_CANConfiguration), ctypes.POINTER(OCI_CANControllerProperties), ] OCI_OpenCANController.restype = OCI_ErrorCode # no .errcheck, since we tolerate OCI_WARN_PARAM_ADAPTED warning # OCI_OpenCANController.errcheck = errcheck_init OCI_CloseCANController = _oci.OCI_CloseCANController OCI_CloseCANController.argtypes = [OCI_ControllerHandle] OCI_CloseCANController.restype = OCI_ErrorCode OCI_CloseCANController.errcheck = errcheck_oper OCI_DestroyCANController = _oci.OCI_DestroyCANController OCI_DestroyCANController.argtypes = [OCI_ControllerHandle] OCI_DestroyCANController.restype = OCI_ErrorCode OCI_DestroyCANController.errcheck = errcheck_oper OCI_AdaptCANConfiguration = _oci.OCI_AdaptCANConfiguration OCI_AdaptCANConfiguration.argtypes = [ OCI_ControllerHandle, ctypes.POINTER(OCI_CANConfiguration), ] OCI_AdaptCANConfiguration.restype = OCI_ErrorCode OCI_AdaptCANConfiguration.errcheck = errcheck_oper OCI_GetCANControllerCapabilities = _oci.OCI_GetCANControllerCapabilities OCI_GetCANControllerCapabilities.argtypes = [ OCI_ControllerHandle, ctypes.POINTER(OCI_CANControllerCapabilities), ] OCI_GetCANControllerCapabilities.restype = OCI_ErrorCode OCI_GetCANControllerCapabilities.errcheck = errcheck_init OCI_GetCANControllerStatus = _oci.OCI_GetCANControllerStatus OCI_GetCANControllerStatus.argtypes = [ OCI_ControllerHandle, ctypes.POINTER(OCI_CANControllerStatus), ] OCI_GetCANControllerStatus.restype = OCI_ErrorCode OCI_GetCANControllerStatus.errcheck = errcheck_oper # OCI CAN - Filter class OCI_CANRxFilter(ctypes.Structure): _fields_ = [ ("frameIDValue", ctypes.c_uint32), ("frameIDMask", ctypes.c_uint32), ("tag", ctypes.c_uint32), ] class OCI_CANRxFilterEx(ctypes.Structure): _fields_ = [ ("frameIDValue", ctypes.c_uint32), ("frameIDMask", ctypes.c_uint32), ("tag", ctypes.c_uint32), ("flagsValue", ctypes.c_uint16), ("flagsMask", ctypes.c_uint16), ] OCI_AddCANFrameFilterEx = _oci.OCI_AddCANFrameFilterEx OCI_AddCANFrameFilterEx.argtypes = [ OCI_QueueHandle, ctypes.POINTER(ctypes.POINTER(OCI_CANRxFilterEx)), ctypes.c_uint32, ] OCI_AddCANFrameFilterEx.restype = OCI_ErrorCode OCI_AddCANFrameFilterEx.errcheck = errcheck_oper OCI_RemoveCANFrameFilterEx = _oci.OCI_RemoveCANFrameFilterEx OCI_RemoveCANFrameFilterEx.argtypes = [ OCI_QueueHandle, ctypes.POINTER(ctypes.POINTER(OCI_CANRxFilterEx)), ctypes.c_uint32, ] OCI_RemoveCANFrameFilterEx.restype = OCI_ErrorCode OCI_RemoveCANFrameFilterEx.errcheck = errcheck_oper # OCI CAN - Messages OCI_CAN_MSG_FLAG_EXTENDED = 0x1 OCI_CAN_MSG_FLAG_REMOTE_FRAME = 0x2 OCI_CAN_MSG_FLAG_SELFRECEPTION = 0x4 OCI_CAN_MSG_FLAG_FD_DATA_BIT_RATE = 0x8 OCI_CAN_MSG_FLAG_FD_TRUNC_AND_PAD = 0x10 OCI_CAN_MSG_FLAG_FD_ERROR_PASSIVE = 0x20 OCI_CAN_MSG_FLAG_FD_DATA = 0x40 OCI_CANMessageDataType = ctypes.c_uint32 OCI_CAN_RX_MESSAGE = OCI_CANMessageDataType(1) OCI_CAN_TX_MESSAGE = OCI_CANMessageDataType(2) OCI_CAN_ERROR_FRAME = OCI_CANMessageDataType(3) OCI_CAN_BUS_EVENT = OCI_CANMessageDataType(4) OCI_CAN_INTERNAL_ERROR_EVENT = OCI_CANMessageDataType(5) OCI_CAN_QUEUE_EVENT = OCI_CANMessageDataType(6) OCI_CAN_TIMER_EVENT = OCI_CANMessageDataType(7) OCI_CANFDRX_MESSAGE = OCI_CANMessageDataType(8) OCI_CANFDTX_MESSAGE = OCI_CANMessageDataType(9) class OCI_CANTxMessage(ctypes.Structure): _fields_ = [ ("frameID", ctypes.c_uint32), ("flags", ctypes.c_uint16), ("res", ctypes.c_uint8), ("dlc", ctypes.c_uint8), ("data", ctypes.c_uint8 * 8), ] class OCI_CANRxMessage(ctypes.Structure): _fields_ = [ ("timeStamp", OCI_Time), ("tag", ctypes.c_uint32), ("frameID", ctypes.c_uint32), ("flags", ctypes.c_uint16), ("res", ctypes.c_uint8), ("dlc", ctypes.c_uint8), ("res1", ctypes.c_uint8 * 4), ("data", ctypes.c_uint8 * 8), ] class OCI_CANErrorFrameMessage(ctypes.Structure): _fields_ = [ ("timeStamp", OCI_Time), ("tag", ctypes.c_uint32), ("frameID", ctypes.c_uint32), ("flags", ctypes.c_uint16), ("res", ctypes.c_uint8), ("dlc", ctypes.c_uint8), ("type", ctypes.c_uint32), ("destination", ctypes.c_uint32), ] class OCI_CANEventMessage(ctypes.Structure): _fields_ = [ ("timeStamp", OCI_Time), ("tag", ctypes.c_uint32), ("eventCode", ctypes.c_uint32), ("destination", ctypes.c_uint32), ] class OCI_CANMessageData(ctypes.Union): _fields_ = [ ("rxMessage", OCI_CANRxMessage), ("txMessage", OCI_CANTxMessage), ("errorFrameMessage", OCI_CANErrorFrameMessage), ("canEventMessage", OCI_CANEventMessage), ("internalErrorEventMessage", OCI_InternalErrorEventMessage), ("timerEventMessage", OCI_TimerEventMessage), ("queueEventMessage", OCI_QueueEventMessage), ] class OCI_CANMessageDataEx(ctypes.Union): _fields_ = [ ("rxMessage", OCI_CANRxMessage), ("txMessage", OCI_CANTxMessage), ("errorFrameMessage", OCI_CANErrorFrameMessage), ("canEventMessage", OCI_CANEventMessage), ("internalErrorEventMessage", OCI_InternalErrorEventMessage), ("timerEventMessage", OCI_TimerEventMessage), ("queueEventMessage", OCI_QueueEventMessage), ("canFDRxMessage", OCI_CANFDRxMessage), ("canFDTxMessage", OCI_CANFDTxMessage), ] class OCI_CANMessage(ctypes.Structure): _fields_ = [ ("type", OCI_CANMessageDataType), ("reserved", ctypes.c_uint32), ("data", OCI_CANMessageData), ] class OCI_CANMessageEx(ctypes.Structure): _fields_ = [ ("type", OCI_CANMessageDataType), ("reserved", ctypes.c_uint32), ("data", OCI_CANMessageDataEx), ] # OCI CAN - Queues OCI_CANRxCallbackFunctionSingleMsg = ctypes.CFUNCTYPE( None, ctypes.c_void_p, ctypes.POINTER(OCI_CANMessage) ) OCI_CANRxCallbackFunctionSingleMsgEx = ctypes.CFUNCTYPE( None, ctypes.c_void_p, ctypes.POINTER(OCI_CANMessageEx) ) class OCI_CANRxCallbackSingleMsg(ctypes.Structure): class _U(ctypes.Union): _fields_ = [ ("function", OCI_CANRxCallbackFunctionSingleMsg), ("functionEx", OCI_CANRxCallbackFunctionSingleMsgEx), ] _anonymous_ = ("u",) _fields_ = [ ("u", _U), ("userData", ctypes.c_void_p), ] class OCI_CANRxQueueConfiguration(ctypes.Structure): _fields_ = [ ("onFrame", OCI_CANRxCallbackSingleMsg), ("onEvent", OCI_CANRxCallbackSingleMsg), ("selfReceptionMode", OCI_SelfReceptionMode), ] class OCI_CANTxQueueConfiguration(ctypes.Structure): _fields_ = [ ("reserved", ctypes.c_uint32), ] OCI_CreateCANRxQueue = _oci.OCI_CreateCANRxQueue OCI_CreateCANRxQueue.argtypes = [ OCI_ControllerHandle, ctypes.POINTER(OCI_CANRxQueueConfiguration), ctypes.POINTER(OCI_QueueHandle), ] OCI_CreateCANRxQueue.restype = OCI_ErrorCode OCI_CreateCANRxQueue.errcheck = errcheck_init OCI_DestroyCANRxQueue = _oci.OCI_DestroyCANRxQueue OCI_DestroyCANRxQueue.argtypes = [OCI_QueueHandle] OCI_DestroyCANRxQueue.restype = OCI_ErrorCode OCI_DestroyCANRxQueue.errcheck = errcheck_oper OCI_CreateCANTxQueue = _oci.OCI_CreateCANTxQueue OCI_CreateCANTxQueue.argtypes = [ OCI_ControllerHandle, ctypes.POINTER(OCI_CANTxQueueConfiguration), ctypes.POINTER(OCI_QueueHandle), ] OCI_CreateCANTxQueue.restype = OCI_ErrorCode OCI_CreateCANTxQueue.errcheck = errcheck_init OCI_DestroyCANTxQueue = _oci.OCI_DestroyCANTxQueue OCI_DestroyCANTxQueue.argtypes = [OCI_QueueHandle] OCI_DestroyCANTxQueue.restype = OCI_ErrorCode OCI_DestroyCANTxQueue.errcheck = errcheck_oper OCI_WriteCANDataEx = _oci.OCI_WriteCANDataEx OCI_WriteCANDataEx.argtypes = [ OCI_QueueHandle, OCI_Time, ctypes.POINTER(ctypes.POINTER(OCI_CANMessageEx)), ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32), ] OCI_WriteCANDataEx.restype = OCI_ErrorCode OCI_WriteCANDataEx.errcheck = errcheck_oper OCI_ReadCANDataEx = _oci.OCI_ReadCANDataEx OCI_ReadCANDataEx.argtypes = [ OCI_QueueHandle, OCI_Time, ctypes.POINTER(ctypes.POINTER(OCI_CANMessageEx)), ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32), ] OCI_ReadCANDataEx.restype = OCI_ErrorCode OCI_ReadCANDataEx.errcheck = errcheck_oper python-can-4.5.0/can/interfaces/gs_usb.py000066400000000000000000000137401472200326600203270ustar00rootroot00000000000000import logging from typing import Optional, Tuple import usb from gs_usb.constants import CAN_EFF_FLAG, CAN_ERR_FLAG, CAN_MAX_DLC, CAN_RTR_FLAG from gs_usb.gs_usb import GsUsb from gs_usb.gs_usb_frame import GS_USB_NONE_ECHO_ID, GsUsbFrame import can from ..exceptions import CanInitializationError, CanOperationError logger = logging.getLogger(__name__) class GsUsbBus(can.BusABC): def __init__( self, channel, bitrate: int = 500_000, index=None, bus=None, address=None, can_filters=None, **kwargs, ): """ :param channel: usb device name :param index: device number if using automatic scan, starting from 0. If specified, bus/address shall not be provided. :param bus: number of the bus that the device is connected to :param address: address of the device on the bus it is connected to :param can_filters: not supported :param bitrate: CAN network bandwidth (bits/s) """ self._is_shutdown = False if (index is not None) and ((bus or address) is not None): raise CanInitializationError( "index and bus/address cannot be used simultaneously" ) if index is None and address is None and bus is None: index = channel self._index = None if index is not None: devs = GsUsb.scan() if len(devs) <= index: raise CanInitializationError( f"Cannot find device {index}. Devices found: {len(devs)}" ) gs_usb = devs[index] self._index = index else: gs_usb = GsUsb.find(bus=bus, address=address) if not gs_usb: raise CanInitializationError(f"Cannot find device {channel}") self.gs_usb = gs_usb self.channel_info = channel self._can_protocol = can.CanProtocol.CAN_20 bit_timing = can.BitTiming.from_sample_point( f_clock=self.gs_usb.device_capability.fclk_can, bitrate=bitrate, sample_point=87.5, ) props_seg = 1 self.gs_usb.set_timing( prop_seg=props_seg, phase_seg1=bit_timing.tseg1 - props_seg, phase_seg2=bit_timing.tseg2, sjw=bit_timing.sjw, brp=bit_timing.brp, ) self.gs_usb.start() self._bitrate = bitrate super().__init__( channel=channel, can_filters=can_filters, **kwargs, ) def send(self, msg: can.Message, timeout: Optional[float] = None): """Transmit a message to the CAN bus. :param Message msg: A message object. :param timeout: timeout is not supported. The function won't return until message is sent or exception is raised. :raises CanOperationError: if the message could not be sent """ can_id = msg.arbitration_id if msg.is_extended_id: can_id = can_id | CAN_EFF_FLAG if msg.is_remote_frame: can_id = can_id | CAN_RTR_FLAG if msg.is_error_frame: can_id = can_id | CAN_ERR_FLAG # Pad message data msg.data.extend([0x00] * (CAN_MAX_DLC - len(msg.data))) frame = GsUsbFrame() frame.can_id = can_id frame.can_dlc = msg.dlc frame.timestamp_us = 0 # timestamp frame field is only useful on receive frame.data = list(msg.data) try: self.gs_usb.send(frame) except usb.core.USBError as exc: raise CanOperationError("The message could not be sent") from exc def _recv_internal( self, timeout: Optional[float] ) -> 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` to read a message multiple times if the filters set by :meth:`~can.BusABC.set_filters` do not match and the call has not yet timed out. Never raises an error/exception. :param float timeout: seconds to wait for a message, see :meth:`~can.BusABC.send` 0 and None will be converted to minimum value 1ms. :return: 1. a message that was read or None on timeout 2. a bool that is True if message filtering has already been done and else False. In this interface it is always False since filtering is not available """ frame = GsUsbFrame() # Do not set timeout as None or zero here to avoid blocking timeout_ms = round(timeout * 1000) if timeout else 1 if not self.gs_usb.read(frame=frame, timeout_ms=timeout_ms): return None, False msg = can.Message( timestamp=frame.timestamp, arbitration_id=frame.arbitration_id, is_extended_id=frame.is_extended_id, is_remote_frame=frame.is_remote_frame, is_error_frame=frame.is_error_frame, channel=self.channel_info, dlc=frame.can_dlc, data=bytearray(frame.data)[0 : frame.can_dlc], is_rx=frame.echo_id == GS_USB_NONE_ECHO_ID, ) return msg, False def shutdown(self): if self._is_shutdown: return super().shutdown() self.gs_usb.stop() if self._index is not None: # Avoid errors on subsequent __init() by repeating the .scan() and .start() that would otherwise fail # the next time the device is opened in __init__() devs = GsUsb.scan() if self._index < len(devs): gs_usb = devs[self._index] try: gs_usb.set_bitrate(self._bitrate) gs_usb.start() gs_usb.stop() except usb.core.USBError: pass self._is_shutdown = True python-can-4.5.0/can/interfaces/ics_neovi/000077500000000000000000000000001472200326600204445ustar00rootroot00000000000000python-can-4.5.0/can/interfaces/ics_neovi/__init__.py000066400000000000000000000003331472200326600225540ustar00rootroot00000000000000""" """ __all__ = [ "ICSApiError", "ICSInitializationError", "ICSOperationError", "NeoViBus", "neovi_bus", ] from .neovi_bus import ICSApiError, ICSInitializationError, ICSOperationError, NeoViBus python-can-4.5.0/can/interfaces/ics_neovi/neovi_bus.py000066400000000000000000000427211472200326600230150ustar00rootroot00000000000000""" Intrepid Control Systems (ICS) neoVI interface module. python-ics is a Python wrapper around the API provided by Intrepid Control Systems for communicating with their neoVI range of devices. Implementation references: * https://github.com/intrepidcs/python_ics """ import functools import logging import os import tempfile from collections import Counter, defaultdict, deque from datetime import datetime from functools import partial from itertools import cycle from threading import Event from warnings import warn from can import BusABC, CanProtocol, Message from ...exceptions import ( CanError, CanInitializationError, CanOperationError, CanTimeoutError, ) logger = logging.getLogger(__name__) try: import ics except ImportError as ie: logger.warning( "You won't be able to use the ICS neoVI can backend without the " "python-ics module installed!: %s", ie, ) ics = None try: from filelock import FileLock except ImportError as ie: logger.warning( "Using ICS neoVI can backend without the " "filelock module installed may cause some issues!: %s", ie, ) class FileLock: """Dummy file lock that does not actually do anything""" def __init__(self, lock_file, timeout=-1): self._lock_file = lock_file self.timeout = timeout def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): return None # Use inter-process mutex to prevent concurrent device open. # When neoVI server is enabled, there is an issue with concurrent device open. open_lock = FileLock(os.path.join(tempfile.gettempdir(), "neovi.lock")) description_id = cycle(range(1, 0x8000)) ICS_EPOCH = datetime.fromisoformat("2007-01-01") ICS_EPOCH_DELTA = (ICS_EPOCH - datetime.fromisoformat("1970-01-01")).total_seconds() class ICSApiError(CanError): """ Indicates an error with the ICS API. """ # A critical error which affects operation or accuracy. ICS_SPY_ERR_CRITICAL = 0x10 # An error which is not understood. ICS_SPY_ERR_QUESTION = 0x20 # An important error which may be critical depending on the application ICS_SPY_ERR_EXCLAMATION = 0x30 # An error which probably does not need attention. ICS_SPY_ERR_INFORMATION = 0x40 def __init__( self, error_code: int, description_short: str, description_long: str, severity: int, restart_needed: int, ): super().__init__(f"{description_short}. {description_long}", error_code) self.description_short = description_short self.description_long = description_long self.severity = severity self.restart_needed = restart_needed == 1 def __reduce__(self): return type(self), ( self.error_code, self.description_short, self.description_long, self.severity, self.restart_needed, ) @property def error_number(self) -> int: """Deprecated. Renamed to :attr:`can.CanError.error_code`.""" warn( "ICSApiError::error_number has been replaced by ICSApiError.error_code in python-can 4.0" "and will be remove in version 5.0.", DeprecationWarning, stacklevel=2, ) return self.error_code @property def is_critical(self) -> bool: return self.severity == self.ICS_SPY_ERR_CRITICAL class ICSInitializationError(ICSApiError, CanInitializationError): pass class ICSOperationError(ICSApiError, CanOperationError): pass def check_if_bus_open(func): """ Decorator that checks if the bus is open before executing the function. If the bus is not open, it raises a CanOperationError. """ @functools.wraps(func) def wrapper(self, *args, **kwargs): """ Wrapper function that checks if the bus is open before executing the function. :raises CanOperationError: If the bus is not open. """ if self._is_shutdown: raise CanOperationError("Cannot operate on a closed bus") return func(self, *args, **kwargs) return wrapper class NeoViBus(BusABC): """ The CAN Bus implemented for the python_ics interface https://github.com/intrepidcs/python_ics """ def __init__(self, channel, can_filters=None, **kwargs): """ :param channel: The channel ids to create this bus with. Can also be a single integer, netid name or a comma separated string. :type channel: int or str or list(int) or list(str) :param list can_filters: See :meth:`can.BusABC.set_filters` for details. :param bool receive_own_messages: If transmitted messages should also be received by this bus. :param bool use_system_timestamp: Use system timestamp for can messages instead of the hardware time stamp :param str serial: Serial to connect (optional, will use the first found if not supplied) :param int bitrate: Channel bitrate in bit/s. (optional, will enable the auto bitrate feature if not supplied) :param bool fd: If CAN-FD frames should be supported. :param int data_bitrate: Which bitrate to use for data phase in CAN FD. Defaults to arbitration bitrate. :param override_library_name: Absolute path or relative path to the library including filename. :raise ImportError: If *python-ics* is not available :raise CanInitializationError: If the bus could not be set up. May or may not be a :class:`~ICSInitializationError`. """ if ics is None: raise ImportError("Please install python-ics") super().__init__( channel=channel, can_filters=can_filters, **kwargs, ) logger.info(f"CAN Filters: {can_filters}") logger.info(f"Got configuration of: {kwargs}") if "override_library_name" in kwargs: ics.override_library_name(kwargs.get("override_library_name")) if isinstance(channel, (list, tuple)): self.channels = channel elif isinstance(channel, int): self.channels = [channel] else: # Assume comma separated string of channels self.channels = [ch.strip() for ch in channel.split(",")] self.channels = [NeoViBus.channel_to_netid(ch) for ch in self.channels] type_filter = kwargs.get("type_filter") serial = kwargs.get("serial") self.dev = self._find_device(type_filter, serial) is_fd = kwargs.get("fd", False) self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20 with open_lock: ics.open_device(self.dev) try: if "bitrate" in kwargs: for channel in self.channels: ics.set_bit_rate(self.dev, kwargs.get("bitrate"), channel) if is_fd: if "data_bitrate" in kwargs: for channel in self.channels: ics.set_fd_bit_rate( self.dev, kwargs.get("data_bitrate"), channel ) except ics.RuntimeError as re: logger.error(re) err = ICSInitializationError(*ics.get_last_api_error(self.dev)) try: self.shutdown() finally: raise err self._use_system_timestamp = bool(kwargs.get("use_system_timestamp", False)) self._receive_own_messages = kwargs.get("receive_own_messages", True) self.channel_info = ( f"{self.dev.Name} {self.get_serial_number(self.dev)} CH:{self.channels}" ) logger.info(f"Using device: {self.channel_info}") self.rx_buffer = deque() self.message_receipts = defaultdict(Event) @staticmethod def channel_to_netid(channel_name_or_id): try: channel = int(channel_name_or_id) except ValueError: netid = f"NETID_{channel_name_or_id.upper()}" if hasattr(ics, netid): channel = getattr(ics, netid) else: raise ValueError( "channel must be an integer or a valid ICS channel name" ) from None return channel @staticmethod def get_serial_number(device): """Decode (if needed) and return the ICS device serial string :param device: ics device :return: ics device serial string :rtype: str """ if int("0A0000", 36) < device.SerialNumber < int("ZZZZZZ", 36): return ics.base36enc(device.SerialNumber) else: return str(device.SerialNumber) def shutdown(self): super().shutdown() ics.close_device(self.dev) @staticmethod def _detect_available_configs(): """Detect all configurations/channels that this interface could currently connect with. :rtype: Iterator[dict] :return: an iterable of dicts, each being a configuration suitable for usage in the interface's bus constructor. """ if ics is None: return [] try: devices = ics.find_devices() except Exception as e: logger.debug("Failed to detect configs: %s", e) return [] # TODO: add the channel(s) return [ {"interface": "neovi", "serial": NeoViBus.get_serial_number(device)} for device in devices ] def _find_device(self, type_filter=None, serial=None): """Returns the first matching device or raises an error. :raise CanInitializationError: If not matching device could be found """ if type_filter is not None: devices = ics.find_devices(type_filter) else: devices = ics.find_devices() for device in devices: if serial is None or self.get_serial_number(device) == str(serial): return device msg = ["No device"] if type_filter is not None: msg.append(f"with type {type_filter}") if serial is not None: msg.append(f"with serial {serial}") msg.append("found.") raise CanInitializationError(" ".join(msg)) @check_if_bus_open def _process_msg_queue(self, timeout=0.1): try: messages, errors = ics.get_messages(self.dev, False, timeout) except ics.RuntimeError: return for ics_msg in messages: channel = ics_msg.NetworkID | (ics_msg.NetworkID2 << 8) if channel not in self.channels: continue is_tx = bool(ics_msg.StatusBitField & ics.SPY_STATUS_TX_MSG) if is_tx: if bool(ics_msg.StatusBitField & ics.SPY_STATUS_GLOBAL_ERR): continue receipt_key = (ics_msg.ArbIDOrHeader, ics_msg.DescriptionID) if ics_msg.DescriptionID and receipt_key in self.message_receipts: self.message_receipts[receipt_key].set() if not self._receive_own_messages: continue self.rx_buffer.append(ics_msg) if errors: logger.warning("%d error(s) found", errors) for msg, count in Counter(ics.get_error_messages(self.dev)).items(): error = ICSApiError(*msg) if count > 1: logger.warning(f"{error} (Repeated {count} times)") else: logger.warning(error) def _get_timestamp_for_msg(self, ics_msg): if self._use_system_timestamp: # This is the system time stamp. # TimeSystem is loaded with the value received from the timeGetTime # call in the WIN32 multimedia API. # # The timeGetTime accuracy is up to 1 millisecond. See the WIN32 # API documentation for more information. # # This timestamp is useful for time comparing with other system # events or data which is not synced with the neoVI timestamp. # # Currently, TimeSystem2 is not used. return ics_msg.TimeSystem else: # This is the hardware time stamp. return ics.get_timestamp_for_msg(self.dev, ics_msg) + ICS_EPOCH_DELTA def _ics_msg_to_message(self, ics_msg): is_fd = ics_msg.Protocol == ics.SPY_PROTOCOL_CANFD message_from_ics = partial( Message, timestamp=self._get_timestamp_for_msg(ics_msg), arbitration_id=ics_msg.ArbIDOrHeader, is_extended_id=bool(ics_msg.StatusBitField & ics.SPY_STATUS_XTD_FRAME), is_remote_frame=bool(ics_msg.StatusBitField & ics.SPY_STATUS_REMOTE_FRAME), is_error_frame=bool(ics_msg.StatusBitField2 & ics.SPY_STATUS2_ERROR_FRAME), channel=ics_msg.NetworkID | (ics_msg.NetworkID2 << 8), dlc=ics_msg.NumberBytesData, is_fd=is_fd, is_rx=not bool(ics_msg.StatusBitField & ics.SPY_STATUS_TX_MSG), ) if is_fd: if ics_msg.ExtraDataPtrEnabled: data = ics_msg.ExtraDataPtr[: ics_msg.NumberBytesData] else: data = ics_msg.Data[: ics_msg.NumberBytesData] return message_from_ics( data=data, error_state_indicator=bool( ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_ESI ), bitrate_switch=bool( ics_msg.StatusBitField3 & ics.SPY_STATUS3_CANFD_BRS ), ) else: return message_from_ics( data=ics_msg.Data[: ics_msg.NumberBytesData], ) def _recv_internal(self, timeout=0.1): if not self.rx_buffer: self._process_msg_queue(timeout=timeout) try: ics_msg = self.rx_buffer.popleft() msg = self._ics_msg_to_message(ics_msg) except IndexError: return None, False return msg, False @check_if_bus_open def send(self, msg, timeout=0): """Transmit a message to the CAN bus. :param Message msg: A message object. :param float timeout: If > 0, wait up to this many seconds for message to be ACK'ed. If timeout is exceeded, an exception will be raised. None blocks indefinitely. :raises ValueError: if the message is invalid :raises can.CanTimeoutError: if sending timed out :raises CanOperationError: If the bus is closed or the message could otherwise not be sent. May or may not be a :class:`~ICSOperationError`. """ if not ics.validate_hobject(self.dev): raise CanOperationError("bus not open") # Check for valid DLC to avoid passing extra long data to the driver if msg.is_fd: if msg.dlc > 64: raise ValueError( f"DLC was {msg.dlc} but it should be <= 64 for CAN FD frames" ) elif msg.dlc > 8: raise ValueError( f"DLC was {msg.dlc} but it should be <= 8 for normal CAN frames" ) message = ics.SpyMessage() flag0 = 0 if msg.is_extended_id: flag0 |= ics.SPY_STATUS_XTD_FRAME if msg.is_remote_frame: flag0 |= ics.SPY_STATUS_REMOTE_FRAME flag3 = 0 if msg.is_fd: message.Protocol = ics.SPY_PROTOCOL_CANFD if msg.bitrate_switch: flag3 |= ics.SPY_STATUS3_CANFD_BRS if msg.error_state_indicator: flag3 |= ics.SPY_STATUS3_CANFD_ESI message.ArbIDOrHeader = msg.arbitration_id msg_data = msg.data[: msg.dlc] message.NumberBytesData = msg.dlc message.Data = tuple(msg_data[:8]) if msg.is_fd and len(msg_data) > 8: message.ExtraDataPtrEnabled = 1 message.ExtraDataPtr = tuple(msg_data) message.StatusBitField = flag0 message.StatusBitField2 = 0 message.StatusBitField3 = flag3 if msg.channel is not None: network_id = msg.channel elif len(self.channels) == 1: network_id = self.channels[0] else: raise ValueError("msg.channel must be set when using multiple channels.") message.NetworkID, message.NetworkID2 = int(network_id & 0xFF), int( (network_id >> 8) & 0xFF ) if timeout != 0: msg_desc_id = next(description_id) message.DescriptionID = msg_desc_id receipt_key = (msg.arbitration_id, msg_desc_id) self.message_receipts[receipt_key].clear() try: ics.transmit_messages(self.dev, message) except ics.RuntimeError: raise ICSOperationError(*ics.get_last_api_error(self.dev)) from None # If timeout is set, wait for ACK # This requires a notifier for the bus or # some other thread calling recv periodically if timeout != 0: got_receipt = self.message_receipts[receipt_key].wait(timeout) # We no longer need this receipt, so no point keeping it in memory del self.message_receipts[receipt_key] if not got_receipt: raise CanTimeoutError("Transmit timeout") python-can-4.5.0/can/interfaces/iscan.py000066400000000000000000000143141472200326600201400ustar00rootroot00000000000000""" Interface for isCAN from *Thorsis Technologies GmbH*, former *ifak system GmbH*. """ import ctypes import logging import time from typing import Optional, Tuple, Union from can import ( BusABC, CanError, CanInitializationError, CanInterfaceNotImplementedError, CanOperationError, CanProtocol, Message, ) logger = logging.getLogger(__name__) CanData = ctypes.c_ubyte * 8 class MessageExStruct(ctypes.Structure): _fields_ = [ ("message_id", ctypes.c_ulong), ("is_extended", ctypes.c_ubyte), ("remote_req", ctypes.c_ubyte), ("data_len", ctypes.c_ubyte), ("data", CanData), ] def check_status_initialization(result: int, function, arguments) -> int: if result > 0: raise IscanInitializationError(function, result, arguments) return result def check_status(result: int, function, arguments) -> int: if result > 0: raise IscanOperationError(function, result, arguments) return result try: iscan = ctypes.cdll.LoadLibrary("iscandrv") except OSError as e: iscan = None logger.warning("Failed to load IS-CAN driver: %s", e) else: iscan.isCAN_DeviceInitEx.argtypes = [ctypes.c_ubyte, ctypes.c_ubyte] iscan.isCAN_DeviceInitEx.errcheck = check_status_initialization iscan.isCAN_DeviceInitEx.restype = ctypes.c_ubyte iscan.isCAN_ReceiveMessageEx.errcheck = check_status iscan.isCAN_ReceiveMessageEx.restype = ctypes.c_ubyte iscan.isCAN_TransmitMessageEx.errcheck = check_status iscan.isCAN_TransmitMessageEx.restype = ctypes.c_ubyte iscan.isCAN_CloseDevice.errcheck = check_status iscan.isCAN_CloseDevice.restype = ctypes.c_ubyte class IscanBus(BusABC): """isCAN interface""" BAUDRATES = { 5000: 0, 10000: 1, 20000: 2, 50000: 3, 100000: 4, 125000: 5, 250000: 6, 500000: 7, 800000: 8, 1000000: 9, } def __init__( self, channel: Union[str, int], bitrate: int = 500000, poll_interval: float = 0.01, **kwargs, ) -> None: """ :param channel: Device number :param bitrate: Bitrate in bits/s :param poll_interval: Poll interval in seconds when reading messages """ if iscan is None: raise CanInterfaceNotImplementedError("Could not load isCAN driver") self.channel = ctypes.c_ubyte(int(channel)) self.channel_info = f"IS-CAN: {self.channel}" self._can_protocol = CanProtocol.CAN_20 if bitrate not in self.BAUDRATES: raise ValueError(f"Invalid bitrate, choose one of {set(self.BAUDRATES)}") self.poll_interval = poll_interval iscan.isCAN_DeviceInitEx(self.channel, self.BAUDRATES[bitrate]) super().__init__( channel=channel, bitrate=bitrate, poll_interval=poll_interval, **kwargs, ) def _recv_internal( self, timeout: Optional[float] ) -> Tuple[Optional[Message], bool]: raw_msg = MessageExStruct() end_time = time.time() + timeout if timeout is not None else None while True: try: iscan.isCAN_ReceiveMessageEx(self.channel, ctypes.byref(raw_msg)) except IscanError as e: if e.error_code != 8: # "No message received" # An error occurred raise if end_time is not None and time.time() > end_time: # No message within timeout return None, False # Sleep a short time to avoid hammering time.sleep(self.poll_interval) else: # A message was received break msg = Message( arbitration_id=raw_msg.message_id, is_extended_id=bool(raw_msg.is_extended), timestamp=time.time(), # Better than nothing... is_remote_frame=bool(raw_msg.remote_req), dlc=raw_msg.data_len, data=raw_msg.data[: raw_msg.data_len], channel=self.channel.value, ) return msg, False def send(self, msg: Message, timeout: Optional[float] = None) -> None: raw_msg = MessageExStruct( msg.arbitration_id, bool(msg.is_extended_id), bool(msg.is_remote_frame), msg.dlc, CanData(*msg.data), ) iscan.isCAN_TransmitMessageEx(self.channel, ctypes.byref(raw_msg)) def shutdown(self) -> None: super().shutdown() iscan.isCAN_CloseDevice(self.channel) class IscanError(CanError): ERROR_CODES = { 0: "Success", 1: "No access to device", 2: "Device with ID not found", 3: "Driver operation failed", 4: "Invalid parameter", 5: "Operation allowed only in online state", 6: "Device timeout", 7: "Device is transmitting a message", 8: "No message received", 9: "Thread not started", 10: "Thread already started", 11: "Buffer overrun", 12: "Device not initialized", 15: "Found the device, but it is being used by another process", 16: "Bus error", 17: "Bus off", 18: "Error passive", 19: "Data overrun", 20: "Error warning", 30: "Send error", 31: "Transmission not acknowledged on bus", 32: "Error critical bus", 35: "Callbackthread is blocked, stopping thread failed", 40: "Need a licence number under NT4", } def __init__(self, function, error_code: int, arguments) -> None: try: description = ": " + self.ERROR_CODES[error_code] except KeyError: description = "" super().__init__( f"Function {function.__name__} failed{description}", error_code=error_code, ) #: Status code self.error_code = error_code #: Function that failed self.function = function #: Arguments passed to function self.arguments = arguments class IscanOperationError(IscanError, CanOperationError): pass class IscanInitializationError(IscanError, CanInitializationError): pass python-can-4.5.0/can/interfaces/ixxat/000077500000000000000000000000001472200326600176235ustar00rootroot00000000000000python-can-4.5.0/can/interfaces/ixxat/__init__.py000066400000000000000000000007741472200326600217440ustar00rootroot00000000000000""" Ctypes wrapper module for IXXAT Virtual CAN Interface V4 on win32 systems Copyright (C) 2016-2021 Giuseppe Corbelli """ __all__ = [ "IXXATBus", "canlib", "canlib_vcinpl", "canlib_vcinpl2", "constants", "exceptions", "get_ixxat_hwids", "structures", ] from can.interfaces.ixxat.canlib import IXXATBus # import this and not the one from vcinpl2 for backward compatibility from can.interfaces.ixxat.canlib_vcinpl import get_ixxat_hwids python-can-4.5.0/can/interfaces/ixxat/canlib.py000066400000000000000000000133241472200326600214300ustar00rootroot00000000000000from typing import Callable, List, Optional, Sequence, Union import can.interfaces.ixxat.canlib_vcinpl as vcinpl import can.interfaces.ixxat.canlib_vcinpl2 as vcinpl2 from can import ( BusABC, BusState, CyclicSendTaskABC, Message, ) from can.typechecking import AutoDetectedConfig class IXXATBus(BusABC): """The CAN Bus implemented for the IXXAT interface. Based on the C implementation of IXXAT, two different dlls are provided by IXXAT, one to work with CAN, the other with CAN-FD. This class only delegates to related implementation (in calib_vcinpl or canlib_vcinpl2) class depending on fd user option. """ def __init__( self, channel: int, can_filters=None, receive_own_messages: bool = False, unique_hardware_id: Optional[int] = None, extended: bool = True, fd: bool = False, rx_fifo_size: Optional[int] = None, tx_fifo_size: Optional[int] = None, bitrate: int = 500000, data_bitrate: int = 2000000, sjw_abr: Optional[int] = None, tseg1_abr: Optional[int] = None, tseg2_abr: Optional[int] = None, sjw_dbr: Optional[int] = None, tseg1_dbr: Optional[int] = None, tseg2_dbr: Optional[int] = None, ssp_dbr: Optional[int] = None, **kwargs, ): """ :param channel: The Channel id to create this bus with. :param can_filters: See :meth:`can.BusABC.set_filters`. :param receive_own_messages: Enable self-reception of sent messages. :param unique_hardware_id: UniqueHardwareId to connect (optional, will use the first found if not supplied) :param extended: Default True, enables the capability to use extended IDs. :param fd: Default False, enables CAN-FD usage. :param rx_fifo_size: Receive fifo size (default 1024 for fd, else 16) :param tx_fifo_size: Transmit fifo size (default 128 for fd, else 16) :param bitrate: Channel bitrate in bit/s :param data_bitrate: Channel bitrate in bit/s (only in CAN-Fd if baudrate switch enabled). :param sjw_abr: Bus timing value sample jump width (arbitration). Only takes effect with fd enabled. :param tseg1_abr: Bus timing value tseg1 (arbitration). Only takes effect with fd enabled. :param tseg2_abr: Bus timing value tseg2 (arbitration). Only takes effect with fd enabled. :param sjw_dbr: Bus timing value sample jump width (data). Only takes effect with fd and baudrate switch enabled. :param tseg1_dbr: Bus timing value tseg1 (data). Only takes effect with fd and bitrate switch enabled. :param tseg2_dbr: Bus timing value tseg2 (data). Only takes effect with fd and bitrate switch enabled. :param ssp_dbr: Secondary sample point (data). Only takes effect with fd and bitrate switch enabled. """ if fd: if rx_fifo_size is None: rx_fifo_size = 1024 if tx_fifo_size is None: tx_fifo_size = 128 self.bus = vcinpl2.IXXATBus( channel=channel, can_filters=can_filters, receive_own_messages=receive_own_messages, unique_hardware_id=unique_hardware_id, extended=extended, rx_fifo_size=rx_fifo_size, tx_fifo_size=tx_fifo_size, bitrate=bitrate, data_bitrate=data_bitrate, sjw_abr=sjw_abr, tseg1_abr=tseg1_abr, tseg2_abr=tseg2_abr, sjw_dbr=sjw_dbr, tseg1_dbr=tseg1_dbr, tseg2_dbr=tseg2_dbr, ssp_dbr=ssp_dbr, **kwargs, ) else: if rx_fifo_size is None: rx_fifo_size = 16 if tx_fifo_size is None: tx_fifo_size = 16 self.bus = vcinpl.IXXATBus( channel=channel, can_filters=can_filters, receive_own_messages=receive_own_messages, unique_hardware_id=unique_hardware_id, extended=extended, rx_fifo_size=rx_fifo_size, tx_fifo_size=tx_fifo_size, bitrate=bitrate, **kwargs, ) super().__init__(channel=channel, **kwargs) self._can_protocol = self.bus.protocol def flush_tx_buffer(self): """Flushes the transmit buffer on the IXXAT""" return self.bus.flush_tx_buffer() def _recv_internal(self, timeout): """Read a message from IXXAT device.""" return self.bus._recv_internal(timeout) def send(self, msg: Message, timeout: Optional[float] = None) -> None: return self.bus.send(msg, timeout) def _send_periodic_internal( self, msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> CyclicSendTaskABC: return self.bus._send_periodic_internal( msgs, period, duration, autostart, modifier_callback ) def shutdown(self) -> None: super().shutdown() self.bus.shutdown() @property def state(self) -> BusState: """ Return the current state of the hardware """ return self.bus.state @staticmethod def _detect_available_configs() -> List[AutoDetectedConfig]: return vcinpl._detect_available_configs() python-can-4.5.0/can/interfaces/ixxat/canlib_vcinpl.py000066400000000000000000001123051472200326600230020ustar00rootroot00000000000000""" Ctypes wrapper module for IXXAT Virtual CAN Interface V4 on win32 systems TODO: We could implement this interface such that setting other filters could work when the initial filters were set to zero using the software fallback. Or could the software filters even be changed after the connection was opened? We need to document that bahaviour! See also the NICAN interface. """ import ctypes import functools import logging import sys import warnings from typing import Callable, List, Optional, Sequence, Tuple, Union from can import ( BusABC, BusState, CanProtocol, CyclicSendTaskABC, LimitedDurationCyclicSendTaskABC, Message, RestartableCyclicTaskABC, ) from can.ctypesutil import HANDLE, PHANDLE, CLibrary from can.ctypesutil import HRESULT as ctypes_HRESULT from can.exceptions import CanInitializationError, CanInterfaceNotImplementedError from can.typechecking import AutoDetectedConfig from can.util import deprecated_args_alias from . import constants, structures from .exceptions import * __all__ = [ "VCITimeout", "VCIError", "VCIBusOffError", "VCIDeviceNotFoundError", "IXXATBus", "vciFormatError", ] log = logging.getLogger("can.ixxat") # Hack to have vciFormatError as a free function, see below vciFormatError = None # main ctypes instance _canlib = None # TODO: Use ECI driver for linux if sys.platform == "win32" or sys.platform == "cygwin": try: _canlib = CLibrary("vcinpl.dll") except Exception as e: log.warning("Cannot load IXXAT vcinpl library: %s", e) else: # Will not work on other systems, but have it importable anyway for # tests/sphinx log.warning("IXXAT VCI library does not work on %s platform", sys.platform) def __vciFormatErrorExtended( library_instance: CLibrary, function: Callable, vret: int, args: Tuple ): """Format a VCI error and attach failed function, decoded HRESULT and arguments :param CLibrary library_instance: Mapped instance of IXXAT vcinpl library :param callable function: Failed function :param HRESULT vret: HRESULT returned by vcinpl call :param args: Arbitrary arguments tuple :return: Formatted string """ # TODO: make sure we don't generate another exception return ( f"{__vciFormatError(library_instance, function, vret)} - arguments were {args}" ) def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int): """Format a VCI error and attach failed function and decoded HRESULT :param CLibrary library_instance: Mapped instance of IXXAT vcinpl library :param callable function: Failed function :param HRESULT vret: HRESULT returned by vcinpl call :return: Formatted string """ buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN) ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN) library_instance.vciFormatError(vret, buf, constants.VCI_MAX_ERRSTRLEN) return "function {} failed ({})".format( function._name, buf.value.decode("utf-8", "replace") ) def __check_status(result, function, args): """ Check the result of a vcinpl function call and raise appropriate exception in case of an error. Used as errcheck function when mapping C functions with ctypes. :param result: Function call numeric result :param callable function: Called function :param args: Arbitrary arguments tuple :raise: :class:VCITimeout :class:VCIRxQueueEmptyError :class:StopIteration :class:VCIError """ if isinstance(result, int): # Real return value is an unsigned long, the following line converts the number to unsigned result = ctypes.c_ulong(result).value if result == constants.VCI_E_TIMEOUT: raise VCITimeout(f"Function {function._name} timed out") elif result == constants.VCI_E_RXQUEUE_EMPTY: raise VCIRxQueueEmptyError() elif result == constants.VCI_E_NO_MORE_ITEMS: raise StopIteration() elif result == constants.VCI_E_ACCESSDENIED: pass # not a real error, might happen if another program has initialized the bus elif result != constants.VCI_OK: raise VCIError(vciFormatError(function, result)) return result try: # Map all required symbols and initialize library --------------------------- # HRESULT VCIAPI vciInitialize ( void ); _canlib.map_symbol("vciInitialize", ctypes.c_long, (), __check_status) # void VCIAPI vciFormatError (HRESULT hrError, PCHAR pszText, UINT32 dwsize); _canlib.map_symbol( "vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) ) # Hack to have vciFormatError as a free function vciFormatError = functools.partial(__vciFormatError, _canlib) # HRESULT VCIAPI vciEnumDeviceOpen( OUT PHANDLE hEnum ); _canlib.map_symbol("vciEnumDeviceOpen", ctypes.c_long, (PHANDLE,), __check_status) # HRESULT VCIAPI vciEnumDeviceClose ( IN HANDLE hEnum ); _canlib.map_symbol("vciEnumDeviceClose", ctypes.c_long, (HANDLE,), __check_status) # HRESULT VCIAPI vciEnumDeviceNext( IN HANDLE hEnum, OUT PVCIDEVICEINFO pInfo ); _canlib.map_symbol( "vciEnumDeviceNext", ctypes.c_long, (HANDLE, structures.PVCIDEVICEINFO), __check_status, ) # HRESULT VCIAPI vciDeviceOpen( IN REFVCIID rVciid, OUT PHANDLE phDevice ); _canlib.map_symbol( "vciDeviceOpen", ctypes.c_long, (structures.PVCIID, PHANDLE), __check_status ) # HRESULT vciDeviceClose( HANDLE hDevice ) _canlib.map_symbol("vciDeviceClose", ctypes.c_long, (HANDLE,), __check_status) # HRESULT VCIAPI canChannelOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, IN BOOL fExclusive, OUT PHANDLE phCanChn ); _canlib.map_symbol( "canChannelOpen", ctypes.c_long, (HANDLE, ctypes.c_uint32, ctypes.c_long, PHANDLE), __check_status, ) # EXTERN_C HRESULT VCIAPI canChannelInitialize( IN HANDLE hCanChn, IN UINT16 wRxFifoSize, IN UINT16 wRxThreshold, IN UINT16 wTxFifoSize, IN UINT16 wTxThreshold ); _canlib.map_symbol( "canChannelInitialize", ctypes.c_long, (HANDLE, ctypes.c_uint16, ctypes.c_uint16, ctypes.c_uint16, ctypes.c_uint16), __check_status, ) # EXTERN_C HRESULT VCIAPI canChannelActivate( IN HANDLE hCanChn, IN BOOL fEnable ); _canlib.map_symbol( "canChannelActivate", ctypes.c_long, (HANDLE, ctypes.c_long), __check_status ) # HRESULT canChannelClose( HANDLE hChannel ) _canlib.map_symbol("canChannelClose", ctypes.c_long, (HANDLE,), __check_status) # EXTERN_C HRESULT VCIAPI canChannelReadMessage( IN HANDLE hCanChn, IN UINT32 dwMsTimeout, OUT PCANMSG pCanMsg ); _canlib.map_symbol( "canChannelReadMessage", ctypes.c_long, (HANDLE, ctypes.c_uint32, structures.PCANMSG), __check_status, ) # HRESULT canChannelPeekMessage(HANDLE hChannel,PCANMSG pCanMsg ); _canlib.map_symbol( "canChannelPeekMessage", ctypes.c_long, (HANDLE, structures.PCANMSG), __check_status, ) # HRESULT canChannelWaitTxEvent (HANDLE hChannel UINT32 dwMsTimeout ); _canlib.map_symbol( "canChannelWaitTxEvent", ctypes.c_long, (HANDLE, ctypes.c_uint32), __check_status, ) # HRESULT canChannelWaitRxEvent (HANDLE hChannel, UINT32 dwMsTimeout ); _canlib.map_symbol( "canChannelWaitRxEvent", ctypes.c_long, (HANDLE, ctypes.c_uint32), __check_status, ) # HRESULT canChannelPostMessage (HANDLE hChannel, PCANMSG pCanMsg ); _canlib.map_symbol( "canChannelPostMessage", ctypes.c_long, (HANDLE, structures.PCANMSG), __check_status, ) # HRESULT canChannelSendMessage (HANDLE hChannel, UINT32 dwMsTimeout, PCANMSG pCanMsg ); _canlib.map_symbol( "canChannelSendMessage", ctypes.c_long, (HANDLE, ctypes.c_uint32, structures.PCANMSG), __check_status, ) # HRESULT canChannelGetStatus (HANDLE hCanChn, PCANCHANSTATUS pStatus ); _canlib.map_symbol( "canChannelGetStatus", ctypes.c_long, (HANDLE, structures.PCANCHANSTATUS), __check_status, ) # EXTERN_C HRESULT VCIAPI canControlOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, OUT PHANDLE phCanCtl ); _canlib.map_symbol( "canControlOpen", ctypes.c_long, (HANDLE, ctypes.c_uint32, PHANDLE), __check_status, ) # EXTERN_C HRESULT VCIAPI canControlInitialize( IN HANDLE hCanCtl, IN UINT8 bMode, IN UINT8 bBtr0, IN UINT8 bBtr1 ); _canlib.map_symbol( "canControlInitialize", ctypes.c_long, (HANDLE, ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint8), __check_status, ) # EXTERN_C HRESULT VCIAPI canControlClose( IN HANDLE hCanCtl ); _canlib.map_symbol("canControlClose", ctypes.c_long, (HANDLE,), __check_status) # EXTERN_C HRESULT VCIAPI canControlReset( IN HANDLE hCanCtl ); _canlib.map_symbol("canControlReset", ctypes.c_long, (HANDLE,), __check_status) # EXTERN_C HRESULT VCIAPI canControlStart( IN HANDLE hCanCtl, IN BOOL fStart ); _canlib.map_symbol( "canControlStart", ctypes.c_long, (HANDLE, ctypes.c_long), __check_status ) # EXTERN_C HRESULT VCIAPI canControlGetStatus( IN HANDLE hCanCtl, OUT PCANLINESTATUS pStatus ); _canlib.map_symbol( "canControlGetStatus", ctypes.c_long, (HANDLE, structures.PCANLINESTATUS), __check_status, ) # EXTERN_C HRESULT VCIAPI canControlGetCaps( IN HANDLE hCanCtl, OUT PCANCAPABILITIES pCanCaps ); _canlib.map_symbol( "canControlGetCaps", ctypes.c_long, (HANDLE, structures.PCANCAPABILITIES), __check_status, ) # EXTERN_C HRESULT VCIAPI canControlSetAccFilter( IN HANDLE hCanCtl, IN BOOL fExtend, IN UINT32 dwCode, IN UINT32 dwMask ); _canlib.map_symbol( "canControlSetAccFilter", ctypes.c_long, (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), __check_status, ) # EXTERN_C HRESULT canControlAddFilterIds (HANDLE hControl, BOOL fExtended, UINT32 dwCode, UINT32 dwMask); _canlib.map_symbol( "canControlAddFilterIds", ctypes.c_long, (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), __check_status, ) # EXTERN_C HRESULT canControlRemFilterIds (HANDLE hControl, BOOL fExtendend, UINT32 dwCode, UINT32 dwMask ); _canlib.map_symbol( "canControlRemFilterIds", ctypes.c_long, (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), __check_status, ) # EXTERN_C HRESULT canSchedulerOpen (HANDLE hDevice, UINT32 dwCanNo, PHANDLE phScheduler ); _canlib.map_symbol( "canSchedulerOpen", ctypes.c_long, (HANDLE, ctypes.c_uint32, PHANDLE), __check_status, ) # EXTERN_C HRESULT canSchedulerClose (HANDLE hScheduler ); _canlib.map_symbol("canSchedulerClose", ctypes.c_long, (HANDLE,), __check_status) # EXTERN_C HRESULT canSchedulerGetCaps (HANDLE hScheduler, PCANCAPABILITIES pCaps ); _canlib.map_symbol( "canSchedulerGetCaps", ctypes.c_long, (HANDLE, structures.PCANCAPABILITIES), __check_status, ) # EXTERN_C HRESULT canSchedulerActivate ( HANDLE hScheduler, BOOL fEnable ); _canlib.map_symbol( "canSchedulerActivate", ctypes.c_long, (HANDLE, ctypes.c_int), __check_status ) # EXTERN_C HRESULT canSchedulerAddMessage (HANDLE hScheduler, PCANCYCLICTXMSG pMessage, PUINT32 pdwIndex ); _canlib.map_symbol( "canSchedulerAddMessage", ctypes.c_long, (HANDLE, structures.PCANCYCLICTXMSG, ctypes.POINTER(ctypes.c_uint32)), __check_status, ) # EXTERN_C HRESULT canSchedulerRemMessage (HANDLE hScheduler, UINT32 dwIndex ); _canlib.map_symbol( "canSchedulerRemMessage", ctypes.c_long, (HANDLE, ctypes.c_uint32), __check_status, ) # EXTERN_C HRESULT canSchedulerStartMessage (HANDLE hScheduler, UINT32 dwIndex, UINT16 dwCount ); _canlib.map_symbol( "canSchedulerStartMessage", ctypes.c_long, (HANDLE, ctypes.c_uint32, ctypes.c_uint16), __check_status, ) # EXTERN_C HRESULT canSchedulerStopMessage (HANDLE hScheduler, UINT32 dwIndex ); _canlib.map_symbol( "canSchedulerStopMessage", ctypes.c_long, (HANDLE, ctypes.c_uint32), __check_status, ) _canlib.vciInitialize() except AttributeError: # In case _canlib == None meaning we're not on win32/no lib found pass except Exception as e: log.warning("Could not initialize IXXAT VCI library: %s", e) # --------------------------------------------------------------------------- CAN_INFO_MESSAGES = { constants.CAN_INFO_START: "CAN started", constants.CAN_INFO_STOP: "CAN stopped", constants.CAN_INFO_RESET: "CAN reset", } CAN_ERROR_MESSAGES = { constants.CAN_ERROR_STUFF: "CAN bit stuff error", constants.CAN_ERROR_FORM: "CAN form error", constants.CAN_ERROR_ACK: "CAN acknowledgment error", constants.CAN_ERROR_BIT: "CAN bit error", constants.CAN_ERROR_CRC: "CAN CRC error", constants.CAN_ERROR_OTHER: "Other (unknown) CAN error", } CAN_STATUS_FLAGS = { constants.CAN_STATUS_TXPEND: "transmission pending", constants.CAN_STATUS_OVRRUN: "data overrun occurred", constants.CAN_STATUS_ERRLIM: "error warning limit exceeded", constants.CAN_STATUS_BUSOFF: "bus off", constants.CAN_STATUS_ININIT: "init mode active", constants.CAN_STATUS_BUSCERR: "bus coupling error", } # ---------------------------------------------------------------------------- class IXXATBus(BusABC): """The CAN Bus implemented for the IXXAT interface. .. warning:: This interface does implement efficient filtering of messages, but the filters have to be set in ``__init__`` using the ``can_filters`` parameter. Using :meth:`~can.BusABC.set_filters` does not work. """ CHANNEL_BITRATES = { 0: { 10000: constants.CAN_BT0_10KB, 20000: constants.CAN_BT0_20KB, 50000: constants.CAN_BT0_50KB, 100000: constants.CAN_BT0_100KB, 125000: constants.CAN_BT0_125KB, 250000: constants.CAN_BT0_250KB, 500000: constants.CAN_BT0_500KB, 666000: constants.CAN_BT0_667KB, 666666: constants.CAN_BT0_667KB, 666667: constants.CAN_BT0_667KB, 667000: constants.CAN_BT0_667KB, 800000: constants.CAN_BT0_800KB, 1000000: constants.CAN_BT0_1000KB, }, 1: { 10000: constants.CAN_BT1_10KB, 20000: constants.CAN_BT1_20KB, 50000: constants.CAN_BT1_50KB, 100000: constants.CAN_BT1_100KB, 125000: constants.CAN_BT1_125KB, 250000: constants.CAN_BT1_250KB, 500000: constants.CAN_BT1_500KB, 666000: constants.CAN_BT1_667KB, 666666: constants.CAN_BT1_667KB, 666667: constants.CAN_BT1_667KB, 667000: constants.CAN_BT1_667KB, 800000: constants.CAN_BT1_800KB, 1000000: constants.CAN_BT1_1000KB, }, } @deprecated_args_alias( deprecation_start="4.0.0", deprecation_end="5.0.0", UniqueHardwareId="unique_hardware_id", rxFifoSize="rx_fifo_size", txFifoSize="tx_fifo_size", ) def __init__( self, channel: int, can_filters=None, receive_own_messages: bool = False, unique_hardware_id: Optional[int] = None, extended: bool = True, rx_fifo_size: int = 16, tx_fifo_size: int = 16, bitrate: int = 500000, **kwargs, ): """ :param channel: The Channel id to create this bus with. :param can_filters: See :meth:`can.BusABC.set_filters`. :param receive_own_messages: Enable self-reception of sent messages. :param unique_hardware_id: unique_hardware_id to connect (optional, will use the first found if not supplied) :param extended: Default True, enables the capability to use extended IDs. :param rx_fifo_size: Receive fifo size (default 16) :param tx_fifo_size: Transmit fifo size (default 16) :param bitrate: Channel bitrate in bit/s """ if _canlib is None: raise CanInterfaceNotImplementedError( "The IXXAT VCI library has not been initialized. Check the logs for more details." ) log.info("CAN Filters: %s", can_filters) # Configuration options self._receive_own_messages = receive_own_messages # Usually comes as a string from the config file channel = int(channel) if bitrate not in self.CHANNEL_BITRATES[0]: raise ValueError(f"Invalid bitrate {bitrate}") if rx_fifo_size <= 0: raise ValueError("rx_fifo_size must be > 0") if tx_fifo_size <= 0: raise ValueError("tx_fifo_size must be > 0") if channel < 0: raise ValueError("channel number must be >= 0") self._device_handle = HANDLE() self._device_info = structures.VCIDEVICEINFO() self._control_handle = HANDLE() self._channel_handle = HANDLE() self._channel_capabilities = structures.CANCAPABILITIES() self._message = structures.CANMSG() self._payload = (ctypes.c_byte * 8)() self._can_protocol = CanProtocol.CAN_20 # Search for supplied device if unique_hardware_id is None: log.info("Searching for first available device") else: log.info("Searching for unique HW ID %s", unique_hardware_id) _canlib.vciEnumDeviceOpen(ctypes.byref(self._device_handle)) while True: try: _canlib.vciEnumDeviceNext( self._device_handle, ctypes.byref(self._device_info) ) except StopIteration: if unique_hardware_id is None: raise VCIDeviceNotFoundError( "No IXXAT device(s) connected or device(s) in use by other process(es)." ) from None else: raise VCIDeviceNotFoundError( f"Unique HW ID {unique_hardware_id} not connected or not available." ) from None else: if (unique_hardware_id is None) or ( self._device_info.UniqueHardwareId.AsChar == bytes(unique_hardware_id, "ascii") ): break log.debug( "Ignoring IXXAT with hardware id '%s'.", self._device_info.UniqueHardwareId.AsChar.decode("ascii"), ) _canlib.vciEnumDeviceClose(self._device_handle) try: _canlib.vciDeviceOpen( ctypes.byref(self._device_info.VciObjectId), ctypes.byref(self._device_handle), ) except Exception as exception: raise CanInitializationError( f"Could not open device: {exception}" ) from exception log.info("Using unique HW ID %s", self._device_info.UniqueHardwareId.AsChar) log.info( "Initializing channel %d in shared mode, %d rx buffers, %d tx buffers", channel, rx_fifo_size, tx_fifo_size, ) try: _canlib.canChannelOpen( self._device_handle, channel, constants.FALSE, ctypes.byref(self._channel_handle), ) except Exception as exception: raise CanInitializationError( f"Could not open and initialize channel: {exception}" ) from exception # Signal TX/RX events when at least one frame has been handled _canlib.canChannelInitialize( self._channel_handle, rx_fifo_size, 1, tx_fifo_size, 1 ) _canlib.canChannelActivate(self._channel_handle, constants.TRUE) log.info("Initializing control %d bitrate %d", channel, bitrate) _canlib.canControlOpen( self._device_handle, channel, ctypes.byref(self._control_handle) ) # compute opmode before control initialize opmode = constants.CAN_OPMODE_STANDARD | constants.CAN_OPMODE_ERRFRAME if extended: opmode |= constants.CAN_OPMODE_EXTENDED # control initialize _canlib.canControlInitialize( self._control_handle, opmode, self.CHANNEL_BITRATES[0][bitrate], self.CHANNEL_BITRATES[1][bitrate], ) _canlib.canControlGetCaps( self._control_handle, ctypes.byref(self._channel_capabilities) ) # With receive messages, this field contains the relative reception time of # the message in ticks. The resolution of a tick can be calculated from the fields # dwClockFreq and dwTscDivisor of the structure CANCAPABILITIES in accordance with the following formula: # frequency [1/s] = dwClockFreq / dwTscDivisor self._tick_resolution = ( self._channel_capabilities.dwClockFreq / self._channel_capabilities.dwTscDivisor ) # Setup filters before starting the channel if can_filters: log.info("The IXXAT VCI backend is filtering messages") # Disable every message coming in for extended in (0, 1): _canlib.canControlSetAccFilter( self._control_handle, extended, constants.CAN_ACC_CODE_NONE, constants.CAN_ACC_MASK_NONE, ) for can_filter in can_filters: # Filters define what messages are accepted code = int(can_filter["can_id"]) mask = int(can_filter["can_mask"]) extended = can_filter.get("extended", False) _canlib.canControlAddFilterIds( self._control_handle, 1 if extended else 0, code << 1, mask << 1 ) log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask) # Start the CAN controller. Messages will be forwarded to the channel _canlib.canControlStart(self._control_handle, constants.TRUE) # For cyclic transmit list. Set when .send_periodic() is first called self._scheduler = None self._scheduler_resolution = None self.channel = channel # Usually you get back 3 messages like "CAN initialized" ecc... # Clear the FIFO by filter them out with low timeout for _ in range(rx_fifo_size): try: _canlib.canChannelReadMessage( self._channel_handle, 0, ctypes.byref(self._message) ) except (VCITimeout, VCIRxQueueEmptyError): break super().__init__(channel=channel, can_filters=None, **kwargs) def _inWaiting(self): try: _canlib.canChannelWaitRxEvent(self._channel_handle, 0) except VCITimeout: return 0 else: return 1 def flush_tx_buffer(self): """Flushes the transmit buffer on the IXXAT""" # TODO #64: no timeout? _canlib.canChannelWaitTxEvent(self._channel_handle, constants.INFINITE) def _recv_internal(self, timeout): """Read a message from IXXAT device.""" data_received = False if self._inWaiting() or timeout == 0: # Peek without waiting recv_function = functools.partial( _canlib.canChannelPeekMessage, self._channel_handle, ctypes.byref(self._message), ) else: # Wait if no message available timeout = ( constants.INFINITE if (timeout is None or timeout < 0) else int(timeout * 1000) ) recv_function = functools.partial( _canlib.canChannelReadMessage, self._channel_handle, timeout, ctypes.byref(self._message), ) try: recv_function() except (VCITimeout, VCIRxQueueEmptyError): # Ignore the 2 errors, overall timeout is handled by BusABC.recv pass else: # See if we got a data or info/error messages if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA: data_received = True elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_INFO: log.info( CAN_INFO_MESSAGES.get( self._message.abData[0], f"Unknown CAN info message code {self._message.abData[0]}", ) ) 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") else: log.warning( CAN_ERROR_MESSAGES.get( self._message.abData[0], f"Unknown CAN error message code {self._message.abData[0]}", ) ) log.warning( "CAN message flags bAddFlags/bFlags2 0x%02X bflags 0x%02X", self._message.uMsgInfo.Bytes.bAddFlags, self._message.uMsgInfo.Bytes.bFlags, ) elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR: pass else: log.warning( "Unexpected message info type 0x%X", self._message.uMsgInfo.Bits.type, ) finally: if not data_received: # Check hard errors status = structures.CANLINESTATUS() _canlib.canControlGetStatus(self._control_handle, ctypes.byref(status)) error_byte_1 = status.dwStatus & 0x0F error_byte_2 = status.dwStatus & 0xF0 if error_byte_1 > constants.CAN_STATUS_TXPEND: # CAN_STATUS_OVRRUN = 0x02 # data overrun occurred # CAN_STATUS_ERRLIM = 0x04 # error warning limit exceeded # CAN_STATUS_BUSOFF = 0x08 # bus off status if error_byte_1 & constants.CAN_STATUS_OVRRUN: raise VCIError("Data overrun occurred") elif error_byte_1 & constants.CAN_STATUS_ERRLIM: raise VCIError("Error warning limit exceeded") elif error_byte_1 & constants.CAN_STATUS_BUSOFF: raise VCIError("Bus off status") elif error_byte_2 > constants.CAN_STATUS_ININIT: # CAN_STATUS_BUSCERR = 0x20 # bus coupling error if error_byte_2 & constants.CAN_STATUS_BUSCERR: raise VCIError("Bus coupling error") if not data_received: # Timed out / can message type is not DATA return None, True # The _message.dwTime is a 32bit tick value and will overrun, # so expect to see the value restarting from 0 rx_msg = Message( timestamp=self._message.dwTime / self._tick_resolution, # Relative time in s is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr), is_extended_id=bool(self._message.uMsgInfo.Bits.ext), arbitration_id=self._message.dwMsgId, dlc=self._message.uMsgInfo.Bits.dlc, data=self._message.abData[: self._message.uMsgInfo.Bits.dlc], channel=self.channel, ) return rx_msg, True def send(self, msg: Message, timeout: Optional[float] = None) -> None: """ Sends a message on the bus. The interface may buffer the message. :param msg: The message to send. :param timeout: Timeout after some time. :raise: :class:CanTimeoutError :class:CanOperationError """ # This system is not designed to be very efficient message = structures.CANMSG() message.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA message.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 message.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 message.uMsgInfo.Bits.srr = 1 if self._receive_own_messages else 0 message.dwMsgId = msg.arbitration_id if msg.dlc: message.uMsgInfo.Bits.dlc = msg.dlc adapter = (ctypes.c_uint8 * len(msg.data)).from_buffer(msg.data) ctypes.memmove(message.abData, adapter, len(msg.data)) if timeout: _canlib.canChannelSendMessage( self._channel_handle, int(timeout * 1000), message ) else: _canlib.canChannelPostMessage(self._channel_handle, message) # Want to log outgoing messages? # log.log(self.RECV_LOGGING_LEVEL, "Sent: %s", message) def _send_periodic_internal( self, msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> CyclicSendTaskABC: """Send a message using built-in cyclic transmit list functionality.""" if modifier_callback is None: if self._scheduler is None: self._scheduler = HANDLE() _canlib.canSchedulerOpen( self._device_handle, self.channel, self._scheduler ) caps = structures.CANCAPABILITIES() _canlib.canSchedulerGetCaps(self._scheduler, caps) self._scheduler_resolution = caps.dwClockFreq / caps.dwCmsDivisor _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) return CyclicSendTask( self._scheduler, msgs, period, duration, self._scheduler_resolution, autostart=autostart, ) # fallback to thread based cyclic task warnings.warn( f"{self.__class__.__name__} falls back to a thread-based cyclic task, " "when the `modifier_callback` argument is given.", stacklevel=3, ) return BusABC._send_periodic_internal( self, msgs=msgs, period=period, duration=duration, autostart=autostart, modifier_callback=modifier_callback, ) def shutdown(self): super().shutdown() if self._scheduler is not None: _canlib.canSchedulerClose(self._scheduler) _canlib.canChannelClose(self._channel_handle) _canlib.canControlStart(self._control_handle, constants.FALSE) _canlib.canControlReset(self._control_handle) _canlib.canControlClose(self._control_handle) _canlib.vciDeviceClose(self._device_handle) @property def state(self) -> BusState: """ Return the current state of the hardware """ status = structures.CANLINESTATUS() _canlib.canControlGetStatus(self._control_handle, ctypes.byref(status)) if status.bOpMode == constants.CAN_OPMODE_LISTONLY: return BusState.PASSIVE error_byte_1 = status.dwStatus & 0x0F # CAN_STATUS_BUSOFF = 0x08 # bus off status if error_byte_1 & constants.CAN_STATUS_BUSOFF: return BusState.ERROR error_byte_2 = status.dwStatus & 0xF0 # CAN_STATUS_BUSCERR = 0x20 # bus coupling error if error_byte_2 & constants.CAN_STATUS_BUSCERR: return BusState.ERROR return BusState.ACTIVE # ~class IXXATBus(BusABC): --------------------------------------------------- class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): """A message in the cyclic transmit list.""" def __init__( self, scheduler, msgs, period, duration, resolution, autostart: bool = True, ): super().__init__(msgs, period, duration) if len(self.messages) != 1: raise ValueError( "IXXAT Interface only supports periodic transmission of 1 element" ) self._scheduler = scheduler self._index = None self._count = int(duration / period) if duration else 0 self._msg = structures.CANCYCLICTXMSG() self._msg.wCycleTime = int(round(period * resolution)) self._msg.dwMsgId = self.messages[0].arbitration_id self._msg.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA self._msg.uMsgInfo.Bits.ext = 1 if self.messages[0].is_extended_id else 0 self._msg.uMsgInfo.Bits.rtr = 1 if self.messages[0].is_remote_frame else 0 self._msg.uMsgInfo.Bits.dlc = self.messages[0].dlc for i, b in enumerate(self.messages[0].data): self._msg.abData[i] = b if autostart: self.start() def start(self): """Start transmitting message (add to list if needed).""" if self._index is None: self._index = ctypes.c_uint32() _canlib.canSchedulerAddMessage(self._scheduler, self._msg, self._index) _canlib.canSchedulerStartMessage(self._scheduler, self._index, self._count) def pause(self): """Pause transmitting message (keep it in the list).""" _canlib.canSchedulerStopMessage(self._scheduler, self._index) def stop(self): """Stop transmitting message (remove from list).""" # Remove it completely instead of just stopping it to avoid filling up # the list with permanently stopped messages _canlib.canSchedulerRemMessage(self._scheduler, self._index) self._index = None def _format_can_status(status_flags: int): """ Format a status bitfield found in CAN_MSGTYPE_STATUS messages or in dwStatus field in CANLINESTATUS. Valid states are defined in the CAN_STATUS_* constants in cantype.h """ states = [] for flag, description in CAN_STATUS_FLAGS.items(): if status_flags & flag: states.append(description) status_flags &= ~flag if status_flags: states.append(f"unknown state 0x{status_flags:02x}") if states: return "CAN status message: {}".format(", ".join(states)) else: return "Empty CAN status message" def get_ixxat_hwids(): """Get a list of hardware ids of all available IXXAT devices.""" hwids = [] device_handle = HANDLE() device_info = structures.VCIDEVICEINFO() _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle)) while True: try: _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info)) except StopIteration: break else: hwids.append(device_info.UniqueHardwareId.AsChar.decode("ascii")) _canlib.vciEnumDeviceClose(device_handle) return hwids def _detect_available_configs() -> List[AutoDetectedConfig]: config_list = [] # list in wich to store the resulting bus kwargs # used to detect HWID device_handle = HANDLE() device_info = structures.VCIDEVICEINFO() # used to attempt to open channels channel_handle = HANDLE() device_handle2 = HANDLE() try: _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle)) while True: try: _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info)) except StopIteration: break else: hwid = device_info.UniqueHardwareId.AsChar.decode("ascii") _canlib.vciDeviceOpen( ctypes.byref(device_info.VciObjectId), ctypes.byref(device_handle2), ) for channel in range(4): try: _canlib.canChannelOpen( device_handle2, channel, constants.FALSE, ctypes.byref(channel_handle), ) except Exception: # Array outside of bounds error == accessing a channel not in the hardware break else: _canlib.canChannelClose(channel_handle) config_list.append( { "interface": "ixxat", "channel": channel, "unique_hardware_id": hwid, } ) _canlib.vciDeviceClose(device_handle2) _canlib.vciEnumDeviceClose(device_handle) except AttributeError: pass # _canlib is None in the CI tests -> return a blank list return config_list python-can-4.5.0/can/interfaces/ixxat/canlib_vcinpl2.py000066400000000000000000001153441472200326600230720ustar00rootroot00000000000000""" Ctypes wrapper module for IXXAT Virtual CAN Interface V3 on win32 systems TODO: We could implement this interface such that setting other filters could work when the initial filters were set to zero using the software fallback. Or could the software filters even be changed after the connection was opened? We need to document that bahaviour! See also the NICAN interface. """ import ctypes import functools import logging import sys import time import warnings from typing import Callable, Optional, Sequence, Tuple, Union from can import ( BusABC, CanProtocol, CyclicSendTaskABC, LimitedDurationCyclicSendTaskABC, Message, RestartableCyclicTaskABC, ) from can.ctypesutil import HANDLE, PHANDLE, CLibrary from can.ctypesutil import HRESULT as ctypes_HRESULT from can.exceptions import CanInitializationError, CanInterfaceNotImplementedError from can.util import deprecated_args_alias, dlc2len, len2dlc from . import constants, structures from .exceptions import * __all__ = [ "VCITimeout", "VCIError", "VCIBusOffError", "VCIDeviceNotFoundError", "IXXATBus", "vciFormatError", ] log = logging.getLogger("can.ixxat") # Hack to have vciFormatError as a free function, see below vciFormatError = None # main ctypes instance _canlib = None # TODO: Use ECI driver for linux if sys.platform == "win32" or sys.platform == "cygwin": try: _canlib = CLibrary("vcinpl2.dll") except Exception as e: log.warning("Cannot load IXXAT vcinpl library: %s", e) else: # Will not work on other systems, but have it importable anyway for # tests/sphinx log.warning("IXXAT VCI library does not work on %s platform", sys.platform) def __vciFormatErrorExtended( library_instance: CLibrary, function: Callable, vret: int, args: Tuple ): """Format a VCI error and attach failed function, decoded HRESULT and arguments :param CLibrary library_instance: Mapped instance of IXXAT vcinpl library :param callable function: Failed function :param HRESULT vret: HRESULT returned by vcinpl call :param args: Arbitrary arguments tuple :return: Formatted string """ # TODO: make sure we don't generate another exception return ( f"{__vciFormatError(library_instance, function, vret)} - arguments were {args}" ) def __vciFormatError(library_instance: CLibrary, function: Callable, vret: int): """Format a VCI error and attach failed function and decoded HRESULT :param CLibrary library_instance: Mapped instance of IXXAT vcinpl library :param callable function: Failed function :param HRESULT vret: HRESULT returned by vcinpl call :return: Formatted string """ buf = ctypes.create_string_buffer(constants.VCI_MAX_ERRSTRLEN) ctypes.memset(buf, 0, constants.VCI_MAX_ERRSTRLEN) library_instance.vciFormatError(vret, buf, constants.VCI_MAX_ERRSTRLEN) return "function {} failed ({})".format( function._name, buf.value.decode("utf-8", "replace") ) def __check_status(result, function, args): """ Check the result of a vcinpl function call and raise appropriate exception in case of an error. Used as errcheck function when mapping C functions with ctypes. :param result: Function call numeric result :param callable function: Called function :param args: Arbitrary arguments tuple :raise: :class:VCITimeout :class:VCIRxQueueEmptyError :class:StopIteration :class:VCIError """ if result == constants.VCI_E_TIMEOUT: raise VCITimeout(f"Function {function._name} timed out") elif result == constants.VCI_E_RXQUEUE_EMPTY: raise VCIRxQueueEmptyError() elif result == constants.VCI_E_NO_MORE_ITEMS: raise StopIteration() elif result == constants.VCI_E_ACCESSDENIED: pass # not a real error, might happen if another program has initialized the bus elif result != constants.VCI_OK: raise VCIError(vciFormatError(function, result)) return result try: hresult_type = ctypes.c_ulong # Map all required symbols and initialize library --------------------------- # HRESULT VCIAPI vciInitialize ( void ); _canlib.map_symbol("vciInitialize", hresult_type, (), __check_status) # void VCIAPI vciFormatError (HRESULT hrError, PCHAR pszText, UINT32 dwsize); try: _canlib.map_symbol( "vciFormatError", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) ) except ImportError: _canlib.map_symbol( "vciFormatErrorA", None, (ctypes_HRESULT, ctypes.c_char_p, ctypes.c_uint32) ) _canlib.vciFormatError = _canlib.vciFormatErrorA # Hack to have vciFormatError as a free function vciFormatError = functools.partial(__vciFormatError, _canlib) # HRESULT VCIAPI vciEnumDeviceOpen( OUT PHANDLE hEnum ); _canlib.map_symbol("vciEnumDeviceOpen", hresult_type, (PHANDLE,), __check_status) # HRESULT VCIAPI vciEnumDeviceClose ( IN HANDLE hEnum ); _canlib.map_symbol("vciEnumDeviceClose", hresult_type, (HANDLE,), __check_status) # HRESULT VCIAPI vciEnumDeviceNext( IN HANDLE hEnum, OUT PVCIDEVICEINFO pInfo ); _canlib.map_symbol( "vciEnumDeviceNext", hresult_type, (HANDLE, structures.PVCIDEVICEINFO), __check_status, ) # HRESULT VCIAPI vciDeviceOpen( IN REFVCIID rVciid, OUT PHANDLE phDevice ); _canlib.map_symbol( "vciDeviceOpen", hresult_type, (structures.PVCIID, PHANDLE), __check_status ) # HRESULT vciDeviceClose( HANDLE hDevice ) _canlib.map_symbol("vciDeviceClose", hresult_type, (HANDLE,), __check_status) # HRESULT VCIAPI canChannelOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, IN BOOL fExclusive, OUT PHANDLE phCanChn ); _canlib.map_symbol( "canChannelOpen", hresult_type, (HANDLE, ctypes.c_uint32, ctypes.c_long, PHANDLE), __check_status, ) # EXTERN_C HRESULT VCIAPI # canChannelInitialize( IN HANDLE hCanChn, # IN UINT16 wRxFifoSize, # IN UINT16 wRxThreshold, # IN UINT16 wTxFifoSize, # IN UINT16 wTxThreshold, # IN UINT32 dwFilterSize, # IN UINT8 bFilterMode ); _canlib.map_symbol( "canChannelInitialize", hresult_type, ( HANDLE, ctypes.c_uint16, ctypes.c_uint16, ctypes.c_uint16, ctypes.c_uint16, ctypes.c_uint32, ctypes.c_uint8, ), __check_status, ) # EXTERN_C HRESULT VCIAPI canChannelActivate( IN HANDLE hCanChn, IN BOOL fEnable ); _canlib.map_symbol( "canChannelActivate", hresult_type, (HANDLE, ctypes.c_long), __check_status ) # HRESULT canChannelClose( HANDLE hChannel ) _canlib.map_symbol("canChannelClose", hresult_type, (HANDLE,), __check_status) # EXTERN_C HRESULT VCIAPI canChannelReadMessage( IN HANDLE hCanChn, IN UINT32 dwMsTimeout, OUT PCANMSG2 pCanMsg ); _canlib.map_symbol( "canChannelReadMessage", hresult_type, (HANDLE, ctypes.c_uint32, structures.PCANMSG2), __check_status, ) # HRESULT canChannelPeekMessage(HANDLE hChannel,PCANMSG2 pCanMsg ); _canlib.map_symbol( "canChannelPeekMessage", hresult_type, (HANDLE, structures.PCANMSG2), __check_status, ) # HRESULT canChannelWaitTxEvent (HANDLE hChannel UINT32 dwMsTimeout ); _canlib.map_symbol( "canChannelWaitTxEvent", hresult_type, (HANDLE, ctypes.c_uint32), __check_status, ) # HRESULT canChannelWaitRxEvent (HANDLE hChannel, UINT32 dwMsTimeout ); _canlib.map_symbol( "canChannelWaitRxEvent", hresult_type, (HANDLE, ctypes.c_uint32), __check_status, ) # HRESULT canChannelPostMessage (HANDLE hChannel, PCANMSG2 pCanMsg ); _canlib.map_symbol( "canChannelPostMessage", hresult_type, (HANDLE, structures.PCANMSG2), __check_status, ) # HRESULT canChannelSendMessage (HANDLE hChannel, UINT32 dwMsTimeout, PCANMSG2 pCanMsg ); _canlib.map_symbol( "canChannelSendMessage", hresult_type, (HANDLE, ctypes.c_uint32, structures.PCANMSG2), __check_status, ) # EXTERN_C HRESULT VCIAPI canControlOpen( IN HANDLE hDevice, IN UINT32 dwCanNo, OUT PHANDLE phCanCtl ); _canlib.map_symbol( "canControlOpen", hresult_type, (HANDLE, ctypes.c_uint32, PHANDLE), __check_status, ) # EXTERN_C HRESULT VCIAPI # canControlInitialize( IN HANDLE hCanCtl, # IN UINT8 bOpMode, # IN UINT8 bExMode, # IN UINT8 bSFMode, # IN UINT8 bEFMode, # IN UINT32 dwSFIds, # IN UINT32 dwEFIds, # IN PCANBTP pBtpSDR, # IN PCANBTP pBtpFDR ); _canlib.map_symbol( "canControlInitialize", hresult_type, ( HANDLE, ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint8, ctypes.c_uint32, ctypes.c_uint32, structures.PCANBTP, structures.PCANBTP, ), __check_status, ) # EXTERN_C HRESULT VCIAPI canControlClose( IN HANDLE hCanCtl ); _canlib.map_symbol("canControlClose", hresult_type, (HANDLE,), __check_status) # EXTERN_C HRESULT VCIAPI canControlReset( IN HANDLE hCanCtl ); _canlib.map_symbol("canControlReset", hresult_type, (HANDLE,), __check_status) # EXTERN_C HRESULT VCIAPI canControlStart( IN HANDLE hCanCtl, IN BOOL fStart ); _canlib.map_symbol( "canControlStart", hresult_type, (HANDLE, ctypes.c_long), __check_status ) # EXTERN_C HRESULT VCIAPI canControlGetStatus( IN HANDLE hCanCtl, OUT PCANLINESTATUS2 pStatus ); _canlib.map_symbol( "canControlGetStatus", hresult_type, (HANDLE, structures.PCANLINESTATUS2), __check_status, ) # EXTERN_C HRESULT VCIAPI canControlGetCaps( IN HANDLE hCanCtl, OUT PCANCAPABILITIES2 pCanCaps ); _canlib.map_symbol( "canControlGetCaps", hresult_type, (HANDLE, structures.PCANCAPABILITIES2), __check_status, ) # EXTERN_C HRESULT VCIAPI canControlSetAccFilter( IN HANDLE hCanCtl, IN BOOL fExtend, IN UINT32 dwCode, IN UINT32 dwMask ); _canlib.map_symbol( "canControlSetAccFilter", hresult_type, (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), __check_status, ) # EXTERN_C HRESULT canControlAddFilterIds (HANDLE hControl, BOOL fExtended, UINT32 dwCode, UINT32 dwMask); _canlib.map_symbol( "canControlAddFilterIds", hresult_type, (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), __check_status, ) # EXTERN_C HRESULT canControlRemFilterIds (HANDLE hControl, BOOL fExtendend, UINT32 dwCode, UINT32 dwMask ); _canlib.map_symbol( "canControlRemFilterIds", hresult_type, (HANDLE, ctypes.c_int, ctypes.c_uint32, ctypes.c_uint32), __check_status, ) # EXTERN_C HRESULT canSchedulerOpen (HANDLE hDevice, UINT32 dwCanNo, PHANDLE phScheduler ); _canlib.map_symbol( "canSchedulerOpen", hresult_type, (HANDLE, ctypes.c_uint32, PHANDLE), __check_status, ) # EXTERN_C HRESULT canSchedulerClose (HANDLE hScheduler ); _canlib.map_symbol("canSchedulerClose", hresult_type, (HANDLE,), __check_status) # EXTERN_C HRESULT canSchedulerGetCaps (HANDLE hScheduler, PCANCAPABILITIES2 pCaps ); _canlib.map_symbol( "canSchedulerGetCaps", hresult_type, (HANDLE, structures.PCANCAPABILITIES2), __check_status, ) # EXTERN_C HRESULT canSchedulerActivate ( HANDLE hScheduler, BOOL fEnable ); _canlib.map_symbol( "canSchedulerActivate", hresult_type, (HANDLE, ctypes.c_int), __check_status ) # EXTERN_C HRESULT canSchedulerAddMessage (HANDLE hScheduler, PCANCYCLICTXMSG2 pMessage, PUINT32 pdwIndex ); _canlib.map_symbol( "canSchedulerAddMessage", hresult_type, (HANDLE, structures.PCANCYCLICTXMSG2, ctypes.POINTER(ctypes.c_uint32)), __check_status, ) # EXTERN_C HRESULT canSchedulerRemMessage (HANDLE hScheduler, UINT32 dwIndex ); _canlib.map_symbol( "canSchedulerRemMessage", hresult_type, (HANDLE, ctypes.c_uint32), __check_status, ) # EXTERN_C HRESULT canSchedulerStartMessage (HANDLE hScheduler, UINT32 dwIndex, UINT16 dwCount ); _canlib.map_symbol( "canSchedulerStartMessage", hresult_type, (HANDLE, ctypes.c_uint32, ctypes.c_uint16), __check_status, ) # EXTERN_C HRESULT canSchedulerStopMessage (HANDLE hScheduler, UINT32 dwIndex ); _canlib.map_symbol( "canSchedulerStopMessage", hresult_type, (HANDLE, ctypes.c_uint32), __check_status, ) _canlib.vciInitialize() except AttributeError: # In case _canlib == None meaning we're not on win32/no lib found pass except Exception as e: log.warning("Could not initialize IXXAT VCI library: %s", e) # --------------------------------------------------------------------------- CAN_INFO_MESSAGES = { constants.CAN_INFO_START: "CAN started", constants.CAN_INFO_STOP: "CAN stopped", constants.CAN_INFO_RESET: "CAN reset", } CAN_ERROR_MESSAGES = { constants.CAN_ERROR_STUFF: "CAN bit stuff error", constants.CAN_ERROR_FORM: "CAN form error", constants.CAN_ERROR_ACK: "CAN acknowledgment error", constants.CAN_ERROR_BIT: "CAN bit error", constants.CAN_ERROR_CRC: "CAN CRC error", constants.CAN_ERROR_OTHER: "Other (unknown) CAN error", } CAN_STATUS_FLAGS = { constants.CAN_STATUS_TXPEND: "transmission pending", constants.CAN_STATUS_OVRRUN: "data overrun occurred", constants.CAN_STATUS_ERRLIM: "error warning limit exceeded", constants.CAN_STATUS_BUSOFF: "bus off", constants.CAN_STATUS_ININIT: "init mode active", constants.CAN_STATUS_BUSCERR: "bus coupling error", } # ---------------------------------------------------------------------------- class IXXATBus(BusABC): """The CAN Bus implemented for the IXXAT interface. .. warning:: This interface does implement efficient filtering of messages, but the filters have to be set in ``__init__`` using the ``can_filters`` parameter. Using :meth:`~can.BusABC.set_filters` does not work. """ @deprecated_args_alias( deprecation_start="4.0.0", deprecation_end="5.0.0", UniqueHardwareId="unique_hardware_id", rxFifoSize="rx_fifo_size", txFifoSize="tx_fifo_size", ) def __init__( self, channel: int, can_filters=None, receive_own_messages: int = False, unique_hardware_id: Optional[int] = None, extended: bool = True, rx_fifo_size: int = 1024, tx_fifo_size: int = 128, bitrate: int = 500000, data_bitrate: int = 2000000, sjw_abr: Optional[int] = None, tseg1_abr: Optional[int] = None, tseg2_abr: Optional[int] = None, sjw_dbr: Optional[int] = None, tseg1_dbr: Optional[int] = None, tseg2_dbr: Optional[int] = None, ssp_dbr: Optional[int] = None, **kwargs, ): """ :param channel: The Channel id to create this bus with. :param can_filters: See :meth:`can.BusABC.set_filters`. :param receive_own_messages: Enable self-reception of sent messages. :param unique_hardware_id: unique_hardware_id to connect (optional, will use the first found if not supplied) :param extended: Default True, enables the capability to use extended IDs. :param rx_fifo_size: Receive fifo size (default 1024) :param tx_fifo_size: Transmit fifo size (default 128) :param bitrate: Channel bitrate in bit/s :param data_bitrate: Channel bitrate in bit/s (only in CAN-Fd if baudrate switch enabled). :param sjw_abr: Bus timing value sample jump width (arbitration). :param tseg1_abr: Bus timing value tseg1 (arbitration) :param tseg2_abr: Bus timing value tseg2 (arbitration) :param sjw_dbr: Bus timing value sample jump width (data) :param tseg1_dbr: Bus timing value tseg1 (data). Only takes effect with fd and bitrate switch enabled. :param tseg2_dbr: Bus timing value tseg2 (data). Only takes effect with fd and bitrate switch enabled. :param ssp_dbr: Secondary sample point (data). Only takes effect with fd and bitrate switch enabled. """ if _canlib is None: raise CanInterfaceNotImplementedError( "The IXXAT VCI library has not been initialized. Check the logs for more details." ) log.info("CAN Filters: %s", can_filters) # Configuration options self._receive_own_messages = receive_own_messages # Usually comes as a string from the config file channel = int(channel) if bitrate not in constants.CAN_BITRATE_PRESETS and ( tseg1_abr is None or tseg2_abr is None or sjw_abr is None ): raise ValueError( f"To use bitrate {bitrate} (that has not predefined preset) is mandatory " f"to use also parameters tseg1_abr, tseg2_abr and swj_abr" ) if data_bitrate not in constants.CAN_DATABITRATE_PRESETS and ( tseg1_dbr is None or tseg2_dbr is None or sjw_dbr is None ): raise ValueError( f"To use data_bitrate {data_bitrate} (that has not predefined preset) is mandatory " f"to use also parameters tseg1_dbr, tseg2_dbr and swj_dbr" ) if rx_fifo_size <= 0: raise ValueError("rx_fifo_size must be > 0") if tx_fifo_size <= 0: raise ValueError("tx_fifo_size must be > 0") if channel < 0: raise ValueError("channel number must be >= 0") self._device_handle = HANDLE() self._device_info = structures.VCIDEVICEINFO() self._control_handle = HANDLE() self._channel_handle = HANDLE() self._channel_capabilities = structures.CANCAPABILITIES2() self._message = structures.CANMSG2() self._payload = (ctypes.c_byte * 64)() self._can_protocol = CanProtocol.CAN_FD # Search for supplied device if unique_hardware_id is None: log.info("Searching for first available device") else: log.info("Searching for unique HW ID %s", unique_hardware_id) _canlib.vciEnumDeviceOpen(ctypes.byref(self._device_handle)) while True: try: _canlib.vciEnumDeviceNext( self._device_handle, ctypes.byref(self._device_info) ) except StopIteration: if unique_hardware_id is None: raise VCIDeviceNotFoundError( "No IXXAT device(s) connected or device(s) in use by other process(es)." ) from None else: raise VCIDeviceNotFoundError( f"Unique HW ID {unique_hardware_id} not connected or not available." ) from None else: if (unique_hardware_id is None) or ( self._device_info.UniqueHardwareId.AsChar == bytes(unique_hardware_id, "ascii") ): break else: log.debug( "Ignoring IXXAT with hardware id '%s'.", self._device_info.UniqueHardwareId.AsChar.decode("ascii"), ) _canlib.vciEnumDeviceClose(self._device_handle) try: _canlib.vciDeviceOpen( ctypes.byref(self._device_info.VciObjectId), ctypes.byref(self._device_handle), ) except Exception as exception: raise CanInitializationError( f"Could not open device: {exception}" ) from exception log.info("Using unique HW ID %s", self._device_info.UniqueHardwareId.AsChar) log.info( "Initializing channel %d in shared mode, %d rx buffers, %d tx buffers", channel, rx_fifo_size, tx_fifo_size, ) try: _canlib.canChannelOpen( self._device_handle, channel, constants.FALSE, ctypes.byref(self._channel_handle), ) except Exception as exception: raise CanInitializationError( f"Could not open and initialize channel: {exception}" ) from exception # Signal TX/RX events when at least one frame has been handled _canlib.canChannelInitialize( self._channel_handle, rx_fifo_size, 1, tx_fifo_size, 1, 0, constants.CAN_FILTER_PASS, ) _canlib.canChannelActivate(self._channel_handle, constants.TRUE) pBtpSDR = IXXATBus._canptb_build( defaults=constants.CAN_BITRATE_PRESETS, bitrate=bitrate, tseg1=tseg1_abr, tseg2=tseg2_abr, sjw=sjw_abr, ssp=0, ) pBtpFDR = IXXATBus._canptb_build( defaults=constants.CAN_DATABITRATE_PRESETS, bitrate=data_bitrate, tseg1=tseg1_dbr, tseg2=tseg2_dbr, sjw=sjw_dbr, ssp=ssp_dbr if ssp_dbr is not None else tseg1_dbr, ) log.info( "Initializing control %d with SDR={%s}, FDR={%s}", channel, pBtpSDR, pBtpFDR, ) _canlib.canControlOpen( self._device_handle, channel, ctypes.byref(self._control_handle) ) _canlib.canControlGetCaps( self._control_handle, ctypes.byref(self._channel_capabilities) ) # check capabilities bOpMode = constants.CAN_OPMODE_UNDEFINED if ( self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT ) != 0: # controller supportes CAN_OPMODE_STANDARD and CAN_OPMODE_EXTENDED at the same time bOpMode |= constants.CAN_OPMODE_STANDARD # enable both 11 bits reception if extended: # parameter from configuration bOpMode |= constants.CAN_OPMODE_EXTENDED # enable 29 bits reception elif ( self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_STDANDEXT ) != 0: log.warning( "Channel %d capabilities allow either basic or extended IDs, but not both. using %s according to parameter [extended=%s]", channel, "extended" if extended else "basic", "True" if extended else "False", ) bOpMode |= ( constants.CAN_OPMODE_EXTENDED if extended else constants.CAN_OPMODE_STANDARD ) if ( self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_ERRFRAME ) != 0: bOpMode |= constants.CAN_OPMODE_ERRFRAME bExMode = constants.CAN_EXMODE_DISABLED if (self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_EXTDATA) != 0: bExMode |= constants.CAN_EXMODE_EXTDATALEN if ( self._channel_capabilities.dwFeatures & constants.CAN_FEATURE_FASTDATA ) != 0: bExMode |= constants.CAN_EXMODE_FASTDATA _canlib.canControlInitialize( self._control_handle, bOpMode, bExMode, constants.CAN_FILTER_PASS, constants.CAN_FILTER_PASS, 0, 0, ctypes.byref(pBtpSDR), ctypes.byref(pBtpFDR), ) # With receive messages, this field contains the relative reception time of # the message in ticks. The resolution of a tick can be calculated from the fields # dwClockFreq and dwTscDivisor of the structure CANCAPABILITIES in accordance with the following formula: # frequency [1/s] = dwClockFreq / dwTscDivisor self._tick_resolution = ( self._channel_capabilities.dwTscClkFreq / self._channel_capabilities.dwTscDivisor ) # Setup filters before starting the channel if can_filters: log.info("The IXXAT VCI backend is filtering messages") # Disable every message coming in for extended in (0, 1): _canlib.canControlSetAccFilter( self._control_handle, extended, constants.CAN_ACC_CODE_NONE, constants.CAN_ACC_MASK_NONE, ) for can_filter in can_filters: # Filters define what messages are accepted code = int(can_filter["can_id"]) mask = int(can_filter["can_mask"]) extended = can_filter.get("extended", False) _canlib.canControlAddFilterIds( self._control_handle, 1 if extended else 0, code << 1, mask << 1 ) log.info("Accepting ID: 0x%X MASK: 0x%X", code, mask) # Start the CAN controller. Messages will be forwarded to the channel _canlib.canControlStart(self._control_handle, constants.TRUE) # For cyclic transmit list. Set when .send_periodic() is first called self._scheduler = None self._scheduler_resolution = None self.channel = channel # Usually you get back 3 messages like "CAN initialized" ecc... # Clear the FIFO by filter them out with low timeout for _ in range(rx_fifo_size): try: _canlib.canChannelReadMessage( self._channel_handle, 0, ctypes.byref(self._message) ) except (VCITimeout, VCIRxQueueEmptyError): break super().__init__(channel=channel, can_filters=None, **kwargs) @staticmethod def _canptb_build(defaults, bitrate, tseg1, tseg2, sjw, ssp): if bitrate in defaults: d = defaults[bitrate] if tseg1 is None: tseg1 = d.wTS1 if tseg2 is None: tseg2 = d.wTS2 if sjw is None: sjw = d.wSJW if ssp is None: ssp = d.wTDO dw_mode = d.dwMode else: dw_mode = 0 return structures.CANBTP( dwMode=dw_mode, dwBPS=bitrate, wTS1=tseg1, wTS2=tseg2, wSJW=sjw, wTDO=ssp, ) def _inWaiting(self): try: _canlib.canChannelWaitRxEvent(self._channel_handle, 0) except VCITimeout: return 0 else: return 1 def flush_tx_buffer(self): """Flushes the transmit buffer on the IXXAT""" # TODO #64: no timeout? _canlib.canChannelWaitTxEvent(self._channel_handle, constants.INFINITE) def _recv_internal(self, timeout): """Read a message from IXXAT device.""" # TODO: handling CAN error messages? data_received = False if timeout == 0: # Peek without waiting try: _canlib.canChannelPeekMessage( self._channel_handle, ctypes.byref(self._message) ) except (VCITimeout, VCIRxQueueEmptyError, VCIError): # VCIError means no frame available (canChannelPeekMessage returned different from zero) return None, True else: if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA: data_received = True else: # Wait if no message available if timeout is None or timeout < 0: remaining_ms = constants.INFINITE t0 = None else: timeout_ms = int(timeout * 1000) remaining_ms = timeout_ms t0 = time.perf_counter() while True: try: _canlib.canChannelReadMessage( self._channel_handle, remaining_ms, ctypes.byref(self._message) ) except (VCITimeout, VCIRxQueueEmptyError): # Ignore the 2 errors, the timeout is handled manually with the perf_counter() pass else: # See if we got a data or info/error messages if self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_DATA: data_received = True break elif self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_INFO: log.info( CAN_INFO_MESSAGES.get( self._message.abData[0], f"Unknown CAN info message code {self._message.abData[0]}", ) ) elif ( self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR ): log.warning( CAN_ERROR_MESSAGES.get( self._message.abData[0], f"Unknown CAN error message code {self._message.abData[0]}", ) ) elif ( self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_STATUS ): log.info(_format_can_status(self._message.abData[0])) if self._message.abData[0] & constants.CAN_STATUS_BUSOFF: raise VCIBusOffError() elif ( self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_TIMEOVR ): pass else: log.warning("Unexpected message info type") if t0 is not None: remaining_ms = timeout_ms - int((time.perf_counter() - t0) * 1000) if remaining_ms < 0: break if not data_received: # Timed out / can message type is not DATA return None, True data_len = dlc2len(self._message.uMsgInfo.Bits.dlc) # The _message.dwTime is a 32bit tick value and will overrun, # so expect to see the value restarting from 0 rx_msg = Message( timestamp=self._message.dwTime / self._tick_resolution, # Relative time in s is_remote_frame=bool(self._message.uMsgInfo.Bits.rtr), is_fd=bool(self._message.uMsgInfo.Bits.edl), is_rx=True, is_error_frame=bool( self._message.uMsgInfo.Bits.type == constants.CAN_MSGTYPE_ERROR ), bitrate_switch=bool(self._message.uMsgInfo.Bits.fdr), error_state_indicator=bool(self._message.uMsgInfo.Bits.esi), is_extended_id=bool(self._message.uMsgInfo.Bits.ext), arbitration_id=self._message.dwMsgId, dlc=data_len, data=self._message.abData[:data_len], channel=self.channel, ) return rx_msg, True def send(self, msg: Message, timeout: Optional[float] = None) -> None: """ Sends a message on the bus. The interface may buffer the message. :param msg: The message to send. :param timeout: Timeout after some time. :raise: :class:CanTimeoutError :class:CanOperationError """ # This system is not designed to be very efficient message = structures.CANMSG2() message.uMsgInfo.Bits.type = ( constants.CAN_MSGTYPE_ERROR if msg.is_error_frame else constants.CAN_MSGTYPE_DATA ) message.uMsgInfo.Bits.rtr = 1 if msg.is_remote_frame else 0 message.uMsgInfo.Bits.ext = 1 if msg.is_extended_id else 0 message.uMsgInfo.Bits.srr = 1 if self._receive_own_messages else 0 message.uMsgInfo.Bits.fdr = 1 if msg.bitrate_switch else 0 message.uMsgInfo.Bits.esi = 1 if msg.error_state_indicator else 0 message.uMsgInfo.Bits.edl = 1 if msg.is_fd else 0 message.dwMsgId = msg.arbitration_id if msg.dlc: # this dlc means number of bytes of payload message.uMsgInfo.Bits.dlc = len2dlc(msg.dlc) data_len_dif = msg.dlc - len(msg.data) data = msg.data + bytearray( [0] * data_len_dif ) # pad with zeros until required length adapter = (ctypes.c_uint8 * msg.dlc).from_buffer(data) ctypes.memmove(message.abData, adapter, msg.dlc) if timeout: _canlib.canChannelSendMessage( self._channel_handle, int(timeout * 1000), message ) else: _canlib.canChannelPostMessage(self._channel_handle, message) def _send_periodic_internal( self, msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> CyclicSendTaskABC: """Send a message using built-in cyclic transmit list functionality.""" if modifier_callback is None: if self._scheduler is None: self._scheduler = HANDLE() _canlib.canSchedulerOpen( self._device_handle, self.channel, self._scheduler ) caps = structures.CANCAPABILITIES2() _canlib.canSchedulerGetCaps(self._scheduler, caps) self._scheduler_resolution = ( caps.dwCmsClkFreq / caps.dwCmsDivisor ) # TODO: confirm _canlib.canSchedulerActivate(self._scheduler, constants.TRUE) return CyclicSendTask( self._scheduler, msgs, period, duration, self._scheduler_resolution, autostart=autostart, ) # fallback to thread based cyclic task warnings.warn( f"{self.__class__.__name__} falls back to a thread-based cyclic task, " "when the `modifier_callback` argument is given.", stacklevel=3, ) return BusABC._send_periodic_internal( self, msgs=msgs, period=period, duration=duration, autostart=autostart, modifier_callback=modifier_callback, ) def shutdown(self): super().shutdown() if self._scheduler is not None: _canlib.canSchedulerClose(self._scheduler) _canlib.canChannelClose(self._channel_handle) _canlib.canControlStart(self._control_handle, constants.FALSE) _canlib.canControlClose(self._control_handle) _canlib.vciDeviceClose(self._device_handle) class CyclicSendTask(LimitedDurationCyclicSendTaskABC, RestartableCyclicTaskABC): """A message in the cyclic transmit list.""" def __init__( self, scheduler, msgs, period, duration, resolution, autostart: bool = True, ): super().__init__(msgs, period, duration) if len(self.messages) != 1: raise ValueError( "IXXAT Interface only supports periodic transmission of 1 element" ) self._scheduler = scheduler self._index = None self._count = int(duration / period) if duration else 0 self._msg = structures.CANCYCLICTXMSG2() self._msg.wCycleTime = int(round(period * resolution)) self._msg.dwMsgId = self.messages[0].arbitration_id self._msg.uMsgInfo.Bits.type = constants.CAN_MSGTYPE_DATA self._msg.uMsgInfo.Bits.ext = 1 if self.messages[0].is_extended_id else 0 self._msg.uMsgInfo.Bits.rtr = 1 if self.messages[0].is_remote_frame else 0 self._msg.uMsgInfo.Bits.dlc = self.messages[0].dlc for i, b in enumerate(self.messages[0].data): self._msg.abData[i] = b if autostart: self.start() def start(self): """Start transmitting message (add to list if needed).""" if self._index is None: self._index = ctypes.c_uint32() _canlib.canSchedulerAddMessage(self._scheduler, self._msg, self._index) _canlib.canSchedulerStartMessage(self._scheduler, self._index, self._count) def pause(self): """Pause transmitting message (keep it in the list).""" _canlib.canSchedulerStopMessage(self._scheduler, self._index) def stop(self): """Stop transmitting message (remove from list).""" # Remove it completely instead of just stopping it to avoid filling up # the list with permanently stopped messages _canlib.canSchedulerRemMessage(self._scheduler, self._index) self._index = None def _format_can_status(status_flags: int): """ Format a status bitfield found in CAN_MSGTYPE_STATUS messages or in dwStatus field in CANLINESTATUS. Valid states are defined in the CAN_STATUS_* constants in cantype.h """ states = [] for flag, description in CAN_STATUS_FLAGS.items(): if status_flags & flag: states.append(description) status_flags &= ~flag if status_flags: states.append(f"unknown state 0x{status_flags:02x}") if states: return "CAN status message: {}".format(", ".join(states)) else: return "Empty CAN status message" def get_ixxat_hwids(): """Get a list of hardware ids of all available IXXAT devices.""" hwids = [] device_handle = HANDLE() device_info = structures.VCIDEVICEINFO() _canlib.vciEnumDeviceOpen(ctypes.byref(device_handle)) while True: try: _canlib.vciEnumDeviceNext(device_handle, ctypes.byref(device_info)) except StopIteration: break else: hwids.append(device_info.UniqueHardwareId.AsChar.decode("ascii")) _canlib.vciEnumDeviceClose(device_handle) return hwids python-can-4.5.0/can/interfaces/ixxat/constants.py000066400000000000000000000216161472200326600222170ustar00rootroot00000000000000""" Ctypes wrapper module for IXXAT Virtual CAN Interface V4 on win32 systems Copyright (C) 2016 Giuseppe Corbelli """ from . import structures FALSE = 0 TRUE = 1 INFINITE = 0xFFFFFFFF VCI_MAX_ERRSTRLEN = 256 # Bitrates CAN_BT0_10KB = 0x31 CAN_BT1_10KB = 0x1C CAN_BT0_20KB = 0x18 CAN_BT1_20KB = 0x1C CAN_BT0_50KB = 0x09 CAN_BT1_50KB = 0x1C CAN_BT0_100KB = 0x04 CAN_BT1_100KB = 0x1C CAN_BT0_125KB = 0x03 CAN_BT1_125KB = 0x1C CAN_BT0_250KB = 0x01 CAN_BT1_250KB = 0x1C CAN_BT0_500KB = 0x00 CAN_BT1_500KB = 0x1C CAN_BT0_667KB = 0x00 CAN_BT1_667KB = 0x18 CAN_BT0_800KB = 0x00 CAN_BT1_800KB = 0x16 CAN_BT0_1000KB = 0x00 CAN_BT1_1000KB = 0x14 # Facilities/severities SEV_INFO = 0x40000000 SEV_WARN = 0x80000000 SEV_ERROR = 0xC0000000 SEV_MASK = 0xC0000000 SEV_SUCCESS = 0x00000000 RESERVED_FLAG = 0x10000000 CUSTOMER_FLAG = 0x20000000 STATUS_MASK = 0x0000FFFF FACILITY_MASK = 0x0FFF0000 # Or so I hope FACILITY_STD = 0 SEV_STD_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_STD SEV_STD_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_STD SEV_STD_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_STD FACILITY_VCI = 0x00010000 SEV_VCI_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_VCI SEV_VCI_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_VCI SEV_VCI_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_VCI FACILITY_DAL = 0x00020000 SEV_DAL_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_DAL SEV_DAL_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_DAL SEV_DAL_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_DAL FACILITY_CCL = 0x00030000 SEV_CCL_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_CCL SEV_CCL_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_CCL SEV_CCL_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_CCL FACILITY_BAL = 0x00040000 SEV_BAL_INFO = SEV_INFO | CUSTOMER_FLAG | FACILITY_BAL SEV_BAL_WARN = SEV_WARN | CUSTOMER_FLAG | FACILITY_BAL SEV_BAL_ERROR = SEV_ERROR | CUSTOMER_FLAG | FACILITY_BAL # Errors VCI_SUCCESS = 0x00 VCI_OK = 0x00 VCI_E_UNEXPECTED = SEV_VCI_ERROR | 0x0001 VCI_E_NOT_IMPLEMENTED = SEV_VCI_ERROR | 0x0002 VCI_E_OUTOFMEMORY = SEV_VCI_ERROR | 0x0003 VCI_E_INVALIDARG = SEV_VCI_ERROR | 0x0004 VCI_E_NOINTERFACE = SEV_VCI_ERROR | 0x0005 VCI_E_INVPOINTER = SEV_VCI_ERROR | 0x0006 VCI_E_INVHANDLE = SEV_VCI_ERROR | 0x0007 VCI_E_ABORT = SEV_VCI_ERROR | 0x0008 VCI_E_FAIL = SEV_VCI_ERROR | 0x0009 VCI_E_ACCESSDENIED = SEV_VCI_ERROR | 0x000A VCI_E_TIMEOUT = SEV_VCI_ERROR | 0x000B VCI_E_BUSY = SEV_VCI_ERROR | 0x000C VCI_E_PENDING = SEV_VCI_ERROR | 0x000D VCI_E_NO_DATA = SEV_VCI_ERROR | 0x000E VCI_E_NO_MORE_ITEMS = SEV_VCI_ERROR | 0x000F VCI_E_NOT_INITIALIZED = SEV_VCI_ERROR | 0x0010 VCI_E_ALREADY_INITIALIZED = SEV_VCI_ERROR | 0x00011 VCI_E_RXQUEUE_EMPTY = SEV_VCI_ERROR | 0x00012 VCI_E_TXQUEUE_FULL = SEV_VCI_ERROR | 0x0013 VCI_E_BUFFER_OVERFLOW = SEV_VCI_ERROR | 0x0014 VCI_E_INVALID_STATE = SEV_VCI_ERROR | 0x0015 VCI_E_OBJECT_ALREADY_EXISTS = SEV_VCI_ERROR | 0x0016 VCI_E_INVALID_INDEX = SEV_VCI_ERROR | 0x0017 VCI_E_END_OF_FILE = SEV_VCI_ERROR | 0x0018 VCI_E_DISCONNECTED = SEV_VCI_ERROR | 0x0019 VCI_E_WRONG_FLASHFWVERSION = SEV_VCI_ERROR | 0x001A # Controller status CAN_STATUS_TXPEND = 0x01 # transmission pending CAN_STATUS_OVRRUN = 0x02 # data overrun occurred CAN_STATUS_ERRLIM = 0x04 # error warning limit exceeded CAN_STATUS_BUSOFF = 0x08 # bus off status CAN_STATUS_ININIT = 0x10 # init mode active CAN_STATUS_BUSCERR = 0x20 # bus coupling error # Controller operating modes CAN_OPMODE_UNDEFINED = 0x00 # undefined CAN_OPMODE_STANDARD = 0x01 # reception of 11-bit id messages CAN_OPMODE_EXTENDED = 0x02 # reception of 29-bit id messages CAN_OPMODE_ERRFRAME = 0x04 # reception of error frames CAN_OPMODE_LISTONLY = 0x08 # listen only mode (TX passive) CAN_OPMODE_LOWSPEED = 0x10 # use low speed bus interface CAN_OPMODE_AUTOBAUD = 0x20 # automatic bit rate detection # Extended operating modes CAN_EXMODE_DISABLED = 0x00 CAN_EXMODE_EXTDATALEN = 0x01 CAN_EXMODE_FASTDATA = 0x02 CAN_EXMODE_NONISOCANFD = 0x04 # Message types CAN_MSGTYPE_DATA = 0 CAN_MSGTYPE_INFO = 1 CAN_MSGTYPE_ERROR = 2 CAN_MSGTYPE_STATUS = 3 CAN_MSGTYPE_WAKEUP = 4 CAN_MSGTYPE_TIMEOVR = 5 CAN_MSGTYPE_TIMERST = 6 # Information supplied in the abData[0] field of info frames # (CANMSGINFO.Bytes.bType = CAN_MSGTYPE_INFO). CAN_INFO_START = 1 CAN_INFO_STOP = 2 CAN_INFO_RESET = 3 # Information supplied in the abData[0] field of info frames # (CANMSGINFO.Bytes.bType = CAN_MSGTYPE_ERROR). CAN_ERROR_STUFF = 1 # stuff error CAN_ERROR_FORM = 2 # form error CAN_ERROR_ACK = 3 # acknowledgment error CAN_ERROR_BIT = 4 # bit error CAN_ERROR_CRC = 6 # CRC error CAN_ERROR_OTHER = 7 # other (unspecified) error # acceptance code and mask to reject all CAN IDs CAN_ACC_MASK_NONE = 0xFFFFFFFF CAN_ACC_CODE_NONE = 0x80000000 # BTMODEs CAN_BTMODE_RAW = 0x00000001 # raw mode CAN_BTMODE_TSM = 0x00000002 # triple sampling mode CAN_FILTER_VOID = 0x00 # invalid or unknown filter mode (do not use for initialization) CAN_FILTER_LOCK = 0x01 # lock filter (inhibit all IDs) CAN_FILTER_PASS = 0x02 # bypass filter (pass all IDs) CAN_FILTER_INCL = 0x03 # inclusive filtering (pass registered IDs) CAN_FILTER_EXCL = 0x04 # exclusive filtering (inhibit registered IDs) # message information flags (used by ) CAN_MSGFLAGS_DLC = 0x0F # [bit 0] data length code CAN_MSGFLAGS_OVR = 0x10 # [bit 4] data overrun flag CAN_MSGFLAGS_SRR = 0x20 # [bit 5] self reception request CAN_MSGFLAGS_RTR = 0x40 # [bit 6] remote transmission request CAN_MSGFLAGS_EXT = 0x80 # [bit 7] frame format (0=11-bit, 1=29-bit) # extended message information flags (used by ) CAN_MSGFLAGS2_SSM = 0x01 # [bit 0] single shot mode CAN_MSGFLAGS2_HPM = 0x02 # [bit 1] high priority message CAN_MSGFLAGS2_EDL = 0x04 # [bit 2] extended data length CAN_MSGFLAGS2_FDR = 0x08 # [bit 3] fast data bit rate CAN_MSGFLAGS2_ESI = 0x10 # [bit 4] error state indicator CAN_MSGFLAGS2_RES = 0xE0 # [bit 5..7] reserved bits CAN_ACCEPT_REJECT = 0x00 # message not accepted CAN_ACCEPT_ALWAYS = 0xFF # message always accepted CAN_ACCEPT_FILTER_1 = 0x01 # message accepted by filter 1 CAN_ACCEPT_FILTER_2 = 0x02 # message accepted by filter 2 CAN_ACCEPT_PASSEXCL = 0x03 # message passes exclusion filter CAN_FEATURE_STDOREXT = 0x00000001 # 11 OR 29 bit (exclusive) CAN_FEATURE_STDANDEXT = 0x00000002 # 11 AND 29 bit (simultaneous) CAN_FEATURE_RMTFRAME = 0x00000004 # reception of remote frames CAN_FEATURE_ERRFRAME = 0x00000008 # reception of error frames CAN_FEATURE_BUSLOAD = 0x00000010 # bus load measurement CAN_FEATURE_IDFILTER = 0x00000020 # exact message filter CAN_FEATURE_LISTONLY = 0x00000040 # listen only mode CAN_FEATURE_SCHEDULER = 0x00000080 # cyclic message scheduler CAN_FEATURE_GENERRFRM = 0x00000100 # error frame generation CAN_FEATURE_DELAYEDTX = 0x00000200 # delayed message transmitter CAN_FEATURE_SINGLESHOT = 0x00000400 # single shot mode CAN_FEATURE_HIGHPRIOR = 0x00000800 # high priority message CAN_FEATURE_AUTOBAUD = 0x00001000 # automatic bit rate detection CAN_FEATURE_EXTDATA = 0x00002000 # extended data length (CANFD) CAN_FEATURE_FASTDATA = 0x00004000 # fast data bit rate (CANFD) CAN_FEATURE_ISOFRAME = 0x00008000 # ISO conform frame (CANFD) CAN_FEATURE_NONISOFRM = ( 0x00010000 # non ISO conform frame (CANFD) (different CRC computation) ) CAN_FEATURE_64BITTSC = 0x00020000 # 64-bit time stamp counter CAN_BITRATE_PRESETS = { 250000: structures.CANBTP( dwMode=0, dwBPS=250000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 ), # SP = 80,0% 500000: structures.CANBTP( dwMode=0, dwBPS=500000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 ), # SP = 80,0% 1000000: structures.CANBTP( dwMode=0, dwBPS=1000000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=0 ), # SP = 80,0% } CAN_DATABITRATE_PRESETS = { 500000: structures.CANBTP( dwMode=0, dwBPS=500000, wTS1=6400, wTS2=1600, wSJW=1600, wTDO=6400 ), # SP = 80,0% 833333: structures.CANBTP( dwMode=0, dwBPS=833333, wTS1=1600, wTS2=400, wSJW=400, wTDO=1620 ), # SP = 80,0% 1000000: structures.CANBTP( dwMode=0, dwBPS=1000000, wTS1=1600, wTS2=400, wSJW=400, wTDO=1600 ), # SP = 80,0% 1538461: structures.CANBTP( dwMode=0, dwBPS=1538461, wTS1=1000, wTS2=300, wSJW=300, wTDO=1040 ), # SP = 76,9% 2000000: structures.CANBTP( dwMode=0, dwBPS=2000000, wTS1=1600, wTS2=400, wSJW=400, wTDO=1600 ), # SP = 80,0% 4000000: structures.CANBTP( dwMode=0, dwBPS=4000000, wTS1=800, wTS2=200, wSJW=200, wTDO=800 ), # SP = 80,0% 5000000: structures.CANBTP( dwMode=0, dwBPS=5000000, wTS1=600, wTS2=200, wSJW=200, wTDO=600 ), # SP = 75,0% 6666666: structures.CANBTP( dwMode=0, dwBPS=6666666, wTS1=400, wTS2=200, wSJW=200, wTDO=402 ), # SP = 66,7% 8000000: structures.CANBTP( dwMode=0, dwBPS=8000000, wTS1=400, wTS2=100, wSJW=100, wTDO=250 ), # SP = 80,0% 10000000: structures.CANBTP( dwMode=0, dwBPS=10000000, wTS1=300, wTS2=100, wSJW=100, wTDO=200 ), # SP = 75,0% } python-can-4.5.0/can/interfaces/ixxat/exceptions.py000066400000000000000000000017221472200326600223600ustar00rootroot00000000000000""" Ctypes wrapper module for IXXAT Virtual CAN Interface V4 on win32 systems Copyright (C) 2016 Giuseppe Corbelli Copyright (C) 2019 Marcel Kanter """ from can import ( CanInitializationError, CanOperationError, CanTimeoutError, ) __all__ = [ "VCITimeout", "VCIError", "VCIRxQueueEmptyError", "VCIBusOffError", "VCIDeviceNotFoundError", ] class VCITimeout(CanTimeoutError): """Wraps the VCI_E_TIMEOUT error""" class VCIError(CanOperationError): """Try to display errors that occur within the wrapped C library nicely.""" class VCIRxQueueEmptyError(VCIError): """Wraps the VCI_E_RXQUEUE_EMPTY error""" def __init__(self): super().__init__("Receive queue is empty") class VCIBusOffError(VCIError): def __init__(self): super().__init__("Controller is in BUSOFF state") class VCIDeviceNotFoundError(CanInitializationError): pass python-can-4.5.0/can/interfaces/ixxat/structures.py000066400000000000000000000244541472200326600224310ustar00rootroot00000000000000""" Ctypes wrapper module for IXXAT Virtual CAN Interface V4 on win32 systems Copyright (C) 2016 Giuseppe Corbelli """ import ctypes class LUID(ctypes.Structure): _fields_ = [("LowPart", ctypes.c_uint32), ("HighPart", ctypes.c_int32)] PLUID = ctypes.POINTER(LUID) class VCIID(ctypes.Union): _fields_ = [("AsLuid", LUID), ("AsInt64", ctypes.c_int64)] PVCIID = ctypes.POINTER(VCIID) class GUID(ctypes.Structure): _fields_ = [ ("Data1", ctypes.c_uint32), ("Data2", ctypes.c_uint16), ("Data3", ctypes.c_uint16), ("Data4", ctypes.c_char * 8), ] class VCIDEVICEINFO(ctypes.Structure): class UniqueHardwareId(ctypes.Union): _fields_ = [("AsChar", ctypes.c_char * 16), ("AsGuid", GUID)] _fields_ = [ ("VciObjectId", VCIID), ("DeviceClass", GUID), ("DriverMajorVersion", ctypes.c_uint8), ("DriverMinorVersion", ctypes.c_uint8), ("DriverBuildVersion", ctypes.c_uint16), ("HardwareBranchVersion", ctypes.c_uint8), ("HardwareMajorVersion", ctypes.c_uint8), ("HardwareMinorVersion", ctypes.c_uint8), ("HardwareBuildVersion", ctypes.c_uint8), ("UniqueHardwareId", UniqueHardwareId), ("Description", ctypes.c_char * 128), ("Manufacturer", ctypes.c_char * 126), ("DriverReleaseVersion", ctypes.c_uint16), ] def __str__(self): return ( f"Mfg: {self.Manufacturer}, " f"Dev: {self.Description} " f"HW: {self.HardwareBranchVersion}" f".{self.HardwareMajorVersion}" f".{self.HardwareMinorVersion}" f".{self.HardwareBuildVersion} " f"Drv: {self.DriverReleaseVersion}" f".{self.DriverMajorVersion}" f".{self.DriverMinorVersion}" f".{self.DriverBuildVersion}" ) PVCIDEVICEINFO = ctypes.POINTER(VCIDEVICEINFO) class CANLINESTATUS(ctypes.Structure): _fields_ = [ # current CAN operating mode. Value is a logical combination of # one or more CAN_OPMODE_xxx constants ("bOpMode", ctypes.c_uint8), ("bBtReg0", ctypes.c_uint8), # current bus timing register 0 value ("bBtReg1", ctypes.c_uint8), # current bus timing register 1 value ("bBusLoad", ctypes.c_uint8), # average bus load in percent (0..100) ("dwStatus", ctypes.c_uint32), # status of the CAN controller (see CAN_STATUS_) ] PCANLINESTATUS = ctypes.POINTER(CANLINESTATUS) class CANCHANSTATUS(ctypes.Structure): _fields_ = [ ("sLineStatus", CANLINESTATUS), # current CAN line status ("fActivated", ctypes.c_uint32), # TRUE if the channel is activated ("fRxOverrun", ctypes.c_uint32), # TRUE if receive FIFO overrun occurred ("bRxFifoLoad", ctypes.c_uint8), # receive FIFO load in percent (0..100) ("bTxFifoLoad", ctypes.c_uint8), # transmit FIFO load in percent (0..100) ] PCANCHANSTATUS = ctypes.POINTER(CANCHANSTATUS) class CANCAPABILITIES(ctypes.Structure): _fields_ = [ ("wCtrlType", ctypes.c_uint16), ("wBusCoupling", ctypes.c_uint16), ("dwFeatures", ctypes.c_uint32), ("dwClockFreq", ctypes.c_uint32), ("dwTscDivisor", ctypes.c_uint32), ("dwCmsDivisor", ctypes.c_uint32), ("dwCmsMaxTicks", ctypes.c_uint32), ("dwDtxDivisor", ctypes.c_uint32), ("dwDtxMaxTicks", ctypes.c_uint32), ] PCANCAPABILITIES = ctypes.POINTER(CANCAPABILITIES) class CANMSGINFO(ctypes.Union): class Bytes(ctypes.Structure): _fields_ = [ ("bType", ctypes.c_uint8), # type (see CAN_MSGTYPE_ constants) ( "bAddFlags", ctypes.c_uint8, ), # extended flags (see CAN_MSGFLAGS2_ constants). AKA bFlags2 in VCI v4 ("bFlags", ctypes.c_uint8), # flags (see CAN_MSGFLAGS_ constants) ("bAccept", ctypes.c_uint8), # accept code (see CAN_ACCEPT_ constants) ] class Bits(ctypes.Structure): _fields_ = [ ("type", ctypes.c_uint32, 8), # type (see CAN_MSGTYPE_ constants) ("ssm", ctypes.c_uint32, 1), # single shot mode ("hpm", ctypes.c_uint32, 1), # high priority message ("edl", ctypes.c_uint32, 1), # extended data length ("fdr", ctypes.c_uint32, 1), # fast data bit rate ("esi", ctypes.c_uint32, 1), # error state indicator ("res", ctypes.c_uint32, 3), # reserved set to 0 ("dlc", ctypes.c_uint32, 4), # data length code ("ovr", ctypes.c_uint32, 1), # data overrun ("srr", ctypes.c_uint32, 1), # self reception request ("rtr", ctypes.c_uint32, 1), # remote transmission request ( "ext", ctypes.c_uint32, 1, ), # extended frame format (0=standard, 1=extended) ("afc", ctypes.c_uint32, 8), # accept code (see CAN_ACCEPT_ constants) ] _fields_ = [("Bytes", Bytes), ("Bits", Bits)] PCANMSGINFO = ctypes.POINTER(CANMSGINFO) class CANMSG(ctypes.Structure): _fields_ = [ ("dwTime", ctypes.c_uint32), # CAN ID of the message in Intel format (aligned right) without RTR bit. ("dwMsgId", ctypes.c_uint32), ("uMsgInfo", CANMSGINFO), ("abData", ctypes.c_uint8 * 8), ] def __str__(self) -> str: return """ID: 0x{:04x}{} DLC: {:02d} DATA: {}""".format( self.dwMsgId, "[RTR]" if self.uMsgInfo.Bits.rtr else "", self.uMsgInfo.Bits.dlc, memoryview(self.abData)[: self.uMsgInfo.Bits.dlc].hex(sep=" "), ) PCANMSG = ctypes.POINTER(CANMSG) class CANCYCLICTXMSG(ctypes.Structure): _fields_ = [ ("wCycleTime", ctypes.c_uint16), ("bIncrMode", ctypes.c_uint8), ("bByteIndex", ctypes.c_uint8), ("dwMsgId", ctypes.c_uint32), ("uMsgInfo", CANMSGINFO), ("abData", ctypes.c_uint8 * 8), ] PCANCYCLICTXMSG = ctypes.POINTER(CANCYCLICTXMSG) class CANBTP(ctypes.Structure): _fields_ = [ ("dwMode", ctypes.c_uint32), # timing mode (see CAN_BTMODE_ const) ("dwBPS", ctypes.c_uint32), # bits per second or prescaler (see CAN_BTMODE_RAW) ("wTS1", ctypes.c_uint16), # length of time segment 1 in quanta ("wTS2", ctypes.c_uint16), # length of time segment 2 in quanta ("wSJW", ctypes.c_uint16), # re-synchronization jump width im quanta ( "wTDO", ctypes.c_uint16, ), # transceiver delay offset (SSP offset) in quanta (0 = disabled, 0xFFFF = simplified SSP positioning) ] def __str__(self): return "dwMode=%d, dwBPS=%d, wTS1=%d, wTS2=%d, wSJW=%d, wTDO=%d" % ( self.dwMode, self.dwBPS, self.wTS1, self.wTS2, self.wSJW, self.wTDO, ) PCANBTP = ctypes.POINTER(CANBTP) class CANCAPABILITIES2(ctypes.Structure): _fields_ = [ ("wCtrlType", ctypes.c_uint16), # Type of CAN controller (see CAN_CTRL_ const) ("wBusCoupling", ctypes.c_uint16), # Type of Bus coupling (see CAN_BUSC_ const) ( "dwFeatures", ctypes.c_uint32, ), # supported features (see CAN_FEATURE_ constants) ("dwCanClkFreq", ctypes.c_uint32), # CAN clock frequency [Hz] ("sSdrRangeMin", CANBTP), # minimum bit timing values for standard bit rate ("sSdrRangeMax", CANBTP), # maximum bit timing values for standard bit rate ("sFdrRangeMin", CANBTP), # minimum bit timing values for fast data bit rate ("sFdrRangeMax", CANBTP), # maximum bit timing values for fast data bit rate ( "dwTscClkFreq", ctypes.c_uint32, ), # clock frequency of the time stamp counter [Hz] ("dwTscDivisor", ctypes.c_uint32), # divisor for the message time stamp counter ( "dwCmsClkFreq", ctypes.c_uint32, ), # clock frequency of cyclic message scheduler [Hz] ("dwCmsDivisor", ctypes.c_uint32), # divisor for the cyclic message scheduler ( "dwCmsMaxTicks", ctypes.c_uint32, ), # maximum tick count value of the cyclic message ( "dwDtxClkFreq", ctypes.c_uint32, ), # clock frequency of the delayed message transmitter [Hz] ( "dwDtxDivisor", ctypes.c_uint32, ), # divisor for the delayed message transmitter ( "dwDtxMaxTicks", ctypes.c_uint32, ), # maximum tick count value of the delayed message transmitter ] PCANCAPABILITIES2 = ctypes.POINTER(CANCAPABILITIES2) class CANLINESTATUS2(ctypes.Structure): _fields_ = [ ("bOpMode", ctypes.c_uint8), # current CAN operating mode ("bExMode", ctypes.c_uint8), # current CAN extended operating mode ("bBusLoad", ctypes.c_uint8), # average bus load in percent (0..100) ("bReserved", ctypes.c_uint8), # reserved set to 0 ("sBtpSdr", ctypes.c_uint8), # standard bit rate timing ("sBtpFdr", ctypes.c_uint8), # fast data bit rate timing ("dwStatus", ctypes.c_uint32), # status of the CAN controller (see CAN_STATUS_) ] PCANLINESTATUS2 = ctypes.POINTER(CANLINESTATUS2) class CANMSG2(ctypes.Structure): _fields_ = [ ("dwTime", ctypes.c_uint32), # time stamp for receive message ("rsvd", ctypes.c_uint32), # reserved (set to 0) ("dwMsgId", ctypes.c_uint32), # CAN message identifier (INTEL format) ("uMsgInfo", CANMSGINFO), # message information (bit field) ("abData", ctypes.c_uint8 * 64), # message data ] PCANMSG2 = ctypes.POINTER(CANMSG2) class CANCYCLICTXMSG2(ctypes.Structure): _fields_ = [ ("wCycleTime", ctypes.c_uint16), # cycle time for the message in ticks ( "bIncrMode", ctypes.c_uint8, ), # auto increment mode (see CAN_CTXMSG_INC_ const) ( "bByteIndex", ctypes.c_uint8, ), # index of the byte within abData[] to increment ("dwMsgId", ctypes.c_uint32), # message identifier (INTEL format) ("uMsgInfo", CANMSGINFO), # message information (bit field) ("abData", ctypes.c_uint8 * 64), # message data ] PCANCYCLICTXMSG2 = ctypes.POINTER(CANCYCLICTXMSG2) python-can-4.5.0/can/interfaces/kvaser/000077500000000000000000000000001472200326600177615ustar00rootroot00000000000000python-can-4.5.0/can/interfaces/kvaser/__init__.py000066400000000000000000000003321472200326600220700ustar00rootroot00000000000000""" """ __all__ = [ "CANLIBInitializationError", "CANLIBOperationError", "KvaserBus", "canlib", "constants", "get_channel_info", "structures", ] from can.interfaces.kvaser.canlib import * python-can-4.5.0/can/interfaces/kvaser/canlib.py000066400000000000000000000666671472200326600216100ustar00rootroot00000000000000""" Contains Python equivalents of the function and constant definitions in CANLIB's canlib.h, with some supporting functionality specific to Python. Copyright (C) 2010 Dynamic Controls """ import ctypes import logging import sys import time from typing import Optional, Union from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message from can.exceptions import CanError, CanInitializationError, CanOperationError from can.typechecking import CanFilters from can.util import check_or_adjust_timing_clock, time_perfcounter_correlation from . import constants as canstat from . import structures log = logging.getLogger("can.kvaser") # Resolution in us TIMESTAMP_RESOLUTION = 10 TIMESTAMP_FACTOR = TIMESTAMP_RESOLUTION / 1000000.0 try: if sys.platform == "win32": __canlib = ctypes.windll.LoadLibrary("canlib32") else: __canlib = ctypes.cdll.LoadLibrary("libcanlib.so") log.info("loaded kvaser's CAN library") except OSError: log.warning("Kvaser canlib is unavailable.") __canlib = None def _unimplemented_function(*args): raise NotImplementedError("This function is not implemented in canlib") def __get_canlib_function(func_name, argtypes=None, restype=None, errcheck=None): argtypes = [] if argtypes is None else argtypes # log.debug('Wrapping function "%s"' % func_name) try: # e.g. canlib.canBusOn retval = getattr(__canlib, func_name) # log.debug('"%s" found in library', func_name) except AttributeError: log.warning('"%s" was not found in library', func_name) return _unimplemented_function else: # log.debug('Result type is: %s' % type(restype)) # log.debug('Error check function is: %s' % errcheck) retval.argtypes = argtypes retval.restype = restype if errcheck: retval.errcheck = errcheck return retval class CANLIBError(CanError): """ Try to display errors that occur within the wrapped C library nicely. """ def __init__(self, function, error_code, arguments): message = CANLIBError._get_error_message(error_code) super().__init__(f"Function {function.__name__} failed - {message}", error_code) self.function = function self.arguments = arguments @staticmethod def _get_error_message(error_code: int) -> str: errmsg = ctypes.create_string_buffer(128) canGetErrorText(error_code, errmsg, len(errmsg)) return errmsg.value.decode("ascii") class CANLIBInitializationError(CANLIBError, CanInitializationError): pass class CANLIBOperationError(CANLIBError, CanOperationError): pass def __convert_can_status_to_int(result): if isinstance(result, int): return result else: return result.value def __check_status_operation(result, function, arguments): result = __convert_can_status_to_int(result) if not canstat.CANSTATUS_SUCCESS(result): raise CANLIBOperationError(function, result, arguments) return result def __check_status_initialization(result, function, arguments): result = __convert_can_status_to_int(result) if not canstat.CANSTATUS_SUCCESS(result): raise CANLIBInitializationError(function, result, arguments) return result def __check_status_read(result, function, arguments): result = __convert_can_status_to_int(result) if not canstat.CANSTATUS_SUCCESS(result) and result != canstat.canERR_NOMSG: raise CANLIBOperationError(function, result, arguments) return result class c_canHandle(ctypes.c_int): pass canINVALID_HANDLE = -1 def __check_bus_handle_validity(handle, function, arguments): if handle.value > canINVALID_HANDLE: return handle # is valid result = __convert_can_status_to_int(handle) raise CANLIBInitializationError(function, result, arguments) if __canlib is not None: canInitializeLibrary = __get_canlib_function("canInitializeLibrary") canGetErrorText = __get_canlib_function( "canGetErrorText", argtypes=[canstat.c_canStatus, ctypes.c_char_p, ctypes.c_uint], restype=canstat.c_canStatus, errcheck=__check_status_operation, ) # TODO wrap this type of function to provide a more Pythonic API canGetNumberOfChannels = __get_canlib_function( "canGetNumberOfChannels", argtypes=[ctypes.c_void_p], restype=canstat.c_canStatus, errcheck=__check_status_initialization, ) kvReadTimer = __get_canlib_function( "kvReadTimer", argtypes=[c_canHandle, ctypes.POINTER(ctypes.c_uint)], restype=canstat.c_canStatus, errcheck=__check_status_initialization, ) canBusOff = __get_canlib_function( "canBusOff", argtypes=[c_canHandle], restype=canstat.c_canStatus, errcheck=__check_status_operation, ) canBusOn = __get_canlib_function( "canBusOn", argtypes=[c_canHandle], restype=canstat.c_canStatus, errcheck=__check_status_initialization, ) canClose = __get_canlib_function( "canClose", argtypes=[c_canHandle], restype=canstat.c_canStatus, errcheck=__check_status_operation, ) canOpenChannel = __get_canlib_function( "canOpenChannel", argtypes=[ctypes.c_int, ctypes.c_int], restype=c_canHandle, errcheck=__check_bus_handle_validity, ) canSetBusParams = __get_canlib_function( "canSetBusParams", argtypes=[ c_canHandle, ctypes.c_long, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ], restype=canstat.c_canStatus, errcheck=__check_status_initialization, ) canSetBusParamsC200 = __get_canlib_function( "canSetBusParamsC200", argtypes=[ c_canHandle, ctypes.c_byte, ctypes.c_byte, ], restype=canstat.c_canStatus, errcheck=__check_status_initialization, ) canSetBusParamsFd = __get_canlib_function( "canSetBusParamsFd", argtypes=[ c_canHandle, ctypes.c_long, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ], restype=canstat.c_canStatus, errcheck=__check_status_initialization, ) canSetBusOutputControl = __get_canlib_function( "canSetBusOutputControl", argtypes=[c_canHandle, ctypes.c_uint], restype=canstat.c_canStatus, errcheck=__check_status_initialization, ) canSetAcceptanceFilter = __get_canlib_function( "canSetAcceptanceFilter", argtypes=[c_canHandle, ctypes.c_uint, ctypes.c_uint, ctypes.c_int], restype=canstat.c_canStatus, errcheck=__check_status_operation, ) canReadWait = __get_canlib_function( "canReadWait", argtypes=[ c_canHandle, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_long, ], restype=canstat.c_canStatus, errcheck=__check_status_read, ) canWrite = __get_canlib_function( "canWrite", argtypes=[ c_canHandle, ctypes.c_long, ctypes.c_void_p, ctypes.c_uint, ctypes.c_uint, ], restype=canstat.c_canStatus, errcheck=__check_status_operation, ) canWriteSync = __get_canlib_function( "canWriteSync", argtypes=[c_canHandle, ctypes.c_ulong], restype=canstat.c_canStatus, errcheck=__check_status_operation, ) canIoCtlInit = __get_canlib_function( "canIoCtl", argtypes=[c_canHandle, ctypes.c_uint, ctypes.c_void_p, ctypes.c_uint], restype=canstat.c_canStatus, errcheck=__check_status_initialization, ) canIoCtl = __get_canlib_function( "canIoCtl", argtypes=[c_canHandle, ctypes.c_uint, ctypes.c_void_p, ctypes.c_uint], restype=canstat.c_canStatus, errcheck=__check_status_operation, ) canGetVersion = __get_canlib_function( "canGetVersion", restype=ctypes.c_short, errcheck=__check_status_operation ) kvFlashLeds = __get_canlib_function( "kvFlashLeds", argtypes=[c_canHandle, ctypes.c_int, ctypes.c_int], restype=ctypes.c_short, errcheck=__check_status_operation, ) if sys.platform == "win32": canGetVersionEx = __get_canlib_function( "canGetVersionEx", argtypes=[ctypes.c_uint], restype=ctypes.c_uint, errcheck=__check_status_operation, ) canGetChannelData = __get_canlib_function( "canGetChannelData", argtypes=[ctypes.c_int, ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t], restype=canstat.c_canStatus, errcheck=__check_status_initialization, ) canRequestBusStatistics = __get_canlib_function( "canRequestBusStatistics", argtypes=[c_canHandle], restype=canstat.c_canStatus, errcheck=__check_status_operation, ) canGetBusStatistics = __get_canlib_function( "canGetBusStatistics", argtypes=[ c_canHandle, ctypes.POINTER(structures.BusStatistics), ctypes.c_size_t, ], restype=canstat.c_canStatus, errcheck=__check_status_operation, ) def init_kvaser_library(): if __canlib is not None: try: log.debug("Initializing Kvaser CAN library") canInitializeLibrary() log.debug("CAN library initialized") except Exception: log.warning("Kvaser canlib could not be initialized.") DRIVER_MODE_SILENT = False DRIVER_MODE_NORMAL = True BITRATE_OBJS = { 1000000: canstat.canBITRATE_1M, 500000: canstat.canBITRATE_500K, 250000: canstat.canBITRATE_250K, 125000: canstat.canBITRATE_125K, 100000: canstat.canBITRATE_100K, 83000: canstat.canBITRATE_83K, 62000: canstat.canBITRATE_62K, 50000: canstat.canBITRATE_50K, 10000: canstat.canBITRATE_10K, } BITRATE_FD = { 500000: canstat.canFD_BITRATE_500K_80P, 1000000: canstat.canFD_BITRATE_1M_80P, 2000000: canstat.canFD_BITRATE_2M_80P, 4000000: canstat.canFD_BITRATE_4M_80P, 8000000: canstat.canFD_BITRATE_8M_60P, } class KvaserBus(BusABC): """ The CAN Bus implemented for the Kvaser interface. """ def __init__( self, channel: int, can_filters: Optional[CanFilters] = None, timing: Optional[Union[BitTiming, BitTimingFd]] = None, **kwargs, ): """ :param int channel: The Channel id to create this bus with. :param list can_filters: See :meth:`can.BusABC.set_filters`. Backend Configuration :param timing: An instance of :class:`~can.BitTiming` or :class:`~can.BitTimingFd` to specify the bit timing parameters for the Kvaser interface. If provided, it takes precedence over the all other timing-related parameters. Note that the `f_clock` property of the `timing` instance must be 16_000_000 (16MHz) for standard CAN or 80_000_000 (80MHz) for CAN FD. :param int bitrate: Bitrate of channel in bit/s :param bool accept_virtual: If virtual channels should be accepted. :param int tseg1: Time segment 1, that is, the number of quanta from (but not including) the Sync Segment to the sampling point. If this parameter is not given, the Kvaser driver will try to choose all bit timing parameters from a set of defaults. :param int tseg2: Time segment 2, that is, the number of quanta from the sampling point to the end of the bit. :param int sjw: The Synchronization Jump Width. Decides the maximum number of time quanta that the controller can resynchronize every bit. :param int no_samp: Either 1 or 3. Some CAN controllers can also sample each bit three times. In this case, the bit will be sampled three quanta in a row, with the last sample being taken in the edge between TSEG1 and TSEG2. Three samples should only be used for relatively slow baudrates. :param bool driver_mode: Silent or normal. :param bool single_handle: Use one Kvaser CANLIB bus handle for both reading and writing. This can be set if reading and/or writing is done from one thread. :param bool receive_own_messages: If messages transmitted should also be received back. Only works if single_handle is also False. If you want to receive messages from other applications on the same computer, set this to True or set single_handle to True. :param bool fd: If CAN-FD frames should be supported. :param bool fd_non_iso: Open the channel in Non-ISO (Bosch) FD mode. Only applies for FD buses. This changes the handling of the stuff-bit counter and the CRC. Defaults to False (ISO mode) :param bool exclusive: Don't allow sharing of this CANlib channel. :param bool override_exclusive: Open the channel even if it is opened for exclusive access already. :param int data_bitrate: Which bitrate to use for data phase in CAN FD. Defaults to arbitration bitrate. """ log.info(f"CAN Filters: {can_filters}") log.info(f"Got configuration of: {kwargs}") bitrate = kwargs.get("bitrate", 500000) tseg1 = kwargs.get("tseg1", 0) tseg2 = kwargs.get("tseg2", 0) sjw = kwargs.get("sjw", 0) no_samp = kwargs.get("no_samp", 0) driver_mode = kwargs.get("driver_mode", DRIVER_MODE_NORMAL) single_handle = kwargs.get("single_handle", False) receive_own_messages = kwargs.get("receive_own_messages", False) exclusive = kwargs.get("exclusive", False) override_exclusive = kwargs.get("override_exclusive", False) accept_virtual = kwargs.get("accept_virtual", True) fd = isinstance(timing, BitTimingFd) if timing else kwargs.get("fd", False) data_bitrate = kwargs.get("data_bitrate", None) fd_non_iso = kwargs.get("fd_non_iso", False) try: channel = int(channel) except ValueError: raise ValueError("channel must be an integer") from None self.channel = channel self.single_handle = single_handle self._can_protocol = CanProtocol.CAN_20 if fd_non_iso: self._can_protocol = CanProtocol.CAN_FD_NON_ISO elif fd: self._can_protocol = CanProtocol.CAN_FD log.debug("Initialising bus instance") num_channels = ctypes.c_int(0) canGetNumberOfChannels(ctypes.byref(num_channels)) num_channels = int(num_channels.value) log.info("Found %d available channels", num_channels) for idx in range(num_channels): channel_info = get_channel_info(idx) channel_info = f'{channel_info["device_name"]}, S/N {channel_info["serial"]} (#{channel_info["dongle_channel"]})' log.info("%d: %s", idx, channel_info) if idx == channel: self.channel_info = channel_info flags = 0 if exclusive: flags |= canstat.canOPEN_EXCLUSIVE if override_exclusive: flags |= canstat.canOPEN_OVERRIDE_EXCLUSIVE if accept_virtual: flags |= canstat.canOPEN_ACCEPT_VIRTUAL if fd: if fd_non_iso: flags |= canstat.canOPEN_CAN_FD_NONISO else: flags |= canstat.canOPEN_CAN_FD log.debug("Creating read handle to bus channel: %s", channel) self._read_handle = canOpenChannel(channel, flags) canIoCtlInit( self._read_handle, canstat.canIOCTL_SET_TIMER_SCALE, ctypes.byref(ctypes.c_long(TIMESTAMP_RESOLUTION)), 4, ) if isinstance(timing, BitTimingFd): timing = check_or_adjust_timing_clock(timing, [80_000_000]) canSetBusParams( self._read_handle, timing.nom_bitrate, timing.nom_tseg1, timing.nom_tseg2, timing.nom_sjw, 1, 0, ) canSetBusParamsFd( self._read_handle, timing.data_bitrate, timing.data_tseg1, timing.data_tseg2, timing.data_sjw, ) elif isinstance(timing, BitTiming): timing = check_or_adjust_timing_clock(timing, [16_000_000]) canSetBusParamsC200(self._read_handle, timing.btr0, timing.btr1) else: if fd: if "tseg1" not in kwargs and bitrate in BITRATE_FD: # Use predefined bitrate for arbitration bitrate = BITRATE_FD[bitrate] if data_bitrate in BITRATE_FD: # Use predefined bitrate for data data_bitrate = BITRATE_FD[data_bitrate] elif not data_bitrate: # Use same bitrate for arbitration and data phase data_bitrate = bitrate canSetBusParamsFd(self._read_handle, data_bitrate, tseg1, tseg2, sjw) else: if "tseg1" not in kwargs and bitrate in BITRATE_OBJS: bitrate = BITRATE_OBJS[bitrate] canSetBusParams(self._read_handle, bitrate, tseg1, tseg2, sjw, no_samp, 0) # By default, use local echo if single handle is used (see #160) local_echo = single_handle or receive_own_messages if receive_own_messages and single_handle: log.warning("receive_own_messages only works if single_handle is False") canIoCtlInit( self._read_handle, canstat.canIOCTL_SET_LOCAL_TXECHO, ctypes.byref(ctypes.c_byte(local_echo)), 1, ) if self.single_handle: log.debug("We don't require separate handles to the bus") self._write_handle = self._read_handle else: log.debug("Creating separate handle for TX on channel: %s", channel) if exclusive: flags_ = flags & ~canstat.canOPEN_EXCLUSIVE flags_ |= canstat.canOPEN_OVERRIDE_EXCLUSIVE else: flags_ = flags self._write_handle = canOpenChannel(channel, flags_) can_driver_mode = ( canstat.canDRIVER_SILENT if driver_mode == DRIVER_MODE_SILENT else canstat.canDRIVER_NORMAL ) canSetBusOutputControl(self._write_handle, can_driver_mode) self._is_filtered = False super().__init__( channel=channel, can_filters=can_filters, **kwargs, ) # activate channel after CAN filters were applied log.debug("Go on bus") if not self.single_handle: canBusOn(self._read_handle) canBusOn(self._write_handle) # timestamp must be set after bus is online, otherwise kvReadTimer may return erroneous values self._timestamp_offset = self._update_timestamp_offset() def _update_timestamp_offset(self) -> float: timer = ctypes.c_uint(0) try: if time.get_clock_info("time").resolution > 1e-5: ts, perfcounter = time_perfcounter_correlation() kvReadTimer(self._read_handle, ctypes.byref(timer)) current_perfcounter = time.perf_counter() now = ts + (current_perfcounter - perfcounter) return now - (timer.value * TIMESTAMP_FACTOR) else: kvReadTimer(self._read_handle, ctypes.byref(timer)) return time.time() - (timer.value * TIMESTAMP_FACTOR) except Exception as exc: # timer is usually close to 0 log.info(str(exc)) return time.time() - (timer.value * TIMESTAMP_FACTOR) def _apply_filters(self, filters): if filters and len(filters) == 1: can_id = filters[0]["can_id"] can_mask = filters[0]["can_mask"] extended = 1 if filters[0].get("extended") else 0 try: for handle in (self._read_handle, self._write_handle): canSetAcceptanceFilter(handle, can_id, can_mask, extended) except (NotImplementedError, CANLIBError) as e: self._is_filtered = False log.error("Filtering is not supported - %s", e) else: self._is_filtered = True log.info("canlib is filtering on ID 0x%X, mask 0x%X", can_id, can_mask) else: self._is_filtered = False log.info("Hardware filtering has been disabled") try: for handle in (self._read_handle, self._write_handle): for extended in (0, 1): canSetAcceptanceFilter(handle, 0, 0, extended) except (NotImplementedError, CANLIBError) as e: log.error("An error occurred while disabling filtering: %s", e) def flush_tx_buffer(self): """Wipeout the transmit buffer on the Kvaser.""" canIoCtl(self._write_handle, canstat.canIOCTL_FLUSH_TX_BUFFER, 0, 0) def _recv_internal(self, timeout=None): """ Read a message from kvaser device and return whether filtering has taken place. """ arb_id = ctypes.c_long(0) data = ctypes.create_string_buffer(64) dlc = ctypes.c_uint(0) flags = ctypes.c_uint(0) timestamp = ctypes.c_ulong(0) if timeout is None: # Set infinite timeout # http://www.kvaser.com/canlib-webhelp/group___c_a_n.html#ga2edd785a87cc16b49ece8969cad71e5b timeout = 0xFFFFFFFF else: timeout = int(timeout * 1000) # log.log(9, 'Reading for %d ms on handle: %s' % (timeout, self._read_handle)) status = canReadWait( self._read_handle, ctypes.byref(arb_id), ctypes.byref(data), ctypes.byref(dlc), ctypes.byref(flags), ctypes.byref(timestamp), timeout, # This is an X ms blocking read ) if status == canstat.canOK: data_array = data.raw flags = flags.value is_extended = bool(flags & canstat.canMSG_EXT) is_remote_frame = bool(flags & canstat.canMSG_RTR) is_error_frame = bool(flags & canstat.canMSG_ERROR_FRAME) is_fd = bool(flags & canstat.canFDMSG_FDF) bitrate_switch = bool(flags & canstat.canFDMSG_BRS) error_state_indicator = bool(flags & canstat.canFDMSG_ESI) msg_timestamp = timestamp.value * TIMESTAMP_FACTOR rx_msg = Message( arbitration_id=arb_id.value, data=data_array[: dlc.value], dlc=dlc.value, is_extended_id=is_extended, is_error_frame=is_error_frame, is_remote_frame=is_remote_frame, is_fd=is_fd, bitrate_switch=bitrate_switch, error_state_indicator=error_state_indicator, channel=self.channel, timestamp=msg_timestamp + self._timestamp_offset, ) # log.debug('Got message: %s' % rx_msg) return rx_msg, self._is_filtered else: # log.debug('read complete -> status not okay') return None, self._is_filtered def send(self, msg, timeout=None): # log.debug("Writing a message: {}".format(msg)) flags = canstat.canMSG_EXT if msg.is_extended_id else canstat.canMSG_STD if msg.is_remote_frame: flags |= canstat.canMSG_RTR if msg.is_error_frame: flags |= canstat.canMSG_ERROR_FRAME if msg.is_fd: flags |= canstat.canFDMSG_FDF if msg.bitrate_switch: flags |= canstat.canFDMSG_BRS ArrayConstructor = ctypes.c_byte * msg.dlc buf = ArrayConstructor(*msg.data) canWrite( self._write_handle, msg.arbitration_id, ctypes.byref(buf), msg.dlc, flags ) if timeout: canWriteSync(self._write_handle, int(timeout * 1000)) def flash(self, flash=True): """ Turn on or off flashing of the device's LED for physical identification purposes. """ if flash: action = canstat.kvLED_ACTION_ALL_LEDS_ON else: action = canstat.kvLED_ACTION_ALL_LEDS_OFF try: kvFlashLeds(self._read_handle, action, 30000) except (CANLIBError, NotImplementedError) as e: log.error("Could not flash LEDs (%s)", e) def shutdown(self): super().shutdown() # Wait for transmit queue to be cleared try: canWriteSync(self._write_handle, 100) except CANLIBError: # Not a huge deal and it seems that we get timeout if no messages # exists in the buffer at all pass if not self.single_handle: canBusOff(self._read_handle) canClose(self._read_handle) canBusOff(self._write_handle) canClose(self._write_handle) def get_stats(self) -> structures.BusStatistics: """Retrieves the bus statistics. Use like so: .. testsetup:: kvaser from unittest.mock import Mock from can.interfaces.kvaser.structures import BusStatistics bus = Mock() bus.get_stats = Mock(side_effect=lambda: BusStatistics()) .. doctest:: kvaser >>> stats = bus.get_stats() >>> print(stats) std_data: 0, std_remote: 0, ext_data: 0, ext_remote: 0, err_frame: 0, bus_load: 0.0%, overruns: 0 :returns: bus statistics. """ canRequestBusStatistics(self._write_handle) stats = structures.BusStatistics() canGetBusStatistics( self._write_handle, ctypes.pointer(stats), ctypes.sizeof(stats) ) return stats @staticmethod def _detect_available_configs(): config_list = [] try: num_channels = ctypes.c_int(0) canGetNumberOfChannels(ctypes.byref(num_channels)) for channel in range(0, int(num_channels.value)): info = get_channel_info(channel) config_list.append({"interface": "kvaser", "channel": channel, **info}) except (CANLIBError, NameError): pass return config_list def get_channel_info(channel): name = ctypes.create_string_buffer(80) serial = ctypes.c_uint64() number = ctypes.c_uint() canGetChannelData( channel, canstat.canCHANNELDATA_DEVDESCR_ASCII, ctypes.byref(name), ctypes.sizeof(name), ) canGetChannelData( channel, canstat.canCHANNELDATA_CARD_SERIAL_NO, ctypes.byref(serial), ctypes.sizeof(serial), ) canGetChannelData( channel, canstat.canCHANNELDATA_CHAN_NO_ON_CARD, ctypes.byref(number), ctypes.sizeof(number), ) return { "device_name": name.value.decode("ascii", errors="replace"), "serial": serial.value, "dongle_channel": number.value + 1, } init_kvaser_library() python-can-4.5.0/can/interfaces/kvaser/constants.py000066400000000000000000000154651472200326600223620ustar00rootroot00000000000000""" Contains Python equivalents of the function and constant definitions in CANLIB's canstat.h, with some supporting functionality specific to Python. Copyright (C) 2010 Dynamic Controls """ import ctypes class c_canStatus(ctypes.c_int): pass # TODO better formatting canOK = 0 canERR_PARAM = -1 canERR_NOMSG = -2 canERR_NOTFOUND = -3 canERR_NOMEM = -4 canERR_NOCHANNELS = -5 canERR_RESERVED_3 = -6 canERR_TIMEOUT = -7 canERR_NOTINITIALIZED = -8 canERR_NOHANDLES = -9 canERR_INVHANDLE = -10 canERR_INIFILE = -11 canERR_DRIVER = -12 canERR_TXBUFOFL = -13 canERR_RESERVED_1 = -14 canERR_HARDWARE = -15 canERR_DYNALOAD = -16 canERR_DYNALIB = -17 canERR_DYNAINIT = -18 canERR_NOT_SUPPORTED = -19 canERR_RESERVED_5 = -20 canERR_RESERVED_6 = -21 canERR_RESERVED_2 = -22 canERR_DRIVERLOAD = -23 canERR_DRIVERFAILED = -24 canERR_NOCONFIGMGR = -25 canERR_NOCARD = -26 canERR_RESERVED_7 = -27 canERR_REGISTRY = -28 canERR_LICENSE = -29 canERR_INTERNAL = -30 canERR_NO_ACCESS = -31 canERR_NOT_IMPLEMENTED = -32 canERR__RESERVED = -33 def CANSTATUS_SUCCESS(status): return status >= canOK canMSG_MASK = 0x00FF canMSG_RTR = 0x0001 canMSG_STD = 0x0002 canMSG_EXT = 0x0004 canMSG_WAKEUP = 0x0008 canMSG_NERR = 0x0010 canMSG_ERROR_FRAME = 0x0020 canMSG_TXACK = 0x0040 canMSG_TXRQ = 0x0080 canFDMSG_FDF = 0x010000 canFDMSG_BRS = 0x020000 canFDMSG_ESI = 0x040000 canMSGERR_MASK = 0xFF00 canMSGERR_HW_OVERRUN = 0x0200 canMSGERR_SW_OVERRUN = 0x0400 canMSGERR_STUFF = 0x0800 canMSGERR_FORM = 0x1000 canMSGERR_CRC = 0x2000 canMSGERR_BIT0 = 0x4000 canMSGERR_BIT1 = 0x8000 canMSGERR_OVERRUN = 0x0600 canMSGERR_BIT = 0xC000 canMSGERR_BUSERR = 0xF800 canTRANSCEIVER_LINEMODE_NA = 0 canTRANSCEIVER_LINEMODE_SWC_SLEEP = 4 canTRANSCEIVER_LINEMODE_SWC_NORMAL = 5 canTRANSCEIVER_LINEMODE_SWC_FAST = 6 canTRANSCEIVER_LINEMODE_SWC_WAKEUP = 7 canTRANSCEIVER_LINEMODE_SLEEP = 8 canTRANSCEIVER_LINEMODE_NORMAL = 9 canTRANSCEIVER_LINEMODE_STDBY = 10 canTRANSCEIVER_LINEMODE_TT_CAN_H = 11 canTRANSCEIVER_LINEMODE_TT_CAN_L = 12 canTRANSCEIVER_LINEMODE_OEM1 = 13 canTRANSCEIVER_LINEMODE_OEM2 = 14 canTRANSCEIVER_LINEMODE_OEM3 = 15 canTRANSCEIVER_LINEMODE_OEM4 = 16 canTRANSCEIVER_RESNET_NA = 0 canTRANSCEIVER_RESNET_MASTER = 1 canTRANSCEIVER_RESNET_MASTER_STBY = 2 canTRANSCEIVER_RESNET_SLAVE = 3 canTRANSCEIVER_TYPE_UNKNOWN = 0 canTRANSCEIVER_TYPE_251 = 1 canTRANSCEIVER_TYPE_252 = 2 canTRANSCEIVER_TYPE_DNOPTO = 3 canTRANSCEIVER_TYPE_W210 = 4 canTRANSCEIVER_TYPE_SWC_PROTO = 5 canTRANSCEIVER_TYPE_SWC = 6 canTRANSCEIVER_TYPE_EVA = 7 canTRANSCEIVER_TYPE_FIBER = 8 canTRANSCEIVER_TYPE_K251 = 9 canTRANSCEIVER_TYPE_K = 10 canTRANSCEIVER_TYPE_1054_OPTO = 11 canTRANSCEIVER_TYPE_SWC_OPTO = 12 canTRANSCEIVER_TYPE_TT = 13 canTRANSCEIVER_TYPE_1050 = 14 canTRANSCEIVER_TYPE_1050_OPTO = 15 canTRANSCEIVER_TYPE_1041 = 16 canTRANSCEIVER_TYPE_1041_OPTO = 17 canTRANSCEIVER_TYPE_RS485 = 18 canTRANSCEIVER_TYPE_LIN = 19 canTRANSCEIVER_TYPE_KONE = 20 canTRANSCEIVER_TYPE_LINX_LIN = 64 canTRANSCEIVER_TYPE_LINX_J1708 = 66 canTRANSCEIVER_TYPE_LINX_K = 68 canTRANSCEIVER_TYPE_LINX_SWC = 70 canTRANSCEIVER_TYPE_LINX_LS = 72 canTransceiverTypeStrings = { canTRANSCEIVER_TYPE_UNKNOWN: "unknown", canTRANSCEIVER_TYPE_251: "82C251", canTRANSCEIVER_TYPE_252: "82C252/TJA1053/TJA1054", canTRANSCEIVER_TYPE_DNOPTO: "Optoisolated 82C251", canTRANSCEIVER_TYPE_W210: "W210", canTRANSCEIVER_TYPE_SWC_PROTO: "AU5790 prototype", canTRANSCEIVER_TYPE_SWC: "AU5790", canTRANSCEIVER_TYPE_EVA: "EVA", canTRANSCEIVER_TYPE_FIBER: "82C251 with fibre extension", canTRANSCEIVER_TYPE_K251: "K251", canTRANSCEIVER_TYPE_K: "K", canTRANSCEIVER_TYPE_1054_OPTO: "TJA1054 optical isolation", canTRANSCEIVER_TYPE_SWC_OPTO: "AU5790 optical isolation", canTRANSCEIVER_TYPE_TT: "B10011S Truck-And-Trailer", canTRANSCEIVER_TYPE_1050: "TJA1050", canTRANSCEIVER_TYPE_1050_OPTO: "TJA1050 optical isolation", canTRANSCEIVER_TYPE_1041: "TJA1041", canTRANSCEIVER_TYPE_1041_OPTO: "TJA1041 optical isolation", canTRANSCEIVER_TYPE_RS485: "RS485", canTRANSCEIVER_TYPE_LIN: "LIN", canTRANSCEIVER_TYPE_KONE: "KONE", canTRANSCEIVER_TYPE_LINX_LIN: "LINX_LIN", canTRANSCEIVER_TYPE_LINX_J1708: "LINX_J1708", canTRANSCEIVER_TYPE_LINX_K: "LINX_K", canTRANSCEIVER_TYPE_LINX_SWC: "LINX_SWC", canTRANSCEIVER_TYPE_LINX_LS: "LINX_LS", } canDRIVER_NORMAL = 4 canDRIVER_SILENT = 1 canDRIVER_SELFRECEPTION = 8 canDRIVER_OFF = 0 canOPEN_EXCLUSIVE = 0x0008 canOPEN_REQUIRE_EXTENDED = 0x0010 canOPEN_ACCEPT_VIRTUAL = 0x0020 canOPEN_OVERRIDE_EXCLUSIVE = 0x0040 canOPEN_REQUIRE_INIT_ACCESS = 0x0080 canOPEN_NO_INIT_ACCESS = 0x0100 canOPEN_ACCEPT_LARGE_DLC = 0x0200 canOPEN_CAN_FD = 0x0400 canOPEN_CAN_FD_NONISO = 0x0800 canIOCTL_GET_RX_BUFFER_LEVEL = 8 canIOCTL_GET_TX_BUFFER_LEVEL = 9 canIOCTL_FLUSH_RX_BUFFER = 10 canIOCTL_FLUSH_TX_BUFFER = 11 canIOCTL_GET_TIMER_SCALE = 12 canIOCTL_SET_TXRQ = 13 canIOCTL_GET_EVENTHANDLE = 14 canIOCTL_SET_BYPASS_MODE = 15 canIOCTL_SET_WAKEUP = 16 canIOCTL_GET_DRIVERHANDLE = 17 canIOCTL_MAP_RXQUEUE = 18 canIOCTL_GET_WAKEUP = 19 canIOCTL_SET_REPORT_ACCESS_ERRORS = 20 canIOCTL_GET_REPORT_ACCESS_ERRORS = 21 canIOCTL_CONNECT_TO_VIRTUAL_BUS = 22 canIOCTL_DISCONNECT_FROM_VIRTUAL_BUS = 23 canIOCTL_SET_USER_IOPORT = 24 canIOCTL_GET_USER_IOPORT = 25 canIOCTL_SET_BUFFER_WRAPAROUND_MODE = 26 canIOCTL_SET_RX_QUEUE_SIZE = 27 canIOCTL_SET_USB_THROTTLE = 28 canIOCTL_GET_USB_THROTTLE = 29 canIOCTL_SET_BUSON_TIME_AUTO_RESET = 30 canIOCTL_SET_LOCAL_TXECHO = 32 canIOCTL_PREFER_EXT = 1 canIOCTL_PREFER_STD = 2 canIOCTL_CLEAR_ERROR_COUNTERS = 5 canIOCTL_SET_TIMER_SCALE = 6 canIOCTL_SET_TXACK = 7 canCHANNELDATA_CHANNEL_CAP = 1 canCHANNELDATA_TRANS_CAP = 2 canCHANNELDATA_CHANNEL_FLAGS = 3 canCHANNELDATA_CARD_TYPE = 4 canCHANNELDATA_CARD_NUMBER = 5 canCHANNELDATA_CHAN_NO_ON_CARD = 6 canCHANNELDATA_CARD_SERIAL_NO = 7 canCHANNELDATA_TRANS_SERIAL_NO = 8 canCHANNELDATA_CARD_FIRMWARE_REV = 9 canCHANNELDATA_CARD_HARDWARE_REV = 10 canCHANNELDATA_CARD_UPC_NO = 11 canCHANNELDATA_TRANS_UPC_NO = 12 canCHANNELDATA_CHANNEL_NAME = 13 canCHANNELDATA_DLL_FILE_VERSION = 14 canCHANNELDATA_DLL_PRODUCT_VERSION = 15 canCHANNELDATA_DLL_FILETYPE = 16 canCHANNELDATA_TRANS_TYPE = 17 canCHANNELDATA_DEVICE_PHYSICAL_POSITION = 18 canCHANNELDATA_UI_NUMBER = 19 canCHANNELDATA_TIMESYNC_ENABLED = 20 canCHANNELDATA_DRIVER_FILE_VERSION = 21 canCHANNELDATA_DRIVER_PRODUCT_VERSION = 22 canCHANNELDATA_MFGNAME_UNICODE = 23 canCHANNELDATA_MFGNAME_ASCII = 24 canCHANNELDATA_DEVDESCR_UNICODE = 25 canCHANNELDATA_DEVDESCR_ASCII = 26 canCHANNELDATA_DRIVER_NAME = 27 kvLED_ACTION_ALL_LEDS_ON = 0 kvLED_ACTION_ALL_LEDS_OFF = 1 canBITRATE_1M = -1 canBITRATE_500K = -2 canBITRATE_250K = -3 canBITRATE_125K = -4 canBITRATE_100K = -5 canBITRATE_62K = -6 canBITRATE_50K = -7 canBITRATE_83K = -8 canBITRATE_10K = -9 canFD_BITRATE_500K_80P = -1000 canFD_BITRATE_1M_80P = -1001 canFD_BITRATE_2M_80P = -1002 canFD_BITRATE_4M_80P = -1003 canFD_BITRATE_8M_60P = -1004 python-can-4.5.0/can/interfaces/kvaser/structures.py000066400000000000000000000036631472200326600225660ustar00rootroot00000000000000""" Contains Python equivalents of the structures in CANLIB's canlib.h, with some supporting functionality specific to Python. """ import ctypes class BusStatistics(ctypes.Structure): """This structure is used with the method :meth:`~can.interfaces.kvaser.canlib.KvaserBus.get_stats`. """ _fields_ = [ ("m_stdData", ctypes.c_ulong), ("m_stdRemote", ctypes.c_ulong), ("m_extData", ctypes.c_ulong), ("m_extRemote", ctypes.c_ulong), ("m_errFrame", ctypes.c_ulong), ("m_busLoad", ctypes.c_ulong), ("m_overruns", ctypes.c_ulong), ] def __str__(self): return ( f"std_data: {self.std_data}, " f"std_remote: {self.std_remote}, " f"ext_data: {self.ext_data}, " f"ext_remote: {self.ext_remote}, " f"err_frame: {self.err_frame}, " f"bus_load: {self.bus_load / 100.0:.1f}%, " f"overruns: {self.overruns}" ) @property def std_data(self): """Number of received standard (11-bit identifiers) data frames.""" return self.m_stdData @property def std_remote(self): """Number of received standard (11-bit identifiers) remote frames.""" return self.m_stdRemote @property def ext_data(self): """Number of received extended (29-bit identifiers) data frames.""" return self.m_extData @property def ext_remote(self): """Number of received extended (29-bit identifiers) remote frames.""" return self.m_extRemote @property def err_frame(self): """Number of error frames.""" return self.m_errFrame @property def bus_load(self): """The bus load, expressed as an integer in the interval 0 - 10000 representing 0.00% - 100.00% bus load.""" return self.m_busLoad @property def overruns(self): """Number of overruns.""" return self.m_overruns python-can-4.5.0/can/interfaces/neousys/000077500000000000000000000000001472200326600201735ustar00rootroot00000000000000python-can-4.5.0/can/interfaces/neousys/__init__.py000066400000000000000000000002061472200326600223020ustar00rootroot00000000000000""" Neousys CAN bus driver """ __all__ = [ "NeousysBus", "neousys", ] from can.interfaces.neousys.neousys import NeousysBus python-can-4.5.0/can/interfaces/neousys/neousys.py000066400000000000000000000170151472200326600222560ustar00rootroot00000000000000""" Neousys CAN bus driver """ # # This kind of interface can be found for example on Neousys POC-551VTC # One needs to have correct drivers and DLL (Share object for Linux) from Neousys # # https://www.neousys-tech.com/en/support-service/resources/category/299-poc-551vtc-driver # # Beware this is only tested on Linux kernel higher than v5.3. This should be drop in # with Windows but you have to replace with correct named DLL # # pylint: disable=too-few-public-methods # pylint: disable=too-many-instance-attributes # pylint: disable=wrong-import-position import logging import platform import queue from ctypes import ( CFUNCTYPE, POINTER, Structure, byref, c_ubyte, c_uint, c_ushort, sizeof, ) from time import time try: from ctypes import WinDLL except ImportError: from ctypes import CDLL from can import ( BusABC, CanInitializationError, CanInterfaceNotImplementedError, CanOperationError, CanProtocol, Message, ) logger = logging.getLogger(__name__) class NeousysCanSetup(Structure): """C CAN Setup struct""" _fields_ = [ ("bitRate", c_uint), ("recvConfig", c_uint), ("recvId", c_uint), ("recvMask", c_uint), ] class NeousysCanMsg(Structure): """C CAN Message struct""" _fields_ = [ ("id", c_uint), ("flags", c_ushort), ("extra", c_ubyte), ("len", c_ubyte), ("data", c_ubyte * 8), ] # valid:2~16, sum of the Synchronization, Propagation, and # Phase Buffer 1 segments, measured in time quanta. # valid:1~8, the Phase Buffer 2 segment in time quanta. # valid:1~4, Resynchronization Jump Width in time quanta # valid:1~1023, CAN_CLK divider used to determine time quanta class NeousysCanBitClk(Structure): """C CAN BIT Clock struct""" _fields_ = [ ("syncPropPhase1Seg", c_ushort), ("phase2Seg", c_ushort), ("jumpWidth", c_ushort), ("quantumPrescaler", c_ushort), ] NEOUSYS_CAN_MSG_CALLBACK = CFUNCTYPE(None, POINTER(NeousysCanMsg), c_uint) NEOUSYS_CAN_STATUS_CALLBACK = CFUNCTYPE(None, c_uint) NEOUSYS_CAN_MSG_EXTENDED_ID = 0x0004 NEOUSYS_CAN_MSG_REMOTE_FRAME = 0x0040 NEOUSYS_CAN_MSG_DATA_NEW = 0x0080 NEOUSYS_CAN_MSG_DATA_LOST = 0x0100 NEOUSYS_CAN_MSG_USE_ID_FILTER = 0x00000008 NEOUSYS_CAN_MSG_USE_DIR_FILTER = ( 0x00000010 | NEOUSYS_CAN_MSG_USE_ID_FILTER ) # only accept the direction specified in the message type NEOUSYS_CAN_MSG_USE_EXT_FILTER = ( 0x00000020 | NEOUSYS_CAN_MSG_USE_ID_FILTER ) # filters on only extended identifiers NEOUSYS_CAN_STATUS_BUS_OFF = 0x00000080 NEOUSYS_CAN_STATUS_EWARN = ( 0x00000040 # can controller error level has reached warning level. ) NEOUSYS_CAN_STATUS_EPASS = ( 0x00000020 # can controller error level has reached error passive level. ) NEOUSYS_CAN_STATUS_LEC_STUFF = 0x00000001 # a bit stuffing error has occurred. NEOUSYS_CAN_STATUS_LEC_FORM = 0x00000002 # a formatting error has occurred. NEOUSYS_CAN_STATUS_LEC_ACK = 0x00000003 # an acknowledge error has occurred. NEOUSYS_CAN_STATUS_LEC_BIT1 = ( 0x00000004 # the bus remained a bit level of 1 for longer than is allowed. ) NEOUSYS_CAN_STATUS_LEC_BIT0 = ( 0x00000005 # the bus remained a bit level of 0 for longer than is allowed. ) NEOUSYS_CAN_STATUS_LEC_CRC = 0x00000006 # a crc error has occurred. NEOUSYS_CAN_STATUS_LEC_MASK = ( 0x00000007 # this is the mask for the can last error code (lec). ) NEOUSYS_CANLIB = None try: if platform.system() == "Windows": NEOUSYS_CANLIB = WinDLL("./WDT_DIO.dll") else: NEOUSYS_CANLIB = CDLL("libwdt_dio.so") logger.info("Loaded Neousys WDT_DIO Can driver") except OSError as error: logger.info("Cannot load Neousys CAN bus dll or shared object: %s", error) class NeousysBus(BusABC): """Neousys CAN bus Class""" def __init__(self, channel, device=0, bitrate=500000, **kwargs): """ :param channel: channel number :param device: device number :param bitrate: bit rate. """ super().__init__(channel, **kwargs) if NEOUSYS_CANLIB is None: raise CanInterfaceNotImplementedError("Neousys WDT_DIO Can driver missing") self.channel = channel self.device = device self.channel_info = f"Neousys Can: device {self.device}, channel {self.channel}" self._can_protocol = CanProtocol.CAN_20 self.queue = queue.Queue() # Init with accept all and wanted bitrate self.init_config = NeousysCanSetup(bitrate, NEOUSYS_CAN_MSG_USE_ID_FILTER, 0, 0) self._neousys_recv_cb = NEOUSYS_CAN_MSG_CALLBACK(self._neousys_recv_cb) self._neousys_status_cb = NEOUSYS_CAN_STATUS_CALLBACK(self._neousys_status_cb) if NEOUSYS_CANLIB.CAN_RegisterReceived(0, self._neousys_recv_cb) == 0: raise CanInitializationError("Neousys CAN bus Setup receive callback") if NEOUSYS_CANLIB.CAN_RegisterStatus(0, self._neousys_status_cb) == 0: raise CanInitializationError("Neousys CAN bus Setup status callback") if ( NEOUSYS_CANLIB.CAN_Setup( channel, byref(self.init_config), sizeof(self.init_config) ) == 0 ): raise CanInitializationError("Neousys CAN bus Setup Error") if NEOUSYS_CANLIB.CAN_Start(channel) == 0: raise CanInitializationError("Neousys CAN bus Start Error") def send(self, msg, timeout=None) -> None: """ :param msg: message to send :param timeout: timeout is not used here """ tx_msg = NeousysCanMsg( msg.arbitration_id, 0, 0, msg.dlc, (c_ubyte * 8)(*msg.data) ) if NEOUSYS_CANLIB.CAN_Send(self.channel, byref(tx_msg), sizeof(tx_msg)) == 0: raise CanOperationError("Neousys Can can't send message") def _recv_internal(self, timeout): try: return self.queue.get(block=True, timeout=timeout), False except queue.Empty: return None, False def _neousys_recv_cb(self, msg, sizeof_msg) -> None: """ :param msg: struct CAN_MSG :param sizeof_msg: message number """ msg_bytes = bytearray(msg.contents.data) remote_frame = bool(msg.contents.flags & NEOUSYS_CAN_MSG_REMOTE_FRAME) extended_frame = bool(msg.contents.flags & NEOUSYS_CAN_MSG_EXTENDED_ID) if msg.contents.flags & NEOUSYS_CAN_MSG_DATA_LOST: logger.error("_neousys_recv_cb flag CAN_MSG_DATA_LOST") msg = Message( timestamp=time(), arbitration_id=msg.contents.id, is_remote_frame=remote_frame, is_extended_id=extended_frame, channel=self.channel, dlc=msg.contents.len, data=msg_bytes[: msg.contents.len], ) # Reading happens in Callback function and # with Python-CAN it happens polling # so cache stuff in array to for poll try: self.queue.put(msg) except queue.Full: raise CanOperationError("Neousys message Queue is full") from None def _neousys_status_cb(self, status) -> None: """ :param status: BUS Status """ logger.info("%s _neousys_status_cb: %d", self.init_config, status) def shutdown(self): super().shutdown() NEOUSYS_CANLIB.CAN_Stop(self.channel) @staticmethod def _detect_available_configs(): if NEOUSYS_CANLIB is None: return [] else: # There is only one channel return [{"interface": "neousys", "channel": 0}] python-can-4.5.0/can/interfaces/nican.py000066400000000000000000000264401472200326600201360ustar00rootroot00000000000000""" NI-CAN interface module. Implementation references: * http://www.ni.com/pdf/manuals/370289c.pdf * https://github.com/buendiya/NicanPython TODO: We could implement this interface such that setting other filters could work when the initial filters were set to zero using the software fallback. Or could the software filters even be changed after the connection was opened? We need to document that behaviour! See also the IXXAT interface. """ import ctypes import logging import sys from typing import Optional, Tuple, Type import can.typechecking from can import ( BusABC, CanError, CanInitializationError, CanInterfaceNotImplementedError, CanOperationError, CanProtocol, Message, ) logger = logging.getLogger(__name__) NC_SUCCESS = 0 NC_ERR_TIMEOUT = 1 TIMEOUT_ERROR_CODE = -1074388991 NC_DURATION_INFINITE = 0xFFFFFFFF NC_OP_START = 0x80000001 NC_OP_STOP = 0x80000002 NC_OP_RESET = 0x80000003 NC_FRMTYPE_REMOTE = 1 NC_FRMTYPE_COMM_ERR = 2 NC_ST_READ_AVAIL = 0x00000001 NC_ST_WRITE_SUCCESS = 0x00000002 NC_ST_ERROR = 0x00000010 NC_ST_WARNING = 0x00000020 NC_ATTR_BAUD_RATE = 0x80000007 NC_ATTR_START_ON_OPEN = 0x80000006 NC_ATTR_READ_Q_LEN = 0x80000013 NC_ATTR_WRITE_Q_LEN = 0x80000014 NC_ATTR_CAN_COMP_STD = 0x80010001 NC_ATTR_CAN_MASK_STD = 0x80010002 NC_ATTR_CAN_COMP_XTD = 0x80010003 NC_ATTR_CAN_MASK_XTD = 0x80010004 NC_ATTR_LOG_COMM_ERRS = 0x8001000A NC_FL_CAN_ARBID_XTD = 0x20000000 CanData = ctypes.c_ubyte * 8 class RxMessageStruct(ctypes.Structure): _pack_ = 1 _fields_ = [ ("timestamp", ctypes.c_ulonglong), ("arb_id", ctypes.c_ulong), ("frame_type", ctypes.c_ubyte), ("dlc", ctypes.c_ubyte), ("data", CanData), ] class TxMessageStruct(ctypes.Structure): _fields_ = [ ("arb_id", ctypes.c_ulong), ("is_remote", ctypes.c_ubyte), ("dlc", ctypes.c_ubyte), ("data", CanData), ] class NicanError(CanError): """Error from NI-CAN driver.""" def __init__(self, function, error_code: int, arguments) -> None: super().__init__( message=f"{function} failed: {get_error_message(error_code)}", error_code=error_code, ) #: Function that failed self.function = function #: Arguments passed to function self.arguments = arguments class NicanInitializationError(NicanError, CanInitializationError): pass class NicanOperationError(NicanError, CanOperationError): pass def check_status( result: int, function, arguments, error_class: Type[NicanError] = NicanOperationError, ) -> int: if result > 0: logger.warning(get_error_message(result)) elif result < 0: raise error_class(function, result, arguments) return result def check_status_init(*args, **kwargs) -> int: return check_status(*args, **kwargs, error_class=NicanInitializationError) def get_error_message(status_code: int) -> str: """Convert status code to descriptive string.""" errmsg = ctypes.create_string_buffer(1024) nican.ncStatusToString(status_code, len(errmsg), errmsg) return errmsg.value.decode("ascii") if sys.platform == "win32": try: nican = ctypes.windll.LoadLibrary("nican") except Exception as e: nican = None logger.error("Failed to load NI-CAN driver: %s", e) else: nican.ncConfig.argtypes = [ ctypes.c_char_p, ctypes.c_ulong, ctypes.c_void_p, ctypes.c_void_p, ] nican.ncConfig.errcheck = check_status_init nican.ncOpenObject.argtypes = [ctypes.c_char_p, ctypes.c_void_p] nican.ncOpenObject.errcheck = check_status_init nican.ncCloseObject.errcheck = check_status nican.ncAction.argtypes = [ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong] nican.ncAction.errcheck = check_status nican.ncRead.errcheck = check_status nican.ncWrite.errcheck = check_status nican.ncWaitForState.argtypes = [ ctypes.c_ulong, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_void_p, ] nican.ncWaitForState.errcheck = check_status nican.ncStatusToString.argtypes = [ctypes.c_int, ctypes.c_uint, ctypes.c_char_p] else: nican = None logger.warning("NI-CAN interface is only available on Windows systems") class NicanBus(BusABC): """ The CAN Bus implemented for the NI-CAN interface. .. warning:: This interface does implement efficient filtering of messages, but the filters have to be set in ``__init__`` using the ``can_filters`` parameter. Using :meth:`~can.BusABC.set_filters` does not work. """ def __init__( self, channel: str, can_filters: Optional[can.typechecking.CanFilters] = None, bitrate: Optional[int] = None, log_errors: bool = True, **kwargs, ) -> None: """ :param channel: Name of the object to open (e.g. `"CAN0"`) :param bitrate: Bitrate in bit/s :param can_filters: See :meth:`can.BusABC.set_filters`. :param log_errors: If True, communication errors will appear as CAN messages with ``is_error_frame`` set to True and ``arbitration_id`` will identify the error (default True) :raise ~can.exceptions.CanInterfaceNotImplementedError: If the current operating system is not supported or the driver could not be loaded. :raise ~can.interfaces.nican.NicanInitializationError: If the bus could not be set up. """ if nican is None: raise CanInterfaceNotImplementedError( "The NI-CAN driver could not be loaded. " "Check that you are using 32-bit Python on Windows." ) self.channel = channel self.channel_info = f"NI-CAN: {channel}" self._can_protocol = CanProtocol.CAN_20 channel_bytes = channel.encode("ascii") config = [(NC_ATTR_START_ON_OPEN, True), (NC_ATTR_LOG_COMM_ERRS, log_errors)] if not can_filters: logger.info("Filtering has been disabled") config.extend( [ (NC_ATTR_CAN_COMP_STD, 0), (NC_ATTR_CAN_MASK_STD, 0), (NC_ATTR_CAN_COMP_XTD, 0), (NC_ATTR_CAN_MASK_XTD, 0), ] ) else: for can_filter in can_filters: can_id = can_filter["can_id"] can_mask = can_filter["can_mask"] logger.info("Filtering on ID 0x%X, mask 0x%X", can_id, can_mask) if can_filter.get("extended"): config.extend( [ (NC_ATTR_CAN_COMP_XTD, can_id | NC_FL_CAN_ARBID_XTD), (NC_ATTR_CAN_MASK_XTD, can_mask), ] ) else: config.extend( [ (NC_ATTR_CAN_COMP_STD, can_id), (NC_ATTR_CAN_MASK_STD, can_mask), ] ) if bitrate: config.append((NC_ATTR_BAUD_RATE, bitrate)) AttrList = ctypes.c_ulong * len(config) attr_id_list = AttrList(*(row[0] for row in config)) attr_value_list = AttrList(*(row[1] for row in config)) nican.ncConfig( channel_bytes, len(config), ctypes.byref(attr_id_list), ctypes.byref(attr_value_list), ) self.handle = ctypes.c_ulong() nican.ncOpenObject(channel_bytes, ctypes.byref(self.handle)) super().__init__( channel=channel, can_filters=can_filters, bitrate=bitrate, log_errors=log_errors, **kwargs, ) def _recv_internal( self, timeout: Optional[float] ) -> Tuple[Optional[Message], bool]: """ Read a message from a NI-CAN bus. :param timeout: Max time to wait in seconds or ``None`` if infinite :raises can.interfaces.nican.NicanOperationError: If reception fails """ if timeout is None: timeout = NC_DURATION_INFINITE else: timeout = int(timeout * 1000) state = ctypes.c_ulong() try: nican.ncWaitForState( self.handle, NC_ST_READ_AVAIL, timeout, ctypes.byref(state) ) except NicanError as e: if e.error_code == TIMEOUT_ERROR_CODE: return None, True else: raise raw_msg = RxMessageStruct() nican.ncRead(self.handle, ctypes.sizeof(raw_msg), ctypes.byref(raw_msg)) # http://stackoverflow.com/questions/6161776/convert-windows-filetime-to-second-in-unix-linux timestamp = raw_msg.timestamp / 10000000.0 - 11644473600 is_remote_frame = raw_msg.frame_type == NC_FRMTYPE_REMOTE is_error_frame = raw_msg.frame_type == NC_FRMTYPE_COMM_ERR is_extended = bool(raw_msg.arb_id & NC_FL_CAN_ARBID_XTD) arb_id = raw_msg.arb_id if not is_error_frame: arb_id &= 0x1FFFFFFF dlc = raw_msg.dlc msg = Message( timestamp=timestamp, channel=self.channel, is_remote_frame=is_remote_frame, is_error_frame=is_error_frame, is_extended_id=is_extended, arbitration_id=arb_id, dlc=dlc, data=raw_msg.data[:dlc], ) return msg, True def send(self, msg: Message, timeout: Optional[float] = None) -> None: """ Send a message to NI-CAN. :param msg: Message to send :param timeout: The timeout .. warning:: This gets ignored. :raises can.interfaces.nican.NicanOperationError: If writing to transmit buffer fails. It does not wait for message to be ACKed currently. """ arb_id = msg.arbitration_id if msg.is_extended_id: arb_id |= NC_FL_CAN_ARBID_XTD raw_msg = TxMessageStruct( arb_id, bool(msg.is_remote_frame), msg.dlc, CanData(*msg.data) ) nican.ncWrite(self.handle, ctypes.sizeof(raw_msg), ctypes.byref(raw_msg)) # TODO: # ncWaitForState can not be called here if the recv() method is called # from a different thread, which is a very common use case. # Maybe it is possible to use ncCreateNotification instead but seems a # bit overkill at the moment. # state = ctypes.c_ulong() # nican.ncWaitForState(self.handle, NC_ST_WRITE_SUCCESS, int(timeout * 1000), ctypes.byref(state)) def reset(self) -> None: """ Resets network interface. Stops network interface, then resets the CAN chip to clear the CAN error counters (clear error passive state). Resetting includes clearing all entries from read and write queues. :raises can.interfaces.nican.NicanOperationError: If resetting fails. """ nican.ncAction(self.handle, NC_OP_RESET, 0) def shutdown(self) -> None: """Close object.""" super().shutdown() nican.ncCloseObject(self.handle) python-can-4.5.0/can/interfaces/nixnet.py000066400000000000000000000315731472200326600203560ustar00rootroot00000000000000""" NI-XNET interface module. Implementation references: NI-XNET Hardware and Software Manual: https://www.ni.com/pdf/manuals/372840h.pdf NI-XNET Python implementation: https://github.com/ni/nixnet-python Authors: Javier Rubio Giménez , Jose A. Escobar """ import logging import os import time import warnings from queue import SimpleQueue from types import ModuleType from typing import Any, List, Optional, Tuple, Union import can.typechecking from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message from can.exceptions import ( CanInitializationError, CanInterfaceNotImplementedError, CanOperationError, ) from can.util import check_or_adjust_timing_clock, deprecated_args_alias logger = logging.getLogger(__name__) nixnet: Optional[ModuleType] = None try: import nixnet # type: ignore import nixnet.constants # type: ignore import nixnet.system # type: ignore import nixnet.types # type: ignore except Exception as exc: logger.warning("Could not import nixnet: %s", exc) class NiXNETcanBus(BusABC): """ The CAN Bus implemented for the NI-XNET interface. """ @deprecated_args_alias( deprecation_start="4.2.0", deprecation_end="5.0.0", brs=None, log_errors=None, ) def __init__( self, channel: str = "CAN1", bitrate: int = 500_000, timing: Optional[Union[BitTiming, BitTimingFd]] = None, can_filters: Optional[can.typechecking.CanFilters] = None, receive_own_messages: bool = False, can_termination: bool = False, fd: bool = False, fd_bitrate: Optional[int] = None, poll_interval: float = 0.001, **kwargs: Any, ) -> None: """ :param str channel: Name of the object to open (e.g. 'CAN0') :param int bitrate: Bitrate in bits/s :param timing: Optional :class:`~can.BitTiming` or :class:`~can.BitTimingFd` instance to use for custom bit timing setting. The `f_clock` value of the timing instance must be set to 40_000_000 (40MHz). If this parameter is provided, it takes precedence over all other timing-related parameters like `bitrate`, `fd_bitrate` and `fd`. :param list can_filters: See :meth:`can.BusABC.set_filters`. :param receive_own_messages: Enable self-reception of sent messages. :param poll_interval: Poll interval in seconds. :raises ~can.exceptions.CanInitializationError: If starting communication fails """ if os.name != "nt" and not kwargs.get("_testing", False): raise CanInterfaceNotImplementedError( f"The NI-XNET interface is only supported on Windows, " f'but you are running "{os.name}"' ) if nixnet is None: raise CanInterfaceNotImplementedError("The NI-XNET API has not been loaded") self.nixnet = nixnet self._rx_queue = SimpleQueue() # type: ignore[var-annotated] self.channel = channel self.channel_info = "NI-XNET: " + channel self.poll_interval = poll_interval is_fd = isinstance(timing, BitTimingFd) if timing else fd self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20 # Set database for the initialization database_name = ":can_fd_brs:" if is_fd else ":memory:" try: # We need two sessions for this application, # one to send frames and another to receive them self._session_send = nixnet.session.FrameOutStreamSession( channel, database_name=database_name ) self._session_receive = nixnet.session.FrameInStreamSession( channel, database_name=database_name ) self._interface = self._session_send.intf # set interface properties self._interface.can_lstn_only = kwargs.get("listen_only", False) self._interface.echo_tx = receive_own_messages self._interface.bus_err_to_in_strm = True if isinstance(timing, BitTimingFd): timing = check_or_adjust_timing_clock(timing, [40_000_000]) custom_nom_baud_rate = ( # nxPropSession_IntfBaudRate64 0xA0000000 + (timing.nom_tq << 32) + (timing.nom_sjw - 1 << 16) + (timing.nom_tseg1 - 1 << 8) + (timing.nom_tseg2 - 1) ) custom_data_baud_rate = ( # nxPropSession_IntfCanFdBaudRate64 0xA0000000 + (timing.data_tq << 13) + (timing.data_tseg1 - 1 << 8) + (timing.data_tseg2 - 1 << 4) + (timing.data_sjw - 1) ) self._interface.baud_rate = custom_nom_baud_rate self._interface.can_fd_baud_rate = custom_data_baud_rate elif isinstance(timing, BitTiming): timing = check_or_adjust_timing_clock(timing, [40_000_000]) custom_baud_rate = ( # nxPropSession_IntfBaudRate64 0xA0000000 + (timing.tq << 32) + (timing.sjw - 1 << 16) + (timing.tseg1 - 1 << 8) + (timing.tseg2 - 1) ) self._interface.baud_rate = custom_baud_rate else: # See page 1017 of NI-XNET Hardware and Software Manual # to set custom can configuration if bitrate: self._interface.baud_rate = bitrate if is_fd: # See page 951 of NI-XNET Hardware and Software Manual # to set custom can configuration self._interface.can_fd_baud_rate = fd_bitrate or bitrate _can_termination = ( nixnet.constants.CanTerm.ON if can_termination else nixnet.constants.CanTerm.OFF ) self._interface.can_term = _can_termination # self._session_receive.queue_size = 512 # Once that all the parameters have been set, we start the sessions self._session_send.start() self._session_receive.start() except nixnet.errors.XnetError as error: raise CanInitializationError( f"{error.args[0]} ({error.error_type})", error.error_code ) from None self._is_filtered = False super().__init__( channel=channel, can_filters=can_filters, bitrate=bitrate, **kwargs, ) @property def fd(self) -> bool: class_name = self.__class__.__name__ warnings.warn( f"The {class_name}.fd property is deprecated and superseded by " f"{class_name}.protocol. It is scheduled for removal in python-can version 5.0.", DeprecationWarning, stacklevel=2, ) return self._can_protocol is CanProtocol.CAN_FD def _recv_internal( self, timeout: Optional[float] ) -> Tuple[Optional[Message], bool]: end_time = time.perf_counter() + timeout if timeout is not None else None while True: # try to read all available frames for frame in self._session_receive.frames.read(1024, timeout=0): self._rx_queue.put_nowait(frame) if self._rx_queue.qsize(): break # check for timeout if end_time is not None and time.perf_counter() > end_time: return None, False # Wait a short time until we try to read again time.sleep(self.poll_interval) can_frame = self._rx_queue.get_nowait() # Timestamp should be converted from raw frame format(100ns increment # from(12:00 a.m. January 1 1601 Coordinated Universal Time (UTC)) # to epoch time(number of seconds from January 1, 1970 (midnight UTC/GMT)) timestamp = can_frame.timestamp * 1e-7 - 11_644_473_600 if can_frame.type is self.nixnet.constants.FrameType.CAN_BUS_ERROR: msg = Message( timestamp=timestamp, channel=self.channel, is_error_frame=True, ) else: msg = Message( timestamp=timestamp, channel=self.channel, is_remote_frame=can_frame.type is self.nixnet.constants.FrameType.CAN_REMOTE, is_error_frame=False, is_fd=( can_frame.type is self.nixnet.constants.FrameType.CANFD_DATA or can_frame.type is self.nixnet.constants.FrameType.CANFDBRS_DATA ), bitrate_switch=( can_frame.type is self.nixnet.constants.FrameType.CANFDBRS_DATA ), is_extended_id=can_frame.identifier.extended, # Get identifier from CanIdentifier structure arbitration_id=can_frame.identifier.identifier, dlc=len(can_frame.payload), data=can_frame.payload, is_rx=not can_frame.echo, ) return msg, False def send(self, msg: Message, timeout: Optional[float] = None) -> None: """ Send a message using NI-XNET. :param can.Message msg: Message to send :param float timeout: Max time to wait for the device to be ready in seconds, None if time is infinite :raises can.exceptions.CanOperationError: If writing to transmit buffer fails. It does not wait for message to be ACKed currently. """ if timeout is None: timeout = self.nixnet.constants.TIMEOUT_INFINITE if msg.is_remote_frame: type_message = self.nixnet.constants.FrameType.CAN_REMOTE elif msg.is_error_frame: type_message = self.nixnet.constants.FrameType.CAN_BUS_ERROR elif msg.is_fd: if msg.bitrate_switch: type_message = self.nixnet.constants.FrameType.CANFDBRS_DATA else: type_message = self.nixnet.constants.FrameType.CANFD_DATA else: type_message = self.nixnet.constants.FrameType.CAN_DATA can_frame = self.nixnet.types.CanFrame( self.nixnet.types.CanIdentifier(msg.arbitration_id, msg.is_extended_id), type=type_message, payload=msg.data, ) try: self._session_send.frames.write([can_frame], timeout) except self.nixnet.errors.XnetError as error: raise CanOperationError( f"{error.args[0]} ({error.error_type})", error.error_code ) from None def reset(self) -> None: """ Resets network interface. Stops network interface, then resets the CAN chip to clear the CAN error counters (clear error passive state). Resetting includes clearing all entries from read and write queues. """ self._session_send.flush() self._session_receive.flush() self._session_send.stop() self._session_receive.stop() self._session_send.start() self._session_receive.start() def shutdown(self) -> None: """Close object.""" super().shutdown() if hasattr(self, "_session_send"): self._session_send.flush() self._session_send.stop() self._session_send.close() if hasattr(self, "_session_receive"): self._session_receive.flush() self._session_receive.stop() self._session_receive.close() @staticmethod def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: configs = [] try: with nixnet.system.System() as nixnet_system: # type: ignore[union-attr] for interface in nixnet_system.intf_refs_can: channel = str(interface) logger.debug( "Found channel index %d: %s", interface.port_num, channel ) configs.append( { "interface": "nixnet", "channel": channel, "can_term_available": interface.can_term_cap is nixnet.constants.CanTermCap.YES, # type: ignore[union-attr] "supports_fd": interface.can_tcvr_cap is nixnet.constants.CanTcvrCap.HS, # type: ignore[union-attr] } ) except Exception as error: logger.debug("An error occured while searching for configs: %s", str(error)) return configs # type: ignore python-can-4.5.0/can/interfaces/pcan/000077500000000000000000000000001472200326600174075ustar00rootroot00000000000000python-can-4.5.0/can/interfaces/pcan/__init__.py000066400000000000000000000002111472200326600215120ustar00rootroot00000000000000""" """ __all__ = [ "PcanBus", "PcanError", "basic", "pcan", ] from can.interfaces.pcan.pcan import PcanBus, PcanError python-can-4.5.0/can/interfaces/pcan/basic.py000066400000000000000000001205061472200326600210460ustar00rootroot00000000000000# PCANBasic.py # # ~~~~~~~~~~~~ # # PCAN-Basic API # # ~~~~~~~~~~~~ # # ------------------------------------------------------------------ # Author : Keneth Wagner # Last change: 2022-07-06 # ------------------------------------------------------------------ # # Copyright (C) 1999-2022 PEAK-System Technik GmbH, Darmstadt # more Info at http://www.peak-system.com # Module Imports import logging import platform from ctypes import * from ctypes.util import find_library PLATFORM = platform.system() IS_WINDOWS = PLATFORM == "Windows" IS_LINUX = PLATFORM == "Linux" logger = logging.getLogger("can.pcan") # /////////////////////////////////////////////////////////// # Type definitions # /////////////////////////////////////////////////////////// TPCANHandle = c_ushort # Represents a PCAN hardware channel handle TPCANStatus = int # Represents a PCAN status/error code TPCANParameter = c_ubyte # Represents a PCAN parameter to be read or set TPCANDevice = c_ubyte # Represents a PCAN device TPCANMessageType = c_ubyte # Represents the type of a PCAN message TPCANType = c_ubyte # Represents the type of PCAN hardware to be initialized TPCANMode = c_ubyte # Represents a PCAN filter mode TPCANBaudrate = c_ushort # Represents a PCAN Baud rate register value TPCANBitrateFD = c_char_p # Represents a PCAN-FD bit rate string TPCANTimestampFD = c_ulonglong # Represents a timestamp of a received PCAN FD message # /////////////////////////////////////////////////////////// # Value definitions # /////////////////////////////////////////////////////////// # Currently defined and supported PCAN channels # PCAN_NONEBUS = TPCANHandle(0x00) # Undefined/default value for a PCAN bus PCAN_ISABUS1 = TPCANHandle(0x21) # PCAN-ISA interface, channel 1 PCAN_ISABUS2 = TPCANHandle(0x22) # PCAN-ISA interface, channel 2 PCAN_ISABUS3 = TPCANHandle(0x23) # PCAN-ISA interface, channel 3 PCAN_ISABUS4 = TPCANHandle(0x24) # PCAN-ISA interface, channel 4 PCAN_ISABUS5 = TPCANHandle(0x25) # PCAN-ISA interface, channel 5 PCAN_ISABUS6 = TPCANHandle(0x26) # PCAN-ISA interface, channel 6 PCAN_ISABUS7 = TPCANHandle(0x27) # PCAN-ISA interface, channel 7 PCAN_ISABUS8 = TPCANHandle(0x28) # PCAN-ISA interface, channel 8 PCAN_DNGBUS1 = TPCANHandle(0x31) # PCAN-Dongle/LPT interface, channel 1 PCAN_PCIBUS1 = TPCANHandle(0x41) # PCAN-PCI interface, channel 1 PCAN_PCIBUS2 = TPCANHandle(0x42) # PCAN-PCI interface, channel 2 PCAN_PCIBUS3 = TPCANHandle(0x43) # PCAN-PCI interface, channel 3 PCAN_PCIBUS4 = TPCANHandle(0x44) # PCAN-PCI interface, channel 4 PCAN_PCIBUS5 = TPCANHandle(0x45) # PCAN-PCI interface, channel 5 PCAN_PCIBUS6 = TPCANHandle(0x46) # PCAN-PCI interface, channel 6 PCAN_PCIBUS7 = TPCANHandle(0x47) # PCAN-PCI interface, channel 7 PCAN_PCIBUS8 = TPCANHandle(0x48) # PCAN-PCI interface, channel 8 PCAN_PCIBUS9 = TPCANHandle(0x409) # PCAN-PCI interface, channel 9 PCAN_PCIBUS10 = TPCANHandle(0x40A) # PCAN-PCI interface, channel 10 PCAN_PCIBUS11 = TPCANHandle(0x40B) # PCAN-PCI interface, channel 11 PCAN_PCIBUS12 = TPCANHandle(0x40C) # PCAN-PCI interface, channel 12 PCAN_PCIBUS13 = TPCANHandle(0x40D) # PCAN-PCI interface, channel 13 PCAN_PCIBUS14 = TPCANHandle(0x40E) # PCAN-PCI interface, channel 14 PCAN_PCIBUS15 = TPCANHandle(0x40F) # PCAN-PCI interface, channel 15 PCAN_PCIBUS16 = TPCANHandle(0x410) # PCAN-PCI interface, channel 16 PCAN_USBBUS1 = TPCANHandle(0x51) # PCAN-USB interface, channel 1 PCAN_USBBUS2 = TPCANHandle(0x52) # PCAN-USB interface, channel 2 PCAN_USBBUS3 = TPCANHandle(0x53) # PCAN-USB interface, channel 3 PCAN_USBBUS4 = TPCANHandle(0x54) # PCAN-USB interface, channel 4 PCAN_USBBUS5 = TPCANHandle(0x55) # PCAN-USB interface, channel 5 PCAN_USBBUS6 = TPCANHandle(0x56) # PCAN-USB interface, channel 6 PCAN_USBBUS7 = TPCANHandle(0x57) # PCAN-USB interface, channel 7 PCAN_USBBUS8 = TPCANHandle(0x58) # PCAN-USB interface, channel 8 PCAN_USBBUS9 = TPCANHandle(0x509) # PCAN-USB interface, channel 9 PCAN_USBBUS10 = TPCANHandle(0x50A) # PCAN-USB interface, channel 10 PCAN_USBBUS11 = TPCANHandle(0x50B) # PCAN-USB interface, channel 11 PCAN_USBBUS12 = TPCANHandle(0x50C) # PCAN-USB interface, channel 12 PCAN_USBBUS13 = TPCANHandle(0x50D) # PCAN-USB interface, channel 13 PCAN_USBBUS14 = TPCANHandle(0x50E) # PCAN-USB interface, channel 14 PCAN_USBBUS15 = TPCANHandle(0x50F) # PCAN-USB interface, channel 15 PCAN_USBBUS16 = TPCANHandle(0x510) # PCAN-USB interface, channel 16 PCAN_PCCBUS1 = TPCANHandle(0x61) # PCAN-PC Card interface, channel 1 PCAN_PCCBUS2 = TPCANHandle(0x62) # PCAN-PC Card interface, channel 2 PCAN_LANBUS1 = TPCANHandle(0x801) # PCAN-LAN interface, channel 1 PCAN_LANBUS2 = TPCANHandle(0x802) # PCAN-LAN interface, channel 2 PCAN_LANBUS3 = TPCANHandle(0x803) # PCAN-LAN interface, channel 3 PCAN_LANBUS4 = TPCANHandle(0x804) # PCAN-LAN interface, channel 4 PCAN_LANBUS5 = TPCANHandle(0x805) # PCAN-LAN interface, channel 5 PCAN_LANBUS6 = TPCANHandle(0x806) # PCAN-LAN interface, channel 6 PCAN_LANBUS7 = TPCANHandle(0x807) # PCAN-LAN interface, channel 7 PCAN_LANBUS8 = TPCANHandle(0x808) # PCAN-LAN interface, channel 8 PCAN_LANBUS9 = TPCANHandle(0x809) # PCAN-LAN interface, channel 9 PCAN_LANBUS10 = TPCANHandle(0x80A) # PCAN-LAN interface, channel 10 PCAN_LANBUS11 = TPCANHandle(0x80B) # PCAN-LAN interface, channel 11 PCAN_LANBUS12 = TPCANHandle(0x80C) # PCAN-LAN interface, channel 12 PCAN_LANBUS13 = TPCANHandle(0x80D) # PCAN-LAN interface, channel 13 PCAN_LANBUS14 = TPCANHandle(0x80E) # PCAN-LAN interface, channel 14 PCAN_LANBUS15 = TPCANHandle(0x80F) # PCAN-LAN interface, channel 15 PCAN_LANBUS16 = TPCANHandle(0x810) # PCAN-LAN interface, channel 16 # Represent the PCAN error and status codes # PCAN_ERROR_OK = TPCANStatus(0x00000) # No error PCAN_ERROR_XMTFULL = TPCANStatus(0x00001) # Transmit buffer in CAN controller is full PCAN_ERROR_OVERRUN = TPCANStatus(0x00002) # CAN controller was read too late PCAN_ERROR_BUSLIGHT = TPCANStatus( 0x00004 ) # Bus error: an error counter reached the 'light' limit PCAN_ERROR_BUSHEAVY = TPCANStatus( 0x00008 ) # Bus error: an error counter reached the 'heavy' limit PCAN_ERROR_BUSWARNING = TPCANStatus( PCAN_ERROR_BUSHEAVY ) # Bus error: an error counter reached the 'warning' limit PCAN_ERROR_BUSPASSIVE = TPCANStatus( 0x40000 ) # Bus error: the CAN controller is error passive PCAN_ERROR_BUSOFF = TPCANStatus( 0x00010 ) # Bus error: the CAN controller is in bus-off state PCAN_ERROR_ANYBUSERR = TPCANStatus( PCAN_ERROR_BUSWARNING | PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY | PCAN_ERROR_BUSOFF | PCAN_ERROR_BUSPASSIVE ) # Mask for all bus errors PCAN_ERROR_QRCVEMPTY = TPCANStatus(0x00020) # Receive queue is empty PCAN_ERROR_QOVERRUN = TPCANStatus(0x00040) # Receive queue was read too late PCAN_ERROR_QXMTFULL = TPCANStatus(0x00080) # Transmit queue is full PCAN_ERROR_REGTEST = TPCANStatus( 0x00100 ) # Test of the CAN controller hardware registers failed (no hardware found) PCAN_ERROR_NODRIVER = TPCANStatus(0x00200) # Driver not loaded PCAN_ERROR_HWINUSE = TPCANStatus(0x00400) # Hardware already in use by a Net PCAN_ERROR_NETINUSE = TPCANStatus(0x00800) # A Client is already connected to the Net PCAN_ERROR_ILLHW = TPCANStatus(0x01400) # Hardware handle is invalid PCAN_ERROR_ILLNET = TPCANStatus(0x01800) # Net handle is invalid PCAN_ERROR_ILLCLIENT = TPCANStatus(0x01C00) # Client handle is invalid PCAN_ERROR_ILLHANDLE = TPCANStatus( PCAN_ERROR_ILLHW | PCAN_ERROR_ILLNET | PCAN_ERROR_ILLCLIENT ) # Mask for all handle errors PCAN_ERROR_RESOURCE = TPCANStatus( 0x02000 ) # Resource (FIFO, Client, timeout) cannot be created PCAN_ERROR_ILLPARAMTYPE = TPCANStatus(0x04000) # Invalid parameter PCAN_ERROR_ILLPARAMVAL = TPCANStatus(0x08000) # Invalid parameter value PCAN_ERROR_UNKNOWN = TPCANStatus(0x10000) # Unknown error PCAN_ERROR_ILLDATA = TPCANStatus(0x20000) # Invalid data, function, or action PCAN_ERROR_ILLMODE = TPCANStatus( 0x80000 ) # Driver object state is wrong for the attempted operation PCAN_ERROR_CAUTION = TPCANStatus( 0x2000000 ) # An operation was successfully carried out, however, irregularities were registered PCAN_ERROR_INITIALIZE = TPCANStatus( 0x4000000 ) # Channel is not initialized [Value was changed from 0x40000 to 0x4000000] PCAN_ERROR_ILLOPERATION = TPCANStatus( 0x8000000 ) # Invalid operation [Value was changed from 0x80000 to 0x8000000] # PCAN devices # PCAN_NONE = TPCANDevice(0x00) # Undefined, unknown or not selected PCAN device value PCAN_PEAKCAN = TPCANDevice(0x01) # PCAN Non-PnP devices. NOT USED WITHIN PCAN-Basic API PCAN_ISA = TPCANDevice(0x02) # PCAN-ISA, PCAN-PC/104, and PCAN-PC/104-Plus PCAN_DNG = TPCANDevice(0x03) # PCAN-Dongle PCAN_PCI = TPCANDevice(0x04) # PCAN-PCI, PCAN-cPCI, PCAN-miniPCI, and PCAN-PCI Express PCAN_USB = TPCANDevice(0x05) # PCAN-USB and PCAN-USB Pro PCAN_PCC = TPCANDevice(0x06) # PCAN-PC Card PCAN_VIRTUAL = TPCANDevice( 0x07 ) # PCAN Virtual hardware. NOT USED WITHIN PCAN-Basic API PCAN_LAN = TPCANDevice(0x08) # PCAN Gateway devices # PCAN parameters # PCAN_DEVICE_ID = TPCANParameter(0x01) # Device identifier parameter PCAN_5VOLTS_POWER = TPCANParameter(0x02) # 5-Volt power parameter PCAN_RECEIVE_EVENT = TPCANParameter(0x03) # PCAN receive event handler parameter PCAN_MESSAGE_FILTER = TPCANParameter(0x04) # PCAN message filter parameter PCAN_API_VERSION = TPCANParameter(0x05) # PCAN-Basic API version parameter PCAN_CHANNEL_VERSION = TPCANParameter(0x06) # PCAN device channel version parameter PCAN_BUSOFF_AUTORESET = TPCANParameter(0x07) # PCAN Reset-On-Busoff parameter PCAN_LISTEN_ONLY = TPCANParameter(0x08) # PCAN Listen-Only parameter PCAN_LOG_LOCATION = TPCANParameter(0x09) # Directory path for log files PCAN_LOG_STATUS = TPCANParameter(0x0A) # Debug-Log activation status PCAN_LOG_CONFIGURE = TPCANParameter( 0x0B ) # Configuration of the debugged information (LOG_FUNCTION_***) PCAN_LOG_TEXT = TPCANParameter(0x0C) # Custom insertion of text into the log file PCAN_CHANNEL_CONDITION = TPCANParameter(0x0D) # Availability status of a PCAN-Channel PCAN_HARDWARE_NAME = TPCANParameter(0x0E) # PCAN hardware name parameter PCAN_RECEIVE_STATUS = TPCANParameter(0x0F) # Message reception status of a PCAN-Channel PCAN_CONTROLLER_NUMBER = TPCANParameter(0x10) # CAN-Controller number of a PCAN-Channel PCAN_TRACE_LOCATION = TPCANParameter(0x11) # Directory path for PCAN trace files PCAN_TRACE_STATUS = TPCANParameter(0x12) # CAN tracing activation status PCAN_TRACE_SIZE = TPCANParameter( 0x13 ) # Configuration of the maximum file size of a CAN trace PCAN_TRACE_CONFIGURE = TPCANParameter( 0x14 ) # Configuration of the trace file storing mode (TRACE_FILE_***) PCAN_CHANNEL_IDENTIFYING = TPCANParameter( 0x15 ) # Physical identification of a USB based PCAN-Channel by blinking its associated LED PCAN_CHANNEL_FEATURES = TPCANParameter( 0x16 ) # Capabilities of a PCAN device (FEATURE_***) PCAN_BITRATE_ADAPTING = TPCANParameter( 0x17 ) # Using of an existing bit rate (PCAN-View connected to a channel) PCAN_BITRATE_INFO = TPCANParameter(0x18) # Configured bit rate as Btr0Btr1 value PCAN_BITRATE_INFO_FD = TPCANParameter( 0x19 ) # Configured bit rate as TPCANBitrateFD string PCAN_BUSSPEED_NOMINAL = TPCANParameter( 0x1A ) # Configured nominal CAN Bus speed as Bits per seconds PCAN_BUSSPEED_DATA = TPCANParameter( 0x1B ) # Configured CAN data speed as Bits per seconds PCAN_IP_ADDRESS = TPCANParameter( 0x1C ) # Remote address of a LAN channel as string in IPv4 format PCAN_LAN_SERVICE_STATUS = TPCANParameter( 0x1D ) # Status of the Virtual PCAN-Gateway Service PCAN_ALLOW_STATUS_FRAMES = TPCANParameter( 0x1E ) # Status messages reception status within a PCAN-Channel PCAN_ALLOW_RTR_FRAMES = TPCANParameter( 0x1F ) # RTR messages reception status within a PCAN-Channel PCAN_ALLOW_ERROR_FRAMES = TPCANParameter( 0x20 ) # Error messages reception status within a PCAN-Channel PCAN_INTERFRAME_DELAY = TPCANParameter( 0x21 ) # Delay, in microseconds, between sending frames PCAN_ACCEPTANCE_FILTER_11BIT = TPCANParameter( 0x22 ) # Filter over code and mask patterns for 11-Bit messages PCAN_ACCEPTANCE_FILTER_29BIT = TPCANParameter( 0x23 ) # Filter over code and mask patterns for 29-Bit messages PCAN_IO_DIGITAL_CONFIGURATION = TPCANParameter( 0x24 ) # Output mode of 32 digital I/O pin of a PCAN-USB Chip. 1: Output-Active 0 : Output Inactive PCAN_IO_DIGITAL_VALUE = TPCANParameter( 0x25 ) # Value assigned to a 32 digital I/O pins of a PCAN-USB Chip PCAN_IO_DIGITAL_SET = TPCANParameter( 0x26 ) # Value assigned to a 32 digital I/O pins of a PCAN-USB Chip - Multiple digital I/O pins to 1 = High PCAN_IO_DIGITAL_CLEAR = TPCANParameter(0x27) # Clear multiple digital I/O pins to 0 PCAN_IO_ANALOG_VALUE = TPCANParameter(0x28) # Get value of a single analog input pin PCAN_FIRMWARE_VERSION = TPCANParameter( 0x29 ) # Get the version of the firmware used by the device associated with a PCAN-Channel PCAN_ATTACHED_CHANNELS_COUNT = TPCANParameter( 0x2A ) # Get the amount of PCAN channels attached to a system PCAN_ATTACHED_CHANNELS = TPCANParameter( 0x2B ) # Get information about PCAN channels attached to a system PCAN_ALLOW_ECHO_FRAMES = TPCANParameter( 0x2C ) # Echo messages reception status within a PCAN-Channel PCAN_DEVICE_PART_NUMBER = TPCANParameter( 0x2D ) # Get the part number associated to a device # DEPRECATED parameters # PCAN_DEVICE_NUMBER = PCAN_DEVICE_ID # DEPRECATED. Use PCAN_DEVICE_ID instead # PCAN parameter values # PCAN_PARAMETER_OFF = 0x00 # The PCAN parameter is not set (inactive) PCAN_PARAMETER_ON = 0x01 # The PCAN parameter is set (active) PCAN_FILTER_CLOSE = 0x00 # The PCAN filter is closed. No messages will be received PCAN_FILTER_OPEN = ( 0x01 # The PCAN filter is fully opened. All messages will be received ) PCAN_FILTER_CUSTOM = 0x02 # The PCAN filter is custom configured. Only registered messages will be received PCAN_CHANNEL_UNAVAILABLE = 0x00 # The PCAN-Channel handle is illegal, or its associated hardware is not available PCAN_CHANNEL_AVAILABLE = 0x01 # The PCAN-Channel handle is available to be connected (PnP Hardware: it means furthermore that the hardware is plugged-in) PCAN_CHANNEL_OCCUPIED = ( 0x02 # The PCAN-Channel handle is valid, and is already being used ) PCAN_CHANNEL_PCANVIEW = ( PCAN_CHANNEL_AVAILABLE | PCAN_CHANNEL_OCCUPIED ) # The PCAN-Channel handle is already being used by a PCAN-View application, but is available to connect LOG_FUNCTION_DEFAULT = 0x00 # Logs system exceptions / errors LOG_FUNCTION_ENTRY = 0x01 # Logs the entries to the PCAN-Basic API functions LOG_FUNCTION_PARAMETERS = ( 0x02 # Logs the parameters passed to the PCAN-Basic API functions ) LOG_FUNCTION_LEAVE = 0x04 # Logs the exits from the PCAN-Basic API functions LOG_FUNCTION_WRITE = 0x08 # Logs the CAN messages passed to the CAN_Write function LOG_FUNCTION_READ = 0x10 # Logs the CAN messages received within the CAN_Read function LOG_FUNCTION_ALL = ( 0xFFFF # Logs all possible information within the PCAN-Basic API functions ) TRACE_FILE_SINGLE = ( 0x00 # A single file is written until it size reaches PAN_TRACE_SIZE ) TRACE_FILE_SEGMENTED = ( 0x01 # Traced data is distributed in several files with size PAN_TRACE_SIZE ) TRACE_FILE_DATE = 0x02 # Includes the date into the name of the trace file TRACE_FILE_TIME = 0x04 # Includes the start time into the name of the trace file TRACE_FILE_OVERWRITE = 0x80 # Causes the overwriting of available traces (same name) FEATURE_FD_CAPABLE = 0x01 # Device supports flexible data-rate (CAN-FD) FEATURE_DELAY_CAPABLE = ( 0x02 # Device supports a delay between sending frames (FPGA based USB devices) ) FEATURE_IO_CAPABLE = ( 0x04 # Device supports I/O functionality for electronic circuits (USB-Chip devices) ) SERVICE_STATUS_STOPPED = 0x01 # The service is not running SERVICE_STATUS_RUNNING = 0x04 # The service is running # Other constants # MAX_LENGTH_HARDWARE_NAME = ( 33 # Maximum length of the name of a device: 32 characters + terminator ) MAX_LENGTH_VERSION_STRING = ( 256 # Maximum length of a version string: 255 characters + terminator ) # PCAN message types # PCAN_MESSAGE_STANDARD = TPCANMessageType( 0x00 ) # The PCAN message is a CAN Standard Frame (11-bit identifier) PCAN_MESSAGE_RTR = TPCANMessageType( 0x01 ) # The PCAN message is a CAN Remote-Transfer-Request Frame PCAN_MESSAGE_EXTENDED = TPCANMessageType( 0x02 ) # The PCAN message is a CAN Extended Frame (29-bit identifier) PCAN_MESSAGE_FD = TPCANMessageType( 0x04 ) # The PCAN message represents a FD frame in terms of CiA Specs PCAN_MESSAGE_BRS = TPCANMessageType( 0x08 ) # The PCAN message represents a FD bit rate switch (CAN data at a higher bit rate) PCAN_MESSAGE_ESI = TPCANMessageType( 0x10 ) # The PCAN message represents a FD error state indicator(CAN FD transmitter was error active) PCAN_MESSAGE_ECHO = TPCANMessageType( 0x20 ) # The PCAN message represents an echo CAN Frame PCAN_MESSAGE_ERRFRAME = TPCANMessageType( 0x40 ) # The PCAN message represents an error frame PCAN_MESSAGE_STATUS = TPCANMessageType( 0x80 ) # The PCAN message represents a PCAN status message # LookUp Parameters # LOOKUP_DEVICE_TYPE = ( b"devicetype" # Lookup channel by Device type (see PCAN devices e.g. PCAN_USB) ) LOOKUP_DEVICE_ID = b"deviceid" # Lookup channel by device id LOOKUP_CONTROLLER_NUMBER = ( b"controllernumber" # Lookup channel by CAN controller 0-based index ) LOOKUP_IP_ADDRESS = b"ipaddress" # Lookup channel by IP address (LAN channels only) # Frame Type / Initialization Mode # PCAN_MODE_STANDARD = PCAN_MESSAGE_STANDARD PCAN_MODE_EXTENDED = PCAN_MESSAGE_EXTENDED # Baud rate codes = BTR0/BTR1 register values for the CAN controller. # You can define your own Baud rate with the BTROBTR1 register. # Take a look at www.peak-system.com for our free software "BAUDTOOL" # to calculate the BTROBTR1 register for every bit rate and sample point. # PCAN_BAUD_1M = TPCANBaudrate(0x0014) # 1 MBit/s PCAN_BAUD_800K = TPCANBaudrate(0x0016) # 800 kBit/s PCAN_BAUD_500K = TPCANBaudrate(0x001C) # 500 kBit/s PCAN_BAUD_250K = TPCANBaudrate(0x011C) # 250 kBit/s PCAN_BAUD_125K = TPCANBaudrate(0x031C) # 125 kBit/s PCAN_BAUD_100K = TPCANBaudrate(0x432F) # 100 kBit/s PCAN_BAUD_95K = TPCANBaudrate(0xC34E) # 95,238 kBit/s PCAN_BAUD_83K = TPCANBaudrate(0x852B) # 83,333 kBit/s PCAN_BAUD_50K = TPCANBaudrate(0x472F) # 50 kBit/s PCAN_BAUD_47K = TPCANBaudrate(0x1414) # 47,619 kBit/s PCAN_BAUD_33K = TPCANBaudrate(0x8B2F) # 33,333 kBit/s PCAN_BAUD_20K = TPCANBaudrate(0x532F) # 20 kBit/s PCAN_BAUD_10K = TPCANBaudrate(0x672F) # 10 kBit/s PCAN_BAUD_5K = TPCANBaudrate(0x7F7F) # 5 kBit/s # Represents the configuration for a CAN bit rate # Note: # * Each parameter and its value must be separated with a '='. # * Each pair of parameter/value must be separated using ','. # # Example: # f_clock=80000000,nom_brp=10,nom_tseg1=5,nom_tseg2=2,nom_sjw=1,data_brp=4,data_tseg1=7,data_tseg2=2,data_sjw=1 # PCAN_BR_CLOCK = TPCANBitrateFD(b"f_clock") PCAN_BR_CLOCK_MHZ = TPCANBitrateFD(b"f_clock_mhz") PCAN_BR_NOM_BRP = TPCANBitrateFD(b"nom_brp") PCAN_BR_NOM_TSEG1 = TPCANBitrateFD(b"nom_tseg1") PCAN_BR_NOM_TSEG2 = TPCANBitrateFD(b"nom_tseg2") PCAN_BR_NOM_SJW = TPCANBitrateFD(b"nom_sjw") PCAN_BR_NOM_SAMPLE = TPCANBitrateFD(b"nom_sam") PCAN_BR_DATA_BRP = TPCANBitrateFD(b"data_brp") PCAN_BR_DATA_TSEG1 = TPCANBitrateFD(b"data_tseg1") PCAN_BR_DATA_TSEG2 = TPCANBitrateFD(b"data_tseg2") PCAN_BR_DATA_SJW = TPCANBitrateFD(b"data_sjw") PCAN_BR_DATA_SAMPLE = TPCANBitrateFD(b"data_ssp_offset") # Supported Non-PnP Hardware types # PCAN_TYPE_ISA = TPCANType(0x01) # PCAN-ISA 82C200 PCAN_TYPE_ISA_SJA = TPCANType(0x09) # PCAN-ISA SJA1000 PCAN_TYPE_ISA_PHYTEC = TPCANType(0x04) # PHYTEC ISA PCAN_TYPE_DNG = TPCANType(0x02) # PCAN-Dongle 82C200 PCAN_TYPE_DNG_EPP = TPCANType(0x03) # PCAN-Dongle EPP 82C200 PCAN_TYPE_DNG_SJA = TPCANType(0x05) # PCAN-Dongle SJA1000 PCAN_TYPE_DNG_SJA_EPP = TPCANType(0x06) # PCAN-Dongle EPP SJA1000 # string description of the error codes PCAN_DICT_STATUS = { PCAN_ERROR_OK: "OK", PCAN_ERROR_XMTFULL: "XMTFULL", PCAN_ERROR_OVERRUN: "OVERRUN", PCAN_ERROR_BUSLIGHT: "BUSLIGHT", PCAN_ERROR_BUSHEAVY: "BUSHEAVY", PCAN_ERROR_BUSWARNING: "BUSWARNING", PCAN_ERROR_BUSPASSIVE: "BUSPASSIVE", PCAN_ERROR_BUSOFF: "BUSOFF", PCAN_ERROR_ANYBUSERR: "ANYBUSERR", PCAN_ERROR_QRCVEMPTY: "QRCVEMPTY", PCAN_ERROR_QOVERRUN: "QOVERRUN", PCAN_ERROR_QXMTFULL: "QXMTFULL", PCAN_ERROR_REGTEST: "ERR_REGTEST", PCAN_ERROR_NODRIVER: "NODRIVER", PCAN_ERROR_HWINUSE: "HWINUSE", PCAN_ERROR_NETINUSE: "NETINUSE", PCAN_ERROR_ILLHW: "ILLHW", PCAN_ERROR_ILLNET: "ILLNET", PCAN_ERROR_ILLCLIENT: "ILLCLIENT", PCAN_ERROR_ILLHANDLE: "ILLHANDLE", PCAN_ERROR_RESOURCE: "ERR_RESOURCE", PCAN_ERROR_ILLPARAMTYPE: "ILLPARAMTYPE", PCAN_ERROR_ILLPARAMVAL: "ILLPARAMVAL", PCAN_ERROR_UNKNOWN: "UNKNOWN", PCAN_ERROR_ILLDATA: "ILLDATA", PCAN_ERROR_CAUTION: "CAUTION", PCAN_ERROR_INITIALIZE: "ERR_INITIALIZE", PCAN_ERROR_ILLOPERATION: "ILLOPERATION", } # Represents a PCAN message # class TPCANMsg(Structure): """ Represents a PCAN message """ _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier ("MSGTYPE", TPCANMessageType), # Type of the message ("LEN", c_ubyte), # Data Length Code of the message (0..8) ("DATA", c_ubyte * 8), ] # Data of the message (DATA[0]..DATA[7]) # Represents a timestamp of a received PCAN message # Total Microseconds = micros + 1000 * millis + 0x100000000 * 1000 * millis_overflow # class TPCANTimestamp(Structure): """ Represents a timestamp of a received PCAN message Total Microseconds = micros + 1000 * millis + 0x100000000 * 1000 * millis_overflow """ _fields_ = [ ("millis", c_uint), # Base-value: milliseconds: 0.. 2^32-1 ("millis_overflow", c_ushort), # Roll-arounds of millis ("micros", c_ushort), ] # Microseconds: 0..999 # Represents a PCAN message from a FD capable hardware # class TPCANMsgFD(Structure): """ Represents a PCAN message """ _fields_ = [ ("ID", c_uint), # 11/29-bit message identifier ("MSGTYPE", TPCANMessageType), # Type of the message ("DLC", c_ubyte), # Data Length Code of the message (0..15) ("DATA", c_ubyte * 64), ] # Data of the message (DATA[0]..DATA[63]) # Describes an available PCAN channel # class TPCANChannelInformation(Structure): """ Describes an available PCAN channel """ _fields_ = [ ("channel_handle", TPCANHandle), # PCAN channel handle ("device_type", TPCANDevice), # Kind of PCAN device ("controller_number", c_ubyte), # CAN-Controller number ("device_features", c_uint), # Device capabilities flag (see FEATURE_*) ("device_name", c_char * MAX_LENGTH_HARDWARE_NAME), # Device name ("device_id", c_uint), # Device number ("channel_condition", c_uint), ] # Availability status of a PCAN-Channel # /////////////////////////////////////////////////////////// # Additional objects # /////////////////////////////////////////////////////////// PCAN_BITRATES = { 1000000: PCAN_BAUD_1M, 800000: PCAN_BAUD_800K, 500000: PCAN_BAUD_500K, 250000: PCAN_BAUD_250K, 125000: PCAN_BAUD_125K, 100000: PCAN_BAUD_100K, 95000: PCAN_BAUD_95K, 83000: PCAN_BAUD_83K, 50000: PCAN_BAUD_50K, 47000: PCAN_BAUD_47K, 33000: PCAN_BAUD_33K, 20000: PCAN_BAUD_20K, 10000: PCAN_BAUD_10K, 5000: PCAN_BAUD_5K, } PCAN_FD_PARAMETER_LIST = ( "nom_brp", "nom_tseg1", "nom_tseg2", "nom_sjw", "data_brp", "data_tseg1", "data_tseg2", "data_sjw", ) PCAN_CHANNEL_NAMES = { "PCAN_NONEBUS": PCAN_NONEBUS, "PCAN_ISABUS1": PCAN_ISABUS1, "PCAN_ISABUS2": PCAN_ISABUS2, "PCAN_ISABUS3": PCAN_ISABUS3, "PCAN_ISABUS4": PCAN_ISABUS4, "PCAN_ISABUS5": PCAN_ISABUS5, "PCAN_ISABUS6": PCAN_ISABUS6, "PCAN_ISABUS7": PCAN_ISABUS7, "PCAN_ISABUS8": PCAN_ISABUS8, "PCAN_DNGBUS1": PCAN_DNGBUS1, "PCAN_PCIBUS1": PCAN_PCIBUS1, "PCAN_PCIBUS2": PCAN_PCIBUS2, "PCAN_PCIBUS3": PCAN_PCIBUS3, "PCAN_PCIBUS4": PCAN_PCIBUS4, "PCAN_PCIBUS5": PCAN_PCIBUS5, "PCAN_PCIBUS6": PCAN_PCIBUS6, "PCAN_PCIBUS7": PCAN_PCIBUS7, "PCAN_PCIBUS8": PCAN_PCIBUS8, "PCAN_PCIBUS9": PCAN_PCIBUS9, "PCAN_PCIBUS10": PCAN_PCIBUS10, "PCAN_PCIBUS11": PCAN_PCIBUS11, "PCAN_PCIBUS12": PCAN_PCIBUS12, "PCAN_PCIBUS13": PCAN_PCIBUS13, "PCAN_PCIBUS14": PCAN_PCIBUS14, "PCAN_PCIBUS15": PCAN_PCIBUS15, "PCAN_PCIBUS16": PCAN_PCIBUS16, "PCAN_USBBUS1": PCAN_USBBUS1, "PCAN_USBBUS2": PCAN_USBBUS2, "PCAN_USBBUS3": PCAN_USBBUS3, "PCAN_USBBUS4": PCAN_USBBUS4, "PCAN_USBBUS5": PCAN_USBBUS5, "PCAN_USBBUS6": PCAN_USBBUS6, "PCAN_USBBUS7": PCAN_USBBUS7, "PCAN_USBBUS8": PCAN_USBBUS8, "PCAN_USBBUS9": PCAN_USBBUS9, "PCAN_USBBUS10": PCAN_USBBUS10, "PCAN_USBBUS11": PCAN_USBBUS11, "PCAN_USBBUS12": PCAN_USBBUS12, "PCAN_USBBUS13": PCAN_USBBUS13, "PCAN_USBBUS14": PCAN_USBBUS14, "PCAN_USBBUS15": PCAN_USBBUS15, "PCAN_USBBUS16": PCAN_USBBUS16, "PCAN_PCCBUS1": PCAN_PCCBUS1, "PCAN_PCCBUS2": PCAN_PCCBUS2, "PCAN_LANBUS1": PCAN_LANBUS1, "PCAN_LANBUS2": PCAN_LANBUS2, "PCAN_LANBUS3": PCAN_LANBUS3, "PCAN_LANBUS4": PCAN_LANBUS4, "PCAN_LANBUS5": PCAN_LANBUS5, "PCAN_LANBUS6": PCAN_LANBUS6, "PCAN_LANBUS7": PCAN_LANBUS7, "PCAN_LANBUS8": PCAN_LANBUS8, "PCAN_LANBUS9": PCAN_LANBUS9, "PCAN_LANBUS10": PCAN_LANBUS10, "PCAN_LANBUS11": PCAN_LANBUS11, "PCAN_LANBUS12": PCAN_LANBUS12, "PCAN_LANBUS13": PCAN_LANBUS13, "PCAN_LANBUS14": PCAN_LANBUS14, "PCAN_LANBUS15": PCAN_LANBUS15, "PCAN_LANBUS16": PCAN_LANBUS16, } VALID_PCAN_CAN_CLOCKS = [8_000_000] VALID_PCAN_FD_CLOCKS = [ 20_000_000, 24_000_000, 30_000_000, 40_000_000, 60_000_000, 80_000_000, ] # /////////////////////////////////////////////////////////// # PCAN-Basic API function declarations # /////////////////////////////////////////////////////////// # PCAN-Basic API class implementation # class PCANBasic: """PCAN-Basic API class implementation""" def __init__(self): if platform.system() == "Windows": load_library_func = windll.LoadLibrary else: load_library_func = cdll.LoadLibrary if platform.system() == "Windows" or "CYGWIN" in platform.system(): lib_name = "PCANBasic" elif platform.system() == "Darwin": # PCBUSB library is a third-party software created # and maintained by the MacCAN project lib_name = "PCBUSB" else: lib_name = "pcanbasic" lib_path = find_library(lib_name) if not lib_path: raise OSError(f"{lib_name} library not found.") try: self.__m_dllBasic = load_library_func(lib_path) except OSError: raise OSError( f"The PCAN-Basic API could not be loaded. ({lib_path})" ) from None # Initializes a PCAN Channel # def Initialize( self, Channel, Btr0Btr1, HwType=TPCANType(0), # noqa: B008 IOPort=c_uint(0), # noqa: B008 Interrupt=c_ushort(0), # noqa: B008 ): """Initializes a PCAN Channel Parameters: Channel : A TPCANHandle representing a PCAN Channel Btr0Btr1 : The speed for the communication (BTR0BTR1 code) HwType : Non-PnP: The type of hardware and operation mode IOPort : Non-PnP: The I/O address for the parallel port Interrupt: Non-PnP: Interrupt number of the parallel port Returns: A TPCANStatus error code """ try: res = self.__m_dllBasic.CAN_Initialize( Channel, Btr0Btr1, HwType, IOPort, Interrupt ) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.Initialize") raise # Initializes a FD capable PCAN Channel # def InitializeFD(self, Channel, BitrateFD): """Initializes a FD capable PCAN Channel Parameters: Channel : The handle of a FD capable PCAN Channel BitrateFD : The speed for the communication (FD bit rate string) Remarks: * See PCAN_BR_* values. * parameter and values must be separated by '=' * Couples of Parameter/value must be separated by ',' * Following Parameter must be filled out: f_clock, data_brp, data_sjw, data_tseg1, data_tseg2, nom_brp, nom_sjw, nom_tseg1, nom_tseg2. * Following Parameters are optional (not used yet): data_ssp_offset, nom_sam Example: f_clock=80000000,nom_brp=10,nom_tseg1=5,nom_tseg2=2,nom_sjw=1,data_brp=4,data_tseg1=7,data_tseg2=2,data_sjw=1 Returns: A TPCANStatus error code """ try: res = self.__m_dllBasic.CAN_InitializeFD(Channel, BitrateFD) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.InitializeFD") raise # Uninitializes one or all PCAN Channels initialized by CAN_Initialize # def Uninitialize(self, Channel): """Uninitializes one or all PCAN Channels initialized by CAN_Initialize Remarks: Giving the TPCANHandle value "PCAN_NONEBUS", uninitialize all initialized channels Parameters: Channel : A TPCANHandle representing a PCAN Channel Returns: A TPCANStatus error code """ try: res = self.__m_dllBasic.CAN_Uninitialize(Channel) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.Uninitialize") raise # Resets the receive and transmit queues of the PCAN Channel # def Reset(self, Channel): """Resets the receive and transmit queues of the PCAN Channel Remarks: A reset of the CAN controller is not performed Parameters: Channel : A TPCANHandle representing a PCAN Channel Returns: A TPCANStatus error code """ try: res = self.__m_dllBasic.CAN_Reset(Channel) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.Reset") raise # Gets the current status of a PCAN Channel # def GetStatus(self, Channel): """Gets the current status of a PCAN Channel Parameters: Channel : A TPCANHandle representing a PCAN Channel Returns: A TPCANStatus error code """ try: res = self.__m_dllBasic.CAN_GetStatus(Channel) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.GetStatus") raise # Reads a CAN message from the receive queue of a PCAN Channel # def Read(self, Channel): """Reads a CAN message from the receive queue of a PCAN Channel Remarks: The return value of this method is a 3-tuple, where the first value is the result (TPCANStatus) of the method. The order of the values are: [0]: A TPCANStatus error code [1]: A TPCANMsg structure with the CAN message read [2]: A TPCANTimestamp structure with the time when a message was read Parameters: Channel : A TPCANHandle representing a PCAN Channel Returns: A tuple with three values """ try: msg = TPCANMsg() timestamp = TPCANTimestamp() res = self.__m_dllBasic.CAN_Read(Channel, byref(msg), byref(timestamp)) return TPCANStatus(res), msg, timestamp except: logger.error("Exception on PCANBasic.Read") raise # Reads a CAN message from the receive queue of a FD capable PCAN Channel # def ReadFD(self, Channel): """Reads a CAN message from the receive queue of a FD capable PCAN Channel Remarks: The return value of this method is a 3-tuple, where the first value is the result (TPCANStatus) of the method. The order of the values are: [0]: A TPCANStatus error code [1]: A TPCANMsgFD structure with the CAN message read [2]: A TPCANTimestampFD that is the time when a message was read Parameters: Channel : The handle of a FD capable PCAN Channel Returns: A tuple with three values """ try: msg = TPCANMsgFD() timestamp = TPCANTimestampFD() res = self.__m_dllBasic.CAN_ReadFD(Channel, byref(msg), byref(timestamp)) return TPCANStatus(res), msg, timestamp except: logger.error("Exception on PCANBasic.ReadFD") raise # Transmits a CAN message # def Write(self, Channel, MessageBuffer): """Transmits a CAN message Parameters: Channel : A TPCANHandle representing a PCAN Channel MessageBuffer: A TPCANMsg representing the CAN message to be sent Returns: A TPCANStatus error code """ try: res = self.__m_dllBasic.CAN_Write(Channel, byref(MessageBuffer)) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.Write") raise # Transmits a CAN message over a FD capable PCAN Channel # def WriteFD(self, Channel, MessageBuffer): """Transmits a CAN message over a FD capable PCAN Channel Parameters: Channel : The handle of a FD capable PCAN Channel MessageBuffer: A TPCANMsgFD buffer with the message to be sent Returns: A TPCANStatus error code """ try: res = self.__m_dllBasic.CAN_WriteFD(Channel, byref(MessageBuffer)) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.WriteFD") raise # Configures the reception filter # def FilterMessages(self, Channel, FromID, ToID, Mode): """Configures the reception filter Remarks: The message filter will be expanded with every call to this function. If it is desired to reset the filter, please use the 'SetValue' function. Parameters: Channel : A TPCANHandle representing a PCAN Channel FromID : A c_uint value with the lowest CAN ID to be received ToID : A c_uint value with the highest CAN ID to be received Mode : A TPCANMode representing the message type (Standard, 11-bit identifier, or Extended, 29-bit identifier) Returns: A TPCANStatus error code """ try: res = self.__m_dllBasic.CAN_FilterMessages(Channel, FromID, ToID, Mode) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.FilterMessages") raise # Retrieves a PCAN Channel value # def GetValue(self, Channel, Parameter): """Retrieves a PCAN Channel value Remarks: Parameters can be present or not according with the kind of Hardware (PCAN Channel) being used. If a parameter is not available, a PCAN_ERROR_ILLPARAMTYPE error will be returned. The return value of this method is a 2-tuple, where the first value is the result (TPCANStatus) of the method and the second one, the asked value Parameters: Channel : A TPCANHandle representing a PCAN Channel Parameter : The TPCANParameter parameter to get Returns: A tuple with 2 values """ try: if ( Parameter == PCAN_API_VERSION or Parameter == PCAN_HARDWARE_NAME or Parameter == PCAN_CHANNEL_VERSION or Parameter == PCAN_LOG_LOCATION or Parameter == PCAN_TRACE_LOCATION or Parameter == PCAN_BITRATE_INFO_FD or Parameter == PCAN_IP_ADDRESS or Parameter == PCAN_FIRMWARE_VERSION or Parameter == PCAN_DEVICE_PART_NUMBER ): mybuffer = create_string_buffer(256) elif Parameter == PCAN_ATTACHED_CHANNELS: res = self.GetValue(Channel, PCAN_ATTACHED_CHANNELS_COUNT) if TPCANStatus(res[0]) != PCAN_ERROR_OK: return TPCANStatus(res[0]), () mybuffer = (TPCANChannelInformation * res[1])() elif ( Parameter == PCAN_ACCEPTANCE_FILTER_11BIT or PCAN_ACCEPTANCE_FILTER_29BIT ): mybuffer = c_int64(0) else: mybuffer = c_int(0) res = self.__m_dllBasic.CAN_GetValue( Channel, Parameter, byref(mybuffer), sizeof(mybuffer) ) if Parameter == PCAN_ATTACHED_CHANNELS: return TPCANStatus(res), mybuffer else: return TPCANStatus(res), mybuffer.value except: logger.error("Exception on PCANBasic.GetValue") raise # Returns a descriptive text of a given TPCANStatus # error code, in any desired language # def SetValue(self, Channel, Parameter, Buffer): """Returns a descriptive text of a given TPCANStatus error code, in any desired language Remarks: Parameters can be present or not according with the kind of Hardware (PCAN Channel) being used. If a parameter is not available, a PCAN_ERROR_ILLPARAMTYPE error will be returned. Parameters: Channel : A TPCANHandle representing a PCAN Channel Parameter : The TPCANParameter parameter to set Buffer : Buffer with the value to be set BufferLength : Size in bytes of the buffer Returns: A TPCANStatus error code """ try: if ( Parameter == PCAN_LOG_LOCATION or Parameter == PCAN_LOG_TEXT or Parameter == PCAN_TRACE_LOCATION ): mybuffer = create_string_buffer(256) elif ( Parameter == PCAN_ACCEPTANCE_FILTER_11BIT or PCAN_ACCEPTANCE_FILTER_29BIT ): mybuffer = c_int64(0) else: mybuffer = c_int(0) mybuffer.value = Buffer res = self.__m_dllBasic.CAN_SetValue( Channel, Parameter, byref(mybuffer), sizeof(mybuffer) ) return TPCANStatus(res) except: logger.error("Exception on PCANBasic.SetValue") raise def GetErrorText(self, Error, Language=0): """Configures or sets a PCAN Channel value Remarks: The current languages available for translation are: Neutral (0x00), German (0x07), English (0x09), Spanish (0x0A), Italian (0x10) and French (0x0C) The return value of this method is a 2-tuple, where the first value is the result (TPCANStatus) of the method and the second one, the error text Parameters: Error : A TPCANStatus error code Language : Indicates a 'Primary language ID' (Default is Neutral(0)) Returns: A tuple with 2 values """ try: mybuffer = create_string_buffer(256) res = self.__m_dllBasic.CAN_GetErrorText(Error, Language, byref(mybuffer)) return TPCANStatus(res), mybuffer.value except: logger.error("Exception on PCANBasic.GetErrorText") raise def LookUpChannel(self, Parameters): """Finds a PCAN-Basic channel that matches with the given parameters Remarks: The return value of this method is a 2-tuple, where the first value is the result (TPCANStatus) of the method and the second one a TPCANHandle value Parameters: Parameters : A comma separated string contained pairs of parameter-name/value to be matched within a PCAN-Basic channel Returns: A tuple with 2 values """ try: mybuffer = TPCANHandle(0) res = self.__m_dllBasic.CAN_LookUpChannel(Parameters, byref(mybuffer)) return TPCANStatus(res), mybuffer except: logger.error("Exception on PCANBasic.LookUpChannel") raise python-can-4.5.0/can/interfaces/pcan/pcan.py000066400000000000000000000674351472200326600207210ustar00rootroot00000000000000""" Enable basic CAN over a PCAN USB device. """ import logging import platform import time import warnings from typing import Any, List, Optional, Tuple, Union from packaging import version from can import ( BitTiming, BitTimingFd, BusABC, BusState, CanError, CanInitializationError, CanOperationError, CanProtocol, Message, ) from can.util import check_or_adjust_timing_clock, dlc2len, len2dlc from .basic import ( FEATURE_FD_CAPABLE, IS_LINUX, IS_WINDOWS, PCAN_ALLOW_ECHO_FRAMES, PCAN_ALLOW_ERROR_FRAMES, PCAN_API_VERSION, PCAN_ATTACHED_CHANNELS, PCAN_BAUD_500K, PCAN_BITRATES, PCAN_BUSOFF_AUTORESET, PCAN_CHANNEL_AVAILABLE, PCAN_CHANNEL_CONDITION, PCAN_CHANNEL_FEATURES, PCAN_CHANNEL_IDENTIFYING, PCAN_CHANNEL_NAMES, PCAN_DEVICE_NUMBER, PCAN_DICT_STATUS, PCAN_ERROR_BUSHEAVY, PCAN_ERROR_BUSLIGHT, PCAN_ERROR_ILLDATA, PCAN_ERROR_OK, PCAN_ERROR_QRCVEMPTY, PCAN_FD_PARAMETER_LIST, PCAN_LANBUS1, PCAN_LISTEN_ONLY, PCAN_MESSAGE_BRS, PCAN_MESSAGE_ECHO, PCAN_MESSAGE_ERRFRAME, PCAN_MESSAGE_ESI, PCAN_MESSAGE_EXTENDED, PCAN_MESSAGE_FD, PCAN_MESSAGE_RTR, PCAN_MESSAGE_STANDARD, PCAN_NONEBUS, PCAN_PARAMETER_OFF, PCAN_PARAMETER_ON, PCAN_PCCBUS1, PCAN_PCIBUS1, PCAN_RECEIVE_EVENT, PCAN_TYPE_ISA, PCAN_USBBUS1, VALID_PCAN_CAN_CLOCKS, VALID_PCAN_FD_CLOCKS, PCANBasic, TPCANBaudrate, TPCANChannelInformation, TPCANHandle, TPCANMsg, TPCANMsgFD, ) # Set up logging log = logging.getLogger("can.pcan") MIN_PCAN_API_VERSION = version.parse("4.2.0") try: # use the "uptime" library if available import uptime # boottime() and fromtimestamp() are timezone offset, so the difference is not. if uptime.boottime() is None: boottimeEpoch = 0 else: boottimeEpoch = uptime.boottime().timestamp() except ImportError: log.warning( "uptime library not available, timestamps are relative to boot time and not to Epoch UTC", ) boottimeEpoch = 0 HAS_EVENTS = False if IS_WINDOWS: try: # Try builtin Python 3 Windows API from _overlapped import CreateEvent from _winapi import INFINITE, WAIT_OBJECT_0, WaitForSingleObject HAS_EVENTS = True except ImportError: pass elif IS_LINUX: try: import select HAS_EVENTS = True except Exception: pass class PcanBus(BusABC): def __init__( self, channel: str = "PCAN_USBBUS1", device_id: Optional[int] = None, state: BusState = BusState.ACTIVE, timing: Optional[Union[BitTiming, BitTimingFd]] = None, bitrate: int = 500000, receive_own_messages: bool = False, **kwargs: Any, ): """A PCAN USB interface to CAN. On top of the usual :class:`~can.Bus` methods provided, the PCAN interface includes the :meth:`flash` and :meth:`status` methods. :param str channel: The can interface name. An example would be 'PCAN_USBBUS1'. Alternatively the value can be an int with the numerical value. Default is 'PCAN_USBBUS1' :param int device_id: Select the PCAN interface based on its ID. The device ID is a 8/32bit value that can be configured for each PCAN device. If you set the device_id parameter, it takes precedence over the channel parameter. The constructor searches all connected interfaces and initializes the first one that matches the parameter value. If no device is found, an exception is raised. :param can.bus.BusState state: BusState of the channel. Default is ACTIVE :param timing: An instance of :class:`~can.BitTiming` or :class:`~can.BitTimingFd` to specify the bit timing parameters for the PCAN interface. If this parameter is provided, it takes precedence over all other timing-related parameters. If this parameter is not provided, the bit timing parameters can be specified using the `bitrate` parameter for standard CAN or the `fd`, `f_clock`, `f_clock_mhz`, `nom_brp`, `nom_tseg1`, `nom_tseg2`, `nom_sjw`, `data_brp`, `data_tseg1`, `data_tseg2`, and `data_sjw` parameters for CAN FD. Note that the `f_clock` value of the `timing` instance must be 8_000_000 for standard CAN or any of the following values for CAN FD: 20_000_000, 24_000_000, 30_000_000, 40_000_000, 60_000_000, 80_000_000. :param int bitrate: Bitrate of channel in bit/s. Default is 500 kbit/s. Ignored if using CanFD. :param receive_own_messages: Enable self-reception of sent messages. :param bool fd: Should the Bus be initialized in CAN-FD mode. :param int f_clock: Clock rate in Hz. Any of the following: 20000000, 24000000, 30000000, 40000000, 60000000, 80000000. Ignored if not using CAN-FD. Pass either f_clock or f_clock_mhz. :param int f_clock_mhz: Clock rate in MHz. Any of the following: 20, 24, 30, 40, 60, 80. Ignored if not using CAN-FD. Pass either f_clock or f_clock_mhz. :param int nom_brp: Clock prescaler for nominal time quantum. In the range (1..1024) Ignored if not using CAN-FD. :param int nom_tseg1: Time segment 1 for nominal bit rate, that is, the number of quanta from (but not including) the Sync Segment to the sampling point. In the range (1..256). Ignored if not using CAN-FD. :param int nom_tseg2: Time segment 2 for nominal bit rate, that is, the number of quanta from the sampling point to the end of the bit. In the range (1..128). Ignored if not using CAN-FD. :param int nom_sjw: Synchronization Jump Width for nominal bit rate. Decides the maximum number of time quanta that the controller can resynchronize every bit. In the range (1..128). Ignored if not using CAN-FD. :param int data_brp: Clock prescaler for fast data time quantum. In the range (1..1024) Ignored if not using CAN-FD. :param int data_tseg1: Time segment 1 for fast data bit rate, that is, the number of quanta from (but not including) the Sync Segment to the sampling point. In the range (1..32). Ignored if not using CAN-FD. :param int data_tseg2: Time segment 2 for fast data bit rate, that is, the number of quanta from the sampling point to the end of the bit. In the range (1..16). Ignored if not using CAN-FD. :param int data_sjw: Synchronization Jump Width for fast data bit rate. Decides the maximum number of time quanta that the controller can resynchronize every bit. In the range (1..16). Ignored if not using CAN-FD. :param bool auto_reset: Enable automatic recovery in bus off scenario. Resetting the driver takes ~500ms during which it will not be responsive. """ self.m_objPCANBasic = PCANBasic() if device_id is not None: channel = self._find_channel_by_dev_id(device_id) if channel is None: err_msg = f"Cannot find a channel with ID {device_id:08x}" raise ValueError(err_msg) is_fd = isinstance(timing, BitTimingFd) if timing else kwargs.get("fd", False) self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20 self.channel_info = str(channel) hwtype = PCAN_TYPE_ISA ioport = 0x02A0 interrupt = 11 if not isinstance(channel, int): channel = PCAN_CHANNEL_NAMES[channel] self.m_PcanHandle = channel self.check_api_version() if state in [BusState.ACTIVE, BusState.PASSIVE]: self.state = state else: raise ValueError("BusState must be Active or Passive") if isinstance(timing, BitTiming): timing = check_or_adjust_timing_clock(timing, VALID_PCAN_CAN_CLOCKS) pcan_bitrate = TPCANBaudrate(timing.btr0 << 8 | timing.btr1) result = self.m_objPCANBasic.Initialize( self.m_PcanHandle, pcan_bitrate, hwtype, ioport, interrupt ) elif is_fd: if isinstance(timing, BitTimingFd): timing = check_or_adjust_timing_clock( timing, sorted(VALID_PCAN_FD_CLOCKS, reverse=True) ) # We dump the timing parameters into the kwargs because they have equal names # as the kwargs parameters and this saves us one additional code path kwargs.update(timing) clock_param = "f_clock" if "f_clock" in kwargs else "f_clock_mhz" fd_parameters_values = [ f"{key}={kwargs[key]}" for key in (clock_param, *PCAN_FD_PARAMETER_LIST) if key in kwargs ] self.fd_bitrate = ", ".join(fd_parameters_values).encode("ascii") result = self.m_objPCANBasic.InitializeFD( self.m_PcanHandle, self.fd_bitrate ) else: pcan_bitrate = PCAN_BITRATES.get(bitrate, PCAN_BAUD_500K) result = self.m_objPCANBasic.Initialize( self.m_PcanHandle, pcan_bitrate, hwtype, ioport, interrupt ) if result != PCAN_ERROR_OK: raise PcanCanInitializationError(self._get_formatted_error(result)) result = self.m_objPCANBasic.SetValue( self.m_PcanHandle, PCAN_ALLOW_ERROR_FRAMES, PCAN_PARAMETER_ON ) if result != PCAN_ERROR_OK: if platform.system() != "Darwin": raise PcanCanInitializationError(self._get_formatted_error(result)) else: # TODO Remove Filter when MACCan actually supports it: # https://github.com/mac-can/PCBUSB-Library/ log.debug( "Ignoring error. PCAN_ALLOW_ERROR_FRAMES is still unsupported by OSX Library PCANUSB v0.11.2" ) if receive_own_messages: result = self.m_objPCANBasic.SetValue( self.m_PcanHandle, PCAN_ALLOW_ECHO_FRAMES, PCAN_PARAMETER_ON ) if result != PCAN_ERROR_OK: raise PcanCanInitializationError(self._get_formatted_error(result)) if kwargs.get("auto_reset", False): result = self.m_objPCANBasic.SetValue( self.m_PcanHandle, PCAN_BUSOFF_AUTORESET, PCAN_PARAMETER_ON ) if result != PCAN_ERROR_OK: raise PcanCanInitializationError(self._get_formatted_error(result)) if HAS_EVENTS: if IS_WINDOWS: self._recv_event = CreateEvent(None, 0, 0, None) result = self.m_objPCANBasic.SetValue( self.m_PcanHandle, PCAN_RECEIVE_EVENT, self._recv_event ) elif IS_LINUX: result, self._recv_event = self.m_objPCANBasic.GetValue( self.m_PcanHandle, PCAN_RECEIVE_EVENT ) if result != PCAN_ERROR_OK: raise PcanCanInitializationError(self._get_formatted_error(result)) super().__init__( channel=channel, state=state, bitrate=bitrate, **kwargs, ) def _find_channel_by_dev_id(self, device_id): """ Iterate over all possible channels to find a channel that matches the device ID. This method is somewhat brute force, but the Basic API only offers a suitable API call since V4.4.0. :param device_id: The device_id for which to search for :return: The name of a PCAN channel that matches the device ID, or None if no channel can be found. """ for ch_name, ch_handle in PCAN_CHANNEL_NAMES.items(): err, cur_dev_id = self.m_objPCANBasic.GetValue( ch_handle, PCAN_DEVICE_NUMBER ) if err != PCAN_ERROR_OK: continue if cur_dev_id == device_id: return ch_name return None def _get_formatted_error(self, error): """ Gets the text using the GetErrorText API function. If the function call succeeds, the translated error is returned. If it fails, a text describing the current error is returned. Multiple errors may be present in which case their individual messages are included in the return string, one line per error. """ def bits(n): """ Iterate over all the set bits in `n`, returning the masked bits at the set indices """ while n: # Create a mask to mask the lowest set bit in n mask = ~n + 1 masked_value = n & mask yield masked_value # Toggle the lowest set bit n ^= masked_value stsReturn = self.m_objPCANBasic.GetErrorText(error, 0x9) if stsReturn[0] != PCAN_ERROR_OK: strings = [] for b in bits(error): stsReturn = self.m_objPCANBasic.GetErrorText(b, 0x9) if stsReturn[0] != PCAN_ERROR_OK: text = f"An error occurred. Error-code's text ({error:X}h) couldn't be retrieved" else: text = stsReturn[1].decode("utf-8", errors="replace") strings.append(text) complete_text = "\n".join(strings) else: complete_text = stsReturn[1].decode("utf-8", errors="replace") return complete_text def get_api_version(self): error, value = self.m_objPCANBasic.GetValue(PCAN_NONEBUS, PCAN_API_VERSION) if error != PCAN_ERROR_OK: raise CanInitializationError("Failed to read pcan basic api version") # fix https://github.com/hardbyte/python-can/issues/1642 version_string = value.decode("ascii").replace(",", ".").replace(" ", "") return version.parse(version_string) def check_api_version(self): apv = self.get_api_version() if apv < MIN_PCAN_API_VERSION: log.warning( f"Minimum version of pcan api is {MIN_PCAN_API_VERSION}." f" Installed version is {apv}. Consider upgrade of pcan basic package" ) def status(self): """ Query the PCAN bus status. :rtype: int :return: The status code. See values in **basic.PCAN_ERROR_** """ return self.m_objPCANBasic.GetStatus(self.m_PcanHandle) def status_is_ok(self): """ Convenience method to check that the bus status is OK """ status = self.status() return status == PCAN_ERROR_OK def reset(self): """ Command the PCAN driver to reset the bus after an error. """ status = self.m_objPCANBasic.Reset(self.m_PcanHandle) return status == PCAN_ERROR_OK def get_device_number(self): """ Return the PCAN device number. :rtype: int :return: PCAN device number """ error, value = self.m_objPCANBasic.GetValue( self.m_PcanHandle, PCAN_DEVICE_NUMBER ) if error != PCAN_ERROR_OK: return None return value def set_device_number(self, device_number): """ Set the PCAN device number. :param device_number: new PCAN device number :rtype: bool :return: True if device number set successfully """ try: if ( self.m_objPCANBasic.SetValue( self.m_PcanHandle, PCAN_DEVICE_NUMBER, int(device_number) ) != PCAN_ERROR_OK ): raise ValueError() except ValueError: log.error("Invalid value '%s' for device number.", device_number) return False return True def _recv_internal( self, timeout: Optional[float] ) -> Tuple[Optional[Message], bool]: end_time = time.time() + timeout if timeout is not None else None while True: if self._can_protocol is CanProtocol.CAN_FD: result, pcan_msg, pcan_timestamp = self.m_objPCANBasic.ReadFD( self.m_PcanHandle ) else: result, pcan_msg, pcan_timestamp = self.m_objPCANBasic.Read( self.m_PcanHandle ) if result == PCAN_ERROR_OK: # message received break if result == PCAN_ERROR_QRCVEMPTY: # receive queue is empty, wait or return on timeout if end_time is None: time_left: Optional[float] = None timed_out = False else: time_left = max(0.0, end_time - time.time()) timed_out = time_left == 0.0 if timed_out: return None, False if not HAS_EVENTS: # polling mode time.sleep(0.001) continue if IS_WINDOWS: # Windows with event if time_left is None: time_left_ms = INFINITE else: time_left_ms = int(time_left * 1000) _ret = WaitForSingleObject(self._recv_event, time_left_ms) if _ret == WAIT_OBJECT_0: continue elif IS_LINUX: # Linux with event recv, _, _ = select.select([self._recv_event], [], [], time_left) if self._recv_event in recv: continue elif result & (PCAN_ERROR_BUSLIGHT | PCAN_ERROR_BUSHEAVY): log.warning(self._get_formatted_error(result)) elif result == PCAN_ERROR_ILLDATA: # When there is an invalid frame on CAN bus (in our case CAN FD), PCAN first reports result PCAN_ERROR_ILLDATA # and then it sends the error frame. If the PCAN_ERROR_ILLDATA is not ignored, python-can throws an exception. # So we ignore any PCAN_ERROR_ILLDATA results here. pass else: raise PcanCanOperationError(self._get_formatted_error(result)) return None, False is_extended_id = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_EXTENDED.value) is_remote_frame = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_RTR.value) is_fd = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_FD.value) is_rx = not bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_ECHO.value) bitrate_switch = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_BRS.value) error_state_indicator = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_ESI.value) is_error_frame = bool(pcan_msg.MSGTYPE & PCAN_MESSAGE_ERRFRAME.value) if self._can_protocol is CanProtocol.CAN_FD: dlc = dlc2len(pcan_msg.DLC) timestamp = boottimeEpoch + (pcan_timestamp.value / (1000.0 * 1000.0)) else: dlc = pcan_msg.LEN timestamp = boottimeEpoch + ( ( pcan_timestamp.micros + 1000 * pcan_timestamp.millis + 0x100000000 * 1000 * pcan_timestamp.millis_overflow ) / (1000.0 * 1000.0) ) rx_msg = Message( timestamp=timestamp, arbitration_id=pcan_msg.ID, is_extended_id=is_extended_id, is_remote_frame=is_remote_frame, is_error_frame=is_error_frame, dlc=dlc, data=pcan_msg.DATA[:dlc], is_fd=is_fd, is_rx=is_rx, bitrate_switch=bitrate_switch, error_state_indicator=error_state_indicator, ) return rx_msg, False def send(self, msg, timeout=None): msgType = ( PCAN_MESSAGE_EXTENDED.value if msg.is_extended_id else PCAN_MESSAGE_STANDARD.value ) if msg.is_remote_frame: msgType |= PCAN_MESSAGE_RTR.value if msg.is_error_frame: msgType |= PCAN_MESSAGE_ERRFRAME.value if msg.is_fd: msgType |= PCAN_MESSAGE_FD.value if msg.bitrate_switch: msgType |= PCAN_MESSAGE_BRS.value if msg.error_state_indicator: msgType |= PCAN_MESSAGE_ESI.value if self._can_protocol is CanProtocol.CAN_FD: # create a TPCANMsg message structure CANMsg = TPCANMsgFD() # configure the message. ID, Length of data, message type and data CANMsg.ID = msg.arbitration_id CANMsg.DLC = len2dlc(msg.dlc) CANMsg.MSGTYPE = msgType # copy data CANMsg.DATA[: msg.dlc] = msg.data[: msg.dlc] log.debug("Data: %s", msg.data) log.debug("Type: %s", type(msg.data)) result = self.m_objPCANBasic.WriteFD(self.m_PcanHandle, CANMsg) else: # create a TPCANMsg message structure CANMsg = TPCANMsg() # configure the message. ID, Length of data, message type and data CANMsg.ID = msg.arbitration_id CANMsg.LEN = msg.dlc CANMsg.MSGTYPE = msgType # if a remote frame will be sent, data bytes are not important. if not msg.is_remote_frame: # copy data CANMsg.DATA[: CANMsg.LEN] = msg.data[: CANMsg.LEN] log.debug("Data: %s", msg.data) log.debug("Type: %s", type(msg.data)) result = self.m_objPCANBasic.Write(self.m_PcanHandle, CANMsg) if result != PCAN_ERROR_OK: raise PcanCanOperationError( "Failed to send: " + self._get_formatted_error(result) ) def flash(self, flash): """ Turn on or off flashing of the device's LED for physical identification purposes. """ self.m_objPCANBasic.SetValue( self.m_PcanHandle, PCAN_CHANNEL_IDENTIFYING, bool(flash) ) def shutdown(self): super().shutdown() if HAS_EVENTS and IS_LINUX: self.m_objPCANBasic.SetValue(self.m_PcanHandle, PCAN_RECEIVE_EVENT, 0) self.m_objPCANBasic.Uninitialize(self.m_PcanHandle) @property def fd(self) -> bool: class_name = self.__class__.__name__ warnings.warn( f"The {class_name}.fd property is deprecated and superseded by {class_name}.protocol. " "It is scheduled for removal in python-can version 5.0.", DeprecationWarning, stacklevel=2, ) return self._can_protocol is CanProtocol.CAN_FD @property def state(self): return self._state @state.setter def state(self, new_state): # declare here, which is called by __init__() self._state = new_state # pylint: disable=attribute-defined-outside-init if new_state is BusState.ACTIVE: self.m_objPCANBasic.SetValue( self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_OFF ) elif new_state is BusState.PASSIVE: # When this mode is set, the CAN controller does not take part on active events (eg. transmit CAN messages) # but stays in a passive mode (CAN monitor), in which it can analyse the traffic on the CAN bus used by a # PCAN channel. See also the Philips Data Sheet "SJA1000 Stand-alone CAN controller". self.m_objPCANBasic.SetValue( self.m_PcanHandle, PCAN_LISTEN_ONLY, PCAN_PARAMETER_ON ) @staticmethod def _detect_available_configs(): channels = [] try: library_handle = PCANBasic() except OSError: return channels interfaces = [] if platform.system() != "Darwin": res, value = library_handle.GetValue(PCAN_NONEBUS, PCAN_ATTACHED_CHANNELS) if res != PCAN_ERROR_OK: return interfaces channel_information: List[TPCANChannelInformation] = list(value) for channel in channel_information: # find channel name in PCAN_CHANNEL_NAMES by value channel_name = next( _channel_name for _channel_name, channel_id in PCAN_CHANNEL_NAMES.items() if channel_id.value == channel.channel_handle ) channel_config = { "interface": "pcan", "channel": channel_name, "supports_fd": bool(channel.device_features & FEATURE_FD_CAPABLE), "controller_number": channel.controller_number, "device_features": channel.device_features, "device_id": channel.device_id, "device_name": channel.device_name.decode("latin-1"), "device_type": channel.device_type, "channel_condition": channel.channel_condition, } interfaces.append(channel_config) return interfaces for i in range(16): interfaces.append( { "id": TPCANHandle(PCAN_PCIBUS1.value + i), "name": "PCAN_PCIBUS" + str(i + 1), } ) for i in range(16): interfaces.append( { "id": TPCANHandle(PCAN_USBBUS1.value + i), "name": "PCAN_USBBUS" + str(i + 1), } ) for i in range(2): interfaces.append( { "id": TPCANHandle(PCAN_PCCBUS1.value + i), "name": "PCAN_PCCBUS" + str(i + 1), } ) for i in range(16): interfaces.append( { "id": TPCANHandle(PCAN_LANBUS1.value + i), "name": "PCAN_LANBUS" + str(i + 1), } ) for i in interfaces: try: error, value = library_handle.GetValue(i["id"], PCAN_CHANNEL_CONDITION) if error != PCAN_ERROR_OK or value != PCAN_CHANNEL_AVAILABLE: continue has_fd = False error, value = library_handle.GetValue(i["id"], PCAN_CHANNEL_FEATURES) if error == PCAN_ERROR_OK: has_fd = bool(value & FEATURE_FD_CAPABLE) channels.append( {"interface": "pcan", "channel": i["name"], "supports_fd": has_fd} ) except AttributeError: # Ignore if this fails for some interfaces pass return channels def status_string(self) -> Optional[str]: """ Query the PCAN bus status. :return: The status description, if any was found. """ try: return PCAN_DICT_STATUS[self.status()] except KeyError: return None class PcanError(CanError): """A generic error on a PCAN bus.""" class PcanCanOperationError(CanOperationError, PcanError): """Like :class:`can.exceptions.CanOperationError`, but specific to Pcan.""" class PcanCanInitializationError(CanInitializationError, PcanError): """Like :class:`can.exceptions.CanInitializationError`, but specific to Pcan.""" python-can-4.5.0/can/interfaces/robotell.py000066400000000000000000000366141472200326600206740ustar00rootroot00000000000000""" Interface for Chinese Robotell compatible interfaces (win32/linux). """ import io import logging import time from typing import Optional from can import BusABC, CanProtocol, Message from ..exceptions import CanInterfaceNotImplementedError, CanOperationError logger = logging.getLogger(__name__) try: import serial except ImportError: logger.warning( "You won't be able to use the Robotell can backend without " "the serial module installed!" ) serial = None class robotellBus(BusABC): """ robotell interface """ _PACKET_HEAD = 0xAA # Frame starts with 2x FRAME_HEAD bytes _PACKET_TAIL = 0x55 # Frame ends with 2x FRAME_END bytes _PACKET_ESC = ( 0xA5 # Escape char before any HEAD, TAIL or ESC chat (including in checksum) ) _CAN_CONFIG_CHANNEL = 0xFF # Configuration channel of CAN _CAN_SERIALBPS_ID = 0x01FFFE90 # USB Serial port speed _CAN_ART_ID = 0x01FFFEA0 # Automatic retransmission _CAN_ABOM_ID = 0x01FFFEB0 # Automatic bus management _CAN_RESET_ID = 0x01FFFEC0 # ID for initialization _CAN_BAUD_ID = 0x01FFFED0 # CAN baud rate _CAN_FILTER_BASE_ID = 0x01FFFEE0 # ID for first filter (filter0) _CAN_FILTER_MAX_ID = 0x01FFFEE0 + 13 # ID for the last filter (filter13) _CAN_INIT_FLASH_ID = 0x01FFFEFF # Restore factory settings _CAN_READ_SERIAL1 = 0x01FFFFF0 # Read first part of device serial number _CAN_READ_SERIAL2 = 0x01FFFFF1 # Read first part of device serial number _MAX_CAN_BAUD = 1000000 # Maximum supported CAN baud rate _FILTER_ID_MASK = 0x0000000F # Filter ID mask _CAN_FILTER_EXTENDED = 0x40000000 # Enable mask _CAN_FILTER_ENABLE = 0x80000000 # Enable filter _CAN_STANDARD_FMT = 0 # Standard message ID _CAN_EXTENDED_FMT = 1 # 29 Bit extended format ID _CAN_DATA_FRAME = 0 # Send data frame _CAN_REMOTE_FRAME = 1 # Request remote frame def __init__( self, channel, ttyBaudrate=115200, bitrate=None, rtscts=False, **kwargs ): """ :param str channel: port of underlying serial or usb device (e.g. ``/dev/ttyUSB0``, ``COM8``, ...) Must not be empty. Can also end with ``@115200`` (or similarly) to specify the baudrate. :param int ttyBaudrate: baudrate of underlying serial or usb device (Ignored if set via the ``channel`` parameter) :param int bitrate: CAN Bitrate in bit/s. Value is stored in the adapter and will be used as default if no bitrate is specified :param bool rtscts: turn hardware handshake (RTS/CTS) on and off """ if serial is None: raise CanInterfaceNotImplementedError("The serial module is not installed") if not channel: # if None or empty raise TypeError("Must specify a serial port.") if "@" in channel: (channel, ttyBaudrate) = channel.split("@") self.serialPortOrig = serial.serial_for_url( channel, baudrate=ttyBaudrate, rtscts=rtscts ) # Disable flushing queued config ACKs on lookup channel (for unit tests) self._loopback_test = channel == "loop://" self._rxbuffer = bytearray() # raw bytes from the serial port self._rxmsg = [] # extracted CAN messages waiting to be read self._configmsg = [] # extracted config channel messages self._writeconfig(self._CAN_RESET_ID, 0) # Not sure if this is really necessary if bitrate is not None: self.set_bitrate(bitrate) self._can_protocol = CanProtocol.CAN_20 self.channel_info = ( f"Robotell USB-CAN s/n {self.get_serial_number(1)} on {channel}" ) logger.info("Using device: %s", self.channel_info) super().__init__(channel=channel, **kwargs) def set_bitrate(self, bitrate): """ :raise ValueError: if *bitrate* is greater than 1000000 :param int bitrate: Bitrate in bit/s """ if bitrate <= self._MAX_CAN_BAUD: self._writeconfig(self._CAN_BAUD_ID, bitrate) else: raise ValueError(f"Invalid bitrate, must be less than {self._MAX_CAN_BAUD}") def set_auto_retransmit(self, retrans_flag): """ :param bool retrans_flag: Enable/disable automatic retransmission of unacknowledged CAN frames """ self._writeconfig(self._CAN_ART_ID, 1 if retrans_flag else 0) def set_auto_bus_management(self, auto_man): """ :param bool auto_man: Enable/disable automatic bus management """ # Not sure what "automatic bus management" does. Does not seem to control # automatic ACK of CAN frames (listen only mode) self._writeconfig(self._CAN_ABOM_ID, 1 if auto_man else 0) def set_serial_rate(self, serial_bps): """ :param int serial_bps: Set the baud rate of the serial port (not CAN) interface """ self._writeconfig(self._CAN_SERIALBPS_ID, serial_bps) def set_hw_filter(self, filterid, enabled, msgid_value, msgid_mask, extended_msg): """ :raise ValueError: if *filterid* is not between 1 and 14 :param int filterid: ID of filter (1-14) :param bool enabled: This filter is enabled :param int msgid_value: CAN message ID to filter on. The test unit does not accept an extented message ID unless bit 31 of the ID was set. :param int msgid_mask: Mask to apply to CAN messagge ID :param bool extended_msg: Filter operates on extended format messages """ if filterid < 1 or filterid > 14: raise ValueError("Invalid filter ID. ID must be between 0 and 13") else: configid = self._CAN_FILTER_BASE_ID + (filterid - 1) msgid_value += self._CAN_FILTER_ENABLE if enabled else 0 msgid_value += self._CAN_FILTER_EXTENDED if extended_msg else 0 self._writeconfig(configid, msgid_value, msgid_mask) def _getconfigsize(self, configid): if configid == self._CAN_ART_ID or configid == self._CAN_ABOM_ID: return 1 if configid == self._CAN_BAUD_ID or configid == self._CAN_INIT_FLASH_ID: return 4 if configid == self._CAN_SERIALBPS_ID: return 4 if configid == self._CAN_READ_SERIAL1 or configid <= self._CAN_READ_SERIAL2: return 8 if self._CAN_FILTER_BASE_ID <= configid <= self._CAN_FILTER_MAX_ID: return 8 return 0 def _readconfig(self, configid, timeout): self._writemessage( msgid=configid, msgdata=bytearray(8), datalen=self._getconfigsize(configid), msgchan=self._CAN_CONFIG_CHANNEL, msgformat=self._CAN_EXTENDED_FMT, msgtype=self._CAN_REMOTE_FRAME, ) # Read message from config channel with result. Flush any previously pending config messages newmsg = self._readmessage(not self._loopback_test, True, timeout) if newmsg is None: logger.warning( f"Timeout waiting for response when reading config value {configid:04X}." ) return None return newmsg[4:12] def _writeconfig(self, configid, value, value2=0): configsize = self._getconfigsize(configid) configdata = bytearray(configsize) if configsize >= 1: configdata[0] = value & 0xFF if configsize >= 4: configdata[1] = (value >> 8) & 0xFF configdata[2] = (value >> 16) & 0xFF configdata[3] = (value >> 24) & 0xFF if configsize >= 8: configdata[4] = value2 & 0xFF configdata[5] = (value2 >> 8) & 0xFF configdata[6] = (value2 >> 16) & 0xFF configdata[7] = (value2 >> 24) & 0xFF self._writemessage( msgid=configid, msgdata=configdata, datalen=configsize, msgchan=self._CAN_CONFIG_CHANNEL, msgformat=self._CAN_EXTENDED_FMT, msgtype=self._CAN_DATA_FRAME, ) # Read message from config channel to verify. Flush any previously pending config messages newmsg = self._readmessage(not self._loopback_test, True, 1) if newmsg is None: logger.warning( "Timeout waiting for response when writing config value %d", configid ) def _readmessage(self, flushold, cfgchannel, timeout): header = bytearray([self._PACKET_HEAD, self._PACKET_HEAD]) terminator = bytearray([self._PACKET_TAIL, self._PACKET_TAIL]) msgqueue = self._configmsg if cfgchannel else self._rxmsg if flushold: del msgqueue[:] # read what is already in serial port receive buffer - unless we are doing loopback testing if not self._loopback_test: while self.serialPortOrig.in_waiting: self._rxbuffer += self.serialPortOrig.read() # loop until we have read an appropriate message start = time.time() time_left = timeout while True: # make sure first bytes in RX buffer is a new packet header headpos = self._rxbuffer.find(header) if headpos > 0: # data does not start with expected header bytes. Log error and ignore garbage logger.warning("Ignoring extra " + str(headpos) + " garbage bytes") del self._rxbuffer[:headpos] headpos = self._rxbuffer.find(header) # should now be at index 0! # check to see if we have a complete packet in the RX buffer termpos = self._rxbuffer.find(terminator) if headpos == 0 and termpos > headpos: # copy packet into message structure and un-escape bytes newmsg = bytearray() idx = headpos + len(header) while idx < termpos: if self._rxbuffer[idx] == self._PACKET_ESC: idx += 1 newmsg.append(self._rxbuffer[idx]) idx += 1 del self._rxbuffer[: termpos + len(terminator)] # Check one - make sure message structure is the correct length if len(newmsg) == 17: # Check two - verify the checksum cs = 0 for idx in range(16): cs = (cs + newmsg[idx]) & 0xFF if newmsg[16] == cs: # OK, valid message - place it in the correct queue if newmsg[13] == 0xFF: # Check for config channel self._configmsg.append(newmsg) else: self._rxmsg.append(newmsg) else: logger.warning("Incorrect message checksum, discarded message") else: logger.warning( "Invalid message structure length %d, ignoring message", len(newmsg), ) # Check if we have a message in the desired queue - if so copy and return if len(msgqueue) > 0: newmsg = msgqueue[0] del msgqueue[:1] return newmsg # if we still don't have a complete message, do a blocking read self.serialPortOrig.timeout = time_left byte = self.serialPortOrig.read() if byte: self._rxbuffer += byte # If there is time left, try next one with reduced timeout if timeout is not None: time_left = timeout - (time.time() - start) if time_left <= 0: return None def _writemessage(self, msgid, msgdata, datalen, msgchan, msgformat, msgtype): msgbuf = bytearray(17) # Message structure plus checksum byte msgbuf[0] = msgid & 0xFF msgbuf[1] = (msgid >> 8) & 0xFF msgbuf[2] = (msgid >> 16) & 0xFF msgbuf[3] = (msgid >> 24) & 0xFF if msgtype == self._CAN_DATA_FRAME: for idx in range(datalen): msgbuf[idx + 4] = msgdata[idx] msgbuf[12] = datalen msgbuf[13] = msgchan msgbuf[14] = msgformat msgbuf[15] = msgtype cs = 0 for idx in range(16): cs = (cs + msgbuf[idx]) & 0xFF msgbuf[16] = cs packet = bytearray() packet.append(self._PACKET_HEAD) packet.append(self._PACKET_HEAD) for msgbyte in msgbuf: if ( msgbyte == self._PACKET_ESC or msgbyte == self._PACKET_HEAD or msgbyte == self._PACKET_TAIL ): packet.append(self._PACKET_ESC) packet.append(msgbyte) packet.append(self._PACKET_TAIL) packet.append(self._PACKET_TAIL) self.serialPortOrig.write(packet) self.serialPortOrig.flush() def flush(self): del self._rxbuffer[:] del self._rxmsg[:] del self._configmsg[:] while self.serialPortOrig.in_waiting: self.serialPortOrig.read() def _recv_internal(self, timeout): msgbuf = self._readmessage(False, False, timeout) if msgbuf is not None: msg = Message( arbitration_id=msgbuf[0] + (msgbuf[1] << 8) + (msgbuf[2] << 16) + (msgbuf[3] << 24), is_extended_id=(msgbuf[14] == self._CAN_EXTENDED_FMT), timestamp=time.time(), # Better than nothing... is_remote_frame=(msgbuf[15] == self._CAN_REMOTE_FRAME), dlc=msgbuf[12], data=msgbuf[4 : 4 + msgbuf[12]], ) return msg, False return None, False def send(self, msg, timeout=None): if timeout != self.serialPortOrig.write_timeout: self.serialPortOrig.write_timeout = timeout self._writemessage( msg.arbitration_id, msg.data, msg.dlc, 0, self._CAN_EXTENDED_FMT if msg.is_extended_id else self._CAN_STANDARD_FMT, self._CAN_REMOTE_FRAME if msg.is_remote_frame else self._CAN_DATA_FRAME, ) def shutdown(self): super().shutdown() self.serialPortOrig.close() def fileno(self): try: return self.serialPortOrig.fileno() except io.UnsupportedOperation: raise NotImplementedError( "fileno is not implemented using current CAN bus on this platform" ) from None except Exception as exception: raise CanOperationError("Cannot fetch fileno") from exception def get_serial_number(self, timeout: Optional[int]) -> Optional[str]: """Get serial number of the slcan interface. :param timeout: seconds to wait for serial number or None to wait indefinitely :return: None on timeout or a str object. """ sn1 = self._readconfig(self._CAN_READ_SERIAL1, timeout) if sn1 is None: return None sn2 = self._readconfig(self._CAN_READ_SERIAL2, timeout) if sn2 is None: return None serial = "" for idx in range(0, 8, 2): serial += f"{sn1[idx]:02X}{sn1[idx + 1]:02X}-" for idx in range(0, 4, 2): serial += f"{sn2[idx]:02X}{sn2[idx + 1]:02X}-" return serial[:-1] python-can-4.5.0/can/interfaces/seeedstudio/000077500000000000000000000000001472200326600210035ustar00rootroot00000000000000python-can-4.5.0/can/interfaces/seeedstudio/__init__.py000066400000000000000000000001671472200326600231200ustar00rootroot00000000000000""" """ __all__ = [ "SeeedBus", "seeedstudio", ] from can.interfaces.seeedstudio.seeedstudio import SeeedBus python-can-4.5.0/can/interfaces/seeedstudio/seeedstudio.py000066400000000000000000000226121472200326600236750ustar00rootroot00000000000000""" To Support the Seeed USB-Can analyzer interface. The device will appear as a serial port, for example "/dev/ttyUSB0" on Linux machines or "COM1" on Windows. https://www.seeedstudio.com/USB-CAN-Analyzer-p-2888.html SKU 114991193 """ import io import logging import struct from time import time import can from can import BusABC, CanProtocol, Message logger = logging.getLogger("seeedbus") try: import serial except ImportError: logger.warning( "You won't be able to use the serial can backend without " "the serial module installed!" ) serial = None class SeeedBus(BusABC): """ Enable basic can communication over a USB-CAN-Analyzer device. """ BITRATE = { 1000000: 0x01, 800000: 0x02, 500000: 0x03, 400000: 0x04, 250000: 0x05, 200000: 0x06, 125000: 0x07, 100000: 0x08, 50000: 0x09, 20000: 0x0A, 10000: 0x0B, 5000: 0x0C, } FRAMETYPE = {"STD": 0x01, "EXT": 0x02} OPERATIONMODE = { "normal": 0x00, "loopback": 0x01, "silent": 0x02, "loopback_and_silent": 0x03, } def __init__( self, channel, baudrate=2000000, timeout=0.1, frame_type="STD", operation_mode="normal", bitrate=500000, **kwargs, ): """ :param str channel: The serial device to open. For example "/dev/ttyS1" or "/dev/ttyUSB0" on Linux or "COM1" on Windows systems. :param baudrate: The default matches required baudrate :param float timeout: Timeout for the serial device in seconds (default 0.1). :param str frame_type: STD or EXT, to select standard or extended messages :param operation_mode normal, loopback, silent or loopback_and_silent. :param bitrate CAN bus bit rate, selected from available list. :raises can.CanInitializationError: If the given parameters are invalid. :raises can.CanInterfaceNotImplementedError: If the serial module is not installed. """ if serial is None: raise can.CanInterfaceNotImplementedError( "the serial module is not installed" ) self.bit_rate = bitrate self.frame_type = frame_type self.op_mode = operation_mode self.filter_id = bytearray([0x00, 0x00, 0x00, 0x00]) self.mask_id = bytearray([0x00, 0x00, 0x00, 0x00]) self._can_protocol = CanProtocol.CAN_20 if not channel: raise can.CanInitializationError("Must specify a serial port.") self.channel_info = "Serial interface: " + channel try: self.ser = serial.Serial( channel, baudrate=baudrate, timeout=timeout, rtscts=False ) except ValueError as error: raise can.CanInitializationError( "could not create the serial device" ) from error super().__init__(channel=channel, **kwargs) self.init_frame() def shutdown(self): """ Close the serial interface. """ super().shutdown() self.ser.close() def init_frame(self, timeout=None): """ Send init message to setup the device for comms. this is called during interface creation. :param timeout: This parameter will be ignored. The timeout value of the channel is used instead. """ byte_msg = bytearray() byte_msg.append(0xAA) # Frame Start Byte 1 byte_msg.append(0x55) # Frame Start Byte 2 byte_msg.append(0x12) # Initialization Message ID byte_msg.append(SeeedBus.BITRATE[self.bit_rate]) # CAN Baud Rate byte_msg.append(SeeedBus.FRAMETYPE[self.frame_type]) byte_msg.extend(self.filter_id) byte_msg.extend(self.mask_id) byte_msg.append(SeeedBus.OPERATIONMODE[self.op_mode]) byte_msg.append(0x01) # Follows 'Send once' in windows app. byte_msg.extend([0x00] * 4) # Manual bitrate config, details unknown. crc = sum(byte_msg[2:]) & 0xFF byte_msg.append(crc) logger.debug("init_frm:\t%s", byte_msg.hex()) try: self.ser.write(byte_msg) except Exception as error: raise can.CanInitializationError("could send init frame") from error def flush_buffer(self): self.ser.flushInput() def status_frame(self, timeout=None): """ Send status request message over the serial device. The device will respond but details of error codes are unknown but are logged - DEBUG. :param timeout: This parameter will be ignored. The timeout value of the channel is used instead. """ byte_msg = bytearray() byte_msg.append(0xAA) # Frame Start Byte 1 byte_msg.append(0x55) # Frame Start Byte 2 byte_msg.append(0x04) # Status Message ID byte_msg.append(0x00) # In response packet - Rx error count byte_msg.append(0x00) # In response packet - Tx error count byte_msg.extend([0x00] * 14) crc = sum(byte_msg[2:]) & 0xFF byte_msg.append(crc) logger.debug("status_frm:\t%s", byte_msg.hex()) self._write(byte_msg) def send(self, msg, timeout=None): """ Send a message over the serial device. :param can.Message msg: Message to send. :param timeout: This parameter will be ignored. The timeout value of the channel is used instead. """ byte_msg = bytearray() byte_msg.append(0xAA) m_type = 0xC0 if msg.is_extended_id: m_type += 1 << 5 if msg.is_remote_frame: m_type += 1 << 4 m_type += msg.dlc byte_msg.append(m_type) if msg.is_extended_id: a_id = struct.pack(" None: try: self.ser.write(byte_msg) except serial.PortNotOpenError as error: raise can.CanOperationError("writing to closed port") from error except serial.SerialTimeoutException as error: raise can.CanTimeoutError() from error def _recv_internal(self, timeout): """ Read a message from the serial device. :param timeout: .. warning:: This parameter will be ignored. The timeout value of the channel is used. :returns: Received message and False (because not filtering as taken place). :rtype: can.Message, bool """ try: # ser.read can return an empty string # or raise a SerialException rx_byte_1 = self.ser.read() except serial.PortNotOpenError as error: raise can.CanOperationError("reading from closed port") from error except serial.SerialException: return None, False if rx_byte_1 and ord(rx_byte_1) == 0xAA: try: rx_byte_2 = ord(self.ser.read()) time_stamp = time() if rx_byte_2 == 0x55: status = bytearray([0xAA, 0x55]) status += bytearray(self.ser.read(18)) logger.debug("status resp:\t%s", status.hex()) else: length = int(rx_byte_2 & 0x0F) is_extended = bool(rx_byte_2 & 0x20) is_remote = bool(rx_byte_2 & 0x10) if is_extended: s_3_4_5_6 = bytearray(self.ser.read(4)) arb_id = (struct.unpack(" List[Any]: return [] class SerialBus(BusABC): """ Enable basic can communication over a serial device. .. note:: See :meth:`~_recv_internal` for some special semantics. """ def __init__( self, channel: str, baudrate: int = 115200, timeout: float = 0.1, rtscts: bool = False, *args, **kwargs, ) -> None: """ :param channel: The serial device to open. For example "/dev/ttyS1" or "/dev/ttyUSB0" on Linux or "COM1" on Windows systems. :param baudrate: Baud rate of the serial device in bit/s (default 115200). .. warning:: Some serial port implementations don't care about the baudrate. :param timeout: Timeout for the serial device in seconds (default 0.1). :param rtscts: turn hardware handshake (RTS/CTS) on and off :raises ~can.exceptions.CanInitializationError: If the given parameters are invalid. :raises ~can.exceptions.CanInterfaceNotImplementedError: If the serial module is not installed. """ if not serial: raise CanInterfaceNotImplementedError("the serial module is not installed") if not channel: raise TypeError("Must specify a serial port.") self.channel_info = f"Serial interface: {channel}" self._can_protocol = CanProtocol.CAN_20 try: self._ser = serial.serial_for_url( channel, baudrate=baudrate, timeout=timeout, rtscts=rtscts ) except ValueError as error: raise CanInitializationError( "could not create the serial device" ) from error super().__init__(channel, *args, **kwargs) def shutdown(self) -> None: """ Close the serial interface. """ super().shutdown() self._ser.close() def send(self, msg: Message, timeout: Optional[float] = None) -> None: """ Send a message over the serial device. :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. :param timeout: This parameter will be ignored. The timeout value of the channel is used instead. """ # Pack timestamp try: timestamp = struct.pack(" Tuple[Optional[Message], bool]: """ Read a message from the serial device. :param timeout: .. warning:: This parameter will be ignored. The timeout value of the channel is used. :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() if rx_byte and ord(rx_byte) == 0xAA: s = self._ser.read(4) timestamp = struct.unpack(" 8: raise ValueError("received DLC may not exceed 8 bytes") s = self._ser.read(4) arbitration_id = struct.unpack("= 0x20000000: raise ValueError( "received arbitration id may not exceed 2^29 (0x20000000)" ) data = self._ser.read(dlc) delimiter_byte = ord(self._ser.read()) if delimiter_byte == 0xBB: # received message data okay msg = Message( # TODO: We are only guessing that they are milliseconds timestamp=timestamp / 1000, arbitration_id=arbitration_id, dlc=dlc, data=data, ) return msg, False else: raise CanOperationError( f"invalid delimiter byte while reading message: {delimiter_byte}" ) else: return None, False except serial.SerialException as error: raise CanOperationError("could not read from serial") from error def fileno(self) -> int: try: return self._ser.fileno() except io.UnsupportedOperation: raise NotImplementedError( "fileno is not implemented using current CAN bus on this platform" ) from None except Exception as exception: raise CanOperationError("Cannot fetch fileno") from exception @staticmethod def _detect_available_configs() -> List[AutoDetectedConfig]: return [ {"interface": "serial", "channel": port.device} for port in list_comports() ] python-can-4.5.0/can/interfaces/slcan.py000066400000000000000000000273101472200326600201430ustar00rootroot00000000000000""" Interface for slcan compatible interfaces (win32/linux). """ import io import logging import time import warnings from typing import Any, Optional, Tuple, Union from can import BitTiming, BitTimingFd, BusABC, CanProtocol, Message, typechecking from can.exceptions import ( CanInitializationError, CanInterfaceNotImplementedError, CanOperationError, error_check, ) from can.util import check_or_adjust_timing_clock, deprecated_args_alias logger = logging.getLogger(__name__) try: import serial except ImportError: logger.warning( "You won't be able to use the slcan can backend without " "the serial module installed!" ) serial = None class slcanBus(BusABC): """ slcan interface """ # the supported bitrates and their commands _BITRATES = { 10000: "S0", 20000: "S1", 50000: "S2", 100000: "S3", 125000: "S4", 250000: "S5", 500000: "S6", 750000: "S7", 1000000: "S8", 83300: "S9", } _SLEEP_AFTER_SERIAL_OPEN = 2 # in seconds _OK = b"\r" _ERROR = b"\a" LINE_TERMINATOR = b"\r" @deprecated_args_alias( deprecation_start="4.5.0", deprecation_end="5.0.0", ttyBaudrate="tty_baudrate", ) def __init__( self, channel: typechecking.ChannelStr, tty_baudrate: int = 115200, bitrate: Optional[int] = None, timing: Optional[Union[BitTiming, BitTimingFd]] = None, sleep_after_open: float = _SLEEP_AFTER_SERIAL_OPEN, rtscts: bool = False, listen_only: bool = False, timeout: float = 0.001, **kwargs: Any, ) -> None: """ :param str channel: port of underlying serial or usb device (e.g. ``/dev/ttyUSB0``, ``COM8``, ...) Must not be empty. Can also end with ``@115200`` (or similarly) to specify the baudrate. :param int tty_baudrate: baudrate of underlying serial or usb device (Ignored if set via the ``channel`` parameter) :param bitrate: Bitrate in bit/s :param timing: Optional :class:`~can.BitTiming` instance to use for custom bit timing setting. If this argument is set then it overrides the bitrate and btr arguments. The `f_clock` value of the timing instance must be set to 8_000_000 (8MHz) for standard CAN. CAN FD and the :class:`~can.BitTimingFd` class are not supported. :param poll_interval: Poll interval in seconds when reading messages :param sleep_after_open: Time to wait in seconds after opening serial connection :param rtscts: turn hardware handshake (RTS/CTS) on and off :param listen_only: If True, open interface/channel in listen mode with ``L`` command. Otherwise, the (default) ``O`` command is still used. See ``open`` method. :param timeout: Timeout for the serial or usb device in seconds (default 0.001) :raise ValueError: if both ``bitrate`` and ``btr`` are set or the channel is invalid :raise CanInterfaceNotImplementedError: if the serial module is missing :raise CanInitializationError: if the underlying serial connection could not be established """ self._listen_only = listen_only if serial is None: raise CanInterfaceNotImplementedError("The serial module is not installed") btr: Optional[str] = kwargs.get("btr", None) if btr is not None: warnings.warn( "The 'btr' argument is deprecated since python-can v4.5.0 " "and scheduled for removal in v5.0.0. " "Use the 'timing' argument instead.", DeprecationWarning, stacklevel=1, ) if not channel: # if None or empty raise ValueError("Must specify a serial port.") if "@" in channel: (channel, baudrate) = channel.split("@") tty_baudrate = int(baudrate) with error_check(exception_type=CanInitializationError): self.serialPortOrig = serial.serial_for_url( channel, baudrate=tty_baudrate, rtscts=rtscts, timeout=timeout, ) self._buffer = bytearray() self._can_protocol = CanProtocol.CAN_20 time.sleep(sleep_after_open) with error_check(exception_type=CanInitializationError): if isinstance(timing, BitTiming): timing = check_or_adjust_timing_clock(timing, valid_clocks=[8_000_000]) self.set_bitrate_reg(f"{timing.btr0:02X}{timing.btr1:02X}") elif isinstance(timing, BitTimingFd): raise NotImplementedError( f"CAN FD is not supported by {self.__class__.__name__}." ) else: if bitrate is not None and btr is not None: raise ValueError("Bitrate and btr mutually exclusive.") if bitrate is not None: self.set_bitrate(bitrate) if btr is not None: self.set_bitrate_reg(btr) self.open() super().__init__(channel, **kwargs) def set_bitrate(self, bitrate: int) -> None: """ :param bitrate: Bitrate in bit/s :raise ValueError: if ``bitrate`` is not among the possible values """ if bitrate in self._BITRATES: bitrate_code = self._BITRATES[bitrate] else: bitrates = ", ".join(str(k) for k in self._BITRATES.keys()) raise ValueError(f"Invalid bitrate, choose one of {bitrates}.") self.close() self._write(bitrate_code) self.open() def set_bitrate_reg(self, btr: str) -> None: """ :param btr: BTR register value to set custom can speed as a string `xxyy` where xx is the BTR0 value in hex and yy is the BTR1 value in hex. """ self.close() self._write("s" + btr) self.open() def _write(self, string: str) -> None: with error_check("Could not write to serial device"): self.serialPortOrig.write(string.encode() + self.LINE_TERMINATOR) self.serialPortOrig.flush() def _read(self, timeout: Optional[float]) -> Optional[str]: _timeout = serial.Timeout(timeout) with error_check("Could not read from serial device"): while True: # Due to accessing `serialPortOrig.in_waiting` too often will reduce the performance. # We read the `serialPortOrig.in_waiting` only once here. in_waiting = self.serialPortOrig.in_waiting for _ in range(max(1, in_waiting)): new_byte = self.serialPortOrig.read(size=1) if new_byte: self._buffer.extend(new_byte) else: break if new_byte in (self._ERROR, self._OK): string = self._buffer.decode() self._buffer.clear() return string if _timeout.expired(): break return None def flush(self) -> None: self._buffer.clear() with error_check("Could not flush"): self.serialPortOrig.reset_input_buffer() def open(self) -> None: if self._listen_only: self._write("L") else: self._write("O") def close(self) -> None: self._write("C") def _recv_internal( self, timeout: Optional[float] ) -> Tuple[Optional[Message], bool]: canId = None remote = False extended = False data = None string = self._read(timeout) if not string: pass elif string[0] in ( "T", "x", # x is an alternative extended message identifier for CANDapter ): # extended frame canId = int(string[1:9], 16) dlc = int(string[9]) extended = True data = bytearray.fromhex(string[10 : 10 + dlc * 2]) elif string[0] == "t": # normal frame canId = int(string[1:4], 16) dlc = int(string[4]) data = bytearray.fromhex(string[5 : 5 + dlc * 2]) elif string[0] == "r": # remote frame canId = int(string[1:4], 16) dlc = int(string[4]) remote = True elif string[0] == "R": # remote extended frame canId = int(string[1:9], 16) dlc = int(string[9]) extended = True remote = True if canId is not None: msg = Message( arbitration_id=canId, is_extended_id=extended, timestamp=time.time(), # Better than nothing... is_remote_frame=remote, dlc=dlc, data=data, ) return msg, False return None, False def send(self, msg: Message, timeout: Optional[float] = None) -> None: if timeout != self.serialPortOrig.write_timeout: self.serialPortOrig.write_timeout = timeout if msg.is_remote_frame: if msg.is_extended_id: sendStr = f"R{msg.arbitration_id:08X}{msg.dlc:d}" else: sendStr = f"r{msg.arbitration_id:03X}{msg.dlc:d}" else: if msg.is_extended_id: sendStr = f"T{msg.arbitration_id:08X}{msg.dlc:d}" else: sendStr = f"t{msg.arbitration_id:03X}{msg.dlc:d}" sendStr += msg.data.hex().upper() self._write(sendStr) def shutdown(self) -> None: super().shutdown() self.close() with error_check("Could not close serial socket"): self.serialPortOrig.close() def fileno(self) -> int: try: return self.serialPortOrig.fileno() except io.UnsupportedOperation: raise NotImplementedError( "fileno is not implemented using current CAN bus on this platform" ) from None except Exception as exception: raise CanOperationError("Cannot fetch fileno") from exception def get_version( self, timeout: Optional[float] ) -> Tuple[Optional[int], Optional[int]]: """Get HW and SW version of the slcan interface. :param timeout: seconds to wait for version or None to wait indefinitely :returns: tuple (hw_version, sw_version) WHERE int hw_version is the hardware version or None on timeout int sw_version is the software version or None on timeout """ 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 return None, None def get_serial_number(self, timeout: Optional[float]) -> Optional[str]: """Get serial number of the slcan interface. :param timeout: seconds to wait for serial number or :obj:`None` to wait indefinitely :return: :obj:`None` on timeout or a :class:`str` object. """ 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 return None python-can-4.5.0/can/interfaces/socketcan/000077500000000000000000000000001472200326600204405ustar00rootroot00000000000000python-can-4.5.0/can/interfaces/socketcan/__init__.py000066400000000000000000000004361472200326600225540ustar00rootroot00000000000000""" See: https://www.kernel.org/doc/Documentation/networking/can.txt """ __all__ = [ "CyclicSendTask", "MultiRateCyclicSendTask", "SocketcanBus", "constants", "socketcan", "utils", ] from .socketcan import CyclicSendTask, MultiRateCyclicSendTask, SocketcanBus python-can-4.5.0/can/interfaces/socketcan/constants.py000066400000000000000000000021521472200326600230260ustar00rootroot00000000000000""" Defines shared CAN constants. """ # Generic socket constants SO_TIMESTAMPNS = 35 CAN_ERR_FLAG = 0x20000000 CAN_RTR_FLAG = 0x40000000 CAN_EFF_FLAG = 0x80000000 # BCM opcodes CAN_BCM_TX_SETUP = 1 CAN_BCM_TX_DELETE = 2 CAN_BCM_TX_READ = 3 # BCM flags SETTIMER = 0x0001 STARTTIMER = 0x0002 TX_COUNTEVT = 0x0004 TX_ANNOUNCE = 0x0008 TX_CP_CAN_ID = 0x0010 RX_FILTER_ID = 0x0020 RX_CHECK_DLC = 0x0040 RX_NO_AUTOTIMER = 0x0080 RX_ANNOUNCE_RESUME = 0x0100 TX_RESET_MULTI_IDX = 0x0200 RX_RTR_FRAME = 0x0400 CAN_FD_FRAME = 0x0800 CAN_RAW = 1 CAN_BCM = 2 SOL_CAN_BASE = 100 SOL_CAN_RAW = SOL_CAN_BASE + CAN_RAW CAN_RAW_FILTER = 1 CAN_RAW_ERR_FILTER = 2 CAN_RAW_LOOPBACK = 3 CAN_RAW_RECV_OWN_MSGS = 4 CAN_RAW_FD_FRAMES = 5 MSK_ARBID = 0x1FFFFFFF MSK_FLAGS = 0xE0000000 PF_CAN = 29 SOCK_RAW = 3 SOCK_DGRAM = 2 AF_CAN = PF_CAN SIOCGIFNAME = 0x8910 SIOCGIFINDEX = 0x8933 SIOCGSTAMP = 0x8906 EXTFLG = 0x0004 CANFD_BRS = 0x01 CANFD_ESI = 0x02 CANFD_MTU = 72 STD_ACCEPTANCE_MASK_ALL_BITS = 2**11 - 1 MAX_11_BIT_ID = STD_ACCEPTANCE_MASK_ALL_BITS EXT_ACCEPTANCE_MASK_ALL_BITS = 2**29 - 1 MAX_29_BIT_ID = EXT_ACCEPTANCE_MASK_ALL_BITS python-can-4.5.0/can/interfaces/socketcan/socketcan.py000066400000000000000000000772571472200326600230060ustar00rootroot00000000000000""" The main module of the socketcan interface containing most user-facing classes and methods along some internal methods. At the end of the file the usage of the internal methods is shown. """ import ctypes import ctypes.util import errno import logging import select import socket import struct import threading import time import warnings from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, Union import can from can import BusABC, CanProtocol, Message from can.broadcastmanager import ( LimitedDurationCyclicSendTaskABC, ModifiableCyclicTaskABC, RestartableCyclicTaskABC, ) from can.interfaces.socketcan import constants from can.interfaces.socketcan.utils import find_available_interfaces, pack_filters from can.typechecking import CanFilters log = logging.getLogger(__name__) log_tx = log.getChild("tx") log_rx = log.getChild("rx") try: from socket import CMSG_SPACE CMSG_SPACE_available = True except ImportError: CMSG_SPACE_available = False log.error("socket.CMSG_SPACE not available on this platform") # Constants needed for precise handling of timestamps RECEIVED_TIMESTAMP_STRUCT = struct.Struct("@ll") RECEIVED_ANCILLARY_BUFFER_SIZE = ( CMSG_SPACE(RECEIVED_TIMESTAMP_STRUCT.size) if CMSG_SPACE_available else 0 ) # Setup BCM struct def bcm_header_factory( fields: List[Tuple[str, 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]] ] ] = [] pad_index = 0 for field in fields: field_alignment = ctypes.alignment(field[1]) field_size = ctypes.sizeof(field[1]) # If the current stride index isn't a multiple of the alignment # requirements of this field, then we must add padding bytes until we # are aligned while curr_stride % field_alignment != 0: results.append((f"pad_{pad_index}", ctypes.c_uint8)) pad_index += 1 curr_stride += 1 # Now can it fit? # Example: If this is 8 bytes and the type requires 4 bytes alignment # then we can only fit when we're starting at 0. Otherwise, we will # split across 2 strides. # # | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | results.append(field) curr_stride += field_size # Add trailing padding to align to a multiple of the largest scalar member # in the structure while curr_stride % alignment != 0: results.append((f"pad_{pad_index}", ctypes.c_uint8)) pad_index += 1 curr_stride += 1 return type("BcmMsgHead", (ctypes.Structure,), {"_fields_": results}) # The fields definition is taken from the C struct definitions in # # # struct bcm_timeval { # long tv_sec; # long tv_usec; # }; # # /** # * struct bcm_msg_head - head of messages to/from the broadcast manager # * @opcode: opcode, see enum below. # * @flags: special flags, see below. # * @count: number of frames to send before changing interval. # * @ival1: interval for the first @count frames. # * @ival2: interval for the following frames. # * @can_id: CAN ID of frames to be sent or received. # * @nframes: number of frames appended to the message head. # * @frames: array of CAN frames. # */ # struct bcm_msg_head { # __u32 opcode; # __u32 flags; # __u32 count; # struct bcm_timeval ival1, ival2; # canid_t can_id; # __u32 nframes; # struct can_frame frames[0]; # }; BcmMsgHead = bcm_header_factory( fields=[ ("opcode", ctypes.c_uint32), ("flags", ctypes.c_uint32), ("count", ctypes.c_uint32), ("ival1_tv_sec", ctypes.c_long), ("ival1_tv_usec", ctypes.c_long), ("ival2_tv_sec", ctypes.c_long), ("ival2_tv_usec", ctypes.c_long), ("can_id", ctypes.c_uint32), ("nframes", ctypes.c_uint32), ] ) # struct module defines a binary packing format: # https://docs.python.org/3/library/struct.html#struct-format-strings # 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") def build_can_frame(msg: Message) -> bytes: """CAN frame packing/unpacking (see 'struct can_frame' in ) /** * struct can_frame - basic CAN frame structure * @can_id: the CAN ID of the frame and CAN_*_FLAG flags, see above. * @can_dlc: the data length field of the CAN frame * @data: the CAN frame payload. */ struct can_frame { 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))); }; /** * struct canfd_frame - CAN flexible data rate frame structure * @can_id: CAN ID of the frame and CAN_*_FLAG flags, see canid_t definition * @len: frame payload length in byte (0 .. CANFD_MAX_DLEN) * @flags: additional flags for CAN FD * @__res0: reserved / padding * @__res1: reserved / padding * @data: CAN FD frame payload (up to CANFD_MAX_DLEN byte) */ struct canfd_frame { canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */ __u8 len; /* frame payload length in byte */ __u8 flags; /* additional flags for CAN FD */ __u8 __res0; /* reserved / padding */ __u8 __res1; /* reserved / padding */ __u8 data[CANFD_MAX_DLEN] __attribute__((aligned(8))); }; """ can_id = _compose_arbitration_id(msg) flags = 0 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 def build_bcm_header( opcode: int, flags: int, count: int, ival1_seconds: int, ival1_usec: int, ival2_seconds: int, ival2_usec: int, can_id: int, nframes: int, ) -> bytes: result = BcmMsgHead( opcode=opcode, flags=flags, count=count, ival1_tv_sec=ival1_seconds, ival1_tv_usec=ival1_usec, ival2_tv_sec=ival2_seconds, ival2_tv_usec=ival2_usec, can_id=can_id, nframes=nframes, ) return ctypes.string_at(ctypes.addressof(result), ctypes.sizeof(result)) def build_bcm_tx_delete_header(can_id: int, flags: int) -> bytes: opcode = constants.CAN_BCM_TX_DELETE return build_bcm_header(opcode, flags, 0, 0, 0, 0, 0, can_id, 1) def build_bcm_transmit_header( can_id: int, count: int, initial_period: float, subsequent_period: float, msg_flags: int, nframes: int = 1, ) -> bytes: opcode = constants.CAN_BCM_TX_SETUP flags = msg_flags | constants.SETTIMER | constants.STARTTIMER if initial_period > 0: # Note `TX_COUNTEVT` creates the message TX_EXPIRED when count expires flags |= constants.TX_COUNTEVT 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)) return seconds, microseconds ival1_seconds, ival1_usec = split_time(initial_period) ival2_seconds, ival2_usec = split_time(subsequent_period) return build_bcm_header( opcode, flags, count, ival1_seconds, ival1_usec, ival2_seconds, ival2_usec, can_id, nframes, ) def build_bcm_update_header(can_id: int, msg_flags: int, nframes: int = 1) -> bytes: return build_bcm_header( constants.CAN_BCM_TX_SETUP, msg_flags, 0, 0, 0, 0, 0, can_id, nframes ) def 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: # Flags not valid in non-FD frames flags = 0 return can_id, can_dlc, flags, frame[8 : 8 + can_dlc] def create_bcm_socket(channel: str) -> socket.socket: """create a broadcast manager socket and connect to the given interface""" s = socket.socket(constants.PF_CAN, socket.SOCK_DGRAM, constants.CAN_BCM) s.connect((channel,)) return s def send_bcm(bcm_socket: socket.socket, data: bytes) -> int: """ Send raw frame to a BCM socket and handle errors. """ try: return bcm_socket.send(data) except OSError as error: base = f"Couldn't send CAN BCM frame due to OS Error: {error.strerror}" if error.errno == errno.EINVAL: specific_message = " You are probably referring to a non-existing frame." elif error.errno == errno.ENETDOWN: specific_message = " The CAN interface appears to be down." elif error.errno == errno.EBADF: specific_message = " The CAN socket appears to be closed." else: specific_message = "" raise can.CanOperationError(base + specific_message, error.errno) from error def _compose_arbitration_id(message: Message) -> int: can_id = message.arbitration_id if message.is_extended_id: log.debug("sending an extended id type message") can_id |= constants.CAN_EFF_FLAG if message.is_remote_frame: log.debug("requesting a remote frame") can_id |= constants.CAN_RTR_FLAG if message.is_error_frame: log.debug("sending error frame") can_id |= constants.CAN_ERR_FLAG return can_id class CyclicSendTask( LimitedDurationCyclicSendTaskABC, ModifiableCyclicTaskABC, RestartableCyclicTaskABC ): """ A SocketCAN cyclic send task supports: - setting of a task duration - modifying the data - stopping then subsequent restarting of the task """ def __init__( self, bcm_socket: socket.socket, task_id: int, messages: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, autostart: bool = True, ) -> None: """Construct and :meth:`~start` a task. :param bcm_socket: An open BCM socket on the desired CAN channel. :param task_id: The identifier used to uniquely reference particular cyclic send task within Linux BCM. :param messages: The messages to be sent periodically. :param period: The rate in seconds at which to send the messages. :param duration: Approximate duration in seconds to send the messages for. """ # The following are assigned by LimitedDurationCyclicSendTaskABC: # - self.messages # - self.period # - self.duration super().__init__(messages, period, duration) self.bcm_socket = bcm_socket self.task_id = task_id if autostart: self._tx_setup(self.messages) def _tx_setup( self, messages: Sequence[Message], raise_if_task_exists: bool = True, ) -> None: # Create a low level packed frame to pass to the kernel body = bytearray() self.flags = constants.CAN_FD_FRAME if messages[0].is_fd else 0 if self.duration: count = int(self.duration / self.period) ival1 = self.period ival2 = 0.0 else: count = 0 ival1 = 0.0 ival2 = self.period if raise_if_task_exists: self._check_bcm_task() header = build_bcm_transmit_header( self.task_id, count, ival1, ival2, self.flags, nframes=len(messages) ) for message in messages: body += build_can_frame(message) log.debug("Sending BCM command") send_bcm(self.bcm_socket, header + body) def _check_bcm_task(self) -> None: # Do a TX_READ on a task ID, and check if we get EINVAL. If so, # then we are referring to a CAN message with an existing ID check_header = build_bcm_header( opcode=constants.CAN_BCM_TX_READ, flags=0, count=0, ival1_seconds=0, ival1_usec=0, ival2_seconds=0, ival2_usec=0, can_id=self.task_id, nframes=0, ) log.debug( "Reading properties of (cyclic) transmission task id=%d", self.task_id ) try: self.bcm_socket.send(check_header) except OSError as error: if error.errno != errno.EINVAL: raise can.CanOperationError("failed to check", error.errno) from error else: log.debug("Invalid argument - transmission task not known to kernel") else: # No exception raised - transmission task with this ID exists in kernel. # Existence of an existing transmission task might not be a problem! raise can.CanOperationError( f"A periodic task for task ID {self.task_id} is already in progress " "by the SocketCAN Linux layer" ) def stop(self) -> None: """Stop a task by sending TX_DELETE message to Linux kernel. This will delete the entry for the transmission of the CAN-message with the specified ``task_id`` identifier. The message length for the command TX_DELETE is {[bcm_msg_head]} (only the header). """ log.debug("Stopping periodic task") stopframe = build_bcm_tx_delete_header(self.task_id, self.flags) send_bcm(self.bcm_socket, stopframe) def modify_data(self, messages: Union[Sequence[Message], Message]) -> None: """Update the contents of the periodically sent CAN messages by sending TX_SETUP message to Linux kernel. The number of new cyclic messages to be sent must be equal to the original number of messages originally specified for this task. .. note:: The messages must all have the same :attr:`~can.Message.arbitration_id` like the first message. :param messages: The messages with the new :attr:`can.Message.data`. """ messages = self._check_and_convert_messages(messages) self._check_modified_messages(messages) self.messages = messages body = bytearray() header = build_bcm_update_header( can_id=self.task_id, msg_flags=self.flags, nframes=len(messages) ) for message in messages: body += build_can_frame(message) log.debug("Sending BCM command") send_bcm(self.bcm_socket, header + body) def start(self) -> None: """Restart a periodic task by sending TX_SETUP message to Linux kernel. It verifies presence of the particular BCM task through sending TX_READ message to Linux kernel prior to scheduling. :raises ValueError: If the task referenced by ``task_id`` is already running. """ self._tx_setup(self.messages, raise_if_task_exists=False) class MultiRateCyclicSendTask(CyclicSendTask): """Exposes more of the full power of the TX_SETUP opcode.""" def __init__( self, channel: socket.socket, task_id: int, messages: Sequence[Message], count: int, initial_period: float, subsequent_period: float, ): super().__init__(channel, task_id, messages, subsequent_period) # Create a low level packed frame to pass to the kernel header = build_bcm_transmit_header( self.task_id, count, initial_period, subsequent_period, self.flags, nframes=len(messages), ) body = bytearray() for message in messages: body += build_can_frame(message) log.info("Sending BCM TX_SETUP command") send_bcm(self.bcm_socket, header + body) def create_socket() -> socket.socket: """Creates a raw CAN socket. The socket will be returned unbound to any interface. """ sock = socket.socket(constants.PF_CAN, socket.SOCK_RAW, constants.CAN_RAW) log.info("Created a socket") return sock def bind_socket(sock: socket.socket, channel: str = "can0") -> None: """ Binds the given socket to the given interface. :param sock: The socket to be bound :param channel: The channel / interface to bind to :raises OSError: If the specified interface isn't found. """ log.debug("Binding socket to channel=%s", channel) sock.bind((channel,)) log.debug("Bound socket.") def capture_message( sock: socket.socket, get_channel: bool = False ) -> Optional[Message]: """ Captures a message from given socket. :param sock: The socket to read a message from. :param get_channel: Find out which channel the message comes from. :return: The received message, or None on failure. """ # Fetching the Arb ID, DLC and Data try: cf, ancillary_data, msg_flags, addr = sock.recvmsg( constants.CANFD_MTU, RECEIVED_ANCILLARY_BUFFER_SIZE ) if get_channel: channel = addr[0] if isinstance(addr, tuple) else addr else: channel = None except OSError as error: raise can.CanOperationError( f"Error receiving: {error.strerror}", error.errno ) from error can_id, can_dlc, flags, data = dissect_can_frame(cf) # Fetching the timestamp assert len(ancillary_data) == 1, "only requested a single extra field" cmsg_level, cmsg_type, cmsg_data = ancillary_data[0] assert ( cmsg_level == socket.SOL_SOCKET and cmsg_type == constants.SO_TIMESTAMPNS ), "received control message type that was not requested" # see https://man7.org/linux/man-pages/man3/timespec.3.html -> struct timespec for details seconds, nanoseconds = RECEIVED_TIMESTAMP_STRUCT.unpack_from(cmsg_data) if nanoseconds >= 1e9: raise can.CanOperationError( f"Timestamp nanoseconds field was out of range: {nanoseconds} not less than 1e9" ) timestamp = seconds + nanoseconds * 1e-9 # EXT, RTR, ERR flags -> boolean attributes # /* special address description flags for the CAN_ID */ # #define CAN_EFF_FLAG 0x80000000U /* EFF/SFF is set in the MSB */ # #define CAN_RTR_FLAG 0x40000000U /* remote transmission request */ # #define CAN_ERR_FLAG 0x20000000U /* error frame */ is_extended_frame_format = bool(can_id & constants.CAN_EFF_FLAG) is_remote_transmission_request = bool(can_id & constants.CAN_RTR_FLAG) is_error_frame = bool(can_id & constants.CAN_ERR_FLAG) is_fd = len(cf) == constants.CANFD_MTU bitrate_switch = bool(flags & constants.CANFD_BRS) error_state_indicator = bool(flags & constants.CANFD_ESI) # Section 4.7.1: MSG_DONTROUTE: set when the received frame was created on the local host. is_rx = not bool(msg_flags & socket.MSG_DONTROUTE) if is_extended_frame_format: # log.debug("CAN: Extended") # TODO does this depend on SFF or EFF? arbitration_id = can_id & 0x1FFFFFFF else: # log.debug("CAN: Standard") arbitration_id = can_id & 0x000007FF msg = Message( timestamp=timestamp, channel=channel, arbitration_id=arbitration_id, is_extended_id=is_extended_frame_format, is_remote_frame=is_remote_transmission_request, is_error_frame=is_error_frame, is_fd=is_fd, is_rx=is_rx, bitrate_switch=bitrate_switch, error_state_indicator=error_state_indicator, dlc=can_dlc, data=data, ) return msg class SocketcanBus(BusABC): # pylint: disable=abstract-method """A SocketCAN interface to CAN. It implements :meth:`can.BusABC._detect_available_configs` to search for available interfaces. """ def __init__( self, channel: str = "", receive_own_messages: bool = False, local_loopback: bool = True, fd: bool = False, can_filters: Optional[CanFilters] = None, ignore_rx_error_frames=False, **kwargs, ) -> None: """Creates a new socketcan bus. If setting some socket options fails, an error will be printed but no exception will be thrown. This includes enabling: - that own messages should be received, - CAN-FD frames and - error frames. :param channel: The can interface name with which to create this bus. An example channel would be 'vcan0' or 'can0'. An empty string '' will receive messages from all channels. In that case any sent messages must be explicitly addressed to a channel using :attr:`can.Message.channel`. :param receive_own_messages: If transmitted messages should also be received by this bus. :param local_loopback: If local loopback should be enabled on this bus. Please note that local loopback does not mean that messages sent on a socket will be readable on the same socket, they will only be readable on other open sockets on the same machine. More info can be read on the socketcan documentation: See https://www.kernel.org/doc/html/latest/networking/can.html#socketcan-local-loopback1 :param fd: If CAN-FD frames should be supported. :param can_filters: See :meth:`can.BusABC.set_filters`. :param ignore_rx_error_frames: If incoming error frames should be discarded. """ self.socket = create_socket() self.channel = channel self.channel_info = f"socketcan channel '{channel}'" self._bcm_sockets: Dict[str, socket.socket] = {} self._is_filtered = False self._task_id = 0 self._task_id_guard = threading.Lock() self._can_protocol = CanProtocol.CAN_FD if fd else CanProtocol.CAN_20 # set the local_loopback parameter try: self.socket.setsockopt( constants.SOL_CAN_RAW, constants.CAN_RAW_LOOPBACK, 1 if local_loopback else 0, ) except OSError as error: log.error("Could not set local loopback flag(%s)", error) # set the receive_own_messages parameter try: self.socket.setsockopt( constants.SOL_CAN_RAW, constants.CAN_RAW_RECV_OWN_MSGS, 1 if receive_own_messages else 0, ) except OSError as error: log.error("Could not receive own messages (%s)", error) # enable CAN-FD frames if desired if fd: try: self.socket.setsockopt( constants.SOL_CAN_RAW, constants.CAN_RAW_FD_FRAMES, 1 ) except OSError as error: log.error("Could not enable CAN-FD frames (%s)", error) if not ignore_rx_error_frames: # enable error frames try: self.socket.setsockopt( constants.SOL_CAN_RAW, constants.CAN_RAW_ERR_FILTER, 0x1FFFFFFF ) except OSError as error: log.error("Could not enable error frames (%s)", error) # enable nanosecond resolution timestamping # we can always do this since # 1) it is guaranteed to be at least as precise as without # 2) it is available since Linux 2.6.22, and CAN support was only added afterward # so this is always supported by the kernel self.socket.setsockopt(socket.SOL_SOCKET, constants.SO_TIMESTAMPNS, 1) try: bind_socket(self.socket, channel) kwargs.update( { "receive_own_messages": receive_own_messages, "fd": fd, "local_loopback": local_loopback, } ) except OSError as error: log.error("Could not access SocketCAN device %s (%s)", channel, error) raise super().__init__( channel=channel, can_filters=can_filters, **kwargs, ) def shutdown(self) -> None: """Stops all active periodic tasks and closes the socket.""" super().shutdown() for channel, bcm_socket in self._bcm_sockets.items(): log.debug("Closing bcm socket for channel %s", channel) bcm_socket.close() log.debug("Closing raw can socket") self.socket.close() def _recv_internal( self, timeout: Optional[float] ) -> 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) ready_receive_sockets, _, _ = select.select([self.socket], [], [], timeout) except OSError as error: # something bad happened (e.g. the interface went down) raise can.CanOperationError( f"Failed to receive: {error.strerror}", error.errno ) from error if ready_receive_sockets: # not empty get_channel = self.channel == "" msg = capture_message(self.socket, get_channel) if msg and not msg.channel and self.channel: # Default to our own channel msg.channel = self.channel return msg, self._is_filtered # socket wasn't readable or timeout occurred return None, self._is_filtered def send(self, msg: Message, timeout: Optional[float] = None) -> None: """Transmit a message to the CAN bus. :param msg: A message object. :param timeout: Wait up to this many seconds for the transmit queue to be ready. If not given, the call may fail immediately. :raises ~can.exceptions.CanError: if the message could not be written. """ log.debug("We've been asked to write a message to the bus") logger_tx = log.getChild("tx") logger_tx.debug("sending: %s", msg) started = time.time() # If no timeout is given, poll for availability if timeout is None: timeout = 0 time_left = timeout data = build_can_frame(msg) while time_left >= 0: # Wait for write availability ready = select.select([], [self.socket], [], time_left)[1] if not ready: # Timeout break channel = str(msg.channel) if msg.channel else None sent = self._send_once(data, channel) if sent == len(data): return # Not all data were sent, try again with remaining data data = data[sent:] time_left = timeout - (time.time() - started) raise can.CanOperationError("Transmit buffer full") def _send_once(self, data: bytes, channel: Optional[str] = None) -> int: try: if self.channel == "" and channel: # Message must be addressed to a specific channel sent = self.socket.sendto(data, (channel,)) else: sent = self.socket.send(data) except OSError as error: raise can.CanOperationError( f"Failed to transmit: {error.strerror}", error.errno ) from error return sent def _send_periodic_internal( self, msgs: Union[Sequence[Message], Message], period: float, duration: Optional[float] = None, autostart: bool = True, modifier_callback: Optional[Callable[[Message], None]] = None, ) -> can.broadcastmanager.CyclicSendTaskABC: """Start sending messages at a given period on this bus. The Linux kernel's Broadcast Manager SocketCAN API is used to schedule periodic sending of CAN messages. The wrapping 32-bit counter (see :meth:`~_get_next_task_id()`) designated to distinguish different :class:`CyclicSendTask` within BCM provides flexibility to schedule CAN messages sending with the same CAN ID, but different CAN data. :param msgs: The message(s) to be sent periodically. :param period: The rate in seconds at which to send the messages. :param duration: Approximate duration in seconds to continue sending messages. If no duration is provided, the task will continue indefinitely. :param autostart: If True (the default) the sending task will immediately start after creation. Otherwise, the task has to be started by calling the tasks :meth:`~can.RestartableCyclicTaskABC.start` method on it. :raises ValueError: If task identifier passed to :class:`CyclicSendTask` can't be used to schedule new task in Linux BCM. :return: A :class:`CyclicSendTask` task instance. This can be used to modify the data, pause/resume the transmission and to stop the transmission. .. note:: Note the duration before the messages stop being sent may not be exactly the same as the duration specified by the user. In general the message will be sent at the given rate until at least *duration* seconds. """ if modifier_callback is None: msgs = LimitedDurationCyclicSendTaskABC._check_and_convert_messages( # pylint: disable=protected-access msgs ) msgs_channel = str(msgs[0].channel) if msgs[0].channel else None bcm_socket = self._get_bcm_socket(msgs_channel or self.channel) task_id = self._get_next_task_id() task = CyclicSendTask( bcm_socket, task_id, msgs, period, duration, autostart=autostart ) return task # fallback to thread based cyclic task warnings.warn( f"{self.__class__.__name__} falls back to a thread-based cyclic task, " "when the `modifier_callback` argument is given.", stacklevel=3, ) return BusABC._send_periodic_internal( self, msgs=msgs, period=period, duration=duration, autostart=autostart, modifier_callback=modifier_callback, ) def _get_next_task_id(self) -> int: with self._task_id_guard: self._task_id = (self._task_id + 1) % (2**32 - 1) return self._task_id def _get_bcm_socket(self, channel: str) -> socket.socket: if channel not in self._bcm_sockets: self._bcm_sockets[channel] = create_bcm_socket(self.channel) return self._bcm_sockets[channel] def _apply_filters(self, filters: Optional[can.typechecking.CanFilters]) -> None: try: self.socket.setsockopt( constants.SOL_CAN_RAW, constants.CAN_RAW_FILTER, pack_filters(filters) ) except OSError as error: # fall back to "software filtering" (= not in kernel) self._is_filtered = False log.error( "Setting filters failed; falling back to software filtering (not in kernel): %s", error, ) else: self._is_filtered = True def fileno(self) -> int: return self.socket.fileno() @staticmethod def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: return [ {"interface": "socketcan", "channel": channel} for channel in find_available_interfaces() ] python-can-4.5.0/can/interfaces/socketcan/utils.py000066400000000000000000000054211472200326600221540ustar00rootroot00000000000000""" Defines common socketcan functions. """ import errno import json import logging import os import struct import subprocess import sys from typing import List, Optional, cast from can import typechecking from can.interfaces.socketcan.constants import CAN_EFF_FLAG log = logging.getLogger(__name__) def pack_filters(can_filters: Optional[typechecking.CanFilters] = None) -> bytes: if can_filters is None: # Pass all messages can_filters = [{"can_id": 0, "can_mask": 0}] can_filter_fmt = f"={2 * len(can_filters)}I" filter_data = [] for can_filter in can_filters: can_id = can_filter["can_id"] can_mask = can_filter["can_mask"] if "extended" in can_filter: can_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"]: can_id |= CAN_EFF_FLAG filter_data.append(can_id) filter_data.append(can_mask) return struct.pack(can_filter_fmt, *filter_data) 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 is logged to the console and an empty list is returned. :return: The list of available and active CAN interfaces or an empty list of the command failed """ if sys.platform != "linux": return [] try: command = ["ip", "-json", "link", "list", "up"] output_str = subprocess.check_output(command, text=True) except Exception: # pylint: disable=broad-except # subprocess.CalledProcessError is too specific log.exception("failed to fetch opened can devices from ip link") return [] try: output_json = json.loads(output_str) except json.JSONDecodeError: log.exception("Failed to parse ip link JSON output: %s", output_str) return [] log.debug( "find_available_interfaces(): detected these interfaces (before filtering): %s", output_json, ) interfaces = [i["ifname"] for i in output_json if i.get("link_type") == "can"] return interfaces def error_code_to_str(code: Optional[int]) -> str: """ Converts a given error code (errno) to a useful and human readable string. :param code: a possibly invalid/unknown error code :returns: a string explaining and containing the given error code, or a string explaining that the errorcode is unknown if that is the case """ name = errno.errorcode.get(code, "UNKNOWN") # type: ignore description = os.strerror(code) if code is not None else "NO DESCRIPTION AVAILABLE" return f"{name} (errno {code}): {description}" python-can-4.5.0/can/interfaces/socketcand/000077500000000000000000000000001472200326600206045ustar00rootroot00000000000000python-can-4.5.0/can/interfaces/socketcand/__init__.py000066400000000000000000000004231472200326600227140ustar00rootroot00000000000000""" Interface to socketcand see https://github.com/linux-can/socketcand Copyright (C) 2021 DOMOLOGIC GmbH http://www.domologic.de """ __all__ = [ "SocketCanDaemonBus", "detect_beacon", "socketcand", ] from .socketcand import SocketCanDaemonBus, detect_beacon python-can-4.5.0/can/interfaces/socketcand/socketcand.py000066400000000000000000000311601472200326600232750ustar00rootroot00000000000000""" Interface to socketcand see https://github.com/linux-can/socketcand Authors: Marvin Seiler, Gerrit Telkamp Copyright (C) 2021 DOMOLOGIC GmbH http://www.domologic.de """ import logging import os import select import socket import time import traceback import urllib.parse as urlparselib import xml.etree.ElementTree as ET from collections import deque from typing import List import can log = logging.getLogger(__name__) DEFAULT_SOCKETCAND_DISCOVERY_ADDRESS = "" DEFAULT_SOCKETCAND_DISCOVERY_PORT = 42000 def detect_beacon(timeout_ms: int = 3100) -> List[can.typechecking.AutoDetectedConfig]: """ Detects socketcand servers This is what :meth:`can.detect_available_configs` ends up calling to search for available socketcand servers with a default timeout of 3100ms (socketcand sends a beacon packet every 3000ms). Using this method directly allows for adjusting the timeout. Extending the timeout beyond the default time period could be useful if UDP packet loss is a concern. :param timeout_ms: Timeout in milliseconds to wait for socketcand beacon packets :return: See :meth:`~can.detect_available_configs` """ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.bind( (DEFAULT_SOCKETCAND_DISCOVERY_ADDRESS, DEFAULT_SOCKETCAND_DISCOVERY_PORT) ) log.info( "Listening on for socketcand UDP advertisement on %s:%s", DEFAULT_SOCKETCAND_DISCOVERY_ADDRESS, DEFAULT_SOCKETCAND_DISCOVERY_PORT, ) now = time.time() * 1000 end_time = now + timeout_ms while (time.time() * 1000) < end_time: try: # get all sockets that are ready (can be a list with a single value # being self.socket or an empty list if self.socket is not ready) ready_receive_sockets, _, _ = select.select([sock], [], [], 1) if not ready_receive_sockets: log.debug("No advertisement received") continue msg = sock.recv(1024).decode("utf-8") root = ET.fromstring(msg) if root.tag != "CANBeacon": log.debug("Unexpected message received over UDP") continue det_devs = [] det_host = None det_port = None for child in root: if child.tag == "Bus": bus_name = child.attrib["name"] det_devs.append(bus_name) elif child.tag == "URL": url = urlparselib.urlparse(child.text) det_host = url.hostname det_port = url.port if not det_devs: log.debug( "Got advertisement, but no SocketCAN devices advertised by socketcand" ) continue if (det_host is None) or (det_port is None): det_host = None det_port = None log.debug( "Got advertisement, but no SocketCAN URL advertised by socketcand" ) continue log.info(f"Found SocketCAN devices: {det_devs}") return [ { "interface": "socketcand", "host": det_host, "port": det_port, "channel": channel, } for channel in det_devs ] except ET.ParseError: log.debug("Unexpected message received over UDP") continue except Exception as exc: # something bad happened (e.g. the interface went down) log.error(f"Failed to detect beacon: {exc} {traceback.format_exc()}") raise OSError( f"Failed to detect beacon: {exc} {traceback.format_exc()}" ) from exc return [] def convert_ascii_message_to_can_message(ascii_msg: str) -> can.Message: if not ascii_msg.startswith("< frame ") or not ascii_msg.endswith(" >"): log.warning(f"Could not parse ascii message: {ascii_msg}") return None else: # frame_string = ascii_msg.removeprefix("< frame ").removesuffix(" >") frame_string = ascii_msg[8:-2] parts = frame_string.split(" ", 3) can_id, timestamp = int(parts[0], 16), float(parts[1]) is_ext = len(parts[0]) != 3 data = bytearray.fromhex(parts[2]) can_dlc = len(data) can_message = can.Message( timestamp=timestamp, arbitration_id=can_id, data=data, dlc=can_dlc, is_extended_id=is_ext, is_rx=True, ) return can_message def convert_can_message_to_ascii_message(can_message: can.Message) -> str: # Note: socketcan bus adds extended flag, remote_frame_flag & error_flag to id # not sure if that is necessary here can_id = can_message.arbitration_id if can_message.is_extended_id: can_id_string = f"{(can_id&0x1FFFFFFF):08X}" else: can_id_string = f"{(can_id&0x7FF):03X}" # Note: seems like we cannot add CANFD_BRS (bitrate_switch) and CANFD_ESI (error_state_indicator) flags data = can_message.data length = can_message.dlc bytes_string = " ".join(f"{x:x}" for x in data[0:length]) return f"< send {can_id_string} {length:X} {bytes_string} >" def connect_to_server(s, host, port): timeout_ms = 10000 now = time.time() * 1000 end_time = now + timeout_ms while now < end_time: try: s.connect((host, port)) return except Exception as e: log.warning(f"Failed to connect to server: {type(e)} Message: {e}") now = time.time() * 1000 raise TimeoutError( f"connect_to_server: Failed to connect server for {timeout_ms} ms" ) class SocketCanDaemonBus(can.BusABC): def __init__(self, channel, host, port, tcp_tune=False, can_filters=None, **kwargs): """Connects to a CAN bus served by socketcand. It implements :meth:`can.BusABC._detect_available_configs` to search for available interfaces. It will attempt to connect to the server for up to 10s, after which a TimeoutError exception will be thrown. If the handshake with the socketcand server fails, a CanError exception is thrown. :param channel: The can interface name served by socketcand. An example channel would be 'vcan0' or 'can0'. :param host: The host address of the socketcand server. :param port: The port of the socketcand server. :param tcp_tune: This tunes the TCP socket for low latency (TCP_NODELAY, and TCP_QUICKACK). This option is not available under windows. :param can_filters: See :meth:`can.BusABC.set_filters`. """ self.__host = host self.__port = port self.__tcp_tune = tcp_tune self.__socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if self.__tcp_tune: if os.name == "nt": self.__tcp_tune = False log.warning("'tcp_tune' not available in Windows. Setting to False") else: self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) self.__message_buffer = deque() self.__receive_buffer = "" # i know string is not the most efficient here self.channel = channel self.channel_info = f"socketcand on {channel}@{host}:{port}" connect_to_server(self.__socket, self.__host, self.__port) self._expect_msg("< hi >") log.info( f"SocketCanDaemonBus: connected with address {self.__socket.getsockname()}" ) self._tcp_send(f"< open {channel} >") self._expect_msg("< ok >") self._tcp_send("< rawmode >") self._expect_msg("< ok >") super().__init__(channel=channel, can_filters=can_filters, **kwargs) def _recv_internal(self, timeout): if len(self.__message_buffer) != 0: can_message = self.__message_buffer.popleft() return can_message, False try: # get all sockets that are ready (can be a list with a single value # being self.socket or an empty list if self.socket is not ready) ready_receive_sockets, _, _ = select.select( [self.__socket], [], [], timeout ) except OSError as exc: # something bad happened (e.g. the interface went down) log.error(f"Failed to receive: {exc}") raise can.CanError(f"Failed to receive: {exc}") from exc try: if not ready_receive_sockets: # socket wasn't readable or timeout occurred log.debug("Socket not ready") return None, False ascii_msg = self.__socket.recv(1024).decode( "ascii" ) # may contain multiple messages if self.__tcp_tune: self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1) self.__receive_buffer += ascii_msg log.debug(f"Received Ascii Message: {ascii_msg}") buffer_view = self.__receive_buffer chars_processed_successfully = 0 while True: if len(buffer_view) == 0: break start = buffer_view.find("<") if start == -1: log.warning( f"Bad data: No opening < found => discarding entire buffer '{buffer_view}'" ) chars_processed_successfully = len(self.__receive_buffer) break end = buffer_view.find(">") if end == -1: log.warning("Got incomplete message => waiting for more data") if len(buffer_view) > 200: log.warning( "Incomplete message exceeds 200 chars => Discarding" ) chars_processed_successfully = len(self.__receive_buffer) break chars_processed_successfully += end + 1 single_message = buffer_view[start : end + 1] parsed_can_message = convert_ascii_message_to_can_message( single_message ) if parsed_can_message is None: log.warning(f"Invalid Frame: {single_message}") else: parsed_can_message.channel = self.channel self.__message_buffer.append(parsed_can_message) buffer_view = buffer_view[end + 1 :] self.__receive_buffer = self.__receive_buffer[chars_processed_successfully:] can_message = ( None if len(self.__message_buffer) == 0 else self.__message_buffer.popleft() ) return can_message, False except Exception as exc: log.error(f"Failed to receive: {exc} {traceback.format_exc()}") raise can.CanError( f"Failed to receive: {exc} {traceback.format_exc()}" ) from exc def _tcp_send(self, msg: str): log.debug(f"Sending TCP Message: '{msg}'") self.__socket.sendall(msg.encode("ascii")) if self.__tcp_tune: self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1) def _expect_msg(self, msg): ascii_msg = self.__socket.recv(256).decode("ascii") if self.__tcp_tune: self.__socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, 1) if not ascii_msg == msg: raise can.CanError(f"Expected '{msg}' got: '{ascii_msg}'") def send(self, msg, timeout=None): """Transmit a message to the CAN bus. :param msg: A message object. :param timeout: Ignored """ ascii_msg = convert_can_message_to_ascii_message(msg) self._tcp_send(ascii_msg) def shutdown(self): """Stops all active periodic tasks and closes the socket.""" super().shutdown() self.__socket.close() @staticmethod def _detect_available_configs() -> List[can.typechecking.AutoDetectedConfig]: try: return detect_beacon() except Exception as e: log.warning(f"Could not detect socketcand beacon: {e}") return [] python-can-4.5.0/can/interfaces/systec/000077500000000000000000000000001472200326600200005ustar00rootroot00000000000000python-can-4.5.0/can/interfaces/systec/__init__.py000066400000000000000000000002401472200326600221050ustar00rootroot00000000000000__all__ = [ "UcanBus", "constants", "exceptions", "structures", "ucan", "ucanbus", ] from can.interfaces.systec.ucanbus import UcanBus python-can-4.5.0/can/interfaces/systec/constants.py000066400000000000000000000542541472200326600224000ustar00rootroot00000000000000from ctypes import c_ubyte as BYTE from ctypes import c_ulong as DWORD from ctypes import c_ushort as WORD #: Maximum number of modules that are supported. MAX_MODULES = 64 #: Maximum number of applications that can use the USB-CAN-library. MAX_INSTANCES = 64 #: With the method :meth:`UcanServer.init_can` the module is used, which is detected at first. #: This value only should be used in case only one module is connected to the computer. ANY_MODULE = 255 #: No valid USB-CAN Handle (only used internally). INVALID_HANDLE = 0xFF class Baudrate(WORD): """ Specifies pre-defined baud rate values for GW-001, GW-002 and all systec USB-CANmoduls. .. seealso:: :meth:`UcanServer.init_can` :meth:`UcanServer.set_baudrate` :meth:`UcanServer.get_baudrate_message` :class:`BaudrateEx` """ #: 1000 kBit/sec BAUD_1MBit = 0x14 #: 800 kBit/sec BAUD_800kBit = 0x16 #: 500 kBit/sec BAUD_500kBit = 0x1C #: 250 kBit/sec BAUD_250kBit = 0x11C #: 125 kBit/sec BAUD_125kBit = 0x31C #: 100 kBit/sec BAUD_100kBit = 0x432F #: 50 kBit/sec BAUD_50kBit = 0x472F #: 20 kBit/sec BAUD_20kBit = 0x532F #: 10 kBit/sec BAUD_10kBit = 0x672F #: Uses pre-defined extended values of baudrate for all systec USB-CANmoduls. BAUD_USE_BTREX = 0x0 #: Automatic baud rate detection (not implemented in this version). BAUD_AUTO = -1 class BaudrateEx(DWORD): """ Specifies pre-defined baud rate values for all systec USB-CANmoduls. These values cannot be used for GW-001 and GW-002! Use values from enum :class:`Baudrate` instead. .. seealso:: :meth:`UcanServer.init_can` :meth:`UcanServer.set_baudrate` :meth:`UcanServer.get_baudrate_ex_message` :class:`Baudrate` """ #: G3: 1000 kBit/sec BAUDEX_1MBit = 0x20354 #: G3: 800 kBit/sec BAUDEX_800kBit = 0x30254 #: G3: 500 kBit/sec BAUDEX_500kBit = 0x50354 #: G3: 250 kBit/sec BAUDEX_250kBit = 0xB0354 #: G3: 125 kBit/sec BAUDEX_125kBit = 0x170354 #: G3: 100 kBit/sec BAUDEX_100kBit = 0x170466 #: G3: 50 kBit/sec BAUDEX_50kBit = 0x2F0466 #: G3: 20 kBit/sec BAUDEX_20kBit = 0x770466 #: G3: 10 kBit/sec (half CPU clock) BAUDEX_10kBit = 0x80770466 #: G3: 1000 kBit/sec Sample Point: 87,50% BAUDEX_SP2_1MBit = 0x20741 #: G3: 800 kBit/sec Sample Point: 86,67% BAUDEX_SP2_800kBit = 0x30731 #: G3: 500 kBit/sec Sample Point: 87,50% BAUDEX_SP2_500kBit = 0x50741 #: G3: 250 kBit/sec Sample Point: 87,50% BAUDEX_SP2_250kBit = 0xB0741 #: G3: 125 kBit/sec Sample Point: 87,50% BAUDEX_SP2_125kBit = 0x170741 #: G3: 100 kBit/sec Sample Point: 87,50% BAUDEX_SP2_100kBit = 0x1D1741 #: G3: 50 kBit/sec Sample Point: 87,50% BAUDEX_SP2_50kBit = 0x3B1741 #: G3: 20 kBit/sec Sample Point: 85,00% BAUDEX_SP2_20kBit = 0x771772 #: G3: 10 kBit/sec Sample Point: 85,00% (half CPU clock) BAUDEX_SP2_10kBit = 0x80771772 #: G4: 1000 kBit/sec Sample Point: 83,33% BAUDEX_G4_1MBit = 0x406F0000 #: G4: 800 kBit/sec Sample Point: 80,00% BAUDEX_G4_800kBit = 0x402A0001 #: G4: 500 kBit/sec Sample Point: 83,33% BAUDEX_G4_500kBit = 0x406F0001 #: G4: 250 kBit/sec Sample Point: 83,33% BAUDEX_G4_250kBit = 0x406F0003 #: G4: 125 kBit/sec Sample Point: 83,33% BAUDEX_G4_125kBit = 0x406F0007 #: G4: 100 kBit/sec Sample Point: 83,33% BAUDEX_G4_100kBit = 0x416F0009 #: G4: 50 kBit/sec Sample Point: 83,33% BAUDEX_G4_50kBit = 0x416F0013 #: G4: 20 kBit/sec Sample Point: 84,00% BAUDEX_G4_20kBit = 0x417F002F #: G4: 10 kBit/sec Sample Point: 84,00% (half CPU clock) BAUDEX_G4_10kBit = 0x417F005F #: Uses pre-defined values of baud rates of :class:`Baudrate`. BAUDEX_USE_BTR01 = 0x0 #: Automatic baud rate detection (not implemented in this version). BAUDEX_AUTO = 0xFFFFFFFF class MsgFrameFormat(BYTE): """ Specifies values for the frame format of CAN messages for member :attr:`CanMsg.m_bFF` in structure :class:`CanMsg`. These values can be combined. .. seealso:: :class:`CanMsg` """ #: standard CAN data frame with 11 bit ID (CAN2.0A spec.) MSG_FF_STD = 0x0 #: transmit echo MSG_FF_ECHO = 0x20 #: CAN remote request frame with MSG_FF_RTR = 0x40 #: extended CAN data frame with 29 bit ID (CAN2.0B spec.) MSG_FF_EXT = 0x80 class ReturnCode(BYTE): """ Specifies all return codes of all methods of this class. """ #: no error SUCCESSFUL = 0x0 # start of error codes coming from USB-CAN-library ERR = 0x1 # start of error codes coming from command interface between host and USB-CANmodul ERRCMD = 0x40 # start of warning codes WARNING = 0x80 # start of reserved codes which are only used internally RESERVED = 0xC0 #: could not created a resource (memory, handle, ...) ERR_RESOURCE = 0x1 #: the maximum number of opened modules is reached ERR_MAXMODULES = 0x2 #: the specified module is already in use ERR_HWINUSE = 0x3 #: the software versions of the module and library are incompatible ERR_ILLVERSION = 0x4 #: the module with the specified device number is not connected (or used by an other application) ERR_ILLHW = 0x5 #: wrong USB-CAN-Handle handed over to the function ERR_ILLHANDLE = 0x6 #: wrong parameter handed over to the function ERR_ILLPARAM = 0x7 #: instruction can not be processed at this time ERR_BUSY = 0x8 #: no answer from module ERR_TIMEOUT = 0x9 #: a request to the driver failed ERR_IOFAILED = 0xA #: a CAN message did not fit into the transmit buffer ERR_DLL_TXFULL = 0xB #: maximum number of applications is reached ERR_MAXINSTANCES = 0xC #: CAN interface is not yet initialized ERR_CANNOTINIT = 0xD #: USB-CANmodul was disconnected ERR_DISCONECT = 0xE #: the needed device class does not exist ERR_NOHWCLASS = 0xF #: illegal CAN channel ERR_ILLCHANNEL = 0x10 #: reserved ERR_RESERVED1 = 0x11 #: the API function can not be used with this hardware ERR_ILLHWTYPE = 0x12 #: the received response does not match to the transmitted command ERRCMD_NOTEQU = 0x40 #: no access to the CAN controller ERRCMD_REGTST = 0x41 #: the module could not interpret the command ERRCMD_ILLCMD = 0x42 #: error while reading the EEPROM ERRCMD_EEPROM = 0x43 #: reserved ERRCMD_RESERVED1 = 0x44 #: reserved ERRCMD_RESERVED2 = 0x45 #: reserved ERRCMD_RESERVED3 = 0x46 #: illegal baud rate value specified in BTR0/BTR1 for systec USB-CANmoduls ERRCMD_ILLBDR = 0x47 #: CAN channel is not initialized ERRCMD_NOTINIT = 0x48 #: CAN channel is already initialized ERRCMD_ALREADYINIT = 0x49 #: illegal sub-command specified ERRCMD_ILLSUBCMD = 0x4A #: illegal index specified (e.g. index for cyclic CAN messages) ERRCMD_ILLIDX = 0x4B #: cyclic CAN message(s) can not be defined because transmission of cyclic CAN messages is already running ERRCMD_RUNNING = 0x4C #: no CAN messages received WARN_NODATA = 0x80 #: overrun in receive buffer of the kernel driver WARN_SYS_RXOVERRUN = 0x81 #: overrun in receive buffer of the USB-CAN-library WARN_DLL_RXOVERRUN = 0x82 #: reserved WARN_RESERVED1 = 0x83 #: reserved WARN_RESERVED2 = 0x84 #: overrun in transmit buffer of the firmware (but this CAN message was successfully stored in buffer of the #: library) WARN_FW_TXOVERRUN = 0x85 #: overrun in receive buffer of the firmware (but this CAN message was successfully read) WARN_FW_RXOVERRUN = 0x86 #: reserved WARN_FW_TXMSGLOST = 0x87 #: pointer is NULL WARN_NULL_PTR = 0x90 #: not all CAN messages could be stored to the transmit buffer in USB-CAN-library (check output of parameter #: pdwCount_p) WARN_TXLIMIT = 0x91 #: reserved WARN_BUSY = 0x92 class CbEvent(BYTE): """ This enum defines events for the callback functions of the library. .. seealso:: :meth:`UcanServer.get_status` """ #: The USB-CANmodul has been initialized. EVENT_INITHW = 0 #: The CAN interface has been initialized. EVENT_init_can = 1 #: A new CAN message has been received. EVENT_RECEIVE = 2 #: The error state in the module has changed. EVENT_STATUS = 3 #: The CAN interface has been deinitialized. EVENT_DEINIT_CAN = 4 #: The USB-CANmodul has been deinitialized. EVENT_DEINITHW = 5 #: A new USB-CANmodul has been connected. EVENT_CONNECT = 6 #: Any USB-CANmodul has been disconnected. EVENT_DISCONNECT = 7 #: A USB-CANmodul has been disconnected during operation. EVENT_FATALDISCON = 8 #: Reserved EVENT_RESERVED1 = 0x80 class CanStatus(WORD): """ CAN error status bits. These bit values occurs in combination with the method :meth:`UcanServer.get_status`. .. seealso:: :meth:`UcanServer.get_status` :meth:`UcanServer.get_can_status_message` """ #: No error. CANERR_OK = 0x0 #: Transmit buffer of the CAN controller is full. CANERR_XMTFULL = 0x1 #: Receive buffer of the CAN controller is full. CANERR_OVERRUN = 0x2 #: Bus error: Error Limit 1 exceeded (Warning Limit reached) CANERR_BUSLIGHT = 0x4 #: Bus error: Error Limit 2 exceeded (Error Passive) CANERR_BUSHEAVY = 0x8 #: Bus error: CAN controller has gone into Bus-Off state. #: Method :meth:`UcanServer.reset_can` has to be called. CANERR_BUSOFF = 0x10 #: No CAN message is within the receive buffer. CANERR_QRCVEMPTY = 0x20 #: Receive buffer is full. CAN messages has been lost. CANERR_QOVERRUN = 0x40 #: Transmit buffer is full. CANERR_QXMTFULL = 0x80 #: Register test of the CAN controller failed. CANERR_REGTEST = 0x100 #: Memory test on hardware failed. CANERR_MEMTEST = 0x200 #: Transmit CAN message(s) was/were automatically deleted by firmware (transmit timeout). CANERR_TXMSGLOST = 0x400 class UsbStatus(WORD): """ USB error status bits. These bit values occurs in combination with the method :meth:`UcanServer.get_status`. .. seealso:: :meth:`UcanServer.get_status` """ #: No error. USBERR_OK = 0x0 #: Specifies the acceptance mask for receiving all CAN messages. #: #: .. seealso:: #: #: :const:`ACR_ALL` #: #: :meth:`UcanServer.init_can` #: #: :meth:`UcanServer.set_acceptance` AMR_ALL = 0xFFFFFFFF #: Specifies the acceptance code for receiving all CAN messages. #: #: .. seealso:: #: #: :const:`AMR_ALL` #: #: :meth:`UcanServer.init_can` #: #: :meth:`UcanServer.set_acceptance` ACR_ALL = 0x0 class OutputControl(BYTE): """ Specifies pre-defined values for the Output Control Register of SJA1000 on GW-001 and GW-002. These values are only important for GW-001 and GW-002. They does not have an effect on systec USB-CANmoduls. """ #: default OCR value for the standard USB-CANmodul GW-001/GW-002 OCR_DEFAULT = 0x1A #: OCR value for RS485 interface and galvanic isolation OCR_RS485_ISOLATED = 0x1E #: OCR value for RS485 interface but without galvanic isolation OCR_RS485_NOT_ISOLATED = 0xA #: Specifies the default value for the maximum number of entries in the receive and transmit buffer. DEFAULT_BUFFER_ENTRIES = 4096 class Channel(BYTE): """ Specifies values for the CAN channel to be used on multi-channel USB-CANmoduls. """ #: Specifies the first CAN channel (GW-001/GW-002 and USB-CANmodul1 only can be used with this channel). CHANNEL_CH0 = 0 #: Specifies the second CAN channel (this channel cannot be used with GW-001/GW-002 and USB-CANmodul1). CHANNEL_CH1 = 1 #: Specifies all CAN channels (can only be used with the method :meth:`UcanServer.shutdown`). CHANNEL_ALL = 254 #: Specifies the use of any channel (can only be used with the method :meth:`UcanServer.read_can_msg`). CHANNEL_ANY = 255 #: Specifies the first CAN channel (equivalent to :data:`CHANNEL_CH0`). CHANNEL_CAN1 = CHANNEL_CH0 #: Specifies the second CAN channel (equivalent to :data:`CHANNEL_CH1`). CHANNEL_CAN2 = CHANNEL_CH1 #: Specifies the LIN channel (currently not supported by the software). CHANNEL_LIN = CHANNEL_CH1 class ResetFlags(DWORD): """ Specifies flags for resetting USB-CANmodul with method :meth:`UcanServer.reset_can`. These flags can be used in combination. .. seealso:: :meth:`UcanServer.reset_can` """ #: reset everything RESET_ALL = 0x0 #: no CAN status reset (only supported for systec USB-CANmoduls) RESET_NO_STATUS = 0x1 #: no CAN controller reset RESET_NO_CANCTRL = 0x2 #: no transmit message counter reset RESET_NO_TXCOUNTER = 0x4 #: no receive message counter reset RESET_NO_RXCOUNTER = 0x8 #: no transmit message buffer reset at channel level RESET_NO_TXBUFFER_CH = 0x10 #: no transmit message buffer reset at USB-CAN-library level RESET_NO_TXBUFFER_DLL = 0x20 #: no transmit message buffer reset at firmware level RESET_NO_TXBUFFER_FW = 0x80 #: no receive message buffer reset at channel level RESET_NO_RXBUFFER_CH = 0x100 #: no receive message buffer reset at USB-CAN-library level RESET_NO_RXBUFFER_DLL = 0x200 #: no receive message buffer reset at kernel driver level RESET_NO_RXBUFFER_SYS = 0x400 #: no receive message buffer reset at firmware level RESET_NO_RXBUFFER_FW = 0x800 #: complete firmware reset (module will automatically reconnect at USB port in 500msec) RESET_FIRMWARE = 0xFFFFFFFF #: no reset of all message counters RESET_NO_COUNTER_ALL = RESET_NO_TXCOUNTER | RESET_NO_RXCOUNTER #: no reset of transmit message buffers at communication level (firmware, kernel and library) RESET_NO_TXBUFFER_COMM = RESET_NO_TXBUFFER_DLL | 0x40 | RESET_NO_TXBUFFER_FW #: no reset of receive message buffers at communication level (firmware, kernel and library) RESET_NO_RXBUFFER_COMM = ( RESET_NO_RXBUFFER_DLL | RESET_NO_RXBUFFER_SYS | RESET_NO_RXBUFFER_FW ) #: no reset of all transmit message buffers RESET_NO_TXBUFFER_ALL = RESET_NO_TXBUFFER_CH | RESET_NO_TXBUFFER_COMM #: no reset of all receive message buffers RESET_NO_RXBUFFER_ALL = RESET_NO_RXBUFFER_CH | RESET_NO_RXBUFFER_COMM #: no reset of all message buffers at communication level (firmware, kernel and library) RESET_NO_BUFFER_COMM = RESET_NO_TXBUFFER_COMM | RESET_NO_RXBUFFER_COMM #: no reset of all message buffers RESET_NO_BUFFER_ALL = RESET_NO_TXBUFFER_ALL | RESET_NO_RXBUFFER_ALL #: reset of the CAN status only RESET_ONLY_STATUS = 0xFFFF & ~RESET_NO_STATUS #: reset of the CAN controller only RESET_ONLY_CANCTRL = 0xFFFF & ~RESET_NO_CANCTRL #: reset of the transmit buffer in firmware only RESET_ONLY_TXBUFFER_FW = 0xFFFF & ~RESET_NO_TXBUFFER_FW #: reset of the receive buffer in firmware only RESET_ONLY_RXBUFFER_FW = 0xFFFF & ~RESET_NO_RXBUFFER_FW #: reset of the specified channel of the receive buffer only RESET_ONLY_RXCHANNEL_BUFF = 0xFFFF & ~RESET_NO_RXBUFFER_CH #: reset of the specified channel of the transmit buffer only RESET_ONLY_TXCHANNEL_BUFF = 0xFFFF & ~RESET_NO_TXBUFFER_CH #: reset of the receive buffer and receive message counter only RESET_ONLY_RX_BUFF = 0xFFFF & ~(RESET_NO_RXBUFFER_ALL | RESET_NO_RXCOUNTER) #: reset of the receive buffer and receive message counter (for GW-002) only RESET_ONLY_RX_BUFF_GW002 = 0xFFFF & ~( RESET_NO_RXBUFFER_ALL | RESET_NO_RXCOUNTER | RESET_NO_TXBUFFER_FW ) #: reset of the transmit buffer and transmit message counter only RESET_ONLY_TX_BUFF = 0xFFFF & ~(RESET_NO_TXBUFFER_ALL | RESET_NO_TXCOUNTER) #: reset of all buffers and all message counters only RESET_ONLY_ALL_BUFF = RESET_ONLY_RX_BUFF & RESET_ONLY_TX_BUFF #: reset of all message counters only RESET_ONLY_ALL_COUNTER = 0xFFFF & ~RESET_NO_COUNTER_ALL PRODCODE_PID_TWO_CHA = 0x1 PRODCODE_PID_TERM = 0x1 PRODCODE_PID_RBUSER = 0x1 PRODCODE_PID_RBCAN = 0x1 PRODCODE_PID_G4 = 0x20 PRODCODE_PID_RESVD = 0x40 PRODCODE_MASK_DID = 0xFFFF0000 PRODCODE_MASK_PID = 0xFFFF PRODCODE_MASK_PIDG3 = PRODCODE_MASK_PID & 0xFFFFFFBF class ProductCode(WORD): """ These values defines product codes for all known USB-CANmodul derivatives received in member :attr:`HardwareInfoEx.m_dwProductCode` of structure :class:`HardwareInfoEx` with method :meth:`UcanServer.get_hardware_info`. .. seealso:: :meth:`UcanServer.get_hardware_info` :class:`HardwareInfoEx` """ #: Product code for GW-001 (outdated). PRODCODE_PID_GW001 = 0x1100 #: Product code for GW-002 (outdated). PRODCODE_PID_GW002 = 0x1102 #: Product code for Multiport CAN-to-USB G3. PRODCODE_PID_MULTIPORT = 0x1103 #: Product code for USB-CANmodul1 G3. PRODCODE_PID_BASIC = 0x1104 #: Product code for USB-CANmodul2 G3. PRODCODE_PID_ADVANCED = 0x1105 #: Product code for USB-CANmodul8 G3. PRODCODE_PID_USBCAN8 = 0x1107 #: Product code for USB-CANmodul16 G3. PRODCODE_PID_USBCAN16 = 0x1109 #: Reserved. PRODCODE_PID_RESERVED3 = 0x1110 #: Product code for USB-CANmodul2 G4. PRODCODE_PID_ADVANCED_G4 = 0x1121 #: Product code for USB-CANmodul1 G4. PRODCODE_PID_BASIC_G4 = 0x1122 #: Reserved. PRODCODE_PID_RESERVED1 = 0x1144 #: Reserved. PRODCODE_PID_RESERVED2 = 0x1145 #: Definitions for cyclic CAN messages. MAX_CYCLIC_CAN_MSG = 16 class CyclicFlags(DWORD): """ Specifies flags for cyclical CAN messages. These flags can be used in combinations with method :meth:`UcanServer.enable_cyclic_can_msg`. .. seealso:: :meth:`UcanServer.enable_cyclic_can_msg` """ #: Stops the transmission of cyclic CAN messages. CYCLIC_FLAG_STOPP = 0x0 #: Global enable of transmission of cyclic CAN messages. CYCLIC_FLAG_START = 0x80000000 #: List of cyclic CAN messages will be processed in sequential mode (otherwise in parallel mode). CYCLIC_FLAG_SEQUMODE = 0x40000000 #: No echo will be sent back if echo mode is enabled with method :meth:`UcanServer.init_can`. CYCLIC_FLAG_NOECHO = 0x10000 #: CAN message with index 0 of the list will not be sent. CYCLIC_FLAG_LOCK_0 = 0x1 #: CAN message with index 1 of the list will not be sent. CYCLIC_FLAG_LOCK_1 = 0x2 #: CAN message with index 2 of the list will not be sent. CYCLIC_FLAG_LOCK_2 = 0x4 #: CAN message with index 3 of the list will not be sent. CYCLIC_FLAG_LOCK_3 = 0x8 #: CAN message with index 4 of the list will not be sent. CYCLIC_FLAG_LOCK_4 = 0x10 #: CAN message with index 5 of the list will not be sent. CYCLIC_FLAG_LOCK_5 = 0x20 #: CAN message with index 6 of the list will not be sent. CYCLIC_FLAG_LOCK_6 = 0x40 #: CAN message with index 7 of the list will not be sent. CYCLIC_FLAG_LOCK_7 = 0x80 #: CAN message with index 8 of the list will not be sent. CYCLIC_FLAG_LOCK_8 = 0x100 #: CAN message with index 9 of the list will not be sent. CYCLIC_FLAG_LOCK_9 = 0x200 #: CAN message with index 10 of the list will not be sent. CYCLIC_FLAG_LOCK_10 = 0x400 #: CAN message with index 11 of the list will not be sent. CYCLIC_FLAG_LOCK_11 = 0x800 #: CAN message with index 12 of the list will not be sent. CYCLIC_FLAG_LOCK_12 = 0x1000 #: CAN message with index 13 of the list will not be sent. CYCLIC_FLAG_LOCK_13 = 0x2000 #: CAN message with index 14 of the list will not be sent. CYCLIC_FLAG_LOCK_14 = 0x4000 #: CAN message with index 15 of the list will not be sent. CYCLIC_FLAG_LOCK_15 = 0x8000 class PendingFlags(BYTE): """ Specifies flags for method :meth:`UcanServer.get_msg_pending`. These flags can be uses in combinations. .. seealso:: :meth:`UcanServer.get_msg_pending` """ #: number of pending CAN messages in receive buffer of USB-CAN-library PENDING_FLAG_RX_DLL = 0x1 #: reserved PENDING_FLAG_RX_SYS = 0x2 #: number of pending CAN messages in receive buffer of firmware PENDING_FLAG_RX_FW = 0x4 #: number of pending CAN messages in transmit buffer of USB-CAN-library PENDING_FLAG_TX_DLL = 0x10 #: reserved PENDING_FLAG_TX_SYS = 0x20 #: number of pending CAN messages in transmit buffer of firmware PENDING_FLAG_TX_FW = 0x40 #: number of pending CAN messages in all receive buffers PENDING_FLAG_RX_ALL = PENDING_FLAG_RX_DLL | PENDING_FLAG_RX_SYS | PENDING_FLAG_RX_FW #: number of pending CAN messages in all transmit buffers PENDING_FLAG_TX_ALL = PENDING_FLAG_TX_DLL | PENDING_FLAG_TX_SYS | PENDING_FLAG_TX_FW #: number of pending CAN messages in all buffers PENDING_FLAG_ALL = PENDING_FLAG_RX_ALL | PENDING_FLAG_TX_ALL class Mode(BYTE): """ Specifies values for operation mode of a CAN channel. These values can be combined by OR operation with the method :meth:`UcanServer.init_can`. """ #: normal operation mode (transmitting and receiving) MODE_NORMAL = 0 #: listen only mode (receiving only, no ACK at CAN bus) MODE_LISTEN_ONLY = 1 #: CAN messages which was sent will be received back with method :meth:`UcanServer.read_can_msg` MODE_TX_ECHO = 2 #: reserved (not implemented in this version) MODE_RX_ORDER_CH = 4 #: high resolution time stamps in received CAN messages (only available with STM derivatives) MODE_HIGH_RES_TIMER = 8 class VersionType(BYTE): """ Specifies values for receiving the version information of several driver files. .. note:: This structure is only used internally. """ #: version of the USB-CAN-library VER_TYPE_USER_LIB = 1 #: equivalent to :attr:`VER_TYPE_USER_LIB` VER_TYPE_USER_DLL = 1 #: version of USBCAN.SYS (not supported in this version) VER_TYPE_SYS_DRV = 2 #: version of firmware in hardware (not supported, use method :meth:`UcanServer.get_fw_version`) VER_TYPE_FIRMWARE = 3 #: version of UCANNET.SYS VER_TYPE_NET_DRV = 4 #: version of USBCANLD.SYS VER_TYPE_SYS_LD = 5 #: version of USBCANL2.SYS VER_TYPE_SYS_L2 = 6 #: version of USBCANL3.SYS VER_TYPE_SYS_L3 = 7 #: version of USBCANL4.SYS VER_TYPE_SYS_L4 = 8 #: version of USBCANL5.SYS VER_TYPE_SYS_L5 = 9 #: version of USBCANCP.CPL VER_TYPE_CPL = 10 python-can-4.5.0/can/interfaces/systec/exceptions.py000066400000000000000000000113471472200326600225410ustar00rootroot00000000000000from abc import ABC, abstractmethod from typing import Dict from can import CanError from .constants import ReturnCode class UcanException(CanError, ABC): """Base class for USB can errors.""" def __init__(self, result, func, arguments): self.result = result self.func = func self.arguments = arguments message = self._error_message_mapping.get(result, "unknown") super().__init__( message=f"Function {func.__name__} (called with {arguments}): {message}", error_code=result.value, ) @property @abstractmethod def _error_message_mapping(self) -> Dict[ReturnCode, str]: ... class UcanError(UcanException): """Exception class for errors from USB-CAN-library.""" _ERROR_MESSAGES = { ReturnCode.ERR_RESOURCE: "could not created a resource (memory, handle, ...)", ReturnCode.ERR_MAXMODULES: "the maximum number of opened modules is reached", ReturnCode.ERR_HWINUSE: "the specified module is already in use", ReturnCode.ERR_ILLVERSION: "the software versions of the module and library are incompatible", ReturnCode.ERR_ILLHW: "the module with the specified device number is not connected " "(or used by an other application)", ReturnCode.ERR_ILLHANDLE: "wrong USB-CAN-Handle handed over to the function", ReturnCode.ERR_ILLPARAM: "wrong parameter handed over to the function", ReturnCode.ERR_BUSY: "instruction can not be processed at this time", ReturnCode.ERR_TIMEOUT: "no answer from module", ReturnCode.ERR_IOFAILED: "a request to the driver failed", ReturnCode.ERR_DLL_TXFULL: "a CAN message did not fit into the transmit buffer", ReturnCode.ERR_MAXINSTANCES: "maximum number of applications is reached", ReturnCode.ERR_CANNOTINIT: "CAN interface is not yet initialized", ReturnCode.ERR_DISCONECT: "USB-CANmodul was disconnected", ReturnCode.ERR_NOHWCLASS: "the needed device class does not exist", ReturnCode.ERR_ILLCHANNEL: "illegal CAN channel", ReturnCode.ERR_RESERVED1: "reserved", ReturnCode.ERR_ILLHWTYPE: "the API function can not be used with this hardware", } @property def _error_message_mapping(self) -> Dict[ReturnCode, str]: return UcanError._ERROR_MESSAGES class UcanCmdError(UcanException): """Exception class for errors from firmware in USB-CANmodul.""" _ERROR_MESSAGES = { ReturnCode.ERRCMD_NOTEQU: "the received response does not match to the transmitted command", ReturnCode.ERRCMD_REGTST: "no access to the CAN controller", ReturnCode.ERRCMD_ILLCMD: "the module could not interpret the command", ReturnCode.ERRCMD_EEPROM: "error while reading the EEPROM", ReturnCode.ERRCMD_RESERVED1: "reserved", ReturnCode.ERRCMD_RESERVED2: "reserved", ReturnCode.ERRCMD_RESERVED3: "reserved", ReturnCode.ERRCMD_ILLBDR: "illegal baud rate value specified in BTR0/BTR1 for systec " "USB-CANmoduls", ReturnCode.ERRCMD_NOTINIT: "CAN channel is not initialized", ReturnCode.ERRCMD_ALREADYINIT: "CAN channel is already initialized", ReturnCode.ERRCMD_ILLSUBCMD: "illegal sub-command specified", ReturnCode.ERRCMD_ILLIDX: "illegal index specified (e.g. index for cyclic CAN messages)", ReturnCode.ERRCMD_RUNNING: "cyclic CAN message(s) can not be defined because transmission of " "cyclic CAN messages is already running", } @property def _error_message_mapping(self) -> Dict[ReturnCode, str]: return UcanCmdError._ERROR_MESSAGES class UcanWarning(UcanException): """Exception class for warnings, the function has been executed anyway.""" _ERROR_MESSAGES = { ReturnCode.WARN_NODATA: "no CAN messages received", ReturnCode.WARN_SYS_RXOVERRUN: "overrun in receive buffer of the kernel driver", ReturnCode.WARN_DLL_RXOVERRUN: "overrun in receive buffer of the USB-CAN-library", ReturnCode.WARN_RESERVED1: "reserved", ReturnCode.WARN_RESERVED2: "reserved", ReturnCode.WARN_FW_TXOVERRUN: "overrun in transmit buffer of the firmware (but this CAN message " "was successfully stored in buffer of the ibrary)", ReturnCode.WARN_FW_RXOVERRUN: "overrun in receive buffer of the firmware (but this CAN message " "was successfully read)", ReturnCode.WARN_FW_TXMSGLOST: "reserved", ReturnCode.WARN_NULL_PTR: "pointer is NULL", ReturnCode.WARN_TXLIMIT: "not all CAN messages could be stored to the transmit buffer in " "USB-CAN-library", ReturnCode.WARN_BUSY: "reserved", } @property def _error_message_mapping(self) -> Dict[ReturnCode, str]: return UcanWarning._ERROR_MESSAGES python-can-4.5.0/can/interfaces/systec/structures.py000066400000000000000000000312721472200326600226020ustar00rootroot00000000000000import os from ctypes import POINTER, Structure, sizeof from ctypes import ( c_long as BOOL, ) from ctypes import ( c_ubyte as BYTE, ) from ctypes import ( c_ulong as DWORD, ) from ctypes import ( c_ushort as WORD, ) from ctypes import ( c_void_p as LPVOID, ) # Workaround for Unix based platforms to be able to load structures for testing, etc... if os.name == "nt": from ctypes import WINFUNCTYPE as FUNCTYPE else: from ctypes import CFUNCTYPE as FUNCTYPE from .constants import MsgFrameFormat class CanMsg(Structure): """ Structure of a CAN message. .. seealso:: :meth:`UcanServer.read_can_msg` :meth:`UcanServer.write_can_msg` :meth:`UcanServer.define_cyclic_can_msg` :meth:`UcanServer.read_cyclic_can_msg` """ _pack_ = 1 _fields_ = [ ("m_dwID", DWORD), # CAN Identifier ("m_bFF", BYTE), # CAN Frame Format (see enum :class:`MsgFrameFormat`) ("m_bDLC", BYTE), # CAN Data Length Code ("m_bData", BYTE * 8), # CAN Data (array of 8 bytes) ( "m_dwTime", DWORD, ), # Receive time stamp in ms (for transmit messages no meaning) ] def __init__( self, id_=0, frame_format=MsgFrameFormat.MSG_FF_STD, data=None, dlc=None ): data = [] if data is None else data dlc = len(data) if dlc is None else dlc super().__init__(id_, frame_format, dlc, (BYTE * 8)(*data), 0) def __eq__(self, other): if not isinstance(other, CanMsg): return False return ( self.id == other.id and self.frame_format == other.frame_format and self.data == other.data ) @property def id(self): return self.m_dwID @id.setter def id(self, value): self.m_dwID = value @property def frame_format(self): return self.m_bFF @frame_format.setter def frame_format(self, frame_format): self.m_bFF = frame_format @property def data(self): return self.m_bData[: self.m_bDLC] @data.setter def data(self, data): self.m_bDLC = len(data) self.m_bData((BYTE * 8)(*data)) @property def time(self): return self.m_dwTime class Status(Structure): """ Structure with the error status of CAN and USB. Use this structure with the method :meth:`UcanServer.get_status` .. seealso:: :meth:`UcanServer.get_status` :meth:`UcanServer.get_can_status_message` """ _pack_ = 1 _fields_ = [ ("m_wCanStatus", WORD), # CAN error status (see enum :class:`CanStatus`) ("m_wUsbStatus", WORD), # USB error status (see enum :class:`UsbStatus`) ] def __eq__(self, other): if not isinstance(other, Status): return False return ( self.can_status == other.can_status and self.usb_status == other.usb_status ) @property def can_status(self): return self.m_wCanStatus @property def usb_status(self): return self.m_wUsbStatus class InitCanParam(Structure): """ Structure including initialisation parameters used internally in :meth:`UcanServer.init_can`. .. note:: This structure is only used internally. """ _pack_ = 1 _fields_ = [ ("m_dwSize", DWORD), # size of this structure (only used internally) ( "m_bMode", BYTE, ), # selects the mode of CAN controller (see enum :class:`Mode`) # Baudrate Registers for GW-001 or GW-002 ("m_bBTR0", BYTE), # Bus Timing Register 0 (see enum :class:`Baudrate`) ("m_bBTR1", BYTE), # Bus Timing Register 1 (see enum :class:`Baudrate`) ("m_bOCR", BYTE), # Output Control Register (see enum :class:`OutputControl`) ( "m_dwAMR", DWORD, ), # Acceptance Mask Register (see method :meth:`UcanServer.set_acceptance`) ( "m_dwACR", DWORD, ), # Acceptance Code Register (see method :meth:`UcanServer.set_acceptance`) ("m_dwBaudrate", DWORD), # Baudrate Register for all systec USB-CANmoduls # (see enum :class:`BaudrateEx`) ( "m_wNrOfRxBufferEntries", WORD, ), # number of receive buffer entries (default is 4096) ( "m_wNrOfTxBufferEntries", WORD, ), # number of transmit buffer entries (default is 4096) ] def __init__( self, mode, BTR, OCR, AMR, ACR, baudrate, rx_buffer_entries, tx_buffer_entries ): super().__init__( sizeof(InitCanParam), mode, BTR >> 8, BTR, OCR, AMR, ACR, baudrate, rx_buffer_entries, tx_buffer_entries, ) def __eq__(self, other): if not isinstance(other, InitCanParam): return False return ( self.mode == other.mode and self.BTR == other.BTR and self.OCR == other.OCR and self.baudrate == other.baudrate and self.rx_buffer_entries == other.rx_buffer_entries and self.tx_buffer_entries == other.tx_buffer_entries ) @property def mode(self): return self.m_bMode @mode.setter def mode(self, mode): self.m_bMode = mode @property def BTR(self): return self.m_bBTR0 << 8 | self.m_bBTR1 @BTR.setter def BTR(self, BTR): self.m_bBTR0, self.m_bBTR1 = BTR >> 8, BTR @property def OCR(self): return self.m_bOCR @OCR.setter def OCR(self, OCR): self.m_bOCR = OCR @property def baudrate(self): return self.m_dwBaudrate @baudrate.setter def baudrate(self, baudrate): self.m_dwBaudrate = baudrate @property def rx_buffer_entries(self): return self.m_wNrOfRxBufferEntries @rx_buffer_entries.setter def rx_buffer_entries(self, rx_buffer_entries): self.m_wNrOfRxBufferEntries = rx_buffer_entries @property def tx_buffer_entries(self): return self.m_wNrOfTxBufferEntries @tx_buffer_entries.setter def tx_buffer_entries(self, tx_buffer_entries): self.m_wNrOfTxBufferEntries = tx_buffer_entries class Handle(BYTE): pass class HardwareInfoEx(Structure): """ Structure including hardware information about the USB-CANmodul. This structure is used with the method :meth:`UcanServer.get_hardware_info`. .. seealso:: :meth:`UcanServer.get_hardware_info` """ _pack_ = 1 _fields_ = [ ("m_dwSize", DWORD), # size of this structure (only used internally) ("m_UcanHandle", Handle), # USB-CAN-Handle assigned by the DLL ("m_bDeviceNr", BYTE), # device number of the USB-CANmodul ("m_dwSerialNr", DWORD), # serial number from USB-CANmodul ("m_dwFwVersionEx", DWORD), # version of firmware ("m_dwProductCode", DWORD), # product code (see enum :class:`ProductCode`) # unique ID (available since V5.01) !!! m_dwSize must be >= HWINFO_SIZE_V2 ("m_dwUniqueId0", DWORD), ("m_dwUniqueId1", DWORD), ("m_dwUniqueId2", DWORD), ("m_dwUniqueId3", DWORD), ("m_dwFlags", DWORD), # additional flags ] def __init__(self): super().__init__(sizeof(HardwareInfoEx)) def __eq__(self, other): if not isinstance(other, HardwareInfoEx): return False return ( self.device_number == other.device_number and self.serial == other.serial and self.fw_version == other.fw_version and self.product_code == other.product_code and self.unique_id == other.unique_id and self.flags == other.flags ) @property def device_number(self): return self.m_bDeviceNr @property def serial(self): return self.m_dwSerialNr @property def fw_version(self): return self.m_dwFwVersionEx @property def product_code(self): return self.m_dwProductCode @property def unique_id(self): return ( self.m_dwUniqueId0, self.m_dwUniqueId1, self.m_dwUniqueId2, self.m_dwUniqueId3, ) @property def flags(self): return self.m_dwFlags # void PUBLIC UcanCallbackFktEx (Handle UcanHandle_p, DWORD dwEvent_p, # BYTE bChannel_p, void* pArg_p); CallbackFktEx = FUNCTYPE(None, Handle, DWORD, BYTE, LPVOID) class HardwareInitInfo(Structure): """ Structure including information about the enumeration of USB-CANmoduls. .. seealso:: :meth:`UcanServer.enumerate_hardware` .. note:: This structure is only used internally. """ _pack_ = 1 _fields_ = [ ("m_dwSize", DWORD), # size of this structure ( "m_fDoInitialize", BOOL, ), # specifies if the found module should be initialized by the DLL ("m_pUcanHandle", Handle), # pointer to variable receiving the USB-CAN-Handle ("m_fpCallbackFktEx", CallbackFktEx), # pointer to callback function ( "m_pCallbackArg", LPVOID, ), # pointer to user defined parameter for callback function ("m_fTryNext", BOOL), # specifies if a further module should be found ] class ChannelInfo(Structure): """ Structure including CAN channel information. This structure is used with the method :meth:`UcanServer.get_hardware_info`. .. seealso:: :meth:`UcanServer.get_hardware_info` """ _pack_ = 1 _fields_ = [ ("m_dwSize", DWORD), # size of this structure ("m_bMode", BYTE), # operation mode of CAN controller (see enum :class:`Mode`) ("m_bBTR0", BYTE), # Bus Timing Register 0 (see enum :class:`Baudrate`) ("m_bBTR1", BYTE), # Bus Timing Register 1 (see enum :class:`Baudrate`) ("m_bOCR", BYTE), # Output Control Register (see enum :class:`OutputControl`) ( "m_dwAMR", DWORD, ), # Acceptance Mask Register (see method :meth:`UcanServer.set_acceptance`) ( "m_dwACR", DWORD, ), # Acceptance Code Register (see method :meth:`UcanServer.set_acceptance`) ("m_dwBaudrate", DWORD), # Baudrate Register for all systec USB-CANmoduls # (see enum :class:`BaudrateEx`) ( "m_fCanIsInit", BOOL, ), # True if the CAN interface is initialized, otherwise false ( "m_wCanStatus", WORD, ), # CAN status (same as received by method :meth:`UcanServer.get_status`) ] def __init__(self): super().__init__(sizeof(ChannelInfo)) def __eq__(self, other): if not isinstance(other, ChannelInfo): return False return ( self.mode == other.mode and self.BTR == other.BTR and self.OCR == other.OCR and self.AMR == other.AMR and self.ACR == other.ACR and self.baudrate == other.baudrate and self.can_is_init == other.can_is_init and self.can_status == other.can_status ) @property def mode(self): return self.m_bMode @property def BTR(self): return self.m_bBTR0 << 8 | self.m_bBTR1 @property def OCR(self): return self.m_bOCR @property def AMR(self): return self.m_dwAMR @property def ACR(self): return self.m_dwACR @property def baudrate(self): return self.m_dwBaudrate @property def can_is_init(self): return self.m_fCanIsInit @property def can_status(self): return self.m_wCanStatus class MsgCountInfo(Structure): """ Structure including the number of sent and received CAN messages. This structure is used with the method :meth:`UcanServer.get_msg_count_info`. .. seealso:: :meth:`UcanServer.get_msg_count_info` .. note:: This structure is only used internally. """ _fields_ = [ ("m_wSentMsgCount", WORD), # number of sent CAN messages ("m_wRecvdMsgCount", WORD), # number of received CAN messages ] @property def sent_msg_count(self): return self.m_wSentMsgCount @property def recv_msg_count(self): return self.m_wRecvdMsgCount # void (PUBLIC *ConnectControlFktEx) (DWORD dwEvent_p, DWORD dwParam_p, void* pArg_p); ConnectControlFktEx = FUNCTYPE(None, DWORD, DWORD, LPVOID) # typedef void (PUBLIC *EnumCallback) (DWORD dwIndex_p, BOOL fIsUsed_p, # HardwareInfoEx* pHwInfoEx_p, HardwareInitInfo* pInitInfo_p, void* pArg_p); EnumCallback = FUNCTYPE( None, DWORD, BOOL, POINTER(HardwareInfoEx), POINTER(HardwareInitInfo), LPVOID ) python-can-4.5.0/can/interfaces/systec/ucan.py000066400000000000000000001332221472200326600213030ustar00rootroot00000000000000import logging import sys from ctypes import byref from ctypes import c_wchar_p as LPWSTR from ...exceptions import CanInterfaceNotImplementedError from .constants import * from .exceptions import * from .structures import * log = logging.getLogger("can.systec") def check_valid_rx_can_msg(result): """ Checks if function :meth:`UcanServer.read_can_msg` returns a valid CAN message. :param ReturnCode result: Error code of the function. :return: True if a valid CAN messages was received, otherwise False. :rtype: bool """ return (result.value == ReturnCode.SUCCESSFUL) or ( result.value > ReturnCode.WARNING ) def check_tx_ok(result): """ Checks if function :meth:`UcanServer.write_can_msg` successfully wrote CAN message(s). While using :meth:`UcanServer.write_can_msg_ex` the number of sent CAN messages can be less than the number of CAN messages which should be sent. :param ReturnCode result: Error code of the function. :return: True if CAN message(s) was(were) written successfully, otherwise False. :rtype: bool .. :seealso: :const:`ReturnCode.WARN_TXLIMIT` """ return (result.value == ReturnCode.SUCCESSFUL) or ( result.value > ReturnCode.WARNING ) def check_tx_success(result): """ Checks if function :meth:`UcanServer.write_can_msg_ex` successfully wrote all CAN message(s). :param ReturnCode result: Error code of the function. :return: True if CAN message(s) was(were) written successfully, otherwise False. :rtype: bool """ return result.value == ReturnCode.SUCCESSFUL def check_tx_not_all(result): """ Checks if function :meth:`UcanServer.write_can_msg_ex` did not sent all CAN messages. :param ReturnCode result: Error code of the function. :return: True if not all CAN messages were written, otherwise False. :rtype: bool """ return result.value == ReturnCode.WARN_TXLIMIT def check_warning(result): """ Checks if any function returns a warning. :param ReturnCode result: Error code of the function. :return: True if a function returned warning, otherwise False. :rtype: bool """ return result.value >= ReturnCode.WARNING def check_error(result): """ Checks if any function returns an error from USB-CAN-library. :param ReturnCode result: Error code of the function. :return: True if a function returned error, otherwise False. :rtype: bool """ return (result.value != ReturnCode.SUCCESSFUL) and ( result.value < ReturnCode.WARNING ) def check_error_cmd(result): """ Checks if any function returns an error from firmware in USB-CANmodul. :param ReturnCode result: Error code of the function. :return: True if a function returned error from firmware, otherwise False. :rtype: bool """ return (result.value >= ReturnCode.ERRCMD) and (result.value < ReturnCode.WARNING) def check_result(result, func, arguments): if check_warning(result) and (result.value != ReturnCode.WARN_NODATA): log.warning(UcanWarning(result, func, arguments)) elif check_error(result): if check_error_cmd(result): raise UcanCmdError(result, func, arguments) else: raise UcanError(result, func, arguments) return result _UCAN_INITIALIZED = False if os.name != "nt": log.warning("SYSTEC ucan library does not work on %s platform.", sys.platform) else: from ctypes import WinDLL try: # Select the proper dll architecture lib = WinDLL("usbcan64.dll" if sys.maxsize > 2**32 else "usbcan32.dll") # BOOL PUBLIC UcanSetDebugMode (DWORD dwDbgLevel_p, _TCHAR* pszFilePathName_p, DWORD dwFlags_p); UcanSetDebugMode = lib.UcanSetDebugMode UcanSetDebugMode.restype = BOOL UcanSetDebugMode.argtypes = [DWORD, LPWSTR, DWORD] # DWORD PUBLIC UcanGetVersionEx (VersionType VerType_p); UcanGetVersionEx = lib.UcanGetVersionEx UcanGetVersionEx.restype = DWORD UcanGetVersionEx.argtypes = [VersionType] # DWORD PUBLIC UcanGetFwVersion (Handle UcanHandle_p); UcanGetFwVersion = lib.UcanGetFwVersion UcanGetFwVersion.restype = DWORD UcanGetFwVersion.argtypes = [Handle] # BYTE PUBLIC UcanInitHwConnectControlEx (ConnectControlFktEx fpConnectControlFktEx_p, void* pCallbackArg_p); UcanInitHwConnectControlEx = lib.UcanInitHwConnectControlEx UcanInitHwConnectControlEx.restype = ReturnCode UcanInitHwConnectControlEx.argtypes = [ConnectControlFktEx, LPVOID] UcanInitHwConnectControlEx.errcheck = check_result # BYTE PUBLIC UcanDeinitHwConnectControl (void) UcanDeinitHwConnectControl = lib.UcanDeinitHwConnectControl UcanDeinitHwConnectControl.restype = ReturnCode UcanDeinitHwConnectControl.argtypes = [] UcanDeinitHwConnectControl.errcheck = check_result # DWORD PUBLIC UcanEnumerateHardware (EnumCallback fpCallback_p, void* pCallbackArg_p, # BOOL fEnumUsedDevs_p, # BYTE bDeviceNrLow_p, BYTE bDeviceNrHigh_p, # DWORD dwSerialNrLow_p, DWORD dwSerialNrHigh_p, # DWORD dwProductCodeLow_p, DWORD dwProductCodeHigh_p); UcanEnumerateHardware = lib.UcanEnumerateHardware UcanEnumerateHardware.restype = DWORD UcanEnumerateHardware.argtypes = [ EnumCallback, LPVOID, BOOL, BYTE, BYTE, DWORD, DWORD, DWORD, DWORD, ] # BYTE PUBLIC UcanInitHardwareEx (Handle* pUcanHandle_p, BYTE bDeviceNr_p, # CallbackFktEx fpCallbackFktEx_p, void* pCallbackArg_p); UcanInitHardwareEx = lib.UcanInitHardwareEx UcanInitHardwareEx.restype = ReturnCode UcanInitHardwareEx.argtypes = [POINTER(Handle), BYTE, CallbackFktEx, LPVOID] UcanInitHardwareEx.errcheck = check_result # BYTE PUBLIC UcanInitHardwareEx2 (Handle* pUcanHandle_p, DWORD dwSerialNr_p, # CallbackFktEx fpCallbackFktEx_p, void* pCallbackArg_p); UcanInitHardwareEx2 = lib.UcanInitHardwareEx2 UcanInitHardwareEx2.restype = ReturnCode UcanInitHardwareEx2.argtypes = [POINTER(Handle), DWORD, CallbackFktEx, LPVOID] UcanInitHardwareEx2.errcheck = check_result # BYTE PUBLIC UcanGetModuleTime (Handle UcanHandle_p, DWORD* pdwTime_p); UcanGetModuleTime = lib.UcanGetModuleTime UcanGetModuleTime.restype = ReturnCode UcanGetModuleTime.argtypes = [Handle, POINTER(DWORD)] UcanGetModuleTime.errcheck = check_result # BYTE PUBLIC UcanGetHardwareInfoEx2 (Handle UcanHandle_p, # HardwareInfoEx* pHwInfo_p, # ChannelInfo* pCanInfoCh0_p, ChannelInfo* pCanInfoCh1_p); UcanGetHardwareInfoEx2 = lib.UcanGetHardwareInfoEx2 UcanGetHardwareInfoEx2.restype = ReturnCode UcanGetHardwareInfoEx2.argtypes = [ Handle, POINTER(HardwareInfoEx), POINTER(ChannelInfo), POINTER(ChannelInfo), ] UcanGetHardwareInfoEx2.errcheck = check_result # BYTE PUBLIC UcanInitCanEx2 (Handle UcanHandle_p, BYTE bChannel_p, tUcaninit_canParam* pinit_canParam_p); UcanInitCanEx2 = lib.UcanInitCanEx2 UcanInitCanEx2.restype = ReturnCode UcanInitCanEx2.argtypes = [Handle, BYTE, POINTER(InitCanParam)] UcanInitCanEx2.errcheck = check_result # BYTE PUBLIC UcanSetBaudrateEx (Handle UcanHandle_p, # BYTE bChannel_p, BYTE bBTR0_p, BYTE bBTR1_p, DWORD dwBaudrate_p); UcanSetBaudrateEx = lib.UcanSetBaudrateEx UcanSetBaudrateEx.restype = ReturnCode UcanSetBaudrateEx.argtypes = [Handle, BYTE, BYTE, BYTE, DWORD] UcanSetBaudrateEx.errcheck = check_result # BYTE PUBLIC UcanSetAcceptanceEx (Handle UcanHandle_p, BYTE bChannel_p, # DWORD dwAMR_p, DWORD dwACR_p); UcanSetAcceptanceEx = lib.UcanSetAcceptanceEx UcanSetAcceptanceEx.restype = ReturnCode UcanSetAcceptanceEx.argtypes = [Handle, BYTE, DWORD, DWORD] UcanSetAcceptanceEx.errcheck = check_result # BYTE PUBLIC UcanResetCanEx (Handle UcanHandle_p, BYTE bChannel_p, DWORD dwResetFlags_p); UcanResetCanEx = lib.UcanResetCanEx UcanResetCanEx.restype = ReturnCode UcanResetCanEx.argtypes = [Handle, BYTE, DWORD] UcanResetCanEx.errcheck = check_result # BYTE PUBLIC UcanReadCanMsgEx (Handle UcanHandle_p, BYTE* pbChannel_p, # CanMsg* pCanMsg_p, DWORD* pdwCount_p); UcanReadCanMsgEx = lib.UcanReadCanMsgEx UcanReadCanMsgEx.restype = ReturnCode UcanReadCanMsgEx.argtypes = [ Handle, POINTER(BYTE), POINTER(CanMsg), POINTER(DWORD), ] UcanReadCanMsgEx.errcheck = check_result # BYTE PUBLIC UcanWriteCanMsgEx (Handle UcanHandle_p, BYTE bChannel_p, # CanMsg* pCanMsg_p, DWORD* pdwCount_p); UcanWriteCanMsgEx = lib.UcanWriteCanMsgEx UcanWriteCanMsgEx.restype = ReturnCode UcanWriteCanMsgEx.argtypes = [Handle, BYTE, POINTER(CanMsg), POINTER(DWORD)] UcanWriteCanMsgEx.errcheck = check_result # BYTE PUBLIC UcanGetStatusEx (Handle UcanHandle_p, BYTE bChannel_p, Status* pStatus_p); UcanGetStatusEx = lib.UcanGetStatusEx UcanGetStatusEx.restype = ReturnCode UcanGetStatusEx.argtypes = [Handle, BYTE, POINTER(Status)] UcanGetStatusEx.errcheck = check_result # BYTE PUBLIC UcanGetMsgCountInfoEx (Handle UcanHandle_p, BYTE bChannel_p, # MsgCountInfo* pMsgCountInfo_p); UcanGetMsgCountInfoEx = lib.UcanGetMsgCountInfoEx UcanGetMsgCountInfoEx.restype = ReturnCode UcanGetMsgCountInfoEx.argtypes = [Handle, BYTE, POINTER(MsgCountInfo)] UcanGetMsgCountInfoEx.errcheck = check_result # BYTE PUBLIC UcanGetMsgPending (Handle UcanHandle_p, # BYTE bChannel_p, DWORD dwFlags_p, DWORD* pdwPendingCount_p); UcanGetMsgPending = lib.UcanGetMsgPending UcanGetMsgPending.restype = ReturnCode UcanGetMsgPending.argtypes = [Handle, BYTE, DWORD, POINTER(DWORD)] UcanGetMsgPending.errcheck = check_result # BYTE PUBLIC UcanGetCanErrorCounter (Handle UcanHandle_p, # BYTE bChannel_p, DWORD* pdwTxErrorCounter_p, DWORD* pdwRxErrorCounter_p); UcanGetCanErrorCounter = lib.UcanGetCanErrorCounter UcanGetCanErrorCounter.restype = ReturnCode UcanGetCanErrorCounter.argtypes = [Handle, BYTE, POINTER(DWORD), POINTER(DWORD)] UcanGetCanErrorCounter.errcheck = check_result # BYTE PUBLIC UcanSetTxTimeout (Handle UcanHandle_p, # BYTE bChannel_p, DWORD dwTxTimeout_p); UcanSetTxTimeout = lib.UcanSetTxTimeout UcanSetTxTimeout.restype = ReturnCode UcanSetTxTimeout.argtypes = [Handle, BYTE, DWORD] UcanSetTxTimeout.errcheck = check_result # BYTE PUBLIC UcanDeinitCanEx (Handle UcanHandle_p, BYTE bChannel_p); UcanDeinitCanEx = lib.UcanDeinitCanEx UcanDeinitCanEx.restype = ReturnCode UcanDeinitCanEx.argtypes = [Handle, BYTE] UcanDeinitCanEx.errcheck = check_result # BYTE PUBLIC UcanDeinitHardware (Handle UcanHandle_p); UcanDeinitHardware = lib.UcanDeinitHardware UcanDeinitHardware.restype = ReturnCode UcanDeinitHardware.argtypes = [Handle] UcanDeinitHardware.errcheck = check_result # BYTE PUBLIC UcanDefineCyclicCanMsg (Handle UcanHandle_p, # BYTE bChannel_p, CanMsg* pCanMsgList_p, DWORD dwCount_p); UcanDefineCyclicCanMsg = lib.UcanDefineCyclicCanMsg UcanDefineCyclicCanMsg.restype = ReturnCode UcanDefineCyclicCanMsg.argtypes = [Handle, BYTE, POINTER(CanMsg), DWORD] UcanDefineCyclicCanMsg.errcheck = check_result # BYTE PUBLIC UcanReadCyclicCanMsg (Handle UcanHandle_p, # BYTE bChannel_p, CanMsg* pCanMsgList_p, DWORD* pdwCount_p); UcanReadCyclicCanMsg = lib.UcanReadCyclicCanMsg UcanReadCyclicCanMsg.restype = ReturnCode UcanReadCyclicCanMsg.argtypes = [Handle, BYTE, POINTER(CanMsg), POINTER(DWORD)] UcanReadCyclicCanMsg.errcheck = check_result # BYTE PUBLIC UcanEnableCyclicCanMsg (Handle UcanHandle_p, # BYTE bChannel_p, DWORD dwFlags_p); UcanEnableCyclicCanMsg = lib.UcanEnableCyclicCanMsg UcanEnableCyclicCanMsg.restype = ReturnCode UcanEnableCyclicCanMsg.argtypes = [Handle, BYTE, DWORD] UcanEnableCyclicCanMsg.errcheck = check_result _UCAN_INITIALIZED = True except Exception as ex: log.warning("Cannot load SYSTEC ucan library: %s.", ex) class UcanServer: """ UcanServer is a Python wrapper class for using the usbcan32.dll / usbcan64.dll. """ _modules_found = [] _connect_control_ref = None def __init__(self): if not _UCAN_INITIALIZED: raise CanInterfaceNotImplementedError( "The interface could not be loaded on the current platform" ) self._handle = Handle(INVALID_HANDLE) self._is_initialized = False self._hw_is_initialized = False self._ch_is_initialized = { Channel.CHANNEL_CH0: False, Channel.CHANNEL_CH1: False, } self._callback_ref = CallbackFktEx(self._callback) if self._connect_control_ref is None: self._connect_control_ref = ConnectControlFktEx(self._connect_control) UcanInitHwConnectControlEx(self._connect_control_ref, None) @property def is_initialized(self): """ Returns whether hardware interface is initialized. :return: True if initialized, otherwise False. :rtype: bool """ return self._is_initialized @property def is_can0_initialized(self): """ Returns whether CAN interface for channel 0 is initialized. :return: True if initialized, otherwise False. :rtype: bool """ return self._ch_is_initialized[Channel.CHANNEL_CH0] @property def is_can1_initialized(self): """ Returns whether CAN interface for channel 1 is initialized. :return: True if initialized, otherwise False. :rtype: bool """ return self._ch_is_initialized[Channel.CHANNEL_CH1] @classmethod def _enum_callback(cls, index, is_used, hw_info_ex, init_info, arg): cls._modules_found.append( (index, bool(is_used), hw_info_ex.contents, init_info.contents) ) @classmethod def enumerate_hardware( cls, device_number_low=0, device_number_high=-1, serial_low=0, serial_high=-1, product_code_low=0, product_code_high=-1, enum_used_devices=False, ): cls._modules_found = [] UcanEnumerateHardware( cls._enum_callback_ref, None, enum_used_devices, device_number_low, device_number_high, serial_low, serial_high, product_code_low, product_code_high, ) return cls._modules_found def init_hardware(self, serial=None, device_number=ANY_MODULE): """ Initializes the device with the corresponding serial or device number. :param int or None serial: Serial number of the USB-CANmodul. :param int device_number: Device number (0 - 254, or :const:`ANY_MODULE` for the first device). """ if not self._hw_is_initialized: # initialize hardware either by device number or serial if serial is None: UcanInitHardwareEx( byref(self._handle), device_number, self._callback_ref, None ) else: UcanInitHardwareEx2( byref(self._handle), serial, self._callback_ref, None ) self._hw_is_initialized = True def init_can( self, channel=Channel.CHANNEL_CH0, BTR=Baudrate.BAUD_1MBit, baudrate=BaudrateEx.BAUDEX_USE_BTR01, AMR=AMR_ALL, ACR=ACR_ALL, mode=Mode.MODE_NORMAL, OCR=OutputControl.OCR_DEFAULT, rx_buffer_entries=DEFAULT_BUFFER_ENTRIES, tx_buffer_entries=DEFAULT_BUFFER_ENTRIES, ): """ Initializes a specific CAN channel of a device. :param int channel: CAN channel to be initialized (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). :param int BTR: Baud rate register BTR0 as high byte, baud rate register BTR1 as low byte (see enum :class:`Baudrate`). :param int baudrate: Baud rate register for all systec USB-CANmoduls (see enum :class:`BaudrateEx`). :param int AMR: Acceptance filter mask (see method :meth:`set_acceptance`). :param int ACR: Acceptance filter code (see method :meth:`set_acceptance`). :param int mode: Transmission mode of CAN channel (see enum :class:`Mode`). :param int OCR: Output Control Register (see enum :class:`OutputControl`). :param int rx_buffer_entries: The number of maximum entries in the receive buffer. :param int tx_buffer_entries: The number of maximum entries in the transmit buffer. """ if not self._ch_is_initialized.get(channel, False): init_param = InitCanParam( mode, BTR, OCR, AMR, ACR, baudrate, rx_buffer_entries, tx_buffer_entries ) UcanInitCanEx2(self._handle, channel, init_param) self._ch_is_initialized[channel] = True def read_can_msg(self, channel, count): """ Reads one or more CAN-messages from the buffer of the specified CAN channel. :param int channel: CAN channel to read from (:data:`Channel.CHANNEL_CH0`, :data:`Channel.CHANNEL_CH1`, :data:`Channel.CHANNEL_ANY`). :param int count: The number of CAN messages to be received. :return: Tuple with list of CAN message/s received and the CAN channel where the read CAN messages came from. :rtype: tuple(list(CanMsg), int) """ c_channel = BYTE(channel) c_can_msg = (CanMsg * count)() c_count = DWORD(count) UcanReadCanMsgEx(self._handle, byref(c_channel), c_can_msg, byref(c_count)) return c_can_msg[: c_count.value], c_channel.value def write_can_msg(self, channel, can_msg): """ Transmits one ore more CAN messages through the specified CAN channel of the device. :param int channel: CAN channel, which is to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). :param list(CanMsg) can_msg: List of CAN message structure (see structure :class:`CanMsg`). :return: The number of successfully transmitted CAN messages. :rtype: int """ c_can_msg = (CanMsg * len(can_msg))(*can_msg) c_count = DWORD(len(can_msg)) UcanWriteCanMsgEx(self._handle, channel, c_can_msg, c_count) return c_count def set_baudrate(self, channel, BTR, baudarate): """ This function is used to configure the baud rate of specific CAN channel of a device. :param int channel: CAN channel, which is to be configured (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). :param int BTR: Baud rate register BTR0 as high byte, baud rate register BTR1 as low byte (see enum :class:`Baudrate`). :param int baudarate: Baud rate register for all systec USB-CANmoduls (see enum :class:`BaudrateEx`>). """ UcanSetBaudrateEx(self._handle, channel, BTR >> 8, BTR, baudarate) def set_acceptance(self, channel=Channel.CHANNEL_CH0, AMR=AMR_ALL, ACR=ACR_ALL): """ This function is used to change the acceptance filter values for a specific CAN channel on a device. :param int channel: CAN channel, which is to be configured (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). :param int AMR: Acceptance filter mask (AMR). :param int ACR: Acceptance filter code (ACR). """ UcanSetAcceptanceEx(self._handle, channel, AMR, ACR) def get_status(self, channel=Channel.CHANNEL_CH0): """ Returns the error status of a specific CAN channel. :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). :return: Tuple with CAN and USB status (see structure :class:`Status`). :rtype: tuple(int, int) """ status = Status() UcanGetStatusEx(self._handle, channel, byref(status)) return status.can_status, status.usb_status def get_msg_count_info(self, channel=Channel.CHANNEL_CH0): """ Reads the message counters of the specified CAN channel. :param int channel: CAN channel, which is to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). :return: Tuple with number of CAN messages sent and received. :rtype: tuple(int, int) """ msg_count_info = MsgCountInfo() UcanGetMsgCountInfoEx(self._handle, channel, byref(msg_count_info)) return msg_count_info.sent_msg_count, msg_count_info.recv_msg_count def reset_can(self, channel=Channel.CHANNEL_CH0, flags=ResetFlags.RESET_ALL): """ Resets a CAN channel of a device (hardware reset, empty buffer, and so on). :param int channel: CAN channel, to be reset (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). :param int flags: Flags defines what should be reset (see enum :class:`ResetFlags`). """ UcanResetCanEx(self._handle, channel, flags) def get_hardware_info(self): """ Returns the extended hardware information of a device. With multi-channel USB-CANmoduls the information for both CAN channels are returned separately. :return: Tuple with extended hardware information structure (see structure :class:`HardwareInfoEx`) and structures with information of CAN channel 0 and 1 (see structure :class:`ChannelInfo`). :rtype: tuple(HardwareInfoEx, ChannelInfo, ChannelInfo) """ hw_info_ex = HardwareInfoEx() can_info_ch0, can_info_ch1 = ChannelInfo(), ChannelInfo() UcanGetHardwareInfoEx2( self._handle, byref(hw_info_ex), byref(can_info_ch0), byref(can_info_ch1) ) return hw_info_ex, can_info_ch0, can_info_ch1 def get_fw_version(self): """ Returns the firmware version number of the device. :return: Firmware version number. :rtype: int """ return UcanGetFwVersion(self._handle) def define_cyclic_can_msg(self, channel, can_msg=None): """ Defines a list of CAN messages for automatic transmission. :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). :param list(CanMsg) can_msg: List of CAN messages (up to 16, see structure :class:`CanMsg`), or None to delete an older list. """ if can_msg is not None: c_can_msg = (CanMsg * len(can_msg))(*can_msg) c_count = DWORD(len(can_msg)) else: c_can_msg = CanMsg() c_count = 0 UcanDefineCyclicCanMsg(self._handle, channel, c_can_msg, c_count) def read_cyclic_can_msg(self, channel, count): """ Reads back the list of CAN messages for automatically sending. :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). :param int count: The number of cyclic CAN messages to be received. :return: List of received CAN messages (up to 16, see structure :class:`CanMsg`). :rtype: list(CanMsg) """ c_channel = BYTE(channel) c_can_msg = (CanMsg * count)() c_count = DWORD(count) UcanReadCyclicCanMsg(self._handle, byref(c_channel), c_can_msg, c_count) return c_can_msg[: c_count.value] def enable_cyclic_can_msg(self, channel, flags): """ Enables or disables the automatically sending. :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). :param int flags: Flags for enabling or disabling (see enum :class:`CyclicFlags`). """ UcanEnableCyclicCanMsg(self._handle, channel, flags) def get_msg_pending(self, channel, flags): """ Returns the number of pending CAN messages. :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). :param int flags: Flags specifies which buffers should be checked (see enum :class:`PendingFlags`). :return: The number of pending messages. :rtype: int """ count = DWORD(0) UcanGetMsgPending(self._handle, channel, flags, byref(count)) return count.value def get_can_error_counter(self, channel): """ Reads the current value of the error counters within the CAN controller. :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). :return: Tuple with the TX and RX error counter. :rtype: tuple(int, int) .. note:: Only available for systec USB-CANmoduls (NOT for GW-001 and GW-002 !!!). """ tx_error_counter = DWORD(0) rx_error_counter = DWORD(0) UcanGetCanErrorCounter( self._handle, channel, byref(tx_error_counter), byref(rx_error_counter) ) return tx_error_counter, rx_error_counter def set_tx_timeout(self, channel, timeout): """ Sets the transmission timeout. :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0` or :data:`Channel.CHANNEL_CH1`). :param float timeout: Transmit timeout in seconds (value 0 disables this feature). """ UcanSetTxTimeout(self._handle, channel, int(timeout * 1000)) def shutdown(self, channel=Channel.CHANNEL_ALL, shutdown_hardware=True): """ Shuts down all CAN interfaces and/or the hardware interface. :param int channel: CAN channel, to be used (:data:`Channel.CHANNEL_CH0`, :data:`Channel.CHANNEL_CH1` or :data:`Channel.CHANNEL_ALL`) :param bool shutdown_hardware: If true then the hardware interface will be closed too. """ # shutdown each channel if it's initialized for _channel, is_initialized in self._ch_is_initialized.items(): if is_initialized and ( _channel == channel or channel == Channel.CHANNEL_ALL or shutdown_hardware ): UcanDeinitCanEx(self._handle, _channel) self._ch_is_initialized[_channel] = False # shutdown hardware if self._hw_is_initialized and shutdown_hardware: UcanDeinitHardware(self._handle) self._hw_is_initialized = False self._handle = Handle(INVALID_HANDLE) @staticmethod def get_user_dll_version(): """ Returns the version number of the USBCAN-library. :return: Software version number. :rtype: int """ return UcanGetVersionEx(VersionType.VER_TYPE_USER_DLL) @staticmethod def set_debug_mode(level, filename, flags=0): """ This function enables the creation of a debug log file out of the USBCAN-library. If this feature has already been activated via the USB-CANmodul Control, the content of the “old” log file will be copied to the new file. Further debug information will be appended to the new file. :param int level: Debug level (bit format). :param str filename: File path to debug log file. :param int flags: Additional flags (bit0: file append mode). :return: False if logfile not created otherwise True. :rtype: bool """ return UcanSetDebugMode(level, filename, flags) @staticmethod def get_can_status_message(can_status): """ Converts a given CAN status value to the appropriate message string. :param can_status: CAN status value from method :meth:`get_status` (see enum :class:`CanStatus`) :return: Status message string. :rtype: str """ status_msgs = { CanStatus.CANERR_TXMSGLOST: "Transmit message lost", CanStatus.CANERR_MEMTEST: "Memory test failed", CanStatus.CANERR_REGTEST: "Register test failed", CanStatus.CANERR_QXMTFULL: "Transmit queue is full", CanStatus.CANERR_QOVERRUN: "Receive queue overrun", CanStatus.CANERR_QRCVEMPTY: "Receive queue is empty", CanStatus.CANERR_BUSOFF: "Bus Off", CanStatus.CANERR_BUSHEAVY: "Error Passive", CanStatus.CANERR_BUSLIGHT: "Warning Limit", CanStatus.CANERR_OVERRUN: "Rx-buffer is full", CanStatus.CANERR_XMTFULL: "Tx-buffer is full", } return ( "OK" if can_status == CanStatus.CANERR_OK else ", ".join( msg for status, msg in status_msgs.items() if can_status & status ) ) @staticmethod def get_baudrate_message(baudrate): """ Converts a given baud rate value for GW-001/GW-002 to the appropriate message string. :param Baudrate baudrate: Bus Timing Registers, BTR0 in high order byte and BTR1 in low order byte (see enum :class:`Baudrate`) :return: Baud rate message string. :rtype: str """ baudrate_msgs = { Baudrate.BAUD_AUTO: "auto baudrate", Baudrate.BAUD_10kBit: "10 kBit/sec", Baudrate.BAUD_20kBit: "20 kBit/sec", Baudrate.BAUD_50kBit: "50 kBit/sec", Baudrate.BAUD_100kBit: "100 kBit/sec", Baudrate.BAUD_125kBit: "125 kBit/sec", Baudrate.BAUD_250kBit: "250 kBit/sec", Baudrate.BAUD_500kBit: "500 kBit/sec", Baudrate.BAUD_800kBit: "800 kBit/sec", Baudrate.BAUD_1MBit: "1 MBit/s", Baudrate.BAUD_USE_BTREX: "BTR Ext is used", } return baudrate_msgs.get(baudrate, "BTR is unknown (user specific)") @staticmethod def get_baudrate_ex_message(baudrate_ex): """ Converts a given baud rate value for systec USB-CANmoduls to the appropriate message string. :param BaudrateEx baudrate_ex: Bus Timing Registers (see enum :class:`BaudrateEx`) :return: Baud rate message string. :rtype: str """ baudrate_ex_msgs = { Baudrate.BAUDEX_AUTO: "auto baudrate", Baudrate.BAUDEX_10kBit: "10 kBit/sec", Baudrate.BAUDEX_SP2_10kBit: "10 kBit/sec", Baudrate.BAUDEX_20kBit: "20 kBit/sec", Baudrate.BAUDEX_SP2_20kBit: "20 kBit/sec", Baudrate.BAUDEX_50kBit: "50 kBit/sec", Baudrate.BAUDEX_SP2_50kBit: "50 kBit/sec", Baudrate.BAUDEX_100kBit: "100 kBit/sec", Baudrate.BAUDEX_SP2_100kBit: "100 kBit/sec", Baudrate.BAUDEX_125kBit: "125 kBit/sec", Baudrate.BAUDEX_SP2_125kBit: "125 kBit/sec", Baudrate.BAUDEX_250kBit: "250 kBit/sec", Baudrate.BAUDEX_SP2_250kBit: "250 kBit/sec", Baudrate.BAUDEX_500kBit: "500 kBit/sec", Baudrate.BAUDEX_SP2_500kBit: "500 kBit/sec", Baudrate.BAUDEX_800kBit: "800 kBit/sec", Baudrate.BAUDEX_SP2_800kBit: "800 kBit/sec", Baudrate.BAUDEX_1MBit: "1 MBit/s", Baudrate.BAUDEX_SP2_1MBit: "1 MBit/s", Baudrate.BAUDEX_USE_BTR01: "BTR0/BTR1 is used", } return baudrate_ex_msgs.get(baudrate_ex, "BTR is unknown (user specific)") @staticmethod def get_product_code_message(product_code): product_code_msgs = { ProductCode.PRODCODE_PID_GW001: "GW-001", ProductCode.PRODCODE_PID_GW002: "GW-002", ProductCode.PRODCODE_PID_MULTIPORT: "Multiport CAN-to-USB G3", ProductCode.PRODCODE_PID_BASIC: "USB-CANmodul1 G3", ProductCode.PRODCODE_PID_ADVANCED: "USB-CANmodul2 G3", ProductCode.PRODCODE_PID_USBCAN8: "USB-CANmodul8 G3", ProductCode.PRODCODE_PID_USBCAN16: "USB-CANmodul16 G3", ProductCode.PRODCODE_PID_RESERVED3: "Reserved", ProductCode.PRODCODE_PID_ADVANCED_G4: "USB-CANmodul2 G4", ProductCode.PRODCODE_PID_BASIC_G4: "USB-CANmodul1 G4", ProductCode.PRODCODE_PID_RESERVED1: "Reserved", ProductCode.PRODCODE_PID_RESERVED2: "Reserved", } return product_code_msgs.get( product_code & PRODCODE_MASK_PID, "Product code is unknown" ) @classmethod def convert_to_major_ver(cls, version): """ Converts the a version number into the major version. :param int version: Version number to be converted. :return: Major version. :rtype: int """ return version & 0xFF @classmethod def convert_to_minor_ver(cls, version): """ Converts the a version number into the minor version. :param int version: Version number to be converted. :return: Minor version. :rtype: int """ return (version & 0xFF00) >> 8 @classmethod def convert_to_release_ver(cls, version): """ Converts the a version number into the release version. :param int version: Version number to be converted. :return: Release version. :rtype: int """ return (version & 0xFFFF0000) >> 16 @classmethod def check_version_is_equal_or_higher(cls, version, cmp_major, cmp_minor): """ Checks if the version is equal or higher than a specified value. :param int version: Version number to be checked. :param int cmp_major: Major version to be compared with. :param int cmp_minor: Minor version to be compared with. :return: True if equal or higher, otherwise False. :rtype: bool """ return (cls.convert_to_major_ver(version) > cmp_major) or ( cls.convert_to_major_ver(version) == cmp_major and cls.convert_to_minor_ver(version) >= cmp_minor ) @classmethod def check_is_systec(cls, hw_info_ex): """ Checks whether the module is a systec USB-CANmodul. :param HardwareInfoEx hw_info_ex: Extended hardware information structure (see method :meth:`get_hardware_info`). :return: True when the module is a systec USB-CANmodul, otherwise False. :rtype: bool """ return ( hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID ) >= ProductCode.PRODCODE_PID_MULTIPORT @classmethod def check_is_G4(cls, hw_info_ex): """ Checks whether the module is an USB-CANmodul of fourth generation (G4). :param HardwareInfoEx hw_info_ex: Extended hardware information structure (see method :meth:`get_hardware_info`). :return: True when the module is an USB-CANmodul G4, otherwise False. :rtype: bool """ return hw_info_ex.m_dwProductCode & PRODCODE_PID_G4 @classmethod def check_is_G3(cls, hw_info_ex): """ Checks whether the module is an USB-CANmodul of third generation (G3). :param HardwareInfoEx hw_info_ex: Extended hardware information structure (see method :meth:`get_hardware_info`). :return: True when the module is an USB-CANmodul G3, otherwise False. :rtype: bool """ return cls.check_is_systec(hw_info_ex) and not cls.check_is_G4(hw_info_ex) @classmethod def check_support_cyclic_msg(cls, hw_info_ex): """ Checks whether the module supports automatically transmission of cyclic CAN messages. :param HardwareInfoEx hw_info_ex: Extended hardware information structure (see method :meth:`get_hardware_info`). :return: True when the module does support cyclic CAN messages, otherwise False. :rtype: bool """ return cls.check_is_systec(hw_info_ex) and cls.check_version_is_equal_or_higher( hw_info_ex.m_dwFwVersionEx, 3, 6 ) @classmethod def check_support_two_channel(cls, hw_info_ex): """ Checks whether the module supports two CAN channels (at logical device). :param HardwareInfoEx hw_info_ex: Extended hardware information structure (see method :meth:`get_hardware_info`). :return: True when the module (logical device) does support two CAN channels, otherwise False. :rtype: bool """ return cls.check_is_systec(hw_info_ex) and ( hw_info_ex.m_dwProductCode & PRODCODE_PID_TWO_CHA ) @classmethod def check_support_term_resistor(cls, hw_info_ex): """ Checks whether the module supports a termination resistor at the CAN bus. :param HardwareInfoEx hw_info_ex: Extended hardware information structure (see method :meth:`get_hardware_info`). :return: True when the module does support a termination resistor. :rtype: bool """ return hw_info_ex.m_dwProductCode & PRODCODE_PID_TERM @classmethod def check_support_user_port(cls, hw_info_ex): """ Checks whether the module supports a user I/O port. :param HardwareInfoEx hw_info_ex: Extended hardware information structure (see method :meth:`get_hardware_info`). :return: True when the module supports a user I/O port, otherwise False. :rtype: bool """ return ( ( (hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID) != ProductCode.PRODCODE_PID_BASIC ) and ( (hw_info_ex.m_dwProductCode & PRODCODE_MASK_PID) != ProductCode.PRODCODE_PID_RESERVED1 ) and cls.check_version_is_equal_or_higher(hw_info_ex.m_dwFwVersionEx, 2, 16) ) @classmethod def check_support_rb_user_port(cls, hw_info_ex): """ Checks whether the module supports a user I/O port including read back feature. :param HardwareInfoEx hw_info_ex: Extended hardware information structure (see method :meth:`get_hardware_info`). :return: True when the module does support a user I/O port including the read back feature, otherwise False. :rtype: bool """ return hw_info_ex.m_dwProductCode & PRODCODE_PID_RBUSER @classmethod def check_support_rb_can_port(cls, hw_info_ex): """ Checks whether the module supports a CAN I/O port including read back feature. :param HardwareInfoEx hw_info_ex: Extended hardware information structure (see method :meth:`get_hardware_info`). :return: True when the module does support a CAN I/O port including the read back feature, otherwise False. :rtype: bool """ return hw_info_ex.m_dwProductCode & PRODCODE_PID_RBCAN @classmethod def check_support_ucannet(cls, hw_info_ex): """ Checks whether the module supports the usage of USB-CANnetwork driver. :param HardwareInfoEx hw_info_ex: Extended hardware information structure (see method :meth:`get_hardware_info`). :return: True when the module does support the usage of the USB-CANnetwork driver, otherwise False. :rtype: bool """ return cls.check_is_systec(hw_info_ex) and cls.check_version_is_equal_or_higher( hw_info_ex.m_dwFwVersionEx, 3, 8 ) @classmethod def calculate_amr(cls, is_extended, from_id, to_id, rtr_only=False, rtr_too=True): """ Calculates AMR using CAN-ID range as parameter. :param bool is_extended: If True parameters from_id and to_id contains 29-bit CAN-ID. :param int from_id: First CAN-ID which should be received. :param int to_id: Last CAN-ID which should be received. :param bool rtr_only: If True only RTR-Messages should be received, and rtr_too will be ignored. :param bool rtr_too: If True CAN data frames and RTR-Messages should be received. :return: Value for AMR. :rtype: int """ return ( (((from_id ^ to_id) << 3) | (0x7 if rtr_too and not rtr_only else 0x3)) if is_extended else ( ((from_id ^ to_id) << 21) | (0x1FFFFF if rtr_too and not rtr_only else 0xFFFFF) ) ) @classmethod def calculate_acr(cls, is_extended, from_id, to_id, rtr_only=False, rtr_too=True): """ Calculates ACR using CAN-ID range as parameter. :param bool is_extended: If True parameters from_id and to_id contains 29-bit CAN-ID. :param int from_id: First CAN-ID which should be received. :param int to_id: Last CAN-ID which should be received. :param bool rtr_only: If True only RTR-Messages should be received, and rtr_too will be ignored. :param bool rtr_too: If True CAN data frames and RTR-Messages should be received. :return: Value for ACR. :rtype: int """ return ( (((from_id & to_id) << 3) | (0x04 if rtr_only else 0)) if is_extended else (((from_id & to_id) << 21) | (0x100000 if rtr_only else 0)) ) def _connect_control(self, event, param, arg): """ Is the actual callback function for :meth:`init_hw_connect_control_ex`. :param event: Event (:data:`CbEvent.EVENT_CONNECT`, :data:`CbEvent.EVENT_DISCONNECT` or :data:`CbEvent.EVENT_FATALDISCON`). :param param: Additional parameter depending on the event. - CbEvent.EVENT_CONNECT: always 0 - CbEvent.EVENT_DISCONNECT: always 0 - CbEvent.EVENT_FATALDISCON: USB-CAN-Handle of the disconnected module :param arg: Additional parameter defined with :meth:`init_hardware_ex` (not used in this wrapper class). """ log.debug("Event: %s, Param: %s", event, param) if event == CbEvent.EVENT_FATALDISCON: self.fatal_disconnect_event(param) elif event == CbEvent.EVENT_CONNECT: self.connect_event() elif event == CbEvent.EVENT_DISCONNECT: self.disconnect_event() def _callback(self, handle, event, channel, arg): """ Is called if a working event occurred. :param int handle: USB-CAN-Handle returned by the function :meth:`init_hardware`. :param int event: Event type. :param int channel: CAN channel (:data:`Channel.CHANNEL_CH0`, :data:`Channel.CHANNEL_CH1` or :data:`Channel.CHANNEL_ANY`). :param arg: Additional parameter defined with :meth:`init_hardware_ex`. """ log.debug("Handle: %s, Event: %s, Channel: %s", handle, event, channel) if event == CbEvent.EVENT_INITHW: self.init_hw_event() elif event == CbEvent.EVENT_init_can: self.init_can_event(channel) elif event == CbEvent.EVENT_RECEIVE: self.can_msg_received_event(channel) elif event == CbEvent.EVENT_STATUS: self.status_event(channel) elif event == CbEvent.EVENT_DEINIT_CAN: self.deinit_can_event(channel) elif event == CbEvent.EVENT_DEINITHW: self.deinit_hw_event() def init_hw_event(self): """ Event occurs when an USB-CANmodul has been initialized (see method :meth:`init_hardware`). .. note:: To be overridden by subclassing. """ def init_can_event(self, channel): """ Event occurs when a CAN interface of an USB-CANmodul has been initialized. :param int channel: Specifies the CAN channel which was initialized (see method :meth:`init_can`). .. note:: To be overridden by subclassing. """ def can_msg_received_event(self, channel): """ Event occurs when at leas one CAN message has been received. Call the method :meth:`read_can_msg` to receive the CAN messages. :param int channel: Specifies the CAN channel which received CAN messages. .. note:: To be overridden by subclassing. """ def status_event(self, channel): """ Event occurs when the error status of a module has been changed. Call the method :meth:`get_status` to receive the error status. :param int channel: Specifies the CAN channel which status has been changed. .. note:: To be overridden by subclassing. """ def deinit_can_event(self, channel): """ Event occurs when a CAN interface has been deinitialized (see method :meth:`shutdown`). :param int channel: Specifies the CAN channel which status has been changed. .. note:: To be overridden by subclassing. """ def deinit_hw_event(self): """ Event occurs when an USB-CANmodul has been deinitialized (see method :meth:`shutdown`). .. note:: To be overridden by subclassing. """ def connect_event(self): """ Event occurs when a new USB-CANmodul has been connected to the host. .. note:: To be overridden by subclassing. """ def disconnect_event(self): """ Event occurs when an USB-CANmodul has been disconnected from the host. .. note:: To be overridden by subclassing. """ def fatal_disconnect_event(self, device_number): """ Event occurs when an USB-CANmodul has been disconnected from the host which was currently initialized. No method can be called for this module. :param int device_number: The device number which was disconnected. .. note:: To be overridden by subclassing. """ UcanServer._enum_callback_ref = EnumCallback(UcanServer._enum_callback) python-can-4.5.0/can/interfaces/systec/ucanbus.py000066400000000000000000000261321472200326600220160ustar00rootroot00000000000000import logging from threading import Event from can import ( BusABC, BusState, CanError, CanInitializationError, CanOperationError, CanProtocol, Message, ) from .constants import * from .exceptions import UcanException from .structures import * from .ucan import UcanServer log = logging.getLogger("can.systec") class Ucan(UcanServer): """ Wrapper around UcanServer to read messages with timeout using events. """ def __init__(self): super().__init__() self._msg_received_event = Event() def can_msg_received_event(self, channel): self._msg_received_event.set() def read_can_msg(self, channel, count, timeout): self._msg_received_event.clear() if self.get_msg_pending(channel, PendingFlags.PENDING_FLAG_RX_DLL) == 0: if not self._msg_received_event.wait(timeout): return None, False return super().read_can_msg(channel, 1) class UcanBus(BusABC): """ The CAN Bus implemented for the SYSTEC interface. """ BITRATES = { 10000: Baudrate.BAUD_10kBit, 20000: Baudrate.BAUD_20kBit, 50000: Baudrate.BAUD_50kBit, 100000: Baudrate.BAUD_100kBit, 125000: Baudrate.BAUD_125kBit, 250000: Baudrate.BAUD_250kBit, 500000: Baudrate.BAUD_500kBit, 800000: Baudrate.BAUD_800kBit, 1000000: Baudrate.BAUD_1MBit, } def __init__(self, channel, can_filters=None, **kwargs): """ :param int channel: The Channel id to create this bus with. :param list can_filters: See :meth:`can.BusABC.set_filters`. Backend Configuration :param int bitrate: Channel bitrate in bit/s. Default is 500000. :param int device_number: The device number of the USB-CAN. Valid values: 0 through 254. Special value 255 is reserved to detect the first connected device (should only be used, in case only one module is connected to the computer). Default is 255. :param can.bus.BusState state: BusState of the channel. Default is ACTIVE. :param bool receive_own_messages: If messages transmitted should also be received back. Default is False. :param int rx_buffer_entries: The maximum number of entries in the receive buffer. Default is 4096. :param int tx_buffer_entries: The maximum number of entries in the transmit buffer. Default is 4096. :raises ValueError: If invalid input parameter were passed. :raises ~can.exceptions.CanInterfaceNotImplementedError: If the platform is not supported. :raises ~can.exceptions.CanInitializationError: If hardware or CAN interface initialization failed. """ try: self._ucan = Ucan() except CanError as error: raise error except Exception as exception: raise CanInitializationError( "The SYSTEC ucan library has not been initialized." ) from exception self.channel = int(channel) self._can_protocol = CanProtocol.CAN_20 device_number = int(kwargs.get("device_number", ANY_MODULE)) # configuration options bitrate = kwargs.get("bitrate", 500000) if bitrate not in self.BITRATES: raise ValueError(f"Invalid bitrate {bitrate}") state = kwargs.get("state", BusState.ACTIVE) if state is BusState.ACTIVE or state is BusState.PASSIVE: self._state = state else: raise ValueError("BusState must be Active or Passive") # get parameters self._params = { "mode": Mode.MODE_NORMAL | (Mode.MODE_TX_ECHO if kwargs.get("receive_own_messages") else 0) | (Mode.MODE_LISTEN_ONLY if state is BusState.PASSIVE else 0), "BTR": self.BITRATES[bitrate], } # get extra parameters if kwargs.get("rx_buffer_entries"): self._params["rx_buffer_entries"] = int(kwargs.get("rx_buffer_entries")) if kwargs.get("tx_buffer_entries"): self._params["tx_buffer_entries"] = int(kwargs.get("tx_buffer_entries")) try: self._ucan.init_hardware(device_number=device_number) self._ucan.init_can(self.channel, **self._params) hw_info_ex, _, _ = self._ucan.get_hardware_info() self.channel_info = ( f"{self._ucan.get_product_code_message(hw_info_ex.product_code)}, " f"S/N {hw_info_ex.serial}, " f"CH {self.channel}, " f"BTR {self._ucan.get_baudrate_message(self.BITRATES[bitrate])}" ) except UcanException as exception: raise CanInitializationError() from exception self._is_filtered = False super().__init__( channel=channel, can_filters=can_filters, **kwargs, ) def _recv_internal(self, timeout): try: message, _ = self._ucan.read_can_msg(self.channel, 1, timeout) except UcanException as exception: raise CanOperationError() from exception if not message: return None, False msg = Message( timestamp=float(message[0].time) / 1000.0, is_remote_frame=bool(message[0].frame_format & MsgFrameFormat.MSG_FF_RTR), is_extended_id=bool(message[0].frame_format & MsgFrameFormat.MSG_FF_EXT), arbitration_id=message[0].id, dlc=len(message[0].data), data=message[0].data, ) return msg, self._is_filtered def send(self, msg, timeout=None): """ Sends one CAN message. When a transmission timeout is set the firmware tries to send a message within this timeout. If it could not be sent the firmware sets the "auto delete" state. Within this state all transmit CAN messages for this channel will be deleted automatically for not blocking the other channel. :param can.Message msg: The CAN message. :param float timeout: Transmit timeout in seconds (value 0 switches off the "auto delete") :raises ~can.exceptions.CanOperationError: If the message could not be sent. """ try: if timeout is not None and timeout >= 0: self._ucan.set_tx_timeout(self.channel, int(timeout * 1000)) message = CanMsg( msg.arbitration_id, MsgFrameFormat.MSG_FF_STD | (MsgFrameFormat.MSG_FF_EXT if msg.is_extended_id else 0) | (MsgFrameFormat.MSG_FF_RTR if msg.is_remote_frame else 0), msg.data, msg.dlc, ) self._ucan.write_can_msg(self.channel, [message]) except UcanException as exception: raise CanOperationError() from exception @staticmethod def _detect_available_configs(): configs = [] try: # index, is_used, hw_info_ex, init_info for _, _, hw_info_ex, _ in Ucan.enumerate_hardware(): configs.append( { "interface": "systec", "channel": Channel.CHANNEL_CH0, "device_number": hw_info_ex.device_number, } ) if Ucan.check_support_two_channel(hw_info_ex): configs.append( { "interface": "systec", "channel": Channel.CHANNEL_CH1, "device_number": hw_info_ex.device_number, } ) except Exception: log.warning("The SYSTEC ucan library has not been initialized.") return configs def _apply_filters(self, filters): try: if filters and len(filters) == 1: can_id = filters[0]["can_id"] can_mask = filters[0]["can_mask"] self._ucan.set_acceptance(self.channel, can_mask, can_id) self._is_filtered = True log.info("Hardware filtering on ID 0x%X, mask 0x%X", can_id, can_mask) else: self._ucan.set_acceptance(self.channel) self._is_filtered = False log.info("Hardware filtering has been disabled") except UcanException as exception: raise CanOperationError() from exception def flush_tx_buffer(self): """ Flushes the transmit buffer. :raises ~can.exceptions.CanError: If flushing of the transmit buffer failed. """ log.info("Flushing transmit buffer") try: self._ucan.reset_can(self.channel, ResetFlags.RESET_ONLY_TX_BUFF) except UcanException as exception: raise CanOperationError() from exception @staticmethod def create_filter(extended, from_id, to_id, rtr_only, rtr_too): """ Calculates AMR and ACR using CAN-ID as parameter. :param bool extended: if True parameters from_id and to_id contains 29-bit CAN-ID :param int from_id: first CAN-ID which should be received :param int to_id: last CAN-ID which should be received :param bool rtr_only: if True only RTR-Messages should be received, and rtr_too will be ignored :param bool rtr_too: if True CAN data frames and RTR-Messages should be received :return: Returns list with one filter containing a "can_id", a "can_mask" and "extended" key. """ return [ { "can_id": Ucan.calculate_acr( extended, from_id, to_id, rtr_only, rtr_too ), "can_mask": Ucan.calculate_amr( extended, from_id, to_id, rtr_only, rtr_too ), "extended": extended, } ] @property def state(self): return self._state @state.setter def state(self, new_state): if self._state is not BusState.ERROR and ( new_state is BusState.ACTIVE or new_state is BusState.PASSIVE ): try: # close the CAN channel self._ucan.shutdown(self.channel, False) # set mode if new_state is BusState.ACTIVE: self._params["mode"] &= ~Mode.MODE_LISTEN_ONLY else: self._params["mode"] |= Mode.MODE_LISTEN_ONLY # reinitialize CAN channel self._ucan.init_can(self.channel, **self._params) except UcanException as exception: raise CanOperationError() from exception def shutdown(self): """ Shuts down all CAN interfaces and hardware interface. """ super().shutdown() try: self._ucan.shutdown() except Exception as exception: log.error(exception) python-can-4.5.0/can/interfaces/udp_multicast/000077500000000000000000000000001472200326600213435ustar00rootroot00000000000000python-can-4.5.0/can/interfaces/udp_multicast/__init__.py000066400000000000000000000002351472200326600234540ustar00rootroot00000000000000"""A module to allow CAN over UDP on IPv4/IPv6 multicast.""" __all__ = [ "UdpMulticastBus", "bus", "utils", ] from .bus import UdpMulticastBus python-can-4.5.0/can/interfaces/udp_multicast/bus.py000066400000000000000000000414571472200326600225210ustar00rootroot00000000000000import errno import logging import select import socket import struct import warnings from typing import List, Optional, Tuple, Union import can from can import BusABC, CanProtocol from can.typechecking import AutoDetectedConfig from .utils import check_msgpack_installed, pack_message, unpack_message try: 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 IP_ADDRESS_INFO = Union[IPv4_ADDRESS_INFO, IPv6_ADDRESS_INFO] # Additional constants for the interaction with Unix kernels SO_TIMESTAMPNS = 35 SIOCGSTAMP = 0x8906 class UdpMulticastBus(BusABC): """A virtual interface for CAN communications between multiple processes using UDP over Multicast IP. It supports IPv4 and IPv6, specified via the channel (which really is just a multicast IP address as a string). You can also specify the port and the IPv6 *hop limit*/the IPv4 *time to live* (TTL). This bus does not support filtering based on message IDs on the kernel level but instead provides it in user space (in Python) as a fallback. Both default addresses should allow for multi-host CAN networks in a normal local area network (LAN) where multicast is enabled. .. note:: The auto-detection of available interfaces (see) is implemented using heuristic that checks if the required socket operations are available. It then returns two configurations, one based on the :attr:`~UdpMulticastBus.DEFAULT_GROUP_IPv6` address and another one based on the :attr:`~UdpMulticastBus.DEFAULT_GROUP_IPv4` address. .. warning:: The parameter `receive_own_messages` is currently unsupported and setting it to `True` will raise an exception. .. warning:: This interface does not make guarantees on reliable delivery and message ordering, and also does not implement rate limiting or ID arbitration/prioritization under high loads. Please refer to the section :ref:`virtual_interfaces_doc` for more information on this and a comparison to alternatives. :param channel: A multicast IPv4 address (in `224.0.0.0/4`) or an IPv6 address (in `ff00::/8`). This defines which version of IP is used. See `Wikipedia ("Multicast address") `__ for more details on the addressing schemes. Defaults to :attr:`~UdpMulticastBus.DEFAULT_GROUP_IPv6`. :param port: The IP port to read from and write to. :param hop_limit: The hop limit in IPv6 or in IPv4 the time to live (TTL). :param receive_own_messages: If transmitted messages should also be received by this bus. CURRENTLY UNSUPPORTED. :param fd: If CAN-FD frames should be supported. If set to false, an error will be raised upon sending such a frame and such received frames will be ignored. :param can_filters: See :meth:`~can.BusABC.set_filters`. :raises RuntimeError: If the *msgpack*-dependency is not available. It should be installed on all non Windows platforms via the `setup.py` requirements. :raises NotImplementedError: If the `receive_own_messages` is passed as `True`. """ #: An arbitrary IPv6 multicast address with "site-local" scope, i.e. only to be routed within the local #: physical network and not beyond it. It should allow for multi-host CAN networks in a normal IPv6 LAN. #: This is the default channel and should work with most modern routers if multicast is allowed. DEFAULT_GROUP_IPv6 = "ff15:7079:7468:6f6e:6465:6d6f:6d63:6173" #: An arbitrary IPv4 multicast address with "administrative" scope, i.e. only to be routed within #: administrative organizational boundaries and not beyond it. #: It should allow for multi-host CAN networks in a normal IPv4 LAN. #: This is provided as a default fallback channel if IPv6 is (still) not supported. DEFAULT_GROUP_IPv4 = "239.74.163.2" def __init__( self, channel: str = DEFAULT_GROUP_IPv6, port: int = 43113, hop_limit: int = 1, receive_own_messages: bool = False, fd: bool = True, **kwargs, ) -> None: check_msgpack_installed() if receive_own_messages: raise can.CanInterfaceNotImplementedError( "receiving own messages is not yet implemented" ) super().__init__( channel, **kwargs, ) self._multicast = GeneralPurposeUdpMulticastBus(channel, port, hop_limit) self._can_protocol = CanProtocol.CAN_FD if fd else CanProtocol.CAN_20 @property def is_fd(self) -> bool: class_name = self.__class__.__name__ warnings.warn( f"The {class_name}.is_fd property is deprecated and superseded by " f"{class_name}.protocol. It is scheduled for removal in python-can version 5.0.", DeprecationWarning, stacklevel=2, ) return self._can_protocol is CanProtocol.CAN_FD def _recv_internal(self, timeout: Optional[float]): result = self._multicast.recv(timeout) if not result: return None, False data, _, timestamp = result try: can_message = unpack_message( data, replace={"timestamp": timestamp}, check=True ) except Exception as exception: raise can.CanOperationError( "could not unpack received message" ) from exception if self._can_protocol is not CanProtocol.CAN_FD and can_message.is_fd: return None, False return can_message, False def send(self, msg: can.Message, timeout: Optional[float] = None) -> None: if self._can_protocol is not CanProtocol.CAN_FD and msg.is_fd: raise can.CanOperationError( "cannot send FD message over bus with CAN FD disabled" ) data = pack_message(msg) self._multicast.send(data, timeout) def fileno(self) -> int: """Provides the internally used file descriptor of the socket or `-1` if not available.""" return self._multicast.fileno() def shutdown(self) -> None: """Close all sockets and free up any resources. Never throws errors and only logs them. """ super().shutdown() self._multicast.shutdown() @staticmethod def _detect_available_configs() -> List[AutoDetectedConfig]: if hasattr(socket, "CMSG_SPACE"): return [ { "interface": "udp_multicast", "channel": UdpMulticastBus.DEFAULT_GROUP_IPv6, }, { "interface": "udp_multicast", "channel": UdpMulticastBus.DEFAULT_GROUP_IPv4, }, ] # else, this interface cannot be used return [] class GeneralPurposeUdpMulticastBus: """A general purpose send and receive handler for multicast over IP/UDP. However, it raises CAN-specific exceptions for convenience. """ def __init__( self, group: str, port: int, hop_limit: int, max_buffer: int = 4096 ) -> None: self.group = group self.port = port self.hop_limit = hop_limit self.max_buffer = max_buffer # `False` will always work, no matter the setup. This might be changed by _create_socket(). self.timestamp_nanosecond = False # Look up multicast group address in name server and find out IP version of the first suitable target # and then get the address family of it (socket.AF_INET or socket.AF_INET6) connection_candidates = socket.getaddrinfo( # type: ignore group, self.port, type=socket.SOCK_DGRAM ) sock = None for connection_candidate in connection_candidates: address_family: socket.AddressFamily = connection_candidate[0] self.ip_version = 4 if address_family == socket.AF_INET else 6 try: sock = self._create_socket(address_family) except OSError as error: log.info( "could not connect to the multicast IP network of candidate %s; reason: %s", connection_candidates, error, ) if sock is not None: self._socket = sock else: raise can.CanInitializationError( "could not connect to a multicast IP network" ) # used in recv() self.received_timestamp_struct = "@ll" self.received_timestamp_struct_size = struct.calcsize( self.received_timestamp_struct ) if self.timestamp_nanosecond: self.received_ancillary_buffer_size = socket.CMSG_SPACE( self.received_timestamp_struct_size ) else: self.received_ancillary_buffer_size = 0 # used by send() self._send_destination = (self.group, self.port) self._last_send_timeout: Optional[float] = None def _create_socket(self, address_family: socket.AddressFamily) -> socket.socket: """Creates a new socket. This might fail and raise an exception! :param address_family: whether this is of type `socket.AF_INET` or `socket.AF_INET6` :raises can.CanInitializationError: if the socket could not be opened or configured correctly; in this case, it is guaranteed to be closed/cleaned up """ # create the UDP socket # this might already fail but then there is nothing to clean up sock = socket.socket(address_family, socket.SOCK_DGRAM) # configure the socket try: # set hop limit / TTL ttl_as_binary = struct.pack("@I", self.hop_limit) if self.ip_version == 4: sock.setsockopt( socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl_as_binary ) else: sock.setsockopt( socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, ttl_as_binary ) # Allow multiple programs to access that address + port sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 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 self.timestamp_nanosecond = False else: raise error else: self.timestamp_nanosecond = True # Bind it to the port (on any interface) sock.bind(("", self.port)) # Join the multicast group group_as_binary = socket.inet_pton(address_family, self.group) if self.ip_version == 4: request = group_as_binary + struct.pack("@I", socket.INADDR_ANY) sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, request) else: request = group_as_binary + struct.pack("@I", 0) sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, request) return sock except OSError as error: # clean up the incompletely configured but opened socket try: sock.close() except OSError as close_error: # ignore but log any failures in here log.warning("Could not close partly configured socket: %s", close_error) # still raise the error raise can.CanInitializationError( "could not create or configure socket" ) from error def send(self, data: bytes, timeout: Optional[float] = None) -> None: """Send data to all group members. This call blocks. :param timeout: the timeout in seconds after which an Exception is raised is sending has failed :param data: the data to be sent :raises can.CanOperationError: if an error occurred while writing to the underlying socket :raises can.CanTimeoutError: if the timeout ran out before sending was completed """ if timeout != self._last_send_timeout: self._last_send_timeout = timeout # this applies to all blocking calls on the socket, but sending is the only one that is blocking self._socket.settimeout(timeout) try: bytes_sent = self._socket.sendto(data, self._send_destination) if bytes_sent < len(data): raise TimeoutError() except TimeoutError: raise can.CanTimeoutError() from None except OSError as error: raise can.CanOperationError("failed to send via socket") from error def recv( self, timeout: Optional[float] = None ) -> Optional[Tuple[bytes, IP_ADDRESS_INFO, float]]: """ Receive up to **max_buffer** bytes. :param timeout: the timeout in seconds after which `None` is returned if no data arrived :returns: `None` on timeout, or a 3-tuple comprised of: - received data, - the sender of the data, and - a timestamp in seconds """ # get all sockets that are ready (can be a list with a single value # being self.socket or an empty list if self.socket is not ready) try: # get all sockets that are ready (can be a list with a single value # being self.socket or an empty list if self.socket is not ready) ready_receive_sockets, _, _ = select.select([self._socket], [], [], timeout) except OSError as exc: # something bad (not a timeout) happened (e.g. the interface went down) raise can.CanOperationError( f"Failed to wait for IP/UDP socket: {exc}" ) from exc if ready_receive_sockets: # not empty # fetch 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: # Very similar to timestamp handling in can/interfaces/socketcan/socketcan.py -> capture_message() if len(ancillary_data) != 1: raise can.CanOperationError( "Only requested a single extra field but got a different amount" ) cmsg_level, cmsg_type, cmsg_data = ancillary_data[0] if cmsg_level != socket.SOL_SOCKET or cmsg_type != SO_TIMESTAMPNS: raise can.CanOperationError( "received control message type that was not requested" ) # see https://man7.org/linux/man-pages/man3/timespec.3.html -> struct timespec for details seconds, nanoseconds = struct.unpack( self.received_timestamp_struct, cmsg_data ) if nanoseconds >= 1e9: raise can.CanOperationError( f"Timestamp nanoseconds field was out of range: {nanoseconds} not less than 1e9" ) timestamp = seconds + nanoseconds * 1.0e-9 else: result_buffer = ioctl( self._socket.fileno(), SIOCGSTAMP, bytes(self.received_timestamp_struct_size), ) seconds, microseconds = struct.unpack( self.received_timestamp_struct, result_buffer ) if microseconds >= 1e6: raise can.CanOperationError( f"Timestamp microseconds field was out of range: {microseconds} not less than 1e6" ) timestamp = seconds + microseconds * 1e-6 return raw_message_data, sender_address, timestamp # socket wasn't readable or timeout occurred return None def fileno(self) -> int: """Provides the internally used file descriptor of the socket or `-1` if not available.""" return self._socket.fileno() def shutdown(self) -> None: """Close all sockets and free up any resources. Never throws errors and only logs them. """ try: self._socket.close() except OSError as exception: log.error("could not close IP socket: %s", exception) python-can-4.5.0/can/interfaces/udp_multicast/utils.py000066400000000000000000000041611472200326600230570ustar00rootroot00000000000000""" Defines common functions. """ from typing import Any, Dict, Optional from can import CanInterfaceNotImplementedError, Message from can.typechecking import ReadableBytesLike try: import msgpack except ImportError: msgpack = None def check_msgpack_installed() -> None: """Raises a :class:`can.CanInterfaceNotImplementedError` if `msgpack` is not installed.""" if msgpack is None: raise CanInterfaceNotImplementedError("msgpack not installed") def pack_message(message: Message) -> bytes: """ Pack a can.Message into a msgpack byte blob. :param message: the message to be packed """ check_msgpack_installed() as_dict = { "timestamp": message.timestamp, "arbitration_id": message.arbitration_id, "is_extended_id": message.is_extended_id, "is_remote_frame": message.is_remote_frame, "is_error_frame": message.is_error_frame, "channel": message.channel, "dlc": message.dlc, "data": message.data, "is_fd": message.is_fd, "bitrate_switch": message.bitrate_switch, "error_state_indicator": message.error_state_indicator, } return msgpack.packb(as_dict, use_bin_type=True) def unpack_message( data: ReadableBytesLike, replace: Optional[Dict[str, Any]] = None, check: bool = False, ) -> Message: """Unpack a can.Message from a msgpack byte blob. :param data: the raw data :param replace: a mapping from field names to values to be replaced after decoding the new message, or `None` to disable this feature :param check: this is passed to :meth:`can.Message.__init__` to specify whether to validate the message :raise TypeError: if the data contains key that are not valid arguments for :meth:`can.Message.__init__` :raise ValueError: if `check` is true and the message metadata is invalid in some way :raise Exception: if there was another problem while unpacking """ check_msgpack_installed() as_dict = msgpack.unpackb(data, raw=False) if replace is not None: as_dict.update(replace) return Message(check=check, **as_dict) python-can-4.5.0/can/interfaces/usb2can/000077500000000000000000000000001472200326600200235ustar00rootroot00000000000000python-can-4.5.0/can/interfaces/usb2can/__init__.py000066400000000000000000000003751472200326600221410ustar00rootroot00000000000000""" """ __all__ = [ "Usb2CanAbstractionLayer", "Usb2canBus", "serial_selector", "usb2canabstractionlayer", "usb2canInterface", ] from .usb2canabstractionlayer import Usb2CanAbstractionLayer from .usb2canInterface import Usb2canBus python-can-4.5.0/can/interfaces/usb2can/serial_selector.py000066400000000000000000000032051472200326600235540ustar00rootroot00000000000000""" """ import logging from typing import List log = logging.getLogger("can.usb2can") try: import win32com.client except ImportError: log.warning( "win32com.client module required for usb2can. Install the 'pywin32' package." ) raise def WMIDateStringToDate(dtmDate) -> str: if dtmDate[4] == 0: strDateTime = dtmDate[5] + "/" else: strDateTime = dtmDate[4] + dtmDate[5] + "/" if dtmDate[6] == 0: strDateTime = strDateTime + dtmDate[7] + "/" else: strDateTime = strDateTime + dtmDate[6] + dtmDate[7] + "/" strDateTime = ( strDateTime + dtmDate[0] + dtmDate[1] + dtmDate[2] + dtmDate[3] + " " + dtmDate[8] + dtmDate[9] + ":" + dtmDate[10] + dtmDate[11] + ":" + dtmDate[12] + dtmDate[13] ) return strDateTime def find_serial_devices(serial_matcher: str = "") -> List[str]: """ Finds a list of USB devices where the serial number (partially) matches the given string. :param serial_matcher: only device IDs starting with this string are returned """ serial_numbers = [] wmi = win32com.client.GetObject("winmgmts:") for usb_controller in wmi.InstancesOf("Win32_USBControllerDevice"): usb_device = wmi.Get(usb_controller.Dependent) if "USB2CAN" in usb_device.Name: serial_numbers.append(usb_device.DeviceID.split("\\")[-1]) if serial_matcher: return [sn for sn in serial_numbers if serial_matcher in sn] return serial_numbers python-can-4.5.0/can/interfaces/usb2can/usb2canInterface.py000066400000000000000000000147701472200326600235640ustar00rootroot00000000000000""" This interface is for Windows only, otherwise use SocketCAN. """ import logging from ctypes import byref from typing import Optional, Union from can import ( BitTiming, BitTimingFd, BusABC, CanInitializationError, CanOperationError, CanProtocol, Message, ) from can.util import check_or_adjust_timing_clock from .serial_selector import find_serial_devices from .usb2canabstractionlayer import ( IS_ERROR_FRAME, IS_ID_TYPE, IS_REMOTE_FRAME, CanalError, CanalMsg, Usb2CanAbstractionLayer, ) # Set up logging log = logging.getLogger("can.usb2can") def message_convert_tx(msg): message_tx = CanalMsg() length = msg.dlc message_tx.sizeData = length message_tx.id = msg.arbitration_id for i in range(length): message_tx.data[i] = msg.data[i] message_tx.flags = 0x80000000 if msg.is_error_frame: message_tx.flags |= IS_ERROR_FRAME if msg.is_remote_frame: message_tx.flags |= IS_REMOTE_FRAME if msg.is_extended_id: message_tx.flags |= IS_ID_TYPE return message_tx def message_convert_rx(message_rx): """convert the message from the CANAL type to pythoncan type""" is_extended_id = bool(message_rx.flags & IS_ID_TYPE) is_remote_frame = bool(message_rx.flags & IS_REMOTE_FRAME) is_error_frame = bool(message_rx.flags & IS_ERROR_FRAME) return Message( timestamp=message_rx.timestamp, is_remote_frame=is_remote_frame, is_extended_id=is_extended_id, is_error_frame=is_error_frame, arbitration_id=message_rx.id, dlc=message_rx.sizeData, data=message_rx.data[: message_rx.sizeData], ) class Usb2canBus(BusABC): """Interface to a USB2CAN Bus. This interface only works on Windows. Please use socketcan on Linux. :param channel: The device's serial number. If not provided, Windows Management Instrumentation will be used to identify the first such device. :param bitrate: Bitrate of channel in bit/s. Values will be limited to a maximum of 1000 Kb/s. Default is 500 Kbs :param timing: Optional :class:`~can.BitTiming` instance to use for custom bit timing setting. If this argument is set then it overrides the bitrate argument. The `f_clock` value of the timing instance must be set to 32_000_000 (32MHz) for standard CAN. CAN FD and the :class:`~can.BitTimingFd` class are not supported. :param flags: Flags to directly pass to open function of the usb2can abstraction layer. :param dll: Path to the DLL with the CANAL API to load Defaults to 'usb2can.dll' :param serial: Alias for `channel` that is provided for legacy reasons. If both `serial` and `channel` are set, `serial` will be used and channel will be ignored. """ def __init__( self, channel: Optional[str] = None, dll: str = "usb2can.dll", flags: int = 0x00000008, bitrate: int = 500000, timing: Optional[Union[BitTiming, BitTimingFd]] = None, serial: Optional[str] = None, **kwargs, ): self.can = Usb2CanAbstractionLayer(dll) # get the serial number of the device device_id = serial or channel # search for a serial number if the device_id is None or empty if not device_id: devices = find_serial_devices() if not devices: raise CanInitializationError("could not automatically find any device") device_id = devices[0] self.channel_info = f"USB2CAN device {device_id}" if isinstance(timing, BitTiming): timing = check_or_adjust_timing_clock(timing, valid_clocks=[32_000_000]) connector = ( f"{device_id};" "0;" f"{timing.tseg1};" f"{timing.tseg2};" f"{timing.sjw};" f"{timing.brp}" ) elif isinstance(timing, BitTimingFd): raise NotImplementedError( f"CAN FD is not supported by {self.__class__.__name__}." ) else: # convert to kb/s and cap: max rate is 1000 kb/s baudrate = min(int(bitrate // 1000), 1000) connector = f"{device_id};{baudrate}" self._can_protocol = CanProtocol.CAN_20 self.handle = self.can.open(connector, flags) super().__init__(channel=channel, **kwargs) def send(self, msg, timeout=None): tx = message_convert_tx(msg) if timeout: status = self.can.blocking_send(self.handle, byref(tx), int(timeout * 1000)) else: status = self.can.send(self.handle, byref(tx)) if status != CanalError.SUCCESS: raise CanOperationError("could not send message", error_code=status) def _recv_internal(self, timeout): messagerx = CanalMsg() if timeout == 0: status = self.can.receive(self.handle, byref(messagerx)) else: time = 0 if timeout is None else int(timeout * 1000) status = self.can.blocking_receive(self.handle, byref(messagerx), time) if status == CanalError.SUCCESS: rx = message_convert_rx(messagerx) elif status in ( CanalError.RCV_EMPTY, CanalError.TIMEOUT, CanalError.FIFO_EMPTY, ): rx = None else: raise CanOperationError("could not receive message", error_code=status) return rx, False def shutdown(self): """ Shuts down connection to the device safely. :raise cam.CanOperationError: is closing the connection did not work """ super().shutdown() status = self.can.close(self.handle) if status != CanalError.SUCCESS: raise CanOperationError("could not shut down bus", error_code=status) @staticmethod def _detect_available_configs(): return Usb2canBus.detect_available_configs() @staticmethod def detect_available_configs(serial_matcher: Optional[str] = None): """ Uses the *Windows Management Instrumentation* to identify serial devices. :param serial_matcher: search string for automatic detection of the device serial """ if serial_matcher is None: channels = find_serial_devices() else: channels = find_serial_devices(serial_matcher) return [{"interface": "usb2can", "channel": c} for c in channels] python-can-4.5.0/can/interfaces/usb2can/usb2canabstractionlayer.py000066400000000000000000000137241472200326600252300ustar00rootroot00000000000000""" This wrapper is for windows or direct access via CANAL API. Socket CAN is recommended under Unix/Linux systems. """ import logging from ctypes import * from enum import IntEnum import can from ...exceptions import error_check from ...typechecking import StringPathLike log = logging.getLogger("can.usb2can") # type definitions flags_t = c_ulong pConfigureStr = c_char_p handle_t = c_long timeout_t = c_ulong filter_t = c_ulong # flags mappings IS_ERROR_FRAME = 4 IS_REMOTE_FRAME = 2 IS_ID_TYPE = 1 class CanalError(IntEnum): SUCCESS = 0 BAUDRATE = 1 BUS_OFF = 2 BUS_PASSIVE = 3 BUS_WARNING = 4 CAN_ID = 5 CAN_MESSAGE = 6 CHANNEL = 7 FIFO_EMPTY = 8 FIFO_FULL = 9 FIFO_SIZE = 10 FIFO_WAIT = 11 GENERIC = 12 HARDWARE = 13 INIT_FAIL = 14 INIT_MISSING = 15 INIT_READY = 16 NOT_SUPPORTED = 17 OVERRUN = 18 RCV_EMPTY = 19 REGISTER = 20 TRM_FULL = 21 ERRFRM_STUFF = 22 ERRFRM_FORM = 23 ERRFRM_ACK = 24 ERRFRM_BIT1 = 25 ERRFRM_BIT0 = 26 ERRFRM_CRC = 27 LIBRARY = 28 PROCADDRESS = 29 ONLY_ONE_INSTANCE = 30 SUB_DRIVER = 31 TIMEOUT = 32 NOT_OPEN = 33 PARAMETER = 34 MEMORY = 35 INTERNAL = 36 COMMUNICATION = 37 class CanalStatistics(Structure): _fields_ = [ ("ReceiveFrams", c_ulong), ("TransmistFrams", c_ulong), ("ReceiveData", c_ulong), ("TransmitData", c_ulong), ("Overruns", c_ulong), ("BusWarnings", c_ulong), ("BusOff", c_ulong), ] stat = CanalStatistics class CanalStatus(Structure): _fields_ = [ ("channel_status", c_ulong), ("lasterrorcode", c_ulong), ("lasterrorsubcode", c_ulong), ("lasterrorstr", c_byte * 80), ] # data type for the CAN Message class CanalMsg(Structure): _fields_ = [ ("flags", c_ulong), ("obid", c_ulong), ("id", c_ulong), ("sizeData", c_ubyte), ("data", c_ubyte * 8), ("timestamp", c_ulong), ] class Usb2CanAbstractionLayer: """A low level wrapper around the usb2can library. Documentation: http://www.8devices.com/media/products/usb2can/downloads/CANAL_API.pdf """ def __init__(self, dll: StringPathLike = "usb2can.dll") -> None: """ :param dll: the path to the usb2can DLL to load :raises ~can.exceptions.CanInterfaceNotImplementedError: if the DLL could not be loaded """ try: self.__m_dllBasic = windll.LoadLibrary(dll) if self.__m_dllBasic is None: raise Exception("__m_dllBasic is None") except Exception as error: message = f"DLL failed to load at path: {dll}" raise can.CanInterfaceNotImplementedError(message) from error def open(self, configuration: str, flags: int): """ Opens a CAN connection using `CanalOpen()`. :param configuration: the configuration: "device_id; baudrate" :param flags: the flags to be set :returns: Valid handle for CANAL API functions on success :raises ~can.exceptions.CanInterfaceNotImplementedError: if any error occurred """ try: # we need to convert this into bytes, since the underlying DLL cannot # handle non-ASCII configuration strings config_ascii = configuration.encode("ascii", "ignore") result = self.__m_dllBasic.CanalOpen(config_ascii, flags) except Exception as ex: # catch any errors thrown by this call and re-raise raise can.CanInitializationError( f'CanalOpen() failed, configuration: "{configuration}", error: {ex}' ) from ex else: # any greater-than-zero return value indicates a success # (see https://grodansparadis.gitbooks.io/the-vscp-daemon/canal_interface_specification.html) # raise an error if the return code is <= 0 if result <= 0: raise can.CanInitializationError( f'CanalOpen() failed, configuration: "{configuration}"', error_code=result, ) else: return result def close(self, handle) -> CanalError: with error_check("Failed to close"): return CanalError(self.__m_dllBasic.CanalClose(handle)) def send(self, handle, msg) -> CanalError: with error_check("Failed to transmit frame"): return CanalError(self.__m_dllBasic.CanalSend(handle, msg)) def receive(self, handle, msg) -> CanalError: with error_check("Receive error"): return CanalError(self.__m_dllBasic.CanalReceive(handle, msg)) def blocking_send(self, handle, msg, timeout) -> CanalError: with error_check("Blocking send error"): return CanalError(self.__m_dllBasic.CanalBlockingSend(handle, msg, timeout)) def blocking_receive(self, handle, msg, timeout) -> CanalError: with error_check("Blocking Receive Failed"): return CanalError( self.__m_dllBasic.CanalBlockingReceive(handle, msg, timeout) ) def get_status(self, handle, status) -> CanalError: with error_check("Get status failed"): return CanalError(self.__m_dllBasic.CanalGetStatus(handle, status)) def get_statistics(self, handle, statistics) -> CanalError: with error_check("Get Statistics failed"): return CanalError(self.__m_dllBasic.CanalGetStatistics(handle, statistics)) def get_version(self): with error_check("Failed to get version info"): return self.__m_dllBasic.CanalGetVersion() def get_library_version(self): with error_check("Failed to get DLL version"): return self.__m_dllBasic.CanalGetDllVersion() def get_vendor_string(self): with error_check("Failed to get vendor string"): return self.__m_dllBasic.CanalGetVendorString() python-can-4.5.0/can/interfaces/vector/000077500000000000000000000000001472200326600177705ustar00rootroot00000000000000python-can-4.5.0/can/interfaces/vector/__init__.py000066400000000000000000000010641472200326600221020ustar00rootroot00000000000000""" """ __all__ = [ "VectorBus", "VectorBusParams", "VectorCanFdParams", "VectorCanParams", "VectorChannelConfig", "VectorError", "VectorInitializationError", "VectorOperationError", "canlib", "exceptions", "get_channel_configs", "xlclass", "xldefine", "xldriver", ] from .canlib import ( VectorBus, VectorBusParams, VectorCanFdParams, VectorCanParams, VectorChannelConfig, get_channel_configs, ) from .exceptions import VectorError, VectorInitializationError, VectorOperationError python-can-4.5.0/can/interfaces/vector/canlib.py000066400000000000000000001403771472200326600216060ustar00rootroot00000000000000""" Ctypes wrapper module for Vector CAN Interface on win32/win64 systems. Authors: Julien Grave , Christian Sandberg """ import contextlib import ctypes import logging import os import time import warnings from types import ModuleType from typing import ( Any, Callable, Dict, Iterator, List, NamedTuple, Optional, Sequence, Tuple, Union, cast, ) from can import ( BitTiming, BitTimingFd, BusABC, CanInitializationError, CanInterfaceNotImplementedError, CanProtocol, Message, ) from can.typechecking import AutoDetectedConfig, CanFilters from can.util import ( check_or_adjust_timing_clock, deprecated_args_alias, dlc2len, len2dlc, time_perfcounter_correlation, ) from . import xlclass, xldefine from .exceptions import VectorError, VectorInitializationError, VectorOperationError LOG = logging.getLogger(__name__) # Import safely Vector API module for Travis tests xldriver: Optional[ModuleType] = None try: from . import xldriver except FileNotFoundError as exc: LOG.warning("Could not import vxlapi: %s", exc) WaitForSingleObject: Optional[Callable[[int, int], int]] INFINITE: Optional[int] try: # Try builtin Python 3 Windows API from _winapi import ( # type: ignore[attr-defined,no-redef,unused-ignore] INFINITE, WaitForSingleObject, ) HAS_EVENTS = True except ImportError: WaitForSingleObject, INFINITE = None, None HAS_EVENTS = False class VectorBus(BusABC): """The CAN Bus implemented for the Vector interface.""" @deprecated_args_alias( deprecation_start="4.0.0", deprecation_end="5.0.0", **{ "sjwAbr": "sjw_abr", "tseg1Abr": "tseg1_abr", "tseg2Abr": "tseg2_abr", "sjwDbr": "sjw_dbr", "tseg1Dbr": "tseg1_dbr", "tseg2Dbr": "tseg2_dbr", }, ) def __init__( self, channel: Union[int, Sequence[int], str], can_filters: Optional[CanFilters] = None, poll_interval: float = 0.01, receive_own_messages: bool = False, timing: Optional[Union[BitTiming, BitTimingFd]] = None, bitrate: Optional[int] = None, rx_queue_size: int = 2**14, app_name: Optional[str] = "CANalyzer", serial: Optional[int] = None, fd: bool = False, data_bitrate: Optional[int] = None, sjw_abr: int = 2, tseg1_abr: int = 6, tseg2_abr: int = 3, sjw_dbr: int = 2, tseg1_dbr: int = 6, tseg2_dbr: int = 3, listen_only: Optional[bool] = False, **kwargs: Any, ) -> None: """ :param channel: The channel indexes to create this bus with. Can also be a single integer or a comma separated string. :param can_filters: See :class:`can.BusABC`. :param receive_own_messages: See :class:`can.BusABC`. :param timing: An instance of :class:`~can.BitTiming` or :class:`~can.BitTimingFd` to specify the bit timing parameters for the VectorBus interface. The `f_clock` value of the timing instance must be set to 8_000_000 (8MHz) or 16_000_000 (16MHz) for CAN 2.0 or 80_000_000 (80MHz) for CAN FD. If this parameter is provided, it takes precedence over all other timing-related parameters. Otherwise, the bit timing can be specified using the following parameters: `bitrate` for standard CAN or `fd`, `data_bitrate`, `sjw_abr`, `tseg1_abr`, `tseg2_abr`, `sjw_dbr`, `tseg1_dbr`, and `tseg2_dbr` for CAN FD. :param poll_interval: Poll interval in seconds. :param bitrate: Bitrate in bits/s. :param rx_queue_size: Number of messages in receive queue (power of 2). CAN: range `16…32768` CAN-FD: range `8192…524288` :param app_name: Name of application in *Vector Hardware Config*. If set to `None`, the channel should be a global channel index. :param serial: Serial number of the hardware to be used. If set, the channel parameter refers to the channels ONLY on the specified hardware. If set, the `app_name` does not have to be previously defined in *Vector Hardware Config*. :param fd: If CAN-FD frames should be supported. :param data_bitrate: Which bitrate to use for data phase in CAN FD. Defaults to arbitration bitrate. :param sjw_abr: Bus timing value sample jump width (arbitration). :param tseg1_abr: Bus timing value tseg1 (arbitration) :param tseg2_abr: Bus timing value tseg2 (arbitration) :param sjw_dbr: Bus timing value sample jump width (data) :param tseg1_dbr: Bus timing value tseg1 (data) :param tseg2_dbr: Bus timing value tseg2 (data) :param listen_only: if the bus should be set to listen only mode. :raise ~can.exceptions.CanInterfaceNotImplementedError: If the current operating system is not supported or the driver could not be loaded. :raise ~can.exceptions.CanInitializationError: If the bus could not be set up. This may or may not be a :class:`~can.interfaces.vector.VectorInitializationError`. """ self.__testing = kwargs.get("_testing", False) if os.name != "nt" and not self.__testing: raise CanInterfaceNotImplementedError( f"The Vector interface is only supported on Windows, " f'but you are running "{os.name}"' ) if xldriver is None: raise CanInterfaceNotImplementedError("The Vector API has not been loaded") self.xldriver = xldriver # keep reference so mypy knows it is not None self.xldriver.xlOpenDriver() self.poll_interval = poll_interval self.channels: Sequence[int] if isinstance(channel, int): self.channels = [channel] elif isinstance(channel, str): # must be checked before generic Sequence # Assume comma separated string of channels self.channels = [int(ch.strip()) for ch in channel.split(",")] elif isinstance(channel, Sequence): self.channels = [int(ch) for ch in channel] else: raise TypeError( f"Invalid type for parameter 'channel': {type(channel).__name__}" ) self._app_name = app_name.encode() if app_name is not None else b"" self.channel_info = "Application {}: {}".format( app_name, ", ".join(f"CAN {ch + 1}" for ch in self.channels), ) channel_configs = get_channel_configs() is_fd = isinstance(timing, BitTimingFd) if timing else fd self.mask = 0 self.channel_masks: Dict[int, int] = {} self.index_to_channel: Dict[int, int] = {} self._can_protocol = CanProtocol.CAN_FD if is_fd else CanProtocol.CAN_20 self._listen_only = listen_only for channel in self.channels: if ( len(self.channels) == 1 and (_channel_index := kwargs.get("channel_index", None)) is not None ): # VectorBus._detect_available_configs() might return multiple # devices with the same serial number, e.g. if a VN8900 is connected via both USB and Ethernet # at the same time. If the VectorBus is instantiated with a config, that was returned from # VectorBus._detect_available_configs(), then use the contained global channel_index # to avoid any ambiguities. channel_index = cast(int, _channel_index) else: channel_index = self._find_global_channel_idx( channel=channel, serial=serial, app_name=app_name, channel_configs=channel_configs, ) LOG.debug("Channel index %d found", channel) channel_mask = 1 << channel_index self.channel_masks[channel] = channel_mask self.index_to_channel[channel_index] = channel self.mask |= channel_mask permission_mask = xlclass.XLaccess() # Set mask to request channel init permission if needed if bitrate or fd or timing or self._listen_only: permission_mask.value = self.mask interface_version = ( xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4 if is_fd else xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION ) self.port_handle = xlclass.XLportHandle(xldefine.XL_INVALID_PORTHANDLE) self.xldriver.xlOpenPort( self.port_handle, self._app_name, self.mask, permission_mask, rx_queue_size, interface_version, xldefine.XL_BusTypes.XL_BUS_TYPE_CAN, ) self.permission_mask = permission_mask.value LOG.debug( "Open Port: PortHandle: %d, ChannelMask: 0x%X, PermissionMask: 0x%X", self.port_handle.value, self.mask, self.permission_mask, ) assert_timing = (bitrate or timing) and not self.__testing # set CAN settings if isinstance(timing, BitTiming): timing = check_or_adjust_timing_clock(timing, [16_000_000, 8_000_000]) self._set_bit_timing(channel_mask=self.mask, timing=timing) if assert_timing: self._check_can_settings( channel_mask=self.mask, bitrate=timing.bitrate, sample_point=timing.sample_point, ) elif isinstance(timing, BitTimingFd): timing = check_or_adjust_timing_clock(timing, [80_000_000]) self._set_bit_timing_fd(channel_mask=self.mask, timing=timing) if assert_timing: self._check_can_settings( channel_mask=self.mask, bitrate=timing.nom_bitrate, sample_point=timing.nom_sample_point, fd=True, data_bitrate=timing.data_bitrate, data_sample_point=timing.data_sample_point, ) elif fd: timing = BitTimingFd.from_bitrate_and_segments( f_clock=80_000_000, nom_bitrate=bitrate or 500_000, nom_tseg1=tseg1_abr, nom_tseg2=tseg2_abr, nom_sjw=sjw_abr, data_bitrate=data_bitrate or bitrate or 500_000, data_tseg1=tseg1_dbr, data_tseg2=tseg2_dbr, data_sjw=sjw_dbr, ) self._set_bit_timing_fd(channel_mask=self.mask, timing=timing) if assert_timing: self._check_can_settings( channel_mask=self.mask, bitrate=timing.nom_bitrate, sample_point=timing.nom_sample_point, fd=True, data_bitrate=timing.data_bitrate, data_sample_point=timing.data_sample_point, ) elif bitrate: self._set_bitrate(channel_mask=self.mask, bitrate=bitrate) if assert_timing: self._check_can_settings(channel_mask=self.mask, bitrate=bitrate) if self._listen_only: self._set_output_mode(channel_mask=self.mask, listen_only=True) # Enable/disable TX receipts tx_receipts = 1 if receive_own_messages else 0 self.xldriver.xlCanSetChannelMode(self.port_handle, self.mask, tx_receipts, 0) if HAS_EVENTS: self.event_handle = xlclass.XLhandle() self.xldriver.xlSetNotification(self.port_handle, self.event_handle, 1) else: LOG.info("Install pywin32 to avoid polling") # Calculate time offset for absolute timestamps offset = xlclass.XLuint64() try: if time.get_clock_info("time").resolution > 1e-5: ts, perfcounter = time_perfcounter_correlation() try: self.xldriver.xlGetSyncTime(self.port_handle, offset) except VectorInitializationError: self.xldriver.xlGetChannelTime(self.port_handle, self.mask, offset) current_perfcounter = time.perf_counter() now = ts + (current_perfcounter - perfcounter) self._time_offset = now - offset.value * 1e-9 else: try: self.xldriver.xlGetSyncTime(self.port_handle, offset) except VectorInitializationError: self.xldriver.xlGetChannelTime(self.port_handle, self.mask, offset) self._time_offset = time.time() - offset.value * 1e-9 except VectorInitializationError: self._time_offset = 0.0 self._is_filtered = False super().__init__( channel=channel, can_filters=can_filters, **kwargs, ) # activate channels after CAN filters were applied try: self.xldriver.xlActivateChannel( self.port_handle, self.mask, xldefine.XL_BusTypes.XL_BUS_TYPE_CAN, 0 ) except VectorOperationError as error: self.shutdown() raise VectorInitializationError.from_generic(error) from None @property def fd(self) -> bool: class_name = self.__class__.__name__ warnings.warn( f"The {class_name}.fd property is deprecated and superseded by " f"{class_name}.protocol. It is scheduled for removal in python-can version 5.0.", DeprecationWarning, stacklevel=2, ) return self._can_protocol is CanProtocol.CAN_FD def _find_global_channel_idx( self, channel: int, serial: Optional[int], app_name: Optional[str], channel_configs: List["VectorChannelConfig"], ) -> int: if serial is not None: serial_found = False for channel_config in channel_configs: if channel_config.serial_number != serial: continue serial_found = True if channel_config.hw_channel == channel: return channel_config.channel_index if not serial_found: err_msg = f"No interface with serial {serial} found." else: err_msg = ( f"Channel {channel} not found on interface with serial {serial}." ) raise CanInitializationError( err_msg, error_code=xldefine.XL_Status.XL_ERR_HW_NOT_PRESENT ) if app_name: hw_type, hw_index, hw_channel = self.get_application_config( app_name, channel ) idx = cast( int, self.xldriver.xlGetChannelIndex(hw_type, hw_index, hw_channel) ) if idx < 0: # Undocumented behavior! See issue #353. # If hardware is unavailable, this function returns -1. # Raise an exception as if the driver # would have signalled XL_ERR_HW_NOT_PRESENT. raise VectorInitializationError( xldefine.XL_Status.XL_ERR_HW_NOT_PRESENT, xldefine.XL_Status.XL_ERR_HW_NOT_PRESENT.name, "xlGetChannelIndex", ) return idx # check if channel is a valid global channel index for channel_config in channel_configs: if channel == channel_config.channel_index: return channel raise CanInitializationError( f"Channel {channel} not found. The 'channel' parameter must be " f"a valid global channel index if neither 'app_name' nor 'serial' were given.", error_code=xldefine.XL_Status.XL_ERR_HW_NOT_PRESENT, ) def _has_init_access(self, channel: int) -> bool: return bool(self.permission_mask & self.channel_masks[channel]) def _read_bus_params( self, channel_index: int, vcc_list: List["VectorChannelConfig"] ) -> "VectorBusParams": for vcc in vcc_list: if vcc.channel_index == channel_index: bus_params = vcc.bus_params if bus_params is None: # for CAN channels, this should never be `None` raise ValueError("Invalid bus parameters.") return bus_params channel = self.index_to_channel[channel_index] raise CanInitializationError( f"Channel configuration for channel {channel} not found." ) def _set_output_mode(self, channel_mask: int, listen_only: bool) -> None: # set parameters for channels with init access channel_mask = channel_mask & self.permission_mask if channel_mask: if listen_only: self.xldriver.xlCanSetChannelOutput( self.port_handle, channel_mask, xldefine.XL_OutputMode.XL_OUTPUT_MODE_SILENT, ) else: self.xldriver.xlCanSetChannelOutput( self.port_handle, channel_mask, xldefine.XL_OutputMode.XL_OUTPUT_MODE_NORMAL, ) LOG.info("xlCanSetChannelOutput: listen_only=%u", listen_only) else: LOG.warning("No channels with init access to set listen only mode") def _set_bitrate(self, channel_mask: int, bitrate: int) -> None: # set parameters for channels with init access channel_mask = channel_mask & self.permission_mask if channel_mask: self.xldriver.xlCanSetChannelBitrate( self.port_handle, channel_mask, bitrate, ) LOG.info("xlCanSetChannelBitrate: baudr.=%u", bitrate) def _set_bit_timing(self, channel_mask: int, timing: BitTiming) -> None: # set parameters for channels with init access channel_mask = channel_mask & self.permission_mask if channel_mask: if timing.f_clock == 8_000_000: self.xldriver.xlCanSetChannelParamsC200( self.port_handle, channel_mask, timing.btr0, timing.btr1, ) LOG.info( "xlCanSetChannelParamsC200: BTR0=%#02x, BTR1=%#02x", timing.btr0, timing.btr1, ) elif timing.f_clock == 16_000_000: chip_params = xlclass.XLchipParams() chip_params.bitRate = timing.bitrate chip_params.sjw = timing.sjw chip_params.tseg1 = timing.tseg1 chip_params.tseg2 = timing.tseg2 chip_params.sam = timing.nof_samples self.xldriver.xlCanSetChannelParams( self.port_handle, channel_mask, chip_params, ) LOG.info( "xlCanSetChannelParams: baudr.=%u, sjwAbr=%u, tseg1Abr=%u, tseg2Abr=%u", chip_params.bitRate, chip_params.sjw, chip_params.tseg1, chip_params.tseg2, ) else: raise CanInitializationError( f"timing.f_clock must be 8_000_000 or 16_000_000 (is {timing.f_clock})" ) def _set_bit_timing_fd( self, channel_mask: int, timing: BitTimingFd, ) -> None: # set parameters for channels with init access channel_mask = channel_mask & self.permission_mask if channel_mask: canfd_conf = xlclass.XLcanFdConf() canfd_conf.arbitrationBitRate = timing.nom_bitrate canfd_conf.sjwAbr = timing.nom_sjw canfd_conf.tseg1Abr = timing.nom_tseg1 canfd_conf.tseg2Abr = timing.nom_tseg2 canfd_conf.dataBitRate = timing.data_bitrate canfd_conf.sjwDbr = timing.data_sjw canfd_conf.tseg1Dbr = timing.data_tseg1 canfd_conf.tseg2Dbr = timing.data_tseg2 self.xldriver.xlCanFdSetConfiguration( self.port_handle, channel_mask, canfd_conf ) LOG.info( "xlCanFdSetConfiguration.: ABaudr.=%u, DBaudr.=%u", canfd_conf.arbitrationBitRate, canfd_conf.dataBitRate, ) LOG.info( "xlCanFdSetConfiguration.: sjwAbr=%u, tseg1Abr=%u, tseg2Abr=%u", canfd_conf.sjwAbr, canfd_conf.tseg1Abr, canfd_conf.tseg2Abr, ) LOG.info( "xlCanFdSetConfiguration.: sjwDbr=%u, tseg1Dbr=%u, tseg2Dbr=%u", canfd_conf.sjwDbr, canfd_conf.tseg1Dbr, canfd_conf.tseg2Dbr, ) def _check_can_settings( self, channel_mask: int, bitrate: int, sample_point: Optional[float] = None, fd: bool = False, data_bitrate: Optional[int] = None, data_sample_point: Optional[float] = None, ) -> None: """Compare requested CAN settings to active settings in driver.""" vcc_list = get_channel_configs() for channel_index in _iterate_channel_index(channel_mask): bus_params = self._read_bus_params( channel_index=channel_index, vcc_list=vcc_list ) # use bus_params.canfd even if fd==False, bus_params.can and bus_params.canfd are a C union bus_params_data = bus_params.canfd settings_acceptable = True # check bus type settings_acceptable &= ( bus_params.bus_type is xldefine.XL_BusTypes.XL_BUS_TYPE_CAN ) # check CAN operation mode # skip the check if can_op_mode is 0 # as it happens for cancaseXL, VN7600 and sometimes on other hardware (VN1640) if bus_params_data.can_op_mode: if fd: settings_acceptable &= bool( bus_params_data.can_op_mode & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CANFD ) else: settings_acceptable &= bool( bus_params_data.can_op_mode & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CAN20 ) # check bitrates if bitrate: settings_acceptable &= ( abs(bus_params_data.bitrate - bitrate) < bitrate / 256 ) if fd and data_bitrate: settings_acceptable &= ( abs(bus_params_data.data_bitrate - data_bitrate) < data_bitrate / 256 ) # check sample points if sample_point: nom_sample_point_act = ( 100 * (1 + bus_params_data.tseg1_abr) / (1 + bus_params_data.tseg1_abr + bus_params_data.tseg2_abr) ) settings_acceptable &= ( abs(nom_sample_point_act - sample_point) < 2.0 # 2 percent tolerance ) if fd and data_sample_point: data_sample_point_act = ( 100 * (1 + bus_params_data.tseg1_dbr) / (1 + bus_params_data.tseg1_dbr + bus_params_data.tseg2_dbr) ) settings_acceptable &= ( abs(data_sample_point_act - data_sample_point) < 2.0 # 2 percent tolerance ) if not settings_acceptable: # The error message depends on the currently active CAN settings. # If the active operation mode is CAN FD, show the active CAN FD timings, # otherwise show CAN 2.0 timings. if bool( bus_params_data.can_op_mode & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CANFD ): active_settings = bus_params.canfd._asdict() active_settings["can_op_mode"] = "CAN FD" else: active_settings = bus_params.can._asdict() active_settings["can_op_mode"] = "CAN 2.0" settings_string = ", ".join( [f"{key}: {val}" for key, val in active_settings.items()] ) channel = self.index_to_channel[channel_index] raise CanInitializationError( f"The requested settings could not be set for channel {channel}. " f"Another application might have set incompatible settings. " f"These are the currently active settings: {settings_string}." ) def _apply_filters(self, filters: Optional[CanFilters]) -> None: if filters: # Only up to one filter per ID type allowed if len(filters) == 1 or ( len(filters) == 2 and filters[0].get("extended") != filters[1].get("extended") ): try: for can_filter in filters: self.xldriver.xlCanSetChannelAcceptance( self.port_handle, self.mask, can_filter["can_id"], can_filter["can_mask"], ( xldefine.XL_AcceptanceFilter.XL_CAN_EXT if can_filter.get("extended") else xldefine.XL_AcceptanceFilter.XL_CAN_STD ), ) except VectorOperationError as exception: LOG.warning("Could not set filters: %s", exception) # go to fallback else: self._is_filtered = True return else: LOG.warning("Only up to one filter per extended or standard ID allowed") # go to fallback # fallback: reset filters self._is_filtered = False try: self.xldriver.xlCanSetChannelAcceptance( self.port_handle, self.mask, 0x0, 0x0, xldefine.XL_AcceptanceFilter.XL_CAN_EXT, ) self.xldriver.xlCanSetChannelAcceptance( self.port_handle, self.mask, 0x0, 0x0, xldefine.XL_AcceptanceFilter.XL_CAN_STD, ) except VectorOperationError as exc: LOG.warning("Could not reset filters: %s", exc) def _recv_internal( self, timeout: Optional[float] ) -> Tuple[Optional[Message], bool]: end_time = time.time() + timeout if timeout is not None else None while True: try: if self._can_protocol is CanProtocol.CAN_FD: msg = self._recv_canfd() else: msg = self._recv_can() except VectorOperationError as exception: if exception.error_code != xldefine.XL_Status.XL_ERR_QUEUE_IS_EMPTY: raise else: if msg: return msg, self._is_filtered # if no message was received, wait or return on timeout if end_time is not None and time.time() > end_time: return None, self._is_filtered if HAS_EVENTS: # Wait for receive event to occur if end_time is None: time_left_ms = INFINITE else: time_left = end_time - time.time() time_left_ms = max(0, int(time_left * 1000)) WaitForSingleObject(self.event_handle.value, time_left_ms) # type: ignore else: # Wait a short time until we try again time.sleep(self.poll_interval) def _recv_canfd(self) -> Optional[Message]: xl_can_rx_event = xlclass.XLcanRxEvent() self.xldriver.xlCanReceive(self.port_handle, xl_can_rx_event) if xl_can_rx_event.tag == xldefine.XL_CANFD_RX_EventTags.XL_CAN_EV_TAG_RX_OK: is_rx = True data_struct = xl_can_rx_event.tagData.canRxOkMsg elif xl_can_rx_event.tag == xldefine.XL_CANFD_RX_EventTags.XL_CAN_EV_TAG_TX_OK: is_rx = False data_struct = xl_can_rx_event.tagData.canTxOkMsg else: self.handle_canfd_event(xl_can_rx_event) return None msg_id = data_struct.canId dlc = dlc2len(data_struct.dlc) flags = data_struct.msgFlags timestamp = xl_can_rx_event.timeStamp * 1e-9 channel = self.index_to_channel.get(xl_can_rx_event.chanIndex) return Message( timestamp=timestamp + self._time_offset, arbitration_id=msg_id & 0x1FFFFFFF, is_extended_id=bool( msg_id & xldefine.XL_MessageFlagsExtended.XL_CAN_EXT_MSG_ID ), is_remote_frame=bool( flags & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_RTR ), is_error_frame=bool( flags & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_EF ), is_fd=bool(flags & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_EDL), bitrate_switch=bool( flags & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_BRS ), error_state_indicator=bool( flags & xldefine.XL_CANFD_RX_MessageFlags.XL_CAN_RXMSG_FLAG_ESI ), is_rx=is_rx, channel=channel, dlc=dlc, data=data_struct.data[:dlc], ) def _recv_can(self) -> Optional[Message]: xl_event = xlclass.XLevent() event_count = ctypes.c_uint(1) self.xldriver.xlReceive(self.port_handle, event_count, xl_event) if xl_event.tag != xldefine.XL_EventTags.XL_RECEIVE_MSG: self.handle_can_event(xl_event) return None msg_id = xl_event.tagData.msg.id dlc = xl_event.tagData.msg.dlc flags = xl_event.tagData.msg.flags timestamp = xl_event.timeStamp * 1e-9 channel = self.index_to_channel.get(xl_event.chanIndex) return Message( timestamp=timestamp + self._time_offset, arbitration_id=msg_id & 0x1FFFFFFF, is_extended_id=bool( msg_id & xldefine.XL_MessageFlagsExtended.XL_CAN_EXT_MSG_ID ), is_remote_frame=bool( flags & xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_REMOTE_FRAME ), is_error_frame=bool( flags & xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_ERROR_FRAME ), is_rx=not bool( flags & xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_TX_COMPLETED ), is_fd=False, dlc=dlc, data=xl_event.tagData.msg.data[:dlc], channel=channel, ) def handle_can_event(self, event: xlclass.XLevent) -> None: """Handle non-message CAN events. Method is called by :meth:`~can.interfaces.vector.VectorBus._recv_internal` when `event.tag` is not `XL_RECEIVE_MSG`. Subclasses can implement this method. :param event: XLevent that could have a `XL_CHIP_STATE`, `XL_TIMER` or `XL_SYNC_PULSE` tag. """ def handle_canfd_event(self, event: xlclass.XLcanRxEvent) -> None: """Handle non-message CAN FD events. Method is called by :meth:`~can.interfaces.vector.VectorBus._recv_internal` when `event.tag` is not `XL_CAN_EV_TAG_RX_OK` or `XL_CAN_EV_TAG_TX_OK`. Subclasses can implement this method. :param event: `XLcanRxEvent` that could have a `XL_CAN_EV_TAG_RX_ERROR`, `XL_CAN_EV_TAG_TX_ERROR`, `XL_TIMER` or `XL_CAN_EV_TAG_CHIP_STATE` tag. """ def send(self, msg: Message, timeout: Optional[float] = None) -> None: self._send_sequence([msg]) def _send_sequence(self, msgs: Sequence[Message]) -> int: """Send messages and return number of successful transmissions.""" if self._can_protocol is CanProtocol.CAN_FD: return self._send_can_fd_msg_sequence(msgs) else: return self._send_can_msg_sequence(msgs) def _get_tx_channel_mask(self, msgs: Sequence[Message]) -> int: if len(msgs) == 1: return self.channel_masks.get(msgs[0].channel, self.mask) # type: ignore[arg-type] else: return self.mask def _send_can_msg_sequence(self, msgs: Sequence[Message]) -> int: """Send CAN messages and return number of successful transmissions.""" mask = self._get_tx_channel_mask(msgs) message_count = ctypes.c_uint(len(msgs)) xl_event_array = (xlclass.XLevent * message_count.value)( *map(self._build_xl_event, msgs) ) self.xldriver.xlCanTransmit( self.port_handle, mask, message_count, xl_event_array ) return message_count.value @staticmethod def _build_xl_event(msg: Message) -> xlclass.XLevent: msg_id = msg.arbitration_id if msg.is_extended_id: msg_id |= xldefine.XL_MessageFlagsExtended.XL_CAN_EXT_MSG_ID flags = 0 if msg.is_remote_frame: flags |= xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_REMOTE_FRAME xl_event = xlclass.XLevent() xl_event.tag = xldefine.XL_EventTags.XL_TRANSMIT_MSG xl_event.tagData.msg.id = msg_id xl_event.tagData.msg.dlc = msg.dlc xl_event.tagData.msg.flags = flags xl_event.tagData.msg.data = tuple(msg.data) return xl_event def _send_can_fd_msg_sequence(self, msgs: Sequence[Message]) -> int: """Send CAN FD messages and return number of successful transmissions.""" mask = self._get_tx_channel_mask(msgs) message_count = len(msgs) xl_can_tx_event_array = (xlclass.XLcanTxEvent * message_count)( *map(self._build_xl_can_tx_event, msgs) ) msg_count_sent = ctypes.c_uint(0) self.xldriver.xlCanTransmitEx( self.port_handle, mask, message_count, msg_count_sent, xl_can_tx_event_array ) return msg_count_sent.value @staticmethod def _build_xl_can_tx_event(msg: Message) -> xlclass.XLcanTxEvent: msg_id = msg.arbitration_id if msg.is_extended_id: msg_id |= xldefine.XL_MessageFlagsExtended.XL_CAN_EXT_MSG_ID flags = 0 if msg.is_fd: flags |= xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_EDL if msg.bitrate_switch: flags |= xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_BRS if msg.is_remote_frame: flags |= xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_RTR xl_can_tx_event = xlclass.XLcanTxEvent() xl_can_tx_event.tag = xldefine.XL_CANFD_TX_EventTags.XL_CAN_EV_TAG_TX_MSG xl_can_tx_event.transId = 0xFFFF xl_can_tx_event.tagData.canMsg.canId = msg_id xl_can_tx_event.tagData.canMsg.msgFlags = flags xl_can_tx_event.tagData.canMsg.dlc = len2dlc(msg.dlc) xl_can_tx_event.tagData.canMsg.data = tuple(msg.data) return xl_can_tx_event def flush_tx_buffer(self) -> None: """ Flush the TX buffer of the bus. Implementation does not use function ``xlCanFlushTransmitQueue`` of the XL driver, as it works only for XL family devices. .. warning:: Using this function will flush the queue and send a high voltage message (ID = 0, DLC = 0, no data). """ if self._can_protocol is CanProtocol.CAN_FD: xl_can_tx_event = xlclass.XLcanTxEvent() xl_can_tx_event.tag = xldefine.XL_CANFD_TX_EventTags.XL_CAN_EV_TAG_TX_MSG xl_can_tx_event.tagData.canMsg.msgFlags |= ( xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_HIGHPRIO ) self.xldriver.xlCanTransmitEx( self.port_handle, self.mask, ctypes.c_uint(1), ctypes.c_uint(0), xl_can_tx_event, ) else: xl_event = xlclass.XLevent() xl_event.tag = xldefine.XL_EventTags.XL_TRANSMIT_MSG xl_event.tagData.msg.flags |= ( xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_OVERRUN | xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_WAKEUP ) self.xldriver.xlCanTransmit( self.port_handle, self.mask, ctypes.c_uint(1), xl_event ) def shutdown(self) -> None: super().shutdown() with contextlib.suppress(VectorError): self.xldriver.xlDeactivateChannel(self.port_handle, self.mask) self.xldriver.xlClosePort(self.port_handle) self.xldriver.xlCloseDriver() def reset(self) -> None: self.xldriver.xlDeactivateChannel(self.port_handle, self.mask) self.xldriver.xlActivateChannel( self.port_handle, self.mask, xldefine.XL_BusTypes.XL_BUS_TYPE_CAN, 0 ) @staticmethod def _detect_available_configs() -> List[AutoDetectedConfig]: configs = [] channel_configs = get_channel_configs() LOG.info("Found %d channels", len(channel_configs)) for channel_config in channel_configs: if ( not channel_config.channel_bus_capabilities & xldefine.XL_BusCapabilities.XL_BUS_ACTIVE_CAP_CAN ): continue LOG.info( "Channel index %d: %s", channel_config.channel_index, channel_config.name, ) configs.append( { # 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 ), "vector_channel_config": channel_config, } ) return configs # type: ignore @staticmethod def popup_vector_hw_configuration(wait_for_finish: int = 0) -> None: """Open vector hardware configuration window. :param wait_for_finish: Time to wait for user input in milliseconds. """ if xldriver is None: raise CanInterfaceNotImplementedError("The Vector API has not been loaded") xldriver.xlPopupHwConfig(ctypes.c_char_p(), ctypes.c_uint(wait_for_finish)) @staticmethod def get_application_config( app_name: str, app_channel: int ) -> Tuple[Union[int, xldefine.XL_HardwareType], int, int]: """Retrieve information for an application in Vector Hardware Configuration. :param app_name: The name of the application. :param app_channel: The channel of the application. :return: Returns a tuple of the hardware type, the hardware index and the hardware channel. :raises can.interfaces.vector.VectorInitializationError: If the application name does not exist in the Vector hardware configuration. """ if xldriver is None: raise CanInterfaceNotImplementedError("The Vector API has not been loaded") hw_type = ctypes.c_uint() hw_index = ctypes.c_uint() hw_channel = ctypes.c_uint() _app_channel = ctypes.c_uint(app_channel) try: xldriver.xlGetApplConfig( app_name.encode(), _app_channel, hw_type, hw_index, hw_channel, xldefine.XL_BusTypes.XL_BUS_TYPE_CAN, ) except VectorError as e: raise VectorInitializationError( error_code=e.error_code, error_string=( f"Vector HW Config: Channel '{app_channel}' of " f"application '{app_name}' is not assigned to any interface" ), function="xlGetApplConfig", ) from None return _hw_type(hw_type.value), hw_index.value, hw_channel.value @staticmethod def set_application_config( app_name: str, app_channel: int, hw_type: Union[int, xldefine.XL_HardwareType], hw_index: int, hw_channel: int, **kwargs: Any, ) -> None: """Modify the application settings in Vector Hardware Configuration. This method can also be used with a channel config dictionary:: import can from can.interfaces.vector import VectorBus configs = can.detect_available_configs(interfaces=['vector']) cfg = configs[0] VectorBus.set_application_config(app_name="MyApplication", app_channel=0, **cfg) :param app_name: The name of the application. Creates a new application if it does not exist yet. :param app_channel: The channel of the application. :param hw_type: The hardware type of the interface. E.g XL_HardwareType.XL_HWTYPE_VIRTUAL :param hw_index: The index of the interface if multiple interface with the same hardware type are present. :param hw_channel: The channel index of the interface. :raises can.interfaces.vector.VectorInitializationError: If the application name does not exist in the Vector hardware configuration. """ if xldriver is None: raise CanInterfaceNotImplementedError("The Vector API has not been loaded") xldriver.xlSetApplConfig( app_name.encode(), app_channel, hw_type, hw_index, hw_channel, xldefine.XL_BusTypes.XL_BUS_TYPE_CAN, ) def set_timer_rate(self, timer_rate_ms: int) -> None: """Set the cyclic event rate of the port. Once set, the port will generate a cyclic event with the tag XL_EventTags.XL_TIMER. This timer can be used to keep an application alive. See XL Driver Library Description for more information :param timer_rate_ms: The timer rate in ms. The minimal timer rate is 1ms, a value of 0 deactivates the timer events. """ timer_rate_10us = timer_rate_ms * 100 self.xldriver.xlSetTimerRate(self.port_handle, timer_rate_10us) class VectorCanParams(NamedTuple): bitrate: int sjw: int tseg1: int tseg2: int sam: int output_mode: xldefine.XL_OutputMode can_op_mode: xldefine.XL_CANFD_BusParams_CanOpMode class VectorCanFdParams(NamedTuple): bitrate: int data_bitrate: int sjw_abr: int tseg1_abr: int tseg2_abr: int sam_abr: int sjw_dbr: int tseg1_dbr: int tseg2_dbr: int output_mode: xldefine.XL_OutputMode can_op_mode: xldefine.XL_CANFD_BusParams_CanOpMode class VectorBusParams(NamedTuple): bus_type: xldefine.XL_BusTypes can: VectorCanParams canfd: VectorCanFdParams class VectorChannelConfig(NamedTuple): """NamedTuple which contains the channel properties from Vector XL API.""" name: str hw_type: Union[int, xldefine.XL_HardwareType] hw_index: int hw_channel: int channel_index: int channel_mask: int channel_capabilities: xldefine.XL_ChannelCapabilities channel_bus_capabilities: xldefine.XL_BusCapabilities is_on_bus: bool connected_bus_type: xldefine.XL_BusTypes bus_params: Optional[VectorBusParams] serial_number: int article_number: int transceiver_name: str def _get_xl_driver_config() -> xlclass.XLdriverConfig: if xldriver is None: raise VectorError( error_code=xldefine.XL_Status.XL_ERR_DLL_NOT_FOUND, error_string="xldriver is unavailable", function="_get_xl_driver_config", ) driver_config = xlclass.XLdriverConfig() xldriver.xlOpenDriver() xldriver.xlGetDriverConfig(driver_config) xldriver.xlCloseDriver() return driver_config def _read_bus_params_from_c_struct( bus_params: xlclass.XLbusParams, ) -> Optional[VectorBusParams]: bus_type = xldefine.XL_BusTypes(bus_params.busType) if bus_type is not xldefine.XL_BusTypes.XL_BUS_TYPE_CAN: return None return VectorBusParams( bus_type=bus_type, can=VectorCanParams( bitrate=bus_params.data.can.bitRate, sjw=bus_params.data.can.sjw, tseg1=bus_params.data.can.tseg1, tseg2=bus_params.data.can.tseg2, sam=bus_params.data.can.sam, output_mode=xldefine.XL_OutputMode(bus_params.data.can.outputMode), can_op_mode=xldefine.XL_CANFD_BusParams_CanOpMode( bus_params.data.can.canOpMode ), ), canfd=VectorCanFdParams( bitrate=bus_params.data.canFD.arbitrationBitRate, data_bitrate=bus_params.data.canFD.dataBitRate, sjw_abr=bus_params.data.canFD.sjwAbr, tseg1_abr=bus_params.data.canFD.tseg1Abr, tseg2_abr=bus_params.data.canFD.tseg2Abr, sam_abr=bus_params.data.canFD.samAbr, sjw_dbr=bus_params.data.canFD.sjwDbr, tseg1_dbr=bus_params.data.canFD.tseg1Dbr, tseg2_dbr=bus_params.data.canFD.tseg2Dbr, output_mode=xldefine.XL_OutputMode(bus_params.data.canFD.outputMode), can_op_mode=xldefine.XL_CANFD_BusParams_CanOpMode( bus_params.data.canFD.canOpMode ), ), ) def get_channel_configs() -> List[VectorChannelConfig]: """Read channel properties from Vector XL API.""" try: driver_config = _get_xl_driver_config() except VectorError: return [] channel_list: List[VectorChannelConfig] = [] for i in range(driver_config.channelCount): xlcc: xlclass.XLchannelConfig = driver_config.channel[i] vcc = VectorChannelConfig( name=xlcc.name.decode(), hw_type=_hw_type(xlcc.hwType), hw_index=xlcc.hwIndex, hw_channel=xlcc.hwChannel, channel_index=xlcc.channelIndex, channel_mask=xlcc.channelMask, channel_capabilities=xldefine.XL_ChannelCapabilities( xlcc.channelCapabilities ), channel_bus_capabilities=xldefine.XL_BusCapabilities( xlcc.channelBusCapabilities ), is_on_bus=bool(xlcc.isOnBus), bus_params=_read_bus_params_from_c_struct(xlcc.busParams), connected_bus_type=xldefine.XL_BusTypes(xlcc.connectedBusType), serial_number=xlcc.serialNumber, article_number=xlcc.articleNumber, transceiver_name=xlcc.transceiverName.decode(), ) channel_list.append(vcc) return channel_list def _hw_type(hw_type: int) -> Union[int, xldefine.XL_HardwareType]: try: return xldefine.XL_HardwareType(hw_type) except ValueError: LOG.warning(f'Unknown XL_HardwareType value "{hw_type}"') return hw_type def _iterate_channel_index(channel_mask: int) -> Iterator[int]: """Iterate over channel indexes in channel mask.""" for channel_index, bit in enumerate(reversed(bin(channel_mask)[2:])): if bit == "1": yield channel_index python-can-4.5.0/can/interfaces/vector/exceptions.py000066400000000000000000000016361472200326600225310ustar00rootroot00000000000000"""Exception/error declarations for the vector interface.""" from can import CanError, CanInitializationError, CanOperationError class VectorError(CanError): def __init__(self, error_code, error_string, function): super().__init__( message=f"{function} failed ({error_string})", error_code=error_code ) # keep reference to args for pickling self._args = error_code, error_string, function def __reduce__(self): return type(self), self._args, {} class VectorInitializationError(VectorError, CanInitializationError): @staticmethod def from_generic(error: VectorError) -> "VectorInitializationError": return VectorInitializationError(*error._args) class VectorOperationError(VectorError, CanOperationError): @staticmethod def from_generic(error: VectorError) -> "VectorOperationError": return VectorOperationError(*error._args) python-can-4.5.0/can/interfaces/vector/xlclass.py000066400000000000000000000204151472200326600220150ustar00rootroot00000000000000""" Definition of data types and structures for vxlapi. Authors: Julien Grave , Christian Sandberg """ # Import Standard Python Modules # ============================== import ctypes # Vector XL API Definitions # ========================= from . import xldefine XLuint64 = ctypes.c_int64 XLaccess = XLuint64 XLhandle = ctypes.c_void_p XLstatus = ctypes.c_short XLportHandle = ctypes.c_long XLeventTag = ctypes.c_ubyte XLstringType = ctypes.c_char_p # structure for XL_RECEIVE_MSG, XL_TRANSMIT_MSG class s_xl_can_msg(ctypes.Structure): _fields_ = [ ("id", ctypes.c_ulong), ("flags", ctypes.c_ushort), ("dlc", ctypes.c_ushort), ("res1", XLuint64), ("data", ctypes.c_ubyte * xldefine.MAX_MSG_LEN), ("res2", XLuint64), ] class s_xl_can_ev_error(ctypes.Structure): _fields_ = [("errorCode", ctypes.c_ubyte), ("reserved", ctypes.c_ubyte * 95)] class s_xl_chip_state(ctypes.Structure): _fields_ = [ ("busStatus", ctypes.c_ubyte), ("txErrorCounter", ctypes.c_ubyte), ("rxErrorCounter", ctypes.c_ubyte), ] class s_xl_sync_pulse(ctypes.Structure): _fields_ = [ ("pulseCode", ctypes.c_ubyte), ("time", XLuint64), ] class s_xl_can_ev_chip_state(ctypes.Structure): _fields_ = [ ("busStatus", ctypes.c_ubyte), ("txErrorCounter", ctypes.c_ubyte), ("rxErrorCounter", ctypes.c_ubyte), ("reserved", ctypes.c_ubyte), ("reserved0", ctypes.c_uint), ] class s_xl_can_ev_sync_pulse(ctypes.Structure): _fields_ = [ ("triggerSource", ctypes.c_uint), ("reserved", ctypes.c_uint), ("time", XLuint64), ] # BASIC bus message structure class s_xl_tag_data(ctypes.Union): _fields_ = [ ("msg", s_xl_can_msg), ("chipState", s_xl_chip_state), ("syncPulse", s_xl_sync_pulse), ] # CAN FD messages class s_xl_can_ev_rx_msg(ctypes.Structure): _fields_ = [ ("canId", ctypes.c_uint), ("msgFlags", ctypes.c_uint), ("crc", ctypes.c_uint), ("reserved1", ctypes.c_ubyte * 12), ("totalBitCnt", ctypes.c_ushort), ("dlc", ctypes.c_ubyte), ("reserved", ctypes.c_ubyte * 5), ("data", ctypes.c_ubyte * xldefine.XL_CAN_MAX_DATA_LEN), ] class s_xl_can_ev_tx_request(ctypes.Structure): _fields_ = [ ("canId", ctypes.c_uint), ("msgFlags", ctypes.c_uint), ("dlc", ctypes.c_ubyte), ("txAttemptConf", ctypes.c_ubyte), ("reserved", ctypes.c_ushort), ("data", ctypes.c_ubyte * xldefine.XL_CAN_MAX_DATA_LEN), ] class s_xl_can_tx_msg(ctypes.Structure): _fields_ = [ ("canId", ctypes.c_uint), ("msgFlags", ctypes.c_uint), ("dlc", ctypes.c_ubyte), ("reserved", ctypes.c_ubyte * 7), ("data", ctypes.c_ubyte * xldefine.XL_CAN_MAX_DATA_LEN), ] class s_rxTagData(ctypes.Union): _fields_ = [ ("canRxOkMsg", s_xl_can_ev_rx_msg), ("canTxOkMsg", s_xl_can_ev_rx_msg), ("canTxRequest", s_xl_can_ev_tx_request), ("canError", s_xl_can_ev_error), ("canChipState", s_xl_can_ev_chip_state), ("canSyncPulse", s_xl_can_ev_sync_pulse), ] class s_txTagData(ctypes.Union): _fields_ = [("canMsg", s_xl_can_tx_msg)] class XLevent(ctypes.Structure): _fields_ = [ ("tag", XLeventTag), ("chanIndex", ctypes.c_ubyte), ("transId", ctypes.c_ushort), ("portHandle", ctypes.c_ushort), ("flags", ctypes.c_ubyte), ("reserved", ctypes.c_ubyte), ("timeStamp", XLuint64), ("tagData", s_xl_tag_data), ] # CAN FD events class XLcanRxEvent(ctypes.Structure): _fields_ = [ ("size", ctypes.c_int), ("tag", ctypes.c_ushort), ("chanIndex", ctypes.c_ubyte), ("reserved", ctypes.c_ubyte), ("userHandle", ctypes.c_int), ("flagsChip", ctypes.c_ushort), ("reserved0", ctypes.c_ushort), ("reserved1", XLuint64), ("timeStamp", XLuint64), ("tagData", s_rxTagData), ] class XLcanTxEvent(ctypes.Structure): _fields_ = [ ("tag", ctypes.c_ushort), ("transId", ctypes.c_ushort), ("chanIndex", ctypes.c_ubyte), ("reserved", ctypes.c_ubyte * 3), ("tagData", s_txTagData), ] # CAN configuration structure class XLchipParams(ctypes.Structure): _fields_ = [ ("bitRate", ctypes.c_ulong), ("sjw", ctypes.c_ubyte), ("tseg1", ctypes.c_ubyte), ("tseg2", ctypes.c_ubyte), ("sam", ctypes.c_ubyte), ] # CAN FD configuration structure class XLcanFdConf(ctypes.Structure): _fields_ = [ ("arbitrationBitRate", ctypes.c_uint), ("sjwAbr", ctypes.c_uint), ("tseg1Abr", ctypes.c_uint), ("tseg2Abr", ctypes.c_uint), ("dataBitRate", ctypes.c_uint), ("sjwDbr", ctypes.c_uint), ("tseg1Dbr", ctypes.c_uint), ("tseg2Dbr", ctypes.c_uint), ("reserved", ctypes.c_ubyte), ("options", ctypes.c_ubyte), ("reserved1", ctypes.c_ubyte * 2), ("reserved2", ctypes.c_ubyte), ] # channel configuration structures class s_xl_bus_params_data_can(ctypes.Structure): _fields_ = [ ("bitRate", ctypes.c_uint), ("sjw", ctypes.c_ubyte), ("tseg1", ctypes.c_ubyte), ("tseg2", ctypes.c_ubyte), ("sam", ctypes.c_ubyte), ("outputMode", ctypes.c_ubyte), ("reserved", ctypes.c_ubyte * 7), ("canOpMode", ctypes.c_ubyte), ] class s_xl_bus_params_data_canfd(ctypes.Structure): _fields_ = [ ("arbitrationBitRate", ctypes.c_uint), ("sjwAbr", ctypes.c_ubyte), ("tseg1Abr", ctypes.c_ubyte), ("tseg2Abr", ctypes.c_ubyte), ("samAbr", ctypes.c_ubyte), ("outputMode", ctypes.c_ubyte), ("sjwDbr", ctypes.c_ubyte), ("tseg1Dbr", ctypes.c_ubyte), ("tseg2Dbr", ctypes.c_ubyte), ("dataBitRate", ctypes.c_uint), ("canOpMode", ctypes.c_ubyte), ] class s_xl_bus_params_data(ctypes.Union): _fields_ = [ ("can", s_xl_bus_params_data_can), ("canFD", s_xl_bus_params_data_canfd), ("most", ctypes.c_ubyte * 12), ("flexray", ctypes.c_ubyte * 12), ("ethernet", ctypes.c_ubyte * 12), ("a429", ctypes.c_ubyte * 28), ] class XLbusParams(ctypes.Structure): _fields_ = [("busType", ctypes.c_uint), ("data", s_xl_bus_params_data)] class XLchannelConfig(ctypes.Structure): _pack_ = 1 _fields_ = [ ("name", ctypes.c_char * 32), ("hwType", ctypes.c_ubyte), ("hwIndex", ctypes.c_ubyte), ("hwChannel", ctypes.c_ubyte), ("transceiverType", ctypes.c_ushort), ("transceiverState", ctypes.c_ushort), ("configError", ctypes.c_ushort), ("channelIndex", ctypes.c_ubyte), ("channelMask", XLuint64), ("channelCapabilities", ctypes.c_uint), ("channelBusCapabilities", ctypes.c_uint), ("isOnBus", ctypes.c_ubyte), ("connectedBusType", ctypes.c_uint), ("busParams", XLbusParams), ("_doNotUse", ctypes.c_uint), ("driverVersion", ctypes.c_uint), ("interfaceVersion", ctypes.c_uint), ("raw_data", ctypes.c_uint * 10), ("serialNumber", ctypes.c_uint), ("articleNumber", ctypes.c_uint), ("transceiverName", ctypes.c_char * 32), ("specialCabFlags", ctypes.c_uint), ("dominantTimeout", ctypes.c_uint), ("dominantRecessiveDelay", ctypes.c_ubyte), ("recessiveDominantDelay", ctypes.c_ubyte), ("connectionInfo", ctypes.c_ubyte), ("currentlyAvailableTimestamps", ctypes.c_ubyte), ("minimalSupplyVoltage", ctypes.c_ushort), ("maximalSupplyVoltage", ctypes.c_ushort), ("maximalBaudrate", ctypes.c_uint), ("fpgaCoreCapabilities", ctypes.c_ubyte), ("specialDeviceStatus", ctypes.c_ubyte), ("channelBusActiveCapabilities", ctypes.c_ushort), ("breakOffset", ctypes.c_ushort), ("delimiterOffset", ctypes.c_ushort), ("reserved", ctypes.c_uint * 3), ] class XLdriverConfig(ctypes.Structure): _fields_ = [ ("dllVersion", ctypes.c_uint), ("channelCount", ctypes.c_uint), ("reserved", ctypes.c_uint * 10), ("channel", XLchannelConfig * 64), ] python-can-4.5.0/can/interfaces/vector/xldefine.py000066400000000000000000000225441472200326600221470ustar00rootroot00000000000000""" Definition of constants for vxlapi. """ # Import Python Modules # ============================== from enum import IntEnum, IntFlag MAX_MSG_LEN = 8 XL_CAN_MAX_DATA_LEN = 64 XL_INVALID_PORTHANDLE = -1 class XL_AC_Flags(IntEnum): XL_ACTIVATE_NONE = 0 XL_ACTIVATE_RESET_CLOCK = 8 class XL_AcceptanceFilter(IntEnum): XL_CAN_STD = 1 XL_CAN_EXT = 2 class XL_BusCapabilities(IntFlag): XL_BUS_COMPATIBLE_CAN = 1 XL_BUS_ACTIVE_CAP_CAN = 1 << 16 XL_BUS_COMPATIBLE_LIN = 2 XL_BUS_ACTIVE_CAP_LIN = 2 << 16 XL_BUS_COMPATIBLE_FLEXRAY = 4 XL_BUS_ACTIVE_CAP_FLEXRAY = 4 << 16 XL_BUS_COMPATIBLE_MOST = 16 XL_BUS_ACTIVE_CAP_MOST = 16 << 16 XL_BUS_COMPATIBLE_DAIO = 64 XL_BUS_ACTIVE_CAP_DAIO = 64 << 16 XL_BUS_COMPATIBLE_J1708 = 256 XL_BUS_ACTIVE_CAP_J1708 = 256 << 16 XL_BUS_COMPATIBLE_KLINE = 2048 XL_BUS_ACTIVE_CAP_KLINE = 2048 << 16 XL_BUS_COMPATIBLE_ETHERNET = 4096 XL_BUS_ACTIVE_CAP_ETHERNET = 4096 << 16 XL_BUS_COMPATIBLE_A429 = 8192 XL_BUS_ACTIVE_CAP_A429 = 8192 << 16 class XL_BusStatus(IntEnum): XL_CHIPSTAT_BUSOFF = 1 XL_CHIPSTAT_ERROR_PASSIVE = 2 XL_CHIPSTAT_ERROR_WARNING = 4 XL_CHIPSTAT_ERROR_ACTIVE = 8 class XL_BusTypes(IntFlag): XL_BUS_TYPE_NONE = 0 # =0x00000000 XL_BUS_TYPE_CAN = 1 # =0x00000001 XL_BUS_TYPE_LIN = 2 # =0x00000002 XL_BUS_TYPE_FLEXRAY = 4 # =0x00000004 XL_BUS_TYPE_AFDX = 8 # =0x00000008 XL_BUS_TYPE_MOST = 16 # =0x00000010 XL_BUS_TYPE_DAIO = 64 # =0x00000040 XL_BUS_TYPE_J1708 = 256 # =0x00000100 XL_BUS_TYPE_KLINE = 2048 # =0x00000800 XL_BUS_TYPE_ETHERNET = 4096 # =0x00001000 XL_BUS_TYPE_A429 = 8192 # =0x00002000 class XL_CANFD_BusParams_CanOpMode(IntFlag): XL_BUS_PARAMS_CANOPMODE_CAN20 = 1 XL_BUS_PARAMS_CANOPMODE_CANFD = 2 XL_BUS_PARAMS_CANOPMODE_CANFD_NO_ISO = 8 class XL_CANFD_ConfigOptions(IntEnum): CANFD_CONFOPT_NO_ISO = 8 class XL_CANFD_RX_EV_ERROR_errorCode(IntEnum): XL_CAN_ERRC_BIT_ERROR = 1 XL_CAN_ERRC_FORM_ERROR = 2 XL_CAN_ERRC_STUFF_ERROR = 3 XL_CAN_ERRC_OTHER_ERROR = 4 XL_CAN_ERRC_CRC_ERROR = 5 XL_CAN_ERRC_ACK_ERROR = 6 XL_CAN_ERRC_NACK_ERROR = 7 XL_CAN_ERRC_OVLD_ERROR = 8 XL_CAN_ERRC_EXCPT_ERROR = 9 class XL_CANFD_RX_EventTags(IntEnum): XL_SYNC_PULSE = 11 XL_CAN_EV_TAG_RX_OK = 1024 XL_CAN_EV_TAG_RX_ERROR = 1025 XL_CAN_EV_TAG_TX_ERROR = 1026 XL_CAN_EV_TAG_TX_REQUEST = 1027 XL_CAN_EV_TAG_TX_OK = 1028 XL_CAN_EV_TAG_CHIP_STATE = 1033 class XL_CANFD_RX_MessageFlags(IntFlag): XL_CAN_RXMSG_FLAG_NONE = 0 XL_CAN_RXMSG_FLAG_EDL = 1 XL_CAN_RXMSG_FLAG_BRS = 2 XL_CAN_RXMSG_FLAG_ESI = 4 XL_CAN_RXMSG_FLAG_RTR = 16 XL_CAN_RXMSG_FLAG_EF = 512 XL_CAN_RXMSG_FLAG_ARB_LOST = 1024 XL_CAN_RXMSG_FLAG_WAKEUP = 8192 XL_CAN_RXMSG_FLAG_TE = 16384 class XL_CANFD_TX_EventTags(IntEnum): XL_CAN_EV_TAG_TX_MSG = 1088 # =0x0440 XL_CAN_EV_TAG_TX_ERRFR = 1089 # =0x0441 class XL_CANFD_TX_MessageFlags(IntFlag): XL_CAN_TXMSG_FLAG_NONE = 0 XL_CAN_TXMSG_FLAG_EDL = 1 XL_CAN_TXMSG_FLAG_BRS = 2 XL_CAN_TXMSG_FLAG_RTR = 16 XL_CAN_TXMSG_FLAG_HIGHPRIO = 128 XL_CAN_TXMSG_FLAG_WAKEUP = 512 class XL_ChannelCapabilities(IntFlag): XL_CHANNEL_FLAG_TIME_SYNC_RUNNING = 1 XL_CHANNEL_FLAG_NO_HWSYNC_SUPPORT = 1024 XL_CHANNEL_FLAG_SPDIF_CAPABLE = 16384 XL_CHANNEL_FLAG_CANFD_BOSCH_SUPPORT = 536870912 XL_CHANNEL_FLAG_CMACTLICENSE_SUPPORT = 1073741824 XL_CHANNEL_FLAG_CANFD_ISO_SUPPORT = 2147483648 class XL_EventFlags(IntEnum): XL_EVENT_FLAG_OVERRUN = 1 class XL_EventTags(IntEnum): XL_NO_COMMAND = 0 XL_RECEIVE_MSG = 1 XL_CHIP_STATE = 4 XL_TRANSCEIVER = 6 XL_TIMER = 8 XL_TRANSMIT_MSG = 10 XL_SYNC_PULSE = 11 XL_APPLICATION_NOTIFICATION = 15 class XL_InterfaceVersion(IntEnum): XL_INTERFACE_VERSION_V2 = 2 XL_INTERFACE_VERSION_V3 = 3 XL_INTERFACE_VERSION = XL_INTERFACE_VERSION_V3 XL_INTERFACE_VERSION_V4 = 4 class XL_MessageFlags(IntEnum): XL_CAN_MSG_FLAG_NONE = 0 XL_CAN_MSG_FLAG_ERROR_FRAME = 1 XL_CAN_MSG_FLAG_OVERRUN = 2 XL_CAN_MSG_FLAG_NERR = 4 XL_CAN_MSG_FLAG_WAKEUP = 8 XL_CAN_MSG_FLAG_REMOTE_FRAME = 16 XL_CAN_MSG_FLAG_RESERVED_1 = 32 XL_CAN_MSG_FLAG_TX_COMPLETED = 64 XL_CAN_MSG_FLAG_TX_REQUEST = 128 XL_CAN_MSG_FLAG_SRR_BIT_DOM = 512 class XL_MessageFlagsExtended(IntEnum): XL_CAN_EXT_MSG_ID = 2147483648 class XL_OutputMode(IntEnum): XL_OUTPUT_MODE_SILENT = 0 XL_OUTPUT_MODE_NORMAL = 1 XL_OUTPUT_MODE_TX_OFF = 2 XL_OUTPUT_MODE_SJA_1000_SILENT = 3 class XL_Sizes(IntEnum): XL_MAX_LENGTH = 31 XL_MAX_APPNAME = 32 XL_MAX_NAME_LENGTH = 48 XLEVENT_SIZE = 48 XL_CONFIG_MAX_CHANNELS = 64 XL_APPLCONFIG_MAX_CHANNELS = 256 class XL_Status(IntEnum): XL_SUCCESS = 0 # =0x0000 XL_PENDING = 1 # =0x0001 XL_ERR_QUEUE_IS_EMPTY = 10 # =0x000A XL_ERR_QUEUE_IS_FULL = 11 # =0x000B XL_ERR_TX_NOT_POSSIBLE = 12 # =0x000C XL_ERR_NO_LICENSE = 14 # =0x000E XL_ERR_WRONG_PARAMETER = 101 # =0x0065 XL_ERR_TWICE_REGISTER = 110 # =0x006E XL_ERR_INVALID_CHAN_INDEX = 111 # =0x006F XL_ERR_INVALID_ACCESS = 112 # =0x0070 XL_ERR_PORT_IS_OFFLINE = 113 # =0x0071 XL_ERR_CHAN_IS_ONLINE = 116 # =0x0074 XL_ERR_NOT_IMPLEMENTED = 117 # =0x0075 XL_ERR_INVALID_PORT = 118 # =0x0076 XL_ERR_HW_NOT_READY = 120 # =0x0078 XL_ERR_CMD_TIMEOUT = 121 # =0x0079 XL_ERR_CMD_HANDLING = 122 # = 0x007A XL_ERR_HW_NOT_PRESENT = 129 # =0x0081 XL_ERR_NOTIFY_ALREADY_ACTIVE = 131 # =0x0083 XL_ERR_INVALID_TAG = 132 # = 0x0084 XL_ERR_INVALID_RESERVED_FLD = 133 # = 0x0085 XL_ERR_INVALID_SIZE = 134 # = 0x0086 XL_ERR_INSUFFICIENT_BUFFER = 135 # = 0x0087 XL_ERR_ERROR_CRC = 136 # = 0x0088 XL_ERR_BAD_EXE_FORMAT = 137 # = 0x0089 XL_ERR_NO_SYSTEM_RESOURCES = 138 # = 0x008A XL_ERR_NOT_FOUND = 139 # = 0x008B XL_ERR_INVALID_ADDRESS = 140 # = 0x008C XL_ERR_REQ_NOT_ACCEP = 141 # = 0x008D XL_ERR_INVALID_LEVEL = 142 # = 0x008E XL_ERR_NO_DATA_DETECTED = 143 # = 0x008F XL_ERR_INTERNAL_ERROR = 144 # = 0x0090 XL_ERR_UNEXP_NET_ERR = 145 # = 0x0091 XL_ERR_INVALID_USER_BUFFER = 146 # = 0x0092 XL_ERR_INVALID_PORT_ACCESS_TYPE = 147 # = 0x0093 XL_ERR_NO_RESOURCES = 152 # =0x0098 XL_ERR_WRONG_CHIP_TYPE = 153 # =0x0099 XL_ERR_WRONG_COMMAND = 154 # =0x009A XL_ERR_INVALID_HANDLE = 155 # =0x009B XL_ERR_RESERVED_NOT_ZERO = 157 # =0x009D XL_ERR_INIT_ACCESS_MISSING = 158 # =0x009E XL_ERR_WRONG_VERSION = 160 # = 0x00A0 XL_ERR_CANNOT_OPEN_DRIVER = 201 # =0x00C9 XL_ERR_WRONG_BUS_TYPE = 202 # =0x00CA XL_ERR_DLL_NOT_FOUND = 203 # =0x00CB XL_ERR_INVALID_CHANNEL_MASK = 204 # =0x00CC XL_ERR_NOT_SUPPORTED = 205 # =0x00CD XL_ERR_CONNECTION_BROKEN = 210 # =0x00D2 XL_ERR_CONNECTION_CLOSED = 211 # =0x00D3 XL_ERR_INVALID_STREAM_NAME = 212 # =0x00D4 XL_ERR_CONNECTION_FAILED = 213 # =0x00D5 XL_ERR_STREAM_NOT_FOUND = 214 # =0x00D6 XL_ERR_STREAM_NOT_CONNECTED = 215 # =0x00D7 XL_ERR_QUEUE_OVERRUN = 216 # =0x00D8 XL_ERROR = 255 # =0x00FF # CAN FD Error Codes XL_ERR_INVALID_DLC = 513 # =0x0201 XL_ERR_INVALID_CANID = 514 # =0x0202 XL_ERR_INVALID_FDFLAG_MODE20 = 515 # =0x203 XL_ERR_EDL_RTR = 516 # =0x204 XL_ERR_EDL_NOT_SET = 517 # =0x205 XL_ERR_UNKNOWN_FLAG = 518 # =0x206 class XL_TimeSyncNewValue(IntEnum): XL_SET_TIMESYNC_NO_CHANGE = 0 XL_SET_TIMESYNC_ON = 1 XL_SET_TIMESYNC_OFF = 2 class XL_HardwareType(IntEnum): XL_HWTYPE_NONE = 0 XL_HWTYPE_VIRTUAL = 1 XL_HWTYPE_CANCARDX = 2 XL_HWTYPE_CANAC2PCI = 6 XL_HWTYPE_CANCARDY = 12 XL_HWTYPE_CANCARDXL = 15 XL_HWTYPE_CANCASEXL = 21 XL_HWTYPE_CANCASEXL_LOG_OBSOLETE = 23 XL_HWTYPE_CANBOARDXL = 25 XL_HWTYPE_CANBOARDXL_PXI = 27 XL_HWTYPE_VN2600 = 29 XL_HWTYPE_VN2610 = XL_HWTYPE_VN2600 XL_HWTYPE_VN3300 = 37 XL_HWTYPE_VN3600 = 39 XL_HWTYPE_VN7600 = 41 XL_HWTYPE_CANCARDXLE = 43 XL_HWTYPE_VN8900 = 45 XL_HWTYPE_VN8950 = 47 XL_HWTYPE_VN2640 = 53 XL_HWTYPE_VN1610 = 55 XL_HWTYPE_VN1630 = 57 XL_HWTYPE_VN1640 = 59 XL_HWTYPE_VN8970 = 61 XL_HWTYPE_VN1611 = 63 XL_HWTYPE_VN5240 = 64 XL_HWTYPE_VN5610 = 65 XL_HWTYPE_VN5620 = 66 XL_HWTYPE_VN7570 = 67 XL_HWTYPE_VN5650 = 68 XL_HWTYPE_IPCLIENT = 69 XL_HWTYPE_VN5611 = 70 XL_HWTYPE_IPSERVER = 71 XL_HWTYPE_VN5612 = 72 XL_HWTYPE_VX1121 = 73 XL_HWTYPE_VN5601 = 74 XL_HWTYPE_VX1131 = 75 XL_HWTYPE_VT6204 = 77 XL_HWTYPE_VN1630_LOG = 79 XL_HWTYPE_VN7610 = 81 XL_HWTYPE_VN7572 = 83 XL_HWTYPE_VN8972 = 85 XL_HWTYPE_VN0601 = 87 XL_HWTYPE_VN5640 = 89 XL_HWTYPE_VX0312 = 91 XL_HWTYPE_VH6501 = 94 XL_HWTYPE_VN8800 = 95 XL_HWTYPE_IPCL8800 = 96 XL_HWTYPE_IPSRV8800 = 97 XL_HWTYPE_CSMCAN = 98 XL_HWTYPE_VN5610A = 101 XL_HWTYPE_VN7640 = 102 XL_HWTYPE_VX1135 = 104 XL_HWTYPE_VN4610 = 105 XL_HWTYPE_VT6306 = 107 XL_HWTYPE_VT6104A = 108 XL_HWTYPE_VN5430 = 109 XL_HWTYPE_VTSSERVICE = 110 XL_HWTYPE_VN1530 = 112 XL_HWTYPE_VN1531 = 113 XL_HWTYPE_VX1161A = 114 XL_HWTYPE_VX1161B = 115 XL_HWTYPE_VGNSS = 116 XL_HWTYPE_VXLAPINIC = 118 XL_MAX_HWTYPE = 120 class XL_SyncPulseSource(IntEnum): XL_SYNC_PULSE_EXTERNAL = 0 XL_SYNC_PULSE_OUR = 1 XL_SYNC_PULSE_OUR_SHARED = 2 python-can-4.5.0/can/interfaces/vector/xldriver.py000066400000000000000000000232451472200326600222070ustar00rootroot00000000000000# type: ignore """ Ctypes wrapper module for Vector CAN Interface on win32/win64 systems. Authors: Julien Grave , Christian Sandberg """ import ctypes import logging import platform from ctypes.util import find_library from . import xlclass from .exceptions import VectorInitializationError, VectorOperationError LOG = logging.getLogger(__name__) # Load Windows DLL DLL_NAME = "vxlapi64" if platform.architecture()[0] == "64bit" else "vxlapi" if dll_path := find_library(DLL_NAME): _xlapi_dll = ctypes.windll.LoadLibrary(dll_path) else: raise FileNotFoundError(f"Vector XL library not found: {DLL_NAME}") # ctypes wrapping for API functions xlGetErrorString = _xlapi_dll.xlGetErrorString xlGetErrorString.argtypes = [xlclass.XLstatus] xlGetErrorString.restype = xlclass.XLstringType def check_status_operation(result, function, arguments): """Check the status and raise a :class:`VectorOperationError` on error.""" if result > 0: raise VectorOperationError( result, xlGetErrorString(result).decode(), function.__name__ ) return result def check_status_initialization(result, function, arguments): """Check the status and raise a :class:`VectorInitializationError` on error.""" if result > 0: raise VectorInitializationError( result, xlGetErrorString(result).decode(), function.__name__ ) return result xlGetDriverConfig = _xlapi_dll.xlGetDriverConfig xlGetDriverConfig.argtypes = [ctypes.POINTER(xlclass.XLdriverConfig)] xlGetDriverConfig.restype = xlclass.XLstatus xlGetDriverConfig.errcheck = check_status_operation xlOpenDriver = _xlapi_dll.xlOpenDriver xlOpenDriver.argtypes = [] xlOpenDriver.restype = xlclass.XLstatus xlOpenDriver.errcheck = check_status_initialization xlCloseDriver = _xlapi_dll.xlCloseDriver xlCloseDriver.argtypes = [] xlCloseDriver.restype = xlclass.XLstatus xlCloseDriver.errcheck = check_status_operation xlGetApplConfig = _xlapi_dll.xlGetApplConfig xlGetApplConfig.argtypes = [ ctypes.c_char_p, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(ctypes.c_uint), ctypes.c_uint, ] xlGetApplConfig.restype = xlclass.XLstatus xlGetApplConfig.errcheck = check_status_initialization xlSetApplConfig = _xlapi_dll.xlSetApplConfig xlSetApplConfig.argtypes = [ ctypes.c_char_p, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ] xlSetApplConfig.restype = xlclass.XLstatus xlSetApplConfig.errcheck = check_status_initialization xlGetChannelIndex = _xlapi_dll.xlGetChannelIndex xlGetChannelIndex.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int] xlGetChannelIndex.restype = ctypes.c_int xlGetChannelMask = _xlapi_dll.xlGetChannelMask xlGetChannelMask.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int] xlGetChannelMask.restype = xlclass.XLaccess xlOpenPort = _xlapi_dll.xlOpenPort xlOpenPort.argtypes = [ ctypes.POINTER(xlclass.XLportHandle), ctypes.c_char_p, xlclass.XLaccess, ctypes.POINTER(xlclass.XLaccess), ctypes.c_uint, ctypes.c_uint, ctypes.c_uint, ] xlOpenPort.restype = xlclass.XLstatus xlOpenPort.errcheck = check_status_initialization xlGetSyncTime = _xlapi_dll.xlGetSyncTime xlGetSyncTime.argtypes = [xlclass.XLportHandle, ctypes.POINTER(xlclass.XLuint64)] xlGetSyncTime.restype = xlclass.XLstatus xlGetSyncTime.errcheck = check_status_initialization xlGetChannelTime = _xlapi_dll.xlGetChannelTime xlGetChannelTime.argtypes = [ xlclass.XLportHandle, xlclass.XLaccess, ctypes.POINTER(xlclass.XLuint64), ] xlGetChannelTime.restype = xlclass.XLstatus xlGetChannelTime.errcheck = check_status_initialization xlClosePort = _xlapi_dll.xlClosePort xlClosePort.argtypes = [xlclass.XLportHandle] xlClosePort.restype = xlclass.XLstatus xlClosePort.errcheck = check_status_operation xlSetNotification = _xlapi_dll.xlSetNotification xlSetNotification.argtypes = [ xlclass.XLportHandle, ctypes.POINTER(xlclass.XLhandle), ctypes.c_int, ] xlSetNotification.restype = xlclass.XLstatus xlSetNotification.errcheck = check_status_initialization xlCanSetChannelMode = _xlapi_dll.xlCanSetChannelMode xlCanSetChannelMode.argtypes = [ xlclass.XLportHandle, xlclass.XLaccess, ctypes.c_int, ctypes.c_int, ] xlCanSetChannelMode.restype = xlclass.XLstatus xlCanSetChannelMode.errcheck = check_status_initialization xlActivateChannel = _xlapi_dll.xlActivateChannel xlActivateChannel.argtypes = [ xlclass.XLportHandle, xlclass.XLaccess, ctypes.c_uint, ctypes.c_uint, ] xlActivateChannel.restype = xlclass.XLstatus xlActivateChannel.errcheck = check_status_operation xlDeactivateChannel = _xlapi_dll.xlDeactivateChannel xlDeactivateChannel.argtypes = [xlclass.XLportHandle, xlclass.XLaccess] xlDeactivateChannel.restype = xlclass.XLstatus xlDeactivateChannel.errcheck = check_status_operation xlCanFdSetConfiguration = _xlapi_dll.xlCanFdSetConfiguration xlCanFdSetConfiguration.argtypes = [ xlclass.XLportHandle, xlclass.XLaccess, ctypes.POINTER(xlclass.XLcanFdConf), ] xlCanFdSetConfiguration.restype = xlclass.XLstatus xlCanFdSetConfiguration.errcheck = check_status_initialization xlReceive = _xlapi_dll.xlReceive xlReceive.argtypes = [ xlclass.XLportHandle, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(xlclass.XLevent), ] xlReceive.restype = xlclass.XLstatus xlReceive.errcheck = check_status_operation xlCanReceive = _xlapi_dll.xlCanReceive xlCanReceive.argtypes = [xlclass.XLportHandle, ctypes.POINTER(xlclass.XLcanRxEvent)] xlCanReceive.restype = xlclass.XLstatus xlCanReceive.errcheck = check_status_operation xlCanSetChannelBitrate = _xlapi_dll.xlCanSetChannelBitrate xlCanSetChannelBitrate.argtypes = [ xlclass.XLportHandle, xlclass.XLaccess, ctypes.c_ulong, ] xlCanSetChannelBitrate.restype = xlclass.XLstatus xlCanSetChannelBitrate.errcheck = check_status_initialization xlCanSetChannelParams = _xlapi_dll.xlCanSetChannelParams xlCanSetChannelParams.argtypes = [ xlclass.XLportHandle, xlclass.XLaccess, ctypes.POINTER(xlclass.XLchipParams), ] xlCanSetChannelParams.restype = xlclass.XLstatus xlCanSetChannelParams.errcheck = check_status_initialization xlCanSetChannelParamsC200 = _xlapi_dll.xlCanSetChannelParamsC200 xlCanSetChannelParamsC200.argtypes = [ xlclass.XLportHandle, xlclass.XLaccess, ctypes.c_ubyte, ctypes.c_ubyte, ] xlCanSetChannelParams.restype = xlclass.XLstatus xlCanSetChannelParams.errcheck = check_status_initialization xlCanTransmit = _xlapi_dll.xlCanTransmit xlCanTransmit.argtypes = [ xlclass.XLportHandle, xlclass.XLaccess, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(xlclass.XLevent), ] xlCanTransmit.restype = xlclass.XLstatus xlCanTransmit.errcheck = check_status_operation xlCanTransmitEx = _xlapi_dll.xlCanTransmitEx xlCanTransmitEx.argtypes = [ xlclass.XLportHandle, xlclass.XLaccess, ctypes.c_uint, ctypes.POINTER(ctypes.c_uint), ctypes.POINTER(xlclass.XLcanTxEvent), ] xlCanTransmitEx.restype = xlclass.XLstatus xlCanTransmitEx.errcheck = check_status_operation xlCanFlushTransmitQueue = _xlapi_dll.xlCanFlushTransmitQueue xlCanFlushTransmitQueue.argtypes = [xlclass.XLportHandle, xlclass.XLaccess] xlCanFlushTransmitQueue.restype = xlclass.XLstatus xlCanFlushTransmitQueue.errcheck = check_status_operation xlCanSetChannelAcceptance = _xlapi_dll.xlCanSetChannelAcceptance xlCanSetChannelAcceptance.argtypes = [ xlclass.XLportHandle, xlclass.XLaccess, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_uint, ] xlCanSetChannelAcceptance.restype = xlclass.XLstatus xlCanSetChannelAcceptance.errcheck = check_status_operation xlCanResetAcceptance = _xlapi_dll.xlCanResetAcceptance xlCanResetAcceptance.argtypes = [xlclass.XLportHandle, xlclass.XLaccess, ctypes.c_uint] xlCanResetAcceptance.restype = xlclass.XLstatus xlCanResetAcceptance.errcheck = check_status_operation xlCanRequestChipState = _xlapi_dll.xlCanRequestChipState xlCanRequestChipState.argtypes = [xlclass.XLportHandle, xlclass.XLaccess] xlCanRequestChipState.restype = xlclass.XLstatus xlCanRequestChipState.errcheck = check_status_operation xlCanSetChannelOutput = _xlapi_dll.xlCanSetChannelOutput xlCanSetChannelOutput.argtypes = [xlclass.XLportHandle, xlclass.XLaccess, ctypes.c_char] xlCanSetChannelOutput.restype = xlclass.XLstatus xlCanSetChannelOutput.errcheck = check_status_operation xlPopupHwConfig = _xlapi_dll.xlPopupHwConfig xlPopupHwConfig.argtypes = [ctypes.c_char_p, ctypes.c_uint] xlPopupHwConfig.restype = xlclass.XLstatus xlPopupHwConfig.errcheck = check_status_operation xlSetTimerRate = _xlapi_dll.xlSetTimerRate xlSetTimerRate.argtypes = [xlclass.XLportHandle, ctypes.c_ulong] xlSetTimerRate.restype = xlclass.XLstatus xlSetTimerRate.errcheck = check_status_operation xlGetEventString = _xlapi_dll.xlGetEventString xlGetEventString.argtypes = [ctypes.POINTER(xlclass.XLevent)] xlGetEventString.restype = xlclass.XLstringType xlCanGetEventString = _xlapi_dll.xlCanGetEventString xlCanGetEventString.argtypes = [ctypes.POINTER(xlclass.XLcanRxEvent)] xlCanGetEventString.restype = xlclass.XLstringType xlGetReceiveQueueLevel = _xlapi_dll.xlGetReceiveQueueLevel xlGetReceiveQueueLevel.argtypes = [xlclass.XLportHandle, ctypes.POINTER(ctypes.c_int)] xlGetReceiveQueueLevel.restype = xlclass.XLstatus xlGetReceiveQueueLevel.errcheck = check_status_operation xlGenerateSyncPulse = _xlapi_dll.xlGenerateSyncPulse xlGenerateSyncPulse.argtypes = [xlclass.XLportHandle, xlclass.XLaccess] xlGenerateSyncPulse.restype = xlclass.XLstatus xlGenerateSyncPulse.errcheck = check_status_operation xlFlushReceiveQueue = _xlapi_dll.xlFlushReceiveQueue xlFlushReceiveQueue.argtypes = [xlclass.XLportHandle] xlFlushReceiveQueue.restype = xlclass.XLstatus xlFlushReceiveQueue.errcheck = check_status_operation python-can-4.5.0/can/interfaces/virtual.py000066400000000000000000000160751472200326600205370ustar00rootroot00000000000000""" This module implements an OS and hardware independent virtual CAN interface for testing purposes. Any VirtualBus instances connecting to the same channel and reside in the same process will receive the same messages. """ import logging import queue 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 can import CanOperationError from can.bus import BusABC, CanProtocol from can.message import Message from can.typechecking import AutoDetectedConfig 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() class VirtualBus(BusABC): """ A virtual CAN bus using an internal message queue. It can be used for example for testing. In this interface, a channel is an arbitrary object used as an identifier for connected buses. Implements :meth:`can.BusABC._detect_available_configs`; see :meth:`_detect_available_configs` for how it behaves here. .. note:: The timeout when sending a message applies to each receiver individually. This means that sending can block up to 5 seconds if a message is sent to 5 receivers with the timeout set to 1.0. .. warning:: This interface guarantees reliable delivery and message ordering, but does *not* implement rate limiting or ID arbitration/prioritization under high loads. Please refer to the section :ref:`virtual_interfaces_doc` for more information on this and a comparison to alternatives. """ def __init__( self, channel: Any = None, receive_own_messages: bool = False, rx_queue_size: int = 0, preserve_timestamps: bool = False, protocol: CanProtocol = CanProtocol.CAN_20, **kwargs: Any, ) -> None: """ The constructed instance has access to the bus identified by the channel parameter. It is able to see all messages transmitted on the bus by virtual instances constructed with the same channel identifier. :param channel: The channel identifier. This parameter can be an arbitrary value. The bus instance will be able to see messages from other virtual bus instances that were created with the same value. :param receive_own_messages: If set to True, sent messages will be reflected back on the input queue. :param rx_queue_size: The size of the reception queue. The reception queue stores messages until they are read. If the queue reaches its capacity, it will start dropping the oldest messages to make room for new ones. If set to 0, the queue has an infinite capacity. Be aware that this can cause memory leaks if messages are read with a lower frequency than they arrive on the bus. :param preserve_timestamps: If set to True, messages transmitted via :func:`~can.BusABC.send` will keep the timestamp set in the :class:`~can.Message` instance. Otherwise, the timestamp value will be replaced with the current system time. :param protocol: The protocol implemented by this bus instance. The value does not affect the operation of the bus instance and can be set to an arbitrary value for testing purposes. :param kwargs: Additional keyword arguments passed to the parent constructor. """ super().__init__( channel=channel, receive_own_messages=receive_own_messages, **kwargs, ) # the channel identifier may be an arbitrary object self.channel_id = channel self._can_protocol = protocol self.channel_info = f"Virtual bus channel {self.channel_id}" self.receive_own_messages = receive_own_messages self.preserve_timestamps = preserve_timestamps self._open = True with channels_lock: # Create a new channel if one does not exist if self.channel_id not in channels: channels[self.channel_id] = [] self.channel = channels[self.channel_id] self.queue: queue.Queue[Message] = queue.Queue(rx_queue_size) self.channel.append(self.queue) def _check_if_open(self) -> None: """Raises :exc:`~can.exceptions.CanOperationError` if the bus is not open. Has to be called in every method that accesses the bus. """ if not self._open: raise CanOperationError("Cannot operate on a closed bus") def _recv_internal( self, timeout: Optional[float] ) -> Tuple[Optional[Message], bool]: self._check_if_open() try: msg = self.queue.get(block=True, timeout=timeout) except queue.Empty: return None, False else: return msg, False def send(self, msg: Message, timeout: Optional[float] = None) -> None: self._check_if_open() timestamp = msg.timestamp if self.preserve_timestamps else time.time() # Add message to all listening on this channel all_sent = True for bus_queue in self.channel: if bus_queue is self.queue and not self.receive_own_messages: continue msg_copy = deepcopy(msg) msg_copy.timestamp = timestamp msg_copy.channel = self.channel_id msg_copy.is_rx = bus_queue is not self.queue try: bus_queue.put(msg_copy, block=True, timeout=timeout) except queue.Full: all_sent = False if not all_sent: raise CanOperationError("Could not send message to one or more recipients") def shutdown(self) -> None: super().shutdown() if self._open: self._open = False with channels_lock: self.channel.remove(self.queue) # remove if empty if not self.channel: del channels[self.channel_id] @staticmethod def _detect_available_configs() -> List[AutoDetectedConfig]: """ Returns all currently used channels as well as one other currently unused channel. .. note:: This method will run into problems if thousands of autodetected buses are used at once. """ with channels_lock: available_channels = list(channels.keys()) # find a currently unused channel def get_extra(): return f"channel-{randint(0, 9999)}" extra = get_extra() while extra in available_channels: extra = get_extra() available_channels += [extra] return [ {"interface": "virtual", "channel": channel} for channel in available_channels ] python-can-4.5.0/can/io/000077500000000000000000000000001472200326600147525ustar00rootroot00000000000000python-can-4.5.0/can/io/__init__.py000066400000000000000000000023331472200326600170640ustar00rootroot00000000000000""" Read and write CAN bus messages using a range of Readers and Writers based off the file extension. """ __all__ = [ "ASCReader", "ASCWriter", "BaseRotatingLogger", "BLFReader", "BLFWriter", "CanutilsLogReader", "CanutilsLogWriter", "CSVReader", "CSVWriter", "Logger", "LogReader", "MESSAGE_READERS", "MESSAGE_WRITERS", "MessageSync", "MF4Reader", "MF4Writer", "Printer", "SizedRotatingLogger", "SqliteReader", "SqliteWriter", "TRCFileVersion", "TRCReader", "TRCWriter", "asc", "blf", "canutils", "csv", "generic", "logger", "mf4", "player", "printer", "sqlite", "trc", ] # Generic from .logger import MESSAGE_WRITERS, BaseRotatingLogger, Logger, SizedRotatingLogger from .player import MESSAGE_READERS, LogReader, MessageSync # isort: split # Format specific from .asc import ASCReader, ASCWriter from .blf import BLFReader, BLFWriter from .canutils import CanutilsLogReader, CanutilsLogWriter from .csv import CSVReader, CSVWriter from .mf4 import MF4Reader, MF4Writer from .printer import Printer from .sqlite import SqliteReader, SqliteWriter from .trc import TRCFileVersion, TRCReader, TRCWriter python-can-4.5.0/can/io/asc.py000066400000000000000000000421031472200326600160720ustar00rootroot00000000000000""" Contains handling of ASC logging files. Example .asc files: - https://bitbucket.org/tobylorenz/vector_asc/src/master/src/Vector/ASC/tests/unittests/data/ - under `test/data/logfile.asc` """ import logging import re from datetime import datetime from typing import Any, Dict, Final, Generator, Optional, TextIO, Union from ..message import Message from ..typechecking import StringPathLike from ..util import channel2int, dlc2len, len2dlc from .generic import TextIOMessageReader, TextIOMessageWriter CAN_MSG_EXT = 0x80000000 CAN_ID_MASK = 0x1FFFFFFF BASE_HEX = 16 BASE_DEC = 10 ASC_TRIGGER_REGEX: Final = re.compile( r"begin\s+triggerblock\s+\w+\s+(?P.+)", re.IGNORECASE ) ASC_MESSAGE_REGEX: Final = re.compile( r"\d+\.\d+\s+(\d+\s+(\w+\s+(Tx|Rx)|ErrorFrame)|CANFD)", re.ASCII | re.IGNORECASE, ) logger = logging.getLogger("can.io.asc") class ASCReader(TextIOMessageReader): """ Iterator of CAN messages from a ASC logging file. Meta data (comments, bus statistics, J1939 Transport Protocol messages) is ignored. """ file: TextIO def __init__( self, file: Union[StringPathLike, TextIO], base: str = "hex", relative_timestamp: bool = True, **kwargs: Any, ) -> None: """ :param file: a path-like object or as file-like object to read from If this is a file-like object, is has to opened in text read mode, not binary read mode. :param base: Select the base(hex or dec) of id and data. If the header of the asc file contains base information, this value will be overwritten. Default "hex". :param relative_timestamp: Select whether the timestamps are `relative` (starting at 0.0) or `absolute` (starting at the system time). Default `True = relative`. """ super().__init__(file, mode="r") if not self.file: raise ValueError("The given file cannot be None") self.base = base self._converted_base = self._check_base(base) self.relative_timestamp = relative_timestamp self.date: Optional[str] = None self.start_time = 0.0 # TODO - what is this used for? The ASC Writer only prints `absolute` self.timestamps_format: Optional[str] = None self.internal_events_logged = False def _extract_header(self) -> None: for _line in self.file: line = _line.strip() datetime_match = re.match( r"date\s+\w+\s+(?P.+)", line, re.IGNORECASE ) base_match = re.match( r"base\s+(?Phex|dec)(?:\s+timestamps\s+" r"(?Pabsolute|relative))?", line, re.IGNORECASE, ) comment_match = re.match(r"//.*", line) events_match = re.match( r"(?Pno)?\s*internal\s+events\s+logged", line, re.IGNORECASE ) if datetime_match: self.date = datetime_match.group("datetime_string") self.start_time = ( 0.0 if self.relative_timestamp else self._datetime_to_timestamp(self.date) ) continue if base_match: base = base_match.group("base") timestamp_format = base_match.group("timestamp_format") self.base = base self._converted_base = self._check_base(self.base) self.timestamps_format = timestamp_format or "absolute" continue if comment_match: continue if events_match: self.internal_events_logged = events_match.group("no_events") is None break break @staticmethod def _datetime_to_timestamp(datetime_string: str) -> float: # ugly locale independent solution month_map = { "Jan": 1, "Feb": 2, "Mar": 3, "Apr": 4, "May": 5, "Jun": 6, "Jul": 7, "Aug": 8, "Sep": 9, "Oct": 10, "Nov": 11, "Dec": 12, "Mär": 3, "Mai": 5, "Okt": 10, "Dez": 12, } for name, number in month_map.items(): datetime_string = datetime_string.replace(name, str(number).zfill(2)) datetime_formats = ( "%m %d %I:%M:%S.%f %p %Y", "%m %d %I:%M:%S %p %Y", "%m %d %H:%M:%S.%f %Y", "%m %d %H:%M:%S %Y", ) for format_str in datetime_formats: try: return datetime.strptime(datetime_string, format_str).timestamp() except ValueError: continue raise ValueError(f"Incompatible datetime string {datetime_string}") def _extract_can_id(self, str_can_id: str, msg_kwargs: Dict[str, Any]) -> None: if str_can_id[-1:].lower() == "x": msg_kwargs["is_extended_id"] = True can_id = int(str_can_id[0:-1], self._converted_base) else: msg_kwargs["is_extended_id"] = False can_id = int(str_can_id, self._converted_base) msg_kwargs["arbitration_id"] = can_id @staticmethod def _check_base(base: str) -> int: if base not in ["hex", "dec"]: raise ValueError('base should be either "hex" or "dec"') return BASE_DEC if base == "dec" else BASE_HEX def _process_data_string( self, data_str: str, data_length: int, msg_kwargs: Dict[str, Any] ) -> None: frame = bytearray() data = data_str.split() for byte in data[:data_length]: frame.append(int(byte, self._converted_base)) msg_kwargs["data"] = frame def _process_classic_can_frame( self, line: str, msg_kwargs: Dict[str, Any] ) -> Message: # CAN error frame if line.strip()[0:10].lower() == "errorframe": # Error Frame msg_kwargs["is_error_frame"] = True else: abr_id_str, direction, rest_of_message = line.split(None, 2) msg_kwargs["is_rx"] = direction == "Rx" self._extract_can_id(abr_id_str, msg_kwargs) if rest_of_message[0].lower() == "r": # CAN Remote Frame msg_kwargs["is_remote_frame"] = True remote_data = rest_of_message.split() if len(remote_data) > 1: dlc_str = remote_data[1] if dlc_str.isdigit(): msg_kwargs["dlc"] = int(dlc_str, self._converted_base) else: # Classic CAN Message try: # There is data after DLC _, dlc_str, data = rest_of_message.split(None, 2) except ValueError: # No data after DLC _, dlc_str = rest_of_message.split(None, 1) data = "" dlc = dlc2len(int(dlc_str, self._converted_base)) msg_kwargs["dlc"] = dlc self._process_data_string(data, min(8, dlc), msg_kwargs) return Message(**msg_kwargs) def _process_fd_can_frame(self, line: str, msg_kwargs: Dict[str, Any]) -> Message: channel, direction, rest_of_message = line.split(None, 2) # See ASCWriter msg_kwargs["channel"] = int(channel) - 1 msg_kwargs["is_rx"] = direction == "Rx" # CAN FD error frame if rest_of_message.strip()[:10].lower() == "errorframe": # Error Frame # TODO: maybe use regex to parse BRS, ESI, etc? msg_kwargs["is_error_frame"] = True else: can_id_str, frame_name_or_brs, rest_of_message = rest_of_message.split( None, 2 ) if frame_name_or_brs.isdigit(): brs = frame_name_or_brs esi, dlc_str, data_length_str, data = rest_of_message.split(None, 3) else: brs, esi, dlc_str, data_length_str, data = rest_of_message.split( None, 4 ) self._extract_can_id(can_id_str, msg_kwargs) msg_kwargs["bitrate_switch"] = brs == "1" msg_kwargs["error_state_indicator"] = esi == "1" dlc = int(dlc_str, self._converted_base) data_length = int(data_length_str) if data_length == 0: # CAN remote Frame msg_kwargs["is_remote_frame"] = True msg_kwargs["dlc"] = dlc else: if dlc2len(dlc) != data_length: logger.warning( "DLC vs Data Length mismatch %d[%d] != %d", dlc, dlc2len(dlc), data_length, ) msg_kwargs["dlc"] = data_length self._process_data_string(data, data_length, msg_kwargs) return Message(**msg_kwargs) def __iter__(self) -> Generator[Message, None, None]: self._extract_header() for _line in self.file: line = _line.strip() if trigger_match := ASC_TRIGGER_REGEX.match(line): datetime_str = trigger_match.group("datetime_string") self.start_time = ( 0.0 if self.relative_timestamp else self._datetime_to_timestamp(datetime_str) ) continue # Handle the "Start of measurement" line if re.match(r"^\d+\.\d+\s+Start of measurement", line): # Skip this line as it's just an indicator continue if not ASC_MESSAGE_REGEX.match(line): # line might be a comment, chip status, # J1939 message or some other unsupported event continue msg_kwargs: Dict[str, Union[float, bool, int]] = {} try: _timestamp, channel, rest_of_message = line.split(None, 2) timestamp = float(_timestamp) + self.start_time msg_kwargs["timestamp"] = timestamp if channel == "CANFD": msg_kwargs["is_fd"] = True elif channel.isdigit(): # See ASCWriter msg_kwargs["channel"] = int(channel) - 1 else: # Not a CAN message. Possible values include "statistic", J1939TP continue except ValueError: # Some other unprocessed or unknown format continue if "is_fd" not in msg_kwargs: msg = self._process_classic_can_frame(rest_of_message, msg_kwargs) else: msg = self._process_fd_can_frame(rest_of_message, msg_kwargs) if msg is not None: yield msg self.stop() class ASCWriter(TextIOMessageWriter): """Logs CAN data to an ASCII log file (.asc). The measurement starts with the timestamp of the first registered message. If a message has a timestamp smaller than the previous one or None, it gets assigned the timestamp that was written for the last message. 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( [ "CANFD", "{channel:>3}", "{dir:<4}", "{id:>8} {symbolic_name:>32}", "{brs}", "{esi}", "{dlc:x}", "{data_length:>2}", "{data}", "{message_duration:>8}", "{message_length:>4}", "{flags:>8X}", "{crc:>8}", "{bit_timing_conf_arb:>8}", "{bit_timing_conf_data:>8}", "{bit_timing_conf_ext_arb:>8}", "{bit_timing_conf_ext_data:>8}", ] ) FORMAT_DATE = "%a %b %d %H:%M:%S.{} %Y" FORMAT_EVENT = "{timestamp: 9.6f} {message}\n" def __init__( self, file: Union[StringPathLike, TextIO], channel: int = 1, **kwargs: Any, ) -> None: """ :param file: a path-like object or as file-like object to write to If this is a file-like object, is has to opened in text write mode, not binary write mode. :param channel: a default channel to use when the message does not have a channel set """ if kwargs.get("append", False): raise ValueError( f"{self.__class__.__name__} is currently not equipped to " f"append messages to an existing file." ) super().__init__(file, mode="w") self.channel = channel # write start of file header start_time = self._format_header_datetime(datetime.now()) self.file.write(f"date {start_time}\n") self.file.write("base hex timestamps absolute\n") self.file.write("internal events logged\n") # the last part is written with the timestamp of the first message self.header_written = False self.last_timestamp = 0.0 self.started = 0.0 def _format_header_datetime(self, dt: datetime) -> str: # Note: CANoe requires that the microsecond field only have 3 digits # Since Python strftime only supports microsecond formatters, we must # manually include the millisecond portion before passing the format # to strftime msec = dt.microsecond // 1000 % 1000 format_w_msec = self.FORMAT_DATE.format(msec) return dt.strftime(format_w_msec) def stop(self) -> None: # This is guaranteed to not be None since we raise ValueError in __init__ if not self.file.closed: self.file.write("End TriggerBlock\n") super().stop() def log_event(self, message: str, timestamp: Optional[float] = None) -> None: """Add a message to the log file. :param message: an arbitrary message :param timestamp: the absolute timestamp of the event """ if not message: # if empty or None logger.debug("ASCWriter: ignoring empty message") return # this is the case for the very first message: if not self.header_written: self.started = self.last_timestamp = timestamp or 0.0 start_time = datetime.fromtimestamp(self.last_timestamp) formatted_date = self._format_header_datetime(start_time) self.file.write(f"Begin Triggerblock {formatted_date}\n") self.header_written = True self.log_event("Start of measurement") # caution: this is a recursive call! # Use last known timestamp if unknown if timestamp is None: timestamp = self.last_timestamp # turn into relative timestamps if necessary if timestamp >= self.started: timestamp -= self.started line = self.FORMAT_EVENT.format(timestamp=timestamp, message=message) self.file.write(line) def on_message_received(self, msg: Message) -> None: channel = channel2int(msg.channel) if channel is None: channel = self.channel else: # Many interfaces start channel numbering at 0 which is invalid channel += 1 if msg.is_error_frame: self.log_event(f"{channel} ErrorFrame", msg.timestamp) return if msg.is_remote_frame: dtype = f"r {msg.dlc:x}" # New after v8.5 data: str = "" else: dtype = f"d {msg.dlc:x}" data = msg.data.hex(" ").upper() arb_id = f"{msg.arbitration_id:X}" if msg.is_extended_id: arb_id += "x" if msg.is_fd: flags = 0 flags |= 1 << 12 if msg.bitrate_switch: flags |= 1 << 13 if msg.error_state_indicator: flags |= 1 << 14 serialized = self.FORMAT_MESSAGE_FD.format( channel=channel, id=arb_id, dir="Rx" if msg.is_rx else "Tx", symbolic_name="", brs=1 if msg.bitrate_switch else 0, esi=1 if msg.error_state_indicator else 0, dlc=len2dlc(msg.dlc), data_length=len(msg.data), data=data, message_duration=0, message_length=0, flags=flags, crc=0, bit_timing_conf_arb=0, bit_timing_conf_data=0, bit_timing_conf_ext_arb=0, bit_timing_conf_ext_data=0, ) else: serialized = self.FORMAT_MESSAGE.format( channel=channel, id=arb_id, dir="Rx" if msg.is_rx else "Tx", dtype=dtype, data=data, ) self.log_event(serialized, msg.timestamp) python-can-4.5.0/can/io/blf.py000066400000000000000000000526551472200326600161040ustar00rootroot00000000000000""" Implements support for BLF (Binary Logging Format) which is a proprietary CAN log format from Vector Informatik GmbH (Germany). No official specification of the binary logging format is available. This implementation is based on Toby Lorenz' C++ library "Vector BLF" which is licensed under GPLv3. https://bitbucket.org/tobylorenz/vector_blf. The file starts with a header. The rest is one or more "log containers" which consists of a header and some zlib compressed data, usually up to 128 kB of uncompressed data each. This data contains the actual CAN messages and other objects types. """ import datetime import logging import struct import time import zlib from typing import Any, BinaryIO, Generator, List, Optional, Tuple, Union, cast from ..message import Message from ..typechecking import StringPathLike from ..util import channel2int, dlc2len, len2dlc from .generic import BinaryIOMessageReader, FileIOMessageWriter TSystemTime = Tuple[int, int, int, int, int, int, int, int] class BLFParseError(Exception): """BLF file could not be parsed correctly.""" LOG = logging.getLogger(__name__) # signature ("LOGG"), header size, # application ID, application major, application minor, application build, # bin log major, bin log minor, bin log build, bin log patch, # file size, uncompressed size, count of objects, count of objects read, # time start (SYSTEMTIME), time stop (SYSTEMTIME) FILE_HEADER_STRUCT = struct.Struct("<4sLBBBBBBBBQQLL8H8H") # Pad file header to this size FILE_HEADER_SIZE = 144 # signature ("LOBJ"), header size, header version, object size, object type OBJ_HEADER_BASE_STRUCT = struct.Struct("<4sHHLL") # flags, client index, object version, timestamp OBJ_HEADER_V1_STRUCT = struct.Struct(" TSystemTime: if timestamp is None or timestamp < 631152000: # Probably not a Unix timestamp return 0, 0, 0, 0, 0, 0, 0, 0 t = datetime.datetime.fromtimestamp(round(timestamp, 3)) return ( t.year, t.month, t.isoweekday() % 7, t.day, t.hour, t.minute, t.second, t.microsecond // 1000, ) def systemtime_to_timestamp(systemtime: TSystemTime) -> float: try: t = datetime.datetime( systemtime[0], systemtime[1], systemtime[3], systemtime[4], systemtime[5], systemtime[6], systemtime[7] * 1000, ) return t.timestamp() except ValueError: return 0 class BLFReader(BinaryIOMessageReader): """ Iterator of CAN messages from a Binary Logging File. Only CAN messages and error frames are supported. Other object types are silently ignored. """ file: BinaryIO def __init__( self, file: Union[StringPathLike, BinaryIO], **kwargs: Any, ) -> None: """ :param file: a path-like object or as file-like object to read from If this is a file-like object, is has to opened in binary read mode, not text read mode. """ super().__init__(file, mode="rb") data = self.file.read(FILE_HEADER_STRUCT.size) header = FILE_HEADER_STRUCT.unpack(data) if header[0] != b"LOGG": raise BLFParseError("Unexpected file format") self.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])) # Read rest of header self.file.read(header[1] - FILE_HEADER_STRUCT.size) self._tail = b"" self._pos = 0 def __iter__(self) -> Generator[Message, None, None]: while True: data = self.file.read(OBJ_HEADER_BASE_STRUCT.size) if not data: # EOF break signature, _, _, obj_size, obj_type = OBJ_HEADER_BASE_STRUCT.unpack(data) if signature != b"LOBJ": raise BLFParseError() obj_data = self.file.read(obj_size - OBJ_HEADER_BASE_STRUCT.size) # Read padding bytes self.file.read(obj_size % 4) if obj_type == LOG_CONTAINER: method, _ = LOG_CONTAINER_STRUCT.unpack_from(obj_data) container_data = obj_data[LOG_CONTAINER_STRUCT.size :] if method == NO_COMPRESSION: data = container_data elif method == ZLIB_DEFLATE: zobj = zlib.decompressobj() data = zobj.decompress(container_data) else: # Unknown compression method LOG.warning("Unknown compression method (%d)", method) continue yield from self._parse_container(data) self.stop() def _parse_container(self, data): if self._tail: data = b"".join((self._tail, data)) try: yield from self._parse_data(data) except struct.error: # There was not enough data in the container to unpack a struct pass # Save the remaining data that could not be processed self._tail = data[self._pos :] def _parse_data(self, data): """Optimized inner loop by making local copies of global variables and class members and hardcoding some values.""" unpack_obj_header_base = OBJ_HEADER_BASE_STRUCT.unpack_from obj_header_base_size = OBJ_HEADER_BASE_STRUCT.size unpack_obj_header_v1 = OBJ_HEADER_V1_STRUCT.unpack_from obj_header_v1_size = OBJ_HEADER_V1_STRUCT.size unpack_obj_header_v2 = OBJ_HEADER_V2_STRUCT.unpack_from obj_header_v2_size = OBJ_HEADER_V2_STRUCT.size unpack_can_msg = CAN_MSG_STRUCT.unpack_from unpack_can_fd_msg = CAN_FD_MSG_STRUCT.unpack_from unpack_can_fd_64_msg = CAN_FD_MSG_64_STRUCT.unpack_from can_fd_64_msg_size = CAN_FD_MSG_64_STRUCT.size unpack_can_error_ext = CAN_ERROR_EXT_STRUCT.unpack_from start_timestamp = self.start_timestamp max_pos = len(data) pos = 0 # Loop until a struct unpack raises an exception while True: self._pos = pos # Find next object after padding (depends on object type) try: pos = data.index(b"LOBJ", pos, pos + 8) except ValueError: if pos + 8 > max_pos: # Not enough data in container return raise BLFParseError("Could not find next object") from None header = unpack_obj_header_base(data, pos) # print(header) signature, _, header_version, obj_size, obj_type = header if signature != b"LOBJ": raise BLFParseError() # Calculate position of next object next_pos = pos + obj_size if next_pos > max_pos: # This object continues in the next container return pos += obj_header_base_size # Read rest of header if header_version == 1: flags, _, _, timestamp = unpack_obj_header_v1(data, pos) pos += obj_header_v1_size elif header_version == 2: flags, _, _, timestamp = unpack_obj_header_v2(data, pos) pos += obj_header_v2_size else: LOG.warning("Unknown object header version (%d)", header_version) pos = next_pos continue # Calculate absolute timestamp in seconds factor = 1e-5 if flags == 1 else 1e-9 timestamp = timestamp * factor + start_timestamp if obj_type in (CAN_MESSAGE, CAN_MESSAGE2): channel, flags, dlc, can_id, can_data = unpack_can_msg(data, pos) yield Message( timestamp=timestamp, arbitration_id=can_id & 0x1FFFFFFF, is_extended_id=bool(can_id & CAN_MSG_EXT), is_remote_frame=bool(flags & REMOTE_FLAG), is_rx=not bool(flags & DIR), dlc=dlc, data=can_data[:dlc], channel=channel - 1, ) elif obj_type == CAN_ERROR_EXT: members = unpack_can_error_ext(data, pos) channel = members[0] dlc = members[5] can_id = members[7] can_data = members[9] yield Message( timestamp=timestamp, is_error_frame=True, is_extended_id=bool(can_id & CAN_MSG_EXT), arbitration_id=can_id & 0x1FFFFFFF, dlc=dlc, data=can_data[:dlc], channel=channel - 1, ) elif obj_type == CAN_FD_MESSAGE: members = unpack_can_fd_msg(data, pos) ( channel, flags, dlc, can_id, _, _, fd_flags, valid_bytes, can_data, ) = members yield Message( timestamp=timestamp, arbitration_id=can_id & 0x1FFFFFFF, is_extended_id=bool(can_id & CAN_MSG_EXT), is_remote_frame=bool(flags & REMOTE_FLAG), is_fd=bool(fd_flags & 0x1), is_rx=not bool(flags & DIR), bitrate_switch=bool(fd_flags & 0x2), error_state_indicator=bool(fd_flags & 0x4), dlc=dlc2len(dlc), data=can_data[:valid_bytes], channel=channel - 1, ) elif obj_type == CAN_FD_MESSAGE_64: ( channel, dlc, valid_bytes, _, can_id, _, fd_flags, _, _, _, _, _, direction, _, _, ) = unpack_can_fd_64_msg(data, pos) pos += can_fd_64_msg_size yield Message( timestamp=timestamp, arbitration_id=can_id & 0x1FFFFFFF, is_extended_id=bool(can_id & CAN_MSG_EXT), is_remote_frame=bool(fd_flags & 0x0010), is_fd=bool(fd_flags & 0x1000), is_rx=not direction, bitrate_switch=bool(fd_flags & 0x2000), error_state_indicator=bool(fd_flags & 0x4000), dlc=dlc2len(dlc), data=data[pos : pos + valid_bytes], channel=channel - 1, ) pos = next_pos class BLFWriter(FileIOMessageWriter): """ 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 #: Application identifier for the log writer application_id = 5 def __init__( self, file: Union[StringPathLike, BinaryIO], append: bool = False, channel: int = 1, compression_level: int = -1, **kwargs: Any, ) -> None: """ :param file: a path-like object or as file-like object to write to If this is a file-like object, is has to opened in mode "wb+". :param channel: Default channel to log as if not specified by the interface. :param append: Append messages to an existing log file. :param compression_level: An integer from 0 to 9 or -1 controlling the level of compression. 1 (Z_BEST_SPEED) is fastest and produces the least compression. 9 (Z_BEST_COMPRESSION) is slowest and produces the most. 0 means that data will be stored without processing. The default value is -1 (Z_DEFAULT_COMPRESSION). Z_DEFAULT_COMPRESSION represents a default compromise between speed and compression (currently equivalent to level 6). """ mode = "rb+" if append else "wb" try: super().__init__(file, mode=mode) except FileNotFoundError: # Trying to append to a non-existing file, create a new one append = False mode = "wb" super().__init__(file, mode=mode) assert self.file is not None self.channel = channel self.compression_level = compression_level self._buffer: List[bytes] = [] self._buffer_size = 0 # If max container size is located in kwargs, then update the instance if kwargs.get("max_container_size", False): self.max_container_size = kwargs["max_container_size"] if append: # Parse file header data = self.file.read(FILE_HEADER_STRUCT.size) header = FILE_HEADER_STRUCT.unpack(data) if header[0] != b"LOGG": raise BLFParseError("Unexpected file format") self.uncompressed_size = header[11] self.object_count = header[12] self.start_timestamp: Optional[float] = systemtime_to_timestamp( cast(TSystemTime, header[14:22]) ) self.stop_timestamp: Optional[float] = systemtime_to_timestamp( cast(TSystemTime, header[22:30]) ) # Jump to the end of the file self.file.seek(0, 2) else: self.object_count = 0 self.uncompressed_size = FILE_HEADER_SIZE self.start_timestamp = None self.stop_timestamp = None # Write a default header which will be updated when stopped self._write_header(FILE_HEADER_SIZE) def _write_header(self, filesize): header = [b"LOGG", FILE_HEADER_SIZE, self.application_id, 0, 0, 0, 2, 6, 8, 1] # The meaning of "count of objects read" is unknown header.extend([filesize, self.uncompressed_size, self.object_count, 0]) header.extend(timestamp_to_systemtime(self.start_timestamp)) header.extend(timestamp_to_systemtime(self.stop_timestamp)) self.file.write(FILE_HEADER_STRUCT.pack(*header)) # Pad to header size self.file.write(b"\x00" * (FILE_HEADER_SIZE - FILE_HEADER_STRUCT.size)) def on_message_received(self, msg): channel = channel2int(msg.channel) if channel is None: channel = self.channel else: # Many interfaces start channel numbering at 0 which is invalid channel += 1 arb_id = msg.arbitration_id if msg.is_extended_id: arb_id |= CAN_MSG_EXT flags = REMOTE_FLAG if msg.is_remote_frame else 0 if not msg.is_rx: flags |= DIR can_data = bytes(msg.data) if msg.is_error_frame: data = CAN_ERROR_EXT_STRUCT.pack( channel, 0, # length 0, # flags 0, # ecc 0, # position len2dlc(msg.dlc), 0, # frame length arb_id, 0, # ext flags can_data, ) self._add_object(CAN_ERROR_EXT, data, msg.timestamp) elif msg.is_fd: fd_flags = EDL if msg.bitrate_switch: fd_flags |= BRS if msg.error_state_indicator: fd_flags |= ESI data = CAN_FD_MSG_STRUCT.pack( channel, flags, len2dlc(msg.dlc), arb_id, 0, 0, fd_flags, len(can_data), can_data, ) self._add_object(CAN_FD_MESSAGE, data, msg.timestamp) else: data = CAN_MSG_STRUCT.pack(channel, flags, msg.dlc, arb_id, can_data) self._add_object(CAN_MESSAGE, data, msg.timestamp) def log_event(self, text, timestamp=None): """Add an arbitrary message to the log file as a global marker. :param str text: The group name of the marker. :param float timestamp: Absolute timestamp in Unix timestamp format. If not given, the marker will be placed along the last message. """ try: # Only works on Windows text = text.encode("mbcs") except LookupError: text = text.encode("ascii") comment = b"Added by python-can" marker = b"python-can" data = GLOBAL_MARKER_STRUCT.pack( 0, 0xFFFFFF, 0xFF3300, 0, len(text), len(marker), len(comment) ) self._add_object(GLOBAL_MARKER, data + text + marker + comment, timestamp) def _add_object(self, obj_type, data, timestamp=None): if timestamp is None: timestamp = self.stop_timestamp or time.time() if self.start_timestamp is None: self.start_timestamp = timestamp self.stop_timestamp = timestamp timestamp = int((timestamp - self.start_timestamp) * 1e9) header_size = OBJ_HEADER_BASE_STRUCT.size + OBJ_HEADER_V1_STRUCT.size obj_size = header_size + len(data) base_header = OBJ_HEADER_BASE_STRUCT.pack( b"LOBJ", header_size, 1, obj_size, obj_type ) obj_header = OBJ_HEADER_V1_STRUCT.pack(TIME_ONE_NANS, 0, 0, max(timestamp, 0)) self._buffer.append(base_header) self._buffer.append(obj_header) self._buffer.append(data) padding_size = len(data) % 4 if padding_size: self._buffer.append(b"\x00" * padding_size) self._buffer_size += obj_size + padding_size self.object_count += 1 if self._buffer_size >= self.max_container_size: self._flush() def _flush(self): """Compresses and writes data in the buffer to file.""" if self.file.closed: return buffer = b"".join(self._buffer) if not buffer: # Nothing to write return uncompressed_data = memoryview(buffer)[: self.max_container_size] # Save data that comes after max size to next container tail = buffer[self.max_container_size :] self._buffer = [tail] self._buffer_size = len(tail) if not self.compression_level: data = uncompressed_data method = NO_COMPRESSION else: data = zlib.compress(uncompressed_data, self.compression_level) method = ZLIB_DEFLATE obj_size = OBJ_HEADER_BASE_STRUCT.size + LOG_CONTAINER_STRUCT.size + len(data) base_header = OBJ_HEADER_BASE_STRUCT.pack( b"LOBJ", OBJ_HEADER_BASE_STRUCT.size, 1, obj_size, LOG_CONTAINER ) container_header = LOG_CONTAINER_STRUCT.pack(method, len(uncompressed_data)) self.file.write(base_header) self.file.write(container_header) self.file.write(data) # Write padding bytes self.file.write(b"\x00" * (obj_size % 4)) self.uncompressed_size += OBJ_HEADER_BASE_STRUCT.size self.uncompressed_size += LOG_CONTAINER_STRUCT.size self.uncompressed_size += len(uncompressed_data) def file_size(self) -> int: """Return an estimate of the current file size in bytes.""" return self.file.tell() + self._buffer_size def stop(self): """Stops logging and closes the file.""" self._flush() if self.file.seekable(): filesize = self.file.tell() # Write header in the beginning of the file self.file.seek(0) self._write_header(filesize) else: LOG.error("Could not write BLF header since file is not seekable") super().stop() python-can-4.5.0/can/io/canutils.py000066400000000000000000000142511472200326600171510ustar00rootroot00000000000000""" This module works with CAN data in ASCII log files (*.log). It is is compatible with "candump -L" from the canutils program (https://github.com/linux-can/can-utils). """ import logging from typing import Any, Generator, TextIO, Union from can.message import Message from ..typechecking import StringPathLike from .generic import TextIOMessageReader, TextIOMessageWriter log = logging.getLogger("can.io.canutils") CAN_MSG_EXT = 0x80000000 CAN_ERR_FLAG = 0x20000000 CAN_ERR_BUSERROR = 0x00000080 CAN_ERR_DLC = 8 CANFD_BRS = 0x01 CANFD_ESI = 0x02 class CanutilsLogReader(TextIOMessageReader): """ Iterator over CAN messages from a .log Logging File (candump -L). .. note:: .log-format looks for example like this: ``(0.0) vcan0 001#8d00100100820100`` """ file: TextIO def __init__( self, file: Union[StringPathLike, TextIO], **kwargs: Any, ) -> None: """ :param file: a path-like object or as file-like object to read from If this is a file-like object, is has to opened in text read mode, not binary read mode. """ super().__init__(file, mode="r") def __iter__(self) -> Generator[Message, None, None]: for line in self.file: # skip empty lines temp = line.strip() if not temp: continue channel_string: str if temp[-2:].lower() in (" r", " t"): timestamp_string, channel_string, frame, is_rx_string = temp.split() is_rx = is_rx_string.strip().lower() == "r" else: timestamp_string, channel_string, frame = temp.split() is_rx = True timestamp = float(timestamp_string[1:-1]) can_id_string, data = frame.split("#", maxsplit=1) channel: Union[int, str] if channel_string.isdigit(): channel = int(channel_string) else: channel = channel_string is_extended = len(can_id_string) > 3 can_id = int(can_id_string, 16) is_fd = False brs = False esi = False if data and data[0] == "#": is_fd = True fd_flags = int(data[1]) brs = bool(fd_flags & CANFD_BRS) esi = bool(fd_flags & CANFD_ESI) data = data[2:] if data and data[0].lower() == "r": is_remote_frame = True if len(data) > 1: dlc = int(data[1:]) else: dlc = 0 data_bin = None else: is_remote_frame = False dlc = len(data) // 2 data_bin = bytearray() for i in range(0, len(data), 2): data_bin.append(int(data[i : (i + 2)], 16)) if can_id & CAN_ERR_FLAG and can_id & CAN_ERR_BUSERROR: msg = Message(timestamp=timestamp, is_error_frame=True) else: msg = Message( timestamp=timestamp, arbitration_id=can_id & 0x1FFFFFFF, is_extended_id=is_extended, is_remote_frame=is_remote_frame, is_fd=is_fd, is_rx=is_rx, bitrate_switch=brs, error_state_indicator=esi, dlc=dlc, data=data_bin, channel=channel, ) yield msg self.stop() class CanutilsLogWriter(TextIOMessageWriter): """Logs CAN data to an ASCII log file (.log). This class is is compatible with "candump -L". If a message has a timestamp smaller than the previous one (or 0 or None), it gets assigned the timestamp that was written for the last message. It the first message does not have a timestamp, it is set to zero. """ def __init__( self, file: Union[StringPathLike, TextIO], channel: str = "vcan0", append: bool = False, **kwargs: Any, ): """ :param file: a path-like object or as file-like object to write to If this is a file-like object, is has to opened in text write mode, not binary write mode. :param channel: a default channel to use when the message does not have a channel set :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) self.channel = channel self.last_timestamp = None def on_message_received(self, msg): # this is the case for the very first message: if self.last_timestamp is None: self.last_timestamp = msg.timestamp or 0.0 # figure out the correct timestamp if msg.timestamp is None or msg.timestamp < self.last_timestamp: timestamp = self.last_timestamp else: timestamp = msg.timestamp channel = msg.channel if msg.channel is not None else self.channel if isinstance(channel, int) or isinstance(channel, str) and channel.isdigit(): channel = f"can{channel}" framestr = f"({timestamp:f}) {channel}" if msg.is_error_frame: framestr += f" {CAN_ERR_FLAG | CAN_ERR_BUSERROR:08X}#" elif msg.is_extended_id: framestr += f" {msg.arbitration_id:08X}#" else: framestr += f" {msg.arbitration_id:03X}#" if msg.is_error_frame: eol = "\n" else: eol = " R\n" if msg.is_rx else " T\n" if msg.is_remote_frame: framestr += f"R{eol}" else: if msg.is_fd: fd_flags = 0 if msg.bitrate_switch: fd_flags |= CANFD_BRS if msg.error_state_indicator: fd_flags |= CANFD_ESI framestr += f"#{fd_flags:X}" framestr += f"{msg.data.hex().upper()}{eol}" self.file.write(framestr) python-can-4.5.0/can/io/csv.py000066400000000000000000000104401472200326600161160ustar00rootroot00000000000000""" This module contains handling for CSV (comma separated values) files. TODO: CAN FD messages are not yet supported. TODO: This module could use https://docs.python.org/2/library/csv.html#module-csv to allow different delimiters for writing, special escape chars to circumvent the base64 encoding and use csv.Sniffer to automatically deduce the delimiters of a CSV file. """ from base64 import b64decode, b64encode from typing import Any, Generator, TextIO, Union from can.message import Message from ..typechecking import StringPathLike from .generic import TextIOMessageReader, TextIOMessageWriter class CSVReader(TextIOMessageReader): """Iterator over CAN messages from a .csv file that was generated by :class:`~can.CSVWriter` or that uses the same format as described there. Assumes that there is a header and thus skips the first line. Any line separator is accepted. """ file: TextIO def __init__( self, file: Union[StringPathLike, TextIO], **kwargs: Any, ) -> None: """ :param file: a path-like object or as file-like object to read from If this is a file-like object, is has to opened in text read mode, not binary read mode. """ super().__init__(file, mode="r") def __iter__(self) -> Generator[Message, None, None]: # skip the header line try: next(self.file) except StopIteration: # don't crash on a file with only a header return for line in self.file: timestamp, arbitration_id, extended, remote, error, dlc, data = line.split( "," ) yield Message( timestamp=float(timestamp), is_remote_frame=(remote == "1"), is_extended_id=(extended == "1"), is_error_frame=(error == "1"), arbitration_id=int(arbitration_id, base=16), dlc=int(dlc), data=b64decode(data), ) self.stop() class CSVWriter(TextIOMessageWriter): """Writes a comma separated text file with a line for each message. Includes a header line. The columns are as follows: ================ ======================= =============== name of column format description example ================ ======================= =============== timestamp decimal float 1483389946.197 arbitration_id hex 0x00dadada extended 1 == True, 0 == False 1 remote 1 == True, 0 == False 0 error 1 == True, 0 == False 0 dlc int 6 data base64 encoded WzQyLCA5XQ== ================ ======================= =============== Each line is terminated with a platform specific line separator. """ file: TextIO def __init__( self, file: Union[StringPathLike, TextIO], append: bool = False, **kwargs: Any, ) -> None: """ :param file: a path-like object or a file-like object to write to. If this is a file-like object, is has to open in text write mode, not binary write mode. :param bool append: if set to `True` messages are appended to the file and no header line is written, else the file is truncated and starts with a newly written header line """ mode = "a" if append else "w" super().__init__(file, mode=mode) # Write a header row if not append: self.file.write("timestamp,arbitration_id,extended,remote,error,dlc,data\n") def on_message_received(self, msg: Message) -> None: row = ",".join( [ repr(msg.timestamp), # cannot use str() here because that is rounding hex(msg.arbitration_id), "1" if msg.is_extended_id else "0", "1" if msg.is_remote_frame else "0", "1" if msg.is_error_frame else "0", str(msg.dlc), b64encode(msg.data).decode("utf8"), ] ) self.file.write(row) self.file.write("\n") python-can-4.5.0/can/io/generic.py000066400000000000000000000072321472200326600167440ustar00rootroot00000000000000"""Contains generic base classes for file IO.""" import gzip import locale from abc import ABCMeta from types import TracebackType from typing import ( Any, BinaryIO, ContextManager, Iterable, Literal, Optional, TextIO, Type, Union, cast, ) from typing_extensions import Self from .. import typechecking from ..listener import Listener from ..message import Message class BaseIOHandler(ContextManager, metaclass=ABCMeta): """A generic file handler that can be used for reading and writing. Can be used as a context manager. :attr file: the file-like object that is kept internally, or `None` if none was opened """ file: Optional[typechecking.FileLike] 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), ) # for multiple inheritance super().__init__() def __enter__(self) -> Self: return self def __exit__( self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: self.stop() return False 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() class MessageWriter(BaseIOHandler, Listener, metaclass=ABCMeta): """The base class for all writers.""" file: Optional[typechecking.FileLike] class FileIOMessageWriter(MessageWriter, metaclass=ABCMeta): """A specialized base class for all writers with file descriptors.""" file: typechecking.FileLike def __init__( self, file: typechecking.AcceptedIOType, mode: str = "wt", **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") 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 TextIOMessageWriter(FileIOMessageWriter, metaclass=ABCMeta): file: TextIO class BinaryIOMessageWriter(FileIOMessageWriter, metaclass=ABCMeta): file: Union[BinaryIO, gzip.GzipFile] class MessageReader(BaseIOHandler, Iterable[Message], metaclass=ABCMeta): """The base class for all readers.""" class TextIOMessageReader(MessageReader, metaclass=ABCMeta): file: TextIO class BinaryIOMessageReader(MessageReader, metaclass=ABCMeta): file: Union[BinaryIO, gzip.GzipFile] python-can-4.5.0/can/io/logger.py000066400000000000000000000325201472200326600166050ustar00rootroot00000000000000""" See the :class:`Logger` class. """ import gzip import os import pathlib from abc import ABC, abstractmethod from datetime import datetime from types import TracebackType 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 .asc import ASCWriter from .blf import BLFWriter from .canutils import CanutilsLogWriter from .csv import CSVWriter from .generic import ( BinaryIOMessageWriter, FileIOMessageWriter, MessageWriter, ) from .mf4 import MF4Writer from .printer import Printer from .sqlite import SqliteWriter from .trc import TRCWriter #: A map of file suffixes to their corresponding #: :class:`can.io.generic.MessageWriter` class MESSAGE_WRITERS: Final[Dict[str, Type[MessageWriter]]] = { ".asc": ASCWriter, ".blf": BLFWriter, ".csv": CSVWriter, ".db": SqliteWriter, ".log": CanutilsLogWriter, ".mf4": MF4Writer, ".trc": TRCWriter, ".txt": Printer, } def _update_writer_plugins() -> None: """Update available message writer plugins from entry points.""" for entry_point in read_entry_points("can.io.message_writer"): if entry_point.key in MESSAGE_WRITERS: continue writer_class = entry_point.load() if issubclass(writer_class, MessageWriter): MESSAGE_WRITERS[entry_point.key] = writer_class def _get_logger_for_suffix(suffix: str) -> Type[MessageWriter]: try: return MESSAGE_WRITERS[suffix] except KeyError: raise ValueError( f'No write support for unknown log format "{suffix}"' ) from None def _compress( filename: StringPathLike, **kwargs: Any ) -> Tuple[Type[MessageWriter], FileLike]: """ Return the suffix and io object of the decompressed file. File will automatically recompress upon close. """ suffixes = pathlib.Path(filename).suffixes if len(suffixes) != 2: raise ValueError( f"No write support for unknown log format \"{''.join(suffixes)}\"" ) from None real_suffix = suffixes[-2].lower() if real_suffix in (".blf", ".db"): raise ValueError( f"The file type {real_suffix} is currently incompatible with gzip." ) logger_type = _get_logger_for_suffix(real_suffix) append = kwargs.get("append", False) if issubclass(logger_type, BinaryIOMessageWriter): mode = "ab" if append else "wb" else: mode = "at" if append else "wt" return logger_type, gzip.open(filename, mode) def Logger( # noqa: N802 filename: Optional[StringPathLike], **kwargs: Any ) -> MessageWriter: """Find and return the appropriate :class:`~can.io.generic.MessageWriter` instance for a given file suffix. The format is determined from the file suffix which can be one of: * .asc :class:`can.ASCWriter` * .blf :class:`can.BLFWriter` * .csv: :class:`can.CSVWriter` * .db :class:`can.SqliteWriter` * .log :class:`can.CanutilsLogWriter` * .mf4 :class:`can.MF4Writer` (optional, depends on `asammdf `_) * .trc :class:`can.TRCWriter` * .txt :class:`can.Printer` Any of these formats can be used with gzip compression by appending the suffix .gz (e.g. filename.asc.gz). However, third-party tools might not be able to read these files. The **filename** may also be *None*, to fall back to :class:`can.Printer`. The log files may be incomplete until `stop()` is called due to buffering. :param filename: the filename/path of the file to write to, may be a path-like object or None to instantiate a :class:`~can.Printer` :raises ValueError: if the filename's suffix is of an unknown file type .. note:: This function itself is just a dispatcher, and any positional and keyword arguments are passed on to the returned instance. """ if filename is None: return Printer(**kwargs) _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) class BaseRotatingLogger(MessageWriter, ABC): """ Base class for rotating CAN loggers. This class is not meant to be instantiated directly. Subclasses must implement the :meth:`should_rollover` and :meth:`do_rollover` methods according to their rotation strategy. The rotation behavior can be further customized by the user by setting the :attr:`namer` and :attr:`rotator` attributes after instantiating the subclass. These attributes as well as the methods :meth:`rotation_filename` and :meth:`rotate` and the corresponding docstrings are carried over from the python builtin :class:`~logging.handlers.BaseRotatingHandler`. Subclasses must set the `_writer` attribute upon initialization. """ _supported_formats: ClassVar[Set[str]] = set() #: If this attribute is set to a callable, the :meth:`~BaseRotatingLogger.rotation_filename` #: method delegates to this callable. The parameters passed to the callable are #: those passed to :meth:`~BaseRotatingLogger.rotation_filename`. namer: Optional[Callable[[StringPathLike], StringPathLike]] = None #: If this attribute is set to a callable, the :meth:`~BaseRotatingLogger.rotate` method #: delegates to this callable. The parameters passed to the callable are those #: passed to :meth:`~BaseRotatingLogger.rotate`. rotator: Optional[Callable[[StringPathLike, StringPathLike], None]] = None #: An integer counter to track the number of rollovers. 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: """This attribute holds an instance of a writer class which manages the actual file IO.""" raise NotImplementedError def rotation_filename(self, default_name: StringPathLike) -> StringPathLike: """Modify the filename of a log file when rotating. This is provided so that a custom filename can be provided. The default implementation calls the :attr:`namer` attribute of the handler, if it's callable, passing the default name to it. If the attribute isn't callable (the default is :obj:`None`), the name is returned unchanged. :param default_name: The default name for the log file. """ if not callable(self.namer): return default_name return self.namer(default_name) # pylint: disable=not-callable def rotate(self, source: StringPathLike, dest: StringPathLike) -> None: """When rotating, rotate the current log. The default implementation calls the :attr:`rotator` attribute of the handler, if it's callable, passing the `source` and `dest` arguments to it. If the attribute isn't callable (the default is :obj:`None`), the source is simply renamed to the destination. :param source: The source filename. This is normally the base filename, e.g. `"test.log"` :param dest: The destination filename. This is normally what the source is rotated to, e.g. `"test_#001.log"`. """ if not callable(self.rotator): if os.path.exists(source): os.rename(source, dest) else: self.rotator(source, dest) # pylint: disable=not-callable def on_message_received(self, msg: Message) -> None: """This method is called to handle the given message. :param msg: the delivered message """ if self.should_rollover(msg): self.do_rollover() self.rollover_count += 1 self.writer.on_message_received(msg) def _get_new_writer(self, filename: StringPathLike) -> FileIOMessageWriter: """Instantiate a new writer. .. note:: The :attr:`self.writer` should be closed prior to calling this function. :param filename: Path-like object that specifies the location and name of the log file. The log file format is defined by the suffix of `filename`. :return: An instance of a writer class. """ suffixes = pathlib.Path(filename).suffixes for suffix_length in range(len(suffixes), 0, -1): suffix = "".join(suffixes[-suffix_length:]).lower() if suffix not in self._supported_formats: continue logger = Logger(filename=filename, **self.writer_kwargs) if isinstance(logger, FileIOMessageWriter): return logger elif isinstance(logger, Printer) and logger.file is not None: return cast(FileIOMessageWriter, logger) raise ValueError( f'The log format of "{pathlib.Path(filename).name}" ' f"is not supported by {self.__class__.__name__}. " f"{self.__class__.__name__} supports the following formats: " f"{', '.join(self._supported_formats)}" ) def stop(self) -> None: """Stop handling new messages. Carry out any final tasks to ensure data is persisted and cleanup any open resources. """ self.writer.stop() def __enter__(self) -> Self: return self def __exit__( self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], ) -> Literal[False]: return self.writer.__exit__(exc_type, exc_val, exc_tb) @abstractmethod def should_rollover(self, msg: Message) -> bool: """Determine if the rollover conditions are met.""" @abstractmethod def do_rollover(self) -> None: """Perform rollover.""" class SizedRotatingLogger(BaseRotatingLogger): """Log CAN messages to a sequence of files with a given maximum size. The logger creates a log file with the given `base_filename`. When the size threshold is reached the current log file is closed and renamed by adding a timestamp and the rollover count. A new log file is then created and written to. This behavior can be customized by setting the :attr:`~can.io.BaseRotatingLogger.namer` and :attr:`~can.io.BaseRotatingLogger.rotator` attribute. Example:: from can import Notifier, SizedRotatingLogger from can.interfaces.vector import VectorBus bus = VectorBus(channel=[0], app_name="CANape", fd=True) logger = SizedRotatingLogger( base_filename="my_logfile.asc", max_bytes=5 * 1024 ** 2, # =5MB ) logger.rollover_count = 23 # start counter at 23 notifier = Notifier(bus=bus, listeners=[logger]) The SizedRotatingLogger currently supports the formats * .asc: :class:`can.ASCWriter` * .blf :class:`can.BLFWriter` * .csv: :class:`can.CSVWriter` * .log :class:`can.CanutilsLogWriter` * .txt :class:`can.Printer` (if pointing to a file) .. note:: The :class:`can.SqliteWriter` is not supported yet. The log files on disk may be incomplete due to buffering until :meth:`~can.Listener.stop` is called. """ _supported_formats: ClassVar[Set[str]] = {".asc", ".blf", ".csv", ".log", ".txt"} def __init__( self, base_filename: StringPathLike, max_bytes: int = 0, **kwargs: Any, ) -> None: """ :param base_filename: A path-like object for the base filename. The log file format is defined by the suffix of `base_filename`. :param max_bytes: The size threshold at which a new log file shall be created. If set to 0, no rollover will be performed. """ super().__init__(**kwargs) self.base_filename = os.path.abspath(base_filename) self.max_bytes = max_bytes self._writer = self._get_new_writer(self.base_filename) @property def writer(self) -> FileIOMessageWriter: 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 return False def do_rollover(self) -> None: if self.writer: self.writer.stop() sfn = self.base_filename dfn = self.rotation_filename(self._default_name()) self.rotate(sfn, dfn) self._writer = self._get_new_writer(self.base_filename) def _default_name(self) -> StringPathLike: """Generate the default rotation filename.""" path = pathlib.Path(self.base_filename) new_name = ( path.stem.split(".")[0] + "_" + datetime.now().strftime("%Y-%m-%dT%H%M%S") + "_" + f"#{self.rollover_count:03}" + "".join(path.suffixes[-2:]) ) return str(path.parent / new_name) python-can-4.5.0/can/io/mf4.py000066400000000000000000000501361472200326600160170ustar00rootroot00000000000000""" Contains handling of MF4 logging files. MF4 files represent Measurement Data Format (MDF) version 4 as specified by the ASAM MDF standard (see https://www.asam.net/standards/detail/mdf/) """ import abc import heapq import logging from 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 ..message import Message from ..typechecking import StringPathLike from ..util import channel2int, len2dlc from .generic import BinaryIOMessageReader, BinaryIOMessageWriter logger = logging.getLogger("can.io.mf4") try: import asammdf import numpy as np from asammdf import Signal, Source from asammdf.blocks.mdf_v4 import MDF4 from asammdf.blocks.v4_blocks import ChannelGroup, SourceInformation from asammdf.blocks.v4_constants import BUS_TYPE_CAN, FLAG_CG_BUS_EVENT, SOURCE_BUS from asammdf.mdf import MDF STD_DTYPE = np.dtype( [ ("CAN_DataFrame.BusChannel", " None: """ :param file: A path-like object or as file-like object to write to. If this is a file-like object, is has to be opened in binary write mode, not text write mode. :param database: optional path to a DBC or ARXML file that contains message description. :param compression_level: compression option as integer (default 2) * 0 - no compression * 1 - deflate (slower, but produces smaller files) * 2 - transposition + deflate (slowest, but produces the smallest files) """ if asammdf is None: raise NotImplementedError( "The asammdf package was not found. Install python-can with " "the optional dependency [mf4] to use the MF4Writer." ) if kwargs.get("append", False): raise ValueError( f"{self.__class__.__name__} is currently not equipped to " f"append messages to an existing file." ) super().__init__(file, mode="w+b") now = datetime.now() self._mdf = cast(MDF4, MDF(version="4.10")) self._mdf.header.start_time = now self.last_timestamp = self._start_time = now.timestamp() self._compression_level = compression_level if database: database = Path(database).resolve() if database.exists(): data = database.read_bytes() attachment = data, database.name, md5(data).digest() else: attachment = None else: attachment = None acquisition_source = SourceInformation( source_type=SOURCE_BUS, bus_type=BUS_TYPE_CAN ) # standard frames group self._mdf.append( Signal( name="CAN_DataFrame", samples=np.array([], dtype=STD_DTYPE), timestamps=np.array([], dtype=" int: """Return an estimate of the current file size in bytes.""" # TODO: find solution without accessing private attributes of asammdf return cast(int, self._mdf._tempfile.tell()) # pylint: disable=protected-access def stop(self) -> None: self._mdf.save(self.file, compression=self._compression_level) self._mdf.close() super().stop() def on_message_received(self, msg: Message) -> None: channel = channel2int(msg.channel) timestamp = msg.timestamp if timestamp is None: timestamp = self.last_timestamp else: self.last_timestamp = max(self.last_timestamp, timestamp) timestamp -= self._start_time if msg.is_remote_frame: if channel is not None: self._rtr_buffer["CAN_RemoteFrame.BusChannel"] = channel self._rtr_buffer["CAN_RemoteFrame.ID"] = msg.arbitration_id self._rtr_buffer["CAN_RemoteFrame.IDE"] = int(msg.is_extended_id) self._rtr_buffer["CAN_RemoteFrame.Dir"] = 0 if msg.is_rx else 1 self._rtr_buffer["CAN_RemoteFrame.DLC"] = msg.dlc sigs = [(np.array([timestamp]), None), (self._rtr_buffer, None)] self._mdf.extend(2, sigs) elif msg.is_error_frame: if channel is not None: self._err_buffer["CAN_ErrorFrame.BusChannel"] = channel self._err_buffer["CAN_ErrorFrame.ID"] = msg.arbitration_id self._err_buffer["CAN_ErrorFrame.IDE"] = int(msg.is_extended_id) self._err_buffer["CAN_ErrorFrame.Dir"] = 0 if msg.is_rx else 1 data = msg.data size = len(data) self._err_buffer["CAN_ErrorFrame.DataLength"] = size self._err_buffer["CAN_ErrorFrame.DataBytes"][0, :size] = data if msg.is_fd: self._err_buffer["CAN_ErrorFrame.DLC"] = len2dlc(msg.dlc) self._err_buffer["CAN_ErrorFrame.ESI"] = int(msg.error_state_indicator) self._err_buffer["CAN_ErrorFrame.BRS"] = int(msg.bitrate_switch) self._err_buffer["CAN_ErrorFrame.EDL"] = 1 else: self._err_buffer["CAN_ErrorFrame.DLC"] = msg.dlc self._err_buffer["CAN_ErrorFrame.ESI"] = 0 self._err_buffer["CAN_ErrorFrame.BRS"] = 0 self._err_buffer["CAN_ErrorFrame.EDL"] = 0 sigs = [(np.array([timestamp]), None), (self._err_buffer, None)] self._mdf.extend(1, sigs) else: if channel is not None: self._std_buffer["CAN_DataFrame.BusChannel"] = channel self._std_buffer["CAN_DataFrame.ID"] = msg.arbitration_id self._std_buffer["CAN_DataFrame.IDE"] = int(msg.is_extended_id) self._std_buffer["CAN_DataFrame.Dir"] = 0 if msg.is_rx else 1 data = msg.data size = len(data) self._std_buffer["CAN_DataFrame.DataLength"] = size self._std_buffer["CAN_DataFrame.DataBytes"][0, :size] = data if msg.is_fd: self._std_buffer["CAN_DataFrame.DLC"] = len2dlc(msg.dlc) self._std_buffer["CAN_DataFrame.ESI"] = int(msg.error_state_indicator) self._std_buffer["CAN_DataFrame.BRS"] = int(msg.bitrate_switch) self._std_buffer["CAN_DataFrame.EDL"] = 1 else: self._std_buffer["CAN_DataFrame.DLC"] = msg.dlc self._std_buffer["CAN_DataFrame.ESI"] = 0 self._std_buffer["CAN_DataFrame.BRS"] = 0 self._std_buffer["CAN_DataFrame.EDL"] = 0 sigs = [(np.array([timestamp]), None), (self._std_buffer, None)] self._mdf.extend(0, sigs) # reset buffer structure self._std_buffer = np.zeros(1, dtype=STD_DTYPE) self._err_buffer = np.zeros(1, dtype=ERR_DTYPE) self._rtr_buffer = np.zeros(1, dtype=RTR_DTYPE) class FrameIterator(metaclass=abc.ABCMeta): """ Iterator helper class for common handling among CAN DataFrames, ErrorFrames and RemoteFrames. """ # Number of records to request for each asammdf call _chunk_size = 1000 def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float, name: str): self._mdf = mdf self._group_index = group_index self._start_timestamp = start_timestamp self._name = name # Extract names channel_group: ChannelGroup = self._mdf.groups[self._group_index] self._channel_names = [] for channel in channel_group.channels: if str(channel.name).startswith(f"{self._name}."): self._channel_names.append(channel.name) def _get_data(self, current_offset: int) -> Signal: # NOTE: asammdf suggests using select instead of get. Select seem to miss converting some # channels which get does convert as expected. data_raw = self._mdf.get( self._name, self._group_index, record_offset=current_offset, record_count=self._chunk_size, raw=False, ) return data_raw @abc.abstractmethod def __iter__(self) -> Generator[Message, None, None]: pass class MF4Reader(BinaryIOMessageReader): """ Iterator of CAN messages from a MF4 logging file. The MF4Reader only supports MF4 files with CAN bus logging. """ # NOTE: Readout based on the bus logging code from asammdf GUI class _CANDataFrameIterator(FrameIterator): def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float): super().__init__(mdf, group_index, start_timestamp, "CAN_DataFrame") def __iter__(self) -> Generator[Message, None, None]: for current_offset in range( 0, self._mdf.groups[self._group_index].channel_group.cycles_nr, self._chunk_size, ): data = self._get_data(current_offset) names = data.samples[0].dtype.names for i in range(len(data)): data_length = int(data["CAN_DataFrame.DataLength"][i]) kv: Dict[str, Any] = { "timestamp": float(data.timestamps[i]) + self._start_timestamp, "arbitration_id": int(data["CAN_DataFrame.ID"][i]) & 0x1FFFFFFF, "data": data["CAN_DataFrame.DataBytes"][i][ :data_length ].tobytes(), } if "CAN_DataFrame.BusChannel" in names: kv["channel"] = int(data["CAN_DataFrame.BusChannel"][i]) if "CAN_DataFrame.Dir" in names: kv["is_rx"] = int(data["CAN_DataFrame.Dir"][i]) == 0 if "CAN_DataFrame.IDE" in names: kv["is_extended_id"] = bool(data["CAN_DataFrame.IDE"][i]) if "CAN_DataFrame.EDL" in names: kv["is_fd"] = bool(data["CAN_DataFrame.EDL"][i]) if "CAN_DataFrame.BRS" in names: kv["bitrate_switch"] = bool(data["CAN_DataFrame.BRS"][i]) if "CAN_DataFrame.ESI" in names: kv["error_state_indicator"] = bool(data["CAN_DataFrame.ESI"][i]) yield Message(**kv) class _CANErrorFrameIterator(FrameIterator): def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float): super().__init__(mdf, group_index, start_timestamp, "CAN_ErrorFrame") def __iter__(self) -> Generator[Message, None, None]: for current_offset in range( 0, self._mdf.groups[self._group_index].channel_group.cycles_nr, self._chunk_size, ): data = self._get_data(current_offset) names = data.samples[0].dtype.names for i in range(len(data)): kv: Dict[str, Any] = { "timestamp": float(data.timestamps[i]) + self._start_timestamp, "is_error_frame": True, } if "CAN_ErrorFrame.BusChannel" in names: kv["channel"] = int(data["CAN_ErrorFrame.BusChannel"][i]) if "CAN_ErrorFrame.Dir" in names: kv["is_rx"] = int(data["CAN_ErrorFrame.Dir"][i]) == 0 if "CAN_ErrorFrame.ID" in names: kv["arbitration_id"] = ( int(data["CAN_ErrorFrame.ID"][i]) & 0x1FFFFFFF ) if "CAN_ErrorFrame.IDE" in names: kv["is_extended_id"] = bool(data["CAN_ErrorFrame.IDE"][i]) if "CAN_ErrorFrame.EDL" in names: kv["is_fd"] = bool(data["CAN_ErrorFrame.EDL"][i]) if "CAN_ErrorFrame.BRS" in names: kv["bitrate_switch"] = bool(data["CAN_ErrorFrame.BRS"][i]) if "CAN_ErrorFrame.ESI" in names: kv["error_state_indicator"] = bool( data["CAN_ErrorFrame.ESI"][i] ) if "CAN_ErrorFrame.RTR" in names: kv["is_remote_frame"] = bool(data["CAN_ErrorFrame.RTR"][i]) if ( "CAN_ErrorFrame.DataLength" in names and "CAN_ErrorFrame.DataBytes" in names ): data_length = int(data["CAN_ErrorFrame.DataLength"][i]) kv["data"] = data["CAN_ErrorFrame.DataBytes"][i][ :data_length ].tobytes() yield Message(**kv) class _CANRemoteFrameIterator(FrameIterator): def __init__(self, mdf: MDF4, group_index: int, start_timestamp: float): super().__init__(mdf, group_index, start_timestamp, "CAN_RemoteFrame") def __iter__(self) -> Generator[Message, None, None]: for current_offset in range( 0, self._mdf.groups[self._group_index].channel_group.cycles_nr, self._chunk_size, ): data = self._get_data(current_offset) names = data.samples[0].dtype.names for i in range(len(data)): kv: Dict[str, Any] = { "timestamp": float(data.timestamps[i]) + self._start_timestamp, "arbitration_id": int(data["CAN_RemoteFrame.ID"][i]) & 0x1FFFFFFF, "dlc": int(data["CAN_RemoteFrame.DLC"][i]), "is_remote_frame": True, } if "CAN_RemoteFrame.BusChannel" in names: kv["channel"] = int(data["CAN_RemoteFrame.BusChannel"][i]) if "CAN_RemoteFrame.Dir" in names: kv["is_rx"] = int(data["CAN_RemoteFrame.Dir"][i]) == 0 if "CAN_RemoteFrame.IDE" in names: kv["is_extended_id"] = bool(data["CAN_RemoteFrame.IDE"][i]) yield Message(**kv) def __init__( self, file: Union[StringPathLike, BinaryIO], **kwargs: Any, ) -> None: """ :param file: a path-like object or as file-like object to read from If this is a file-like object, is has to be opened in binary read mode, not text read mode. """ if asammdf is None: raise NotImplementedError( "The asammdf package was not found. Install python-can with " "the optional dependency [mf4] to use the MF4Reader." ) super().__init__(file, mode="rb") self._mdf: MDF4 if isinstance(file, BufferedIOBase): self._mdf = cast(MDF4, MDF(BytesIO(file.read()))) else: self._mdf = cast(MDF4, MDF(file)) self._start_timestamp = self._mdf.header.start_time.timestamp() def __iter__(self) -> Iterator[Message]: # To handle messages split over multiple channel groups, create a single iterator per # channel group and merge these iterators into a single iterator using heapq. iterators: List[FrameIterator] = [] for group_index, group in enumerate(self._mdf.groups): channel_group: ChannelGroup = group.channel_group if not channel_group.flags & FLAG_CG_BUS_EVENT: # Not a bus event, skip continue if channel_group.cycles_nr == 0: # No data, skip continue acquisition_source: Optional[Source] = channel_group.acq_source if acquisition_source is None: # No source information, skip continue if not acquisition_source.source_type & Source.SOURCE_BUS: # Not a bus type (likely already covered by the channel group flag), skip continue channel_names = [channel.name for channel in group.channels] if acquisition_source.bus_type == Source.BUS_TYPE_CAN: if "CAN_DataFrame" in channel_names: iterators.append( self._CANDataFrameIterator( self._mdf, group_index, self._start_timestamp ) ) elif "CAN_ErrorFrame" in channel_names: iterators.append( self._CANErrorFrameIterator( self._mdf, group_index, self._start_timestamp ) ) elif "CAN_RemoteFrame" in channel_names: iterators.append( self._CANRemoteFrameIterator( self._mdf, group_index, self._start_timestamp ) ) else: # Unknown bus type, skip continue # Create merged iterator over all the groups, using the timestamps as comparison key return iter(heapq.merge(*iterators, key=lambda x: x.timestamp)) def stop(self) -> None: self._mdf.close() self._mdf = None super().stop() python-can-4.5.0/can/io/player.py000066400000000000000000000136631472200326600166310ustar00rootroot00000000000000""" This module contains the generic :class:`LogReader` as well as :class:`MessageSync` which plays back messages in the recorded order and time intervals. """ import gzip import pathlib import time 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 .asc import ASCReader from .blf import BLFReader from .canutils import CanutilsLogReader from .csv import CSVReader from .generic import BinaryIOMessageReader, MessageReader from .mf4 import MF4Reader from .sqlite import SqliteReader from .trc import TRCReader #: A map of file suffixes to their corresponding #: :class:`can.io.generic.MessageReader` class MESSAGE_READERS: Final[Dict[str, Type[MessageReader]]] = { ".asc": ASCReader, ".blf": BLFReader, ".csv": CSVReader, ".db": SqliteReader, ".log": CanutilsLogReader, ".mf4": MF4Reader, ".trc": TRCReader, } def _update_reader_plugins() -> None: """Update available message reader plugins from entry points.""" for entry_point in read_entry_points("can.io.message_reader"): if entry_point.key in MESSAGE_READERS: continue reader_class = entry_point.load() if issubclass(reader_class, MessageReader): MESSAGE_READERS[entry_point.key] = reader_class def _get_logger_for_suffix(suffix: str) -> Type[MessageReader]: """Find MessageReader class for given suffix.""" try: return MESSAGE_READERS[suffix] except KeyError: raise ValueError(f'No read support for unknown log format "{suffix}"') from None def _decompress( filename: StringPathLike, ) -> Tuple[Type[MessageReader], Union[str, FileLike]]: """ 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 real_suffix = suffixes[-2].lower() reader_type = _get_logger_for_suffix(real_suffix) mode = "rb" if issubclass(reader_type, BinaryIOMessageReader) else "rt" return reader_type, gzip.open(filename, mode) def LogReader(filename: StringPathLike, **kwargs: Any) -> MessageReader: # noqa: N802 """Find and return the appropriate :class:`~can.io.generic.MessageReader` instance for a given file suffix. The format is determined from the file suffix which can be one of: * .asc :class:`can.ASCReader` * .blf :class:`can.BLFReader` * .csv :class:`can.CSVReader` * .db :class:`can.SqliteReader` * .log :class:`can.CanutilsLogReader` * .mf4 :class:`can.MF4Reader` (optional, depends on `asammdf `_) * .trc :class:`can.TRCReader` Gzip compressed files can be used as long as the original files suffix is one of the above (e.g. filename.asc.gz). Exposes a simple iterator interface, to use simply:: for msg in can.LogReader("some/path/to/my_file.log"): print(msg) :param filename: the filename/path of the file to read from :raises ValueError: if the filename's suffix is of an unknown file type .. note:: There are no time delays, if you want to reproduce the measured delays between messages look at the :class:`can.MessageSync` class. .. note:: This function itself is just a dispatcher, and any positional and keyword arguments are passed on to the returned instance. """ _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) class MessageSync: """ Used to iterate over some given messages in the recorded time. """ def __init__( self, messages: Iterable[Message], timestamps: bool = True, gap: float = 0.0001, skip: float = 60.0, ) -> None: """Creates an new **MessageSync** instance. :param messages: An iterable of :class:`can.Message` instances. :param timestamps: Use the messages' timestamps. If False, uses the *gap* parameter as the time between messages. :param gap: Minimum time between sent messages in seconds :param skip: Skip periods of inactivity greater than this (in seconds). Example:: import can with can.LogReader("my_logfile.asc") as reader, can.Bus(interface="virtual") as bus: for msg in can.MessageSync(messages=reader): print(msg) bus.send(msg) """ self.raw_messages = messages self.timestamps = timestamps self.gap = gap self.skip = skip def __iter__(self) -> Generator[Message, None, None]: t_wakeup = playback_start_time = time.perf_counter() recorded_start_time = None t_skipped = 0.0 for message in self.raw_messages: # Work out the correct wait time if self.timestamps: if recorded_start_time is None: recorded_start_time = message.timestamp t_wakeup = playback_start_time + ( message.timestamp - t_skipped - recorded_start_time ) else: t_wakeup += self.gap sleep_period = t_wakeup - time.perf_counter() if self.skip and sleep_period > self.skip: t_skipped += sleep_period - self.skip sleep_period = self.skip if sleep_period > 1e-4: time.sleep(sleep_period) yield message python-can-4.5.0/can/io/printer.py000066400000000000000000000034221472200326600170100ustar00rootroot00000000000000""" This Listener simply prints to stdout / the terminal or a file. """ import logging from typing import Any, Optional, TextIO, Union, cast from ..message import Message from ..typechecking import StringPathLike from .generic import MessageWriter log = logging.getLogger("can.io.printer") class Printer(MessageWriter): """ 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 string using :meth:`~can.Message.__str__`. :attr write_to_file: `True` if this instance prints to a file instead of standard out """ file: Optional[TextIO] def __init__( self, file: Optional[Union[StringPathLike, TextIO]] = None, append: bool = False, **kwargs: Any, ) -> None: """ :param file: An optional path-like object or a file-like object to "print" to instead of writing to standard out (stdout). If this is a file-like object, it has to be opened in text write mode, not binary write mode. :param append: If set to `True` messages, are appended to the file, else the file is truncated """ self.write_to_file = file is not None mode = "a" if append else "w" super().__init__(file, mode=mode) 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 def file_size(self) -> int: """Return an estimate of the current file size in bytes.""" if self.file is not None: return self.file.tell() return 0 python-can-4.5.0/can/io/sqlite.py000066400000000000000000000216441472200326600166340ustar00rootroot00000000000000""" Implements an SQL database writer and reader for storing CAN messages. .. note:: The database schema is given in the documentation of the loggers. """ import logging import sqlite3 import threading import time from typing import Any, Generator from can.listener import BufferedReader from can.message import Message from ..typechecking import StringPathLike from .generic import MessageReader, MessageWriter log = logging.getLogger("can.io.sqlite") class SqliteReader(MessageReader): """ Reads recorded CAN messages from a simple SQL database. This class can be iterated over or used to fetch all messages in the database with :meth:`~SqliteReader.read_all`. Calling :func:`len` on this object might not run in constant time. :attr str table_name: the name of the database table used for storing the messages .. note:: The database schema is given in the documentation of the loggers. """ def __init__( self, file: StringPathLike, table_name: str = "messages", **kwargs: Any, ) -> None: """ :param file: a `str` path like object that points to the database file to use :param str table_name: the name of the table to look for the messages .. warning:: In contrary to all other readers/writers the Sqlite handlers 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 def __iter__(self) -> Generator[Message, None, None]: for frame_data in self._cursor.execute(f"SELECT * FROM {self.table_name}"): yield SqliteReader._assemble_message(frame_data) @staticmethod def _assemble_message(frame_data): timestamp, can_id, is_extended, is_remote, is_error, dlc, data = frame_data return Message( timestamp=timestamp, is_remote_frame=bool(is_remote), is_extended_id=bool(is_extended), is_error_frame=bool(is_error), arbitration_id=can_id, dlc=dlc, data=data, ) def __len__(self): # 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): """Fetches all messages in the database. :rtype: Generator[can.Message] """ result = self._cursor.execute(f"SELECT * FROM {self.table_name}").fetchall() return (SqliteReader._assemble_message(frame) for frame in result) def stop(self): """Closes the connection to the database.""" super().stop() self._conn.close() class SqliteWriter(MessageWriter, BufferedReader): """Logs received CAN data to a simple SQL database. The sqlite database may already exist, otherwise it will be created when the first message arrives. Messages are internally buffered and written to the SQL file in a background thread. Ensures that all messages that are added before calling :meth:`~can.SqliteWriter.stop()` are actually written to the database after that call returns. Thus, calling :meth:`~can.SqliteWriter.stop()` may take a while. :attr str table_name: the name of the database table used for storing the messages :attr int num_frames: the number of frames actually written to the database, this excludes messages that are still buffered :attr float last_write: the last time a message war actually written to the database, as given by ``time.time()`` .. note:: When the listener's :meth:`~SqliteWriter.stop` method is called the thread writing to the database will continue to receive and internally buffer messages if they continue to arrive before the :attr:`~SqliteWriter.GET_MESSAGE_TIMEOUT`. If the :attr:`~SqliteWriter.GET_MESSAGE_TIMEOUT` expires before a message is received, the internal buffer is written out to the database file. However if the bus is still saturated with messages, the Listener will continue receiving until the :attr:`~can.SqliteWriter.MAX_TIME_BETWEEN_WRITES` timeout is reached or more than :attr:`~can.SqliteWriter.MAX_BUFFER_SIZE_BEFORE_WRITES` messages are buffered. .. note:: The database schema is given in the documentation of the loggers. """ GET_MESSAGE_TIMEOUT = 0.25 """Number of seconds to wait for messages from internal queue""" MAX_TIME_BETWEEN_WRITES = 5.0 """Maximum number of seconds to wait between writes to the database""" MAX_BUFFER_SIZE_BEFORE_WRITES = 500 """Maximum number of messages to buffer before writing to the database""" def __init__( self, file: StringPathLike, table_name: str = "messages", **kwargs: Any, ) -> None: """ :param file: a `str` or path like object that points to the database file to use :param str table_name: the name of the table to store messages in .. warning:: In contrary to all other readers/writers the Sqlite handlers do not accept file-like objects as the `file` parameter. """ if kwargs.get("append", False): raise ValueError( f"The append argument should not be used in " f"conjunction with the {self.__class__.__name__}." ) super().__init__(file=None) 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 self.last_write = time.time() self._insert_template = ( f"INSERT INTO {self.table_name} VALUES (?, ?, ?, ?, ?, ?, ?)" ) def _create_db(self): """Creates a new databae or opens a connection to an existing one. .. note:: You can't share sqlite3 connections between threads (by default) 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) # create table structure self._conn.cursor().execute( f"""CREATE TABLE IF NOT EXISTS {self.table_name} ( ts REAL, arbitration_id INTEGER, extended INTEGER, remote INTEGER, error INTEGER, dlc INTEGER, data BLOB )""" ) self._conn.commit() def _db_writer_thread(self): self._create_db() try: while True: messages = [] # reset buffer msg = self.get_message(self.GET_MESSAGE_TIMEOUT) while msg is not None: # log.debug("SqliteWriter: buffering message") messages.append( ( msg.timestamp, msg.arbitration_id, msg.is_extended_id, msg.is_remote_frame, msg.is_error_frame, msg.dlc, memoryview(msg.data), ) ) if ( time.time() - self.last_write > self.MAX_TIME_BETWEEN_WRITES or len(messages) > self.MAX_BUFFER_SIZE_BEFORE_WRITES ): break # just go on msg = self.get_message(self.GET_MESSAGE_TIMEOUT) count = len(messages) if count > 0: with self._conn: # log.debug("Writing %d frames to db", count) self._conn.executemany(self._insert_template, messages) self._conn.commit() # make the changes visible to the entire database self.num_frames += count self.last_write = time.time() # check if we are still supposed to run and go back up if yes if self._stop_running_event.is_set(): break finally: self._conn.close() log.info("Stopped sqlite writer after writing %d messages", self.num_frames) def stop(self): """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) python-can-4.5.0/can/io/trc.py000066400000000000000000000374161472200326600161270ustar00rootroot00000000000000""" Reader and writer for can logging files in peak trc format See https://www.peak-system.com/produktcd/Pdf/English/PEAK_CAN_TRC_File_Format.pdf for file format description Version 1.1 will be implemented as it is most commonly used """ import logging import os from datetime import datetime, timedelta, timezone from enum import Enum from typing import Any, Callable, Dict, Generator, Optional, TextIO, Tuple, Union from ..message import Message from ..typechecking import StringPathLike from ..util import channel2int, len2dlc from .generic import TextIOMessageReader, TextIOMessageWriter logger = logging.getLogger("can.io.trc") class TRCFileVersion(Enum): UNKNOWN = 0 V1_0 = 100 V1_1 = 101 V1_2 = 102 V1_3 = 103 V2_0 = 200 V2_1 = 201 def __ge__(self, other): if self.__class__ is other.__class__: return self.value >= other.value return NotImplemented class TRCReader(TextIOMessageReader): """ Iterator of CAN messages from a TRC logging file. """ file: TextIO def __init__( self, file: Union[StringPathLike, TextIO], **kwargs: Any, ) -> None: """ :param file: a path-like object or as file-like object to read from If this is a file-like object, is has to opened in text read mode, not binary read mode. """ super().__init__(file, mode="r") self.file_version = TRCFileVersion.UNKNOWN self._start_time: float = 0 self.columns: Dict[str, int] = {} self._num_columns = -1 if not self.file: raise ValueError("The given file cannot be None") self._parse_cols: Callable[[Tuple[str, ...]], Optional[Message]] = ( lambda x: None ) @property def start_time(self) -> Optional[datetime]: if self._start_time: return datetime.fromtimestamp(self._start_time, timezone.utc) return None def _extract_header(self): line = "" for _line in self.file: line = _line.strip() if line.startswith(";$FILEVERSION"): logger.debug("TRCReader: Found file version '%s'", line) try: file_version = line.split("=")[1] if file_version == "1.1": self.file_version = TRCFileVersion.V1_1 elif file_version == "1.3": self.file_version = TRCFileVersion.V1_3 elif file_version == "2.0": self.file_version = TRCFileVersion.V2_0 elif file_version == "2.1": self.file_version = TRCFileVersion.V2_1 else: self.file_version = TRCFileVersion.UNKNOWN except IndexError: logger.debug("TRCReader: Failed to parse version") elif line.startswith(";$STARTTIME"): logger.debug("TRCReader: Found start time '%s'", line) try: self._start_time = ( datetime(1899, 12, 30, tzinfo=timezone.utc) + timedelta(days=float(line.split("=")[1])) ).timestamp() except IndexError: logger.debug("TRCReader: Failed to parse start time") elif line.startswith(";$COLUMNS"): logger.debug("TRCReader: Found columns '%s'", line) try: columns = line.split("=")[1].split(",") self.columns = {column: columns.index(column) for column in columns} self._num_columns = len(columns) - 1 except IndexError: logger.debug("TRCReader: Failed to parse columns") elif line.startswith(";"): continue else: break if self.file_version >= TRCFileVersion.V1_1: if self._start_time is None: raise ValueError("File has no start time information") if self.file_version >= TRCFileVersion.V2_0: if not self.columns: raise ValueError("File has no column information") if self.file_version == TRCFileVersion.UNKNOWN: logger.info( "TRCReader: No file version was found, so version 1.0 is assumed" ) self._parse_cols = self._parse_msg_v1_0 elif self.file_version == TRCFileVersion.V1_0: self._parse_cols = self._parse_msg_v1_0 elif self.file_version == TRCFileVersion.V1_1: self._parse_cols = self._parse_cols_v1_1 elif self.file_version == TRCFileVersion.V1_3: self._parse_cols = self._parse_cols_v1_3 elif self.file_version in [TRCFileVersion.V2_0, TRCFileVersion.V2_1]: self._parse_cols = self._parse_cols_v2_x else: raise NotImplementedError("File version not fully implemented for reading") return line def _parse_msg_v1_0(self, cols: Tuple[str, ...]) -> Optional[Message]: arbit_id = cols[2] if arbit_id == "FFFFFFFF": logger.info("TRCReader: Dropping bus info line") return None msg = Message() msg.timestamp = float(cols[1]) / 1000 msg.arbitration_id = int(arbit_id, 16) msg.is_extended_id = len(arbit_id) > 4 msg.channel = 1 msg.dlc = int(cols[3]) 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]: arbit_id = cols[3] msg = Message() msg.timestamp = float(cols[1]) / 1000 + self._start_time msg.arbitration_id = int(arbit_id, 16) msg.is_extended_id = len(arbit_id) > 4 msg.channel = 1 msg.dlc = int(cols[4]) 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]: arbit_id = cols[4] msg = Message() msg.timestamp = float(cols[1]) / 1000 + self._start_time msg.arbitration_id = int(arbit_id, 16) msg.is_extended_id = len(arbit_id) > 4 msg.channel = int(cols[2]) msg.dlc = int(cols[6]) 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]: type_ = cols[self.columns["T"]] bus = self.columns.get("B", None) if "l" in self.columns: length = int(cols[self.columns["l"]]) dlc = len2dlc(length) elif "L" in self.columns: dlc = int(cols[self.columns["L"]]) else: raise ValueError("No length/dlc columns present.") msg = Message() msg.timestamp = float(cols[self.columns["O"]]) / 1000 + self._start_time msg.arbitration_id = int(cols[self.columns["I"]], 16) msg.is_extended_id = len(cols[self.columns["I"]]) > 4 msg.channel = int(cols[bus]) if bus is not None else 1 msg.dlc = dlc if dlc: msg.data = bytearray.fromhex(cols[self.columns["D"]]) msg.is_rx = cols[self.columns["d"]] == "Rx" msg.is_fd = type_ in {"FD", "FB", "FE", "BI"} msg.bitrate_switch = type_ in {"FB", "FE"} msg.error_state_indicator = type_ in {"FE", "BI"} return msg def _parse_cols_v1_1(self, cols: Tuple[str, ...]) -> Optional[Message]: dtype = cols[2] if dtype in ("Tx", "Rx"): return self._parse_msg_v1_1(cols) else: logger.info("TRCReader: Unsupported type '%s'", dtype) return None def _parse_cols_v1_3(self, cols: Tuple[str, ...]) -> Optional[Message]: dtype = cols[3] if dtype in ("Tx", "Rx"): return self._parse_msg_v1_3(cols) else: logger.info("TRCReader: Unsupported type '%s'", dtype) return None def _parse_cols_v2_x(self, cols: Tuple[str, ...]) -> Optional[Message]: dtype = cols[self.columns["T"]] if dtype in {"DT", "FD", "FB", "FE", "BI"}: return self._parse_msg_v2_x(cols) else: logger.info("TRCReader: Unsupported type '%s'", dtype) return None def _parse_line(self, line: str) -> Optional[Message]: logger.debug("TRCReader: Parse '%s'", line) try: cols = tuple(line.split(maxsplit=self._num_columns)) return self._parse_cols(cols) except IndexError: logger.warning("TRCReader: Failed to parse message '%s'", line) return None def __iter__(self) -> Generator[Message, None, None]: first_line = self._extract_header() if first_line is not None: msg = self._parse_line(first_line) if msg is not None: yield msg for line in self.file: temp = line.strip() if temp.startswith(";"): # Comment line continue if len(temp) == 0: # Empty line continue msg = self._parse_line(temp) if msg is not None: yield msg self.stop() class TRCWriter(TextIOMessageWriter): """Logs CAN data to text file (.trc). The measurement starts with the timestamp of the first registered message. If a message has a timestamp smaller than the previous one or None, it gets assigned the timestamp that was written for the last message. If the first message does not have a timestamp, it is set to zero. """ file: TextIO first_timestamp: Optional[float] FORMAT_MESSAGE = ( "{msgnr:>7} {time:13.3f} DT {channel:>2} {id:>8} {dir:>2} - {dlc:<4} {data}" ) FORMAT_MESSAGE_V1_0 = "{msgnr:>6}) {time:7.0f} {id:>8} {dlc:<1} {data}" def __init__( self, file: Union[StringPathLike, TextIO], channel: int = 1, **kwargs: Any, ) -> None: """ :param file: a path-like object or as file-like object to write to If this is a file-like object, is has to opened in text write mode, not binary write mode. :param channel: a default channel to use when the message does not have a channel set """ super().__init__(file, mode="w") self.channel = channel if hasattr(self.file, "reconfigure"): self.file.reconfigure(newline="\r\n") else: raise TypeError("File must be opened in text mode.") self.filepath = os.path.abspath(self.file.name) self.header_written = False self.msgnr = 0 self.first_timestamp = None self.file_version = TRCFileVersion.V2_1 self._msg_fmt_string = self.FORMAT_MESSAGE_V1_0 self._format_message = self._format_message_init def _write_header_v1_0(self, start_time: datetime) -> None: lines = [ ";##########################################################################", f"; {self.filepath}", ";", "; Generated by python-can TRCWriter", f"; Start time: {start_time}", "; PCAN-Net: N/A", ";", "; Columns description:", "; ~~~~~~~~~~~~~~~~~~~~~", "; +-current number in actual sample", "; | +time offset of message (ms", "; | | +ID of message (hex", "; | | | +data length code", "; | | | | +data bytes (hex ...", "; | | | | |", ";----+- ---+--- ----+--- + -+ -- -- ...", ] self.file.writelines(line + "\n" for line in lines) def _write_header_v2_1(self, start_time: datetime) -> None: header_time = start_time - datetime( year=1899, month=12, day=30, tzinfo=timezone.utc ) lines = [ ";$FILEVERSION=2.1", f";$STARTTIME={header_time/timedelta(days=1)}", ";$COLUMNS=N,O,T,B,I,d,R,L,D", ";", f"; {self.filepath}", ";", f"; Start time: {start_time}", "; Generated by python-can TRCWriter", ";-------------------------------------------------------------------------------", "; Bus Name Connection Protocol", "; N/A N/A N/A N/A", ";-------------------------------------------------------------------------------", "; Message Time Type ID Rx/Tx", "; Number Offset | Bus [hex] | Reserved", "; | [ms] | | | | | Data Length Code", "; | | | | | | | | Data [hex] ...", "; | | | | | | | | |", ";---+-- ------+------ +- +- --+----- +- +- +--- +- -- -- -- -- -- -- --", ] self.file.writelines(line + "\n" for line in lines) def _format_message_by_format(self, msg, channel): if msg.is_extended_id: arb_id = f"{msg.arbitration_id:07X}" else: arb_id = f"{msg.arbitration_id:04X}" data = [f"{byte:02X}" for byte in msg.data] serialized = self._msg_fmt_string.format( msgnr=self.msgnr, time=(msg.timestamp - self.first_timestamp) * 1000, channel=channel, id=arb_id, dir="Rx" if msg.is_rx else "Tx", dlc=msg.dlc, data=" ".join(data), ) return serialized def _format_message_init(self, msg, channel): if self.file_version == TRCFileVersion.V1_0: self._format_message = self._format_message_by_format self._msg_fmt_string = self.FORMAT_MESSAGE_V1_0 elif self.file_version == TRCFileVersion.V2_1: self._format_message = self._format_message_by_format self._msg_fmt_string = self.FORMAT_MESSAGE else: raise NotImplementedError("File format is not supported") return self._format_message_by_format(msg, channel) def write_header(self, timestamp: float) -> None: # write start of file header start_time = datetime.fromtimestamp(timestamp, timezone.utc) if self.file_version == TRCFileVersion.V1_0: self._write_header_v1_0(start_time) elif self.file_version == TRCFileVersion.V2_1: self._write_header_v2_1(start_time) else: raise NotImplementedError("File format is not supported") self.header_written = True def log_event(self, message: str, timestamp: float) -> None: if not self.header_written: self.write_header(timestamp) self.file.write(message + "\n") def on_message_received(self, msg: Message) -> None: if self.first_timestamp is None: self.first_timestamp = msg.timestamp if msg.is_error_frame: logger.warning("TRCWriter: Logging error frames is not implemented") return if msg.is_remote_frame: logger.warning("TRCWriter: Logging remote frames is not implemented") return channel = channel2int(msg.channel) if channel is None: channel = self.channel else: # Many interfaces start channel numbering at 0 which is invalid channel += 1 if msg.is_fd: logger.warning("TRCWriter: Logging CAN FD is not implemented") return serialized = self._format_message(msg, channel) self.msgnr += 1 self.log_event(serialized, msg.timestamp) python-can-4.5.0/can/listener.py000066400000000000000000000126351472200326600165510ustar00rootroot00000000000000""" This module contains the implementation of `can.Listener` and some readers. """ import asyncio import sys import warnings from abc import ABCMeta, abstractmethod from queue import Empty, SimpleQueue from typing import Any, AsyncIterator, Optional from can.bus import BusABC from can.message import Message class Listener(metaclass=ABCMeta): """The basic listener that can be called directly to handle some CAN message:: listener = SomeListener() msg = my_bus.recv() # now either call listener(msg) # or listener.on_message_received(msg) # Important to ensure all outputs are flushed listener.stop() """ @abstractmethod def on_message_received(self, msg: Message) -> None: """This method is called to handle the given message. :param msg: the delivered message """ def __call__(self, msg: Message) -> None: self.on_message_received(msg) def on_error(self, exc: Exception) -> None: """This method is called to handle any exception in the receive thread. :param exc: The exception causing the thread to stop """ raise NotImplementedError() def stop(self) -> None: # noqa: B027 """ Stop handling new messages, carry out any final tasks to ensure data is persisted and cleanup any open resources. Concrete implementations override. """ class RedirectReader(Listener): # pylint: disable=abstract-method """ A RedirectReader sends all received messages to another Bus. """ def __init__(self, bus: BusABC, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.bus = bus def on_message_received(self, msg: Message) -> None: self.bus.send(msg) class BufferedReader(Listener): # pylint: disable=abstract-method """ A BufferedReader is a subclass of :class:`~can.Listener` which implements a **message buffer**: that is, when the :class:`can.BufferedReader` instance is notified of a new message it pushes it into a queue of messages waiting to be serviced. The messages can then be fetched with :meth:`~can.BufferedReader.get_message`. Putting in messages after :meth:`~can.BufferedReader.stop` has been called will raise an exception, see :meth:`~can.BufferedReader.on_message_received`. :attr is_stopped: ``True`` if the reader has been stopped """ def __init__(self) -> None: # set to "infinite" size self.buffer: SimpleQueue[Message] = SimpleQueue() self.is_stopped: bool = False def on_message_received(self, msg: Message) -> None: """Append a message to the buffer. :raises: BufferError if the reader has already been stopped """ if self.is_stopped: raise RuntimeError("reader has already been stopped") else: self.buffer.put(msg) def get_message(self, timeout: float = 0.5) -> Optional[Message]: """ Attempts to retrieve the message that has been in the queue for the longest amount of time (FIFO). If no message is available, it blocks for given timeout or until a message is received (whichever is shorter), or else returns None. This method does not block after :meth:`can.BufferedReader.stop` has been called. :param timeout: The number of seconds to wait for a new message. :return: the received :class:`can.Message` or `None`, if the queue is empty. """ try: if self.is_stopped: return self.buffer.get(block=False) else: return self.buffer.get(block=True, timeout=timeout) except Empty: return None def stop(self) -> None: """Prohibits any more additions to this reader.""" self.is_stopped = True class AsyncBufferedReader( Listener, AsyncIterator[Message] ): # pylint: disable=abstract-method """A message buffer for use with :mod:`asyncio`. See :ref:`asyncio` for how to use with :class:`can.Notifier`. Can also be used as an asynchronous iterator:: async for msg in reader: print(msg) """ def __init__(self, **kwargs: Any) -> None: self.buffer: asyncio.Queue[Message] if "loop" in kwargs: warnings.warn( "The 'loop' argument is deprecated since python-can 4.0.0 " "and has no effect starting with Python 3.10", DeprecationWarning, stacklevel=2, ) if sys.version_info < (3, 10): self.buffer = asyncio.Queue(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. Must only be called inside an event loop! """ if not self._is_stopped: self.buffer.put_nowait(msg) async def get_message(self) -> Message: """ Retrieve the latest message when awaited for:: msg = await reader.get_message() :return: The CAN message. """ return await self.buffer.get() def __aiter__(self) -> AsyncIterator[Message]: return self async def __anext__(self) -> Message: return await self.buffer.get() def stop(self) -> None: self._is_stopped = True python-can-4.5.0/can/logconvert.py000066400000000000000000000030051472200326600170750ustar00rootroot00000000000000""" Convert a log file from one format to another. """ import argparse import errno import sys from can import Logger, LogReader, SizedRotatingLogger class ArgumentParser(argparse.ArgumentParser): def error(self, message): self.print_help(sys.stderr) self.exit(errno.EINVAL, f"{self.prog}: error: {message}\n") def main(): parser = ArgumentParser( description="Convert a log file from one format to another.", ) parser.add_argument( "-s", "--file_size", dest="file_size", type=int, help="Maximum file size in bytes. Rotate log file when size threshold is reached.", default=None, ) parser.add_argument( "input", metavar="INFILE", type=str, help="Input filename. The type is dependent on the suffix, see can.LogReader.", ) parser.add_argument( "output", metavar="OUTFILE", type=str, help="Output filename. The type is dependent on the suffix, see can.Logger.", ) args = parser.parse_args() with LogReader(args.input) as reader: if args.file_size: logger = SizedRotatingLogger( base_filename=args.output, max_bytes=args.file_size ) else: logger = Logger(filename=args.output) with logger: try: for m in reader: logger(m) except KeyboardInterrupt: sys.exit(1) if __name__ == "__main__": main() python-can-4.5.0/can/logger.py000066400000000000000000000242661472200326600162060ustar00rootroot00000000000000import 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.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 : (matches when & mask ==" " can_id & mask)" "\n ~ (matches when & mask !=" " can_id & mask)" "\nFx to show only frames with ID 0x100 to 0x103 and 0x200 to 0x20F:" "\n python -m can.viewer --filter 100:7FC 200:7F0" "\nNote that the ID and mask are always interpreted as hex values", metavar="{:,~}", 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]: """Parse command line arguments for logger script.""" parser = argparse.ArgumentParser( description="Log CAN traffic, printing messages to stdout or to a " "given file.", ) # Generate the standard arguments: # Channel, bitrate, data_bitrate, interface, app_name, CAN-FD support _create_base_argument_parser(parser) parser.add_argument( "-f", "--file_name", dest="log_file", help="Path and base log filename, for supported types see can.Logger.", default=None, ) parser.add_argument( "-a", "--append", dest="append", help="Append to the log file if it already exists.", action="store_true", ) parser.add_argument( "-s", "--file_size", dest="file_size", type=int, help="Maximum file size in bytes. Rotate log file when size threshold " "is reached. (The resulting file sizes will be consistent, but are not " "guaranteed to be exactly what is specified here due to the rollover " "conditions being logger implementation specific.)", default=None, ) parser.add_argument( "-v", action="count", dest="verbosity", help="""How much information do you want to see at the command line? You can add several of these e.g., -vv is DEBUG""", default=2, ) _append_filter_argument(parser) state_group = parser.add_mutually_exclusive_group(required=False) state_group.add_argument( "--active", help="Start the bus as active, this is applied by default.", action="store_true", ) state_group.add_argument( "--passive", help="Start the bus as passive.", action="store_true" ) # 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) additional_config = _parse_additional_config([*results.extra_args, *unknown_args]) return results, additional_config def main() -> None: results, additional_config = _parse_logger_args(sys.argv[1:]) bus = _create_bus(results, **additional_config) if results.active: bus.state = BusState.ACTIVE elif results.passive: bus.state = BusState.PASSIVE print(f"Connected to {bus.__class__.__name__}: {bus.channel_info}") print(f"Can Logger (Started on {datetime.now()})") logger: Union[MessageWriter, BaseRotatingLogger] if results.file_size: logger = SizedRotatingLogger( base_filename=results.log_file, max_bytes=results.file_size, append=results.append, **additional_config, ) else: logger = Logger( filename=results.log_file, append=results.append, **additional_config, ) try: while True: msg = bus.recv(1) if msg is not None: logger(msg) except KeyboardInterrupt: pass finally: bus.shutdown() logger.stop() if __name__ == "__main__": main() python-can-4.5.0/can/message.py000066400000000000000000000274351472200326600163540ustar00rootroot00000000000000""" This module contains the implementation of :class:`can.Message`. .. note:: Could use `@dataclass `__ starting with Python 3.7. """ from copy import deepcopy from math import isinf, isnan from typing import Optional from . import typechecking class Message: # pylint: disable=too-many-instance-attributes; OK for a dataclass """ The :class:`~can.Message` object is used to represent CAN messages for sending, receiving and other purposes like converting between different logging formats. Messages can use extended identifiers, be remote or error frames, contain data and may be associated to a channel. Messages are always compared by identity and never by value, because that may introduce unexpected behaviour. See also :meth:`~can.Message.equals`. :func:`~copy.copy`/:func:`~copy.deepcopy` is supported as well. Messages do not support "dynamic" attributes, meaning any others than the documented ones, since it uses :obj:`~object.__slots__`. """ __slots__ = ( "timestamp", "arbitration_id", "is_extended_id", "is_remote_frame", "is_error_frame", "channel", "dlc", "data", "is_fd", "is_rx", "bitrate_switch", "error_state_indicator", "__weakref__", # support weak references to messages ) def __init__( # pylint: disable=too-many-locals, too-many-arguments self, timestamp: float = 0.0, arbitration_id: int = 0, is_extended_id: bool = True, is_remote_frame: bool = False, is_error_frame: bool = False, channel: Optional[typechecking.Channel] = None, dlc: Optional[int] = None, data: Optional[typechecking.CanData] = None, is_fd: bool = False, is_rx: bool = True, bitrate_switch: bool = False, error_state_indicator: bool = False, check: bool = False, ): """ To create a message object, simply provide any of the below attributes together with additional parameters as keyword arguments to the constructor. :param check: By default, the constructor of this class does not strictly check the input. Thus, the caller must prevent the creation of invalid messages or set this parameter to `True`, to raise an Error on invalid inputs. Possible problems include the `dlc` field not matching the length of `data` or creating a message with both `is_remote_frame` and `is_error_frame` set to `True`. :raises ValueError: If and only if `check` is set to `True` and one or more arguments were invalid """ self.timestamp = timestamp self.arbitration_id = arbitration_id self.is_extended_id = is_extended_id self.is_remote_frame = is_remote_frame self.is_error_frame = is_error_frame self.channel = channel self.is_fd = is_fd self.is_rx = is_rx self.bitrate_switch = bitrate_switch self.error_state_indicator = error_state_indicator if data is None or is_remote_frame: self.data = bytearray() elif isinstance(data, bytearray): self.data = data else: try: self.data = bytearray(data) except TypeError as error: err = f"Couldn't create message from {data} ({type(data)})" raise TypeError(err) from error if dlc is None: self.dlc = len(self.data) else: self.dlc = dlc if check: self._check() def __str__(self) -> str: field_strings = [f"Timestamp: {self.timestamp:>15.6f}"] if self.is_extended_id: arbitration_id_string = f"{self.arbitration_id:08x}" else: arbitration_id_string = f"{self.arbitration_id:03x}" field_strings.append(f"ID: {arbitration_id_string:>8}") flag_string = " ".join( [ "X" if self.is_extended_id else "S", "Rx" if self.is_rx else "Tx", "E" if self.is_error_frame else " ", "R" if self.is_remote_frame else " ", "F" if self.is_fd else " ", "BS" if self.bitrate_switch else " ", "EI" if self.error_state_indicator else " ", ] ) field_strings.append(flag_string) field_strings.append(f"DL: {self.dlc:2d}") data_strings = "" if self.data is not None: data_strings = self.data[: min(self.dlc, len(self.data))].hex(" ") if data_strings: # if not empty field_strings.append(data_strings.ljust(24, " ")) else: field_strings.append(" " * 24) if (self.data is not None) and (self.data.isalnum()): field_strings.append(f"'{self.data.decode('utf-8', 'replace')}'") if self.channel is not None: try: field_strings.append(f"Channel: {self.channel}") except UnicodeEncodeError: pass return " ".join(field_strings).strip() def __len__(self) -> int: # return the dlc such that it also works on remote frames return self.dlc def __bool__(self) -> bool: return True def __repr__(self) -> str: args = [ f"timestamp={self.timestamp}", f"arbitration_id={self.arbitration_id:#x}", f"is_extended_id={self.is_extended_id}", ] if not self.is_rx: args.append("is_rx=False") if self.is_remote_frame: args.append(f"is_remote_frame={self.is_remote_frame}") if self.is_error_frame: args.append(f"is_error_frame={self.is_error_frame}") if self.channel is not None: args.append(f"channel={self.channel!r}") data = [f"{byte:#02x}" for byte in self.data] args += [f"dlc={self.dlc}", f"data=[{', '.join(data)}]"] if self.is_fd: args.append("is_fd=True") args.append(f"bitrate_switch={self.bitrate_switch}") args.append(f"error_state_indicator={self.error_state_indicator}") return f"can.Message({', '.join(args)})" def __format__(self, format_spec: Optional[str]) -> str: if not format_spec: return self.__str__() else: raise ValueError("non-empty format_specs are not supported") def __bytes__(self) -> bytes: return bytes(self.data) def __copy__(self) -> "Message": return Message( timestamp=self.timestamp, arbitration_id=self.arbitration_id, is_extended_id=self.is_extended_id, is_remote_frame=self.is_remote_frame, is_error_frame=self.is_error_frame, channel=self.channel, dlc=self.dlc, data=self.data, is_fd=self.is_fd, is_rx=self.is_rx, bitrate_switch=self.bitrate_switch, error_state_indicator=self.error_state_indicator, ) def __deepcopy__(self, memo: dict) -> "Message": return Message( timestamp=self.timestamp, arbitration_id=self.arbitration_id, is_extended_id=self.is_extended_id, is_remote_frame=self.is_remote_frame, is_error_frame=self.is_error_frame, channel=deepcopy(self.channel, memo), dlc=self.dlc, data=deepcopy(self.data, memo), is_fd=self.is_fd, is_rx=self.is_rx, bitrate_switch=self.bitrate_switch, error_state_indicator=self.error_state_indicator, ) def _check(self) -> None: """Checks if the message parameters are valid. Assumes that the attribute types are already correct. :raises ValueError: If and only if one or more attributes are invalid """ if self.timestamp < 0.0: raise ValueError("the timestamp may not be negative") if isinf(self.timestamp): raise ValueError("the timestamp may not be infinite") if isnan(self.timestamp): raise ValueError("the timestamp may not be NaN") if self.is_remote_frame: if self.is_error_frame: raise ValueError( "a message cannot be a remote and an error frame at the same time" ) if self.is_fd: raise ValueError("CAN FD does not support remote frames") if self.arbitration_id < 0: raise ValueError("arbitration IDs may not be negative") if self.is_extended_id: if self.arbitration_id >= 0x20000000: raise ValueError("Extended arbitration IDs must be less than 2^29") elif self.arbitration_id >= 0x800: raise ValueError("Normal arbitration IDs must be less than 2^11") if self.dlc < 0: raise ValueError("DLC may not be negative") if self.is_fd: if self.dlc > 64: raise ValueError( f"DLC was {self.dlc} but it should be <= 64 for CAN FD frames" ) elif self.dlc > 8: raise ValueError( f"DLC was {self.dlc} but it should be <= 8 for normal CAN frames" ) if self.is_remote_frame: if self.data: raise ValueError("remote frames may not carry any data") elif self.dlc != len(self.data): raise ValueError( "the DLC and the length of the data must match up for non remote frames" ) if not self.is_fd: if self.bitrate_switch: raise ValueError("bitrate switch is only allowed for CAN FD frames") if self.error_state_indicator: raise ValueError( "error state indicator is only allowed for CAN FD frames" ) def equals( self, other: "Message", timestamp_delta: Optional[float] = 1.0e-6, check_channel: bool = True, check_direction: bool = True, ) -> bool: """ Compares a given message with this one. :param other: the message to compare with :param timestamp_delta: the maximum difference in seconds at which two timestamps are still considered equal or `None` to not compare timestamps :param check_channel: whether to compare the message channel :param check_direction: whether to compare the messages' directions (Tx/Rx) :return: True if and only if the given message equals this one """ # see https://github.com/hardbyte/python-can/pull/413 for a discussion # on why a delta of 1.0e-6 was chosen return ( # check for identity first and finish fast self is other or # then check for equality by value ( ( timestamp_delta is None or abs(self.timestamp - other.timestamp) <= timestamp_delta ) and (self.is_rx == other.is_rx or not check_direction) and self.arbitration_id == other.arbitration_id and self.is_extended_id == other.is_extended_id and self.dlc == other.dlc and self.data == other.data and self.is_remote_frame == other.is_remote_frame and self.is_error_frame == other.is_error_frame and (self.channel == other.channel or not check_channel) and self.is_fd == other.is_fd and self.bitrate_switch == other.bitrate_switch and self.error_state_indicator == other.error_state_indicator ) ) python-can-4.5.0/can/notifier.py000066400000000000000000000147211472200326600165410ustar00rootroot00000000000000""" This module contains the implementation of :class:`~can.Notifier`. """ import asyncio import functools import logging import threading import time from typing import Any, Awaitable, Callable, Iterable, List, Optional, Union from can.bus import BusABC from can.listener import Listener from can.message import Message logger = logging.getLogger("can.Notifier") MessageRecipient = Union[Listener, Callable[[Message], Union[Awaitable[None], None]]] class Notifier: def __init__( self, bus: Union[BusABC, List[BusABC]], listeners: Iterable[MessageRecipient], timeout: float = 1.0, loop: Optional[asyncio.AbstractEventLoop] = None, ) -> None: """Manages the distribution of :class:`~can.Message` instances to listeners. Supports multiple buses and listeners. .. Note:: Remember to call `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 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. """ self.listeners: List[MessageRecipient] = list(listeners) self.bus = bus self.timeout = timeout self._loop = loop #: Exception raised in thread self.exception: Optional[Exception] = None self._running = True 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.add_bus(each_bus) def add_bus(self, bus: BusABC) -> None: """Add a bus for notification. :param bus: CAN bus instance. """ reader: int = -1 try: reader = 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: # Use bus file descriptor to watch for messages self._loop.add_reader(reader, self._on_message_available, bus) self._readers.append(reader) else: reader_thread = threading.Thread( target=self._rx_thread, args=(bus,), name=f'can.notifier for bus "{bus.channel_info}"', ) reader_thread.daemon = True reader_thread.start() self._readers.append(reader_thread) def stop(self, timeout: float = 5) -> None: """Stop notifying Listeners when new :class:`~can.Message` objects arrive and call :meth:`~can.Listener.stop` on each Listener. :param timeout: Max time in seconds to wait for receive threads to finish. Should be longer than timeout given at instantiation. """ self._running = False end_time = time.time() + timeout for reader in self._readers: if isinstance(reader, threading.Thread): now = time.time() if now < end_time: reader.join(end_time - now) elif self._loop: # reader is a file descriptor self._loop.remove_reader(reader) for listener in self.listeners: if hasattr(listener, "stop"): listener.stop() def _rx_thread(self, bus: BusABC) -> None: # determine message handling callable early, not inside while loop if self._loop: handle_message: Callable[[Message], Any] = functools.partial( self._loop.call_soon_threadsafe, self._on_message_received, # type: ignore[arg-type] ) else: handle_message = self._on_message_received while self._running: try: if msg := bus.recv(self.timeout): with self._lock: handle_message(msg) except Exception as exc: # pylint: disable=broad-except self.exception = exc if self._loop is not None: self._loop.call_soon_threadsafe(self._on_error, exc) # Raise anyway raise elif not self._on_error(exc): # If it was not handled, raise the exception here raise else: # It was handled, so only log it logger.debug("suppressed exception: %s", exc) def _on_message_available(self, bus: BusABC) -> None: if msg := bus.recv(0): self._on_message_received(msg) def _on_message_received(self, msg: Message) -> None: for callback in self.listeners: res = callback(msg) if res and self._loop and asyncio.iscoroutine(res): # Schedule coroutine self._loop.create_task(res) def _on_error(self, exc: Exception) -> bool: """Calls ``on_error()`` for all listeners if they implement it. :returns: ``True`` if at least one error handler was called. """ was_handled = False for listener in self.listeners: if hasattr(listener, "on_error"): try: listener.on_error(exc) except NotImplementedError: pass else: was_handled = True return was_handled def add_listener(self, listener: MessageRecipient) -> None: """Add new Listener to the notification list. If it is already present, it will be called two times each time a message arrives. :param listener: Listener to be added to the list to be notified """ self.listeners.append(listener) def remove_listener(self, listener: MessageRecipient) -> None: """Remove a listener from the notification list. This method throws an exception if the given listener is not part of the stored listeners. :param listener: Listener to be removed from the list to be notified :raises ValueError: if `listener` was never added to this notifier """ self.listeners.remove(listener) python-can-4.5.0/can/player.py000066400000000000000000000056261472200326600162220ustar00rootroot00000000000000""" Replays CAN traffic saved with can.logger back to a CAN bus. Similar to canplayer in the can-utils package. """ import argparse import errno import sys from datetime import datetime from typing import Iterable, cast from can import LogReader, Message, MessageSync from .logger import _create_base_argument_parser, _create_bus, _parse_additional_config def main() -> None: parser = argparse.ArgumentParser(description="Replay CAN traffic.") _create_base_argument_parser(parser) parser.add_argument( "-f", "--file_name", dest="log_file", help="Path and base log filename, for supported types see can.LogReader.", default=None, ) parser.add_argument( "-v", action="count", dest="verbosity", help="""Also print can frames to stdout. You can add several of these to enable debugging""", default=2, ) parser.add_argument( "--ignore-timestamps", dest="timestamps", help="""Ignore timestamps (send all frames immediately with minimum gap between frames)""", action="store_false", ) parser.add_argument( "--error-frames", help="Also send error frames to the interface.", action="store_true", ) parser.add_argument( "-g", "--gap", type=float, help=" minimum time between replayed frames", default=0.0001, ) parser.add_argument( "-s", "--skip", type=float, default=60 * 60 * 24, help=" skip gaps greater than 's' seconds", ) parser.add_argument( "infile", metavar="input-file", type=str, help="The file to replay. For supported types see can.LogReader.", ) # print help message when no arguments were given if len(sys.argv) < 2: parser.print_help(sys.stderr) raise SystemExit(errno.EINVAL) results, unknown_args = parser.parse_known_args() additional_config = _parse_additional_config([*results.extra_args, *unknown_args]) verbosity = results.verbosity error_frames = results.error_frames with _create_bus(results, **additional_config) as bus: with LogReader(results.infile, **additional_config) as reader: in_sync = MessageSync( cast(Iterable[Message], reader), timestamps=results.timestamps, gap=results.gap, skip=results.skip, ) print(f"Can LogReader (Started on {datetime.now()})") try: for message in in_sync: if message.is_error_frame and not error_frames: continue if verbosity >= 3: print(message) bus.send(message) except KeyboardInterrupt: pass if __name__ == "__main__": main() python-can-4.5.0/can/py.typed000066400000000000000000000000001472200326600160300ustar00rootroot00000000000000python-can-4.5.0/can/thread_safe_bus.py000066400000000000000000000060511472200326600200350ustar00rootroot00000000000000from threading import RLock try: # Only raise an exception on instantiation but allow module # to be imported from wrapt import ObjectProxy import_exc = None except ImportError as exc: ObjectProxy = object import_exc = exc from contextlib import nullcontext from .interface import Bus class ThreadSafeBus(ObjectProxy): # pylint: disable=abstract-method """ Contains a thread safe :class:`can.BusABC` implementation that wraps around an existing interface instance. All public methods of that base class are now safe to be called from multiple threads. The send and receive methods are synchronized separately. Use this as a drop-in replacement for :class:`~can.BusABC`. .. note:: This approach assumes that both :meth:`~can.BusABC.send` and :meth:`~can.BusABC._recv_internal` of the underlying bus instance can be called simultaneously, and that the methods use :meth:`~can.BusABC._recv_internal` instead of :meth:`~can.BusABC.recv` directly. """ def __init__(self, *args, **kwargs): if import_exc is not None: raise import_exc super().__init__(Bus(*args, **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() # 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 with self._lock_recv: return self.__wrapped__.recv(timeout=timeout, *args, **kwargs) def send( self, msg, timeout=None, *args, **kwargs ): # pylint: disable=keyword-arg-before-vararg with self._lock_send: return self.__wrapped__.send(msg, timeout=timeout, *args, **kwargs) # send_periodic does not need a lock, since the underlying # `send` method is already synchronized @property def filters(self): with self._lock_recv: return self.__wrapped__.filters @filters.setter def filters(self, filters): with self._lock_recv: self.__wrapped__.filters = filters def set_filters( self, filters=None, *args, **kwargs ): # pylint: disable=keyword-arg-before-vararg with self._lock_recv: return self.__wrapped__.set_filters(filters=filters, *args, **kwargs) def flush_tx_buffer(self, *args, **kwargs): with self._lock_send: return self.__wrapped__.flush_tx_buffer(*args, **kwargs) def shutdown(self, *args, **kwargs): with self._lock_send, self._lock_recv: return self.__wrapped__.shutdown(*args, **kwargs) @property def state(self): with self._lock_send, self._lock_recv: return self.__wrapped__.state @state.setter def state(self, new_state): with self._lock_send, self._lock_recv: self.__wrapped__.state = new_state python-can-4.5.0/can/typechecking.py000066400000000000000000000037571472200326600174060ustar00rootroot00000000000000"""Types for mypy type-checking """ import gzip import struct import sys import typing if sys.version_info >= (3, 10): from typing import TypeAlias else: from typing_extensions import TypeAlias if sys.version_info >= (3, 12): from typing import TypedDict else: from typing_extensions import TypedDict if typing.TYPE_CHECKING: import os class CanFilter(TypedDict): can_id: int can_mask: int class CanFilterExtended(TypedDict): can_id: int can_mask: int extended: bool CanFilters = typing.Sequence[typing.Union[CanFilter, CanFilterExtended]] # 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]] # Used for the Abstract Base Class ChannelStr = str ChannelInt = int Channel = typing.Union[ChannelInt, ChannelStr] # 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] BusConfig = typing.NewType("BusConfig", typing.Dict[str, typing.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], ] class AutoDetectedConfig(TypedDict): interface: str channel: Channel ReadableBytesLike = typing.Union[bytes, bytearray, memoryview] class BitTimingDict(TypedDict): f_clock: int brp: int tseg1: int tseg2: int sjw: int nof_samples: int class BitTimingFdDict(TypedDict): f_clock: int nom_brp: int nom_tseg1: int nom_tseg2: int nom_sjw: int data_brp: int data_tseg1: int data_tseg2: int data_sjw: int python-can-4.5.0/can/util.py000066400000000000000000000400121472200326600156670ustar00rootroot00000000000000""" Utilities and configuration file parsing. """ import contextlib import copy import functools import json import logging import os import os.path import platform import re import warnings 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, ) from typing_extensions import ParamSpec import can from . import typechecking from .bit_timing import BitTiming, BitTimingFd from .exceptions import CanInitializationError, CanInterfaceNotImplementedError from .interfaces import VALID_INTERFACES log = logging.getLogger("can.util") # List of valid data lengths for a CAN FD message CAN_FD_DLC = [0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64] REQUIRED_KEYS = ["interface", "channel"] CONFIG_FILES = ["~/can.conf"] if platform.system() in ("Linux", "Darwin"): CONFIG_FILES.extend(["/etc/can.conf", "~/.can", "~/.canrc"]) elif platform.system() == "Windows" or platform.python_implementation() == "IronPython": CONFIG_FILES.extend(["can.ini", os.path.join(os.getenv("APPDATA", ""), "can.ini")]) def load_file_config( path: Optional[typechecking.AcceptedIOType] = None, section: str = "default" ) -> Dict[str, str]: """ Loads configuration from file with following content:: [default] interface = socketcan channel = can0 :param path: path to config file. If not specified, several sensible default locations are tried depending on platform. :param section: name of the section to read configuration from. """ config = ConfigParser() # make sure to not transform the entries such that capitalization is preserved config.optionxform = lambda optionstr: optionstr # type: ignore[method-assign] if path is None: config.read([os.path.expanduser(path) for path in CONFIG_FILES]) else: config.read(path) _config: Dict[str, str] = {} if config.has_section(section): _config.update(config.items(section)) return _config def load_environment_config(context: Optional[str] = None) -> Dict[str, str]: """ Loads config dict from environmental variables (if set): * CAN_INTERFACE * CAN_CHANNEL * CAN_BITRATE * CAN_CONFIG if context is supplied, "_{context}" is appended to the environment variable name we will look at. For example if context="ABC": * CAN_INTERFACE_ABC * CAN_CHANNEL_ABC * CAN_BITRATE_ABC * CAN_CONFIG_ABC """ mapper = { "interface": "CAN_INTERFACE", "channel": "CAN_CHANNEL", "bitrate": "CAN_BITRATE", } context_suffix = f"_{context}" if context else "" can_config_key = f"CAN_CONFIG{context_suffix}" config: Dict[str, str] = json.loads(os.environ.get(can_config_key, "{}")) for key, val in mapper.items(): config_option = os.environ.get(val + context_suffix, None) if config_option: config[key] = config_option return config def load_config( path: Optional[typechecking.AcceptedIOType] = None, config: Optional[Dict[str, Any]] = None, context: Optional[str] = None, ) -> typechecking.BusConfig: """ Returns a dict with configuration details which is loaded from (in this order): - config - can.rc - Environment variables CAN_INTERFACE, CAN_CHANNEL, CAN_BITRATE - Config files ``/etc/can.conf`` or ``~/.can`` or ``~/.canrc`` where the latter may add or replace values of the former. Interface can be any of the strings from ``can.VALID_INTERFACES`` for example: kvaser, socketcan, pcan, usb2can, ixxat, nican, virtual. .. note:: The key ``bustype`` is copied to ``interface`` if that one is missing and does never appear in the result. :param path: Optional path to config file. :param config: A dict which may set the 'interface', and/or the 'channel', or neither. It may set other values that are passed through. :param context: Extra 'context' pass to config sources. This can be used to section other than 'default' in the configuration file. :return: A config dictionary that should contain 'interface' & 'channel':: { 'interface': 'python-can backend interface to use', 'channel': 'default channel to use', # possibly more } Note ``None`` will be used if all the options are exhausted without finding a value. All unused values are passed from ``config`` over to this. :raises: CanInterfaceNotImplementedError if the ``interface`` name isn't recognized """ # Start with an empty dict to apply filtering to all sources given_config = config or {} config = {} # Use the given dict for default values config_sources = cast( Iterable[Union[Dict[str, Any], Callable[[Any], Dict[str, Any]]]], [ given_config, can.rc, lambda _context: load_environment_config( # pylint: disable=unnecessary-lambda _context ), lambda _context: load_environment_config(), lambda _context: load_file_config(path, _context), lambda _context: load_file_config(path), ], ) # Slightly complex here to only search for the file config if required for _cfg in config_sources: cfg = _cfg(context) if callable(_cfg) else _cfg # remove legacy operator (and copy to interface if not already present) if "bustype" in cfg: if "interface" not in cfg or not cfg["interface"]: cfg["interface"] = cfg["bustype"] del cfg["bustype"] # copy all new parameters for key, val in cfg.items(): if key not in config: if isinstance(val, str): config[key] = cast_from_string(val) else: config[key] = cfg[key] bus_config = _create_bus_config(config) can.log.debug("can config: %s", bus_config) return bus_config def _create_bus_config(config: Dict[str, Any]) -> typechecking.BusConfig: """Validates some config values, performs compatibility mappings and creates specific structures (e.g. for bit timings). :param config: The raw config as specified by the user :return: A config that can be used by a :class:`~can.BusABC` :raises NotImplementedError: if the ``interface`` is unknown """ # substitute None for all values not found for key in REQUIRED_KEYS: if key not in config: config[key] = None if config["interface"] not in VALID_INTERFACES: raise CanInterfaceNotImplementedError( f'Unknown interface type "{config["interface"]}"' ) if "port" in config: # convert port to integer if necessary if isinstance(config["port"], int): port = config["port"] elif isinstance(config["port"], str): if config["port"].isnumeric(): config["port"] = port = int(config["port"]) else: raise ValueError("Port config must be a number!") else: raise TypeError("Port config must be string or integer!") if not 0 < port < 65535: raise ValueError("Port config must be inside 0-65535 range!") if "timing" not in config: if timing := _dict2timing(config): config["timing"] = timing if "fd" in config: config["fd"] = config["fd"] not in (0, False) return cast(typechecking.BusConfig, config) 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.""" with contextlib.suppress(ValueError, TypeError): if set(typechecking.BitTimingFdDict.__annotations__).issubset(data): return BitTimingFd( **{ key: int(data[key]) for key in typechecking.BitTimingFdDict.__annotations__ }, strict=False, ) elif set(typechecking.BitTimingDict.__annotations__).issubset(data): return BitTiming( **{ key: int(data[key]) for key in typechecking.BitTimingDict.__annotations__ }, strict=False, ) return None def set_logging_level(level_name: str) -> None: """Set the logging level for the `"can"` logger. :param level_name: One of: `'critical'`, `'error'`, `'warning'`, `'info'`, `'debug'`, `'subdebug'`, or the value :obj:`None` (=default). Defaults to `'debug'`. """ can_logger = logging.getLogger("can") try: can_logger.setLevel(getattr(logging, level_name.upper())) except AttributeError: can_logger.setLevel(logging.DEBUG) log.debug("Logging set to %s", level_name) def len2dlc(length: int) -> int: """Calculate the DLC from data length. :param length: Length in number of bytes (0-64) :returns: DLC (0-15) """ if length <= 8: return length for dlc, nof_bytes in enumerate(CAN_FD_DLC): if nof_bytes >= length: return dlc return 15 def dlc2len(dlc: int) -> int: """Calculate the data length from DLC. :param dlc: DLC (0-15) :returns: Data length in number of bytes (0-64) """ return CAN_FD_DLC[dlc] if dlc <= 15 else 64 def channel2int(channel: Optional[typechecking.Channel]) -> Optional[int]: """Try to convert the channel to an integer. :param channel: Channel string (e.g. `"can0"`, `"CAN1"`) or an integer :returns: Channel integer or ``None`` if unsuccessful """ if isinstance(channel, int): return channel if isinstance(channel, str): match = re.match(r".*?(\d+)$", channel) if match: return int(match.group(1)) return None P1 = ParamSpec("P1") T1 = TypeVar("T1") def deprecated_args_alias( deprecation_start: str, deprecation_end: Optional[str] = None, **aliases: Optional[str], ) -> Callable[[Callable[P1, T1]], Callable[P1, T1]]: """Allows to rename/deprecate a function kwarg(s) and optionally have the deprecated kwarg(s) set as alias(es) Example:: @deprecated_args_alias("1.2.0", oldArg="new_arg", anotherOldArg="another_new_arg") def library_function(new_arg, another_new_arg): pass @deprecated_args_alias( deprecation_start="1.2.0", deprecation_end="3.0.0", oldArg="new_arg", obsoleteOldArg=None, ) def library_function(new_arg): pass :param deprecation_start: The *python-can* version, that introduced the :class:`DeprecationWarning`. :param deprecation_end: The *python-can* version, that marks the end of the deprecation period. :param aliases: keyword arguments, that map the deprecated argument names to the new argument names or ``None``. """ def deco(f: Callable[P1, T1]) -> Callable[P1, T1]: @functools.wraps(f) def wrapper(*args: P1.args, **kwargs: P1.kwargs) -> T1: _rename_kwargs( func_name=f.__name__, start=deprecation_start, end=deprecation_end, kwargs=kwargs, aliases=aliases, ) return f(*args, **kwargs) return wrapper return deco def _rename_kwargs( func_name: str, start: str, end: Optional[str], kwargs: P1.kwargs, aliases: Dict[str, Optional[str]], ) -> None: """Helper function for `deprecated_args_alias`""" for alias, new in aliases.items(): if alias in kwargs: deprecation_notice = ( f"The '{alias}' argument is deprecated since python-can v{start}" ) if end: deprecation_notice += ( f", and scheduled for removal in python-can v{end}" ) deprecation_notice += "." value = kwargs.pop(alias) if new is not None: deprecation_notice += f" Use '{new}' instead." if new in kwargs: raise TypeError( f"{func_name} received both '{alias}' (deprecated) and '{new}'." ) kwargs[new] = value warnings.warn(deprecation_notice, DeprecationWarning, stacklevel=3) T2 = TypeVar("T2", BitTiming, BitTimingFd) def check_or_adjust_timing_clock(timing: T2, valid_clocks: Iterable[int]) -> T2: """Adjusts the given timing instance to have an *f_clock* value that is within the allowed values specified by *valid_clocks*. If the *f_clock* value of timing is already within *valid_clocks*, then *timing* is returned unchanged. :param timing: The :class:`~can.BitTiming` or :class:`~can.BitTimingFd` instance to adjust. :param valid_clocks: An iterable of integers representing the valid *f_clock* values that the timing instance can be changed to. The order of the values in *valid_clocks* determines the priority in which they are tried, with earlier values being tried before later ones. :return: A new :class:`~can.BitTiming` or :class:`~can.BitTimingFd` instance with an *f_clock* value within *valid_clocks*. :raises ~can.exceptions.CanInitializationError: If no compatible *f_clock* value can be found within *valid_clocks*. """ if timing.f_clock in valid_clocks: # create a copy so this function always returns a new instance return copy.deepcopy(timing) for clock in valid_clocks: try: # Try to use a different f_clock adjusted_timing = timing.recreate_with_f_clock(clock) warnings.warn( f"Adjusted f_clock in {timing.__class__.__name__} from " f"{timing.f_clock} to {adjusted_timing.f_clock}", stacklevel=2, ) return adjusted_timing except ValueError: pass raise CanInitializationError( f"The specified timing.f_clock value {timing.f_clock} " f"doesn't match any of the allowed device f_clock values: " f"{', '.join([str(f) for f in valid_clocks])}" ) from None def time_perfcounter_correlation() -> Tuple[float, float]: """Get the `perf_counter` value nearest to when time.time() is updated Computed if the default timer used by `time.time` on this platform has a resolution higher than 10μs, otherwise the current time and perf_counter is directly returned. This was chosen as typical timer resolution on Linux/macOS is ~1μs, and the Windows platform can vary from ~500μs to 10ms. Note this value is based on when `time.time()` is observed to update from Python, it is not directly returned by the operating system. :returns: (t, performance_counter) time.time value and time.perf_counter value when the time.time is updated """ # use this if the resolution is higher than 10us if get_clock_info("time").resolution > 1e-5: t0 = time() while True: t1, performance_counter = time(), perf_counter() if t1 != t0: break else: return time(), perf_counter() return t1, performance_counter def cast_from_string(string_val: str) -> Union[str, int, float, bool]: """Perform trivial type conversion from :class:`str` values. :param string_val: the string, that shall be converted """ if re.match(r"^[-+]?\d+$", string_val): # value is integer return int(string_val) if re.match(r"^[-+]?\d*\.\d+(?:e[-+]?\d+)?$", string_val): # value is float return float(string_val) if re.match(r"^(?:True|False)$", string_val, re.IGNORECASE): # value is bool return string_val.lower() == "true" # value is string return string_val python-can-4.5.0/can/viewer.py000066400000000000000000000534341472200326600162270ustar00rootroot00000000000000# Copyright (C) 2018 Kristian Sloth Lauszus. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # Contact information # ------------------- # Kristian Sloth Lauszus # Web : http://www.lauszus.com # e-mail : lauszus@gmail.com import argparse import errno import logging 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.typechecking import TAdditionalCliArgs, TDataStructs logger = logging.getLogger("can.viewer") try: import curses from curses.ascii import ESC as KEY_ESC # type: ignore[attr-defined,unused-ignore] from curses.ascii import SP as KEY_SPACE # type: ignore[attr-defined,unused-ignore] except ImportError: # Probably on Windows while windows-curses is not installed (e.g. in PyPy) logger.warning( "You won't be able to use the viewer program without curses installed!" ) curses = None # type: ignore[assignment] class CanViewer: # pylint: disable=too-many-instance-attributes def __init__(self, stdscr, bus, data_structs, testing=False): self.stdscr = stdscr self.bus = bus self.data_structs = data_structs # Initialise the ID dictionary, Previous values dict, start timestamp, # scroll and variables for pausing the viewer and enabling byte highlighting self.ids = {} self.start_time = None self.scroll = 0 self.paused = False self.highlight_changed_bytes = False self.previous_values = {} # Get the window dimensions - used for resizing the window self.y, self.x = self.stdscr.getmaxyx() # Do not wait for key inputs, disable the cursor and choose the background color # automatically self.stdscr.nodelay(True) curses.curs_set(0) curses.use_default_colors() # Used to color error frames red curses.init_pair(1, curses.COLOR_RED, -1) # Used to color changed bytes curses.init_pair(2, curses.COLOR_CYAN, curses.COLOR_BLUE) if not testing: # pragma: no cover self.run() def run(self): # Clear the terminal and draw the header self.draw_header() while True: # Do not read the CAN-Bus when in paused mode if not self.paused: # Read the CAN-Bus and draw it in the terminal window msg = self.bus.recv(timeout=1.0 / 1000.0) if msg is not None: self.draw_can_bus_message(msg) else: # Sleep 1 ms, so the application does not use 100 % of the CPU resources time.sleep(1.0 / 1000.0) # Read the terminal input key = self.stdscr.getch() # Stop program if the user presses ESC or 'q' if key == KEY_ESC or key == ord("q"): break # Clear by pressing 'c' if key == ord("c"): self.ids = {} self.start_time = None self.scroll = 0 self.draw_header() # Toggle byte change highlighting pressing 'h' elif key == ord("h"): self.highlight_changed_bytes = not self.highlight_changed_bytes if not self.highlight_changed_bytes: # empty the previous values dict when leaving higlighting mode self.previous_values.clear() self.draw_header() # Sort by pressing 's' elif key == ord("s"): # Sort frames based on the CAN-Bus ID self.draw_header() for i, key in enumerate(sorted(self.ids.keys())): # Set the new row index, but skip the header self.ids[key]["row"] = i + 1 # Do a recursive call, so the frames are repositioned self.draw_can_bus_message(self.ids[key]["msg"], sorting=True) # Pause by pressing space elif key == KEY_SPACE: self.paused = not self.paused # Scroll by pressing up/down elif key == curses.KEY_UP: # Limit scrolling, so the user do not scroll passed the header if self.scroll > 0: self.scroll -= 1 self.redraw_screen() elif key == curses.KEY_DOWN: # Limit scrolling, so the maximum scrolling position is one below the last line if self.scroll <= len(self.ids) - self.y + 1: self.scroll += 1 self.redraw_screen() # Check if screen was resized resized = curses.is_term_resized(self.y, self.x) if resized is True: self.y, self.x = self.stdscr.getmaxyx() if hasattr(curses, "resizeterm"): # pragma: no cover curses.resizeterm(self.y, self.x) self.redraw_screen() # Shutdown the CAN-Bus interface self.bus.shutdown() # Unpack the data and then convert it into SI-units @staticmethod def unpack_data(cmd: int, cmd_to_struct: Dict, 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: 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] # The conversion from raw values to SI-units are given in the rest of the tuple values = [ d // val if isinstance(val, int) else float(d) / val for d, val in zip(struct_t.unpack(data), value[1:]) ] else: # No conversion from SI-units is needed as_struct_t: struct.Struct = value values = list(as_struct_t.unpack(data)) return values raise ValueError(f"Unknown command: 0x{cmd:02X}") def draw_can_bus_message(self, msg, sorting=False): # Use the CAN-Bus ID as the key in the dict key = msg.arbitration_id # Sort the extended IDs at the bottom by setting the 32-bit high if msg.is_extended_id: key |= 1 << 32 new_id_added, length_changed = False, False if not sorting: # Check if it is a new message or if the length is not the same if key not in self.ids: new_id_added = True # Set the start time when the first message has been received if not self.start_time: self.start_time = msg.timestamp elif msg.dlc != self.ids[key]["msg"].dlc: length_changed = True # Clear the old data bytes when the length of the new message is shorter if msg.dlc < self.ids[key]["msg"].dlc: self.draw_line( self.ids[key]["row"], # Start drawing at the last byte that is not in the new message 52 + msg.dlc * 3, # Draw spaces where the old bytes were drawn " " * ((self.ids[key]["msg"].dlc - msg.dlc) * 3 - 1), ) if new_id_added or length_changed: # Increment the index if it was just added, but keep it if the length just changed row = len(self.ids) + 1 if new_id_added else self.ids[key]["row"] # It's a new message ID or the length has changed, so add it to the dict # The first index is the row index, the second is the frame counter, # the third is a copy of the CAN-Bus frame # and the forth index is the time since the previous message self.ids[key] = {"row": row, "count": 0, "msg": msg, "dt": 0} else: # Calculate the time since the last message and save the timestamp self.ids[key]["dt"] = msg.timestamp - self.ids[key]["msg"].timestamp # Copy the CAN-Bus frame - this is used for sorting self.ids[key]["msg"] = msg # Increment frame counter self.ids[key]["count"] += 1 # Format the CAN-Bus ID as a hex value arbitration_id_string = ( "0x{0:0{1}X}".format( # pylint: disable=consider-using-f-string msg.arbitration_id, 8 if msg.is_extended_id else 3, ) ) # Use red for error frames if msg.is_error_frame: color = curses.color_pair(1) else: color = curses.color_pair(0) # Now draw the CAN-Bus message on the terminal window self.draw_line(self.ids[key]["row"], 0, str(self.ids[key]["count"]), color) self.draw_line( self.ids[key]["row"], 8, f"{self.ids[key]['msg'].timestamp - self.start_time:.6f}", color, ) self.draw_line(self.ids[key]["row"], 23, f"{self.ids[key]['dt']:.6f}", color) self.draw_line(self.ids[key]["row"], 35, arbitration_id_string, color) self.draw_line(self.ids[key]["row"], 47, str(msg.dlc), color) try: previous_byte_values = self.previous_values[key] except KeyError: # no row of previous values exists for the current message ID # initialise a row to store the values for comparison next time self.previous_values[key] = {} previous_byte_values = self.previous_values[key] for i, b in enumerate(msg.data): col = 52 + i * 3 if col > self.x - 2: # Data does not fit self.draw_line(self.ids[key]["row"], col - 4, "...", color) break if self.highlight_changed_bytes: try: if b != previous_byte_values[i]: # set colour to highlight a changed value data_color = curses.color_pair(2) else: data_color = color except KeyError: # previous entry for byte didn't exist - default to rest of line colour data_color = color finally: # write the new value to the previous values dict for next time previous_byte_values[i] = b else: data_color = color text = f"{b:02X}" self.draw_line(self.ids[key]["row"], col, text, data_color) if self.data_structs: try: values_list = [] for x in self.unpack_data( msg.arbitration_id, self.data_structs, msg.data ): if isinstance(x, float): values_list.append(f"{x:.6f}") else: values_list.append(str(x)) values_string = " ".join(values_list) self.ids[key]["values_string_length"] = len(values_string) values_string += " " * (self.x - len(values_string)) self.draw_line(self.ids[key]["row"], 77, values_string, color) except (ValueError, struct.error): pass return self.ids[key] def draw_line(self, row, col, txt, *args): if row - self.scroll < 0: # Skip if we have scrolled past the line return try: self.stdscr.addstr(row - self.scroll, col, txt, *args) except curses.error: # Ignore if we are trying to write outside the window # This happens if the terminal window is too small pass def draw_header(self): self.stdscr.erase() self.draw_line(0, 0, "Count", curses.A_BOLD) self.draw_line(0, 8, "Time", curses.A_BOLD) self.draw_line(0, 23, "dt", curses.A_BOLD) self.draw_line(0, 35, "ID", curses.A_BOLD) self.draw_line(0, 47, "DLC", curses.A_BOLD) self.draw_line(0, 52, "Data", curses.A_BOLD) # Indicate that byte change highlighting is enabled if self.highlight_changed_bytes: self.draw_line(0, 57, "(changed)", curses.color_pair(2)) # Only draw if the dictionary is not empty if self.data_structs: self.draw_line(0, 77, "Parsed values", curses.A_BOLD) def redraw_screen(self): # Trigger a complete redraw self.draw_header() for ids in self.ids.values(): self.draw_can_bus_message(ids["msg"]) class SmartFormatter(argparse.HelpFormatter): def _get_default_metavar_for_optional(self, action): return action.dest.upper() def _format_usage(self, usage, actions, groups, prefix): # Use uppercase for "Usage:" text return super()._format_usage(usage, actions, groups, "Usage: ") def _format_args(self, action, default_metavar): if action.nargs not in (argparse.REMAINDER, argparse.ONE_OR_MORE): return super()._format_args(action, default_metavar) # Use the metavar if "REMAINDER" or "ONE_OR_MORE" is set get_metavar = self._metavar_formatter(action, default_metavar) return str(get_metavar(1)) def _format_action_invocation(self, action): if not action.option_strings or action.nargs == 0: return super()._format_action_invocation(action) # Modified so "-s ARGS, --long ARGS" is replaced with "-s, --long ARGS" else: parts = [] default = self._get_default_metavar_for_optional(action) args_string = self._format_args(action, default) for i, option_string in enumerate(action.option_strings): if i == len(action.option_strings) - 1: parts.append(f"{option_string} {args_string}") else: parts.append(str(option_string)) return ", ".join(parts) def _split_lines(self, text, width): # Allow to manually split the lines if text.startswith("R|"): return text[2:].splitlines() return super()._split_lines(text, width) def _fill_text(self, text, width, indent): if text.startswith("R|"): return "".join(indent + line + "\n" for line in text[2:].splitlines()) else: return super()._fill_text(text, width, indent) def _parse_viewer_args( args: List[str], ) -> Tuple[argparse.Namespace, TDataStructs, TAdditionalCliArgs]: # Parse command line arguments parser = argparse.ArgumentParser( "python -m can.viewer", description="A simple CAN viewer terminal application written in Python", epilog="R|Shortcuts: " "\n +---------+-------------------------------+" "\n | Key | Description |" "\n +---------+-------------------------------+" "\n | ESQ/q | Exit the viewer |" "\n | c | Clear the stored frames |" "\n | s | Sort the stored frames |" "\n | h | Toggle highlight byte changes |" "\n | SPACE | Pause the viewer |" "\n | UP/DOWN | Scroll the viewer |" "\n +---------+-------------------------------+", formatter_class=SmartFormatter, add_help=False, allow_abbrev=False, ) # Generate the standard arguments: # Channel, bitrate, data_bitrate, interface, app_name, CAN-FD support _create_base_argument_parser(parser) optional = parser.add_argument_group("Optional arguments") optional.add_argument( "-h", "--help", action="help", help="Show this help message and exit" ) optional.add_argument( "--version", action="version", help="Show program's version number and exit", version=f"%(prog)s (version {__version__})", ) optional.add_argument( "-d", "--decode", dest="decode", help="R|Specify how to convert the raw bytes into real values." "\nThe ID of the frame is given as the first argument and the format as the second." "\nThe Python struct package is used to unpack the received data" "\nwhere the format characters have the following meaning:" "\n < = little-endian, > = big-endian" "\n x = pad byte" "\n c = char" "\n ? = bool" "\n b = int8_t, B = uint8_t" "\n h = int16, H = uint16" "\n l = int32_t, L = uint32_t" "\n q = int64_t, Q = uint64_t" "\n f = float (32-bits), d = double (64-bits)" "\nFx to convert six bytes with ID 0x100 into uint8_t, uint16 and uint32_t:" '\n $ python -m can.viewer -d "100: = big-endian # x = pad byte # c = char # ? = bool # b = int8_t, B = uint8_t # h = int16, H = uint16 # l = int32_t, L = uint32_t # q = int64_t, Q = uint64_t # f = float (32-bits), d = double (64-bits) # # An optional conversion from real units to integers can be given as additional arguments. # In order to convert from raw integer value the real units are multiplied with the values and # similarly the values # are divided by the value in order to convert from real units to raw integer values. data_structs: TDataStructs = {} if parsed_args.decode: if os.path.isfile(parsed_args.decode[0]): with open(parsed_args.decode[0], encoding="utf-8") as f: structs = f.readlines() else: structs = parsed_args.decode for s in structs: tmp = s.rstrip("\n").split(":") # The ID is given as a hex value, the format needs no conversion key, fmt = int(tmp[0], base=16), tmp[1] # The scaling scaling: List[float] = [] for t in tmp[2:]: # First try to convert to int, if that fails, then convert to a float try: scaling.append(int(t)) except ValueError: scaling.append(float(t)) if scaling: data_structs[key] = (struct.Struct(fmt), *scaling) 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 def main() -> None: parsed_args, data_structs, additional_config = _parse_viewer_args(sys.argv[1:]) bus = _create_bus(parsed_args, **additional_config) curses.wrapper(CanViewer, bus, data_structs) # type: ignore[attr-defined,unused-ignore] if __name__ == "__main__": # Catch ctrl+c try: main() except KeyboardInterrupt: pass python-can-4.5.0/doc/000077500000000000000000000000001472200326600143475ustar00rootroot00000000000000python-can-4.5.0/doc/api.rst000066400000000000000000000005751472200326600156610ustar00rootroot00000000000000Library API =========== The main objects are the :class:`~can.Bus` and the :class:`~can.Message`. A form of CAN interface is also required. .. hint:: Check the backend specific documentation for any implementation specific details. .. toctree:: :maxdepth: 2 bus message notifier file_io asyncio bcm errors bit_timing utils internal-api python-can-4.5.0/doc/asyncio.rst000066400000000000000000000015151472200326600165500ustar00rootroot00000000000000.. _asyncio: Asyncio support =============== The :mod:`asyncio` module built into Python 3.4 and later can be used to write asynchronous code in a single thread. This library supports receiving messages asynchronously in an event loop using the :class:`can.Notifier` class. There will still be one thread per CAN bus but the user application will execute entirely in the event loop, allowing simpler concurrency without worrying about threading issues. Interfaces that have a valid file descriptor will however be supported natively without a thread. You can also use the :class:`can.AsyncBufferedReader` listener if you prefer to write coroutine based code instead of using callbacks. Example ------- Here is an example using both callback and coroutine based code: .. literalinclude:: ../examples/asyncio_demo.py :language: python python-can-4.5.0/doc/bcm.rst000066400000000000000000000023331472200326600156430ustar00rootroot00000000000000.. _bcm: Broadcast Manager ================= .. module:: can.broadcastmanager The broadcast manager allows the user to setup periodic message jobs. For example sending a particular message at a given period. The broadcast manager supported natively by several interfaces and a software thread based scheduler is used as a fallback. This example shows the socketcan backend using the broadcast manager: .. literalinclude:: ../examples/cyclic.py :language: python :linenos: Message Sending Tasks ~~~~~~~~~~~~~~~~~~~~~ The class based api for the broadcast manager uses a series of `mixin classes `_. All mixins inherit from :class:`~can.broadcastmanager.CyclicSendTaskABC` which inherits from :class:`~can.broadcastmanager.CyclicTask`. .. autoclass:: can.broadcastmanager.CyclicTask :members: .. autoclass:: can.broadcastmanager.CyclicSendTaskABC :members: .. autoclass:: LimitedDurationCyclicSendTaskABC :members: .. autoclass:: MultiRateCyclicSendTaskABC :members: .. autoclass:: can.ModifiableCyclicTaskABC :members: .. autoclass:: can.RestartableCyclicTaskABC :members: .. autoclass:: can.broadcastmanager.ThreadBasedCyclicSendTask :members: python-can-4.5.0/doc/bit_timing.rst000066400000000000000000000120151472200326600172250ustar00rootroot00000000000000Bit Timing Configuration ======================== The CAN protocol, specified in ISO 11898, allows the bitrate, sample point and number of samples to be optimized for a given application. These parameters, known as bit timings, can be adjusted to meet the requirements of the communication system and the physical communication channel. These parameters include: * **tseg1**: The time segment 1 (TSEG1) is the amount of time from the end of the sync segment until the sample point. It is expressed in time quanta (TQ). * **tseg2**: The time segment 2 (TSEG2) is the amount of time from the sample point until the end of the bit. It is expressed in TQ. * **sjw**: The synchronization jump width (SJW) is the maximum number of TQ that the controller can resynchronize every bit. * **sample point**: The sample point is defined as the point in time within a bit where the bus controller samples the bus for dominant or recessive levels. It is typically expressed as a percentage of the bit time. The sample point depends on the bus length and propagation time as well as the information processing time of the nodes. .. figure:: images/bit_timing_light.svg :align: center :class: only-light .. figure:: images/bit_timing_dark.svg :align: center :class: only-dark Bit Timing and Sample Point For example, consider a bit with a total duration of 8 TQ and a sample point at 75%. The values for TSEG1, TSEG2 and SJW would be 5, 2, and 2, respectively. The sample point would be 6 TQ after the start of the bit, leaving 2 TQ for the information processing by the bus nodes. .. note:: The values for TSEG1, TSEG2 and SJW are chosen such that the sample point is at least 50% of the total bit time. This ensures that there is sufficient time for the signal to stabilize before it is sampled. .. note:: In CAN FD, the arbitration (nominal) phase and the data phase can have different bit rates. As a result, there are two separate sample points to consider. Another important parameter is **f_clock**: The CAN system clock frequency in Hz. This frequency is used to derive the TQ size from the bit rate. The relationship is ``f_clock = (tseg1+tseg2+1) * bitrate * brp``. The bit rate prescaler value **brp** is usually determined by the controller and is chosen to ensure that the resulting bit time is an integer value. Typical CAN clock frequencies are 8-80 MHz. In most cases, the recommended settings for a predefined set of common bit rates will work just fine. In some cases, however, it may be necessary to specify custom bit timings. The :class:`~can.BitTiming` and :class:`~can.BitTimingFd` classes can be used for this purpose to specify bit timings in a relatively interface agnostic manner. :class:`~can.BitTiming` or :class:`~can.BitTimingFd` can also help you to produce an overview of possible bit timings for your desired bit rate: >>> import contextlib >>> import can ... >>> timings = set() >>> for sample_point in range(50, 100): ... with contextlib.suppress(ValueError): ... timings.add( ... can.BitTiming.from_sample_point( ... f_clock=8_000_000, ... bitrate=250_000, ... sample_point=sample_point, ... ) ... ) ... >>> for timing in sorted(timings, key=lambda x: x.sample_point): ... print(timing) BR: 250_000 bit/s, SP: 50.00%, BRP: 2, TSEG1: 7, TSEG2: 8, SJW: 4, BTR: C176h, CLK: 8MHz BR: 250_000 bit/s, SP: 56.25%, BRP: 2, TSEG1: 8, TSEG2: 7, SJW: 4, BTR: C167h, CLK: 8MHz BR: 250_000 bit/s, SP: 62.50%, BRP: 2, TSEG1: 9, TSEG2: 6, SJW: 4, BTR: C158h, CLK: 8MHz BR: 250_000 bit/s, SP: 68.75%, BRP: 2, TSEG1: 10, TSEG2: 5, SJW: 4, BTR: C149h, CLK: 8MHz BR: 250_000 bit/s, SP: 75.00%, BRP: 2, TSEG1: 11, TSEG2: 4, SJW: 4, BTR: C13Ah, CLK: 8MHz BR: 250_000 bit/s, SP: 81.25%, BRP: 2, TSEG1: 12, TSEG2: 3, SJW: 3, BTR: 812Bh, CLK: 8MHz BR: 250_000 bit/s, SP: 87.50%, BRP: 2, TSEG1: 13, TSEG2: 2, SJW: 2, BTR: 411Ch, CLK: 8MHz BR: 250_000 bit/s, SP: 93.75%, BRP: 2, TSEG1: 14, TSEG2: 1, SJW: 1, BTR: 010Dh, CLK: 8MHz It is possible to specify CAN 2.0 bit timings using the config file: .. code-block:: none [default] f_clock=8000000 brp=1 tseg1=5 tseg2=2 sjw=1 nof_samples=1 The same is possible for CAN FD: .. code-block:: none [default] f_clock=80000000 nom_brp=1 nom_tseg1=119 nom_tseg2=40 nom_sjw=40 data_brp=1 data_tseg1=29 data_tseg2=10 data_sjw=10 A :class:`dict` of the relevant config parameters can be easily obtained by calling ``dict(timing)`` or ``{**timing}`` where ``timing`` is the :class:`~can.BitTiming` or :class:`~can.BitTimingFd` instance. Check :doc:`configuration` for more information about saving and loading configurations. .. autoclass:: can.BitTiming :class-doc-from: both :show-inheritance: :members: :member-order: bysource .. autoclass:: can.BitTimingFd :class-doc-from: both :show-inheritance: :members: :member-order: bysource python-can-4.5.0/doc/bus.rst000066400000000000000000000060051472200326600156730ustar00rootroot00000000000000.. _bus: Bus --- The :class:`~can.BusABC` class provides a wrapper around a physical or virtual CAN Bus. An interface specific instance is created by calling the :func:`~can.Bus` function with a particular ``interface``, for example:: vector_bus = can.Bus(interface='vector', ...) The created bus is then able to handle the interface specific software/hardware interactions while giving the user the same top level API. A thread safe bus wrapper is also available, see `Thread safe bus`_. Transmitting '''''''''''' Writing individual messages to the bus is done by calling the :meth:`~can.BusABC.send` method and passing a :class:`~can.Message` instance. .. code-block:: python :emphasize-lines: 8 with can.Bus() as bus: msg = can.Message( arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True ) try: bus.send(msg) print(f"Message sent on {bus.channel_info}") except can.CanError: print("Message NOT sent") Periodic sending is controlled by the :ref:`broadcast manager `. Receiving ''''''''' Reading from the bus is achieved by either calling the :meth:`~can.BusABC.recv` method or by directly iterating over the bus:: with can.Bus() as bus: for msg in bus: print(msg.data) Alternatively the :ref:`listeners_doc` api can be used, which is a list of various :class:`~can.Listener` implementations that receive and handle messages from a :class:`~can.Notifier`. Filtering ''''''''' Message filtering can be set up for each bus. Where the interface supports it, this is carried out in the hardware or kernel layer - not in Python. All messages that match at least one filter are returned. Example defining two filters, one to pass 11-bit ID ``0x451``, the other to pass 29-bit ID ``0xA0000``: .. code-block:: python filters = [ {"can_id": 0x451, "can_mask": 0x7FF, "extended": False}, {"can_id": 0xA0000, "can_mask": 0x1FFFFFFF, "extended": True}, ] bus = can.interface.Bus(channel="can0", interface="socketcan", can_filters=filters) See :meth:`~can.BusABC.set_filters` for the implementation. Bus API ''''''' .. autofunction:: can.Bus .. autoclass:: can.BusABC :class-doc-from: class :members: :inherited-members: .. autoclass:: can.BusState :members: :undoc-members: .. autoclass:: can.bus.CanProtocol :members: :undoc-members: Thread safe bus ''''''''''''''' This thread safe version of the :class:`~can.BusABC` class can be used by multiple threads at once. Sending and receiving is locked separately to avoid unnecessary delays. Conflicting calls are executed by blocking until the bus is accessible. It can be used exactly like the normal :class:`~can.BusABC`: .. code-block:: python # 'socketcan' is only an example interface, it works with all the others too my_bus = can.ThreadSafeBus(interface='socketcan', channel='vcan0') my_bus.send(...) my_bus.recv(...) .. autoclass:: can.ThreadSafeBus :members: python-can-4.5.0/doc/conf.py000077500000000000000000000161431472200326600156560ustar00rootroot00000000000000""" python-can documentation build configuration file This file is execfile()d with the current directory set to its containing dir. """ # -- Imports ------------------------------------------------------------------- import ctypes import os import sys from importlib.metadata import version as get_version from unittest.mock import MagicMock # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) from can import ctypesutil # pylint: disable=wrong-import-position # -- General configuration ----------------------------------------------------- # pylint: disable=invalid-name # The version info for the project, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The full version, including alpha/beta/rc tags. release: str = get_version("python-can") # The short X.Y version. version = ".".join(release.split(".")[:2]) # General information about the project. project = "python-can" primary_domain = "py" # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.doctest", "sphinx.ext.extlinks", "sphinx.ext.todo", "sphinx.ext.intersphinx", "sphinx.ext.coverage", "sphinx.ext.viewcode", "sphinx.ext.graphviz", "sphinxcontrib.programoutput", "sphinx_inline_tabs", "sphinx_copybutton", ] # Now, you can use the alias name as a new role, e.g. :issue:`123`. extlinks = {"issue": ("https://github.com/hardbyte/python-can/issues/%s/", "issue #%s")} intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} # If this is True, todo and todolist produce output, else they produce nothing. # The default is False. todo_include_todos = True # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] graphviz_output_format = "png" # 'svg' # The suffix of source filenames. source_suffix = {".rst": "restructuredtext"} # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # Include documentation from both the class level and __init__ autoclass_content = "both" # The default autodoc directive flags autodoc_default_flags = ["members", "show-inheritance"] # Keep cached intersphinx inventories indefinitely intersphinx_cache_limit = -1 # location of typehints autodoc_typehints = "description" # disable specific warnings nitpick_ignore = [ # Ignore warnings for type aliases. Remove once Sphinx supports PEP613 ("py:class", "BusConfig"), ("py:class", "can.typechecking.BusConfig"), ("py:class", "can.typechecking.CanFilter"), ("py:class", "can.typechecking.CanFilterExtended"), ("py:class", "can.typechecking.AutoDetectedConfig"), ("py:class", "can.util.T1"), ("py:class", "can.util.T2"), ("py:class", "~P1"), # intersphinx fails to reference some builtins ("py:class", "asyncio.events.AbstractEventLoop"), ("py:class", "_thread.allocate_lock"), ] # mock windows specific attributes autodoc_mock_imports = ["win32com"] ctypes.windll = MagicMock() ctypesutil.HRESULT = ctypes.c_long # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "furo" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "python-can" python-can-4.5.0/doc/configuration.rst000066400000000000000000000127301472200326600177530ustar00rootroot00000000000000.. _configuration: Configuration ============= Usually this library is used with a particular CAN interface, this can be specified in code, read from configuration files or environment variables. See :func:`can.util.load_config` for implementation. In Code ------- The ``can`` object exposes an ``rc`` dictionary which can be used to set the **interface** and **channel**. :: import can can.rc['interface'] = 'socketcan' can.rc['channel'] = 'vcan0' can.rc['bitrate'] = 500000 from can.interface import Bus bus = Bus() You can also specify the interface and channel for each Bus instance:: import can bus = can.interface.Bus(interface='socketcan', channel='vcan0', bitrate=500000) Configuration File ------------------ On Linux and macOS systems the config file is searched in the following paths: #. ``~/can.conf`` #. ``/etc/can.conf`` #. ``$HOME/.can`` #. ``$HOME/.canrc`` On Windows systems the config file is searched in the following paths: #. ``%USERPROFILE%/can.conf`` #. ``can.ini`` (current working directory) #. ``%APPDATA%/can.ini`` The configuration file sets the default interface and channel: :: [default] interface = channel = bitrate = The configuration can also contain additional sections (or context): :: [default] interface = channel = bitrate = [HS] # All the values from the 'default' section are inherited channel = bitrate = [MS] # All the values from the 'default' section are inherited channel = bitrate = :: from can.interface import Bus hs_bus = Bus(config_context='HS') ms_bus = Bus(config_context='MS') Environment Variables --------------------- Configuration can be pulled from these environmental variables: * CAN_INTERFACE * CAN_CHANNEL * CAN_BITRATE * CAN_CONFIG The ``CAN_CONFIG`` environment variable allows to set any bus configuration using JSON. For example: ``CAN_INTERFACE=socketcan CAN_CONFIG={"receive_own_messages": true, "fd": true}`` .. _interface names: Interface Names --------------- Lookup table of interface names: +---------------------+-------------------------------------+ | Name | Documentation | +=====================+=====================================+ | ``"canalystii"`` | :doc:`interfaces/canalystii` | +---------------------+-------------------------------------+ | ``"cantact"`` | :doc:`interfaces/cantact` | +---------------------+-------------------------------------+ | ``"etas"`` | :doc:`interfaces/etas` | +---------------------+-------------------------------------+ | ``"gs_usb"`` | :doc:`interfaces/gs_usb` | +---------------------+-------------------------------------+ | ``"iscan"`` | :doc:`interfaces/iscan` | +---------------------+-------------------------------------+ | ``"ixxat"`` | :doc:`interfaces/ixxat` | +---------------------+-------------------------------------+ | ``"kvaser"`` | :doc:`interfaces/kvaser` | +---------------------+-------------------------------------+ | ``"neousys"`` | :doc:`interfaces/neousys` | +---------------------+-------------------------------------+ | ``"neovi"`` | :doc:`interfaces/neovi` | +---------------------+-------------------------------------+ | ``"nican"`` | :doc:`interfaces/nican` | +---------------------+-------------------------------------+ | ``"nixnet"`` | :doc:`interfaces/nixnet` | +---------------------+-------------------------------------+ | ``"pcan"`` | :doc:`interfaces/pcan` | +---------------------+-------------------------------------+ | ``"robotell"`` | :doc:`interfaces/robotell` | +---------------------+-------------------------------------+ | ``"seeedstudio"`` | :doc:`interfaces/seeedstudio` | +---------------------+-------------------------------------+ | ``"serial"`` | :doc:`interfaces/serial` | +---------------------+-------------------------------------+ | ``"slcan"`` | :doc:`interfaces/slcan` | +---------------------+-------------------------------------+ | ``"socketcan"`` | :doc:`interfaces/socketcan` | +---------------------+-------------------------------------+ | ``"socketcand"`` | :doc:`interfaces/socketcand` | +---------------------+-------------------------------------+ | ``"systec"`` | :doc:`interfaces/systec` | +---------------------+-------------------------------------+ | ``"udp_multicast"`` | :doc:`interfaces/udp_multicast` | +---------------------+-------------------------------------+ | ``"usb2can"`` | :doc:`interfaces/usb2can` | +---------------------+-------------------------------------+ | ``"vector"`` | :doc:`interfaces/vector` | +---------------------+-------------------------------------+ | ``"virtual"`` | :doc:`interfaces/virtual` | +---------------------+-------------------------------------+ Additional interface types can be added via the :ref:`plugin interface`. python-can-4.5.0/doc/development.rst000066400000000000000000000123101472200326600174200ustar00rootroot00000000000000Developer's Overview ==================== Contributing ------------ Contribute to source code, documentation, examples and report issues: https://github.com/hardbyte/python-can Note that the latest released version on PyPi may be significantly behind the ``main`` branch. Please open any feature requests against the ``main`` branch There is also a `python-can `__ mailing list for development discussion. 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. Pre-releases ------------ The latest pre-release can be installed with:: pip install --upgrade --pre python-can Building & Installing --------------------- The following assumes that the commands are executed from the root of the repository: The project can be built with:: pipx run build pipx run twine check dist/* The project can be installed in editable mode with:: pip install -e . The unit tests can be run with:: pipx run tox -e py The documentation can be built with:: pipx run tox -e docs The linters can be run with:: 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 Creating a new interface/backend -------------------------------- These steps are a guideline on how to add a new backend to python-can. - Create a module (either a ``*.py`` or an entire subdirectory depending on the complexity) inside ``can.interfaces`` - Implement the central part of the backend: the bus class that extends :class:`can.BusABC`. See :ref:`businternals` for more info on this one! - Register your backend bus class in ``BACKENDS`` in the file ``can.interfaces.__init__.py``. - Add docs where appropriate. At a minimum add to ``doc/interfaces.rst`` and add a new interface specific document in ``doc/interface/*``. It should document the supported platforms and also the hardware/software it requires. A small snippet of how to install the dependencies would also be useful to get people started without much friction. - Also, don't forget to document your classes, methods and function with docstrings. - Add tests in ``test/*`` where appropriate. 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. .. 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. Code Structure -------------- The modules in ``python-can`` are: +---------------------------------+------------------------------------------------------+ |Module | Description | +=================================+======================================================+ |:doc:`interfaces ` | Contains interface dependent code. | +---------------------------------+------------------------------------------------------+ |:doc:`bus ` | Contains the interface independent Bus object. | +---------------------------------+------------------------------------------------------+ |:doc:`message ` | Contains the interface independent Message object. | +---------------------------------+------------------------------------------------------+ |:doc:`io ` | Contains a range of file readers and writers. | +---------------------------------+------------------------------------------------------+ |:doc:`broadcastmanager ` | Contains interface independent broadcast manager | | | code. | +---------------------------------+------------------------------------------------------+ Creating a new Release ---------------------- - 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 `__, `Read the Docs `__ and `GitHub `__. Manual release steps (deprecated) --------------------------------- - Create a temporary virtual environment. - Create a new tag in the repository. Use `semantic versioning `__. - 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*``. python-can-4.5.0/doc/doc-requirements.txt000066400000000000000000000001241472200326600203730ustar00rootroot00000000000000sphinx>=5.2.3 sphinxcontrib-programoutput sphinx-inline-tabs sphinx-copybutton furo python-can-4.5.0/doc/errors.rst000066400000000000000000000001601472200326600164120ustar00rootroot00000000000000.. _errors: Error Handling ============== .. automodule:: can.exceptions :members: :show-inheritance: python-can-4.5.0/doc/file_io.rst000066400000000000000000000125201472200326600165070ustar00rootroot00000000000000File IO ======= Reading and Writing Files ------------------------- .. autofunction:: can.LogReader .. autofunction:: can.Logger .. autodata:: can.io.logger.MESSAGE_WRITERS .. autodata:: can.io.player.MESSAGE_READERS Printer ------- .. autoclass:: can.Printer :show-inheritance: :members: CSVWriter --------- .. autoclass:: can.CSVWriter :show-inheritance: :members: .. autoclass:: can.CSVReader :show-inheritance: :members: SqliteWriter ------------ .. autoclass:: can.SqliteWriter :show-inheritance: :members: .. autoclass:: can.SqliteReader :show-inheritance: :members: Database table format ~~~~~~~~~~~~~~~~~~~~~ The messages are written to the table ``messages`` in the sqlite database by default. The table is created if it does not already exist. The entries are as follows: ============== ============== ============== Name Data type Note -------------- -------------- -------------- ts REAL The timestamp of the message arbitration_id INTEGER The arbitration id, might use the extended format extended INTEGER ``1`` if the arbitration id uses the extended format, else ``0`` remote INTEGER ``1`` if the message is a remote frame, else ``0`` error INTEGER ``1`` if the message is an error frame, else ``0`` dlc INTEGER The data length code (DLC) data BLOB The content of the message ============== ============== ============== ASC (.asc Logging format) ------------------------- ASCWriter logs CAN data to an ASCII log file compatible with other CAN tools such as Vector CANalyzer/CANoe and other. Since no official specification exists for the format, it has been reverse- engineered from existing log files. One description of the format can be found `here `_. .. note:: Channels will be converted to integers. .. autoclass:: can.ASCWriter :show-inheritance: :members: ASCReader reads CAN data from ASCII log files .asc, as further references can-utils can be used: `asc2log `_, `log2asc `_. .. autoclass:: can.ASCReader :show-inheritance: :members: Log (.log can-utils Logging format) ----------------------------------- CanutilsLogWriter logs CAN data to an ASCII log file compatible with `can-utils ` As specification following references can-utils can be used: `asc2log `_, `log2asc `_. .. autoclass:: can.CanutilsLogWriter :show-inheritance: :members: **CanutilsLogReader** reads CAN data from ASCII log files .log .. autoclass:: can.CanutilsLogReader :show-inheritance: :members: BLF (Binary Logging Format) --------------------------- Implements support for BLF (Binary Logging Format) which is a proprietary CAN log format from Vector Informatik GmbH. The data is stored in a compressed format which makes it very compact. .. note:: Channels will be converted to integers. .. autoclass:: can.BLFWriter :show-inheritance: :members: The following class can be used to read messages from BLF file: .. autoclass:: can.BLFReader :show-inheritance: :members: MF4 (Measurement Data Format v4) -------------------------------- Implements support for MF4 (Measurement Data Format v4) which is a proprietary format from ASAM (Association for Standardization of Automation and Measuring Systems), widely used in many automotive software (Vector CANape, ETAS INCA, dSPACE ControlDesk, etc.). The data is stored in a compressed format which makes it compact. .. note:: MF4 support has to be installed as an extra with for example ``pip install python-can[mf4]``. .. note:: Channels will be converted to integers. .. note:: MF4Writer does not suppport the append mode. .. autoclass:: can.MF4Writer :show-inheritance: :members: The MDF format is very flexible regarding the internal structure and it is used to handle data from multiple sources, not just CAN bus logging. MDF4Writer will always create a fixed internal file structure where there will be three channel groups (for standard, error and remote frames). Using this fixed file structure allows for a simple implementation of MDF4Writer and MF4Reader classes. Therefor MF4Reader can only replay files created with MF4Writer. The following class can be used to read messages from MF4 file: .. autoclass:: can.MF4Reader :show-inheritance: :members: TRC ---- Implements basic support for the TRC file format. .. note:: Comments and contributions are welcome on what file versions might be relevant. .. autoclass:: can.TRCWriter :show-inheritance: :members: The following class can be used to read messages from TRC file: .. autoclass:: can.TRCReader :show-inheritance: :members: Rotating Loggers ---------------- .. autoclass:: can.io.BaseRotatingLogger :show-inheritance: :members: .. autoclass:: can.SizedRotatingLogger :show-inheritance: :members: Replaying Files --------------- .. autoclass:: can.MessageSync :members: python-can-4.5.0/doc/history.rst000066400000000000000000000075251472200326600166130ustar00rootroot00000000000000History ======= Background ---------- Originally written at `Dynamic Controls `__ for internal use testing and prototyping wheelchair components. Maintenance was taken over and the project was open sourced by Brian Thorne in 2010. Acknowledgements ---------------- Originally written by Ben Powell as a thin wrapper around the Kvaser SDK to support the leaf device. Support for linux socketcan was added by Rose Lu as a summer coding project in 2011. The socketcan interface was helped immensely by Phil Dixon who wrote a leaf-socketcan driver for Linux. The pcan interface was contributed by Albert Bloomfield in 2013. Support for pcan on Mac was added by Kristian Sloth Lauszus in 2018. The usb2can interface was contributed by Joshua Villyard in 2015. The IXXAT VCI interface was contributed by Giuseppe Corbelli and funded by `Weightpack `__ in 2016. The NI-CAN and virtual interfaces plus the ASCII and BLF loggers were contributed by Christian Sandberg in 2016 and 2017. The BLF format is based on a C++ library by Toby Lorenz. The slcan interface, ASCII listener and log logger and listener were contributed by Eduard Bröcker in 2017. The NeoVi interface for ICS (Intrepid Control Systems) devices was contributed by Pierre-Luc Tessier Gagné in 2017. Many improvements all over the library, cleanups, unifications as well as more comprehensive documentation and CI testing was contributed by Felix Divo in 2017 and 2018. The CAN viewer terminal script was contributed by Kristian Sloth Lauszus in 2018. The CANalyst-II interface was contributed by Shaoyu Meng in 2018. @deonvdw added support for the Robotell interface in 2019. Felix Divo and Karl Ding added type hints for the core library and many interfaces leading up to the 4.0 release. Eric Evenchick added support for the CANtact devices in 2020. Felix Divo added an interprocess virtual bus interface in 2020. @jxltom added the gs_usb interface in 2020 supporting Geschwister Schneider USB/CAN devices and bytewerk.org candleLight USB CAN devices such as candlelight, canable, cantact, etc. @jaesc added the nixnet interface in 2021 supporting NI-XNET devices from National Instruments. Tuukka Pasanen @illuusio added the neousys interface in 2021. Francisco Javier Burgos Maciá @fjburgos added ixxat FD support. @domologic contributed a socketcand interface in 2021. Felix N @felixn contributed the ETAS interface in 2021. Felix Divo unified exception handling across every interface in the lead up to the 4.0 release. Felix Divo prepared the python-can 4.0 release. Support for CAN within Python ----------------------------- Python natively supports the CAN protocol from version 3.3 on, if running on Linux (with a sufficiently new kernel): ============== ============================================================== ==== Python version Feature Link ============== ============================================================== ==== 3.3 Initial SocketCAN support `Docs `__ 3.4 Broadcast Management (BCM) commands are natively supported `Docs `__ 3.5 CAN FD support `Docs `__ 3.7 Support for CAN ISO-TP `Docs `__ 3.9 Native support for joining CAN filters `Docs `__ ============== ============================================================== ==== python-can-4.5.0/doc/images/000077500000000000000000000000001472200326600156145ustar00rootroot00000000000000python-can-4.5.0/doc/images/bit_timing_dark.svg000066400000000000000000000154301472200326600214660ustar00rootroot00000000000000 sync_seg prop_seg phase_seg1 phase_seg2 Nominal Bit Time tseg1 tseg2 1 TQ 75% Sample Point python-can-4.5.0/doc/images/bit_timing_light.svg000066400000000000000000000150261472200326600216550ustar00rootroot00000000000000 sync_seg prop_seg phase_seg1 phase_seg2 Nominal Bit Time tseg1 tseg2 1 TQ 75% Sample Point python-can-4.5.0/doc/images/viewer.png000066400000000000000000015240761472200326600176420ustar00rootroot00000000000000PNG  IHDR (iCCPICC ProfileHXS$$$@D)H*I T袂kAE+*ZYlؕEaXPQł 7I]{;79s9Ν@=#gd$1!̉ILC P,`J~r^*<  9'fC>K @<1d"hK`圮dS  Ñ&M~ԖAv"Mgȣs [CNO?|p҇YBTRqgY-Y9` $1u 3yQjdd-ׄ<dX38a "i`6dX{4NǎSEyA 4(v9\rbYfߠ>{gc .Q'ږ/L436ly rF" 9$1J<[:!#9{ ݇fOxBh'<$\'tnMJ~ "@'1x0-Wo{|,s@ʆ3VA_d2JA%[˰y2j Gw{؏ v;]zĎc X vTkbm '4gpNyդ@Fe ϔyL?[lw(#E{rkyP7] ܋2#O3{ Jqe|_me 3r 0D8: :`6X@ X ւrl>pԃ&p@^^#BBhC qBX7#1H#"DF"%H)RlCߑ#IҎB kTT5D-1( C8t "t9DuIzD_}T1fc,,’4LŊ2ᓾub=Gq&nk(s\|. /wui*ſh&L$eÄ3"#  {I ,2&b-G"Hv$/RC#6HTTUUTUUD**e*{T\QyO [=Qdy&yy|EhR(^8Jee=rrFUUT]uPuzU~jQmTu9u FҒiy*)}5h5OmZZdu u?e/h5,548s5*4hthi5545iѼLeZ]#:F7й3.m6[;CD{vvX::Gu:Òfd1V00n0>07?b鈚WF-֭սIJ^>o?Af3=#Gz,y`m `v>C#CS=F #_ 5FnjB5Ǎ3u~,zifIdIIiii=3,lYYyljd b9V--YZ ZӬ}s+mX66llQ[[me;Nhɮ}a(ѨQT{?|jGr1Ɯ!aG-񎅎lNNלi_yMKbf/nn7sn,mV4k;}{GW<{{fz|6j܎qL8^ۼ:)[;}L|8>>}|y;}e{/?>#`N@,0$85H+(><~ipzpupoKȬаUlC6]6~a԰ذ4b|ꈻ(ŎZu/*:7 *&vj>Zn sMkP6l::ٺ6oXszEFK7te-[J|*zs[ȶJʲ۟Hq7oU;wKswUnUU{ Fe{'mƾf[-d?/o;|uš釋됺uΆ#47z6cL*]qrlѱNOL?yZSO];=t뙰3=u^.x\8ru륺yյ6qǮ\9y5kkG^ofΛnezu;vw wi+op_6t< A؇wqx,}kړO9=kn{>y 4塿}nJj7zov/w?}ܧOO&}^Kװw G)z$elDyTO<)] ~l2quvn"MsvR CH| o{ 3\gЭrj?ʿ\?pV " pHYs%%IR$biTXtXML:com.adobe.xmp Screenshot n.@IDATx leEPE] *Zſ ٚ?וR_륓Frv=Xu:[h4Ƈvڵ)0Ꟙ^ j^xǽ^(j=eu)өLȁn먞'{??jǙI~jǨv ?@ @ @CU Ry{~Wv͓u%^:묹( vaN'N!bWr(,OuʳLlͼelVj# @ @ _v:$^|ͼYF6/~ \,/+]I|fWQAAl?mҊ"ʡǑR$"ERG @ @ @CO@2LRIHfٳg#]IյTcjQzE @ @ @a҅A탤}o>]-"-@ @ @ a(PTTD%%%6vUtlUC *A @ @ Y@P]ITd0ְ2ta @  ב=Giζ` &}_QW}8 .W_ol]ƓFG5#jiQiʂ{I6|; @ P o_ ;RI=O{Ν,gė:A @ 5O?M]-ߓ&B:U.TQxHc0<ǝLEf-=]%{4Jb2%_y7uԴ[m[ @r#q}oUDsr޼yI<_&H @ w~~ rStcfsɖͽMfIzo+ adFI_H@ - 3`%r <(//T%ODf~ڒp>fB @ȥ@D. HgF>DRR^C;T!@ 06'g WDC& [BoRQ4J&;X!@ d_ {uww+:ʝ_H@ !s o!tq)u VOQ'B- @ .ioo/"쑳K4eҟwė C @` /~F3ח)2͓FI @ @y.DRvA邝;WIXIz<}x @#P^;~4Dv_&ϟRp^sPnaÑx!GA$ 1ՕK3;o+fe^ > @,lEKiyʑϧ}n$s0N}. wֲKx҄Y @O /PwI_~%ICI>Bs2iZJ#eKB @@[oh$'n8 }ڿ+Nw])gk=ji["Ya֞pHvɦzd0 @@VŒזb04vX$V(QRǐ=V.Qy<'K @{}` E$I(̃ @ /Y +WXXHT\\L*^\$]-$uYV^9IN_NQ @ @ @y"<#€ @ @ @PDQb @ @ @ OA @ @ @@A-Y @ @ @TDy0  @ @ dKDْE @ @ @ OA @ @ @@A-Y @ @ @TDy0  @ @ dKDْE @ @ @ Ot'N @ @ @@6pQ6T' @ @ @cŶ;Ch @ @ @* "A @ @ @wt{ !>@ @ @ :TEv @ @ @ A- @ @ @ RA @ @ |@Q @ @ @* HePd@ @ @]DB @ @ ,"A @ @ @w]Q/8C(%7x.CDL @ @ $ؒI) :A+{G7w> Vɾ@ wl=M-K.-bEl3) fdSrD4rŪ}ꊃKK1y~hojܰB4oޱ4aBmTE\/lb?V$`;Sc(IAqk.VA@@%uu-[кu8~1D^̘w=yJbN!R %k._2# gZ1y~#јc >r=;Pm7vUiv(=ONK_\ٹǯ rkWI^(vs$5j*< !a+HP#IJ=bX]{;(D9$=}g -Ŀ / LF]b_k|3|p3 Nχ[rDXe%6Adˇxc趶="VCeXfF#_r (IΖNWlWӍ VS G{whx@JmRRvWl_k4kn}5LF-ۥKmmn޺ &BD"N%ɠo2sDϖo*:ï]?HG 퉔cM"Nqk&. +L|n5!%t, ^OZ l&;r2e,~wՇ8&RFvx«2  RqlqɹU5M-n=K_^$oXTU:شCTJ>2=tjk-ZAGՆN󁃼_2\8-+<>r$۬IBHQ`_R6Vˉ:sGEl4I>Vc eoݴi=6vl0󚦷/::g^`;8~jǭp4>8Аvb:tGl*KkL@ %Pů^LG:u+;Jc6y 0&T -k~BWX[1v6[svs! a,|ʓ>eRMH @K@^u%j4/O^ncO=A?XK.s'sӌi(ݻ'Qd=[g<[{:%Y?cUL֨t3/.=CcԷ z!QTmwy"i: g K=2Jہ[/I@zZ3.ל|AB{;ɺRx@ 3?:J](x[vڂֹA {KO/sٵURe4Σ[Vd@W s:Mˌy4ģeYܾs]?1os]oH@ ]O;B#Pa;G:D(睡mP7⮯6^yQ};w"aΩ3֔iuּL`E#u>9?wB}OTKŖ[:^A A4Q8@Yz[ZC\xa )K~ e>;wrup]KL9ٸั˭Iکqzًk,ݜV\ңkfҴk_moWYSc""KgVVBL%HGHg_ZyiwW+ב>90r螩io+:okfźN(vax>/UO;dQEc 0@ZC=l&IoMg.|MxO(Cf6}'@JW)繣rRlB'rniïrt^tM5w1{*Qk'^R,Ϗ~fM-墆sw }t|~c9 䇿4A OU49ɭN=W\nss(@;?nNW8|ㅘjLbJs;|\W̬;Dgg:>`K]mV|Oͦ>vxO=GAJkbOZwǯ÷1ϦZ-wqC|=/Λ2hTu @}"O7^r _/?:CI3V㼇+=u{!s[@!+ &iiO_Ez l'#cC.8[Q'ԅo R[)Gܹڂ撲rKAן8eyS! %[;4r^ uE4U6`˨XFrzr]=s}  cF؋ BAb8>='4&o_j藚4lt]<U8Ь4Ko>C A9fFWr~b'嵅37Zf_W1_^?gA:'~|O _𲚶ls(_Jؽh58۠4k=lq(hѲyFKZdPKoKW\ 5_fV|e&|U::ڮŗυu㲮G]8_t9>{ [{̖OyQ`@ xGEVOK,8s!A䢵w[*xn2~f2 rŶ+Wf,>=G"{oxڣR8E£C^{3KvH;4oV |/3~rBmJ6 ܗm$(Ƈ_wWq%XsV׆{;!rIwD_('/zrgEHAFEU:޽ @6ռ6x՗E g|!}ٮl.GfįȖ4Bbk %u+ @.ZSo<]E~8I|Q^0@;/Ͼn] lee|#?m9mIhN5m5> }߳֞^[OFUQj4l*>s˵ȕSν>:6]MlTmW^d@{@zD8j_UJf]䊓+rnWO݄L4îXZt܍}{(:0L H| 5qj9<4Eo,OǏV ed_=xZd8*,ҏz zetK cF\PP0Ps;:nS.% ys#4iq@᜘[,p8tdW+G2K_;r ^jkӣZ`|ϹW{5#++xP=BαEǥ굃gviB V_qQn|c}AmQ6Y տ=H#v襶Rb: JE7r]ǮWPU`UF%^[yH4/S\ Gm-'A++a tR$p٧ؤ ;ڔي&e@@Ϸl^[/غkMC{\59K~0+o|@1я'o[vKXѕU7]Xsj&&$k,~9+nכ(GNӮkѿWJ+8aň<s/N r% 鵑 ͽN@5{4:Cs ")1DmN퉣Jڷf%Pd "}Dq]Э R3w&<)8ɱz(g+ *@`x 4zxY/ ~sL]S,hz;ȏ-Kqr'}zplPBJ?qVl_/:0BÖ$rH ?o9!:=:!!@iwss݄c)/E^}|>yRHkV)plL9M yCO`_|r~T DT1}6-jr@Nzv2lǮϞ4&6Tθ[Ӵf6+Wv 0< $T7?[VCg5vS,zk6Ka[gPOu7<80.3F(=r58KCT*.g@@uj/BO4Fex e~:zte%/Yzq%'˒+s쒐m-=/~M 6՟ϬVNV.UXBܬ  V6tȆ@O]F 3c GJn1ˢ^`!!~qA9OW{PK@UʨOB-] %fxŴ@yRy* +@ȷ铼+*Wߔ?4iUp5 ZJrΨ؋J&ʣX7y[9r^r8ۡ~ڽX @H*f+_KtU?u#A$I{ow-}ÛL5i|NiȰ> (Cڙrь`WO4뜊73)JO< /\ ^:XSΫevȹMy§7cE~$'3$pﺕ+ܨ*4Nvb̶Kؿdqmvp}uΒ(Y8x;s2:_s]t7Ybd)c\cgWrJCw` 'עު  _h#~ĉ禼ngv=ײ%[7v}_Riٗi9`X~pIsHر1&" =zU둧9%ǶsFy9E!̾wc6p6>Egϭ{KDorvt[}~CPS[Ud&mꨦ["9|{Ntg t5' !*5Gh۴:ne{T1 @HI@9J[$%-/m.QCS9sFC*kLd:iU~el|=Mwl%1O*t9çotIkm.s|#!n g˯D10j:?he7Dݷx芒P琗vo|kseͥҊ[Zv&4tU=,ˋ|1h(\tUG8o}6Cg2?ܻR,б28?W"۝9k{p^u}Qto;-PUjN!]Jge,^&g'턝-Z:ՈΡNXzۙ"8[3sYMǖ,h5|l͊U:zwxG{nbQg^yi #`a֠-oFWȝC֏^rZL琔/̿[~޵ ZշsHBvG> F]ye/VTaG  TF_Ay})%% 1pᏎkqpO$]H8m2^_a MiiN2D뷵KĨUI(PO^,CMY 03W}QQ:~ \ʁ 8[|\k^yΟyFyłe] 4G' G8^[[R(к}_z3V{N.0 w^*vȬs(@DAu @ @ @{s{ @ @ @:[> @ @ @`t  @ @ @` h(@ @ @@@X @ @ @:[> @ @ @`t  @ @ @` h(@ @ @@@X @ @ @:[> @ @ @`t  @ @ @` h(@ @ @@@X @ @ @.  4K/3^OՙMtNK|=iqǑ" \!]@^smvq!z~^BUvhat"R(j\`K-T0Rx{?uy=b0J-A'bsnW9K2mt rSZO =9ݮ~](t*liuxM~7ne 5>o"j~M{83_Ǜ.yM'3oyҍO^Ǜ.> @@FK7"RcM@};wu7NfEF4;g|$Ǻ _ؗ# 'k7l9t̶v4{H+GU gG$ [ kEUqwls%2=5EBqeD;{9e:/mt5 5{}^/z,K;]u%9g~c?~OT>o~M[LKs%qfZt-7]ǛN.7gxӥ>oyr}-7]a}@ 8; tS4˨)CЈ>ovu~5.,A^{˜_m?tZOITh}ݯ5Yg\aWw˳Th YtzbSQNH+6;zY3[K*+=ZKfA߱:҂)l+y^փ}+͑XxP#uz[IG3,ͺR}^=6TIvSuxMק RI' +%xV"8>WrgFAΓ;b}By}3ݛ]qzߨJ TS`N|ZE69Xe^^\[8AGɖso),~M[ >Wr'"-7_;o3q/Piw! @,V]9ܹ.dH~UtH6hK35Vm?tDtty}>9w"e0-*LǛ.pi`#$a֘3y1Py佷}u f:i4NpE:5TL6KPbԄ?iMWOws(Hs&[L{"9'J]gnI[o>*wYP6+ ^t5/s%q׏/RxEI[o:ęDǛ.Q @ 4O36[{Alj!ГWZm5Sj9`-^M _i gŇm}\DiRgH-x)uRq6u]+DRZ.O41=XoH!h2иp"}O^&?ziGÔh[K7@W W*mcWJt!ju櫃JdZyI5 ӆp$ZZ'?xM'zVq1^eS l78yx֒<ϕ\_?HyM%4L,›.q-7a @ l A;lU.p~Uc6g < {v4c/)yۏ7NNYvT$݇'gMIϼVoff2U8mwğhպ]Wj@tI7IKIx[js*j[tFO/;}^G|]E6u.EUR|:[ O⢂G>8RrW$r3JZgQ!~qF!Ro+-m}t5̤<ϕ\ǙIxb-7ORx!/%m?t|@ \$=UJJ4gR%@+Z.yuZ#*-NTSa"i?-ǛHjiV,:Pg9{'G>YSjcE7,w?$^?TK# ύ.:]Es{s3TOMvAO5+Uv Eiv5{sUhCYFzkDNS QXtT&v'w36F?mV /9j ؏;ڀuC=mǛN=ߍ9Ix.~./q~gNCro ǚn[}rWD{i(Ҏ_ Aг{I"mng-fsH)!\lF21xq%1'f/-~Ml-vEj4&Jo yw捳"%TFеmήFd stnb:L/uIMtkh^ew>o)ՠ3O83/q&FS,PL5`. @@/7[k̖Q^IRr.'=2R Mvr l6۬-;Duv}v|wU*f6vUiˉR*O&~E-`*ۈBC]L]JF#nuve@@IDATB`Mud>/=Hݑ4Xw{]Z;=Wd5hVdq Lݞ=R5zӢd.kXTzEyMvUmO#DRyxrg[o:x8d gM7P<ɖǛ.Y@ tI^(fÉcf($oЂj*ىbg'4TU[\Ԡr奛NZKT;8~폺#Cwt/kqoq_1{wG\v o/N?jy:Z`}gM'h9vW{=z=Zvоl먒NؐsCxihfyYͿjˊW֔ߧWfZn "E4T3ِ~{i%MFK}]^\>2mZŇbTlCqt켼'ib:Jɘt?o!-2C y(./Ű'o>8ǛO)-7]aa5@ AdJQ2rn+я`ձ_mWB E-.V-\Mv}AU-hD֤q=ݞ!2Ȓ,m%x/+o,u~ZyQU>⦫v[^v;jRזht߹eK(0ENOUT1IWGieR^G[+=^4(W)m* RVux}ֱ{״SqW̥@m0}?cuoyG@>$~Ő_樵dkwZAUZOL'yX\.c6uuTNal`ԑKJsƴ+^6T[DFC [W:+ˌ&M:O:\,1>/)./> D4q"pѝCl%QK qdxMd_QeI~:#YH H )o*Ut~MU?K w(XAv?#\[o:h{W4\Ý3:\׏U,<]hAcJ%ٝI 6eŦ[oD1w6k,x5H:)y^A/qt|>թ^O{CM M9⯜K&0@{Mf}CJd%'{y }`MB{m}x)Q]g\Rp~NdZŵcjٰ78kXg?=bTQfEqǛ.~du(,yx+x?Ճ7Nth>oy'rx%c(dyM,Ǜnx @ {o[ _ʷdonwFZ ֐uu[T>4T^ZjP[\\7x% #:#!S^QM\e,4|Hp%R^fCEm1igT "ް{q %xԕ 7ꪥE]nW}f~H:-oV9 ]3JnJKfӳGԯL,R'ӭ=Ulw7~!M tRyiǛ.hLAAs+o:^5u"%KFvxIM%YFMyJ$?:{ ^R]nE wnIvy]Tiڻٽ=R 2 U#CW Ef::~=^59LTX<Dڦ['xued?H9]mx1Y-op̅zTz]gc;d t5gyryV}FVKCY=ήS{#FUT vV0i)o,TxLu?o&~M[s%gq J~fXL ›.q-7%^͹q2tM78'wgH< wyñ`~C2 @  K痩l_:*ހ~1U"jawqW4MZn}__}ŗ&訨4hw[Iߌrx2ٮj:-ʑN޶?utK[kV>*uFL.=Oz^STZtMo2 _td~qm=!@ @` }|Ew/rT]{(҈!KV'^ܒY슥W, m:.:Q~B9FJ1|Ϛ-ݤB'C5C_X*p~oWY+];[ܹ&⾗)=JkHǙ?κլB`+Xg&7yIM7\E<[_ cjw\.X-v4}#6]|pqmlXvlc2y60Rc+`gyӱq8/*4^z/o?_ްk!Y!ߐl6 @ @V@[3>рorE'L\%ulcOow W gg5QX upxv¡xځh+%}]\O*%lMsTyQp[ާ7;uiR~W?xۏhEǖHCݯ9hI}^KuL.x@uIWm,S^4y'F]<9$_Vi-jjf/n7#oxIeGLfRmS3i+D @ @{@'3hyNb<(ϙN]GW_yU_9_;$+jnot7|טe!@GTvY/Noh,}́hh(v}c_[?8ۯth7cītnvTn=y#7@ۆn!a: mɿ\!;#aJ4aFo7_?)/Azt Jxφ u ~օa9'A@ =  @ $ y[fx&׿ڔxiiZcc;ֿ*s֭ݡWȿfO#jTtɁu٦{:PWmKɻ8/e,k2ZpZ4|^`&┷W_)~kwjJ}0ȿR۵V^m;$]AvKdo`ZKM‡%m:33$M(3<' |WQeu ~:?Q%芓·/O2M?YyCkhڲ @  ]$' P=䓅O|ۮ͙ώ0~ UD#kI>JSq}╎6Mx:<=T:G*ߏ"OTa'sps=`qyii,voV3O/:xSab#L~[zWcGls*quҫ~Y.8-`ȳ5uFm)l ļɶȾY4TjLmۮR_5e}>p ΍FjtqMwh;Bi3.oH 75 @ !/K:v!)jPuБ'EW}Xur>g納dN{U/' }M'W RFVk.bSr$]p5U[)4֡w9{~Agm(@m>qU$'T\{klۼVM5Y]sQ$֏$a$qCG{Wq#ϙ~tpmyte?_x ?I%ق7ѵX FHOK7rUtw?:ʉkB:Mk?ByCUOhӖ  @  4 O>ÎOnP0WM2ϹieO>̇~:Sm6ܡǯV^f:[_vUpY#O-YӒKj$p'OwU TQ>eVsמņN,c v8Fi]<3WyRjfb66bC{#v_o Cϼ*wSjȮ/[2/};e~4˴=Vi:ءξ %U?'7]HR_Լd4Dgd}n};&fyy̙scfI׺Pn̒Ai醿(/C@@@@@@ ȜAT[9㣍s.%fSK?擽տ+SWLn$RrmwXz^ŹjrW}+&g[4~O*/oWĪyٳǏѿJn&~G{\Eei(,n?2]Ǎ;jl4ϊKL}kʻ,["5T}$X~)?7^n>jTQ?/0mZgMDu$ѯ ~HT__>{V_w}>l˸v޷!qI8~ŒMqLיDT9/$O59",@V.>su ǥu~ZNl>VpgAF׃2EXvP~_@X9G^+cbO/g'--}6g]Şö-y ՞Ėc64%%a{}4c?K|Wy/zD ?gMqh0vrW CYjm-y-ex&$>(I=>6g_"*.*Zq ⎓^^ϦfNv%~mX~7t~Ǒ׊՗|uٹpƲc7sIHbqg+kZwݯjKjE m?KN#5."!U3p?W?K0m0L@3׽vtxQOn=TWR(2B'%n8%Xߚ^3պ.>9(#w]sD4yO=6p]& e3$|`䳿>>+-Ò?oTt-{ej6q5FufɵSϞ\5~O;VZlcmڌ%oXo4@>!aS :I̷G0no[,('/.% r||h$ݯNԥtU|߰.i]V~oϻVNVgsE۱\UYVKb$5FwnB=Hh1(C       :3\Ro:]Y]ȋf/OkްLّlHM:3!5#&azcQE`\J6utǖ"o`fL7ٶ L3 O?SavE,3ڥKHtN tX$ӯM7U/ceC\w\X~9Q4̟C'UΑ@dyOhluI:ۗ@Tv/̡@@@@@@P}({a8nF+UG$ u{Gfo,??^dc GSEbuLbWrؙͧ(Kb?WJ=% LZQځ/L:ӁėG)B?#磻 z^¾XcU_wubv_|YY_05p T9l~rg>*o&o9NG_5uAsή{ v1똮VE^YcP~Xj@@@@@@ <@T CcqӖ2| $ĚLIؔ,U[J/L&_ ٬Qq{:'C8vyzlD79 0zBӶU Ƹ}TtJnA}>o[o˸jm-UηqE')k>ұ:l~{@1kJ᠍h ]8(       "C>[PM-iZ;!'YYHliE]`0vU$  4n/(NIUU}_\hvߒ]l]\Z)2Ə'VlVRZz7`f!i|ڍ:Ǧ4$9}Kߕp Y%iwtm7_1yOC ƳKqAG\La@ ||}HxPv`8RwNyYf T-^[^;muϊ#:-aAB7hX6i|vc)92֙B|7oʺX눉Ǎ:7ضw?s I<3b(EaPS85MA^n3ol97fkRp 㓆&So>~G^"Kf "Ӎv:v ~>?lnO{|hɼڨF /s^[4JG<1f`[>=d3~>ÕŸ.NKt=:mP.'KqX\{Y׾aw\|yjʎO~jssGt/c'L}I3Y9#rX ~቗ܾcU2)–-cV8Rq´z$s؋%Y      .<p"vtmv`%?h֎Zӿvn9Pq{_bBt,,V:䶲W_T+،]MJ8n܂.J뎽p\?$dE`di,6aPe l8xj6]UZwJM} x|Ddp-aɺKLsce۲9p]S?>zU75:25&>cԔ FMqpno,PCn.8ccsrوd^'}a=gxFBl71=E%{s)v| Ysߺ*$NFW'k,|=Qy叙#`dzםzz j@i)N^'Uj/UU.OR{/U% II2ABA8՟Js+AST9lk.OۗKl5Ý=oNTnŞAI#˾o5ɞ-p! kTzČ\זuck$$&Y.g ;~rf4(5o=3}]*'QOD\܀o;uX~nfg|WYaG_oZ=zF՗ö!Mc1s7+_|CVxL5.`X#QB _P~ߺ,0$nVQAA TNzzG r},OQEInOR< xJ-?\@<#=BfgT}T9O\;S)_'_B- j;ߐ(b\_snŞ9Rd/s  >3 lųrG&۳1meʵK+w4YT9vG= GNV[k[1u.RkumEQxn=Siӡ?ė^&aA3w^^O$$ 1`/MyK9e.ز4*:{Gӵ^sڞ^XxtIgQM۝߾%l0;?j+ŧIxr#7nu%ʩ?7k.b9(&k)4?,qOfu4[i1)K(Wn}k;D⟪)Ӟ"@'zP%Q娦RQ"N!2&T9GGn3v#'FGaT*ʬ'zZNA9L[USh|dζʮuc.N2L?_Rd/,ND^F@ /DϼZVc|4JnU.0/AƔ+1yyQ.3XISA/*EƛL/k{Y6:Ǔ{;|ٷ>!^[^{sQ$7n8XZ)XԽv 6 +#v'hxEֹW'^ZTs{ݧrO,^h-cTן8]cQzRx'Y'{g߸" PLZ[zvJwi!4WO>/iiNoK_U>oK]'rNBBPvOʩCNz*RJϑgj'_B-IWyK~\n$nhV̯E %wK$@ʳ3MȾ2:wsΨyiPN?8վVcTrd>jdRM]@l55J-|npHc5]͕ gkmٵOnߕNS_EsM>V޷T?5}kgvC˽Ų˲e={|[ `,\S5;%ֻNB8i'z:_J|j'UΗ©ru*ˎU.Eb1\rb6& T}T9IO';xEcy0+&љ;ST9) W ك31=u_IDba<q3gp1vQj_anq7K 9l[5i|.7y%}h;Ӈp>mYS/ێrW/}K _i1og2Y qݫ3ǹ?}Qq? 9f]߈p?Qt古׻r8Q'z4Cʕs?ءDRvR|(*_שrN-ABA0PIGS%}@IPai}c<Z zk: n:@P  `TcFmT9skom@ gzvL0.{hŴ;%gsf KVgW.)m[?,ƪ _\nIcqoK2ciJm\av"g*+lSRܸU=;r!SR[?ןEok8qk0C;IpMi2>e1ĎK$[6ٺukm2E>^N_Jl{0mJ3׻R8i=#չĬRèvR|(*_שrNtrRlCBԯֹvOʩOtzG$?OZ^捉_bIJ }~)_~^g NiU]72܇q+qʹƱ@t}_|ܪ3QռR~_E܀ k8Ҿ`KW$pn|SszJ~?XA {1oϥܗ, |6ˈ{('K8fZw۶:F IfEHɟGJן%7Q!'S?LtŶdE' t ~ Mn)Mṵ̋(>s^wݗ8tG:~SIYN=]$St3TĔSgb8¨vRePQdNGs(*\0lN>\xR5'5AS?j9<' knw|a .[v GYN/zDo)v_R5H @H *i-i#&~KENg#U.h [5̼TXpqqiW1\y{Yh6kb5V<V7ܻEV@QE 61:8~XM0UlYu'Ѧ'<نyWL$\1;NkC |$J..wWp_vXUOֲ3I1a@S˯™s#y];n78U:2~ۄsnm(A>LEY?Pڸ{[~tnCqᩅ ^uَla*W%ޕgbZDL Rgޒ~Bv*lHoGF@;DCådo?jS~ÓzW՟fޏ\P/SPN3>Ō;g_dnwcx8!|ۻ~řAՎT;)Ke @ "nͽ"NyqyFF QA52Ac;f--=e*PչcёIGVMǏmѮFaә/o-cuFqIv>RꟕOeOI#6ӇldegpfKkjZ^@IDAT(SvzdE% /rg^~vRUUί1~.RQ\rjC T}T9yRm'5j?j92Oӧ[:vJszXwn̕i/-skn !!Ho)ɇ^L fٖs}ڎlD|Ɗ7owJbVLmgGʋj;BYG Kw_yEm`*{+<3m)@%L[ߕrbx%Cl޴$VLn:>oǧD hs*‘_o*PX<d4|Zib2֔ww mY6vR%9L,]l}KI~Ck5\Y6+m9-IHTޕcN@y=I+A2g^rvRgOG*'׾pOB#Pj;r iԟA IPyb=Odo w4N|ƩK57.~v_M#$_:saڲ{Z÷KQ~<;/IQ@@D6vT6ط*ʪwcX^_1-sgM48$ftb_~Ú:&~bq#,c?g,M_ dXd4L.x7f.>m[>=O׉͈h;/.k(^SV!nɚw[ fwsA^vKY&>Y]/˲%t!JYto;yZnf~C 7)j'% Pm=0~UgnT;rnePQd*<O\r*" T}T9y')3 ?j92Z*IT\TSJ!!-ww~65[¸+_<=ܷJ$/yd  Л7f~o__=#0K\/쯏JKr^aIhTbVYI fBKyGWYroR]}rtNU;魯.a]* )9mHiz(Fm:Ld}8aT9A_rTe՟zYciǪY˚6͍%m}O-6l~Iz\~9UM" h'U`*D&B=I=~p7[{gT;rFHI0I4 UUNԈ rʩNj'vRQIJן(lK 5{WQ(cnV#Kԅm{W(Upߪh'ə@!6D^25x5|ܾYrQzOKr؟X|7hv|GW48dUmSGlIHQ/"T}m.;!9S7TifFePEìU'K^th~F{2a!7R}ϟw𳐨rx jˡS jƤZAո#3 k?^3?SVk3 mߡnIW~bIaT;.HD04vRzɵ*g\r*q T}T9xOן3D/S@n G1K# $%fF0mJ]9#y[/5b@ :H&P.' fSI dg9Y'>uc,C<=6ODs氂`[-`1jlS]Mo}%̢$5KwyMIDx}_" N;bZ*IcPNvi]˪iZČh- #'-kY$~HNJ{_[txYS muė)ӟbz_|\ %D(M .xvRĭJG lx >C}RJʅ*2=*'Zän(BT h9H [a!2eKn2gaէ$8ⓐ#A}+ӯ=9/$ auU5.KȒk1r~=5RQW%4f{H'*%7Z 4 =?:nd؁l:qqڃ\pe!n{_{ٚ:tsٟ.`[q*K_i}eQW'=Z#aPX(= ~!&JoEeɬ??l۴&\{峂C%_vVמ|mJ3o n^#q_ac 8y{ƳKWgJt(AP\O]L3iVyǢI@ZUUNUޱrCbS ry}.j;r)3!{R%r >\Ǐ/.4x%)9غRd vrqا~޻vκMgt2֙Boy7oʺX눉ǍoŚmkz3I ϙ R5c.g3R>@l},(DhO{|hɼڨF /ma-Rl}kr9'Ё%?m<7W ~Z&?RelފFr;mJEB` ('BmkHjLQv rTcrm'Qarr)1~j;r x %s).Ũ.r. }zf~>$]DKϪyZ^ rڻ_h\&/s^A@Dc,cu,>)me¢\|Rx鄘Y((Rהܼ`di,6aPe l8$Ʒm<25-&6D3aid}-MLFkf]s5Qk-g*.w0uاV+Lջtq]Ҩ;j Cu\buH)BR޷T9^'|b}ܒ;"5Y|i3|bʬeep_]dsߡ;e6v+tY97wZZ*L%l;]3g[WBH@I=B3C*HIf*vRy'+ us*e{zN*H%|?\B-G x=ZGg2p) 'ڟyR C!@J{7Kȟ3~M_r-(@9(Lr3{/^68U70)IӚ['>:`\3zV0jT)diG}#g#>C6vպ?JJv֙\.fH>m1&SɮC3G>qr=? 4rpfT_;0=->V+dUߜ8:NefMԍlHLw5`]VU!rsŁ~NsI-]J3o5}DiFK]UeWwwI尧t1scg6g~oO{Q^uU',u$a+7G_syJ߾\|wZYzrE<@g.˴SQ==O IODYD屎W?fyCfpxN_-׶ڶsrوd^m,O^<烂=RC>j[>E$_˭EE~͑p^Tv%%Xjر3J,LZ rQZO'v@fFʟ!"}q D@e!C 'f X_S&!1r9k؁3c%GysYbT9z"%r~}kߩw3U5wn.8M* s?j>zԂ 3 i.f \xa{ldžCt.nR9  j6.0dx?SKf=$U՟٩gvVoݳ}ffoW Y OQ%Ywzփ3h!g/aaI,[a=۷~FZ) Jo{B_d1|܄yXFv`uBٽ"+烒rBd=rVq|w-'#vrX~=Ǔφzޞu]>_rQZ'dO4<<Ow 3% 'b )B^Q()cS) y\$C>rݸ,_+Ԍ#LDUq#RX4F~L3ri 2sq.>UΧݑvAf9pDWpCڎ}qw'lٺ_Ji<W9zYtWLM>_Zx!yy?I4|ƀݫ4z.,n`˪XL.]NrEϞcRckҮe75թ15opp|͕5lv4cC?.{I#v0WO:~+"I9 !sӶ鹦fk515-&E7}ejw:Lһc1vSc$!r sRb:viTW֎Tb,c\afMUU-ۃ1}f Oī/[ŵ\?Rf?5?#L GiawtP͜ i 'Pe)t'\h0@:˅O<* ^#ɯu N@Qg^wDU[>znV-Ib-2fmBomL2q*_QD\%d^îRe**ך7^,ltk`ee^**廊sP;&U19Z+͞Gn.*rktRr]^m# 馢݇,ҵ W-4gM5bda<mwˬLS~]7= JVYŚsHr}ҋn/vHL)e$G?~'%u3> 利b%AH x)S^=HT'/K;Xl&wZvXS6מҍFEv/nEoN~~ynB)bהV|_?,o ?iH*z>Rd{ی0i,^HzE W?z]/D4}kgvC Ʋ˲eQշGR̙.x46Dc?h2-I䯫:y?x(%e+{>X`;Y [3jbIgo#/rQ=JIrD 廖}v@ӋT_?Aa(GgoT9) W ك31=ѳ֧4Ugr!X0 Zےh\Ww^_JҮZӋp1䰡v 3o2.2L3b% g,1;=A`K'/9M !\dYS/Ϩ-U.b&4b?icX 2! _p?QtQ0&Dg Mrq#a%Uٓ~lTюUv6% 9 ö9z߰[5iTX7RbgjC|^>Tg"⹓b͓yFLv4ZԳ<'_dh_pzP~=ERˁŮ7{r~Mg/C  noM=fK[umR(??[ֳ֧(Ugat!C+Sb4oTHZ|)a[%4}]3d \b]dmޫK4~H)AvIf0hW6Af'uҙ&ۧ)l\Y`qR[?|՟T7oL  i;6ŭGlṷdn-Ћ||xDaԆgt;]Yz5]@yN5f$j[lz"_[*Zƈc=5|}cKF#ڶ0~XUϨ_\nI-ҟC`#K|15Ycu#2+\S'cz"U jOt%^C=grp)=us? "OwYg.{S|''?_I'G-~v]dsE8*( zt}_ܪ3bx^VJ}qH-q3w!_nox/jsڎcumk#$C=6_;9lAeO䈜͉[ ~~quq,3,(~}øzI|7 tͷZk8ܛU^ms31lŶdE'1o'd( 9\ve2`+? ijG2L_v>kհ{/p%r xvA \Y~u]Q~rp (P9i%'=[/!9`7!6PkFY⾇eu8{ý[$a%/7}QMŤj7L=eu݉z2"vRqcuNbXt]IQw򮣟!S㷽G.Х>Yk]~LW~05"|A6j{I"O,R3,?zУr]=>ʟ_+p5@ fq{|n|JD]N@b? |gy c'OkmչchstU*?c'~hhs|!zE2KWNw5M$ 2vs|) Z羔/E1j@ݣJ;9+v[ж!\ugkMN=n+aǔv$5x[:vJs䕗Xwn̕i/-p[KٝXlӇ}ܜ42nc=}XFv_v7lnfIi1K)SzпdxQhw*5|̙dX]gH(k?IkYX3']7u;!{t}ϥe<%K>A π#'J։%*dTOlQ? J{OT…R6o7v SNrD&SA  Л "=۲yPT|3U_Ulߊə>{wPy|"Vm'W=kza.?﹨lROH <1}nÅL}q |w^{%rRP~)}&/ٖqo[k]C2tq:%]{㊙3/Ci=[@Lni~Fcm[.$Ldo w4N|ƩK5.~v_M#$_:saڲ{/=M$tAeb3׼^p\Jj[Zf&4+m8ϒTtfG=@PX<d-S͗!Yq)oMywЖ%yZdowPWe[OԡTUt?%%r1A@D6vT6ط*Ů geӄ\hpHLSb_ܚ:&~>KCNY_e{[w=E=clRr vⲟN_Xvv~Ca3X]ڒZ~,/ ;4xv5mS<]'6泣>{2WzIk2Z5*.*Zqx'"MUg^]vd*]:LG-73\~}w?KJvY tI plͻ /|oKtքħv%ܮV/svu0b7B#-G3,f_pF}^Z烋:Y%9ɇA~>dDJ6I,ZtbƇq#?Ch/LSjf$Q~3hA_9T|+'7_::A@ b {%,nh旹&qxe*g}|VZ o%3ިJ-{e*k&$+j"[kե]'n (\۱ e\OeYs?mth,ܕiv= pN70-&JL{h=Q:9Ld}g55Ly] R#w(>۠}rzIaqR2q<2 7^EivJzkU iZԉnWn;bb.a];ӺQmw|Gf{g|A}> NfK3kj8#q%o)>CG}OIf{b_U?U7+ oOA}\?SA! H~}|A!@@@A%St ocT9 .(9Oo,Jn;Q^mSGlIHQ/"T}mBgKBC\/ceC\w\X~Y;CW`X0kf/Ok4e#۽a0A o&ywm (?%vc(ٷvìt_۾CݡIW~nqe|׃>21V3;] k?=[f5qd\ \6rh+Mo$[1!{[Q43QdP|P^>쏒f^RmsS\~9"hG}LR{IJBjQjsiIiOփDM.^F3A) H~@_`OG a2Mqbdw[_.\/wCW'XÑX/hL?SEPVPEw/,ukp9A}jag>*o̓Qht|WmWħ P:ϽmF>N7f'O1: V3}/g?JZYDZ-F:Og/;({޶|-OZ9shZv3WA}g|ʑv-+"( nh,5]4"_%+}T3j=/Gy^fHSM\8fѓgOE~n/{#7{=+nV9$gm|>,_0/5LNTJ)4Xb%ZJT)IhЗg9yoiOpD|G5]U,Uʓkǹ$dmb6UNȊ ٬Q~{:}]+,Y}Kb9sXAta0-\։ }n0ގS87 wַodϕJwZ;]@'+B܀隥<$RuEl_".}P"~PNӗ_yzcWyeu2۷NV}JҌ#$,̳;EIgꚏtνE7_?q5p\y@|R;}[-+!%`ͪihYيD;&^m[p1cLjqrF$̑h_wD<}>i>  U}  ]Z4dɵGxHjD]TӺ_:Z;Y 4 ==?:nd؁l:qqڃ\pe!njY!YYO7iZwNt"r֋+cӇq?uIއ?w/X4|FzY fn )3`[ݥgR)][3ݶL6i|vm% a܏›L>T9j(: l_$iϜ'Ǧ0o0ؓ0tviS~u͗-ǿTUlM?a|d9t3 4fooӦ,W9y~o#liS~yWl}>h^*rيgf'67wıK2v҈d>9Ӹrr;ݴA|33SGiMu?1r8I/eҲ{D92<}9ielڑuo3Lw;VQh'Ē<94[?~޻72Z26* 3[v K0:gD7Kbt:J*{l$(ʞl2ޝ̥۠*ʼmwPBϓZSҔW~RO0ܞ;GJVRˏȅ\<FRGPT9F=+h_߷@zDKL喿^G⓸U]~}]Fur|r 1Q_JiT9/r֚,,& lԠ疄gWVĆgO  4Fn3]uI"PkG_;}XEIHZocʸN|ޓBna8q%PQf0UzdnZVqI_ݱWnxRKorN'ѯyedlM{L5eQSD3Xړ=yvcBW6OCw4޾k٣ ];4]VNMږ$l;9CIAmw>%wDj.>gro[Vv`y),k>7d.lW(ᒟ&p=Ja%, 烴tՍ68dfg:*?gwpT9b2Rʏ PR<=?{v \?i \JSؗJ˜8  Ok̐"FKUo4 6E /^68U70?N56O|tt߁!'=+\]5*U^+]!w;j%G%i;[?zT\Z.NŁ\6~HCңbu}5ƪ/MLezBig|7Gc2[[Ϝ:1N>j&ǯ2ӐWqT~Ҙ;*޹DjӻҭHʷ s/1a=lPBTfsCƵŻ({0'^3(*o/QBF=NyPpXy0E9C`!ͯz@""T'C@.D  E`"K            5@ BE@@@@@@@@@@@@ k            0@,? G@@@@@@@@@@@@  `( Y0@@Q*           `\$,Td @@@@@@@@@@@?:           H JqO㒩eee&F^6$&}`Rtim绚0}tec 0UN41zZ~ _j|И́cbbtc^^g)V.?srوd^J/z̐xk,cb4&]wP|LlUUvuyg~)*̟l}>@9Sy}g$ęFk{^TWtK<ܔ})}r᝜5Ý=o}){_w(g2ײ3``v+5Q=Qy叙|=iKI[72 eeL@X_?}G[H[%+k0E |"f{LrsbDeE SO̞A:ff Ɠ_}fpr=ŞVw8' >j(تٸwxL5.` Ow­7'z f֊qտsRwgvRw?R/);{?(Sb>W&8oMlk#(r?,C4m 'zyEޑvOW}jXyK[Ц!{N݆~S]F~".t>Y,\:vo;t$ {iɍ.Mqh}yMZ]HS!$k|PҾ%h{B_dǦ0og\h@i"bDeȣ]. u)W eRX`)g-?YAqeD(AiiiiK[Hg$d&93pO!|Jƅ~-I3OeK5GH tRg]jؔ7:{ﮱ].K:X.-ծ0HFu]X UW:{US~R{d^1P|yFXygqŽEdEFdp}0fT6Jc?kq=bȄ|Ծa~ENg-agi*5wZ*"'_n1 TKѥLCVՁɋz$UL5IE( a COP9 -mnJdq3ΫA'> #i!S' a.w3t _z|Z){W vK"[KS/DI~CfJ_:3{|Y.A曌cg⒥>XwC-o/5G\ P\=dfVgX~=xwE^;R$i/$˨g1W>7C)rpd'Xy !~)OV? e   pȅk)M]X @HJNFt]:}Ҫ˟;~EZ(uo$5_oׯ^rǕ6\Kˡn;{iu䦑i&ŕV^/Tq(q<=X,nv[5p;#*|*ܔ*TΕ?EH ~_+wWn?쎙 i_Hwab]ve3j9Hһm[mΒhm?qM''S ʃך(3#9eYՓc{f g2BNJy" azb|  7Z;EӴe-ܥ\㑐kmXaÆ 'o2 E3a><1t?trφ@QGr#˸mduhe/-|0Zt8Бri4岭('~La̓u3E+Wa uAZW%;*j`"xA0@BS_!&TmC{gp~n6&*{s m~ƮZNzY0KOߪWc`LC@Z@RqH $O@T㷿2g&eԼUx|%HӊBcH~]7ԥ3K-K/^4SK9y-#G w?Fo߾m[//~z0|ׂVLfĎft  ?CLw? DoFkYWXiѰfc)SMv,S3ImvI<N5dYjgp1MrDkKfǣH󟲞yJ(gz#[/eGUDsMGs}hL 6gpU'ZbS"MbO=oT+:L n)h$CGgÅh=(+ңU~vzVD;*5=)]j?7@'ByJ4Wθ.f4M'J*xce e^0na eS'0,U }Um$kf ;\S'z /)t#uTHWRRgGRid7ꆤOԓ=^L ~MVIS 65=>ׂ y䩌hym%o}~N$t,-6z'/%g[Nuu 1zQ=[oe0W5=,vQ^)& ̯ٽ-32$儹l#5o1 T;+ =.g"5` س,5ɸ{smcgJ:_:tt2/&.I(K >NU{MT:kq=h؎Y/ح$MQìUe$|422aMi/y74r ơݗk`j/ ~ U2\zK4uY\vyx彍uƄDH~.wU~-:/jQL9Qܕ뷪'P.P9H&TIj?&nH $O@"^2vPz}YS/[x+P9~Sr\S B[8:_Z0IUp!O2Bm~ۦר xz~a2)r{ҒCppsE+/PdCms$Ƒ% ~&ߟg HdG^[\KO~Q@Н?iyP"~-Mֳ<16aV}O7h`J?ʋ'lO\W1YvFh_ч>\rM\@{H $%jQQ,̥@j\ʹvֻn\X6s H_JKfZ!j&8IYmIM;R1,oO~7Ĕ^ǹx5qLڵ&moqr/\M*Զ[fmF'btfy翮+gJ2w&K/+] 2kХ7F_J$>!}|G65YyO=2/!{_ao(W;=B\Z^XTyȖ/p}{n7Z:X~< D`9SHmF1+b+ HޜN$z݇j>O 9}B>1H;T)=yv%~n* ܄9~P9%!$FRDxl]5{~Y^t51MuǪ(/163gμ_9! Z?~/Bm.TIhհthZeħ R~ KJt=)e{Э(M~`#HHCZ,qgnC,ݜ9s͛RE+j@!iv%~V}Bu;@@弌} #Tέ $v r)T~v潒2*KK $|ajB"jK0Fn? ]kܹ*9?8Э$Qg[FV]2t3F<'guիf,}ىF I8MAc,r}yyT>=Oܸ2窉kRGj2^~:dih"ΈdҥmƐK x7o5502d"ivz|bvٛQ^GNWgG"}9r&GqOD^Z:;;S\W& m⨨=/aW&p4แׇ๶bu<?;<)yfYCG~]Zt&%WWu3IH)ϳgGxvw˵$0IP1C5T[FNZ=O-cNW컴#ƨy=n ;ʋ\ʗ9lǏpۋ ݯGNOb." pU>vr&O$rPAxSH $E@+*ͭLBp)uZ*I#ۇ6B7Q3:\hԶ_zfo#No#^(} G `X.َ O%O)IKR٠c}->uqC2+ԐkَGwRC5mhnfiYdz̙&N7+X)ŋgƋw 2<}q0rd)Qe `Zm-x>/F2 %4q];ڞY} |g<5td7 rm(Gsn B4@* ~-Mo,Bо:˄Ƚ{rrFŵLG~;IVB+%X^/_3{NcVVVnqR~vz"*iwߪZh;@@Ę>sS(>9rBy$0Da55fǣd=\8?A16>'P~+ә;GK ]R\Eg6*F-b3am 9D2 id:*u%9?ΥD!PLձDc|i:jﷂ2n5EBΑm3fW&8/~+bW;=65xP͏ɔ[.z ;p6䮻Os| ڃ)TE!E Jf{xVB?9v3g~%CǣQLjCDΠiY{Sn~nNxO\q.ڬR &?` |>ԳHF&m%,&&-0&վ 2s)oU~vz+iwߪ[PP.P91Oj?vx $\ Dv|KىF@\_.A摩ú4' _$ÂM'Yd."!=eat|Bro8;ү̷b'I]F3ShKc}Xj*(GGv 3sxD>pe/DHҍÜG~FG+BΔ:gA]}j-҆/w1LQ]ݑ^m)̽^[Gys{?3GFCӲ>=8+yn~<S`p 7~{AS%F݇ol8ߎl (uAa.Jx_= Y1zfw~n}Bm(\Pyn AT/ $@Dw.4V{}^lҬTV@?d]P9<' ~wqV%FQk&ϙ$ pJ'"k Lb\>9ݝ~Փ51/DLY4K&$0 u%ΰU9'$ȇr]HNml(]F>,=!gRd~)&ACL")?а 9!X//d<;l۸[Ev<>5`/߬F2f`Nb'q!~(qDro"9eq}Rt>$v`o,VMiȫ{3cƫ'K36z }:7z,]f;Ta,JXAx߳Lf |ʒb?IÞrYf뻱Ϊ67ڴ,o:V"RпS?Sb҃?]PP.`9s(T @H `*'Ȇws [@b6Xw!!'ZObBJ]nͤֆ N?kv䨑1}cul#_pﲷo1:@گn͔lE0]5s ɧ4jr]rDܓ󻲕<̔JEۋ\᪊ ] i+ vϗKϝoH=z oJ)wWj[)N-u7Vl G>bXf\eqlG>s`~rʙ{$ ou?5jK*#a90]sض ݧ#Ln~Kcm.GK8ޞ651:O- wgL˵orC nKbxs~늽-%ﯞ̴NKՏa?M fRz9sv'iv.[>PR=[ܨ'_滜f+JધV֝֝^M#[rv4_ַ}ѡ5jF@v֙2}Lgp=%/\?sl~uP jj"ܒd?)Xs%!)L~ݻC먌gp0H]x .~e#a Z&~[&oNƲrxj 0$!k.vd"o9j۝֒ }nBdOu;H + t /鈑%օDHCy{ I Q=ïֳun#2G^\_=T+r<"W_yU8bR*=,z %54Qh+h~sŧ7y7җ;j>zK}9f彋6 ZZxE@ ?[لskٮe3z\c7#+qGcoW9(& &=_CÍI3l[V}O_ϙQ3=,ׂ&\^\r:ڠ9$o%;*VN橋3 YOW,g4W)/~1b c?wZ'KFW Ĝ`x 6_ϻ´> ~T&*\6 gI{tys1f-~3Zu9i묜P@jJ> AB.v; g;W6ۢu]<S.k5 39v4teTOmw{' a?,?@H' qf97U{]x]jl'o]_1 T;P9hЗ?r ,/$$O 45^'jz1֨ۙRɱ?g Teq IOIO7VO.WLJOZ=K9 T@OjWj|&sK L?Qz 3P9Hgkˑ=T_Wȩc(z T.:b.-$?uWHڳzϚkR|qH9JOFDM??@EčK sk} ޣP\u9"˵\ptSqWn J{olKAj@fy|W} bAH D8Lr6/5u驱l_# kmr\(_4⤽f̌#bN{Ieoqr~ /Rg"h琶zK_}w2c2~?,Vi3rݽjOȫ "hZ53z&.Ct|ߞLOkl\zUc9qaI1F}Tp%F| iP~Y|>Ǡr z5}y?g\sQ=w.[PZAIʐ!RF#$0=pN?OrEք{E}Zvw/ AgQdS:=C|FV`Iod ]%MP $3y5l{DkZKwXJlGϮ$!*/{6zh8rBg"iv ,{FGOGt3/ WUL_UU PmII{ Ǹ;@S%1 U*'/!^δTj΅cO0c@"#worv.١[3)%Ac/(8wQ(ġr>!̏8Hڅ6 PgՙwWܛ[$>@<[fݔȔU{M}HZǫǔ$tpЌ!z҆e gyuCsrJ@#ɽΧbs:ypaA$R9q8|QSz_~Ĝ>Ǡrbt 㱽-+B1}W 2'B%`>(TG P9xNqH/ydk%:)?Ï v.\y¸37ҥC>W^\B|Ҿ "$T"N:zC<9tKFL.iq~:Y\|r\=P!޾ș3[ҦD^I;Y:J<)m}#2C}:ӴeIޝCLJ=D2vږ3{GcVs${cos>{u3:)ڻSۖ11OG; +/X!Я]w};f}˱}əf- K f{]OX`J=S1C93koՇ9v-aAۗsE̮$Գ? 9@ik s.m#_l!f`E(DŽu˟&r)s',~G}]|NńC(=Zƥ??6mS>.PH" Khۗx򇧐@F ݁/cL\p1>]l8]x:ءr1]-gBc?bIz3qt-Y,0W'.dd \_yVY'?t7H|GNUq#eחUO)w:K%h|ݜFɱ33 l8!}c-P9C D~ēs T^A`dDn@?&Bs]}0ɯgI&pW19L̔n&gγ']=G@_7;s=_r Eho4XZ.*DOJ=KtV%*{.#UB뜄ȱ#`zIN\hq ".` ew)9w|8_śR(FKڗg@`NZy>QYE ʩ-uL]1hز78m?1N3Eм2r$F,퐦42g'tYY$vV!r簸oFkYWXiѰ `X43yv9 dCT>dK'cۻl߶?ꧧ LK?ydloTD1-Hζi¢N뿉 _lP9n!k,F{O?H6 rvGQV#선l)b库lvȟFXL.b%$@G& "豃һO 1I;n+1'1ub|j޲G 0gS7KGĔhd_ˎ׷>N; ewA"\w*^p?e `+/Js|/O $:8UgβݞK\LR3jMd/5w4wZ'+U@R'g\2}8 bRV~R3?} 3|=ъ& ڰoL^zf=M ʼ}e|gLK1@IDAT~7g}VnJqږt̳ uCR,wLI$,|kbb{A#1q'zMӡ| +),sOQȚnҮ54QGgYO6R{n/]}Ȯ˖'+|O"ϒyrqƁu?RUK˯~pS[qjo"Mg;x|gͣy3Y=q#@rá4> ;u^mk߷ cRVΙ#1Tr>a{ _DfP9j &0hN'YCj|rV}^HSR +'D9 i7JEqOE]jx $ ;HѮMfN<L##~qi˰OPCW$x#`&Ld; GU!DԶ_AGZ}mvv6,bMͧo u'#W9H afms~ǜݍ|#˿;6 c;i~M_l)Q&SnI1pUȣ_vz *g=<*pyO)襤<,mB\&_Tl].+#r-"רwƴl#h-E-h<7{A ed|Ű[bIĺNw ŨaBH6 ܳyzܸqU#+Ϟ_Wu4KbLmݱ*Jp= ?HTNfѪaZֹ6N'_#'vQ=a,ݜ9s͛0H-4A$(NSr9S=2 xJۗx򇧐@-]u-^w'F2U;OA_\xN L&U_UM~bEPǗ^ ct엟7Yk\V,}K=Pe{ _5 7j#mLWvEGwdt~]Cg& ɤK %o訮g4}JKK'FڢPRP@НSzP9He ʧkD6{Iޣ~Z%\F?õOF1N֝ޏHݷKf׹`챍t&2W*[,I9]$."Jۛz)']tSl޾JhR@C `ha==AJUŶ'^##Ԯvh3ߗuX3?;yRyZ("-D;\}?RH$v(/F?|" mG{T׾GiCH BCt19mn%~H* +*4}m;jvШm?.[i"gtK&BGF'ʆٖ([x|☇.{M6;ZOPM"7^rڷϿ%;~,㥱\u*=/ '!gBNU63e- wP/p:\Zj Uowpzxe;2)ѧ?@Qj>Ǡrҵ-*8/4d$ o Sz.ўS/PPA#'p_jOQ$cN>=BD\w2Y;./۾$}(Ac؛̈TNlIy:bP@4Ie|Զoz{.w??^>w.+&UgC~6eu7EZ3(V!듮+f42b`W\'J3P9w2T> -H>ۖӁWb4ہۋw1ӈ|ə[d{<#3"?_Ƶ.7!䮻lXV',tO =؛vӒsG?i1-ԕC.&5 V ҆z$2+t/@,߰R1a'`;z]U-接[ |7Po:vvHzOQ`raPާ;g R/&,mG ݷ};x< ʻ\a`D[N4**'|E~ydl—/)*~iIV j< ĥ㡳YB$8ٟ#G8҇`%)3mmŢ]>#Fd ]̎-uCrǔ->q GSR(,$dE}Ѵe̷o;tZC9*s8O+BGc-uuGs{0^{/1W{sąA$-tggY6>F_Aő'+ί9CTL5xY}A*>Ƈj[ lB]B8W IMR}?@` ݗL(.%=\ME?p \P%U'')q.hĔ{1%dwx BH " |IlҬTOKj 6/7 !y6P901#VON(a;9D龤R|M{`9$@R3t aJiϟrw!&^הhX_JI#u:&.30Kg}نI %R@5w]|g8=T6 G>b5'߉9X2W_D+s;M'm{=QoNZX޼Ew$Y:> uCy226m";ɾf拗oNg#h{ =-D/mpƭ ^Liȫ{3c7zR1Xo?,&(wP}&(aE}_2w]aRT_$K/ۑ]i˟>bI?x2"M=١ʟ{o]/X^\ۗd $& q4ݜn#(ЅG\{FO"/V,wv) `dIS?YK+1jG7V6yӅ?sZr`W ~th2q8YOwiTw*[]C*;ԭfTzwiע֑n:k]7~tq?Ziپ=mdkbf ׳p1TL.y SbFnQmIec; #3 uZ:+l^bI7ǿo:K@}ɱ ˔D:[Qw,x"#Wlf ! wm"&K{U\z|Fѫ_XSr7= oO`inƁwu?P8'ꎗZ7ޢO=G7vI7\F"?\IbHJ?~D:*Y]tϙ$P=r\gg~_Rz+zʘ_(O ;]Hd =ڗ-GH H "ktTX]=-1xӥ$ a{PfhڋK󋽾#@W^Uo%)l Xَt1j)_]#cƤa{6V}O_39ֲ]f$0nFV8!ޮ2LZm^G`8%xExC%HlfFX>9y[_KjDmGk?T? f?{X]l1S;Qq4b%@aG2Ow塀]$y_)tҢw?dW5!T=$=mo3?pdGZ.{ wb +=@kR?ȩ'?@) /,J^zK) =edse,`q _.GJy7ʩʋ\퇲})Px $Ru6̹zziPc;~B!o}rޱQ{P~&2w\D(oQs~ )9cGE4fǙ TtyopptYw dN'#oDi45'Pl@8QkWʋKOn Ɉt}2- _oݔ_>?tǩ#iV8se]_ Ot=Use'~۹ h= #t3,4m'5oghhTR4?yE)]yu`l9bx:? g Qz.R~KRİH T *#$@H $@H $@H (A@D$@H $@H $@H $@!DV)@H $@H $@H $!D`H@H $@H $@H $y`Qj@H $@H $@H E`"1$@H $@H $@H tAyl"$@H $@H $@H@AF $@H $@H $@vu[H $@H $@H $Pv)#AH $@H $@H $@!DV)@H $@H $@H $!D`H@H $@H $@H $y`Qj@H $@H $@H E`"1$@H $@H $@H tz٪$ݔ@ љȜctCK O#ZKֺ֯iٴ+ Ok?FŤڛ O5,<<\J:ѼU֝|W~ gJK'cc}%~|N%g >Kzf7]l/[j#GO4G`NcF(fq4\|MyO(%]v9$.֏Rjfҥ1\SYZҲ-g rn_1o|<IcE=WU9pIC2D'L$/T=Y82W<[<S0!`=CR/os-Cʎts7ëؖ⋿H +'s_4IN#OW,/,l-K{ez{54y_yǂGDݾ˄~]Ĝ\Z"'S8dW^lO'o{s7heN>vd#^gw%TO]`l8x4]w7x&1@QYZm)^yuAZ\bVFpo藎ۓ WWim޶Gԗ xlCځьz:!RH(3_cx A넿54:tfצ7k:{S~ p?sgVڎ~sMmVFDž%Q=3$F3u>lRãÒb]z jzIS;z&7F״K3(+T3(ܕlx x O^_1,Ohi{rș*\92 znnuS"S&V2pGG瑑W)E`QLQh?8gY\qont <u3+Ի_ $v?ǵ|jbW vK"[KSe^J0ֻ;ǗzU MKtJ߫w?{jFui Cf[3/Aē;0(z+lD dmˍ__H++0.iϯz'{ȓ:<=驱\=XC09KyAHwӃ>d zo_!Y~-#0ã@H!t2*xsCV\K-FOu|'B|z+!C }3 :f0MmN/s&v1N6so}#2C}Jt1  C(q՜ y4szRwc.71k+LݏYC쭟16E{Wvjeז"՟5*:>M/+6-_5O4kiG}u]:chtnO\0)5htO s?{qpԼ_ ϸ؉d'g#֠˹Ցޞ#vx'{ڟ-ٔHM !ODuXDD,1Hr6\K ӑF)̐4'z7-e<9ilWz2T? v>or,AGR#]zJU;=B`#9z [^w?t<@H 8 Hl< _q;f.d O|я.a^?LW˙؏Fm#j/x UmKo:=?Wո k,]|#?8=yC*9"?7nĵ%]MiZHo\yP]`E9e^Y>[@M¶{^|F^9bN3oM04gTO6]s wX.t+{Y;'dzI@0O,TԢ8|sow'y`e5s%f'D<@eEicorv^2spfY=m)k."2J8=bChj9?⧗FM`;E~:ɱzʹ8*x9-e* 3(>0ƙxF7N {nvw͕+?X|#drFnذ!yöL<?hDLa;-p דI(#[9u=cΞ%{iq{xnO% ZFA«}r? Hk-my#VfН#S~>ؔ{JvC߰1M|=K:)qCX_1dh:R}B\>ɫGvzf\O9rˋg@H`oPOZy>ATiZ^9\(pǙnK7f[^}'6=firZFN$F[*N5۟Mid'O?{gE6#ɐ r#˲#r( p )Q "⒰)aA߬aeVP *EQR0 d&3Ud30U<Hede1\d֓I>^ G'$W\¨Rji*z;j }3;~i ( ]KvW4Q'\0{_ekM_ Eqhްw: E͜լi3v M7؁<T$ PL0vAgԻv겫`5g\U f}͇gW@&zx|5;V yGtn-Y&|ICOя2>TrTh،"o;KlE8SL>\ɤN% gKs8HV<9 Swп>OpNsIfk6y$eO}b+YF"&Y=o u]q,`ۏL^8wފ~~{cE>(??cقwUGƆZ;{>}#TG7<ꅹ~pGdz:;.AAp|x Ҿ}CM* ,?ί-UdM:U&SERf/,GP1լVdC&QAA0*· ) @F{9b2-7H P3,-_ϭ/'eѝ36B Tv/ =m"NoSzhThYPU )$ B؏4I@NRDR%EM @'NG:Xu/y?eU**Iۄ[s ۨ+Bx|/5 OJC2@dބ1ضwB]f9jhm9׃K%JRTje_%**?%NNv$/K[/\< Sw#I~qq鳾>@VA B.vGJW$=M5@0 qTN_^ol[!r}~OKb0_'r6<&N$ɤ҄ڝ}/ HH<Ίyx$SF~Cw_}M> ' G D%̣rQsrzdTհȮ Tgi8+2U`xW修O~(_yڥoJMGP+vYR[#_諔ϼS;}%;6=ﴅ1+~XK/Nv\ph[[%VT~͟"uĀ6$93<$ETښtg%\T؄eqwϽP7cq2%]X+Ϟﺒ-m2>w̞M~o!NCW!U-`$]x"ta#yAJgxʓn74Vu3UkЙF}O籋;DÛ؏vA)Dgijbۢ<>{r*Mo6d2t%rʯU^#N}͟nnXrRTz%/>Tзn n̨R}MsE&۾I|m{^:~ԱҖ''& 5O˲ٝ; a sl'{ [HG<lGzly5धF3A|k.@QJYzsg+/Ikզ3LڙkW'_S҃m? 񊃾~WG:Wi? C^1}p<z|SOW5g;*p@¶jFzTQOW1[/E;|F\(vGPVڢӿ4p&k4I"ӺcgW'lDBgH :rm5q~qWpG1Wh\?  5ޤⓆO\_YGiW6?7B4~jƊx{?UH XEuKƹsޜ3gN嗲wbmU_ Cf„ ɣF_\_u%,"4ܽ>R( CyxiDGWfϝ;EO3Mp) tu2D~#}>BUp)[O/5ɓ2e ,oἸ4hsjw-i8|ڎdӗ[3b=~7_8  @ٕ(2 z #4].K 1Q8XSo m?&yɓɨP^Ǹ$eWmSGS<^»#re(.B\iW%)Ebғrw!Pz:RlKdH*IiwHpK6.= )G6}fRF";{C U6Fz{Lw%Z/G)t$^Bm tǷ̥ԅǪYh!ԠWMF&gX5}޾xRIcDŽ|em׮H].q[^Y̝aTED{=vP_dK|+E;/4O|鳫T@Ljdi}DQrB暣jAW// = W-0쮝NAC؃m?Oy7Mܥ3]:kl8΄"?ãy,H=}@@%jo 7+WF<^E7;҇Jb[a1vfwL@9rᝇ](Mxg|eԀdӖ[yk9 kK۹CZ܍7S\2":݌|Z͙z(d=隂vVy!U '#g.g#=4M}M7mm=h =d.屒ӾV~t9Pd E'(}Ek}xnbR6@PNBHJ*[Dlg qg.Ggkbu [b+nN% 5%+v$r[/]|='ws}- }3 h7_uk%$/N xF좑=C1]넖i}bBtݻ\s}-NWz&ĪYKt/VC\=OU*zmIGN]/Ɛ֍JR$ X{xdIb*lKHG׼>c-wz@g,G'ym!I {2a{)iw8v$--LK\7Qx~S_(@JmW J/o.?}/i8? EOM֝g;[ 3y/e&H%aOal:{?^B(n0[G8&x&;BͨRG!@jXpr ;8,]|=B-z+DKٙBCR3\ˑԣGesjQkiRPwzT#}V;dS'^o.} qĬX7g&VMJeR 6e+gaNm@G]7L+#¥g@\&V 5h@>;Jg΃C(mcR.8;q0}y泇 R5S͗MG(oFzҐ $Ug.G'#1upd;4R^#pH/89*O$ntLB!l󬯉  pk<@Tf"X+$2Y;X$Gxѷ6;! wk_iG8r3KߐT9ql?_VPjҺ1OXÒB=۝XOB; Drkn=v M^BNm?Cm\fڝ8y$ǭPk2IrRcjO2S/WG0Wm2Fe'l RW-sKy ճfHx2']k4yz=؏9 hmN MC/n~}T"̊ u R}M̓!}\3`ZLhReD*zT1 uw:oGcC԰nK v"ߐH ^Lpg7zB|v ү;⭄Xcfwٻ|it#CÆ΀'Q)!UR[!"qf v'ѓ=շrIk W޶iQCö?UZW9z`4uҟ.Y݅34rOC;VF'\9CӅ_z}_ &W&,LiG%{vޒn;Ĩh ٷVV09^H=wzOz6}h7{8_=f xcŃ~rBhp2x峔)vzM#*cֲG{+d2S|]_JTnbD% G>" '(aCw~oIHZ00 DuU~Ӓ!U޵ץ׮⪥Š;qǰQ.lrG*,u;O4[pF~n~&HCꯘO1 Qj%) Jђ͵yVM`D7s٦.} =z:{]+ ppq״\4%Tw۔feRB_wj>OUFOjJ]TEmF]" G1!lGXy|N>"֓컑(RS_; C>G p 28iQFV^ѲxYR&57Ԛ~uԴm`]8i8X஭a? Z%F<@IDATk/7@'Q ydYG I6gEO2 >->&Sڅ_0Y&laOhQCcj\~B$ڲsSQ`z6B_:ڙ#Czx%zuy{MHM灌]MF$HyR)mn2)GC|%P@1(G$)-A8i8[ ۑ̴8 D#r= >G p-@ @ @vE@ڮe@ @ @ N @ @ @}e @ @ 'DG @ @ @ оQh@ @ @#@ @ @ h_`}@ @ @ @ Q @ @ @/0@Ծ @ @ @ `(A@ @ @  j_m@ @ @ p0@p  @ @  /{6@ @ @ 8` -Jh*QQQ){t`8;="B2$56lW_5}umcR+c`pRH)_E|! B&1e޴4)ihpURszDE!lrgT.Ihi_o/if“J2w.=U *5ygesn3n_-t V`b MKCT)k!-O9G3Lzpl6^3~遦bMIR9o#eJP~4v SH) uTD|c"mQiinNNWT|5G3Jf2:T̨x᧓g~נ4)ejxu .nL:T$\QilDWǔ>&%mnbӓؓ؞S1&|}}O3@۝?'"w?RBy$_;)䥧z<`xsSߛدl'8i HpHSx^fḸ8eȌ"RΒ(-!{276?'  1n? zn׶.ver5^UȖO'MhPM_}ř6mҿƪݝ[QuѢvo J+ MLϐ&#y5:aJ: >QXe]7Q#a$hT ј*N4,{_$- QN<ʑ[ <n:!EHځLOAX~'ƴrgS%mL֧)O7߽o,y%~ NUt;,;[_{+9glcj/sUB>zn4\3:j-.mVZ ,/D'Sͅ~"''m'D̳_gMϯȿ ֑BbyD偠%'y$=F*w >r'iӤ- "-ޱJ68:Eb47^U(zM]/0pO6DZɬƆG }st“RÃCRC̷o|/ʭ O!KoXt7Lz׬Į8VRĔ^__IºBc&I>/zi~,ym#=umUIc:x܍<:۠hW7xhK{Ң-ƃCfƷ^}3x iq1(Y]1gVjX$8Lb1Y[i)d&gx+5,댗$5X|S/7P/4>m|~=U#_޽2z?IVޙĽ.vpW /"BOAH1 Ԡ=e27Yq[FJMr E'&ԣ{@QiK%R!C$$W Deq0mTWVV=PGEº$ T~]=U:a2ٛa]拿\Hc͕flJQ!q*yxOvVN]zSk|KZԌ_mC18)NGO5wb/|!pR+SwQ~q//E,<՟$z ȟ @e_p0_P;T/@3=@icPKRFwJЃ@ZK_>pL:"Z7G W\hAK8 ݾBmJKw~0q*CŶQl';V(~3NKC, Jё=2H?xʻʭˑGPk%B&2;)$L괻Zʏ6Ξ&weȂ$_̮wYiֽ[xEr#:rv}YjI=o؅%];ex&E'/-ui̬{"Q؀=[JJס#֕|)j#n^mR /= 2lG$no\3md4g<=ۑХq",/c$cpD$|i#ٕEj);R:8uEMveDw{spqOTJHv.!RM(6zkQt T(dƲ-Sœ:+rkm m{zTSum=9fwD'HG9r9\pc;_ȒEf2=՟dPb[Dӹ6cfkq OGIJL [;}Dd7 ^$josq9' "rĢӄIj~lsyͷ)DǙ聪I'nΜ;r9p4hSH=0Qc9dv ShTڒDml8ް/5mJANiSݜÝS' i};|iY D> _C|j!{GkwnUh>/)Hvx?ف^^UxiɁdzR R~_LĀf2j:ug'p1v'BOgOVQKbMr{`*IaQ+X,sG(\;e!boDQ)Yt_L%+U${zj׾aERVZ<%0l1]sw;)G̊CA6zvFm8C^lKݢYUgdY WW.O9rGC(vpղ=_{)l۶m o0Lf%oFS~6=6 l[I7[ߴ'Kq3Nsų߸DS&jU+"R"GRux\9n|FƖݩwՒUpLO[,\L _H/rNt.xv]Ŧgu'mXpHӭ&#yś.ݨU4_y9l,m?4YIghBqZ*~;ZcEM~M ZTbœ̌TNV3IhQ` r`G.GyB3]*%!DM Ç#QڵˮW<#s =XJ {^Yq>nHK.}C UK M4VΦe 5>i9Vݰw: K2)ó9Ym"@Oa'F ǂj;گԶ[3*/qDѡj}BX23qt[5\-ٱ^94FM >yx#!ہK{‘^Kh\xx~!J`yC YOz Hl6m.d й ȃdܡnf,JBYFx:'eD.iei2IVO닷C*2wN~d2ƹFV+9l]znKgاop|$_bkG7<ꅹŶv)TeL5*ِrTzЭӟ ⱡ!EqxB\SШ^Ѳ衪m?=XS.k){mO4џ6|ES\c6! 844teeq~mA| o҉2*7OV#Hp> /Ԋ%K kdC-/IYt錍PU݋lnYz,7{L4<"$ oblvWuuE:~G)|1S.QCnIڣWxp'-7g n՞]3o x s)-e_p8 wBǭ=]OdKgiow*UgL/"UDNL .BO/E{Oc* :7!\;#u+|$O\M m-myӗzc8%~V~,<w\~_?ӡZU3*?SߜxĘXi77La!CDݴ7G/ZO=a=~տuXa{HME̵8"o[KT ̇S#w?ԙFb QwEGņEmTT3 TʌYUfzt~Oduy9t'.ZYK˱/G69<[ln;Z]swǡM߉M}ڊ_<$FS[ cRF+M6qÖkGV{O,XZ,aZUR{2BEsʋ9J͉9؊lG7guVX;_z7>8:'pv9Wdi TE]?XWQm?Kߔޛ(f-5 GzUJCgީR w˜G˩f6YoU6aYAs/7T-De\LImŻ$ʓn74VW;*5̉ց Ju>ۧ~Yzh6jDg<(jԧb~S\W1{fJ_dlL[!R5Qn?ۀytX"*^ZNXW!U-`$]џ.y$9H !G=֟"i $=&P` n5T.ldnJɕ_lU|JfTWv9"Y|L \-V\(Oч -~qh-3s}<9ݟ;az =؞ O C9]~W /N2ĺC^ WQ~a*~/oy[kg?i(,c!$Yi#@S"j_<sSo.~lkڴqK)pg hW'_SOyڅxfh_3C[#]Ss4pq;cycy ^'?R o561\Iu֭;2Go 濆›3V|Ґ93ӓt7(!x+s#\5e[ռ䟌檭Ig gOféP! !r*L坴sQ.]8e).7)W)^f:Ѹ} STDbDTDR~qܹ7̙oven }oʥ۵Q4Qd94+3Νٍ\czL0:9ytՈ󋿮ēQfRׇ:1cK!wG0AgT>k;R`KǹݺcHaz 7Y[IcDŽ|ea2G~0S=Jsg>!D-Ra"$C4M喴%JxԥuE]Ea{yO&,GX]KZ -W@~qĴ:o'j}kCuiO_v ~\`ǿ~!mP0{gn'oYr)tOdelRΕ/&/v  :46]AJB[£f󹆡IyW-;:8dly3fvv,>lO3|ã9R;u\](xmGMrɈhLg3Gk=?L7g.jѫz p5҃]`}dFJ\tʈGS;ԢWЫ;jĶGeh]&,,4#pF@@~q'%ܡV׈AvGȍXzb֟Bԅ{L<-;NL%?m+2{SG~t9;/89Jٻz{0j>&)m~e5'pk9/t~^=^wDKk^U,u /hlbElpJyDA)vS,=TH&;~H D')A @%.VQ!%P |I1xO0+QͩgEf#_gҗ*r?^s@Nn+6pf{ :{L(zU-Rȁ]\ UI$!VMJ,ҽ2"ϟ~rI>av3=<i*Eg:Jѷ. Ut,P׫\5rj@+Pk>* -Bѿ]ΥsMuy{y~q3]넖i}bBtݻ\s}d+1d\{,)i'ҎE5 7+z ZxP/+gCQ[+ÚRT`2pB#Qhewu7FW{Uk"?'b BR8SJGkU:} /L呔& ՟ӓ;v8n"/S@K@Dt% -36TIm.?2=Jp1OXYN7-~x<2efrvَ <e9#:\u)̻5kVȩփ~)0wܶ"CiCНh,Js_A;IUh]13B$? D=$:Uw鈱2HlȊ 9[Qx(ӗm)^$ՁK57oCR=*u"Nz#~~"خDG_7[$'vwk ﵐk <jg?I v Ti.zeOFpxB7?)hہnLf)ΐMEPi+gTS[!!:-fkƋtg_4U nj);/λWG6y8~i|/Y{6@F7{7H3ls>|v ү;mkM76tޖw|t?8oJ9.gƤ+ ߜ¯fA/tH*71x[*EE3ЄvS O?9?Sf[n*4' @F 1 r׸oxRlLZ*ip9Pa/Zaw׎KJ/;/Ԡ}{C~!}o3bp.w]N AX;wIggww*aHAq^ (< _y%C~]5T\4Sqǰ=6ʍQ K+,("7$sǍX7Dw<\1bKS3I%kOUF$ˑoYlİo)=?=ptYG 1Utt68dFk_\񊚭F(D%8\ W/h\puVt%Ӯ/\C>Z~;M{B[|WM@[_ܕ wYy"?I']' g CB@\`@[TtkQFV^ѲxYR&57Ԛ~uԴm`]p][~&K>,$/_n(yXiӳ$* Km^Ϝ544zPiX HjΙ>LE{Vligg I.~zuy{;v[MF$HyR)mn2)G#c ]a25Y]h9eOKy~ n) n@[TB/4R7=CP~|Z»ޅ\vvPd)_][Xn߷x6kgԓ:Γ/hq$v)@**YAGJ 9 qsT @ @ @v4A_ @ @ @a`H? @ @ @G:@a @ @ @#DAh @ @ @t80@L  @ @ @@ B @ @ @g2P @ @ 0~ @ @  u8@ @ @ F@ @ @ p`Ù @ @ @ 00@$@ @ @ @#Dd0@ @ @  Ck(M/*QQQ){t`8;="B2$56lW_5}umcR+c`pRH)_E|! B&1e޴4)ihpURszDIO4"f&zqJhmfM5UtR%fL|䝕ͅGXŻssEnɯ݃9{dS{ );TWV`5wOp=I'h_|MQ oGV(Y\RPc4 Ńٳ_y~!B]Szz2' @g#@HwR F_ ii^}Ww/ ?=gQxV2knat߿i}.7|APc:_}˅r+efM*5/+:F5m?1奠חD/bx7!#~F Q1P-;82^lƃCT˟_|)Azi3&멊ՖsָthmDU%1Vss7n^ Xz-.u6&yKjbԝ^i]~o Kd#n`YEw#wC_l"=坴 + | /)%("A Ί^ OC-5`O|Ey[JswoZxbҌcQbͭӡ%DYzl3++b* utH\*K@ՃXxC{3}O12G܌*y-ohʬd:\nPm`~ik ;^;PTR(q=UOc4~ m?{0ӽ,拿\݄]a@#Bۡk{o[?IޛB$˟DԤ4"[#fvmHU&,ͭwO2LOO@~!B= PN'Lp@t>Wem|=d=RTb|%Fy$^>p wzp78Hu>3rO4(=BmJx% a{x}ŶQ?j~|y!P>rO6n|)Yۋ+7ixFP=#HRuOչ.}CmVX*o.x(6ؓnLX,#+#J%˖FΩpXóY@U)wX?>d刏Wd[Ao_Y1ԢW?5ׯ6_]RcP&L&5ibwzr퉏p{Bƕ?PtUQv:m2nգwO,ukLDytx4*?|sRO^" Umz JܟU/GfWsi="{ o6NW1%>08~.Gjw:Y% Zi3/fYF嗢e~(|{dG:GNALJ҃7mCm8CMٖ.Eѳ@ #4& .}4KW5ܹ#TἩx/h҃@`ʑ[PVn3J_Ӧm۶o+8-S0M8CINKi=C©{/Ǜ򿰝 aQ 'n/S$\B$#sɪH={5mC{Vy޵)5ʪ qDI%~D,I 7vXޞFHv%~)GڗmڟߛB$˟b?|M{]5O'$#/¸nwazK; @%@-u}%{SG5fnm3i8.q~W)ҍZk{UU M(NKz#=LJwBXi_yGώ'W\Pg$H<@IDATRiU0ԐgxĦ=#(1VMa<ҤYK+V<)He N%vfyWnICX1x"䗭6}Y;8i+]4߰w: ]2)ó9]_ & Dxʌ~|Q~BCf8E;$^;\Fv&q@^٢ARBd|DDR ;Ct{BhݷuㆴDjkjR۾ͨ㝝="@{8!Ȟں{G6O`)kp\v[nG>޵kW];UyF>3r~&/`=R/@s3yɸ|ݬ;bFY"ೌy sXO /\l*iyhS{ZP)ZvAFX,>d"UC-j{MPRi<]+\lޖi;:S@h*fj *\)Hg,*[T{+Z=T}^Y?=XS(zw QCk{9lHñXw$ oblYUZq T$cch4g-˟b{%JRTjof_%**u {G\;a{ VH5i8E]n&ݗ+DR%E۞%i?Wm,~Dx<<.p"Aiʓ ֹ:\EhQ=}Cݙ3֞$~|i)7QA1g HFLnI;xfO *:*gٚtgu]T؄eqwϽP7cq2%-O+Ϟ\IFXJi;ܷ]`Ӝ#N[Hq9fu)h w喬XTiYVZ-D#6+OeX[s :su`R5O_ L<]a¸&m/㮙>J2m^\"ki&v#tp"Vnj.4Xd2[*(7imUڷѦ!nvp;v}h'fp$/4}Ix[I>"m5g&CHHK"˲#B UA@A]9+JЗ5bŠAQ#( \JBBBLf23_uO\tWSS: 9҈ 5̾_;iMԇ$~WEeq}-E H_r%Kwvb1 @" "ϙ|cgʩ7-G Ӄ@*P$9 UlrMPq^ӟ{OkZ#bq4C%:zw ( X/V8~:zeO^TSx&u_(fvݝ*kYZ^SkT Xr}'lyt<}FA4Di(+bEto@IqJt AeS\jW,*F[ƅKĝH; Õƿrt{Ҫd?>G >H *!\'&5dMBӍZJxa^;VPԻ>6܆bZ>qzC_"j[ʎ5Ι3ٳʘU4=X,FEAP N#UD{P#:"~ΜYi?/&Q9 )_NR͟??1s}WAݔNGIxTI^D[Y$u[>Gv|EGHe #G_rI]Hp\cmɒy~""ϐ1@R9Lg/~>˯ ).?rǪb[ 0w{ZjYuvUlqOmU\ƯTWN"#+2^xJc7 Gyg+ZdL=U/LcOn+9vVE(+wo"g-J|ϋ3(}BB#a@{Q2s0WNi\I~޸FK!ǽ c"0Ѵ9rQee@3Px}B9/jFeKy8 hr?!8{{(>-gy Vy'`Tu'9\w{m݈?(@@jCc9iшe#X>XCA-p.r(UJǢ-9=[vFv-8Q0e+WQ[gEPN<5~¬ gM]V9zT3:*%FE\|wտxu:&djY$MZ oC%2PFH?.QR#y=-Aٹ:5)6fhCV0wF3Bu lr^Z\f3Pѿ78)*70I/eek^l=&,| oOhq6MQJks}BbY9b㛯Y!t[LuO_CJXycM9syڡﮟzb=9Nnδ)y#oGs&E|v3%Og{VX_mjңOO+QJgP>[d<- l Sv|RKw|-G!M{qIj'y4[YTNH3y|϶W]F+g~@? ߶4H !|O@A@B##4ax+}THOuk5o(boF(,azQoB?{Ή (P֋Nw|NJ@nI?Ŗ?&פrV`WVpx{}-d'(5ʞIa~g˵&K?Bʅ,_{S`Lw ҏ]# :j`TQfv>qQxN5|qĺgbR9P஭H~:[=,._f.Wdhhzg{8h0ѣ*oMNPGF5 EMY1#[q度8,Kۼ6?PGW%$_/.)&n8v>h4B i$_hq=&G*eG%ςIX]fT7[/Vooh.h |JsYMuC>Q!\#Vy u(G6WrB@ 8 (0RR ̿*V/P׍,/d , (k];֝~fbֆ&r;g@OmbC\m7V?#wI/}#:sgRPkN[u7|Y?~q NR9A[kL8y]{9%K_/G@*67&YoX:mI䞰to'=XC N$& <3 pCAPprՋ6yEl>M 2+[s[QRC3'"1p/Wugohp;^n99x8IĔdET`~ >AR}rS)= 0=S)=Tیt ͟`B}иnOUѳW OPy?$<K[D~!)Ay Ů?rӕzUa9ĸ(:837UX 1I Ѷ-“C6_^^5xte欱qXeΩ#q5Bkл9㓜ԣo}’  Qܑ:N?X[|y).V[3].khtGq>?ڬ >AR}r4vEz-~BV4_,"vdK#cBW3>6wzzs ѷV^\zńx,Fʭ3oMDμm^/D]GZ|w0dNZTq_^W{3\$zP W>:a@Lzʝs_+g<pBчǖ+" ?) OgU{7A⁴됢lWq{[Ͽy}̜M7T+Dc&]=1 _dr?R9Od\\7јɡ__7rJ37 u.l'11:gBCo *Pro}[xrJ\RqL]B՟419DN16>0;59d/=\C poIˑ`E1{ҹV=*zwrЫ[Yqy*d)1)Tk^fA?H'J2 \c":GEzߜ*~ K7AR}r\ i0 -i$YdHʑY)LJLZN݈?/V2'(;>cdS=O{8}nԅӐ鱋s'=#=ˏ5,.@~bIIJr+]=/e%wBOzTb#>6B&wHINL:X˻1 ԡb^z K5|kK FIIXs> C~ӶEO~yPDR) !?&3x@k8AT-@=[2̑IV^Ml`r'wI(r&OM/98!w~sjmp2Exc*ti喛3gOi>g-#=*4初XeRWuF$ # %ڷCԊJWXZr+IʑH$;~HHISMU޵9i!s,7"׌>y&]=V/x˃${ 4N$Na\gt@tm@Vxa\_dӚ->%jś@T zPS>۾ϾN?}ip\ѫ9ZN74NM4ݻb~酒 &ITkrS'˗?ʜ2mBMHMЍ ?존HuݼekoP{(˻T/XyhtK%OMW#!DJV]EUI6j9-N0YqԜ):{jIe  "[z K;m=ґ|_[I}lm sڹ@ɷU72t#,A'8}r׃HHQn}v?R}r\8$8B YL w6V5(i{>Wĥ.}m"%ORV$.R9>vq'K@-gSڙ7',6Bೌwk<,#R99u*VMOŽh~ ӏL_&0g>~|cBҗMa 4Ef'l ,Gf!54}~?8)5@-l'jA^@Ho=B ^him8OwMOLG ^t_QvkN,;Wq;`_@uA0ͭ֟p[2Ra)q"E[z^Ltm T3r+R铻$G*J7o"Mff*PaOQHhBI /-2իOR>Q Ztg}HQ6WD"9KWkZ+ϟ6/$>3{r.d' @D"A='^!b9HH_>ߘH ^i \Q?}v?,\zndu٫iЖ3sOy,Ӂg S0E0|RygRSw͚[D,~`1._~ ~pn3(?F % =]lV=`*X琷+| ZEw%^YYwmIJDW:77ZZʭOꂸC;.ߙ]sms&p޲m0ْ{"c?ޔN}ěÕ'r+c\aPvQRlP+d]5-w"F >R9Ҵ'gC뫛5vg٥>ێ$RLGFT"xg"~/ޒtKzuyl;osKRmY7}Yqo$J"2}Ӵ6z&Jc:S1_AE^70|8HǓa7[`IS 6j+6x|8SN9F7nN2T.P'rv$ZٿM*wg\W7MinAJɺyZށ#:atBo,Cth[f>Oa~;1CL:C14sb)XɶBۯqzK.?W.._/q~QlKuUo\?;DIdw\=~ѕ#-*/䉢 {ؽ. D !\'&5dMBӍZJxa^T-hAO8ZCVo%q4Imy!1k);8gΜgn7+cV?/%r+B]#JAёs3r&͟T[q.HFٓ o'=!9`#1 鮙>]w笊|yX/qOmUb`Wհ.7ѫtI0AĎX~95B/<5TWsֺRet}y"1U4H>I HTH+ic i$ H2J'L`{ ZLsߕ/[~}ټ~猏FTj=N{0}ڿ`<=/xӧOp7+e/ !~qPp@0Z3}="V+_R9 J |hQL_<xzw͔'vr;YxJdž,`:tRUhdk9>Ga4y9".)*>7L]RWD=2߰^>c"sj7q.Sy90.K_|@dR;I>R9R.lr[X)BRݿ VX۾nG{@k /}B?¯LkojUʝ_$) @ Ep@.I#8Wv.?H\Sal:T}Oۧa;3[۽xх{I"G vo_Bveηo**π~K[￐n+)?%벺 ݴzT2ψnPS/]FZtKS8ag3ۜj1?ɕvr-A5 V Pju=nϸP{H$^g2 |t)Զi=wvGй-"l>4}-QbTeůŔj{2}r׃HH_n}v?R}r\3t#+ihx[V;Eq 8N,L3?˛_B~ąT.w1\RɅ܁n "܈nIQV5id~QT/ p8ϰMMӜ)zẫ)K2'&D>FN:XDMhD߾%%Cn{KB%Gj't.N@Qybwʀj\X9պEO8.qR9< NO>G/WG/I 7>k BQo8^9Ï-rWVS&{lc9#oQ hW^K?Co68Y螴zzڲOH9U{w&[dx@_ش_3\BfW"l܍J\eg!ŧ<ӥzuI5-@ʌ0_=*kv5ٜ[%L5>ř՗ ;8ox^|5m0rC&O)]##r#TN##"DNo^Ly7pT߆TkN68,2}B%hXB Eʈ闓˱D,r|ɇ@\z pٮ' THz:ޮEС3jUw*j+IOurᬩ :GTcFGĨAnR'KT*WhI *F w~A\ۈYin-23#_)#Y;I*GYwn :ws›>v,Z\G~SRGm @IDAT\߳mأ]|P W֩I1C÷w>5U9[7z`SK[6{[w&'{1+N_CJycM9saڡﮟzb=9N^NDJ1Ҝ9ze5(umOo(*qv!~G>xR=i;Aȭ?kMC&ZT~gF/=?)E~aSuLӭ46-fR9uǧ~`΄2WRvE }r׃HRGH;~Hki3yr^Z\f3Pѿ78)zG߿'ULV$crO ֯Z~9ނh\ ?с ] "GFh2f}THcHQ+f"zio֫Fv4Y>عʂ r>Í @TW^UoE8>|$eTآZ!,BnZ7/$rN%hM5<'s nlg5>_oU)9ֲfI7~fVx/*OY}ϣdYe$_˟^ad+YbBr_<՝]עկb[U"o4O[\C\ ]7 uP ?vʥt#џpmOy4=}p\*ة,r4l-Fn.r!?bG,xfwz8:zԅࡏ7"-zonW'w=HT4< 6R}rl6MoJ[["GR|@4Gߨo6M,H1&P"nA'\<|J'TvrqA ) Pۥ̈2S7St*v#E>CwmE@3a Vuq2sAC'+R;Isʆ=Հѣ*oMNPGpBQSz>c-q)u#W~,vl3M@ @ @ @ @W!*x @ @ @8  @ @ @t10A @ @ @ <@ @ @.F&XCt@ @ @ D@ @ @ QKp.@ @ @ `@ @ @  b @ @ @ LA@ @ @ @#D],!@ @ @  "@ @ @ b`%8D @ @ j FnT ih7'kzFi*4hbm{K¤rG Ri1yWhzvhZRg+p|u=l/wIϐ8էMLzn`:r7 3Ԭ9tת6|9*~Bҝp" O 3&Fl6ˏ4ן^&"r7ejۭvoZO}f_%)b:o?j}^0N\T~A@hyigj擊c+2LlrアƬsE DC@ ɍqoj;՟02[񌕫 &H1  Ow]ނ6PiDp_+cPr>Q|CدcX.. ~*Jiݟo&6i~" >,6|?BKd?"NTZK~Ə)=__:dgĔ#(@YJטWcq 9公kXcǺoLT7c'? ][M׽;{H }7=~N?oDGrL *wix↯֠#}9n|0gvor3VX}I? ӘUq1QbxiK긆R*<ט"KrF="KZ= V7ˍoc 18Qv-A-7*f奷 ژ!1E!%U)ek;;ˊݟDLIV SXXQ#0<[ZN|z">0}[I/1 m7)qa-i>#mECDX",I_q[zwAXddpji}&\ND9!o @[ \AdD9/ԥqE@a]7~Hjg;s֎?6DF9N8%ziy “>݊'jO]֟Η94^^e={`կWNHEi3}\sOחcHAw7EDwgsG-v.n78(ҪT M|uv;Aycgn$g=ݷ(,i@(y#!%`DOto_y{{x>aYc⵱vş)S'vf\O~.khtG\>?ڬ^>?lCYBZhGv{|ui#41nFy%r+qG?HuR}JͪXMb\^Kk*jK6RVygr#w"XVxv+|AH(V@L?Xt'-$-yD"4~l IԭC䗎~p91( @WeoxE3=#^e{mŝ* r,:W3̛{ 8+i+iE*f+rΪ'w  rO oqEش?|.IݍMWK#3}PcQc %s#g ?tgnL+er2ܳl㓯*μ52k5; ʪS rBDy'yͯy֊y;:B<ӓ OgU{ x< ە;*j6gO|zԌ>\?O>屯,U~o^3g v.]aj ^ATޚ7,/eu2@V.1kT0oZًn[m|`dw*KB$/d(^?i9tuK1 Yyc Oj[gg]!k+1teT#Y'PSQ*FU4:]X*#L7=(-?Yx"E.&k?rܒ:ZC/8=hG*RLzBv)v<K&@ݤ$L44K712$.!^" IP$w%xSgĕT@s]=zE#b[&|׍Ǜٹw]ʹФ2F}I-%Z`r?}3Ю2RyVڻ%pd؞%_|ni !1 ЬѩC+ KB}x|s/D.&/#I?%%x5 #n~!ޔ};G_.> ˟vH1͈FϕXop7}(czH`zXe5eYԠz\&Kx‰_8nj] aYB I3R9B39!Ay@b_[z ~~{@/~<Mg$rbˑ?Fp@tJ'Jкeݫe&92ʫܗ O>7r>tPBOLSKb~/9X'w~{jmp2Rxc*׺3>:H<<%fTq3p__hҝۖDNiIa$aՅݻ"^az$ǮLΥnhL窳9sFh,4nKXvL3~7oݺ5i#I[ ,~(?KOZ:")vTE2>ԾVx $&PS={X!Z|r$UoKK=JlR?i>rhTCUQ_K[Ne [~]`pÕ^y< U5# j "!x{&IwqO6PD?o>N=7\w%iI?LN\9 F : oi?9kb_¸`Oɦ5Y[x}F*'7審V)|}U>ȝ~|Q4O7ḔW3rD <.~inh-hw>s_\[4(`U b`NBM 7/ i㩳z+#Suv6VPS_rB$Rq=?iHCT._*+s2$@`.ʻX}b_?Fڵۮ;/zFZe0#ّ/[y5mp2KtDk~~^R՝4)k;'Wܷx~i X'wD ;Џol(l #{o$LEj!ԨϠ?Ewч3XL46_[Ч;*AilǛt"J&mfW#KC 'yib\H8yE~3'`]>q}✦3 @'LC?d߶|>1\M 5pPڽ& ;t+,3OQրp8*%QQ}TqC9w+%QTMER]CAN$v5 7YWP{2~PFe=>FZ\{`퟿0uûsmcͩs7KMV&P"rĤ_?|F**DCj]GJU/@/l |u~ -\ :-Y'yEރ1{EO`a}%"}1/Ӗ013KkZ, nh5$@ o,jL2Pf+ptӐ)1=3575*$ ZލH>WL|WkYfs]MK۟n٩ͯUd >ֱ29oӋh+,=E m,'7-LMo6kkʟgm9rk'Wao_ ;}2_c['z)?ސ/E؍Tu Ӟ߯]ë?,qzԣ;&'E9\BpvY!39嬯ŵG~4~DL΍?>9K].~rƊr//%z+ )O)Y{-Gk4qMHx$΋q𕖦"%s#{pI/:H3R9iʑ]@tJN͵Ce.ي&~Y:з{2H\zW9㓨ы6p4˝~B|Myw:Z hC<sKV^-EtL}Υ:[KdB.1JeŽ9SW?$2(?YsB) rϣ=딚oLfKf?uAEeg~*_ Zx iD?f߯4Xjį/m72>/LvQqD[++NT?3I鑐Ji`ڈ1{rܡ *gLd޺~7~izֻNsBٍFJ BP\2jO-X_ݬT_svgpぜ5y{^ퟟ`1:FáV:ZC) =wv`㰀~[gyJk90_i>#o>i?Dl#moKBR=@_c:N2| d vo`heeN?TNv :!j+6x|8SN:F7nN2T.P'rv$zUb U ϸ`ŏ3-lenzNs{ WM:'/)+"vYwy''as'/ނ'<+ΑT="3__(˦X?\G*[ƅKĝ;- eݡT+aG+!pG Y.+#t2BWм?/J>ڗcs̹1{2f//BȊ9sfimpi=G1(ِׄXSP~ݥw\=~ѕ#-*/쎢 {ؽ.B$跶!O$(r䗍$~s grKD?DVBQOVH;x|c;8X_?]w]l׽VIN$)H{5?xou@U-DE+v)"0cz|u]ɉ*,B_IVwWRCq E_9G4Go+-s5Z9[*D|ay~FBjB U3gTH+iATA+1#u}Mak?OLӯSkqXWbq+,#Q|WԟAIV O#_~#ȋ,t"gr<"&BT$I M{˧^S {8_J/gJuXm|n %H3R9'r :v1ADA5zdR_ ' .Ѵck9|߄Qc),m_s 3ǷN큀sT0aƥz$.O%֠ӫ;&f U$an]fi:K˵"#".Zfq)bO|ss`\4zM1u[Hs CQtOZ 7|ވ[䑯YBn-X2hxd78;ENs(>שǽ'dސ&@naj(EYgjSJÚcN@FtW? }g-f$I#/d)A֯Chޫc'췶;;WHu$gOCXBџgqLw_7-YD?*dĸtP;I?T.Hs  Љ *P\Pgxie2f.WJ_j( oroaC*PI?'oC3& r0]J'_U]$icjw粟z ,csa!k"wrr<%XsbFWt0 䛯M8N֩8x&^lt  +0CzmF>oQ hk~O!1Xؕ1Vtrh{胝 Y*̎v< if =Xv/v0ߙzӣvg>Ayl+t,~Mb_#8:gkO@/Ó_&KV_n*L ޵eo9w&QEU]ybOQ="nRL~]rCʡg!G% '(~+b!73!˧ݔe2i>#RK1oe\ױݙ@%t#lEo'6 n118ܪn+?6ԮEС3jUw*j+IOurᬩgl(?ymo0"" ( (zWTD(*~-,'' /jTE1oJKKK[ڒIg7MIvͦ))ٝFWBqc'g>(tRC:/x=@&=>9.k_B*9P~!eqPJH%/0!17(GcF[&g\4[9C˩_+V8;'Re9No3#jz˔ Lxlw{'%ݼ\:N!WXI")v-^e[r-1nL}{~M1Yc_QS=dwM#N@"JGи)R&u/+ΝohA^4 =mB}jHfT$"УcR"xkUspz(5oNNP=kmΜ&7@'-Ԍ>UWbj{&,:vӝ?;fA zȈ4}dLnm(*ya!~== #+l3oګEnSZkV].~SeۚނohU^ܣQ+lmc߂Pk둘Uv|6q_{MR/e*O_VyFτR^7ebH1DqW}HZINV]"-_νM#NS>Bun8@P8M *p"'$ҭysyg+JA1q׎c| j}m%&Id>H瓁WUdGg9Q!J-۹8`)J[u?)<יoܷ(c$1>R4BS!Z`ܵbVJFպ Rsuw?wLh E>էkވdQwO>X_${`'Ux+ upѳ ^=mP*#+~φ\7Ӊ`==_s1}{UsЫG=g ˈ I8xygDUWr5jMÃkK#(!jm`5Q+.)is=G7:ON ]7 }y?rؚϟ|tB~'_VrDGMҐuni:?v)BN^Hb%6<2( %Gڞ#G<< v<%1L}Uk:\8ƒ2!_}HG" (ezD]]^Zt$)䅟Ǝ/U(??K015}.Bړ;=v{©C{ Z'i9{ $cS83.zd%Ϙ=/}U8N\8 2E_*r"F^[ZP󎟾>V |@^; f>Z3wZo+u3.8@)0k9 DzDF=:[ɨI)?3I$%d1!de]_o]")E/:Cdݤ`Ci{&$zDV4H2E P @ @ ]@]P8 @ @ @D@ @ @ fp(.@ @ @ `| @ @ W @ @ @   @ @ @ #DW@ @ @ " @ @ @\a` 38 @ @ 0@>@ @ @+ ]a @ @ @@ @ @ p+P\ @ @ @E#0Q!gSJKMмUïM할ѪfX{偷lEL.ĤX3|G%l1eA齺kZJaktT>a=N6m?DXk9O@w^W18bfBґ؏) w.fΉs8l nKaq{cZ!W3캤npx4^l/Z.<6hb.EuQ`Ң\d T7/rv}E]74'zH'pט~fTsM(m}$S}o;"ŌJ.*4r')Ot!h:- ^j$~!T:44t} }Z~e<=ΰÙB6|$qdjwJ_;!1 i'3FV݀>$͞vH^(CNzz_ @W$ -1Yz Γ5sJY3B[˟MsjXOO[{4o.pN 훻Mн;{NT}6;;m?eG nO`+ugEu~ Hm߭E{N\^18;tGہ==Pcfy4٫4/xffSaʿxG2 yYЁzƪ_]/.u).B`=c/tD\Ӌ] TrMwaD^7 o#4;Yh'R Z-e[ѽ=q/H]a,,sNպ)}Dzh*5ݜL 'i:R ?E~ r]2y!Gj7,b=ez 8* 2-uC= 69#M[CnON/gT̚Sjl\=>}nCxt @3AxpHiإ/?nTI3T9wHT)o%+ O嶟k b_hQ_ǃC;˛!~.9SI)(SԁP;x2~yN@sb+[ͨ#{zA^ܱ9ޕM^zF|{>hM2G]`I$_k#O-FDꉤO]%?.DO|q5TT4]jsm6>Y1$~fk7^Aު]b]KQT$E6ߠ6Ts-vR'iҒZe#_w]᫛O<;V0(jE_0ތ7'>Gd<|4C%vPZmUP}44ߏQE禸l.yos/-<*&I;ў)vgk}UZKY>ir]2y繮fE"FO` @# x(kit)Z?. ӥDA3*EO( `g2w5Ad>mݽj4]$h'@f?3J2;^U ţ'MQ!S{TEvI/زDw}ۡr CxpH<\ lr:cEP832;#W!~dѶFA.9byާٴzYRLx="SǛͥCk. >t!T𧻓!gZ&Z]髃|>;"gߥBViAn*tW3닖/1O xʗ绔0-1OIO$ԍ%Z5Whjߣ"lxlaQs{7x+HXڊ yD?(iq{??v|bֳ=aG~z]pi&%fƃ~>:Sȵ E %򓇿%{u53+3lҕ-0.IsrI;x,C^nCgUK݂6[^&EЄDqx=ǚ?n3V%~ 2d5R; KE&c {:&Tݽz>ӎtwg&PpML^pmKRu[VotZbƐ Y} iɴ߽?\HE߇0/8uS"+_ˬ22؉^ }?Rgq#󀥭PSώȁHK2+Cy`V i_j۷w۾mOgfպ|ǚ+PnK ~n ZOE\@E@-gq2ճMu:Fk+PW;.&εHY+Q!obŴ)T٫,XpTnE܈'wXAiKypLdUp}mgkcוKΨ|'N ˏL\ (-ʹkl#&N9CiDuSB~_Z0wB|__lG܍iuRyGrLSYz5VJK"ퟤ,dr{J2x*PM4Bf?ry$턚~&6~@ ȉjNX^yRvo!+_P+OD뉐,"N!@G@"X2|IQ/X/麞:OWgL^7tﯥ#b#qs7uaϿmvUΤ\*uGYZXa-~aQ2EiBL~Զ<}?kwНR\nHԕǛZ Js5Wھ|=]=lx\^, $ /F$<_f J?AVb.@{b_FK^6m-i(an}c~-e}ƪh]]3ϔe)G,cps/\~0#ף-K5[YrNn$IȼDs ݅?^# 7/eƸ㨙y@$!**ۍ}ѫs4{اGow$ MN-DōP+_'J6v )6&*9)5&-QOOgOYMTay|GnGf?;cDzz_ @W% Qy\ǘBT遙1}~ĀWwZ{a't9+1hotWR?.N~o_Q嶟Py'PO@}ޕBiOUU?5\C2tI#OtWE9KO*E'dJ T?<ԊPG1ԝx#ꔚoO~-8p%*Y;EByxMgYcOQ}xqMrE b.YjIJՊJܶ5+{cRrv\>d(+naɦelr:N9?y߶ =Ly>q@ gZ5N3mH')Ot$@Es 럈{2=}-VOW^T*_V+~ Uonܹaxdj̡iiڞ?cd52I8g(oXÀ󨸤^4Xj?&Q i':z!) SV}SVhc/a{ZmA6͙3@%9'i:<r ݉ 럇_b=}U̙3'Чsf7-*6qffCt_՞!wV1_.1A?"g㵧'sRj޼yiAM($w+xԧ~O<SQ 8<#oε_cQ 0/ SBMg~/ z5C"td$rTmNUL$ԓU>=*q!PfvݔQzj ^ϳMh/>O]~|#KCuœ_n"d<1k";%*K.~ E_L &W/agWyD=Z<1^)󬩱H;_V}~W#>vPq+?^W]~i(s+d8yb 6-26%Uu>8{\f([lw~8'jQ\dQzg]K-2ūns%;㒴BIE,5 ľ!Ol58!!٭Qlvv y5W}Cc-_%kظ|oh0KɊݷo_"`\nBI)l\5y?n%~"k'x-xL߳=Hna?>0@'&Г*d](4=iu,$!i Y)bnLpPі?qMĒ.T?owEte5|Pfj<=pV9M"|zic]JWye?JpJ2o+-iɩT__+(gAMύ9O7u[)8H_]C'Kb$"Fۺys~}r.9uc)7;ۡW/nuB۲iS4vߵ֜9qBOn ߍ,ZKOudDuS>*Gd7f )^H :r ^Jt칊Br?EP1E/*=s|('Sѯv&AC}R!_:uk*JT|u _vhOaR M"!&S\KZ^vJGh?qAkskm~L^D}cO@xf)EM=Ɛݣe==KHU_ c9+eK~\p{:uR57^y^MΝ~'!k>zm_n >)hʉ+>"~IēA4#\տ<Yʈn0 GVϥ3p>#OjwHLf?Ri\jY^)=vxމf.vJoځTOTO "@ 8\nAvuSA?xQi:z<ǰ{5Q6(tF-5R\QSL}v's5!ʱ&OT|Pt/麀]T2eUG'~K欣ߨwLR7; 5Ts)*ya!~== #+l3oګEDߚ1dv@,8J vׯi{oᱛS7kb\Cߩ;$˽XI"w-^e[r-1nL}{~M1Yc_QS$dwM#1B"8%DKȬ:!УcRxkUՋ>ޔy|vrzv]kwЋLKbm=Gs)lm)u nF^pk@~w geot)qjC'6U3\W$PO!<"M#>!;@VB [u GLM`ЄLd+g%WeME/-Y/9xhV2z;gWJ~dܩ o@LHx׽\8wze 4p?@<[i'?Sv}B;/z']"ibtNM|E*O 3nO;[Q 뭽vD^׎Èj}m%&u\Hd>H瓁WUdG*lGY(VDϗ aK̖CUcOH.ۿw2zwH@)|T+wpn¬Tg9vݏ+#Z0eC}ѻ'ki#&7b'rԝh֗H$\^V)WmݱC̥#.'KǺCw?wT6(EgCubAr9Pk_Q&KPH"DFIRSz$"[n\w?/4oi `OR2z /@zfH_spzRnUO9knUv/7]8<{mЙo ]Aa=&:IN45zqRqM<w\j᝛.>UUx8ǔ/%@gI"FO6r̈r3IVJTJGsėK||!M E3vuybk |tG8==}f@8ŪpԹ|{ȶhWV!IW$ į=wٿnoq5_8vhsA;1'_qϘ!au1uuq#W^y\J-K-v=i('PԮ.dWF_5Ĵ@"d?v˜=rU8N\8NHn'wAl"ROOr@tQ2uQP, @ @ @De  @ @ @$D"Br @ @ @D 6@ @ @ ID@ @ @ 60@m}@ @ @ Ɂ@ @ @ m`(, @ @ @@$   @ @ @ QY @ @ @ H0@$ $@ @ @ @h @ @ @`H$@H@ @ @h#Df1 @ @ " EG`DنM5*-5qggBW 6WFBJr[cy޲2Xc Ŕצj*Qq[k;/$gFC]cez>OW1z}^vR\Ǭ>gTOt HAGA y0=tN쐴8as_}w[ O в a%u[6b/?|vVG@|Mh}#' lEV /"4}BEsi) lN\:u7,jtnȏLۀ| 0lC>,RyV=";i:8}R1cͺ1*@-uS?[EVbl\ؒkg،0.y{OMOe2q]3u L~MK}9E* yTy/@#UQ"y' 6}] "wdE1>X9|ZOyҹftnU)kfYhkɃuN ixIyZ|ϓ =}sIwgω uZ^rgG'@1@U0}޶1 赿O>hک}*Ug`i $zVH1ob=^Dv'MLJF8Y 90QSyƑkf/̼Loe;Ue&5ny)r=b:29/(,- zҸ9.ňCZ}9X.vW<~=_ /8=%~=/aMi8@j @&@lY)pX<zikq]0pϭ?Yoߔ0dj=+O_4]C@ hRZǿ=v9Ul8t U]#{(Uf9uBSj',ڂBqAwdUϘ8>uT܋;V8޻ oOߛo4xN˷ct*s vw# f섳1t]B>iwX\keťrQ/ljUA]`zf6Ts-vR'iҒZe#KՃ>/ķdK#޺s&h'xɎl8fۨ;x»j -PLDf=\NzҪT͖svOӁ**.UՊӿZCk:t֝?~?FX禸l.79Mr( 6IĴ$7idϗ"hDdOgb[D ui>WҥT8k4dHrOYϨ?6*UUwB/#t hk* dzxUMxAy2GUdt-Kt9.2?γw?&?\ 9I̅|dzr)lѽc )jXi<8Dmfd6o35^y֠Tu;^ky9~ksiP⚩zǂ;EjFZq.D|SGV!AVVy2L>*(*?}BS}wf+e {ޘbߝ˸KV|O<-Y;C%(hՕl:'Qz١ ";i:hmE)l8]]̘* \7}B/.%$Gp?!=*AG8PgC@tuZATyQwYHU~@7=:\=M])Ht~0iڦB !m?~fM9TsAs4v p!GBшj6o4#~zB(h0zāokyUu?JwlB)s滒{6.GIv'$ ȋ|s cqek 88^FVH {QKGMHX'.y%k'%8a>Z8?v6/,xs'M'$==PI3_xd~EF>q Z[(YQlN\_V>v7\ep pA7R:v8QG@tuLh?Km!X.5:^˨?#Mǝc?!Td}O rbN#ܘV'm"sʝqx [yǎ*|ޥzj f]2%YajX)Hf)~zzO$q^GI## b-I;DQXI];ϗoYLz"l(HӱEd*{cjc#1ґ ^iRY 3ӀnF9R{&f|%Yf+}9B$q_1_|]WZH e!O҉\*>wZXlQݺ'z(㫽_X>HBwqAD 'KlˍME聿%~1)~=cjZ1w(H쐬TwWdJq7gK~O`?z&~j Srus7MWK~Ӣ=DVL#S/VK1p}yGbxeY 7dI:*Darq'(Bi$`FNPsRʳ /80EWPsmըm/C)!&,QYt=YLJ/5YnOVSo@%u-e!fS:}9[aa(_vWy, A!ZYP07i @Tu1+b>L1?_R?K6EtQa(U2{y4u#~h1}q [rۏ@ފeNez+o6IS*eG.K'i:HOZ(;6M:e,uܖ}NZh7Kl|Wz\H52kP*Ֆ}}i5;a hBɌ4'_yoK2C]gSZmNM|OUrRjLZU=Z| #an:^ΐ IkCޗ$uBR/DT)'D'$lB zNL3eQ @DY :J̜|ŌS#$vzr ?ȁ_ d/G{ &SOɏ/J*;7JܻY(-c~r*fkH.>iIߴ(gIBETO\$r7))-~yVxCP5P4~` mwhā,_VQUEsǼ{fd{Zm[㱒:);RH'POJZm?]y٢R|KTsք*N"!p=^_nsjBS0ɜoxg-:'<[9K̀6mI;d9Qq z`r:5e@ 58VBTq>~t`7JEH*j#7#lK=kIx Ћ˧Pwӗ4z~[7S4OK."祧Ss _Hn_ y$mg?zs-Wst"aD^g~Q+_{T-lUL1Pi+FGH&p= Vy6 m]31kooŵ~%$JOY;E$5ֲ~4"$Q𵻿t;MU>)b.ómbP!Rgw^G3z*J7;I3_90pz˱r"Ro+<’Yuz! ^_ oz HP|@ j:\0* 6Gθ$-uy, u4]|?,ʾj*S*jpȓwnŏ ?ݣ]_CbŨdj [ՇݐXKm:^uB[kpISL}\CD_lIqmr҄{bG{-px WemtzN}^w"y筸uW>%_| ½P{hg/κ>HғT>ߵu򑱶!"JMQ/j j 6M~4G妟i3܆gz(ͽ'N/N =9, 2*%+v߾}^Si/$%[/8}mYsj׼w|OE̯ߗ DDJ$Ti:$Ŋ=/8S-:V^C@:9&bi: 穘:TwON![`nைN>/6LU8]Qɧgo?Wy=caKF=}ÑLFnZ}r*=Gj iPSWvѾv?֙Cn"wUR /Swy|֥GKGu^ m˦O }ʟZsVnK'!]$ 7m(NJb_ȴz{Ao?m p ;?)8rl3Fdf/{]J>'=qK;|Q _L;EtBAa~ΥT)iKC1f@7TOtWk8r`N6<:xv]~oěRǛu^_#GL@}n3/H;)_ Y }UQkxu:cb~|jϏ>ճ={:p="3|48UIx|>y|R Wj'5z_Z%DaӓS.Iq)IzdGpKX,dI.a3(k-xwMuA,7j閾G㸅'UCf^g&_ Etq_̙ ,'$-T@A0#?Dpj*lu.6w~^?$%ylY3v0SؐA꓂'&tꄙ״UTlʃkϛ,О骥Gg+ýHG*x1~墼 кs-(H/ s{@IDATUksk*W.QSQS~[h5uCvu+j]B*PTl^PbvGX"TH>5;,rA(g8!t-X us^U&h)bd/9Ffɐ5/7}3фL$=R[>4ѽBEO"I e[^qFAO0pU9_g0vܥK/5UU0SeWtOۄ> +ߗPFtu=AZK|oKGg`%ÓT~ͷ@g+=]>.<~1p_'"<b3B$rN SekW:@JO$]Yo_{5Q6(tF-5R\QSL}v's5!ʱ&OT|Pt/5i.g% DfL*QuC{\mY3TOtPOQzԑ޽ׯ(6OKOJy?q&5tCP㱒E~SZ>[cbݘ/gcv:AH|c2FҁDhF/xݡGǤ&SxkUՋ>s1y|vrzv]kwЋL#DZdZoj‘#SA&`b%۵ v&6$~w>/˽.KWvSɲMdO|W2jDFOJ=Rь>UWbj{&,:vӝ?;fA zȈ4}dL_Bи)R&u/+ΝohA^4 =}γ65]CdA_j䠫⡑Zp]:TlZ^#[ /'q#1dW7uLܞs/dMzċD@t 95>2غʚ;[Q 뭽vD^׎Èj}m%&u\Hd>H瓁WUdGgHa;828D%<"z@?[b<J;~Fra`R=IQrI G|n*DS 앻VJHZ7aV^Z5ޮ|edB l/zQd>m^FLtԝh֗ xOGPїDt;x|GY޿=΀2lȵN_>3LQ f[n\wLǵ~rNXoۉ qGӪۿLO!zR% ;i:njAΘ۫m^=9kX=I.跩Sek?fz~x禋O 0/B9w 'V5d}f}y./^5k3-b~a9EDH]D OL'y2 /O"3*O"/Oۿ4  P yiDk$z%u*q%e>C"E@ P|TxkR#`e>Z3wZbU8\g=d[]J'i2 ֓$I@_{tٿnoq5_8vhsA;1'_qϘ!au1uuq#WVyF" -zWjPoPo܀7f'&V &?}}xW9GN>1fDܨU:NS68K7q(HSG2҉kwZ~HS>/&QQTc #1CG=@D5(@ @ @.C@eJ@ @ @ ExaH@ @ @ :`J@ @ @^`&@ @ @C-$@ @ @ ExaH@ @ @ :`J@ @ @^`&@ @ @C-$@ @ @ ExaH@ @ @ :`J@ @ @^`&@ @ @C-$@ @ @ E@+VH#6ըӐM~m?mjV6/e+er&&Mǚk?*!e)7 M]jU [ 끷w_n3fg#ʋCms)oEœa&z!iq:j`?6xRe}r uC8 G8!_B6j]ԭ>9-~ַ : 3QBǢEACh1Ҁ;D8-+~i![C|rx=I+@hҞ}frŜ;uתzYSann YhhKQwٌ xD'GvbwzR6'O󰥐( 3d$ɋKH8")!<Hq ,F`Ԇ#LjyO'" h ) cǧ('kFVf?[8!' ]`Zw80:x G$D;!%3s񌰎]`L_p?#%0%cO0+>虹ljݔ>!5nyIp! A @cL/BN/z$T#$ BF܎uqc%>:Wr̥%vseG nh!"ԓ'?hp=I9>;,&4$If~L̙;wsΜ3O'@BH  2ӆS&_ mk^*_|w~gP͚너!wܻi=ߙ@r ӓAtpHRc:u䕯=el34w֨oDŽC幎Q ͢[VbYyVj4M͜gP./6!fg/XJ isBfL[0*+,c[0]鍯"3ÌtT] $/ 9\Mӛn6a:qX%;Oͮ/Z)$8e!'֗~V_BA&euѮ_dk!`I,>F22 <@4ٛ fRWVpVZ ,lv].}$^#EO^~/=FYu*c;:c!chV'e~Oxpc gBG2RkRc+ږ /vq[G'sFйwVޟ[,J$%XZƮXRUiz븻z]y֠7V<'#|>H?o-CM]0<]wJ?jt|v|sG%V|+AF~IIuhm̀alw i3hǛ r_`gJuD 䝹s(0 (X=~/XyaG:v/)vՉU2K|rdR/ѶT &<âՁ/no^͙ݚņidρ\R$ \ rc#'`twfpRvA#,_3r'?>F`/0=5D5JuDiȘ/;}į/WXb M~*!={gLǪ';Č_&UbV/Xe{QSy]0=}[FL?w(^|;#K<S4)s'sEFrn/pG~;=y9 =O $G@JU[*cWVv&_mX-3=⽍@ʑ^p~1,'M ;s[ #[yW:)hH_ buk}?Xy,,Mv79gN>TRI-@N/9N!aErgt~Ǧu*^\O⮡ET"] =E&vPH~#h]йyqv I 8>ĩK^}Ȩ?}= $pm8@TB,qƌ?eYح{p P9I G39St̔Kg%soU~k3]򦴤g\RIaGN4 ׮K^*-M%cfJ8'#{xkjT>jhٹ"^:~kF1ZJG9#hXx~={GӾDdׂʟm>3On|t5<7oN\p0isgRFgAn3HyvSVջ¡yoy{)}į)~Ѳ7282/O_hf9>S0_7@k0+~["#hxa)JJ*v*3q_=ow̟X^O.Pa<:S=/r!s8<}yӷ׃rt@|M\S /ͫ275 *'&l#@Jn=U _n[~I~JOJ|IڱcGu|6dfЙjBVdÜ=R4[TeTg!d5ċ{9g&l wGwz3r >ҥ&F1{M񲝉`RA /U}T2/)uv̌<#b'/>r!#l|h 0brqQhݥ?\QjA2 t!Xnwv\ \>SFb?$;pVmfh0"ЇѶty]f[ _(,UOEyvSFR9Baxsjwoa2zW3q6&#'M'Unu/^oab5^dqM GKH*iMU0G}̞Peou)#RB=UX #"$ɋ9:9Y0LmU?g]"_ąN0ϮbwCIpd%^J@Οsqz <,'Uq.j?qzewH XσA?3J۟Y]@ EW1ߞKSҳT3OՍݾMt'=giI~y\ĬWB>Q➆&O}6)#eFht+S*D.26ѭE@s_lr_ГMg5Ux> .{x<94tTq:$9<*v߰8=iu⿢K cw?k`:۵]]u}z 6?>r3$kL^_'m T_.@ëUմΨ?YXp($i L5ԴZ4˖-1k' և$ֻФ reSfn-{c#&..Huzs[TBhM8o+ə]IU;D3lCE\P=鑚Njwv9P{Dm̜3SJ1a&zeon47|9SidMHxH2!6qk9˞uՓvr 9ڎʟLLg|4t%w>>ΤWә#M [/w~ғ&rpiF^ӄ2lHQ,Jw9Ijp{X%e/HԅE'i'H&K?[3|:l]Mg||m&yC/ SA]U9:rץWUV_{~P9/Az}$)>󪄗?|F/!$pq:D->09Y疱z}P9M _Wv)!?J_i'|VJHDIr~d4$**gBw[Y,Dq~Cg}zjEM6z=z2;Ӳ|zv|;ɘ!= c#2eLUĿ~ #m*'W»&1D<(!_I L>.V dwi}DYC_~SC~ULrJP@2,ޑIw9Ijp>΢~tVZB*RSӈpVZT{-UGe$=7cxMl9z2U\ղ6{M'Sڬ٭!Mߵ-P3Z5:;z*ÓtST QݙP?td[2x*~T=g1Q:a?R~pQNb=3lB!$pD\̕u-aX᳈P9+T cAS;k٣R~؏v%r)=J5Z\GF2;ՒӇ}澅gc'3ۛhj5o>sjTsHÂ- vI[v`,dc"&6H"$2.3Zbg|/)/.IIe;w5zhw,G΍v,ްKwIip唸>Hwp cVOHTŻhi$/Fʳݏz::gWPPܿ <τ 6ghNJ_hptFMGZ?\Jȉݴ6CU>R| yeeI&ܽݔ\8iYT Q@Ra),#rO:U;t]w<Ӛb;r=!LGr+lH MZ)̕t YCx-Wu+si~Ԓ{zwY@:Pvrt<׮9ﺊ3+vW*{~\.PTzb9BĆO(>ysE _ ۟->@~]Ap. ܘȴnr_ TN8k=q^"CWRǧ.%NB|.eC--9Mc;7 ~L!:jnKt@&:;f);P[Hzȷquێ5jx uu*|,G|"58z=_#l:L$}9V%l+:pLa&/}CY,Knyu߼/:f=^qpOʔ!C#t{3bݟ#}b:Qͭr: ۶¯sD#7i_ޣU5LayY>w DUqqQamu0_sC !MPGRMv.ȪOX7ߵiu#숟 GONL˭q#4.ĔwB#~r=cnYljxFc/k6PwQbrHII]P=R|(]$v4Oh?a0?YCspxOAt~K݁1&:H= l3EH \*nNaV(hxK?ƶL SשtF5uVdQ)SO3 ~TVUoN3vLt y: Nf QH/Mg߻2v:v_5r6uo#M%yғS'O>Kş]zX{%Oa)w llO{% >uUaa3$9^ԟ,Y̳%r&/vpyT)yn>n==٧GO-Io I7WvERYH.ፍKV|kLX9_`Һ1ݯV3[q8_Ch|{`Bz8VU~,"V`픤(͓FJiAgk,AXny wC.B#yш~I#9[%_.mRno '6aXR J۹SJxq_ђEi]0= UgGSAzj8@k?bݿdCczV2'i $ H@!ݣ"C i.:en_+.ZZI< 쭽aD^7E!kcs o\@ٯDc&P :@ER?@3Azj)5H;?&ԃ6.?s#$\ 0\ug Y)izEi|\\5ʹwEӓY!#L!KZ & !m&=|xbB"խUS_}`+,?yhK=&Sٶ{_0F {5$N:wdH %*s܉y=?⽟ HŐ!qaz䟭ս8LVHUeUTJdž6M,@EGDZ71WEߗ*UmGO){*v's?iH6ۋ#ʆ!*et|\=/z^j="-a @ qbK$@H $@H $@H #qhyi#$@H $@H $@H (B $@H $@H $@Hc  $@H $@H $@"E0c$H $@H $@H $<p5AH $@H $@H $"pH @H $@H $@H $: <@M@H $@H $@H $ R3F@H $@H $@H C:-P$@H $@H $@H (B $@H $@H $@Hc  $@H $@H $@"E0c$H $@H $@H $<p5AH $@H $@H $"BdǢ74}< NETZD2ޡr О\}*7uAIjZXo.t֘v񙟑>^3~ڄ^ѡZ QW_4}u-c_a3&9w42,ƪ9j˓S2fO[o6De6ZJ[b{pg#$FFks%ןۜϿv;r˵(9h!.\TAqcJGG #oT!=QzeL^bB4U6%|2/!v#TlCF-7vFZi;rm};wcMMvi^@\C{/I*hV-bU/%)Ռ O!f\[c<ñ3{m|#XTy3$~h>kщ]l%g4_=!l;>5VI[t݉ .Rڑ|&?tCH MP4QU+OIPY~QQ$"0rDx`+xśh;?m݅n&߾TPUj'gkvuAK%k_1;7iqlo /\9_d4_PT\Zhr}-CfyDc3Ui\TN5!,g)YcHr:qÜ${${Wkni[3j@8Ir/:D&jٷ5$M}c4|rՏo]3G@Dwn gP9n;ry? ؟XSk /Ov뗎;>!^ZW ^#$pl]6,.ƎDe47%5kL0_L**@ :8n1:qWߞ2|J'aghCQ< هs :lTM/l #Ϡ5;= Cw{SfQηU,mV4F̩{*ymQjly^Ǩ8Ne4zgwCg,$Vr#z ̏{3n,|ځFjbR_|)!̘4.aT;WXܷҳ;ް:rkD9hMʭU ɘC$yS$@U4궺Ű 3j.hvNJHI{;L_Qq1nCf5=LmՄlNYfI{3wj:q>dЯIu] BIe6[QqѺZìe &A@~ǯ4Bd8m gbmp|;-*i$Q-nKfW* Cҵn[A= *gObh;DF^~_LdSj֎K ($e{-tڸVRm{!CzZyF dҨ)Ҭmz*=x b5wznCVRrUЕ^QS5dmt&U{+)ΫCQq]{='b!]Qunqtxlkv8yLYJY,+Kȁ-u{[&#oJ_{*ȠvH#FDUuC̯h+{bA !~=kӃyA)rx p:8:ӫ*? 6Y3~=eةRe@C&rp 06oi;kЬ o>wB.Aogx*CՇZ1D(m2R+M#߾?}Nsfݫ94D ;acqc'8' rg %ѪWx%A=+xr\gsVs=XwoY?+=u|9*(=P}Zi;@szeNd#(@:4268fp#W֌SWZ=@O~X~yj O\7w m 9=rQct܋xzWmWL|!24ؤì η02_98zPYdp5Cm>{:N?wfIw3x?ǿt)<*3Prʭv)?O_2P69&}~91s 9עH )ۨ]"Jʭ5D-]$Mu?U|Z[h|P9o?"d3 /Kف{, e,qE}zm'['K7 2zNRY,<&ՏyҖ mmm&_nwp[:r~/L4A1w+!C.<Zy;ʻzo)# kw%- G1iv[dm0Hp|4/-u-(  <Ɛ>f*.qDΥo\7vnf=< l|3ɎGf?6USogH%YV +2B&a557}i~ ~ָ!z#qNg4_?@F} +Z$ٯvaH§H  D%d͒? 8T[tэs{R/Vv-(  =y&gYlA~X5:=êǯEL[\|mK)js-tEޟ=/95+c*DKx:!\2Ut}uQLTZZ㋰@IDATg[KZvr fahX9hҮ ?o͛7'm.89S3YxΡ|k38.\Af:nb&X %4>v٣LShڷ[rЅux\)!P(ܜ/-L<;Ѷ8=V J+@Tλ6J=|!=!םif\:BO`ŸvW]H7z56>DH& YuMsA{*+kyJn=ٖ_n[~y^0U{e:q5ǛWen21R8`'RKlԎ;(خ{e!3SNgcgmSЇQR&!zIdgP3tb5CE|mjOq'VoSSQꖓ5s mh+ yB@AʭsSsGkÈa^nJµD xsZg @F@ lFϾ;n]٢'9RC&.ŻfJI5~ב&@XŞ!i1qIJe=~R1gG8e&$2sxșoֈ7M,%f/vKTq2cvOWm۳?*XΥ'4A+Nt>ҟv;*z:es׎~ $1gX,R>"@"#%tgnV60HjճnH0fUH*aE c;/&gXIK $V'Bq}tl &#'M'Unu/aeuҍ?ojqRej4M !%]m c)jW;o k92mVN-j4f^oa3rrYV,!*[@+؏3ֽ|'\`s!cHoKz>qẜ#JWRCKj B4ߕwT΍vLA!X˶r sXR wLÏk}k'ЉTLOۦ'w(nEA୰R4Λ?d}BNU4.Nvk𵰥*2pFFe0v*;d,zBTοe։!^4r3H~DvKíPE.v$9K$Y__Z)#T:Qi3$2:}I8*n';fhZ}I5:K~]ݩS'j;vdٺ5MA9I˦rgZdځo;9K_"r+ɓ*u++yhD^*cYԗ_vl4%N)zSG{€cJs=VyBπmf\t r"Sg<' R鰹|ƪjƋr!m34=T)WHK2}`(?КptR7Yulfڲ7=ۄ\-^,ȑ#I̿u&$UTMytPxP9>ЗnƢ>=^\OkT6e@8P9@TD:BOh>H#~$yb$8u=J+aw?q֎~#x#Vɦ3sN}<_P9>dFҖ9}k'uPqN.%=K5cT݈1ݯyoO뻣J'JDifA;eüS_}jzC}Ghf%V]{ݔ|㾵t؈ o}/<1Zl{r%qWy)MitKa b @TN^H- ޷lH qvapUvLzڍ :VwX}W]1椔=th?H7z0G$.(cK=-έʹ[!3վγ>1{Յo`#qtq+-"/<#Ψ'DG&d,mOW =]$kڢ4Ez$F˥խwoe>=>z FhܶIG¬VJ%~˸*xߞpt5ASi9GVnOp]Ϸ+tHӂGu"D>EtH Jvpn6[icWPTz Mh$HL䠞Ax3FBh41mz4,{SO;X3qꔔ1U.,u%dM{l߹&Rh=TGFic*a${;B gbRNTڦb?[!j)Bd+Y#3d8@əy%z7Nb=ngR=˵?ۑrQF@Q$@ K@g43|cm}f ZAX/zF9J6>hI[ v~WmRM4Д5 7xζޟ|I< <5qHbYۏbytVKU-_vmQ}= sRRkcu vPClK4$**gzHE?-,J"g5Ŀ<5Si9S[oylcU&,Rݧ3c:CrM{ciY>=޾zx MoeFeğ\}bmLj. ς]T_,'W˿Bs#]xljt B%dI/}~iÅ} }6=*#o`k;_RzNg)y `M9_hLǾȾ/ٿ>:ROh>pheY+e?U+c\lmHQmUOKH;]+3v5oOP"E >I-|Pk(zk9Br W_Wk4Y{דe կki#:be"(?_h|MGNq1СJQQAq;$;~lk2^1py"GIU\clY4tRe-ٻ.r>\@W 0->k'TZN(Sz[!vP;ޱہv(*CqmG\g!39Z24Wάud{k $u{Y@Jl;lqsJd\_üؽcCi-#iP9^%:/ N[&>5ǹ jXRyۘ`n֟mtg=reϠr\Z:}.;"GW`L ;t"&3K7˰_ ۑ\Qz@ $lΗL2=.Q Thg†FI$lKomYeixK_hFmtݶݟ7n\%߿;rQwU1ҕBM|Z܇T2zHwv說3Xۡ(gtNVg~H}3 EKyeesxBOp/P]07S4vy;؉{ (yGG:BoN 9!mw,ǯ~zl9:H%tnv[qRTNZnYm}i5L0zXv+q6n-!&v*W}F8TND^x헞׾:]-4:::[YKLGN+5,>#~e ).NOh>;"GH 3ih?@|uAHCap Ogw8@#~#׺xxeS.5dW74o?7MTΟPPXv'GdӜ}_0ح'ZO7yݗ~oy}kN{m{\>S!9#`[0`Uk˟'Px 9 ֓egN+!{§`;kB$a7Dҗp#+#5{eO0G=4~-5GU>SM춒95TG?ᙣ/{FT޹$Ϡr")ů1`>џl'T_qٯz/>_|%Pk k2mn؟WCIr Fe{LP9SQv=]*3lV-Lq~q#hф~#j" uQS}:ieB۔3dU˲4ᦑWi~|\_:##ͪeS3j2kYk?,LYl_l?껙 o9SlDUnRrPn$;aoXcd5ٹ >`엮~ע_dGx99DD?m[%Џ 9,ZsYʄ M#wy~DZj?| ԗX, WTNH!wP;Gj0i=x\UqqQa)*8!.xЧ'c+ ΄!O'Ig ;`GmՖUG3 0RmӖįJ'a qRŖϠrR)_`3pn@?R_v_z/< S(mw(p;<9@$nJ6cX)բb;m;o 1tǴᇐ0uKgT۶ٔ5~a*3.f l(2e푤)^o:ޕTD2uZ{/:SpǟLeU!1cDѰ}wO^9оI9=<'cNt}?_-prJ:PÖvB|zj%l]|ߧk>K% sg؄aI1LJ9N!A@'YԼyZRl}\WZf61CHšż[pk [S19mWNz8&ZU28M?2[=ꃵS4O^+==\Ϡb`׳9R4` }Xݐ{z7,Oԟ*3Z2nXϳ <ءD>Ho Ug*c`|/4>TMa,&KV|kLX9_`Ҷc_gH|#{YRUtcOkiU#C}*P9{J_]zvQ9mCֲGj5 '2ɯǶ̰ڞ%k8REI!#~է#? P9imw%t ϠrΘ;!b>M%c0)6@ڎ~c:ƀ@' qQat+}4DGՁ1EjCgqc ȱ7V\x[{È޽nqKr///,:DcDe`>38 uWQ?֖J6]~&O[K'6n7Ffq#[;VK$-s $ܕc*߽bV|ruge&0쿪oWv~)6_31B-TZ*&rˤK$OCVRjgy ?<΀kSnh6ϠrDr5N>EKڰM?G*6-k`xF68d&ZJymrT5v;9hڼwGL5o$GD{B?Ce ':fcW?/z5K;*H—n?/v-] Y}w7tS+Kɽ**_(xLwW!cz2%.؇P9* iO 4Avڿ~hH9Hv%d~I?)%@Nڑoa+@*fZM<[ OW4vQkͧ:hXS;=Z2"RPh V!m&=dbB"խUS_}`+,Sc_WzCsp~ !QaPyTXSk25[/m;.L]C;.9P9UTbi(Vk9vh EY;)ڑotR+$; r'H $@H $@H $r O& $@H $@H $@H[$@H $@H $@H ; v c@H $@H $@H $F܀-@H $@H $@H $1}H $@H $@H $p#Dn@ $@H $@H $@N˜>$@H $@H $@H "7 x@H $@H $@H `'DnaL@H $@H $@H $EH $@H $@H $@`0 $@H $@H $@np "$@H $@H $@H Q[Ӈ@H $@H $@H 7!norD(9=IKӋ(eCDG<E١=T2oh̓kzuj\閃o13?#3}f C66]mh[Ƽ%VgHK#L.s1OOWOy.F 0\JOY逧W~&y!f\{g6Ee2Bo1[7j6Z/ַs Wr;iן)U'ҭNK[ {%OHd*etM^>sWG8uZnrTAʡ.h"M @V'+i_U42,ƪ9j{ lwF +R8 Ƈr2/P*_Cg~G!P9~3/@H@EUM!y{T-kFgTL\*rpĤ0a2^*>X}im*'2`&t;6w<{wᴛɷo-xxc'l/~>np%zUə]]wRI'ZW̎< e~M$l&U`273$2BHO{v3?1$D98Wfa#v^ʵL\d՟ucEFB񖪙K M|7iZ/ \A?Y3ge.{Vr Gh|P9EBʉV#4>[oS\-;ݰ^hxH`dC1y}HIudm/ٷW x h~RAC9b?5<|[wY{dgJ': $pl]6,.De47%5kL0_L*۪ @ :8n1:qWߞ2|J'aghCQ< هs:lTM/l #Ϡ5;= Cw{SfQс-Fe1YZ۬w4Z홁e!2izmu 3j.hv.n3^RmCLn=Y/7>dBڜ%|y ˘V6rlRg͐fK?/t!}ɠ_N2h/ vgD_А\Ǩ8Ne4zgwCg,$Vr#z ̏{3n, $8HϟzI*.?'}g]КA2$\-P9%@IP+4>KnA$Jԕ5\io'kBctݒDy-j ;Q Ax{ ~Zϯ#3݋S< $$ ZZq{Ej+CTKKͭZ|󍝾>\{SYVT? gܠ](]q]ŬJoM5lP^~~,s̹wVޟ[,J&b}6uTt}+()i%_FW,-cW,4=pu9^Wo5(  +s~LُDg'CUw-َcA `n?PtTjwo==ϔe;5WY,+{8zl:)+/Ѳ2` ʟ?e.h`T F4A-Tkh|P9)qBqr *'E7_r)@ޙ;<ץ:-_c8mӽH[8ϕ-J9[b~-39塋 K&ٞ*3Qm}?|hQ~_ڟ:].G aP{R[n=%.Q~A3e*tCH 'd)71CWf]/; S0CբVF@9ufP;!&#5BI7 mf毿b IrL3JLlt>pkQӁ0|Cfm5}1Ea271U~`twfpRv~8vӳ4 `v%sK㞽zeaUSߓⲝ#,^btʿΟYWk0yoS-P9>ĸAʉщ4>bܠAݏh(ETQE'W%ZUޕhϻݦEXbNӐǤ0QC@k=..bxgځ/MVXI=tjQw~]L - ~>yzZxBgiq-gšwAP@a >QB`uc?,' E{@3&&Fp WK 8ܘ8ƣ/?M0Yw@љ~;sxOL|~v'P9>8n|!gr}xEyv Α@O=^⤴K^B!@ 28m~?rrSahfG*M4>To;"oaE}pW) 7ۦ]>yik0RoJe.~'!3ɎGf:n?cS:wMrejwW.Rʭ;W9P}9=@H H *!kT8cF,鞥^.oZ]P*'`z;Lt@eA~ 2ȬI?~(|t_lފs?6WKv晝+"kF1YJ2؏?hr\nF/٦Œ1-`={Cin">ܔ൓9Cv;!P9o ՟Z84P&k&nnf@OXīxr>bI &mL7?r@DϺ^U`G-P9(Oh|P9ԓ=4Oqvq3˪M ntTy|{^[پCχo :eJG|`n֪av"Wlzy;Ө̕׃%%5\[WY;qQ\e;rLI;x]@LKs'{5cLC{D4:N743z0Sܶ˹"jM\S /hͫ275 xF?cǎn; ^YlT~deRX;GgNING(f9mDvU='r6C(uIӚMUv'wy~ QqT2/)uv̌<`[i?yߪ m \w2皏ZüDED uxsZg{ggJTZ=ブ[4}r'{PibpTVlOlb;=/74rS ./vKTudP~Qy2ЇѾ^yۧWk*]=\iPsJ‰S>H $J k1c@||>TGc}fՊi*W Bz f٬ S38Rul廟$ +i!:go~09i:ytӾW{}m]U'N7\s}V- )Oj74rVd7&t.!B7#VF ڿrCuml5pB #g;%p2~|l_#JYy?K*TsV%mJ)2ɤᡅCdw[/u~CV,!*[@ϑj3ֽ xRMv.Uogb@TFZnfEg]QNۭ(Ykϋ"1eAcہBfrqq][$OfCiJֺ|t'A.\ک/( V @HtD?7B弅y-?K[E{!8Qrg%~T8Tq%bWZ5o;uDc.,=[w1C^7H73 pT.JlD[-1v:W1߽gGWWF`ْ:pc=SGZKߩ[vO葿nr:2}.^~5w4:8n,7~M+.\٪ʹeod QtQqu>"r+ɓ*u+yhD^*cuSJIU t r"c#sGdAJ"67oXU{ |;fG(dr To*j79eglࢪaTysXJW]ם\%!_Z~hj]5_0t7wKnnR3pҬ,ҩ|)Af̽޹33}9s=Xځemj) 5-ԡ9FL<J퓎 oou[!~bµ@Aȷ(*<N@@ K݃hroA$QWI|؃<33!~0Q>{U<7FL|9H{,%W[3=Ba̕z,Z8o&N:B%-vٚR2[J nPK BɨW:1<}u:Ps)=BX/8݇2&?zqY h섾Kr[RHyU.8U)kLPMں(i%#BT$Q'tl_CQSzgxwAlx?w\ԻCo~^CC5>ujQ!w\O7u;6S?m.n+~#&MP]f*ԊC8{$Ox~gl8:rp` bP۵. B OY^^JKK}Д.ZQbj3b38Ri]PĶP'a'@Y8m"zovwPT'>R9F>(6O`:{lc~>Ϛ ӃYh #۵ u?!s%8L.*Onh:):]Tټ˳sa~ M [~&9Q3 zj?a\47ů?lqpo8P{xİ 5h3R*dx5jF"" !}h&X*Ksztg J~Ju&M/:>:=GkTG:BaEENCoc{) Uw\bE. cG"W2cA K֝i^:=@,|Y7;#|+n}*yz_!lN^LF^UfyC.~| = \~~P6ޟU,v<\ .oP3EeGvm~*)y7gq|G*|b {omH-iHʁAyL4]o=&|߅'Sd>k[2fLNT+~5OAbgR__p!ɷ Ny)g=*mÂg$lēm?g" Ur,.J imJɚt/S#+ϑW\8Meтe N:v0m9nSBO֯Γ1^*[׶[3z3N_dmґ.ZW<9(w  KzY:֭2Z}O_ zl?g~dّ\N{br%5G[fCٟB[f".ݽ%9L4%3S((_!OrɛJ?PLHF]оy 6{w)~{U8KlE{3d4ߒʑFT\k:NuN[~TL_iذN=\wXpCz*=)ȷkfj趔<=qCgQ<>c.9']@__TN$=Ub `~&yEB7dhY/̨UͶ ifۖ"Y}9qu+:[rvʱ +>E+_mK [W'D~S)3!]Ι֘ у*y~-Zr޷~|X_zt`rb_ZKWZ_Jb&K\U;cQDqhOU-Cn:_z[3zsɔje+[g$ӣ~iK@IDATGsw~IWa>v{>O$m+{ͱf܌.}C`T1u12T+ͬyMgϝ[F4I:=sTO3|u껶T<'R߉]äw$-v3Y~K5kKjZ)eALKz,UcMO>~MӻE\|K)!l`>R9yENw-.{rPmW$*+g׾1!j~E}_l͡Ӟ5 _o\[T~d_bqvJQ+-%yl>>c. < _/>˷(rd0\@h yS%C˯}%wBb:$c \\\_y5-kK=);cلRmJ'| knBx3QKu4PmW*~F%kx|,vp&(uˉ꒵sRbSM8v7p$OJ4sK㎃XG+I[8jCt?Lkl!MwR9J'OJRnKqݽbITBpOCS(7L0YJF-9+lK?.{po;#D*GʃTX$G QA{g`BCQ<#Ed3}glAG_kJ 궯9B\R'ηvvw9!@xB( LT2<" @I "͋K/#UdRsKGL[ 9jP޺ml4v0٠_ʣT*i{KvWS;=1s"\>fܰ+|wc-nULm֖g;xڴotA'sPE|TLb6XW}C۴प<'u@fFJoLMGcTH$UgLӦR#h2@!?]P? 0f࠻T>R9<xU|@> "Oa #,v=@N_R"G!3  B@ @ @ @i`@ @ @ @0Q0B@ @ @ c0@Ɖ @ @ @ `(T!L @ @ @1 Ӏ@ @ @  0@ &@ @ @ ˜ qi@ @ @  U @ @ aL8q4 @ @ @ * @ @ @0&Da8`@ @ @ AA@ @ @ @0N0  @ @  F4 8ZTZZ>8VrafIX>Jj2̱8HܯBT$FiÑ63%?%3sl~1 I:Ly͸*(LB}ڬ,DyqlO箑̛4WR.1-uU7V۟`K|d.Q4OdGMRFk[el+,*֯=isɷ)?s,4&uZ"PUO\[hddVI2 wKi٪C= \ʉ007&0by%,#?hXD#$ A=˿ T9IfM*t Q[c_ Q5lUñP<*U nH$LR@ɔL=۷LPs#}jsЏ-/?%G_ BK@WyJnĤ8'Ʊ95,c-)"Lꑚw6\P"c<:ۓ{ֻ︛ѷy`kLUTE~?VMmWIHe=lJ(@>F?H*G k6EOayⅺ٫MӁ,~̝6wdBm2}f9޲|2=Ә).Ϸ[~jr-)-ѡ^|ɿ~oWgn8WmpL_QuO6wdQ\Ntݨ9rxK KԱ\BODZ?s#!rE`-3ݩ02Wwp, 酷njaI]{qc&OWeAO)NR9W̽|8N:ZêP_g#r|+D8_H߷rd ޷y' @  Ңרc 1ͭ95hȌz;LWr>n78 I S_L?nSgɲ8*T&͜$cj|͒CGXPhs W/35mt "M7?Mi9J!ljgZth5_>kʍՉc۷2~::e [B9^<8dVi}MO߇O%͹kb|2Nboygڽ4#.=M;'&Œ2\~CJ.Ur ET{9hlvlVit")>F-Y2jq(U}D^?#I ,R;kZɟM+oF߼僥(!%}z8anrV{ \.//n[ SrnI'D勐btھ>{W{;A"oIHq<޷D5>Y聻j\B\?mFjy⪜;OwEb'Gj'{ji"\An?R;I#N"BbկIGrj~#2[6r}KΓL>8@tktc\=z%co;:C"}G-1CЌ=ѳ֧4 ?x4.KߋY/O?,6wh6 Y5}T*=. Nb?bEvE&5οxN{,CLub^T.YOm"ih/0?Umuǚ$5ȸ%HSEZFZ=sHTΩ(]6;tME|+tt|(r=wg;ږkYZ4ǫ!o!>NWCsLj'\hb5h勷*+b'G}#R9BK+ZTW|oӇ>c@nAQ%ڸ/b׵-Xa 8^v낾qK/m)J*3.t#Ԕjw*Y6RsМVm?}:iaL/5Ka[4A_+ng3-}c޻wo{v_|vpo,`դhQ=ӂh py6SYOgNZ98MDҌipҴq:Njz\7a,B;ߩGY#LG蓅4({E45|_HSS:KˠIܖ.#vaa̷i\O%eHݮO_95IIԷ̡W߷ASqp  Ѝ Ȼbܨع^*Fϸy6풵3'Pg5G37Rqzms}h6K #@u_r#Xaō8>9mBx44zLӭe/=\|8U@uLsR-5M* 2P_rkIQg~Ivv?kihqA˳oGtOv=T_g,xDdԱ@#sʶ<6#Iqe#ؖI`(Z|oC1C3m7 s_TSv?+NJɉLDhv~65 5P=ڷv-\I˔d%VZUJK] )'Bgm4jΟ?QOkyyή“~ƙ>rYz$@>8Xc>)AQ! rtrvIa7 xg(פ#R9O~l+rA9A9r?}~yWݚ@ {|u΍T뉗W2u62c`peG_M:bb.TΦK|y Ɯ5#75TFaQD%F$Ūe7gQks6tȕ~XO7e㥿8f{E~$6s2Rʋt Q\s_lЌVV =T܁׮{s/Ĵl; ]~1_Y:^ymr\[(j_ԽV{I(fjsÑ;mqz67o_Y_󜠅5_jtI0sҹ^ OʉC0:xjb uWg᧻O3 lzXճΕg\tGj'3av$t2zd5T΁c{! -g9r?}+4@ @M iж}ZOo|ٱ?HcfA''S4~+Y@ QWzvdL׍+_{Dz&DTzơiȻG9muqT3%G2mm7%"'/ߓt -zsGtb*)F*e3]g9N2;z?mx3jFcтgZ=vQEιڼҭG&S͙9:4E7޸K5/^>?"=UCqq.S|39x#ު[4..^=޾6HzKqZO7<#̨|sC"YMdܫE|儯uZ*$*\Ap@{7-^w“{?C#zC$݉qQ/ǹ e}<}I* iPD6%TT\jG0N34kSQ{lH߷n/4s @GKAYͱ(RjЙrطT%1ҮDoO.?W}!&%;˼5T5~zYt'|ӕய.:hRΪ+GyKKOІ!띘ѤIA+6zWmٴ2 P@;ƴ=GEmh nPf= G] uL3kK©~‹H|ؒ;sӛKuך(gQL~@a +Ex&3I;L^uTwهM_hSxxi?.Ȫy6ww-ou ?{EBⅷ0yuc,.*4b4Sڷ1kg.̓~/*ʷL{\!;R{yMϐ8*`3 vz]r^ ҅ &h~R]P=@_H\qjq9 {-~[[}~x- @w$u{27_-wL*rs^iQGa䃡r},T0bwdI{,%6%rk2&?zqY h};/R@%F^ dYvنd(:$ǕE!! +\& %^JkÞ$S[3=Ma̕zZ8o+Mu{1gԉ2~C$4Uouwlxď6R]'8U)K 2 O’Vxr"<&rVU]<%#O-/yZ{IZ?cr+H9(l=>'\.U(`[3u@ly>N#T;Ru wN:_|s1#Y}\ׯIGr.9W\8T$qn( =$蟖OUQ/ڶ^Z!R"Kb*ͲS?&CJQ#KT*ޡl/_R}&F{A-k'1^5!8N73z3TײeVƧy0/xش@t`)IЅgp)?5+$c⭘}Fq-QQ@;|?Mg4 yS3YmߡN1TO]ի|ަk=+Y8,'2 C*ga3p(<^INoSqCE9 TNHTB\0,@; ?_H:rn0%D𮅟oI|'y ?[^),IوmZ:Onzla_ra=8':a5ʐ&wdufj/J<:]0;8kzK;iVʩe>d][vJ8X[#Nrϊ~26IDwt(uɃl[5\ls : q?PL;Ѿy/7{w)~{f0E*9r"%yni2݇sbmg3H8dogn$؟,|KST5u3V$FmGӧ%7 A1Η@ ngW$._9SU2T:~A2;;.[уO#+70L7"A2''L\KU]~Y)kЏo>U릗TeWŧ\:]_z LjIÓc^]i"U;; Tկ7}Fr\-ytuoWOm'3.f _#Dolq.?KJgizgߣeU}x\hIr<3Alzd\b9iC1(5M#:3M~ Yo=mOqNvw;J3Y⩃};Sk\'{L\!}ߒʑ3ox $ @8@P(E^ʶkdH㶲P/QR9JCRU}^ǤrCwط\_y5-kK=)dدhM} }Hbh>08D=M 5DB/?vmWQ>{T U*J!z,UjG現Vw&˹yBlg@p PY盪KIHN5yNndMNpC9ˈz_Sϒ[R9J'Î[#^|ƒ5<>V;8r Ym[@ '7F=gt;?񾭕eŤ|zʹ 3nLdDؒc6HIcS؉ ]0&KT6M3DK}\B*$(Za( >ml4v0٠_ʣT*i{KvWDNBǍޘ(E*$ƪ3iSrNH䭴`у*M6k?qGIQT10EmQTZdA~JR&1UNѴ)N?i2Xd\Eq!=wdޤrhҵf6zȡIQ*hmklmEe3BT. kՕ" $cL|xKZ%G1↸^!O[W:~7 w/I~w [}qwH{%@ns'z?^H1I.|mb4 fn:zdI3pN2֣#| G}S~ɳ|{?`ϟpOtoboA*oP JǻN'{M~:'y".Zƺ&#!|!u\68RÒe.ur]'3 .1r%\ڎ<aӧei>uftt}R!ӏK|y{ӋGSWuAl;IErA18t? ǵC)"η8   Ңר#)1ͭjh^}tO ߝ>cTdμ&FF 3k_w'2.~t#L>yoO~>[mpY?#I3/+VT:F5N?!eK3"0IfhsgLU-1֚YRw J}pNaJE/?v澭g%[5~#34)k|֔D=oeu-n~~$dJz8$=i^2?[h֯/"4@3?\>Im㬯لX ~TNF8]"bnjRe)]Sb&$J$ gέqvte==ޱ:c/KϹ1 +Kj\"K?H\U{wa K8 ~Vrt-IJѢ+v1#c4"nh9(ז̧\= u^%~F/`4hvJM"n܈ߍnp}}\PvA?P!,?W*4cAԌ\0еB0[4Uo7NZM#Nzo Ufʖ"L/s|$ī]pg5pLF;,3 xBVf__}#66[kdI;^vAOzS !rD|K+@J@-bYhBMճǯn;SW۹Lmy% 眓ηȨ8N?rIr5+*oY[Ө/}sprGz*sX;W;w͒ږ{kFeϻQr{E:Y.˱'Wpav)N:_|ޔYʛ Qw2}OM_B m𜍇_;g(\H'x6Rbw闓-^?GydG9Qiu_~ʐr@ !Py-)u@a q*_[ z hvUrCv*s?BǓʤ(mM?%xP3ϒM "Kœۛ^Xo))\Wz!dwAwY_H=|mOBp [ߵۣG^X2yˮQ wCo`|//eL7CR9zʕҌG[k쾔b;}Fl;$mDǹٗ2r߃oZug7Cd RfhV9hl眿 _~!8 6m%SE^Yoޯ7XSA;^^W®Q.<6wu#ܡ_;D~tۮ #~vaz~C"/ M|ģn ~<O*"XIaW  @Jq_LłUMjr߈H|#z<J#1l~6b;}YNSZeԌ;*,QVyDIB튢c",~z6Lur=XTVMsu&ԥ84~}0]f.r!?k#y()ה~2 2e{pᵢ.KK/mr۷'os$y{x%zfCAn_~Pp (|\XiucD+3rRU06ZCpOcŮ*n©|aWoe_ZER5s-^og;zA8vUx2 ?{&P2Q62)`_#/2_n%әBO 5q/V:eIY_ :@-N2s7H^X?wȣmsЍTNX,-+_CRt}v|s;֥qA/gXZ/&_Oj(jɆQ uڵs#:PjipҴq:zjJ_'k]=!wp!$2<%51g1@ \,7'a{0-}qjd9s>OL7-m5 @څIΙ JGL?asVj??-w"*kc܁ נ=3R&`: li0Z}my-z+ss*ЙC6MBvv&wzA_hxz$=>g:TOsh!]Q_ڦ@b$סX! I9H/\=m@! Ӕtaf,GHx/#(bߕ;s/#R91uirK@ŽhQb,ӏL_4o^`ۊ~|mSMQ<ꌙhZ6"k2j@IDATUr_rkIQgXt6/+-Ͼ @zo0iEU&SRAgh鼆2~:-&FOTw쥧+M<"mo:sl:O-J/Q'/IQG|S\m@nD B.vR%O>]H*Ǎ<ͅ@z$jC3W8ݹ,PT nPw\x[?k%X+EX64oʔK-qZQ}Imex'ty|O:o׽ٴ}9Ĵ(̾25#75TFaQD%F$Ūe7gQks6(hMDSLHFY[_~z12jPlX B Z$>_ܘHU̝^F/ͥ݋nNB1#Uߩmӓ}_^]gdTx3n%Pݲpu-qPR{"&r_NNBE$rg4 ! QE|BTm7==;O2kfzTZlF[cYg]@t(r2պo_W=(bW}^ޞ*f*8w] [Zr n(+K`EN^'yZjTURLE-S;g$$ ?gd^ڎ_jIn_nO3TG*=ӨiȻG9muqԯ3%AAb `/(ZP*BƩK5.}P}cXqV]@}xm[gps՝;g)+^j爷W R?Y砶SB#V~&.&y;[La2՜iљSCSTqdP{ TiFMU궄'Kpa[^;l5ԚU_kd6%8UGbPk5PZ*$*\A(M<ץ,cR;]^'OgN"q/Z57I۷^?9m3\BEe.}xjx]EG> K;6U&R#3Оp:5~>R.O?z]_]ȨѤUn{E&@/L90|c\>8DGdC; g5OeXu~H*g˳cQԠ3me/[d&7xtU)a;޺ܟ  !p͇-3>7;Tw2Jg<0&tYY{U=1C*g AyffmEkϋ~LҧNQ~C$4UouwlxďWtcꂂcUg-x)Kr|p٦PttoI+5$d +\MF:g5믮a:`Y θK#˚1;m>FRŶU\ma_V*\3roA=p;~_<8u(|@sn~&Z(D453гj\/X0SF0iℨC/^='H??Q/zay-)2C+ڸ<ُ/QuMym1W/ Ol@,}g>6\vW֗w>4pkooR:O(~U翰|ԩS}7WgފgߒL ã+߆p ^h`&C%*Nk7R9vsvKbAS?=TKɀh4 حo]].ZMbj յhvbMՈlZIPп쑒}  e0A!Ac^ZxfnG'*XEVK˱'Q~Vbwj0Փ{Wr{F~&Z$d4+$cjj6RM.nѳл| DQck1l:l"QSg]sQ 2=|aǓ٫z˞l[g/ b3|k;bݾ?\Wp6_:q '6W9H/r(z|v'S5 @ W xVm-r`ڄŏur~[ b*p$>1I];MI/=k}y=ӯm၇c2 C# 6up2%f 3@gjW}s<%`[*IJ1Q.+9_ vK|OY^^2}/EhHSRǠ!pdӝn< ,k3-MAsμ}IHr4NyO|#Ƈ}79`lߊD,3Y9H!e=6O xUm\H,uZq!gdMn/SAokvrvyCqԌΞֱZ,T?h䅋V'WZP(}90F]:35z+Ϲ~tŻ'2 iVʇE fɾ'aݏX6_'c65 Tm9fgLs7J>l_Jс(>;!(Z^۞ Ϫ<#/DtM&c}8'֖QpBj]zA0ӝ]%:O!3BrTP)+Ͽ5jW [+X+]O}y|D"io阰~F쟄8I3"`9(6a{H-@t;@i[$tN-=#G&WͪΊ4F:-sF”.ϝ5霩NoI*?a|̠XIp@*RI_zm(M+8<͏=o]0o='T=XmڸQwe ro#+Y(U)WtN}vjޠB1bXk4{(o om;2rK}&]]uU>Yܰm C4!sr”?ε_ZKWZ_Jb&-mͧjg/@RzcƴdPɊe/9r7'?1u1R++W;"THٹhFJOc_!B=2.1 kk\FֽS0-9Z&{ zv~@gd|b(3*}i&z} W}q*%y[C$lޛOl[EǧTĹv1P:=]ѡ)BE|ޚzS3lhZji(9l[k~_b;qӝ'6OG!35ۍq8@Xe޳zIse%+"mozTl$2>B=&ArP(R!@8@P(E^6{"Rĸҫ撥%GwqnT߳ʚJI7$үلR1ܠh>08D%>(g6ÓmRiRnG_ \Te2 f6溓k^a5ltMtki?5nҲ5p,Ҭj*%003s033nT' cް6MU꼀}2m6'(Vˋ}ߡS{R>Zܻ}N'KL<9]7medz {"<:>Ϛ >yShYtqS?[@fbMϭ+F6NqB~Zl;Rt{8CD(ۗ@JT]חmQ_orµw\ve菞!~2i )OE-g㐑4^ar頧SK1}G~|oFkOD񋠗?A_hˋg9z+q@`;JBAK37bdRc}WG [ C9"ӐՙayaR]u#$">ހy5T2zPixydD,\!T1|6}X6Owh|M}^ȫOGm fA>E#T*iS}Wr ?51 }yѦeJ5P˔SQg>_^kt[ЗovyzHEߞr9ӿ-1V\6g2 @B~"YоғSN^Be=I$}@<免t=K-L ΌiC@vT&             Q((#hC` jCU@@@@@@@@@@@@ ` e           m Dm(3  D8@@@@@@@@@@@@  eTP(!0̀*             026D6P@@@@@@@@@@@BAPPF            І@Ԇ2@(D%8&r_98M 7mlMIQ H ךk+/:>gWV708 GJoG٫GWRIƒ_N뎾ΰ/COJG%U' G3VJfҥZ)#,o;bDL>!ifMOLPzsce7l-G݆|-^qj% 6rbEk)dtŐctan51崡J?}N.[T> |˘=\}U^H@Q8hHDO }Fᷨ}(>{{&Gg5jC! G|nCC9xxymNFJ~!{_D;#a5$yR3˙w"!`B~0^}!~Ny|xZH2}U=#dH~[9F|sX)@@@Z6zms%tgΚ֍L/ ,*|.vʤ8֞zzRƆFrΡӐgl2o׮!F[+Rh u8ԍh^aL>X{yڪCYϴd ". WXgpts=Im6y0_2mqSgrt.=7-1Qr63snS#95Y qYfSg;՟OrF+O pto"*+ɡSJP{.Q 6_{蕪[ |gS$<]+;y_gi/HN,# pcfo-еG+-~Ny{ўx$5e1y}=E/^Y @@@@ t(giI֎VU%zz&FjgLuD$i%3gE5pJzۡVM88ҟ1IuUSߟՏ +5y8xtYڟvʤ|&~ٱlu"Rȫ Sb{4$3s^Ж3Z9A2$qȨ2V+;k33:.Mw\l2F6y鞸:6zdowU/ ,Q$F=ڙ+_# {zc~y',3͜\'a]ktJ*{J5rKSoC!ϋ.!代lI]Aք^Iu޽|0D!C$w$WvV zQWS)5K%Jӏ1_80D_l\>vnmļx.c:ŅA+'B.4$-&<D *łu3X*>:UsG |J^WarRTbVǣK#$ F*sˇTf nDw/q\%E5鶙Y5N>KƘ["lAӟ-6?5}5*VLYīm}l=  Tz*osSmBďB~@[h(dsk(Ku?OWaGʗ)rO_UW!3q B; -yAŋ|K *6hl9’j 埯P=ݧwJ;fCf1BެzOv G Mnh,~#H`0X&n(var>u_e%ќ#hWGuNN |[-C|h#SxiODr^B s_^ )C@hm'g*{ezSyg @4uK1b [̑~SmvB+P'rFT]jv* E?B{j|u_YθSh7{#Jnveqmr@%Yl? |H!7 L|jYazd ˔2z%[.'̚[vge/X+ә.i}Ϟ=BM`>΅fe `%ۍGrǟ7o۶׶G{m `: _el|dfB_(XmV=wc-Ylr$nJGjpQZev^H4! \xcX)b\<}RiVΛށNs}1aw dLK$1RɷwyXWzp@@@@ {=Ym㚌8ʅ&U+e˦ktcg)|se}v<u ]_}&IKy--u'-IFBa1A|U=ArugFU[úVbJIFJu' nK8ոiy\/ܱN54Yi\ep I~ 6&\$]IYFd#F>gϞ.{vRiR=ek_"38he'@\ȾL{&V͂մ3i\u 䒝pV䰤;J4$kDE$;oֲA gQб]"&vT=M p Ǐ)]/Wo[9KH0pu:mk<'^jYñ>.@@@@y(uHf:μ)ި7%0ˈG*{ǷzˈV.iqi2IVMǦXn:K'fb7oO!<.T'#jِԻ萓цa&E-~Y_~Uɒ[,")Dgbk$fL߽-ZX,v%S qv.m̏|7r 'j dx[ =f*gx,'VB껜k QVH 'N#Vn(x幚']}Q^)" U؝Oܗsu*O'},rB_ܴr|a-JkNPx`TXYPu։ YR.B!p΄qL\}ro՜-_O**Ie"qsPO84a9OYf_0方NNU06V|{UoG+[#zi\B>o-&*S<nQ%2i%NQ!5;)vߗ$>V1 @? Ѣ|4ϳ#GߪD$2)];gKӱ'_Dm߁Q'>sQrW.pX}Y.s,BRבsEvڝNbn*~u7ß/̞E? ז3z;R֠W3FRz)1s}^5 #wSWĽNHoF"ASAt&EdwYlL|XB[L_=#kw+O_g5HizR i@]^ZډdvͥWߞ@6>Mj|"91ɏ){!*gwwybn{YWnZL&sZT"H.V ;Ok,ʞ?~Զ,''&H p[|"O mQHhmk

3ϙ47 #$sU52˺uOhvK"+ş֬{oy.+ `1]YHjrv{bXnTk0o3kKuņ-ry Pr\応(_l9nϪ'fMOY.Yz d|Cx:)uyqt Ma8#6ww+rF+  %&Vs!fO3ڌk5l ffK(CbLՄ *GW4rfR Lk;!k.=0{kfͪw7޺%EK&syc2rѸ]toރ P)D7>?G+ǧWP%jӯ}XLT2USJ a\]0R2sĪ/ܟrQY_mn06:.|j介lU1\ DŹ_híu=s@~W ߞEH{z5m9 rZ7nVmӖZg;Sl1^>>q$ W0Z"k|>ľ;ȓL6i5j`|qY~JZ}%ɻ_q󭖳V N sZ<м9 ϯĽ?QC $Fʛ"bo5! ~ W_}=c|h8zʓ2YCug1#Fv~'J?xe$KRQ/&/zx8B\59an6T߲gӣK&؎9Y׍[ [=#&{i137[FzFr`2OOk(n[q8AH{Abw5j=˻8 m9 ffP|jq3hevG!cZ ~DٹE5myw~ȰV3T2ļĥюYcbZV!b8 Tɯ+my26i\>j < ~6Z+n    %`]*ĦTX0t&K%''Z9Tؠ(T}.KsuoWKvE9qwBF,c"*'VD/mEEVl("=*U=Av^\8f;ϫYZ$e6DE" % S13i~:mkz4-6QrN?FjU}K@@gѯ3%7LJqKA4npQ9rCw6;Os५{)3HFd˙磈\+JnDu itK}}w =]SDfx,)*VM;Nsb<rF+מ2CjY,I>F8\fxDNOɀx)[/mby~JZ՗ F(T3ZWkmt\h=NAH[yY~,R D%z>S܃e;XEr׬Y뛀#㈥s;],h*6@_פ/5o՞ T|}h<(&Y19)Y+/#$^5[+servt{kʜ;!'2~_?Hk|'b Ӥv"c -);Sɚ2*J^)ݲahElm񚾼b י\I>ia ~IfCM-gL~AyR!%PYesxr_R8z }%Tb͐\ G )ɫG[`֑Tq ( t<]Z򇛻q*Ҟ5m=}'2K{KS247?'v)޶sZ,@I=,^e2_aZ9l>)o CBhνwu /P|SRm@et-4@gug+WZ݅c;SJ:7Oaf[^cxoa.8y2޶_tQMqEy;Urkמ]5鑿^nُxfhg  -לukւo62m\+>YdQ)%SK=-FdNR֐'R K^+xJ1H:./!,踧4w;ةfZP")+JԍL6y'>Ж3Z/eO> c;eZ6~ɉS%zS˓эZG]0uZ#*|.l]3Եh]#̀qyIw{k}/.UǼMtyW{]Y,R|יAxIuo?ZOi?=ۙ6h|kM {',\hkCsZ;ϖk2ț;3:ƾV Ej>w݇1i϶O @ ރkd"u_HFQ*Gn!eLM$ET4grŌޫ/{㕕yEN9 '4@_Ye$Y/1j*>$XZ0Riě#GNM*[;Y:ɔ,CU3nPM?!ފTcbD(-S N}?pkN:ߋɛs>nT' cް6-U<3lNjH53p ^H1sn֕>۷S9ΆΓx=Ynˋ}ߡS{݌C,#ks-_;,1%tݴ 3"܇vn*g ̼#s'8c\pײ]ޯ[vFϴ? ՞%}䫋3LFfLˌ5'2G+箁wzۚiq\S~x8;|Gwhzt8́*Y&:@@@@ D G=-IL;V#*JRɤj鯎8csWE !3 ܥu8>MJ*mT,0)vgsHX)=~Hj\bԛ*J)͋ rα3y)޺᚟@xjfMOLPzsce7l-o?M!+6+ӥ Ĕӆ+Ϳe;ٻn՗-k"65O_GCq>qOrLpǼDL}(gܳ}$A48m~:Z-⓺o2xztGj+چ: &/L{Ub01*n:VѱusQL;!F|z64G9˻n(:Q?Z9^%\G6\tn(&H|wZi^˧9)^զ?'Y=\BwFPb:kZ72,7*6X{I9+~OCݳˤ]ĺɏo,{hK'R7Yc{1 *`i5f=ӒM{7DN8^,^1cyэZ=icr'*ڌm j5 e'L[\fi񛜹Mm|$,[wg*GxCFiF\R=@qu3ʋ?\rF>O pto"*+ɡSJP{.)k !qnC[$Rחӽ_,/ jPwO5vBf>\YWl)pxgeC{.ޮhM7]ߣ?Z9QWmEuO{NN矣s]s Qy,bR    3$kJUh+Hhl^|_|S9j̜y[|xD)>cmNxZ97D@liH8$UN}W?2zIeiM*:e Kmԡο@ƗB^]tGA3CCC^Bn}1MƧΑOؿ:~Dċ{WFZw7+'ɦӚHHoM?ȒGlL=1IrnILSS"Sd Nh4O+? =ҧ"%9//`CF⇆7^Ys1xtib1KĝΟ]/PQp Dȶ!dMePG~I$I2DrGx`oO{͘D~?PūUO}KuFh4K#c QjyDOvH~8) }|ts$6hD*O=2X??*;D.4NVޭ%^E:51     D^UL;G,*M?|u}qعA^"ucL'dk.4$-&<X1#>`F ֕XW`ߣ;We3+y^faRTbVǣK#$ F*%FO(c8w$Z Iʼn^\Ж3JzkKnh2wJwϹ5}6cN 5i"WҰ \91o-(cxbo{%_[hrqm Z\<G3+&ROǓU5.>oɧ?Z9ow%/{CajfQyw!V{ xRU     H<1ɷIJơN\]7rJ٦SWą_j|'Yʹyix4GPҷJ;+l2Zrؾmp5v}ZV4Izϲdn2!XOwh4N6sԲl[)x8ĪճK8k[Gve H0]ӒssK=xOyA[/g|zn|í5$FGrmᷓn#Gy1děEFjBgdqT}ָf"dE F{/8Vq5m;'͕\MuGv~r0KU>w|}_ 9G+YxGpeD~3qȮ =N?-@@@:;K̕C^pd9I .\982uʹY\D3f;-Rj޹:7yhxO1ZVݘȓm å|Lc’d{zxǼЃ9|az:˰W傔*t0Fn? YtYLefYckHvvv%!(|osVѿ]t<>_EeNry9!Ze+" rlי3XVT$ JH%qU1Ku&@ۗo8NZUS3wAwpi˦#ʾw;mɑle*)g:zX=xpGZdIB{'%hJ%&$8mZ*npTX.8P/ѥ =/6ȉGi!{֖ʧi<HQ`MX+ypĘ^JT}۸&c|r.ҍTb˾ :0]Ƥ峣źO$p#̦ۃRرR"r:2t{(%Q)՝4fÓod: 阌HnVliRȼC1fɕM$ȧ#H\q6;jrD\0Q;slKc߂mۣµ2ւ`'"Y㐬[_26 zVZçm?y䒝yyĞaIwF;'xK5CvS` xJ֎gupv=v50N&RZj bj"6 fl9Fu.PBݾyK{Gfi\ Ҋ@@@͵3o7Fje Cݾ*>NcU\S٭dIZ®r|!&E1KzD+ǣ`'O@@@-~EуRKg|IuȰkO]Cg6e>-soG" =g˟;0{zῗn uúp۬P(i"E NXnȹ"c9ڝNbkS5{NZ&^tI驺ǣK#o+zNn24OtM]:!s:RO5^5e1a jn%ulݶ<}AFNK>+K]^.Nm\zu $jjccߤ'sy'ąv0@[/]_|_]/6P^ޙ$bGp ٍ7g~ dw/ԗ#T Q2LNoɚq&5\z+go;'U2Y -sopMŠI¸})s#=ZfߘIKU/qqΏ[vwȵ-h 3k 1Aɋ|WPW[ \ ] */D-4}{GĪZIW)3d^C9v#]"@g˙%>3@"? h3s[ 1ePvؘd~'-I[Vq~*ju f!BN~mORn-]-u ךf\%Q ֵwwybn{CݾM4vIM&I&3s{UYYIΚYeGr|:wC >A@@@`"jpٓ-xt۩i޸a:M)Z9oa@e佬8Ge 55>|fn?#t nLҎ OV,5>)B-!Zn̗ `{Փd q`ٖjr QBvC |#~9S3b+C\ >;0owIɎ;-PIq>x&ӗGDU.@D;@]^әt Ma8#9 UGb{5gb.>cHK/VFHҁ`іOh.xٟ,5KRBBu¿0aBers|SnmuWP/b D\Q#6fϾڬYT5m=sN+O@@@ V5Y)mya^#ceqH'o>?-ZEn' */y80ggϏ?8UuepXv՘֑;|ԥCYVח14u DM Oϖȼy 7[*b1MHddWIKO)5*Bt.[8 {.-3 Ͷ=.|jN|zVF@^f 9^z?}kW?ʋk=We & v3˘ !/֍U[neYoxTm54~Lġ汏w0Kb.-ܻ͞ _tfelTO!t^P f>Q\Dvogg+,hD&;|EP+*{g[&*>?'c,d#Z9Z(    m@1)2ngIg| E+',tx eToe?u?cǎ-9U6-\QcƒZM!#vɹUa ~IW=}ʹA|_Z5x5WW+\ JHB.[fm:ՓDֿ\0ῲ-!hY0,%C'}l;A_^<=Oɖw^YynE)]n]1ݏ`3~dJE#NTـ(&y i=wt 2֮T^z')?rX 1R)|uh5[7p/T=G uB}|h>D֣6$zD+G哖@@@@m~s-9>LŢi*= M+p%14ɴaݹ~/]cpZD 铝6uKȕ\+ѵ YV4KeRGQooxN= r{_ᅺ}6} ?Z9>D#'6pAWMr(t&~J}.$f7̘6}deFUCWl k۴rVy& $H8b#N[:}Mɋ^[׼+3W{Z0P H͋ƃ2mX_0`Z=il:w8Nr]ZV/02K{hO|YQŞiR쳐}Q}IٱyŖd͊LH%nY|yDQZ9B> ?}y!$mӴ}":`2j2n鞸> ,dv~wh*ܗ1N$cBf)M樺(ܐN;3-`<f2pmjK֤muEvEP;.&ڥpIF2~zՑxBݾ">1smʉR _ϴVR\>) 1D.B68 o2I8CL 5nVl|hn#!"'a鿖OdB3BzqャLqݩJ*jQnj-: sr67}&.7,Hr{W#S[W>RgY<[$k+źNxRcNESY1fZ5B g wG 31鳦[/mʉQ9szD+GS>)1"BFF(˜e+uTӊ!]Zr.H 詼iX7 eS=1ח?ڽʼ"iDžs @E򯬲@>p"_4~cb T||Iu ؕJ]o9l"珽UqAzʱj2ߝ0tЋ@Z9EdJtqQ7&̈;Uw?wYFEd97UM1oDt=P0vSI] fK7FAumTN,DZ9.11m\ty;d&V?9YK{oکd)6-D}ԟvPh\}!lѲ~2(;on] 09we[G Źޱev?Cӣ[gPyiM~סl'#QuQ0_'}zK}Zv+w=>G.biVN~ȺszD+'T/|K2޵$=3YzSXY(fغJ&5WOu԰%0s(j-? \)o.Y=MR QM%G OGFIuÇo!k;rñU )VE-2tW 皏dNXdi{F T2Tbԙim_m 2r Y*x 9y^yfG9J9vJ5K_^vvyzHEߞr9/1V\6gR``K4RﺞpH\2`1mzn_B_"F~V_ _(>t!4u\H@@@@@@@@@@@@@ Ђ_` $0            @2I1` C ~A@@@@@@@@@@@d"           b@$@ QD$@@@@@@@@@@@H -@H!Z             D C@.3_j[$? I!󦍓 )I#JZsm%WG/2 G7BdbH({P*2Xiѷe/iRRhdH=CrJCT+TQRtMyGl^xOiSS [| ē6JEMlԽ|#HifMOLPzsce7l-o?J!+6+ӥ Ĕӆ+Ϳe;ٻn՗;riHj!Fwv|ׯG?˙ˋOh'JZCM%봏 H|/ "@{{; -ˋH|z n:y >¾xD}Q|zSчt^&%צ?'Y=\Bw&i|RȮbL یc'elhtO+ ? yv϶.vj$?-&P_KHf$샵:lOLK61:)"{p{ŌyG79\ 8,sمW_M:yѷDoZ?t-v&Bu&8O6_2mqor63s #!d3cbM>}bқO#>Ou25V!_9OOֽӚHHoM?ȒGll *g_Y&U:MЧZl}𧜹j) h$!jyny,3\'a߽vQ/hF(bJ}qSJNs^' uyhSg$O'F& OwjNO@(sëc􈔚S,%^3'Oȯt]Td.ܟSc2u¸{m [Lph!@du/cUTtĸ5e0##I$2RT`<W^ݱLuKQ$X#CSش\"r${M1҈[ҖeVB+g 9ઊ [8;?d>,$ w?\c(T\\dzMGLa zD.~hd0b& z1G1).:\TZٶIwFycd -ơcSقg>c!Κwf)3!%G-up91˺k]~;ʵ]}/gj */Bn5j'Z-Ahý7_M{Ԕ~"5,>(;vKam]3AD+>Wh+/~ci1Tz=ZwwU @    x1I%coeCWϝn䔲M6F/V|'Yʹyi1ٸyE?ofg-ѹmI7/"_+!;e>e+Y$gYr˲prp3BLM!$1:ldQr^'5s1oJ&89hmteY:RqV.Q9iyk\4oGq1Ipq4i'],)lm/mO}D&>wYqzrS^S5I/2'49qwq\H.θ?o{lN\b5;sr{A[lْ{8fKen*کܬr,ii C_ZNC!<7=H񺹜\9V +,7?9))pxmܾQW@ MMJJ[-[4_7ȣ6s8H?ZR2vʦiuf)|m>;(;ӦoN⸔S2,-3&w.:M̡:f"em~IJF(f!v+wj=^sж%tR ޫH2s?Q%*Bν79ęae`ݢi͜܆ 6-z:&v 6:Ghn"iF3$N:hyv7U?XurH9}`^|b9洿5Bts3ZЭ͟3Y 6pzWRng~WkR]/Цӆ) w8EST9MrqBׯEs1>~Wqan@ :?H gܪ1fNΖ:VRRKO5e;:Z# ֗^)穠{8?!wGя;W˲f/.%v'M;:$shk+ZZ2 Eh~o(JZu,iC]*zbRHk~zrl/\^􏞬s)phaUv =z/@ :ve6Bv )iSTC#n`?nxgk\۟Hc֋oyuoEIw[Sz_In:nIz6j/8D-8{FLeqʐaD~ߧnqJFK8K+nLK!R3pmJ!(Z}G_TQ{KU/zqSEe*,e݃g~kg5x1ytzߡ{im d/q|Wq@ :1.,lyF]xpc\PUh7;\幮%=gW߿!.ٮшax6 MQ 1Z$JG&X,4"C>P{>AV\1\#btvmZC'i{ ʩH.Zt?%)ZtS$݇*ƱI~'=.Q1n/ #;^]:yUfx793zy}?7/'J?9,/M)+҃9Zݓ3ߖ Cj.Y17Q4@CC Vpj(N9Q+qˑ{'0ý8]squ$~N \ @3 ס4J_S#>Įmi硩C~3qFG%NGiIܖ]q+<0 t\\^&,{B/F(ږgj}BiH'J[V.P׺̖mkT&3`Ɗ6T"7p^I;hW-w%pgTtc_?& Qoo.yQی#;;Q8={^6m"| 2Y fjO2Tsk ='C6?ۙwŻwpS+'|v \h~;ZnWAP;oĝC^0x,':{\xN/K $Ju$ @O@pQcRF?]jdyZ[W*"v݀=RvC(QΝsZr-V%,z[zd1s->/+bɣtzh(M"YESm]5ӥUe"[smˌաOs_uL+=w(m|VLU[;\;gva=Q+s`6ie|`/cF_~ULw&wN׵)=%eGm{Yvt!^JٰO1nDW RW.5NɄ@o-wx~MWWxk<[%}@S@b^{Cs!$aOR9~'l*ra@ o*ۘc??jWL3K3>ϓQ֥ثzjjjT{*jKVFxwD`А5ꁯ~5wd⨰CJj{wNvy6 ^ w5̻ʜ; C*/{&mȞ:b@lg]6]oS9љ&;6/OvԡJ[-hN;3^(ѶUqps9;~߿^!^I?'?;f8}]tJ @CAPXH"/v"R:`խծ첥 E֡#XK殿vmj3OxhrWL{< _]J䁑jlzMs2xr,kڵn}BqR^[|O4tkgmi=4w$>v.~ o,Mfg{FtO<)G{_7_18VՂo%B׎\Bi{Eh99`s㙋['YՏ x:Y9sKz5?gR|ܓ#L?|ҽ_>Ų_Oy(=t*Z;+^& _] Hl~"$Kl?%M?{ @ / xaF'ܮ:;vm͈eϽwݳ-eK5sM~!sBwq'Wp@]4mY:TF͡g X64ZѳWYfIjX6~cZȈ0jPsȳPBۼSCwP[:ИxjuSߨhjY/jU'} #>;{!9IQ̮rY9w5MDp+AEdt ndX,[ 1* `X=IeںE ےf0;܊LyjJr=!~*UJ04!q=@,^\)[Wj@ `4q xp 7YzX'8Vw"_Ե _ېߤҎBZfԔ]ἔOlyGBK/߭lƤ}5).nYV<Ǿ>J0ѮLJ2ܯ\qFXѣGx #3rt^~ /~ڝYP_艧n UD' 9:އGK%>tSN}f_tBn_g;uHH(آZk̓O:r3D=tQi z𥾩Յ$rDB\#lg[ʎ/S}z#Ȉ u\6e<T;=;ס-v%$A ,Lʲ{w@ D Y)OLSь1^Fgwmggt>'J׺E!\$5Z`N1Ǐi7yͦ+M"H7:W?vݴH6E'a <r2jF"Kp^wv6kjᬩ0_F.O(Ssi ={H ׵SF @@"mAr4 {LW5yIn/<>h˖-1[rlIӲ E;ՔOelN~{3or~ۇY9>v|E ߏ6|cQQMoW\R$qD-wjvϰ&G?oؼ>3 R)wx\UPxϟs©墭|p"7H7H@ \kpRjޢXՉUi9 #G !cl[wjۖ_χӏχڵۮM+OOt3gCۡixEoKc!R)Ӛ9 F =M^9AM:MӪ l8%$ 4!TcFG;6M_$xF YO=m;XB⽷(3Ub"D~S$۳od`rg*Ilp$9*>IKxl@zyn[Ч!^\ UM FiSP|Um|e.vF^Jk/| 0I2^FEيNZ tQa>yT6scz7銇ƨeAwhQ^$3ƫJJXf>Y; -OhLHUn pQ]}jweg]] nͤ޴X Ri i9(V:~<#@ p#kbWl_$/=AIFJUꢩNZq7uIWt'3oyuHO5&Qj04[!2u&0:BE/RSMUu5- xւE+. 4Umҿ_v&6 {N@֊Ob/~X;eS^"A 3IQ.3*=]הhh4Z_j{k t!}"z5eYNalOD|;&SJAXx}ί;ᩞ~)lhx$co7A},^Z%( FCs _qqsU/iA`D"rB$E4vtHѬgV\9mUDctޫKDY *sh?TMR-x3uS2XJfsZVmk1AF=b}y9D)r6|W@LΕ"h^ZZ1y##ժ]- ⴬=Wxvkgx8Y9(HUN_MK7d2e@%r«cet`.=Xn@Aq3bQs3LhӝRjnlJR)!0OQnߚs^j8jk}N~*u s^!+s__/ϸ3@  PK^Qm}zV)xy]_EF@dN\+g!;\^yaۿlkQqKMqJPރ Q?GR~FŲO:Rظn9I M:QDvvRnMbd?9wJn~2p#+ibBۉ6|;v‹}^6զ$Pxd}Y:ғT/ jG"U5kL+(CG>fxȠ1mcK\?c K[f *K9E@Pt UǏLHU1|΂Ҝʩ-Co߽&͎BTF^S܍ʒu a9(44:zy8@  M];d7>jB}'^GsH g | :5QϼiӽӏT>)RYmAxg.U![ IR6=Kp"ԭe&38g3aX^}YaԳ_~+͗H=} 3ѐxT)@_jg)#c\:w-z}l ˧fg3ӵLftMsG>9Ð8ekK׮][:g97P%Hb$ĉP؋*e^Dkg}Shr¿1*M"S8v+OOZ o躼U?xqtg F&z-Mwoi?hy }yK }!]'7)^Za<ݮt.rPh:9iuA8@ @' ne\ɸ%827%"'@.5т޸l9d#hluG2:aiè,ѾutG=~(eDӲti2Oc32͌N+vp 3cű7hG0clwmp6۞vِ6xG yP*((@N)*ΏwW Jֳ-uރ4p˿.QVmFKq'"{Q:p҇,@ |Mt(lkK,[Iq \ < 5+QՉƿ,`G*OѲUZ)F'KkiSU5*Ȏ =\RiT5r>rYD?W=1cƔ*L{?Vڸ/ Gv ^AKjMt b$=VBO$2Kq>޴͡Gy_;P{zy.D4b@ݕojgP-#V#zRVݷۮy^/?9,4}q ??:^9x0RR$q-%nP%[2T| k6lF^C щ8K`Ǣ[>KbT]o[){hT8iQwGXw3xrg1*! j/Am@zɊ3 I9 nO;MN)tMV8& \v<'HC>YÅT_[g䛜!pgt۾DmWnӁͱ=v=+ȥ E]Y֋BEjl*Qo>?BMBmq~PgʤtaFBZ3C-^<=!돗}fmsɛ 7)pT(5STV{¬t$NH﹞Knӏμ%y9-dx.ߔbhv#6F:޸`,O7ˉ*n<gNs9,9trA|N! <#w>H=kB%Wb4 g7?+_ Y9t`u8@ m B3fJ!Yq֕!xcqwu9Ά Hïw\B:tֈ=h$V%,z[zd1s->7w!<]*Z,]n2zpnm:i n2Vff=!zz !)\Dqkr;+@WTQbݳYp?_!Eߟ56ѠrN'#bGեF{G++O-{Eڴ-eIqQTX_}]T` R6S*Ufv'7%5rJ?B;/$]PQrK#$G.ƒ^|[wzL%a/\|6{*%MLukr)Za>V!z},ZX_:7/ :%kx;SV iF*u3rqvp{'Dž_ӏ^ @@GO іM Q uSJ(m3jnHQFߥ*%/[\x}M%QGэz{齈(HqOWsI~MC7z#f:ѳѝ|b_OKEQ2-]2͌X^*txdK6 kxE_mLwH_Z>u8]`˔?WZO9j^Tn'H"K$DO7 dWQa הO_Shvn >vM{#3ʧm.|#3.c^=կ\LvsLI- ѩeot$wJsUӴ.X%1ztG֡L(m]g2-Z7|sk Jw_ ^Ѹү$:hjٙw=y0@rtpuRrnDJ&=BdGlg>(]tDya{Q@u Y8}J*0ߟ-Yy/^~FrѦoK@;+NY])C EqZ UitK-w<-O`<*P@SE/ټVuS]~VJ,lY\M[SeF_<7]6堠tp- #+xN@ D%{?`E ]HTx-rvq՗]4(3֡={:t$֗?]uy^Ӿ '$ ү΄lK'X:(ՠjᩑ*=bo ѴM_|"SQqi*_1#*6fi ޫߩs(ЉzبG~.qkaE 6Tg:D:ܡ4cѣy"Ͱyu [_$RG"Ҭ'-J2c̻p݊ds\m:`25Z믜k>'?Yڗص]r;ӥK9@I6Teꦣ秎P q{\epquv~NV^d4ji8<+H!Tb^1e9BS9Ap3N~<3qM=AU]l/s ᾿yP=j ] @ A!J @ @ @vC@ @ @ @Q7>@ @ @ @QKs1@ @ @ @Q7>@ @ @ @QKs1@ @ @ @Q7>@ @ @ @QKs1@ @ @ @Q7>@ @ @ @QKs1@ @ @ @Q7>@ @ @ @QKs1@ @ @ @Q7>@ @ @ @QKs1@ @ @ _C(L9*((v6͝:V6ָMPE I כk+/91;k`p Gy"i~}bz)dcwט%ӏP.9QVhCzWSD+f/7fj~ד%p|-vp"Z!U1n&P{7&Zu]^J]^:nDn)ޥ  <ӮD Ow~_#xQA q]/9=&&AVIթBC=O3AW׳*IEKFyVmc>PCy:TܦQ&Ciꋣ}2 $I3$P̨xo>~7̚ ~@ |O@Eѥ,Y9olt8@b9kY3"5DeuE߬=AĴuI9WkKaa!Qww=G*/~.J`~Vvp+43ҶHE7[MeO}3!Ն|+5-f4!gfq=0=q:v|L #dOm6CJt`N/B=~Cb/Bpӏ] ..e{dzy(O)/ZE!y=:9.۔6E Q{$>_[ BKw<9NYݳkO^0M;Խ^ vBD ^{,ڹ XI/~P;lzg@ )D:u2y:®h67/zECѯ6t mB*wYt2hCo>jDvf ~w'I]꿨pB}P;,K~sI^gBF"3vC6$C_;Js Uc^4h4icQ%*ЕPODS&I~[^5k,u\.i:rDcabd (De=hz=_֟>H 9 CȊ7XeVjП!$T]] bٶzMnBgxG Ә@?ajbÿ$PU` :/(n.G!(n`= TXEď5 @ cgۡ2U*TZ~zmuĨ7/31?}Z?pNORRoY. `=gP הO_Shk%B^^ /-(%GGc l s n-3i( gv.({41=΢O)v{#9lw bU7& 7$~>`so.(і'ۤH,Lԙ3Qq)._T }vl|'+NrSƐ,?H?"&xY~]`B;uSDu9J Aڲdxz"@IDAT֏s:ڃq_ﱻKV.Ň'.Kvu{ ^f:޵ѳ6\7DV2 شn:^Pʛ3 1l@?@MT_(j,N)@  ' 5N]Mu];wښ^9}_m]St~m.3_Rtk#CC 2#_N?. K&-KW 79wQ,zv ri^iYlո#2NBl##¨!Kɡ:nCT ˨!Te!~lDWLwPat [(;wzw YIy5Yzte(xڹ+wEY;$r\.~KR[}{ƅDp5ŕ|b{Pk~7J, y쬐+QmrM[Ha["ތ.C:_ @ `4q xp 7TC^l\Iou7zȗ2um#6r7釴P/5eW8͒;Ė7~-:ܲ:uN,kRc/zj=&7 #7 ܽKm=Mݙ%z0#Wv!85_w_YT ta= \qFb_#wb{Q;f>ɷ{лG+(B۽B>OꙌa[NRv~ab, .n},<@ Dh͒Ly=f+C/MxzvvHCIp Zz`N1Ǐ7yͦ+ HO7<vݴH{E Df$t==~|Όf@-(55sgׂy,57 墭] Oe5 ^Lyk3BxXJZ7c`?'ӓ=s^yx}Ж-[bْev)7ؤ7]ʮ;Ow~M};?_s+TsTV5Cuvv%8®z5_ 7Z}*䄉h Ȇ$ڥ y?*~1!OꙒ y!3:w;cw@ |McNVKW*U'Wmn2?.a~2vʦiuf)|m ݇#/f!v+wj #Y ]/;4j{(1V"52PA/ޡ΢'`gqzVl?F/fd<#KKMbNpsՐҽ, mKB#(3Ub"|~3%sc ik<^;8fEx\??$ym&%)|aפz?RwR-nt]r>(ӴWQ6JSqkA(1(ңJBu\R.WL[j[ 9$x@ hWrJO gܪ1f$ITI5B]rTP}?r%phݬcE,/IG8]A.Ry$YD}/ɷ,t'O4g#P|1ȡ @@A1)#&w&IKl2~ǐE%y߹hKXfh-Ƥ4J f"$\FGeT~陹\S!eסsŸ!v<ÝMMk\FjYG~Ot&F?ͨt]S־5`k}׮̓tהeQ;=uLJx/gPzx{iN_lrMȖ[*ʟs;&Փsg>'rIxC{yǼQW\hmnF?\U:KZ$h^ODkԆ`db;/g{X5PRjPrm8ndp,./~X;{ckǍsۼ=}EPC)\C2VbmsT\9,6xVCǍsBSOrY<\ @`AT5<2 &KMMR y؃߻qm"g"H^dFIOv%_^1Ztk6& 9ٹ%٨Ue&ud ^r)3UjIuuFg)p8Y ׍GO諔*jOW=yi[4wʼC{נּhC T?>^-8{FG~כb!x(R}fBX d2%KTV=yjc[ִ,zj g@ @ATm}zV)`)A#-\x΁.}9)&;:BwKs'BRq½cYp*{0{"ʝHޯߨ`)QIvt=Bi{+ YeΝC›6GȲ5(HjЛ6z_YIwEG>OjR k(Ex}Yn) m8G6odӧjRsz߃s2ƭ$z}S}"pi.=0k֬`bUގ"Z',Rm}QOkT.ma[@ @%ЮDvl|Rb7 5xt/?/cRѿ_W~S97nrx#yfTu[^KU"C!!aMh7n*b،AZfrp@B;,zr@@͎Q}aG(ԲdZ[nҝ|jw`X>5>tq I5l@;t_ ȟ8WU) ru 5nע)߉v2O2@x$L+}YaԳ_~+͗LiC>yZ$[\Hd CQLJW3窮8-$ ?0~$qǿz~e` F&U  @:Dű@m A$ v~B"zqgGՓ.sȮT2\ξ\?uO!qjI wFKXJNױK~Y Lnӑ]Y֋BEj맴uG>AEJl3hNϚk@?~%O83Nl+Y2)`<3Vqd:z~C־B̧w4f=d7vBzʷѽ ˇj (q_UavzB|r^:D eJ(x>~C @ATbs!4[a,R3$6/ܺR9$A1;ۃ g pI?+ *u{HKXgc[}oTn BbyTjY=dVp2quWy;'jeYH&̐ 6=S/۟w=¯wK0\*@WTQbݳum}ǑH$H^e7_ά4Kw% MP{3`b3V+c^j*(GFCKA=h}FrgW^iܼfK ~a9I!;,۸q6mily?o4qE+/EH $PsW)\m&ٶߨ>Vm]^ktq^HkE燄Do-Q, )-,NT4񔵲>B=v! w:wo16Arf!BhYOya \[ةoA]怿5`}uv+Ϝm?J҇F 3sj.g'on>n:e~_5VF;FE?vDt~H ; cZY9@=s3_`}?ޢ}Mc^3KwzWw3iNY[uu]Rt0.Kg>n:Vi]u6Q3 &UqjKwDݡoLeeA[ʽe כּ5Ji<<&O|8S2&.~%+ߥbn{tm̵=``r=U j5$krGADۜ WLp΃q,KkHpe@mOOY;;4<.f%k}PQs-'#FWܠ[eYu' "$@H H& {}b(4BP (!-g$D]>zGe^ц+˽?*w@UW]l%ٱ}18ĘHg:J= 8柧sW_djjY'M!\{<N7pӇHhfUrpʦv\34`I]ꏷuѽ Œu*괝oz=tgzj=eIg4+8Z~srd' ; Mv7 1!O1fʹɚ̸rӗhpQ^sYi0ރqwJ.YrY>`_r=;dEj!io:gMO!%v~?|A?䎛k,zpq@c1UkEdT^@H $bD be/5j[K[K@K" G$oUvtU[V\-C>SL2fH͐_%hczu{KXw.N_g9ZΝ8I뫼.kv%GؖQI8]V@}[V-L1QCj$lG[=E1]aQvb{Wx7_(mAֆֳ0jC]vȣhj`TFϟ=BĶ ̏@H $r $@H $@H $@H "@@H $@H $@H $@!z@H $@H $@H QpH.̌@H $@H $@H 'DC $@H $@H $@(\ $@H $@H $@O?@H $@H $@H $D"Q03@H $@H $@H $Ÿ $@H $@H $@H "Dpaf$@H $@H $@H ? H $@H $@H $EDH $@H $@H $8@1D@H $@H $@H $ …@H $@H $@H $]0H1SCJJ3fyk|.e@|NCԖ֋Mug|ŲcOS*Y&?FŔ1꫇ѥ 4*KqWZ_ Y13Jpꏖ:Ts'g'3"5VoYm ,_~mca)wvI?1qwي (Sl}8,ߔu114f6_ͯBڻEҐ`PiL?Qqڲ'<87$+ʗZQBE*()tIEAp? hPԛ~[.LYY,^()!.c]=ᚘVE'^_^ɳPCK󻷰=0.AԴV`' Ex ?>g0C-}F?u a;TN,U:Y8(-%T.AgwTT_O{gX^xGp.@XPQ++rP?>?-@eѾvLNu ga䅲'ۣ8Ħco6L]}*] q?#m1]j#߾-evw' `iԡw֜r7)wjj"O^;vp,3ekg.+zϻ؞~)Y`b8j MF'SkWcJ]!JON}ŅtuXU3r66!f\RA Ys3& hH!+c\$'?qw (T_VߺeIC2{M6z !C_VqåO`";]@lz[7/w+&P1>)YtW52ٙ_X 1-yrYa6H8&ݗ=_'oaWljopp^shtMKk1뉃^R8~ZY\IwCàvv|YF& 2'D[=^}7?i6…'EN꺭 u[ժOb[ ?>+z_1r^Kb';T" H!S':Y8(-EH.B;q_0!9&" zQ|Հ8CٿˆIU ۲t.7z) ".+,~FsjO1rit;t3;LښkK*A)?1|mQ]~e7~oj?n=o1-zIm zX 4כ*SB}ެ:Tf`Paԥ/xqލyϫ\Dg=~@Ws Wt?zσGWNa֕;RJ/@$raS? !~GaX$=Ʋ6 b\[,֓_7{P|7 28Yeɮ.Qqq)X^xc[>*BI1erY~:1wRF e?-©~*'-rKꂝ Nhܡr Z*6ar)A㠴7ms+SY}wuugq t8Yd\ƕIЅ_X;fj.+?e\\t{ C o-f/g*1oXi}֔7C\>=8Āӧ-+oѵ T.\~qW1}!{faCLL==!_8HNt*_!ܓ""mZ)F 0~RBOk #(1WnTkw[&rCU/omb_)po\.S~߅ !A|ʑNH>AAr_;Y?иC&X .r!޿yqۭO /L1:)n0Fg=?BOtSS&^5op*9D~b_ H,(d1)^~F\"u\EEs R";y*Oy${0"{yrP}̺3ԗjRvrg Tv6!O8oeֵsZk-*[49?N"ԿE࡞J@B}5b$L "x{/$ϿS>݌|K};e#)(!3=*DKaMalo_D[#rSB}ٚ =GӼ.N<7y$\ WT$<4y 1ڵ+fW gV=pZ_*a$+}m%jFfz;=?kC{h=Ut(OO딨)}yC{_%/u?Law[SqCd9̀A㠴S2"|x}U S!}awu<4GaH@Y,6J2/v᭼2)[oeݦZ9mZڅL_Y?{@:z/ŗ>{Llo;,ξA>yၺr:cl'FcHP8 $GF`y @Zh}hKK[w {MQi @e=db_o,_|X+;R {Ӡ$Mҵ 5}o.|b>>&z:v#v%BE {r2 MΫg& Xz3GH3Mv"'?y,R! W?!5=-{ML.iahӦƫkƧK|+mWkr$序ZQBzf.Zt5YjI~c/9HQg)R7,4Gz2ObJoUAŬ}!%% ?>s|*Gw'(;ð@@+(]JD^'wq _7w;ԧt}xjM tp0 "+|H02>jX8_r=?Rg=z~ @n.`;ñHhD_'{&?ם.<%l nG:$r}J`nC `$ (:@Tqʅ%#;O5c }/Ŀﻤk}s2ZALLcB4|>_f'ViN-L\U5.QǔS8Vrxh럯՗8,ICn{`0: kX;GO6m~r*ol寮-sMQk `Ljt}\$~gH -3{BHn= mAxEB_yvMI?FWMctplKf-JM@z?!E]^9YpT+1f5TUR{ "ۮ(]GL3qqQujiuĥ ֧k2`ZL"fT)GޞS;4Vnok4:`HxO`|e>AA|AU"ˡIwȺ"RMWv<^'K ېE\bک>t).?l(E=@~ló\\jՑtaQ@傕07 neo>{]!_@}t°3:>=ttv\r ALR4﷉LrxHqL _9<9Qcak4-X{o}`$&gxiU]<)7;b#S\}m_ΦQ%>jsu wOi7SĤ,0[ǭl\{5I"j\z.O@Pp.ಜٳs Zy?P}P9`} XH﫤^sX;qq%5I< 4Jz( h.9qwqdR*y#n@`oJ;HȠk /TΥ%CD*ViaGb[g~ǥ/cx jdg/s&1$w_LΚ' mFطƛ1 n׏550)b?3Oɞ.-w7,guu/ɳbv˩4%_ _͠U a?`>J" C?!Ix W>xvV|E!J5+/l t6̙c~[]PjݺuUso*klܟ0`0BV|z2Ow8'8 y"uOP>AAx}俯z{];qvMQ~.b_' 0?h4 'ha Qo. @i!kU)a13r]/pv< H/6{oiQ*v )6)eoݘTuCi| n}zjp͸\8\AAy_%'\..@vB✄<\\ŁxB4J ͏ *@}\v*/.t?̆z2*n.ʹfn,6\Bϒ6=|>IOP:~|wx鮲,S53UMWctPΪ:b7\7j%tySP-&Ɏ{#uk 1H{OOc_8P٫{Gk~MMǡ֪%ۏY3G>h|UJ-+́Ș4_<~/;O5tj=Ng|}Ý_A]VD(H'BaUxչCMEG4;ї.#j'>/k䞹mxxm{RyJ9p(嬟q~+\ե'Jr].#0;#pq“+>-7 ߼\)Os', wa%e/Bو}];O׎HuDH3pګy *_bo>?|2}tyg.SD2gܜ)Opm? fgzȤ_'K+5K.k柿g\ecPx ]OIjsωӕG\Ρ֨O!+ OgTҟmJk>m0I\VXXgחN7}()i\ﲽy|9>)o,qxD'?84DGW}'|f7ڌk23˞/я+럔~IIƼ\0(A}ykE_'˃v*;q>.%2|}srȕͅn;vW"H;u<w@<@Tiq~BerL9>J--_u9򦋮Br.yAy3LNlYe,zI}hci/O=L?pϷ\Ss(~Ukօܭ#cmt)~d[\ǡrr'(.oPB5RB~V[m#o{w| =>Բ9˾hM p)t܉CRWgOSN "/do5.EKCݏջ^X?{jtWL2S_  H'X!zy&t^r佢L#t9mS'3 sii=PKR xBR YO'LTTN*&}yx]j'4P9h}r*b[Pn%L@x[q`m*I+7UnW]&Fo HkE燄Do-Q, )-,NT4񔵲>B=v! aw:w5P9忬YOBz fg߲NK0V࿫pL3KS3kkf-u=vg17MKcz72+jߢ;" i:ZT?z$v ShfufZu 6/;-:ޔ9=${Wo|l%E 3t8?XŞvuڌG j&o,TiNYxg ny ~wSf_YP}r6yj@IDAT2WwݚLaWh]D$O{4~<vOtT\71Mo|1 }h0cZ?׌XV3g[W{ l^3vyXh)*[~NRek2ɰ0*-y"y%-2Dmɲ~'3B$p)}qD5TI*o\C6䭗kOp=иC I rs=4JsL2CށM!V0;!6G}M$nDo\lT4]>" 1{,.AHKY{;I&ѱu87yqrTY+p~Eqw+r^H #W뚭d0;B?z|c\\UL-VY9+9=RY)= =zmCq)O/NdM ;=6IgH=iѓb?E6{uTSso~){֨M°>]8?.g_J܈i;g6_TU ;tlw݁57֬6J]KwOB/y,yȓի]]s_g7Oz#>SGWy#z^vB m)\,%xA&i(<-8̉z3o"9y%iǫz`;AR!o-P9Rp?#[bՖ.5=tI}Itx:rdOZ_ h+SX/x@@Lv/軪yoQ̎o) eEofǧ:0V%QDew>rزfǞn~)ړ0חp< (~)k&3bfȯzvw;\')Z)8z>R5T=i- A8RxK*NKi+gwOX>AA唪`rW9}vb?*'Hªซ~>TΥWpD;E[/B' vvI_$> (h)@H $@H $@H $"G#Y $@H $@H $@H$@H $@H $@H $z ]Fo@H $@H $@H $3 $@H $@H $@HD-/@H $@H $@H $ Q $@H $@H $mpEEH $@H $@H $z= U $@H $@H $@H $@H $@H $@'D $@H $@H $@Hq $@H $@H $pW@H $@H $@H 68@""$@H $@H $@V2di158c&7}RG4DmiTwW,;;8racXLz]ڀQ:N4*O7|eU+?3Ȩh@=wq&y>#Rc6p6rNɌhiU6ڋg$YYFbVĴ#)̩2,ߔu114ַ֦?Y_5_]zhzW *_*_[I%)1'YIEۃ~eD}bow/uJnjN#)uE$OSVaBsM'v"yMY "{:O)џ)A.37,xt.Gq $< NgmSUǧB*crK8 $/=ty&6{ar6P9RpɟwmWAWq%RۭR!}>V]$EO6`徦nN< YةUnƾK#?]ӧXt{@:Ä'd6=?.Edb~xeA)ܽ=֎`ՖYuh_ݡکK_ 4)4ݩW͹:ki(Dhb uu0vr D \AXK\T~IҞ.oʶȷv>/(OXVs2AP+Y9PUN(OB*}tQ+_(۟)WŠ_ l'Q(Oh?r@H 8 Ł"2Lb.1S@,xBH#@<yޫ!:X} U~" /]|!}IR .`_"I%%4#Ʊd3@yljk3ٿyq,(\E D| P=c0 ' ZKl$P=:WJkD~~O %D}nqKavCnuvt{|;idCq>0p<*9Lm U\PZš`&~ȳK€'tW8T"ڥw}cF9d֘4v)l}ί*_b EYRpl{\ZJO%E CGsvc8xH&,-;uwPeqɹFD2v3H~O&篕F4I 39& W]Kyg˞]#kRBӎA!gߠct.LwfwzV=[5U ,ٽ2Sߺ)qff]hK5y)wi9}>@S!tu|ԺvnQkEŮ N9\ [4e;tc"?R>Vm3 v|L  lpJ;Rc[׸1[T3c 'ΔߩdrE%оې >%X0jG)lCgiTvƣu{J L<<k׮]E/Yvffi: G 2 !#P&GnbiY #]γ6@H  |ͬSlMVEoIVoABAMr04R9TXs!gH0^Y_m  ޣ;Z[];=E[ZWE&KW:󷵢gBJ@ڻ1R3Fm t6spsQsp $| (;{E)Ffg#XOԛrw%E7{/2 |&I \ӌU;f}b?xKj>|+BK\LĘb3Ukn߸p|RTYߑ++t͡ʹ;xv>i" h`T݅BdvՅt]o,o(|@Hm۝ $E,=ROvUmRۭ]R2;%ijT|]cGG+N6oiHnYPV<ȹ=q)VA;%tP+ض.ߢ.!0$e`{b)[i #]:BH -8vFR'#;O5c }/Ŀﻤk}s2ZALLc _xg1X}E;շ 33isU VV4DSNܧZ}q lvE:[bRSKM#.e>5^g*exx ^9^\q;n0lX?#'.Tί LOO,u(LLaAgtX}{sʅ\r 3 (m"\3r=v7eAC_l)cơR|ŇkoY MK } ( gȉ [ߴzmr!0d՟ YDڻzFb袣NM%Cϋ\ep}vxT{?3tFG;=h+A%2K=9migYN#NxI@{ѻ-5;,CP)ώígϾxwx#ŅKJ@Y@wP? _({}%g='t!$@(rߕv:A׌]j%*S &CDº"X#җ1y]5~3Tߗ9vmkѻ/&f'!# ~fH?஢l,x0t`q[V2gmuAuUͽᦪ/z&':Yeo|-h?tɯf/wR_ ; jzl}SB>حOL E3#yz~lo+7f;cM }q4 ;}J ] NwU aJЉ=~g!2[Y-W)| -~yV>xv_|EEIP[Y픉J5sH8FJ@LwY.'`$z Fsc=6[DB8¤2^p h=a/HrR:~~f:Tb# '\zԶhXI￞Y]ސw;V!a,Lz($D3˃,lG: 矏Ѓ'/ڶr>tz஄zfiuѡbb<&Ikb1w$| 㡛ri\3.ivK3IB}RYPgjHw-#eJщ=~G r[-.)|+^yz$Ԗ Bne3< HP?l]>bh<` / $ Vv(Z*W@۠r Ń~~?KG?ȸ,nKwNgڷ.Ajj3$ЇtVաk}}Q r#&sIƌKӲs5r$y%ۏ 3G>h·{=@99g[yإHrH~ȎE|޵.ı/*=r*H6CU8.N"f^_茬G h[ر|ڐ䨡SYu={kⷤⵡDO" _I~kV^K|;J -Ϩ΅jf/RX(ۭ|vgDx 8gܗ u_ ƒ'o@37 &rEJ E{V7C/7፟q>>?\x3 X)"~3In>c6ʟ[~M!^-f"~.Qg<]"νk13ʞ/)K•ĿTҟJk"u)kp='B^59NgDr yIJ#]t=slPkIFBb=V|jtR Iƽ33E5M_ ~t]7O/0' ܐ%S4WoW>0A~~I~KzTR-3rȷx> @P$$W%1Yr!k:A#g0HS? RPep2=#ٿf ѡN2Ԏ|t zVB~V[Ϩ6fۑg7;>WUjYX\᜽l]ge_hs~&UVrH!t G?Kj"d^_޴7j\~м~$2ٛ/~gVn@/ݕ<3+H{RdJ h  B/)5Ki=DoyryK,y70v`<^cr#f()ݟE%}1??(Oh?r\Ӑ@H3hSh/lWoTݮb 48M^Eg;HkE燄Do-Q, )-,NT4񔵲>B=v! w:w5P@c忬YOz fg߲NK1V࿫pL3KSsgJ\[_qg17MKc^72+jߢ;"i:ZT?z $eeI+_/U&  .R:9Iϴhz۬ati}V)Z)ڸhM(IkI_0z*^\=sI1MyoDfv.}MaSZmL1QCj$lG[#C.|jGFD<BSxd̈!JZ۱߱(. p_%Bmcx` HP?SK/۟7 {A$~0zh;E@L@n#$@H $@H $@H >Z\@H $@H $@H $"Ev|;$@H $@H $@H "?$@H $@H $@H &D_ $@H $@H $@~p & $@H $@H $@H QdCH $@H $@H $ C H $@H $@H $l8@E@H $@H $@H $G`@H $@H $@H $"Ev|;$@H $@H $@H "?$@H $@H $@H &D_ $@H $@H $@~p & $@H $@H $@H h%g4,c2-g$\{`]ʀ(-;Z??eǞr0T0L# 4~ )cWK7JiT&[惯~'>cfP-u ']=ᚘVek++сRBmg xw03L}Fm6m,,?er"1&t>k} 6+IVKc֊ݦBmg "오" ?]?%!#hzW *8j~mٓV׭iGd_w)j8H% g$keskU>Å(;~ ӕ_u#&? rL$ hy y Wy ЎǓ1\_|LFH qT4{8wnSUǧI*crK܇=2 eO$]Gy$zm6{ar6D. y׶C.Io_YQ}ߖ2;Y_rhP;kM_p<{}%rl{eVc~i!O&wam L ߟk̘M98_AvF-O]Od=s59n"^8$r)E#Rw)L$~Jq e'`!/>;?x/_7j Lގ=ֿm҂+NqfK/{72XO~aM]e7\xS(gq=N-)h.X\tXOH\\J?Aڑ`x2KKҞOtGxƒH $ Ad"ϭgJJpTYlV׎_hs𧟚;~^LH`j_1U'W*g&x?#Nz#G/|1'L GM~jYL?TV)S_&yqafb4b~VUy>~7$9 *K{j:a'ב;od0(c~KNT߻c+;K [RVf&:8 VaS [* 2!kvƤ )7>{}읫Z|˾u dқ*>l,,zB&KG"J,TccӢ4oR@E1 @ ?Oc();:8dji}o7Kg<0Csd]f]Kf׼*^v AEH{/Iv;YQi~?~enЯc(j)^_=T?yyVl<e AQHy>|H $ 3ֿ5ĩjFH꯲Tؖ<%t>}XŕaZeHs.W)YƊ9N8SfRJzJO{[?wT|(_wkOW dWpڸpu};= >fH=Enf'u{n{sӢf Oa =sA;<[Ň[̀LݡکK_ $4ݩо!XWTq 8;9IP ? e@1Qyϫ\œs׃lz=0vU:(JMe#ۃ| #NXv"]#yi|gWxIngp"u/Tη޳_bxn0D`}9[U9tpcΓAx]7eVe$wpI&Q9ԃշg0![%2 q3P3C4gRaߕZp=c2^yYSLh;SrF}fpԶ58D{R_`>g0QJ,8ϲroArڬ$ רb-IH ["b\OdN%n;Ґf+p 389 =J> Mn=_8y ҎD^_qG~I e #@, $!:X} U~] ]z8UKb؇*;/S_ TοޒX2sADsV:~$~ S!Í*%ݶ;!L< \B-Mi\x Jy>x  @H ! r]Gkq7f'.-?r%FPď%)1*϶w .it[R40`f\QfF ;Bou }s Ecv$žE~ |B8H!"$@ ~ؔ5IY>չ[ۄ R6G #f13KW WJO>f=hK5y) r.O`]ӆ|&ʾA<@KԌy T);aޡpkܐU|{(3]6[ZkQUIs NcG`k<ƘI>zww,ID\( UIyX~@U W "MjyB b17AJDE1VJbBB6!l;gfvgNˉܙ;s{g޹.S>Κ1zH}gR\svD%xs~}sW%l9^G=8';gud{þ1h141{{brg)N#';#{^DO ӥ0=`9sҝӒ>Hh_4;`g4k>G耽|sO:2bBV,f *kHw}Luգn?9hײC9$@w%aiCs,fE {H͉xd+^FP9%v-goS=1s0]2-WieC2?lH$5閘fxƴ0Rvȣӟ &E =zӉBvʒYLD2:HmPYy3-] kH:1Y"Ħ☁BÈ}۹_a)q$ƶ8E%^w=VNbAy.0_.]VT0+*$#Ԝ:f썯3Ԓa=Oy򁩈%pdݫ5헠I)Iߙ [W$6AA AR˜'_}Ȭd;i  wə'$htFIQcѹ hϧP}1sO{Eec ݈uAM H$Z(A_{'eЌ^zޭrݙ|ۖm!y 0[7kE8{@{F oo)+&ǿbdROpߵK9^;쁑-{ T1]Q,3{9h$\lZ^[_oXv RMt#Oi3f!uSs\i5->_\;w{Jq$;d_Gҟޔ+;wv;mcpytvOLĎn*x8o"Ӓ~{m3{]ekϠe u]jg]k@2J: T}ؾVRN'-h*0ÓU잃؛ܷ,< (G3*Ϟnz]bgw>G%P;.A'Q?@rH nN@D-cIIZfj麑z^7w<ؿ;k92VCyS}OG;Ѭ3,ۭ}@X,3a]kky$5;ƒ tɭ GͻTm$ &j:?3ofL"0Ot)DTa,Iޱ`:xK/ya\{sel2ẁ̫rv:l _Aŕ򢥶熿Qhl|nȽSTӍtIdkf7JCCe$ -[mU6[؆jcGᾁ3seR*JlvBl2.3>sCeNI%1?Lu OB_=\JL@TTNI]EXJCt֣~dDZ!$# "|a{dK^9[tjnm8P9/x !`%oMc.k"@IDATj?g!.Jϧ>":auʚ$g?ֵЯ{& J%5jfq0[!Nh#@~S{tpfY:)\x&컃k:ȡ=~uv&V0mXK*! wα{&yQtom r]og)ˋE+f騦3,57/^gL\홄  @ W}ؔ=3˯60ɇy@~4]\]3v MVӇWWEޗ ߰oҍRl2) #cYdT7}L QWOKa 7%pAQ$Z)mޘcSp֓޳qaBdOvƣB(?.}S'j; e}?i&"4 Lk1{g5ݾj=<+#5moDG_ˋ'&4E_(mUW)2[]^34D11=U>ZUSu~~}110Ng؛ bMi{jSN@@a|φZ7NwCBXBysuk"葳},г L$>giyNKl_"[Dc 2ʌf?j5%)ʖtK"=1nwT2S.;E<@p22瞋u N(OȮR>xUHYczӥ\vn F8al$@DL̹5:/"wcGR@:KN3{9T)NL Bf-fbR>8D=p/#T+Sn`'c)L8iҘ i+cͬVOlegw8J3=sSl7sU^Zv[tBMi˧j5u'Zp.-紜t.+BHBy ʾPGhPGʉLZb:J#^5F??T-;f?>g4u׈BھH"SAF ҹ>oǹ߱Bqtex@'M풷'еoFx5b5"\CObfLb1O8xXԝl#rQsT+r]ǝtvW1c#d̐ty+Yi;k.o]cx.?2r|G.!]* 5{eև1\$owӭo}e3dDzxv,~s 1i=Ly^tPrԇM>\:;Vק//.kI1lgZe-L#+'E790ʦS(W`K))%zg@vu4ՀY*mX-q'g2 k O9IZG 'Uu]W祮$F7>Gb.qi疣 ];~Oc $ʼr^z ;5"٥Z+6{QʟT?k Ȭ~%^^ BN "D]F>/%atbNKeV=.#)}͕#%o͵C7ι43@v>2a S~ؓm8ܗh߻ B$2>~%IO!|}ƑLzWtBtd>ñLQ)ZKﶼqt:% qD93K*MՙMQ}]zW G /qw_>N^?E%>}Lj_x˒[o3 #*,<%)#9"ʉ4/cwt?IX!9Xv.jx~y'x`$@@Q炙nQhUNs{[svD,}K:.C럀~t毑;[.h*&\K[R,Z|n\gKG҂y2ަZ5}@"ľ~!2ψB3>Buy Fsv -,䓽vJ8^Sďgڑy2\6--*N!&^ac !ORGRK.Ωs| qW6;ud=:!AO0b ~񽥕M8]N{eRbKk\P=R>Hi_tv 򃌙@~}c/vyZ:KN{W&^lSSgy΃d;E?p{?!ŁʹCk>& i (>%hD @H@慡Bxd^âZ*v@UljHnnPrv-Rڪ5'&7]2myh1bSǍ;0NvKw-_k-&/i6%j;ƕ6|roޏg K1Po*qL3K]O.Ɠ RX2!r3_b4iq4,%*8=|ǁO۵'7嚕27La{M?}H۰k I`0.9$Z8 mc-_>wMXYiX#weNu.!x)/tߡ#oKf}eI[e5q&4F 4{*#g5pz$;:0 d@;X}.z1~kzyNUŜtMW6#/Hx 39GB: *'$9>&w / .ʵKR'}(@~A'5 $+'M}CMXVQ[-g?;Ҷc`3g*xU VZTķE9ސAƍi@RXLt&*\}X5 )-v{Gؚ.k?vɶW} !3Gu! ,&7[j{: ;//P(0!5&|P0NT:Sv&8A,`i_Ne,&(ъn&P9hrh'7yR+VOsQMs*Eq7mDjɡ 3*q?PvB!z ʡwy6ioKfv+m -E<%Cv6&#l.[9{P'P>nb ڞA:[ [ݜP/g0.)9PFq.)?(`E]Iy\.v!}:խogbCHf(:+E}~k ҫ#"ѿ#߸ 0@8@'~8S]'>+ݸ߇+ NiU*U!OnpB]Gf7IfӨmVsjXl]җ48v;e+gB;lhTqwmϠr-GZ C I59*`\EN+P t!3r6>s-}b6Zkە!Cz喕L>x^&htbӀu!.W ݠWij9JXz;7,6Α |ګ8}@Za-YUPX$uz^~*N*AnQmϠr\6@„#h B.{ρ:A?(`%@w |-/O9r+̨z̕V pF}Ik޿P9ts |\`}Y[UYtp}?.rA̪7x1ṻ>22ʦ37 v1=qp,Z`: 9DIɳCL}{D1ͷXwHvX7-A 38De!:ŞSNY?Y*r!DgJ@j\ fx#IM?`$7|F MZor@AN<?H3ZQ tE TNyycv*u [9.xe˟]L>nbmϠrF \`K3]a9P^bqG%XıH]M:zf[+oU>5"9 ^l'J"ʉPbQ?Gu|Pu5{='L}[k5)\\ d ˪~˿AFH$7@vV_&h:䓽yM&1@)\Ϯ䩩e)l4-an,P~*|f6CO/wK)6vbH7s}W'vۇ4.2_ykfu2D{ρ@+w*OC9vP8y *a OLoذ(Ow,y+?(`@'H   *#WO[N Ydv)?oO*ǟb_ y,P~.ֻB[SΚuz_rJ}//ݳ)j۶m) ls|A_NKw'0q6-~1oyi@(RE"iD c1_gMw-ZorCAN<~Vol]7XK?Tځ;C_}o_ !v])\r&]l9'@w; *GK3y@s< ;o@q?e[ N^_8?'u'[fom.TNHGݚ)>r)+?ug#i^ȋy>sǂm|~Atkz}^)I1z[t g:7QMvt` ЯejqCfTutm}5f[ʵ/*Kw׎D1w/x+/[B5-GÂdL`-D%ٛݫ۞)p% ߩ`{7#{^DO ӥ0]=XHP9P}P94O$[}9 ,Ow|beՑ*x}~A"T_ʘ?ٳǞ]g!lN͑uo '@+ Tq.#h{IGK38~$'wX&|HAr>%$,”44vh{Cl"Ut/#9l*dޮE]m'fKF!5|*?,qоtH畍U\{ O.e'Iy%&B1-gth;rr&nѡTMC%˧ E=9 T C"(;m"鸡>AD" d%[`Hf:'ZQ9វ{bT~--eK19#zꔇ\iaGenpԈ@I:-LckctpHTi'VZi:s-3W%Ν^51Jt4serxan-7,e;[\F& VK\{=4$&'_\Y-*<VI?1e;B(,NKPU!I4v>=^l˘+n|P9( >}W v?;r>jn3:v@\)Aho,rocT=o颺w)]#X{%wIQw `١2Do-lCR1p噹D\YoI@ <ʉ4*T,*'96mTS5z./Vh{B+;QMgYj>n^9vSqϘ3p{Ưg;/WmWְCtg?cvfZcqƜ1ȏ ӺD'B堙ʡwy:[}h5}ȦUGe7w6ĤCf9'Ю/ȤPX9h9 ]R>K_8Tu"k,.؞8(^Sw/qH 8Բ-[sq*¬z{P90L,SxtY_ǥ/uDM|=DDݱ>i6fאּWM~3* p\ƜAtո8}boAWYVlә~s]Li mOm| gDgK\#ФTe>x^k WNn>J!=:f9wHN@a{6 *+x;y7`Md=rOޑEr+j岛[##'Ϭի7 KYF67\!I=4Ms8ۺ(6BلʡJd;F.s~蹿g9yQSlID/vGcSs*y mNȬI1*&׻HӀ,w9?߉ ՇrPA_gP9yǐh_By9'K{3GӇ/@*@ t "U7ڙskt<^DP90't򏷗gKrRh͜[8+Ť}pj{_F2DW,4XdkWKE'ǘ (?Nj4fFSB؇F3+ՓsN 2G:6.6uvwi˧j5u'Z^Zõ~ x|Zew#4v{V#DB{&Dmv^Q>{$B?Pw-;f>' ?hABAN;<zAF o@bzo/o||J 2B_rv7`5Nh4JrP?P9h{*l_+Ik u9y)cjHKtDL#5t$fƄg1/&*LxoGhIɖ?X._J[>SN:懃ja.d̐ty+Yi;k.1v~+Ӝ!إӓųcR[JP<I46߬KHb`=BQà"QNJrM&g/h(x&CD" B8\ޗC^oFu mmfj@fGx~4pdSc钂>@\J_x65Qr 磻7 {;~amC] h{)9Y| wb%vI,k% ZvrcR$Akf/J@SOٛ9[% D/}Κ"B4eDRA'@\V=.#)}v{v_[F:2J*Wq) ` p~2~ԙ]bW0)H(VitKrLzW$)@8,P^8:q#`^A|9Pe LA0 DfffO'熬\4W;hGWL2T! H@aɹ_ڲG7<`Ѫctj=OǓ|` &!=O&THdԟw/brxMs1RkGNe<l":ڴn1% yZˋ^4jxguM[64BO[%k6fjO@&B| ||nl]K.Ωs| qW6;ud=:('_|oieI"w%xeRbNԻ9Brh']izk ;XQKg;'>z+`SSgy8ϡ/P9N#BU9W  7  J3N#(W<3E֞Shy|l_&,}ʓRʙ8hB] x ^!t`^aQmaV;PЪl i6^o$DHAٵRHi^lt÷ghM7~\8 Ip.| TNx~@徤HBW忕{?A7\o&~Sc\X b9Kl4 B2Haʄ/|rJѤ%6rҰ8.c [KguB~:C|guplj̒?U%&|{8yZhf m3S^Swz&6lqC5 .|k IڞCòmsF͂9/'l 0HyY~[23+KR1@jΆi)Lop̼F;'= D; <^xD7[Iw|3fk-& ӋH^_2{9 ˪8s!g+6TTΥYTTNuP}P9fQGmZ):s,Otq XC_rELPZk^Sfb:(|>Kz>Dzn&? 7JƘ2Oy., ۳=P9w3EwAc_ܾ0cyq8IĿ;u /g]?(`s8@QϘHUu4DKWv~7FHSOVH"F0oFuEk4_2 P9ă 8Umd}[$c\12ATOVH1f C?J+v|H2tH%g)1Iwe2cəlQkbY>Vi'NFkuwtb"(舰/KC~pJˋk!㙽6ב>z̛516gf/(?p@Ts^ȝOY3%iGMI9 g޺|-PrPP}P90O6#ř5oMo<]*bNSg^33ERO?LR^sOfnW^~|GqŚ&K|ާp9jhy) {OuՇrB3k4Ȓ[z9BPtRFr_Rx0$(*CH $@H $@H $! `$@H $@H $@H %D!X@H $@H $@H $ GH $@H $@H $@u,f $@H $@H $@|p #$@H $@H $@H D Q:@H $@H $@H >8@GÑ@H $@H $@H $p(DBH $@H $@H $ #H $@H $@H $Q8@l!$@H $@H $@Hp$@H $@H $@H ( Qb@H $@H $@H $G`8@H $@H $@H $B@| $CH&eɂY5#n'6;yǾ20T31 D@AǨ~>=#""4*RYӑWַ~'>}zgS՝..DR;J^7ix0l(o=֢ YNVm ~c|4Vx_o}-ˍ<`z䐁'5$9Zkm-̟t%!뻞dd=4:i./&:*2,I*/@H~]Ӡrw TW!Jbe-S͞+mjߘH :NwOTjr,VěI1g ,=yhg4<@brbsć9#$NkNط.*-9s#~(t#N)aڐAj}"u$T4_мZhbroubɮҿ$ܢs^3oOy* q?=m=ٓ;%&BzeuՃ[J8JOf}ȑ )4CUכּ4C1GRȓ{7F뙦b9+6u&#z5ܣ^j'tH:8fh@3Ne}}_͡ZI(rCs{ MK[oR6gw5/2/sf,S Hnů5i^5"eN@IDAT[xj3b+[/|d|邦rҏLʘ6{M}uIF? 븻xt1I;73돩QN5X/B䚄' G}rYJ O@vu h-5_6ޥ3tMĄĈx{ϫ~5t O䝼)Mɗ~`qY{5!Wݖ6I7;9INL9T_5B**:=T?h` L`C*+*'>G|>C_.Gb2pa>1'F(׀]~~rqRocyoۭ*ڟfӨmVg:&YMFyO 0>vgl|!|[Ĩmj7+C$V-+'W |\C;ť vI?=̺Ag+;tQpd}%Q?*?1~m}bb6U{שU k̪@oc*cAƧM6i%3G jf؅cA^Ŝ q3//_T57{c 6$F%UpT=0ݞ#"-yeI|P<}kW4#x ` K݅b sWys[OqyBGZ&VDZ^oJWk)xw7%΄Lo \_ZK3DT͛_cDEܞLq sS-*'8P}P97բ ͦe;<Ϥ=_{e??@r\ (t>,׮g󯯬;TP?% !`' 2>fpʹ33WZť9K?\'9yBӹvΡ+'C oMXJO,:8^>[=a*#5ރCLxLM;aٯmwLO&!kF3Z` 9iэY!&ž=~g{,Y{`;c`Zfp_:Cr\+`&'S}P9XfQJq1lN?&5!ސ&U( xKF[ȧ!tӜQѧ!I*s =aـ3<\qg#T]?xhfO<_wN ?@r܌ (t,^iA9$D|Wپ=vyOsvNBQOvVe:ҵ8ҏ UܖDUi 7ypt_@~֚c *Z#3qS! dY5o|P|\`;o$NƩ˰.gG'#k0DP=a4uߴ=ES=% -wNc_ezzyЏy˵#|@}|K8 `3z{'G}P9x+8'᭞U6M= #9"G21ƾX""h9x披>f)G"ڳ@ẏ7* P}P93+ϯAGPtπ(?ݳ!PVT\ߏBJO/"CtKBԡ##! r_?mE=fEIaj&O~ P9CJ Gyt̔fw uņ~zI M옙6wZl">>}+.,n1*IvO!Ro+'3_^!$0l:>%$TʆcTx2n}Ytϑ-fltg: =s/vϦm۶l+<-[(ݝٴpۦ݅wC-U43A>힯 cѵ{P<[eG}fjѯ޿K k)[i};ˈiul (TTοE1rVUZ!j(?@9//}K~~P:j'(Fz{vw*@-TK'g^6N"$e18i5=z1S>}~%!(?}f#i^ȋy;>:ϱi$_P3hu7 )e/O' rssdgM v׀]F~M( W~p0{cԦm+l1آX0~wإ;kGY;Wr'} k?#I 3:2aA]OYнZ7h b 4;[Zc+I^{cKzӗPoeOoꯎd㴒ra)?a&,w^;!~.(Ϗ9tYT}<_C_AA(t2N\PZs^N={SK?Ild1)aJ&vh{Cl"Ut/#(9l*dޮE]m'ff:ZP@ieC2?l*PQNtKLN350<{kL=!db{<ܠ.TN1=ٞ{4 #ty3%;S{늤$[eyZF\e9ď JOF}MWT^դXķgr߽2y a>x }H3'}Leꑷz.D}?@m_EA;~nw wE>gB"%2ls?L:-@e(Dm"鸡Ϗ>k欚P wN6?c-C2g'hTil?p,v|:Ti+WڢuGD˙w^ uYf7FrU./ 嵵= HMVI{S/+\+q;M,d$wJ&# Ϳ,NKUpޤa.Tf%JOOϼg7FO77XeJ#,1zYXTBΟltoBdb'=Sʳ_:Gb<σ $GbKW]ߡr<_C{ھ~nw wE{Y}HʿMkVLޚ8\.&ݜB/:k=~(-/ EJ[JŴYu#nyw<7s_ d&&0ԟiwYigX[}Xfλ4%VK6%&//L5BS4@2lμ)3vC"x>pz.Ut硖\ */.EKm fݐ{{[ꩦ3+tI.Tf%J!gzIZڪm* Iď7}g65?#Pyh"m@m DJB*7ֵ[5R2pg2ߡyʠrh'7z ;G>D]5 B"4%{QAu#>N_ lx̻3sOĴ3_?ĵ_TI/ۆ@#Х{a{dK^9[tjnm8;&t\F4vJQigXDg8 ;áS?nc k}j-ϥU 6RAt8'&_:Lfi:~swj/..o4HS~āgJXGI&|pK|Bzts(;]:)FP+cr'+>tDBysuk"葳},< 탘@"ճzi\\moRm۝t VuC.0M6SIAm>O-"I36&qW:Ӑ9>/@AS_PP9_ekP}P9_&#_YN}щZ{ zurr\~? u?(?YjmNȬ&I1*n,M@ .A䎫Μ[#C$;H'x{I\ |fo!GJ6¶No'}I>\L;Yhne$3Ueb9!ĥbcʏ&I6!meCɁ}XByg*CC޳G)'v*'V>{$B?PK-;=ZUo UZ>ZGϚt.=vi0'TNyP}P9bP}P99+ϩ8D=~|-" u99%~~P:^S@ҜQi|)!`&-f1#5**|1/\0;+loGhIɖ?w{^[~ŞwY5?\Usnj cy1Czug 3~e\iΐQReK1)Ig-%m& &PFNUo%$+}{0f@r*{6袡g#"WvE_+`;WN闟k(B|X8/0}iBcf>_枢c}޵]`"$qN M&~1fuJXJr߱_ӳڵ+y ĞukřFDPKɤ7;|~/[\Ov">G ./?p,3STcqhw]G ZoPHi&/}=s;B$W؞(993h'Th&h'7qp[uU? x_!hB]?X#~u?(?`Nx{M #J(ddu$$UMRڭ<$ Yi7~xKwю%nd櫎P9<' _"k$Ζ <i^TXʂEэ KjRy2ަZ5}@"ľϸM+&1iF3z1tMUUGs|HAKXh./zvҨ5mMn57d'{R;;6%?!CrݡD>!z// 'd̀IMݩ#WI>I//yI"w%xeRb˵&z t+q?xĵ^op=ș?r+ˆ@'kCY*Gz"uO)dAo2§,d#!UM:v0װV(DhzhUt47"a$Rݠ~sZf)Uk6NOLlod3VQcŦQ?.v`$8Sv[*']XPq źrY)-I3Ifo) ^3s>3iӲӃ *M7>~},3ٻ3<뺜fzåS-SIaoIgSTg]gG[Y  IE_?t'RIp&=g0a㾧z|+"@iw)vTH:{)Nh_)mJw\]:q^>Zv,Rϩ~J-5_ٞQ&_8Kffz.mѿޝ_5+@Qpn+u4LbHecMX v]{mNMg?)Z~~aqΩp8S2NjʚF`yTgx'TE t{܏juduNة[mo=9ź۟0&|g7eX{է+POJ#qm񣬀3"FMI`wjq3ݣB:M.k<-k݃ L/}ms^njՒq'hR2:~u L%P+H w%U^RKSm_)XM52df榟[@vByy!݈ʪVׯO|}'$1\Ng~ޥq'ϟTTPQuSOz(-B|MYWl"YLN.G~?C;:)ˈCf?#ݯY7!Qru4ϡZe[Y[HL8ˈDѳtWkĨ#"4jcS{ +w8 9[KYOPt,wQn͉¹7ARžV4{:}&!$DGkzTu?/J<9Hxήj>UAےBO}QcȉkUfaC܉^0OXv6Bxw$@Rp B@?=3~XhjTZ#ڗzI ~=]4h/jbvn-n- ;'U.RQu\xj茇vPkMi}m v|{GmobỎlX`_ņDEDۚǾeX[,|P@0Q0B@@@@@@@@@@@@ mVUB @@@@@@@@@@@@<':           AHDAبx""Otp @@@@@@@@@@@QQ%DD! aJ            <5B FE@@@@@@@@@@@@ yk            0@*' D@@@@@@@@@@@@  `(UOB<]tMg.k+))2wh\; ,_Lhۛ/v4brϖ[̂bN0$~o1Կ~`XrޡaaaU{=[j~tXr7z rtO/W:HUxG[ߞah錥,T=0ڰT]Q޶mb"xp$BH7LVRw}[ w?le/ft]\d$MsE;nݗ}ץ'lO>3ux{509mo2~^8R-UT{T#^.PQu^LGՉ:U\E;>S *@t2A{NX\@'*PA>EH2dZ:,D 7DB/s ޯ+gYuι ~:M#J[$>Ⱦݲ&{2=y#ù UZrn;;up$Ufiz^Z ۙ?g/l^uw cgJTO_lp0/37TժSIZb f6ә7=w'|8ﶳu .Rpy|Q^0/p?xftaf7?eݷ @yIT'_<]ڣ<UOר:OxFGy@@)O=ĿPbʓbPAN@@'Xf7gy^xrlZ^bu[MLJE6,,eeߢ5qml)niꗪS%FW-oat]߭챹c[k Ïv\(yƭn0mN&YRwtcび6Cn?%e<>Q, i:=[1c7o̯~y'tU(VMs;Ncl_gXrXxs7|cAk"51Wy[ы}vW%t9&,e~oNk(x>IrqQ 5 [Z!MK:կyWA}x/.L7[?ySղ|{T? sNC {;e6>ĬQ vd4ͮ7K78p/JDIR-U'ꈗ T{TwD/SQux@GyqG2U'.T{Љ~!E''ڃ.۝/Ё@=(wkF/ZmVL5o7_HKj/71G2*}nhAp-O ]Z,Ub+T2?;̎U0JcuEL1&VWMuty{wO8ZqYD 6k2U eUڜ|/v fx:/e̷\"VNGTJ܌3qCӯWO>/ƙ-g?I/jVevO>w=J!^U?HgDzw~}>oULqh?a~K {=jp=N T{TwsPQu^ %ڃN{) }ކ} |g4u%^ÿ$sfqϝ.vt5#{Yb?t?87ݍRf1<@~/z+d?m~Bgo jQQuڣQQuڣqGIJtPyRl=ݩ$DU{̼̺?N z8Iȕ񑖇*35(a"ȲvO1(c}zn?i&M o>P3XW= MJt׳_Yj[N 6¸H%icWGJ~r*y7#]mYSS}˛S4ucܬ2"WWZcfK|c/g(k,kaܖKBff_(5QW}=Hi:92WsHr|es֩nqIc,/<'eT{@t\]*Yy ]ٿ}mLcyEfFmqцA3%YfOqR*ϥjfjbI$PF:Zz԰tJQsu"ցD$F[fW5`SsVϛ&'{ xT{Ts*ٖ/jS fuV=Ti8l΋>P?%'{&zӦM6hW.z&dbORA?@u¦[T{T|Ojd@vޏ %Q'ϊ=7tŸv3G[6)q9h\ pc<6jW65نi5e/ZM"̬hcc{;aMuuoGɱ$؜G7;l-߼w=N^ H!^@O>]\(W"KdS6rkI\3V6ϰ)Cv XXx';dzǪ7_)qo(wh?~f~>x ~~J < q_{,W">:{Qv @.qA_C}XToK~@y>8ptsŮ<-*{qÆ+ʸ^T 騳<طp`?Tl}>Wʝ_ԄECo?U{Q +n=ى2{\)⵭'KLzKF>=V@L&78n:\iէj[a49nAycoUߩ:{-?/U/_>1C">ދe,g3ғa#vWMtlg#+cǎ:t6wh_W\KL݆'h?7|~Kչ9 1j[6= T{TDܲQQun j:9Ę$cT{vS:a~ATZ0x[;kszVj1ڛSc{_5wq~e̪w}D]a μQ&uv~[wɵz!1*Si5IEU<$Q=7TTn`NK`ܧ[6Κص'ZSrݎ!qF.JMU ƎI1~Y7o\L"=\l:_bWqK!Z8fWJ,_{ gZk"ԡ ީyi'l}c7=ag{^9*SQux#(/~MWO-.^`KT=hq#k탔v~,cO~43]f3zUÅJXϕ$oʤ y@Gt+;UeXU'ӽT{T]a@7w|RAlPyRڃJ\XG)\RA@ (w ҭkO<0-+[˗MuvJչy-5G{I$?*e0Hrڣx 9^+^dQyʳ  zt#'( I d~43J!tuLZ'F.uWw+D3Wuk1/?ZŜ-_2~7 =|]J\J #*D7.ِo_&M;o6>}~̣Crul۲y]d Q%r3=n-&I{i;T@cQw=dp,7}zf;ԙ38G:ć͖Q)9R$pٵRuFڣ৕;xR#:~D_-)Z?yV칩2twD) r@$fB5İVb^|苪- <9cp˳96,#ju_Wn*]qrfl0~Zrtі*Sl+?UΊ?=p˸u?ˢlXZonY:;eia+\H?2vlC ]d6>Tdfkծ{e Յaumve:%-ytWq1EV]KQ;UUz-@{ոc3nM֟_&umYϿm1=#_Rv. -Fy._-&y \Vr%@шQQGہf*L ?ѳ#xmg7ΨGKՉx9_m?t6澖q;V^3˘rX= {.ńq|*nbVkԖwX΢BU:Q c bE\D eE! .եmHC7UkڗhӖKf[ˌbWƲ^ܝ=UaqDT{T[]@;#KgE =4n{⵶?zrgn.E ?]200nvn6ݩoKܮ#&pyRuڣ;xR#:PuT?]goIyGT.Vʓl '%dKD>̟SHT sO]=nQĐ=+of{EeΦǻwRڏ]¿ى咖s+~Oޟ?+yI^J(ZѤү2YJLdλYΨ-jϴ8>ԉT^Z`̀C[ϿYHpqHKuyk =.Űы 2r[Ogd^¶}Q]˟2ջ ]WUoz:H:n?=^_-tx:6ȭ`/_U Ljs7%Lifj>uԾ'E/əl#dKT: . fڣHGIpI0 U'A@aJgw=O3uprqv\A깿I @@R"THw(ޞ^62P DFY FpѺ1z^ 5RH ,t~:in0Ǥo~{X!]/aNz}.w2n!) 'Bƛ@IDAT0eA>o}Qc ǭμ:D`Ri[b̎U0:^tM7hv.__->=pݔ|-!#nDzL#{wgˉe)[>]ˉZ#5wcyI9#;./wZ?re%5-: ٮSaYyVxXJ0L5bř_73skt}l3.˂:q@[4NW:ވ_ڣ=|jU=N\(~{g먑@`ʓs0-d9  d1^gYʛcAs܃|}_T/N˃'!l'E/,.sڏ4@7 j3%Tƥ#T9ΪRlu:cَM?jAMAT .Kq?ة[e1z#g~\cRK1m}~RB@W}i/ ٔOf{vn NoyŒ߻ eu`Gwh r 6%\5K]߀*?_RYlIYB*뱹ϷVy=+Rz3uY֡(ΡYT{T,2SQueRQusLG9!toS v50\:]SyRu.%۞dǐ@z"_ֳtWkĨ#"4jcS{ +w8 9[KYOPt,wQn͉¹Ŷu='Q?=3~XhjTZ#ڗ*pSd6D}ĐpMP?X`L׶[5C 3ZMNt-}ªg--4|샶%6w=9:z)TxϜȆ UlHTDx]ťku~Ky5=Γ/QQu|tj䋧kT{T'_p "K|ZҾڃFPw*O:ݽ <:w۞4 @zz            p۴X@@@@@@@@@@@@}Q;p#"7$H&n_@@@@@@@@@@@` @@@@@@@@@@@@ `(7 rCn E@@@@@@@@@@@@ܐ @@@@@@@@@@@}Q;p#"7$H&n_@@@@@@@@@@@` @@@@@@@@@@@@ ȭ>3%H52,SǗTJJԏw6Cic~G[ߞAf}%DhwkL_8ӱLql~jFƧ##9+:h`O$ڽIjꨕڣ0T x6OaァJ{pjIMڃ@@dPqy+`E{%o]K΍~gP>}Uߑԗ2dZ:,D(cg>gjT}V `&#Ɂvб?lZyWjrC#vGV2؆?a 95{e-Ω3=={aDY?Szb5lOވpp?#_9fiz3a;Ǟ]#.ݗN3Y}r,c1ۓ?_OR?);әϕNxq6ds㴇=u[Z!=~ OȠrꄽJGyH8U'E@G3uHrڃ@@zfK]N1kT&`4F?9ͮmNѳuF<96N궚m>)D˪7&vz_CsϝMkUبQW_ߺ]掊m5|w?qԷ9dmJvKTэ쥱[S_7ڹ- [qw5MRuĨаʼ=; ,rߩ~+j2xܺu :HMhUDs{uiW3kkKD?OtP~zűcoX_O4i/^=^QZ {wU¹;ޗN3_!ncȯjSy$6fr?Wx!^l8'8Y{zޭ֩VjsS82\:a/RQu=AG {}3PQ v5\3l3BN;꾂RWBǍ_ڬVjL~oW^n?bJV܌K3q:ӯWO>wƙ-S`ARu6t@ϵ"q3vq39*gv,txe>kc@j 6k2U eU,v fx:Je:mzy+rxغ"kKaY%].DK:8MͿNm5Kvř/xxyܠ(cؿ̬go`=w|2F\ߗ^w4R-Uh[1U'7ǼT{Tm9T{TRQuSQur|sKG9ږsLGM3PQ v5\٤WHЂobEe '^X:ll .嶥s'5+<#{\נ'. fw+gC|g4gpd/_rɪ⧞7"_j!>}ކ} |K_^1{*g!Tk#JF[Mn?Sjnprm:୮r%zۘK3CwYG4gfws-^NvĚJo:9ڣꜭK?ڣ{朓js.jsN=ٺ3=Ng9:go?`Q#ʅj:*=kfzf]Pt 9?jՖ4߷:H7=ii9.wZ;x]?^$2>2آ21SW# dSZ|odG?W 0v~XD鑡 *xB6XW MJ̴z+>Q\?uCʖ.vwt\ʱ;L777/ϮsE*J[j|3L ?Gu<\>ˇR`4z{MTFT{T‘AB {=jp=N؋Kbg먑HBP : d,cKL[>pG] /w:U#>96/Ø7ZOQ۸e1}64,ٲ[O_5|I32iX(F૙F{4h' S4];d8ym)YRߒ׏ ;$ Ϛc%whK|WTQX :r_z%Onkq[P>θJVL)]WDF!~6"N޿իWI^7^1ϳ|m9ZkZhh/Rur|sKG9ږsLG1/Uh[1U'7ǼT{Tm9T{TRQuqo?`Qcʅj:[aDoڴ)rSEτLc"d:WmFJh4? V(/v68?(gO[1$؋xw2/9.!HR}U9϶|Q˸{q8O-HruFt*i8l΋ ݳ iˠhwF[ɸ=NkNY:'2N:9eڣ꜌8ڣd攕js2.j"Ygأ:jQPA  >=@# B;UwCw?+(@Qtjmrfg Ϙ}aZ ?-Rƽ4XZժ]- /~ܡ%szO޻wo§>K\0udR2ɭ`/#~fF-)stglRy)u6[`.זeB{ d?X?;(,J~l-|wUyq(B/Iiwqs`L8{~O/SIlRvD5h}2uLCٔ?-$7a;@2c5lZ%'*/AӆIE MTiYT{T,2SQueRQusLG9uHGr!3U`Z!U'92@G3uԐrڃ@@| s@]cG:th_j~I^;1(x'p0dod',@|=hpTm?Y̜$S)RI2?O'n?yı#vpf7V>АEW?K?^h)w>qJm yT-F {=yZT ?ly܏G^l}G::TmWj?T躆"ޝ \*%IN3 q1ܤOïakؼ5_1C">ދe,g3ғvW^sis{-LT5:)=<#ʅ{*U#T{Tݗo?`Q#ʅj: Xem.כ.F*0z̬_8\KN_OWqKy(/RkqB]\ μQ5uv~q '#\Մc7jUsG~)hnG>U\QnytSU#:q@DR&_~ů8HFBbx\EMyWI}ZC5ܸrsZJDt>:qvּF%gK۝g`kgLVhw[u L#[/wY^՛ޯ~[\6z{FGJxn 9/hw/m!vFu j^Wv=˰=N{]٩.2:ueڣ <ڣdsv(~pT.T{ЁD2gZ^^xG7|m1cZ62{~Hniu]y?#MM5BaU~{k64#%~{?oex*tZ8sJ~Pv Byir̬+9sm 7w(.OR 8_~UV1fM8p : nUӏdՏ3_dbTiSiJo,)X;2!Syaا1e+2Mf슛G)On6´I~S6??[o;9fD,Se34);4<ukWݳ4޶??~-NJޏO^Y8eS7 kʟ2c uQ;WN嫉/mwJX=Ǚ`-Q8㖖/gզK\ ѫ'* ]3C5Q+'@( ~hw7~Kչ9 1j[6= T{TDܲQQunHLڣ$喍jss@bU'-Ų@G3urڃ@@| Gi_ o_&e">}~̣Cضe,P]f Q%rp[*d~43Jn0溱Omipf?6E)~]JJJm3_>Q~xO0r]pk">O/~$~[ZƷ^/hYfixo)XN\N'=v_RE8a#ݻ_֙#69i~KFf֟\mWxBЩAJU'' ڣ|FGII(U'䃔4=NOJ񷟁bg먱GB (<@zF $}=sgwGe)/SűF}v`l}-w2FH/>SO2&1$Bjn7g/vAQo,BBEz#r+0P;Z{Ȉ$}188Hb}-Aٷx[V?vV?',x ?STp5vg|s;3/"n \ Q-UGU?瀧pdPPu^xOڣ{$jR$|ڃNKD*O=@@@J@>-^+&uԾ'E/yRn]Ne0Y~,Ṗ4c^4/dp3>&"䅀v`W$0CFvxK]T s#]WO[1$qcl_r4g%O  D V-3 SwhW@G~ѺH"pn/.ίl3j #="u`׹LR55θ㬱[_F|_0G/(n.<#ce|V5,ZvM s8 ŞCv %۽ijڣ *N T{TwsPQu^t_TʞT{ wHIP +$3=#LS/Xq时/%5]r/wN˽efRY4aњ7+Ț.Z7a!,\]`%[^7lO˫7qwivhQ]^H\Hky,72H ?2ؓ]Ͳ1 MOPx짓sLJ X+Nz}ϿmًHEbYLڙ_gL7X&+*rcߵ5]s19E^#;%kMF>ciĥ;lϽو^?NЅ[2eߧUo%$}čXfn4wgyV v ܗ=qK%OoTtM>no\?RܾC{f6_Z(s1@B+t2?ETqXV^sn/ivi`oy:j:)=<#ʅ{*U#T{Tݗo?'t]j$RyRA  ضܭ[ |k9kK,q{V1^gY]>sȘŶ>?)![9/cGߩgڋr#xNk>/偣;榳-?L`_? s;U{q)UB\~{]/n;vj殕4m47@{"b?׏Jֽyc|GwsZPSP)ǒR51|8w#7| S4TJȊOnߡO$K5\w>s^,w}A\M+xJb/x;jFi![Y UPC=Ns:Ӳ:Y9dڣL:ڣd9琙js0-j~"Cϗ<   н uϜȆ UlHTDx]ťV]2t`0tDǁm/޲[Pw=MW_{E_L:"B66ǰxx͉¹:}&!$DGkzTu?/k̤G~"{fQ!!,SG/YeP" qæęqF_UEiQ"qk2U! T{T^:dڣDLGyuH$U'd=ΫC g𷟾سj}/ڃN@T   BHDB! @@@@@@@@@@@@ 0Mx              )E@Q4`H)(@@@@@@@@@@@&(EDJD9             0@ 7A@@@@@@@@@@@@) R$!i(            JR$Q HCMP"r@@@@@@@@@@@@ @`(@ nRBd D%lw:=%U2;/&ꕖa4[εo{KAw1r 6u0ܯwhXXF`8yuϖ׌C3abB4L|×{Z|QtrȡZU_ַ`ۡ.#kVa5XfΛ]׽ݮ8`/׎%5>sySdmXn(ob/Aq: J|꧜1( (yX @QOGOa, l#@@@@ wKh]Y(xXkɹj;rULKeWeLV-O 1Tl=~V؄@v$9:M#JArhdnYXȊRBP"L4磆cݕ9zFӞ.鱃bX5lgسK<MΝ{2=,\gOK>ƵkHO={aDŸ3k'/6_}O Oc3E PVA.֙jsS82\:a/RQu=AG {T$̖n*6vHm|Y2Lh4]1nȹov5m;pev{5ɱi{uUt|XnI1'_V17I| {llҤ\{u\dDԯFֽbE ݕv=6wT|lkC}ю %o׸ݍͩ$k#T*U[nl{_nHGK\ڔ:}%Eګ.:n]q7t&!Rs-ggbC\>)sg(@1J}*ХQ?PuZSQuS{x G U'T=λG9:a/     d 9 Jgʒ^^ 7~jZe1}BZZ|_U{q)U+er3Xf,6?xϬzUO^=DށgV>LAI Lvq=ת,PP樜ٱjz9~r׫&:Qe:h6~pӲ67zEغ"kZ53??EefK⬨'^+v-WJ|/جY6f@Ta*UYRH1Q|_y+ou^C\ 4P'sG+F@[$ Zi=~ OȠrꄽJGyH8U'RA@@@?a ZML6<}P}ކ} ۬w^zrT_J|~QB4E>ovˣZ]=~ OȠrꄽJGyH8U'RA@@@A@a֫طTyٻuyĨaW[V~#,+w[4t h \Cvv?+Iue|Luۡ'? ֕B"3]ٯ,(5rqǒcƉl#CUUoK.u٬qvl/7-N[LR?:KugP]qMO}{Κ'%rߥX7}I2L[=  jsS82\:a/RQu=AG {T u-2n١-kNiv}hlBtWܹzT8v$ۯعR;9yU $f%[bk&?OJ5O~|b 6jl~>F1yS4];d8ym)Yo)cm\ VL_JU/GCPʴU6;3BiX(ff_hC9@\;F޿իWI^7X1ϳ|m(t?1Ay#LA@Q+NGO9)T.TS: ڣꄽ@*_m?O&,Q]:#Jn@nYS~%yUa pwkd2+6#%4KVq~ tJH:(J'-kl;mn=<P<ڐq{\3&+>9۶ e8 Tcu׎4>窜g[eܽ8$:#:L4n}6EIlOH.9K*J8DTg=~ OȠrꄽJGyH8U'RA@@@D@Q[Xީ s|YEɟlϬLøTWSmìl6;kP1g:ZM"eK]O!Yz~5d'^b:7{&|zSG&%sm>ܺ"ϝgu?S?,J~l-|wU*9ee ⑱mYV)gCf|k%?{~$SIlw~J"20kMC}xp&E-T]gk!҄4/eɧ%)Zk[Hn²wBrj")>d'Jy6?PuNGՉ-ju=NoT{T7ĮSQub~xKڣ#vj    ~! s@]cG:th_j~I^;1(x'p^"(+ DhpTm?Y̜$S)em$s1T}sg8730C\ >?Q9h+Mkv^|~.;N0y=NmZwZܤ^v"_n#Sĸ}Ӕ;8y 6<|s~mƐVTF\[*Y[/|cD,/|Xf'o7.|E^佷O'c8 Tr?o^ΫC":7&SQu^@GՉ5jH=N T{TWD2PQu"n @@@@?!.n?Qr2ɽ?lЫJXϕ4{j?pKͮ9?tsd.d;F%[O5&LL6VӳrUƏޜ+gV玶2$SVbD 7h87Ol57/jP&qTӱU+&Zj>V3\E'$Ikj|)hnG>`\QnytSU#:q@DR&_nů8Qsq-N2㳼I7_=xmڃ},QVsĆŵ0.1>|駛 E@m@UN gLGOa, l#@@@@3DJ*/a/kHl&iFK6<~h˃VmpHҭkO<0-+A=s& %˿28ĘӦӐXR~!4exm1]s3p:2q-er-ܰ?u1 h?9\55>-fcf֕M;q@N')E/Wt q:k0c n$Ss3$gL{Ճe%񩐟^AKKyETs=~|O7.*N T{TwsPQu^ @@@@Ol1W^Yze0D_L-kaުsBuܾ5*RB,2r"uA!Bٛ/O~M^yaا6 咚V!?h+E}Mãt:efS^Y8O_~†oZ`#U'ڹrjhwݡ ;ٹ֚0KL^u;̪pUHw 鮸fE})FI~GK޳Y=&sۋedd2f,/ We3&OՉ Zڶd1 R_ua>Az|2t0peq:T{T$=<#ʅ{*U#T{THef98lpᒼ/XuŧϏyt("V۶lW]e Q%rp[]gkte2_?{%7|lsC6[;cSTB{S3Wuk{nd_y7ۋ93Zeinޛd6vXk^cjY^VY38G XLJ)\Hh]s:T{Tp‘AB {=jp=N 3xC;lw^eنA0m|U8nÕ5T?d if%)D? c`[UpYP | ?s)іFTF*gşje^fuw-}\s7Fޛnk/u>ܢ~̑O<pέz$7]rm=O< K-hb,w%gJ} s918whzAw]OWy޴0 Z~uK2ҧ Τ0 JR05]<"SugqzEKI`vMҖ\ ǩG~-kY V?;.z8{vv@IDATH^GՕr $#cB4(~pK/n~Dy^Yҍצ%bEǩ{q =@="{D#*G4QQ`{x *]RQ庶H<UUN <>\҉ZY4m7{/IBʌ6 ߹说zǍR0s[aG RlH3J-^tOR,H8\m^m|ϸݦri/*Y8Crhgͭxv{ޒ~;YaܯLTg^ F~G]l)@k_J硑26(Mǧ]sOLC#ɡ^RTnϴuB+#fȬ 3oZ\el娕bzX}UB n$A)=P& "T}T9)5ʅ*'nEסT}T-AG     ޗIV_39Qc1Yϭ皘uP/wx}i\eR؆F3[- TkWfThۈYNҙK؛' '[}Ҫ07kƍSaGkn9$i吜*[bH Ӊ7Ϗ#8{ʙpxR'MeU֨qE V ΘS{<{ȲEv}K6T-i؞ߖnDOڙ0n5F|돖-W^N0za捁G/uhuEvLglJ&?Z[fư@y%@)?Q]Tvz,xilҹҖ/~|rs}Iv>w$ʈrםuDܾC>_؎w>S1@rw͔D~lalZ7ח~Q"?O} R{SvSQ`{x *]RQ庶H<UUN <~1x,!wˬ-Ûcu²Dr!M] e1$n縌Uy{9 FV@}?Jp3Pn|/%"tJ=aw/q0WOW)6jMeo~R::6Ss瑵'ULtd?9nskr9nu9e' -?ԂS]z\?~NNd}&IhD9kB5TVz8~iko\r֋U/o7VXR`W6زh}8{ Cjr>OABP>\Ǡʉ[P j7D:ۤ|j/|ӽM^}ˇeT_;l`lm8{Ю'L-qHg&+/hTJsCاMI/:ޤ.[R2b2|pZB\c=!M;]&^wOW~Z:˯@uRIG:}*wCՑa~! Emw 7)U7 ZRȌKʧN2 aWAߺSϩ*G@Gӿ\w     DޓUL@ٓm            '"E @@@@@@@@@@@@ ?S           =D=x`DgA@@@@@@@@@@@GGL"hpq            p)RM]<0@@@@@@@@@@@O"3E            У AԣƁ }K2-[re1\Bf\ۏ|ZsCTp鰩+t~'ӳT_2бD4Rc|Xh!*\vXMxʉ 9H7=HZ hm=kܼ"h3&\:44~PTH)Mm?>ݿøigZ'g,W̙tU`mZ}-m-';*']{6ZP[+='LDҹO^@ץVE͑"~Z_dۑ=[0..믏s)D?KMIN|&nŞC*ٙ}%,2(t\G2OT xw,N{T9}󅯾a>Ce`ZB<򚈘#[ T9_>-o}jhP%5_zIL&'}cg#YZ5Z/ {{mGA72ap">"$4leR/k~y,>k:M P+͇^?ysA_q*.\u57Di|r6g2zTUc?Ϯzj[ K_J&E{_5~ G^ّ^xwL|ӽ؁W>ޥ̩*GmGT}<kє$|)?1 YAwFϏ~f"xM_#c͙3gkkjZ m*KxxX:4\4r| Q'?=[ʅ*G-RUD0ffueg uϟwWh8Yˮw;< %tBr@ʽCcUC]}ѕgcY^7t?Yb'Nb9@_mPrTru3B}x;?>ȉQ@@?()O=K-">[E%^_Zy{n1_7y!d+E*J#GTKK'̫'js\Yay6l3E+g.j/_Sj˄jeq*_Tn}y<б +T"\Һ3k˶s_SX,yc*\:5>^<5W2We<m5wiW~qe5t!vvw4FGEۯ8;[(PxZ6KIZ=Pˏv 8kc0cagݫ{t9d!|]|=hQѬgOSI%gʙ>Ad`ϻErQ룖Ҿ.GIs?t_[5wT}QK r   hcl9d)grN^"s@*|E,:Nx֮2!^xߛTaF=E]^_}LmC+z9ħ48~W<.1{dSSivXó Ӫ5+i9ǤH!烈4gZ7%],Ou_Xk7sEv6Bs-@ 3/HZQcsK 9nb5k^uA8~x~Q v)W_JrU)NF^?T,tirT;rҬ{Ptjow$}aӍcCWWqVҬ2 䱗+jm~u*n1}cƎ.OEႳEaa&{>%٩Oav3 ӂ}Y&c! #S]m/ \pC5"%sv9m|UQ UH3v>k7֤KEgz/{u8\{cStF@3"/qjJ NZeŎp9T;rrlsl}Sɱ9nw\rΌrNcRyR >D8z{UOT9{B98U]d9̹._FLrTw!-m""VKb*}'Ξ姶(?HT9'')0)R}&A593#cgKKlS.bظγl:r:Hg& Ky5d*^S *7B_{ T#gέUoq̈ɚ'i)+1n8p^;c{8v8?sbKD?>Z_rvyjGCv%M_kmذ!qC :8dX>{&ڷs5[{_`cn#p{R=3o xs*O7n,*+q;DqFseWN*ץA"[3 I /zCKB%_rvʓ*G;RmJ{/+SxRyp>Ɖ*\Ex&G DǛ;u)l~ʬ?2ҧ *98m۶oߢ}v#Y)^?WvY#U5S>U:{X4/Y05VIC)IHeyMUFF1E65cb/Y=~+^`}cEŘ54F5*1oyew6lQwot,;ڔH޹lO);&c/^tԅqm(*`aW7}N}_gqaL??7u嚝DXU?g:u{@9Џm^m;o]-1ڭwI nK5>A*=s[gK_InMJB >S$ ^zK{IUdQr  Г gl5[S3cC0UF'F;g}=^[dT}o!6uWko:T.ta{&]cGU̫ZTP*iU+wtPm-TbcVJɎz9V;4l40EPg-n^M5Β*TsdVuU5+~ a&E\G<֖}Ībh?̷-CW}Kw@@u),g([;r*:=fBs0CV;>kEKآ jwgl6ErS0<3ޑX5-Awrf)\p+nūJ2ˢq1G}R 5|&KT;)>UUkk*Ya.7sR_-+i6}T;E', w4zK{:vR[G@  ,?jhu(j|2PnMW=߯/X'~MIcTYB"bc25˵Ύ5u%|}_ >XRAO3b؉d= ^yYw3zT<럞5:5'?P?|d~~tgCn抭껮8Hal6S9ogmӯ%aˇUaCs`TnD$qev6m/dBs%A?iK7_XpMYw_gh gf֕OK+dC=}y<_Xv^gL]e5oT󙃤!N3|&j@σBZve ׋9Xm兢$R 6O*2i|d$WﵜO]tV?r!@xOIEvv9?@z7H/rϠeHjor^jv=ͷ=b}E=MJؚK\g`41]j#r1H%SǭQ޸zw S=ٓ}np-[lPS:4}b:$Er޼'\:_(Νk!%e*fΘzxt}̿w_t-:GT%ڹ>#5<1&?p͜ (_&.o畷fOOtIdϏ>scUT]Cyud&2;b>9PZa.*oV4HK/T9]rۑP=ؿإK Yͪ7jG1*E%g){Ss#xgf 5}.0ٕnyakKjU5 F‡ I^NmoF?N`>3+;ei TV%-$\:T9dm4,olRT, ř\c UjuQ\/#T|oiT;rn$PAN"`DC@XMZv*¾jSqsHHf=ZN{ dwS 6ܒO?ngۛMJ_!,fQ%;rR3ylM.`=M Ub_$W|}4wn?gj*iN6g0,o';̩ k T-?~hN7|@BӜ+=Pz+ P 9Ę⃆_}FZKe系r  ^p_֏6Z_rxۑPԿt&Udӭ|f{kHn{@yy{*hORjz>}R3yqU[XWy%114`>I*G-tkb}m|j'UOrm凚̙2J&*rdGr}{@oiT;rrꃜƉ }},YRVL6&r7j_ gj>\_ȏwUXi}w'習W]֏[a$)OΈ&8zڟIK]uCZM_!:nCu [PYxL8O@=5}-j'UZT}k'.ZTH@ "ƌ͖]eڦMtMIp>ykK qΕk;r2s[ jPyQ_H-m"}Y1*ZKDLpܷ$9wͿkTϛ ~%b=X7]f2h ]5?ůKAצh4;894N4*LsM?ҎC!%)mߡvQ4ݶ4\Dc{LBrC7iPU55?s!/-v+Vqvno6誫G?T9l;{K\rT.T`I9B=K\\r{vfj'UI_o@@ Z-QJ%x :aUiKҴ i_ ̪dJn$Fி?6*ʬa5,c연/uZn4>BmҮթy cg;N3:LWXZ,bya:v# ĝ>/)>Rh Kd'?W >8%8ҬJreV &T{Dy޸UǏMv /.DNrڑ>kf~r3;' =n맄6BOsLYǔ+ ),Me||Ai}?ڭ'F%$|</-ԨvRĭ:4H<N}'ʅ*G%l}T;!w!pO lzK{IUxq"jiA@zY"]ia!6"n=yZY2I3`a>k9]fčep<:t ، ~cEϞ;*'p,>o}B#k咝&_,eG ea>Bk&8uL^GՕܲz̩ n۝ɪT)LG'D| NBs SxlQ;=ח(T9hȳ>ku_?STU%Zro7L1sT\HI`ټMp v9? ؕJao?HhSzT;r*kKnWxa0m 1@ΜiQrZ6qɶY'>h3݉m ;a t~(ե,`/mX8E|B*'0^)Nd$D. UN&(A4ӽ$ʓ*G"I=j=jT;ry xξ.Z8L/A=[_q5fn~s{>4RyCʸG4RV[Q>H_39Qc1Yϭ皘uP/5~x}i\~ɳKavgϤn(G*4RQpN̬ѶS0e3%ڒ^RL&8h:sr' UU" U KN=ٿ-_i>az0 Rݷ߫+ xfX~V${p$%/{]35.wOʪQ\7~\԰hT9ɳ?`/`'c,xilҹҖ/~|rs}I9 6p~L]|XhIuу_r o,w]1ۃ+;YU|ɶ[k)Z]9ku}*CL_}sSXXq3%,g{LIZƦex }i>ۊ_!prD]{Ɂn-ŏ򳇭-&C~uVV?B}cnUIvr:r2N٧n .OzkN(lg{Q'g;Nz9xuSyR娸|yO%JHAȓIsȢF>ȹ9A@O{Kآj7:&6Tp_}cXsY[`wW usH+C.up֖J^YMRp##B¸ULYS)E=)j |;La9ħ^˦ew>6;.)bVygK+caC/zKA מ[^P/`'?ƔJexecNVSwTݾdOcS'-cU#M됷ryeC#% ^wag=6%\ݿkݜC V_^jq][f,mF_1B)iPu]ޫOgSd[)3~>͍/HT;rynRy~ʅ*GI\,GIfϗTNY'τj'UΡd7RAmȡp }aQ'}ZFF멒ZW9wFgYlQaVGh4ֆ*O6{qK@2Wd7=&s&S쉶C0QV3U Rj4*|g X&Or~_s#g!–K겘pNA+4՟kOZ_b{=(UjQNGӿ\Sz@P (8me5stVUW2{W؞6x+LL]TX[VM(ؠX;_c/Aruۆ9}3z򊻟)tIa[Rc s>?fM9Ρ3t셿-"i-f{L+_孢څRײøsv@7hG~rzqC#R9Sj5FlGy~$>('5/IQ@Hܠ\}|3{7G%8E{i R娘rS=-o}jhP%5_zIL&'}$J@n9\]296g׾NW0|c2iDFP yMD̑Cv}#YZ5Z/ {{mGA72ap">"$4leR/k~y%ܿT;y=z?+GEY5檯욪e8Ѡm䊋EJoRZ7^Ja/l;&݉Zw@+|vkɾT"+)SǞ cЫg7o.9+.WŅB.&#o|TR>FI{wKrcA.xI֑q6>Ts~7 ֦A1?|Ӥ[5T}T9)5ʅ*'nBA@@@@W=>#K+o-6Kmf !'\9(RiU*,Ֆ92vXj^:a^G܌eŒ%EՇMwܲq:/*7̾<օwn*Ajzg!U>B`^մ%[g#"vKNT.T9q+     j:Dl7OWΡ'>\!-_cl㰔39d#ImT9jQQ`{x *G`_Rɾ>joOșg="~r[? 9OU,!??<ݏ stam 6$nߟ!C'd,Y{D6] DXe?3PkQ7d&CmT9*>OAB      >m۶m[ϮxD=+gp_('6k8iJIRF*[Vo2*=olUAN 9kj.|5/?TfRZ@ECn^S\wmg{~o zsݛ26%)$w~#-[t8~ߕ0A{CND;rKH)ȭ L웭1Q)@e#? 1=3I 0:#w"sdU?gFԓ+J64>Rml'NwScDt33 o>;wD5M_kc/O,X6]EuȀ()l3C9Cku '~]E u%|}K \lj;5;7X1Gϳ ܳN;[>Vp8v&S y|m'"m߮Lg=픖x$N9<ю$h="~O?_H+ӜbRFn%#x~$zt{4xr֗ndY?/[sڼs3zʊ= R7@tZ ogQCkCQsHPnۧS5fk կ/ۚgɞ1jQPQ`{x *^I@ MLZHB\{q 0d⸕_?Zg&ӹ2K޼'\S6`>H]dON:xq\ 9c/1}Q,4hWMg掉Wq~GSw ֑I@IDATȘk`0CiY-nU5{zL";#.W2nu$壴`ެZpCLvRWr}b7yv_Ge߰[4B ?ZMK.ڼGL_="K2Bf0*ב>L:SQ:<4#:UUC>L@@@@z 3<]ZȞ1cZ6)}i҂9m]t{ ˟>ĖˎLҔ l,fa`WwKx&s6.qwYcӮ͙"&N. 9Ę⃆_}FZ!$i|䳻r:/`Uťbfrcv$!ȵYZ8HASj}~cSM?~hN7|@BӜ+=Pz+*El?+g G>TgiN 3ɔ IsՇM\_CB}Wiu+2gb=ZN{KdwS nrTT}T9F@xO7.B UN @$Q{K{MTR]/$gjmh]"#+Ҟ}8Tm;LJd_RE8zڟa")N3wZ[fw`lU#Ԝ\;u3!jY>#>iͼIMmTƏ|a&xʹ~oYˇpBsk'(O#(W{f3%eo*הY|u7{I7 AaعӼvӯ#J]?<|cf*,B9KDp}Tdnثo:r!L0+WDvK-*'ϺT}TNrMG,*':M">M/>VՠKx@n<PiQs_8-]|),s5^;O[|rWֵǖ?E$N]uV-?X1n二?aWσ_pZff4z݊ŞMI-ȍ'ڑ#s줴o_!+,,dv)宛/'07Ԏo]S8'IvA/zKfu.cbԡq|ڵ<úsvD/z,'uB+#]2ky% ԲKrtO$law(d>-ӥؔw{x`âRYi`tS-U >OAB     eNY,?XvsM:(ix]?rӲZ)lԭ`PF5 +t3komD,LY|kꎥY -^]iUuě5Fm?_s=yt$I_H+䬗T9CRNy~ٓuUԸƓߕ?i*F%T?.jXJpƜ۽,[tiCܒVm*FT;T߯ k~h>Gˌ+Q'O=hϕG/uhuEfhT;}vXg0굧z⥱IJ[8M-6b&N 5r|R{;wAnG}“whC'S:XXq3%,g{LI[Ʀexzh1Vz =[_q5f~s{>=ܐ2Q* ;gTltpBfPNT.T9q+     <~1x,!|Z[*M{e7;,nX,sH{s<HYv>6;.[9.cUG^up=աb_ bXʍ=w}OK)UiMDz7?I-nO7/-6Ze?9nghGbEKgpxOD[~ɧSwTݾdr:|hGryGcY<}޿?_Zڛ<3jqU3/c}{ifM0&6mQG>5ǚ{i>q :*琄C>,"SQT:2!2UUAC>,@@@@zyvMG"Z([G?kTPu߷XLLֆ'z} tYzf!AQQJF47Ԛ}LAb+M%-,s,Ȍ!G,)5S_2ڴ0`]$+O.˼ kҧq7Q^>4^PԖ}g￞0Iۑ|s? ۢI3i[o3 sT[>U~vKrNq_.)n=BA@@@@ H`]            %9Gj            =D=D`DA@@@@@@@@@@@(<4p=            `p0FV"@@@@@@@@@@@LF            AJ@ A`H@@@@@@@@@@@z8zZ08 Ƀ@O#͠l]zUńs sm?~Ik[%SSYùæBUNRu|TBѤKaUazsc-w6I*'b ,#iF&Dhfs󊬢i6ΘsAQ!*465Wlt㦝%rzl}fLFG[5Ghw y zbKX3P맫J[`K.T9d39Ln{w5 ?sĞZkFkj _)='L]TQ[r닣bw+ʽ[-rݔ`w?zʅ*'v>v3'&ipdYsmcyRRSu.XS+SA摟_]T}T9"9\29ə85wo(cd'忟 T]uvc{D.!+V,0KWa9nY=6PTuۆ/zY4w,bKg\g #}Æf,w~-Ma[Rc ꏾY{SFNuj>g;Rײø*^&9FDsg7VZZ`rFsOISNzd;uqgu셿-"iˣY3|I.@Hܻ*>O، ^{>E@Bk=>UUή7e1ЭGD+OYm9 µl)s{\3k,;f' O@A{Pˏ*'6ǸT}T9G8 )+^R/q`uy4}y!a_F|Q#m8*:CR545^  Eeo^Imϱh67OsHRc:~}r+ }LUﮎT)9/Ys;/T97?X9xGO>PʆIs3o|6?fw ˫5[ϖ~Uw[9[Pb_9mߙf9B PP& "T}T9\J7{r@jgwUH6'OҎvL{6rf]Y|_hdlHB_xY/Dt9c"c~KNUj(UUj'@@@@|" QS]O>W5mW_A_z}iŶQi ,̜BOrPҪTX-_k=?rd@Լt¼ʎI_NfXZgK^^|rNer3Ֆ 3U6q:o0x>c[VܹqPܳwU +`w;y:]ݩK)eU9S),eiy.:5>^<5 E$;yZ^wӸxϒ#>ӑT%%]B/ r@\&y"o$\H2f<b[>~gt9bC+ye޲G,w}1Z~T999ƥ91@p 8 HP} kn"n.l%$"J Pɾ>joK086$ʧSWqa.RyHvKU56:JͦZcGjkpa[$y>!Nov }Cu %|0v~dm%> ڤỹ0EI6'UHL_ T{wJ/ rѧ^/lqxR^3meːᩮ r:~! g[Kk8FrF{{jj{f?hRѬd*G㖱e-0?-K q+jodL?Z~T9rT;!    ;Y9s םV7n5R (aTбG1h {J$4R\ԷKÄA_6R&gfd liimL 7>kB{&nN{VW=G5YS#k)+1tkǠǔqhVO z{?(RQlerT;9dPّq{ yu_g&+khjL6 ?o[N-?t˜cRQ圵 @@@@ d f+7w RYYReOTrPG"m-gx\Yg=@rVL]~V/?kǤ+Cr6O6g}[=T1T%,g=ȦfRZ@EKq=G>~߇0*؋i~}B {v( æ^~Ҷ[++//!j\ˮKpKR"VBjQ$*    %jS3cC0UF'F۾Tq}zAn}QBch]ɮqXS?{uއ-tM!,Vq0jQFW ҾxyCMk:{(+і{K/6[,bq̪Ri:QO9GҪٗƛfBꬅU}-ݫɗz\},g(g]MZZzc+Fe#_A_ZDRXTT.te-/vQT5"ԅ(--mIK/I$MsOgN&i~9s<33眵E^a" CO}T2 Qr!D}!hw)jA{3 DrD5GvɂJgG$ kark׾l!;[Ҝϸ2,jKR~ŖߵY{Ս'|;MY_7}|}씡}V|Ӎq]t市4? Yβ}\V`S    D*LT@(oh-s$:6eeWPk6~=uiyvquǎ|RsbmSȊ%Ke<]F s%UǴiK;U( KUiȲ-*Q V;s'`}`l]%7g'c|lԷ|ʡkgorGjf _)+ZN>>֏B5鬼ny4C}Y?V|.rX\ }+/VʱͪUNȁ& `ArJg*kF18dVinaY\17:KZSJ{~ݣ۾6L^PATyd_d _b[QknFK9ǥ}wKi*ONMٮB2>J.dMnPwy۵_?}%U8pvNwInu'm3&T*S%E˚OWqOUQ7ie5l9)<K)l5@ґM" ?+!B7L_mNVZCj=+4el=B"o;H1~:ed*^ȩMb$ Z"I^$} Nrf)X>V9o X R7?,"?~u-%_155ox\2~y.McF9nH'Q)]"A8l5Lyr_rJ7:_K!B &+ڸ(TIЗ `2>o ?}aR4j-c#dwmRX۵LMi>hL.7ծkuGLK!M߶-p #+34GJ;e !DAhJKIGM!rZ1Åɥ- p;cws J2W^)H:ڍ$gJib ~ӲlZZ75=,:F869-y0YM|e-UΦY>V9a9>$OݞI}oK,_H}%5DSYI֭^Vʹ5G">V9&! ADر7_E'>J&@5Od.$NJS:!V9U|Ae:N*>mIbh\Yg DEIϙE-1ؾl=D ;_XksǪUY2w< -ʦcEskTics1g>V9f!AߧZf <)qw?_&}0^ îWm'Ǜl`Xw&'*M7*0jRmYVB(eB]E<2:m7Ң貘;4v-RuM]t߹Kùvv3gNFK?5K_ջꬑ;w#v&:2natdOñ/oyBҩBSݵLk`g)Q@XiT9oR[5|AI~gw]=lH_YocΥqlvr|w܂NjC;u7GG.JEvfpْ9p2[J*YI'Uu=Xt% "7h^_FZ޳@%.qDHQ Y騎is9r62~Wx*Ά$';qwKW4r.c^\ByCE/g*rCbX8tYuщ#C=zszk>YQMyH%N}ųVV9D;=Lwu+r)X47RT=ޒ+ 3;r{B6O3˪q&Lj)42v˷dK^K ]Z(wxNV9OvtΪU3{<_?H:!zDy1 +RCg:ͨc&<>[Uνc"a"RJ|Q]Wr/st".<ܸDc*XMV.͕)"c˴_ p["֕Fƴz tmnk gOkxhvV9.iO*=fayKݷ#>F+ZQkWp_?rS}/V <ɲ4ۛH+GDzcՎˌ::Ł8ܮ}1Z顳(s6Kf Jʮ;NoeX|^}ǾpZ'tl4c~nkgp) "*ȸf؁c}\N"h1y&9өBV{ۇ#|ڋs8bkj'wZ r=N~e G%цL]2}nV?xX'Rҏn;/dZu˪ e }` X~Xڅ|    /+VE6sk1>|ta6ZMC*X_:$LH75zM?%5t2j6AGCZb2yOf MګvH Kdz 7E[Zt!>g];k9*gɎO" aK4W0Uٷyj\^Sf>1c'k'}#ڌKsg4 ߟ_֚i3vLX=>u9?Nr~=.iM!I˖ħ\ִ|]mٟ+ ږp2dOJjS*g+Ȼl%l[0}icks>V}rq     X& 2}4"^H9;5Ԟn~驇J!JC=DiuQܿz`jsEz^Wo?FrPF3qRבiГA{ q& Ȕ|}Ů3R 03'q'߮.8]jhl% $,r_?v; >+tDNc>x8Ad?& jםC_T'冨?2ZߪoqX=qo'!%ְ(m!m:*;UMGY2mq~F|G¶>o+1n*2o&zY?Tr{b_߻@(f|0&X!?l-rcc{z*6&f1%kZ:N,!i^j|m2r>0}fErccr    >`Adӧʕ}0bAUE_۲U (Ͳ 6Vʿ|O_XRQN )+ZR+C̨1rxlۑ=wzU4e2pC %4vh}t㊳kaQ2ɘGHJ P9}72r¬3.\VT33F !Db5pnU.QL5lN~waV9^=}xrhِ_  ͤEÙ>m+ `XXe*j'@@@@X bQ% Z            h8@@            h8@@            h8@@            h8@@            h8@@            h8@@            h!>+TH2SEJKދS9:Xܿo\.ItZÅs[ZӍ*3pw5iJAS.}N%S?p>6N6Aѡrꚮik.h˓N?Zd4Gﭺ4m6UV}߶go 7zb-W7)gm뢤g#H9kDT;;gѫ,Y7GaWm|zBC4}%9fvY=vԕUcɸ?vL}ʉ^ G?j ~+g      "ߺ,! `K|Ov2AO?9E;@@@@@@z)ܵNQRTb1ʰa1&)񲞑ڲiꃆm|"Q72dD6).%.ݡ Q_)y`Bi @TPeqwO'd=[o4Ƚ$eGF#&">iF}M]jCCj ՖQuUdm*ׅ_չ%K0Uλ^|>c뷬vX}ecn'[`D'C {NJa      x CIƦ=ze)ϕ_iu1l:G~hq#{${xtDWeB Ķ񶓧>Utez祳~4o79=zgyՆAV9E,{Xl@˹c*kds\gg-Nl!?w4a69nDUC[Y r C:^f3T!GyW^[Dݚ^^&ym):xf폌)MG%S񳓿 j%Oæ 2%XkmuW#  O9(s_NW6UI^gZ/\VsդzUiz4JrRjļYuuXC{n=_r|q?9hN*>Y:1_f٦?6QK8$9\\k~T*wɴ[=ۣ|;@"Z      AN +Wl,:NE_.^Pq=B;*R#BR7Q?Tng_,ua?}28L/4X5D`IB>5c;.&kXyJ6Y;Z[Yc EWXh޳Y7Rg֨I:i1I&Sr},rg<X[kxw1V9w64?/X팒'ڵgj੒=$U}i'UlT*Ɏ`Kg-$o{!zD"H:2?-~a ^_Y"1:vZz=%      @׌ \FOH#c]="Q55ef3'6{s~]„_sAɎ,+mTX IʕLQĽZX罣 w{SK͍zFwy W}doLSqv=|SGzǡCDI6ma^w|gcE#˲ʹ/ק2/X ڵ C4% L/ݲzqaY%+1kɒG mAmF2_Y?@ ?      l?HGON|Z iJsOޯ{t/ QK"Jm4Qe Mʸ[+)6ź0SUͣ#it:MҫhT2JA[>8.[ݍY9 J2_EJңN| nLG?Ο @GD5wS:_CVM6Z 賕ێó#im-"} Ng #;Yi Ս2s`4>mIȞy;,!I_s-nF=I(Yl{m HKñ/oyBҩBSݵLk栾-HİӪ{v#4)i#csio.!,Q?|1͖ .r*rCbX8ǩtשg s0!kXv:N߿ζ \K("ЗK3^(opY(=ĘG5Svu,UΡ?{"kO-@u`mr{ <͍i?J ζ\ᮧL.Ӛi/餽SȀnM_ݺ8@9@tAgMbl3Vy|`]sl.lriLrK$?'2 WyV}nDO'Y1`фȡ7_D:W,L3oiґ=ۃ'ŲsMWLǵ2 O^rsAέ/َ4۠W,*b@N|i|-hMUCq:`G!kбHBjȎ֏9z-RzQq'pL|Y=,C{>>'w.& ptyo~Vɣd\7{X3!$LZ/ג5YieQhd/֟we쬵`A͘0k.tJjer?뗦}٪UvOyg\ "&15a=thʿ?P5E ;&zp,{1˕m(̍^&&tUMrݷyjr\숈S?T3|̰xny!=YR.:ϓӦR9NY:+]V^5[%t:c`|NvoRA@@@@@z#"BFEC-#Ety0kjVanPOh *Q*r]JMd_@/<+/[I  ~׏݁BaJ.A5 z23!yq`}>>֑7yŲӾL;}k2%__k̄k'I`o5%"!hqKu#{5>a'#Mζ~_bdhVY ^!?q/%X_#viC@@@@@z;v'            &,O#X           @HT( @@@@@@@@@@@?`!J"Qq0 >            *Dʼn@@@@@@@@@@@@@ ' O#X           @HT( @@@@@@@@@@@?`!J"Qq0B|6Q"ZL)-U{/NNdr`yrr\&i Νn9jN7L•֤)%5^r K1=. #̴q'ˈTtM[sQ塷tԦ@0Ug&I&j.ryaI\wI'{ wZIǏQ9ٲ{sV[T`.N3cԗL2k-}(!X}Az%%4@qL bl/j$J"nYy3}j sP;@@@@@@GZ\`*YɊybhգ+K$پϤԾ}^6'ȩU>h˓N?Zd4Gﭺ4m6wWNmen DQ[nRfOEI\GߑF NsHr6KQ%)GP&IJ䕿΋1ۉ֐kTrߡyC ^wFnlTk# v|ٖKJ-G-XK)l텁DJrh=X;߱`(O;qyl      ="ߺ,qEm/XnsF^E   4ߧ#.;g\|fg o.6w[)}wryw祵Oh9=l?%tA Y 5SݛKLae"8di~yr!CƠay-ч& C 9n*yݦbӱp!\%qf! 2S{j9ɏ@$!ר[b`Va{҂&کUDE=*K<4M9,: ȁYTo/{\M1~ǺMeD3oA4 Q@O% 0@&I_bF`OVpKt?+R]#MI*)n?\H69:ټU_5:W\ KYTyn՗P1ݚ5j*ah9alm:KAz^"6oޜP?hpx.Ϻ!ѵ6< /m^qg3QN}Nqdq,G)      OKT.-ute~_^vɺDm߾m+ 3ggq_٫Y%]6?!ǛWlj2etemSլ|/S~AdCsUO*MOFI[NW?XTjë]iy g]~DI?PFqҿl{@4(D}ZϺ`SPڰI'9b[,Z,%}h/rE-0S0vͮQ'ocz^[W5Dpfo>i-ݾ @@@@@. )s6KOBU/(Ѹ q}S75qڀk?jq|b?ή409|pG}L_4VqܺED6Y;\Q}kmWDQ%Q-[cm]&OgHgpZt=򬶢竹ə\T]K#TpBm"ݴ .flj%0 dNq'3/R~>rvޗTCb&& *F6Hkau"kׄԐsk "rk/]>i6^wH*><'a,t;/j]J9$%iKl_w@4|[_^~رcOj_mrY?T1#8diy/k[Q7g'bBm?[;}1t02,#-Y,*Q [5/(!v#5kLämhk Yksy:E}04b"w}/L|l,)æx=1%fuĐŦ@9?/9g-$o6aOKx^_Y"{]3&r2_]Ngov;yp?99 kA@@@@@pe\FOHƨ;ɏսN5YZQf={|k7e,LX?/"?޲ن HՊ\i}uOk{izஅ|;ZpW=A![]._Ӳ7Iҩ8E>E<]>PaD#+4j LR£F= Og5RYysq6 oSNA )"&<]tAIѲpnâ_Uݝ?5~$Q<|KV=~ϳX9hJ^e_9JWJQړ%ڂ{%ٴayݡu v)(~@Dwsr:     ]B #Tyd_d ɚ_CSJ{~ݣ۾6L^E" vqNWrASmukdjb}/ oӻme4\s 7:#8.[MWyD7B901V~ܘ39Yi A)c6)f3N*\TC_.O܌'JO\LGMTK&5ԯ㞪*B_ :`(_%}j/,V4)i>4K-׿zk,hjR]hP(ev)Q怑XiT9oR[0IC/ 8<eVvUןa}4Fr˲z\q|/y|7$F{#sFoM ^ mKNMw׊3Yw r_G' %O|}ov|ɝuDA1@G1>/";sNQ i8S1tN=rTİpsk&4g s0!]IHZz,ɓĿζ |\KXLUB_.x.e9T&5WQ|jMvI񐅋zvã]΄n"r#^.qjvŗc~/9U8N}m/{Fʴ[u}}g[pR&XHz1l">i{b~X0`V9b@@@@@@9@tAgMbl3oZ-@Ξgr&+ʔn.INea^ӭȋ Tye9iH[W&4FDӴ^^0yμIGDoE9ESVq+Bn4%^ iVVI"hq5ݶ&9)V{[rq$V;ۮ}ѧY9%K3%p6uEc%g?s_K=cԐskso[4 oOZ,[!NNAnD0@@@@@@tyo~Vɣd}%+ti^,(CBH^%k)ސSѣ?Yk-К FOW/zQ&~iwTv6N>w6.K[lmFBB[%ݹ3/TkM)äcƎ+3G~xKy%;>2(NmM@*VyndiuWWX= *<5ўkykL>fX<ў,4%@ҢC0:9m :cM1? '&VئNN;7H`=>AI_&).[rYug0h[ɐ1?<9so[Zz_fO>`i/tݡCO6[OT>A1G2ޚܐ]ㅃ z]yDAh;w{x,X0Rpk3C n-@"@/$ x QatZ"˺*dD0cVoy7ԓ:JJ\/Rtˋ _dLިn~H.$E_?r@G@Xxe%j7زV4 sHt l%qaL3uص|fBJf$NSQ}ڻC|)l!r&bjYNB{&+8~']3飃*RiFs앓Vᷪ[\fGCpb# Җs q%\E}'jԡ =[-MM)i-*n"N[q;D-g><'=/T@@@@@@@GSp!t1e.mm6k`A"dI/- I u_o(J"7dd>hqKu#?X^Qcz}ٶ#{V ̴ѡ"CBH(jM?8[U%n$GfXڵ0}JU&sA!QaP9}Ӵr-ס8ڋ8:YJ ;YJ@xsP      `%"؃N@{(@@@@@@@@@@@@  @Lނ            D"@@@@@@@@@@@@  @Lނ            D"@@@@@@@@@@@@  @Lނ            D"@@@@@@@@@@@@  @Lނ            D"@@@@@@@@@@@@  @Lނ            D"@@@@@@@@@@@@  @Lނ            D"@@@@@@@@@@@@ lRE2 *RZ^2-}CrL.;rL$cMړM5W< C=2Yq2F\?H?:T.#R]6mERD*IǏQ9ٲ{s*g.TmmMR_י^]9e6ybo&zkȁ5S*VkrJ!6A)7\sL/s,gT9%樒3}Ƨ-Ժ3$v }ni*1-y??wDjYKn qLj }%9fO M۵팏{Z~>~v#T$Vd=q+POrOg+LU 4]2/g)1pYE׷qa`ҦxR_.\X mͦKo|y˷9GRIfʊUbDd)1{VO[k#|ѧ"} oӑiOf3.>A'X=t鬪ưq!mF dfrer5$ip R5d 9;dġEcq̽+mB1NF9_o,8xGIxgӉ T&C?ۮ; d/93OAOtT 6lf o׾3o6Z { ]K      c}%t7cLW 1I4dזwNV4lte{ (E~3)CF-75/H%$6;'l\XWJPZR6*Ծx"i].|)s!G㺻h]WV}HۧB"INEjl]^}_A~bi9Usy<?2z@1IѦG}MC4mW+72Ws4=T|Td'@j'B'zA=8Tszm ҝBBl#c)=w b^E&޶» uqowKkvm$Dqԅ|{Gվ[?E@&%֙dlڍ\p٣WVR\Vw9<7<eG޷Mѱ] -t,NOtV." e$wtY/%6n>h>x"/Hi}M6!BϦGR4 dƅˀ>ĥxh#6*g_;a퓐uˍ.1^8`/?%W="ĮqZ#26` k{ۄ3{YwUSB      s ATEN>T]֟%C:^2cR!GW^ QdT鯗]I.hą\!]Ǽ4yzBW9CD*yb< > F_UDV7)?d JV?L1OI8Gxj'ˤZF~oVَ|c/31XJW}vqEM۫Tjsڐ)瑅S>@thvm+GI~w7~,!:TՋm˱("f~2W~^zQuz~ J4n&kg< }kme+4| :A& kX8؎w=V*ê$n/?Zl)BߵărbfG<~"5ޒ5Uy5qCxRq-^E!h,#=cm*e3Jn~,SjB(O<؁+)G%vWaVۧ,xnOqvD"I$0IDr׾ !;[:CL 4^0wV,si]L~Kx.bB=7Am*{=v_/172㿵;n=z3n*7g'N}>?Vg~o3?G:ٴ9j o//?Qwر'5g/6u|ɯ_\43/:~h gjذls}UNiSo\gU\#"DE8'^QZHt& I+t_>Vlz҄:iBKB5?jf EG+z,#Ӓh8@NNJӵ-C125ﵣ{m5Mܵ{GkT G$}}FaiXJxTܨ>5鬼w˲ykjqs_i"̺grOum.caʯW})H@&T:&w*z&;,sV;y˱(M5Iw?$\-nfu hwxL쐩p^ ̌L}_]g\S-@̑URzOԞ,}\O%ٴayݡuY+,R[ ƀ@"Uٗ?99ªO!G@mzA 2TlWYWxt%b^D|K spN~Wqcδd5jsCSFmSbKA>$hJsOޯ{t/ QK"}(Шex\sNO-FJT:1S*^ArJg*kF18d.y–5bo& AsM$,V-Wo)g>4K-|8d!X!m7y}9{38E@~P)8L l=5Ly:OM'qM_G%Rr׮ku<?}t.2~pH'W!d-c#EjeqAKeٴ&YfaXqs}6ҵ$3Mh@f pp̀ ~s{>~iGQy[iٝ&i)&#vx~" +5rYMߋ>}%af57y+9ɭOWX=*TO/&}Z0 (/3Th"V}dnEZB':7]JmV9ҹkQM!+DțͺY $eY4Rd9ɄFW|ϵmb-cU0\xYWMR-iZ}F# r Q~%{(t@s^Ze3^NJ>j2zɎȩ|j[V~FZG ;mhe~aeP4_&[@6:0 o0I,2D=!uC"'*KsK/ŧ#[Kpkftʰ-1w9ouyylvl\X/~{W~(9CQY94t䨑dgŏ^E,y^Pkԑ['}dVOhOo>[79D]-oא+_NMHJrx`*EmZGc}41]mo^35cy5Ӧ7Mx />²Fa!է+y]Б$_E"?9ZZ1os!aFL0_O|r?Y1"ad[Ce*n L.ܴvs^h.ܮ%l(p|#a9ḧ́.DTtfxrF=GMP?3>?s9ZAٍ0=u[l!NI=+T&:@2 uX+ȲUrs 5+hv6vOړKmɯI{(ѽ7?^wuyNϾ?-YKk eΝH'yC\no<񺀭/FDM0s/m\fqx6'V9v"1%UHb֕{g;NLL ǹ#L21PiNKkqqYk./s"E$,H\_eSAJuqgUr+տʜZ>9*{q&&[$9X(X)oVvԯg6 0{=Do'hNFNV{ThIJZoYPJ%k+:lؐ1bH2rld;Rv*yvpE\`(Xax~Ֆ=.N;4QΡqڤdx޿ T҆2ٯ>2lSC74͵W7a>j+ד~mads9o69MrĨ3_,aUg+*y ovތ<7c?AA@@@@@7 G           ~Ce3p0 @@@@@@@@@@@@ AmA@@@@@@@@@@@DL            K"m;X           L b!_por`"68A@@@@@@@@@@@Dv@@@@@@@@@@@AĄ B             ߶            D@$I K[ӽBT$jcхsûVv/UD%̇ ˥,\'3uZ"7q O.}pȽ@OQ}{xPj0MuWxûی۪&ˠђ$M$5B9ɣeRFuU(eDX]û[ۃYNV9[5SK&Vzs}ןs%>udƘzr eE ]ڐ#0IIk֎\8l>c13VC3[X<6;맏{<qn3U6X;-%CZ{wH{v#y8n~# :A@@@@@ЪyǦq]ə]Kېot'i9qOL,hՕ/hѤ27Pr_NX*IڨFqƈ&yE:Q6EɊ:IL?VO) d[wL !lg߯^O[gvYkM$Y7[橤Xɋ]]vZε#{˦.18K ٟ(2t+5M~|ϼryj(a%9f{g -cO%}pspgtz:-}J" GCR]׾:sbsi !O%qpDWLz״?E/tݶ@Z@FD bBKe>uUƲoyͪ+L<0:"R.K\#\vsV#_>wu.Sӛen uIdl]JfDo4.\>Y]M+VKw~0q}%V2mFroCOo/wi~j'C+99G.}^+Y'4S>e\a|4rږkȫuXY-їL$r"s#^C# & 6$&;dJ^ 8`j?/LX\<^{1h YQlܳ}szV9Z@S*bE~)3.\p78:*Otz.qwD)}%{roD$u..:փYCB5ioJg%1].#\O>5i ]}K+#MH*d rb-Ť{"5K%rә&3?JC|漺uvȨÝn{ 3w%),D#zY"z$eoQ2HڅoK7MtnGX=v&LYÓKw4tT4+б0m3ǥS%~EJ>1ふYZxǙ|_|:&?^~GQ]Kf:9-ّtFX]Xw:"!&uݗ@@@@@@f$ ҅D9t}rVƽLH(I!. "w|cB,o~9]k(8t\I'w˺wCC5pٚ##s];K^:{vWG ն$el?=gt)$M".VC^stqv l~{4-%ǭH~`ssKOy5טfi/mBSlǎgD~*gk!Ƨ?&C?D;IdgEQmJwHu Ÿ oF     ~Iu 4#IOnU%Wj){*XvJ~i1u+EtBgZ-erݞGwt.xbg]lץ;cu{?&]YdqbL۹ʉK񰟹)N!o *sȖfyF 1GW&ޖUCN=B.YܢO ? |Ox`gr6z^ ?//DY6AK;U=o5'%&ϴrŕђn`s[(nxm;W+ XǭWJ!       ODdLPs!Ə x2y&{:Niq=3䛂nmXB4+d|!ؾ֤'pٙ j;1cK~2!vؗsKdq74Ĵk)d?}]'qYLtV;m1MSuΓkN \),օӸh en׹y:b"i4\IEx'^cN?X!mt;{C7m)p̦T ϋɘ=˳nhu~t qϴ_󷖭gk"wȍwǭ^|9Xn3b ŧoMuv|sOZJ yJnk"o~Tduo[& :!V9TVK(MP,5%gE#Υ%͟q u+S7;暭jdӭ}>F 6E9PB4D;mX=+L/d[+ζqAhhZ6+e\3ِ;vަ~mų n\ Sh?&t>x`gr~^LGB^ Y@3-vU_g^H}vɪOK]/꙽`jՠ0c BKU\bW4p      \lFw4$/LC#Ye r yvRzX ]kKPqhGFE Rg˶.U.k9N9VZ,3Uq`Lm˞ؓV>736T .[# ݩ:>VMeyDA@q_Ϋ=QHYb]4[[Qk7NR;h$_lS^@D#fL&%'eDcR-;ipp.qE>:&EM-iRDOu\5CN2tdDٹ7^z59 |`m?B؞3^p<$LUykiyU) TN "Qd_VNY{!rs55{:נ;YO.$!$/0# ;*-ڲ @@@@@DI+_gkI;+&/ɱn9?>&ն}y2anKSUrMZ*>[ӐhhuIk_w,X7+~7iYD{QK8sO@d[ v؀xi/cXgvz"Gr:3٩^t|4W qYĨ%VdɎK@MY]n%z'~]ٳ*O8qt_+#S3cra,45s <UkOƟ[؝গn&雏=CT벞 $ ȼhV63WÍ~Qɷ:#aMPN;*$cӚ 3#-a#tdM$-'2p[T O7Dp^PMWz DEa(77P[he pg{z,NNiV'pKѣ}sQIg߭$N傭gCy{yjM_d۸g4$Ŷ4ɔɉqa]n@R};*\ItEn|dz9٘Y{dSxCf\-66DRETrY.?cR}$!dSch*M{H9aᜃE++~>\% ܖMXk?cy?_lU+Y`~PU.R[}:--Nm[~O ٸaeߡ+(v"tXO̺RÅu2aX\"Kɧ i б ~ @@@@@@` Ҧ3&X&˿{sh7&qe^JYaqEJ>MSkr Wq e>D~([#*BV~sȒu-=^oR\ mwW=[OH>׹n1LnEsoNV;Qo-wsQ@ ${ùo.{Ӿ̩hC$-q~e_*i4]^M\PZy{(*WK0CǷ}c DMƤ, A$VEk~8U6gD',Rh,;V7o:l)_Q #R]a˱51cӧ=@%\% ནBpVqYަFǏ7ݼs=>STZoouħ5}}哻vVܪV|rz-dͬ1uvF >l݄&; F*`ڥOU]d;|syqm\*<o=N1{њҐw ֐uw}v] +TMD!!]$ɯ=Ѝ%933߮ƐH8GsnQKEv#J* 1U~>()tU1WsQ~"E^hy?Xrm'W4oh-Y@'=k~ pithu5境$2C䐐YHMֹ~w&kg D 䍃nugwVsYttHzQq֤. 0)p#9ΓS wB'-ԼSF V-o ]$*]#^D^31"WxΈVb$֦[o%y$ii@tݡC$9Dwrq5Z?8U[wabɪχふkݏ3*'";t#t;9gs~bxDK* |5n*w0H8ѿ>0 Ԩ"'|ܗF\ؿA }bq`IL%=\QHr>X=dR@T.t{gtS,%tNMe.sLמX6JuIgXdPTQ31 Xt+ )QIHʜJsB.N’O޿ ]nD|_9+HM ߙDj1?R~Nm~|x|bsy]Y\[>]ǟkm:W [qd@#E @rkNyu/cMeϊ_kq;pWd]]ECSնm\\s8?'q+lp@@@@@@"4s,~sy]d5rߴuT&U lLF-|ɲ!wγȏ%}i DEZR6kw-=*Ҵ #&L%)^/'4\x`4U9G%{ bDK6 f*:[xS*~;ru\DR9Ne D[IwA DwB+i>.,ふ ʵ9!t}Or²ꓺj|y5T츴%Dn:r{?X@jzoǭ| gt!S% }Q%ԙ|Ա[U\o%Up4I]i]ꙹ(z_RrvAN!Oty]ki4 Uݵ)N[W #Y]H}·G,=wY}ve>Y-Z"]> H2/v־ZkR7I%VsU)d4hy#ov7rv5엽 7Ӟ{~Pk_9S?ɱ=Y_ku4윗Va{ dWR^#r*=}bY!U%QCff~9A@@@@@x x\ ![5pkfw#r+9;pM@Wucg;ؽ=\,Yi~HX@࠙޽Q/0˭Rte{hg d )C,aZ&uYiSʹX=0ICaٟ.ʪ͡ F;Lfq]h˕[{.܇ ƒQf?@W 7r)mr.Slo&ń t?,Q9Wc&e5r c\1i|lndiqf~oRci|t Ve\ݓuhJ~A,}vjK~55HCwp}Kw-\%53!eI/C|NV9 L21PiNKkGy)ʈHҠCsv/U-/TJ*co EͺRvo̖6 LXc9j^.1qxjLi}|dC*{q&&[e)`)ZW e*@{4,xRY|) nBEeSɳ+zDA$ SGqr2N&DEDpLbԙ/|а*+ϭS©\2Вxiߞ4lK%k+:lؐ# 4$mE|pADc[&>AV;Yl6X6w7Por辗 &#IH*~0^2&M%MƂ9<gsg3OP'q<~q      Aa?tbh'phB@@@@@@@@@@@ډDj@@@@@@@@@@@@v"Q;( (-;@@@@@@@@@@@@AN@@@@@@@@@@@: 8:JKh'phB@@@@@@@@@@@ډDj@@@@@@@@@@@@v" gңI K[ӽBT$jcхsûVv/UD%̇ ˥,\'3uZ"7q O.}pȽ@OQ}{xPj0MuWxûی۪&ˠђ$M$5B9ɣeRFuU(eDX]û[ۃY;Y4U:>udƘzr eE ]ڐ#ЩIIk֎\0_kngGFgfMVCSz?m]U&OKa#9kycMy$#3 %22_JTɁ5WR6I4S{H>Qd3]Y]M+VKw~0q}%V2mFroCOo/wi~j'+ yu_0%R8D!u@R3~n (ydC$fd054ynjL)co)HSny˯ddLLqz+j}BgČ*g#s&g)~;>5ٸ9vw=`0k-N>N?Y8l%Wb*R|_|G:>^~GQ]Kf:9ŧ#[;7 !Wi? ÝBW㽃TGf۶]J? B" :>X+^&vlP=S/a!~9]k(8t\I'w˺wCC5pYhȨ;"8еǮ>䥳lw?}N ɿPmK[vӃ~N,\{U/l.svq>,%?=w99wĄYÙ6mԺ_ {9hyfqw;w AgD*Q`}.9U?'NxC68O*!\r2CCQT;xoKS/ƭ/gݡ @@@@@@ppxmf$MѣX-eO%;ڃˎVIlkŴd?3XWRDJOεZ65=]t' ,/K)e+wƢ-V)ofvw- .f^UN\Mvq&ޖ1RCN=B.Yܢ;O@'P^gO;@rYDa6ۧoU΂%ckb/-Jnƚ}hEM>^Y\):؇UNaxcbR+mKP6Ag2&gGYv{|&{:Niq=3䛂nmXB83;_n)5!rvfҥ y{<d?r 9 1Zj 6<Ʋ`O_:ϳ,~&\:\}88\'=k*裞ʓp'%~,_&`qڕ_8U<ٽf픐pZ+Up2r8'~{?n$eDcR-;ipp2p5yt=Me?*#DSa 7> 8]B!dG$ukO+=}6ͫ~ ;[KxA'{Lv\iʊGw.]= 5Ξ=Uyĉ WZQ&:zjFvL2> 9Ш/kԐd=NUϷ8.kwͧd.K}*'_NWJ'J󲿐$}_>n Zd IXGGSwپU\~t/dN% krjˢddd-qٷPP1n $ոw 6Tt E%-'2p[T O7Dp^PMWzZum醙dtPCm*$|]89=-]FG%UT}r8 >[XIs#a}7]~n㒟98"ۮ$S&'DžuuK-3h+~浝"lLͬ=o ]z)۫eSch*M{H9a'E++~>בs,W6)%ҙ-kudۧvS>?Y)[9Vœ~lu]%"߽էs*wԐZ;,bW)B\H.T4(L2aXIOR>5H zzPF%AM'gLL`r!ΥoiMF˼T s.JVYmW_[xˠھLzKT\%Wj/stYnEw޲GH7?DEMq&߳Ԍw+&NQiGGvzIK_{z%WF5:~^J̦{*\BdL`*9$yT?ɛzíQpjr\Wɞp."%JΜj>kd {A_,hR%QJ#ey]甍2e+!a$P+4l`96:rxiJ͌\>}z2Z^5 1h:TwC)=l ˓#[_fO|z^eM7fOg:)[[F3Gec-?D(~uݸ-^m2Y3kL>ݤQ6[7 XvUOo./SuQp6Eǚv+.1{њҐw ֐#oǻ~f_yvpoΙ[CHIBBQ 4 ssIs#%5Eb4|&¬'֓E@Hǧ&M\A8WBBH_{FQ꼞o !AM^bx`ퟬrQ~܌~'W4oh-Y@'=k~> pithu5NABRrt*E&w@u rp       y 0D%7N~֝Y9dfѩ"#yTi&udXJWՅN[}yow*̍&1i+aRZk䲚ýۋȋ~&Ta;rP.L9D zpI׌ؔ@)pU [>_K8W=uߡTޱ49ǫe1e'7^ZV9g$q+Py!@@@@@@8ѿ>0 Ԩ"'|'F\ؿA7 8$S.($9]2wY*vo3X\F'&2OʿJ9϶L@IDATokO],Bf7BbҙU7L!;ۆK޿ ]6D|_oi4HgU!T?4 ȩ;4AJuDqʜ0Ks>B.N5P'~/gy]Y'm=]5K [q7Gʭ9Սm\6w"nb=+~hH~aũ(]1wuJ %O}VjQsr/ƭ0%@g"Yf.<s.kL\Mζaʤ*-R7oc%BD9:O"?BI _,qRD'KkK۬ݵK6ss0$4M5y;ۈKewe˸Ȕ7s Yr?h{+n.~[#zy'"PqfGl~pK8mdNTto_DkkVl=aUNTKOi}Or²ꓺj|y5A츴h$Dn:r{Ƅ *H_ތw5!ގ[ẳ..w@@@@@@:D=#QCΟ+lsJ7?,ȃYK3H!c{[8\2!K/rhnIӺ3sCQH킜B$g]Huh*"ckS%S: P %AD7gu! ߖY(feGej223ȅ3S#P M&ɲ^\{j6\&M"of]֬|rs@s8#p(y|Z&u8aE|@n!e$_sKM b ?iAr6z.9 B~bT4rs^Z%3^NJ>j2zɎȩ|k[!U%w (0@@@@@@Mx\ !$Vpߍh˭HUS4]v$oώU-қb"g`7k\ٳdad!afnzOG2tD.JUuӗ9Ң%M3 4LO iT+<f];HNUN)bmä'E{5g`(*669jdh0w-Wop6KF4H ի܆6NV.$K#b9]_:3cƄquN!2*ql?N7˿6\#q>7ދ|Xv Hڲo6(pH>²Fa!էs*.i-J|> r?Y5a{.R6c=5"*wn4%W *ḧ́e=kBv'gC֎Kj _Oq*'hd0q2.      7#Dm 汳g=^eS0YXѓ_s&31eGFs夀,H\_eSAJuqgלXqTGh4']Bt2udD값_lڒ_M $Pܣ{#ҝ*jdgg^)x"'!m\_s;eߡ;+P :^\I"e !֧&/iY3X)o:2lwӧ۷~־9w7Por辗 EQ~>Y'xː3_< A|ܲ.poAtsj            mN@@@@@@@@@@@@@CC5'Q3P Pc@@@@@@@@@@@@ A@@@@@@@@@@@:8:Tsh{p=chE0@@@@@@@@@@@ڞDm@@@@@@@@@@@@CC5'Q3P Pc@@@@@@@@@@@@ A@@@@@@@@@@@:h$II)%$ىåw뭌ET*e}9] yĵII+0*t;ny…'3ylP^ʨ Hu5|ux~˞Oz/>udƘzrQo*+*l8҆Cndb+N3lr``Ѩ7W^̬<.Ii?&|6Zze|QSaE7V}G}Օˇ Gvo&! 3K*NVU'Hcj{ڔ%+掎F ~2R[yǦq]޶$ə]Kېojɓ@?]Qe-ig%2|w'ysaom/Fܩ2)(V}r|z'v}Zε#{鼜l=V.R$fkՎZ %jO^T3MXI/eLf鸙1"_eq9l9J-ۏ1>~/rti6G|/iͅU9é&o~v}k˔w*P[nU㖏i     Rr;cunXy2f$MjӣBN!u~F\ Eb"v`ȫO@z[^ 9}P B-ZE;F/KYVfU|_O;"f0GWخZ%&5++ZmY _eEqgJfM}hyh>r5Ѧv%N%;ڃ!?7#Z?ɵx`e;ǭp)`>A@@@@: Dd?r|63\D=^Z(7\z6vgh.bM 6<ƲLO_:I\{"OIQpku6k%裞ʛ5K-&d*pىf픐pZԕ*d4%W=-ٲaV7Nez6tm~BLYkc"ĹsC7m)p̦T ϋt2?{gݜM\[>zs KK2܀gCC53䛂&FlV      x|vMPWBȗo.Ҳ#φر#hG6k+OMp0ڌ7 1udo[ +N֯Lئ/4϶QAhqDL fel@}m7me]/`;Y JHu geו%Ӭ,~=[WD;b`8o }/"Ⱥ3Wp%D;;39rq͞y!q9(>{.uͪrmwʩ16;>~UMڛ)ۯ!+N+24nq O @g!pCDM&`*[S=$q=qqq]4wv:nrEkc>vEIpq|n!8ȱ#1~igGZ6{Lt h,;ĴJ`*y$:b r>ο'IQMAK}\BtzV8q|mN973!Sci:76#+smsk>~/r\7R@,̩,`"%̶ܔ*brs^ZU9׃ }KKUY1sC{/o3X6J﮵%|{-@S{*Nǭ[m%p@@@@@3p;ܦLZI:[MWIKrxkHU={ĉWO9DO=5#;&gY 6MWkbO}C HמV瑾\rkͻc=ceJSVl?:[whp֭qI[˩! 'NUx U-!v#Bn\U=/+KKs\s#^,,nl`گDKk\ 7QAuYOud^|t9}eɫb\<[neǎ2"ielԵqg/9/}X=sCknZ]ٸml     x#&ڼVIɁ1ܖ/'~uSkY@g-șOۏ XȹE'xȥ0g/;"Kq23땲Ő0(6qdSܹOBk?(G\rL4|rY)Ʋcu}ªr.˗Kbر#ˆ͚_u) V ]\]$h҈>DhpyOBNt!'4n]2% O>ۑ     ;($@ DBW%,WeÃk ῰ݬae#M@a=(Q9$drOf歞L~L~]O=O}[B<6%u㼪5%ywA6.^Y+=p>YeW6ۻ' &d zFYDblyVVV63+),dn{R8Vc/dp9Q̈́/&JL죙F=&M3yL`@bpno >}uUU8PR?sO [5Qz$N/E?p!]|v0rӨ N02|L>N%lbÏ|o!$eΫI B3'\ОE3wwƺ85Vfkbi[:*ˮhS>Pivr!T݅<nK@DY쩷%;m wpEW:*M 2mn,% P[D"۽z|nWr3/ɥVYs d.N q5lEYOIo "oSaܝusr($" O6#Lu]RNA.%-L{\P;.GrNj۷luɥ,+kmI? $jj1a\z+ow)@8@@@@@Pm?"ɪgLY+xWO/B"忸X_fMlScQ,6jx$S ?\ٗeuR |Vʵic)Nf il/uz2GR)?"/p\+EKwJp3"5Ph̟=}z+:ꃜTv[/'*ɟW\U6v i>#?JQ.2g9]^VSNp.@w$Q\6~@w>yƧ/< n44NcgǾ2K]f+aX D&v~Z^`Rv12YYn$` }J2^w_NSt}w0]p{GMI#lJK9f5}Gm Z= N 4$ĵgeϼxLt3D Y,     mb^[^1УKk +kkp~PŰxmS2=[=y=.ˑό9XY5>CoتNPzdFf?hXƌwLxeqLehvq/s+ՓRSO|ǣ:{ҠcF' M{S<[Dbo !1%ބ]K)[!2݈/-NtC4Z׬gM3f7v#\E 3vuGyڒxW_=78$v5kE: r=;se#5]3hq̈́Pԗk'#hto¿;TaiCj&N`gu+&_*46VNY1E>S ش[O< kC\o}ew D7"`)Zo8 @W& ssbʞF ^R_i=ۖMEA:Zh&`b9s2 NOJ:z=u;%<<'4KLQ׺Gފhl6 $&zj45Y#BeDu,CY,MSGxb2Ѹ re<}Ը.r.*|-GF=BMQ-jj`'\w)n=RA@@@@/n1@}rz]$X            I $]            ]S`D"Iy@$.           $HE             ` :&@$ `(t7tA NI            I $]            ]S`D"Iy@$.           $vF6l*XaaqYlQ_^04.ظ8ZzpVZ^7eg3!PƒB$Yr MÅ?&LZ7!qibtLVWuZ7m/+4L}43]{!.FcmU~yqKnV)1+]lTi,ќKл7o6֝nOZ^^*R5RD I?M5<=AoڛjN~kMkv*.D}Ը ܷF; MMhK6K$3>)풋g%  U_ɫGFm{ vbw1 v{$YYN0dv}vRɉˁ8j*xRɉuևN/tgPAN'5Sm}T;]Nm?/v ^RjyM9<<~<GQ9兢C[70OÎD+ew+uN5ui[o85]mbG)&iҹ)m(zfB@g1>{dCt(X9}qE+M:O5lϒ<9X< Rs6h2Ӕ蓅sYrU"=T9Q5=<єAf) Ra)҉H5U˜Þ߹&1خUחPȌkjoU4N/(k9۳j: ׊KmZzz޺XR4D0ډY%U>P ;1wqxdPPĭ\vRANR#Sm}T;]Nm?/v46%{ k[hfW?,\m泏k|k—^5ou֯qعF83/ser6~8CJ]Ylj}1>+Wo}vF_\Φ\f/PP7|SUQ?^g6gϷ tZQ=wTF\ºS7yR{bξUI^'ƄXS,3>Z{^Dc~>fSj7>x?o5N~}*#UNw:]hZPNoySsnPy OEn۪+>j\S儒ئ/Od[+j*H 3h2 vGq4% T}Q-GGbD}׊M'N4UW7t޽ccz pU~W )D$2y]]ΝG.? *KUN>ȉ[jdPyjg˩E.H@ 9=xzdоog]:]\5\.V˫~ᖠܒT9w g6.ulT~z.dY톛.H?ܼ69IGkP^㜧3&ԕxM{7we=8$ػ0]xT% ]ǥ5UYAɉ[~>X98{;֬ly^Z|F]⓮|-^%nQ(od.C' 6p yKN`/?ڵzPjR TSZ6|nT@vJʅGjIR~P[ ?ȁ@O%IBzߞʍ(npP\34|3`jOKֿq!ﬔގU*ϔņ:b`Z>vѱYqcYJζq@Y_V#-2;>ڶ^22d=q?S&l4{p\ߛwy%5UeŹ*blٮ4_=23x|k.RIl\v׿x5e4^syU_T˅[\p6v=s鮴G'|5 fTdKeX'g̸"N0Qev&ZOܦylP<@;'#1HXuFU&*)uNF9̝ >j\S)Zog4[hRbZkH.ItK=/]y^6lؐ'cCQ>]\pΚ;n U_˅WvϺY6d2.| ֊ T9Bi'T1Ӕ]]N.? *KUN>ȉ[jdPyjg˩E.K `gZH2=jWNy~ӆQJܺuk[ O>YA_sv H rWh^*',>0ª4-]Cs}hq7;f>t#+(ۉԙ ԮԲήq}~xG UN܊ȥm'U-52P;])T.T9q+"T}ȠT[hSjv]> =2\hf҅:i1eaH|MC_|g=YmbH0}I~FRpMמwfv1$p??0{ KvkLI>B>j\S弋/j\G>wM>p|i\E_rC(m֤5ywbYk=)B'<}HrLzMmn Mg kԼ`"sW Ƨveo/.%qsb~Whh/T߱cPu_Q&n=rsK4_7ݡC/ܕ?Ӻ\疽f5z}L:8sn[3!oʫ/Pâ.C@->줒Oq.T.<ŭJ-_Ϡꃜ8Oj*vF~P[_OQ9!ԟAdZ^_2^@I!{䏷~73q9>D!o=cRü + kVOutbcZ v9z6$h7=O|Pro+W]aj?7mq컊V᧫u|~LJT@4{x:v΂3.feٿ }JZ_(^XqMÎ!lNu2ͅ% BcI|rڤ ŧRMx+x(w-oq̬qO]΁2z 㱗%^:;_Ǐ_9Yv~^_cI }l[]{%mn'$}OR>PIN?rr}z T}T9fFG3@bUUNY~QQ pvRA/:Bb?*$&t}T;!a~!zWJ%aylJ6ryUqKk8K3:a6.^Y+=p>YeW6ۻ' &d zFYDbyfc%mOj?3jl?d9hT8){'51oږbm߼qV{_阅G!Ăa}2T9\ND5 |(1fړ4|0d"Bw؅9#{bٔj^6eQ55-Ói$zrv?T] _-vtk;t |OYT}.DGnar\ro]U%utL,WR̶,n9\nrM_x`<Kk'5ev}v;S<2\rVD.Um; '^oA婶>.S|TAz0geޚ|4m<&!=Mٖ0@ooBV98_ jZXWrwYYkO6߼ ަ=%d8&;QJ>aV)~S ۱zc˴4>Bo >+q-UΘPsEZ'Ow{>fT/Ӈz@IDAT;~gdڥЎg(/8;^Τ.Wjhr+X0A/,kiDv$mf:K:Gm':X]傝Nr}}~xG UN܊ȥm'U-52-#Zlرb^=|FkK+cFJ>{Hl_ͮ)*Kй/8ƲXշ:ʹr_O˲:GR |F7LZv1FPd{kf#Q"rV]F+as ٵ1ÉHKw}%Ⱥg@!~͟=}z+|yaTYT}.'%bQd.>}__7{D[ŬO kT EOEM v :N.? *KUN>ȉ[jdPyjg˩E.K@4ƹlʥw>yƧ/5 44NeǾ2K]~%aX D&v~Z^`RvwƁӬ,7gc0+]֎ }f."0SǡyrB_ts/, N+u8|M!>|v0k'pYD1OB\+ϥu`9יZQ=t6b;"5>#.Í;Jf4/ko>p=9d{sv9oJ{QY鬟cb#ݗ|Beij;BgvB!]5q}~d}ɍl6vfvߪ.M?p/ ScICRcj>~qK.܍BjA*ޘw_]֤4~\SނYT9K ` V}@Tu8qM+Gki얼|rK_}b|%_m?x1A*9ծ9Vxf-0nM/DzFkn-'wtrʝa̚˞y+'Y̬x2R`N( gv}ҮR'xJHj鐒FGb(egvF5\|N"TT9_?HWJ9~WR@O% tޗJ/|ڒĴ4]\^gWXYc]2,kko,Z%,yCtY~f̌lϪzVu'[N72{Ab5fuͱʶƅ̥VOJMm?zZJ+IkG44YD>cny%Ƿ/ACbK I\SBd]_6/Z2tIi:sYqg$ ;7K4nq[z8LΕWtzibwnwYwzG߷qWesvN/,mS7z fQWOƵC)I5rݟ0XZCXwՖqe1c/1#^wm|.u81(¿;g/+m]Y۫&fSvmͣ-1 R Q-GG4=pӹ|?5߱Ŷ5Ejonۿsg}b#^[\gD/mA@-, y}MD7h!!q)[_qMFdcSv{ln}uҪb*/>풇9ѽ)+^wޝ*,-Wwh[̈́ \n R|) =f>jɦ#kE_7;'7ps[_NDƢ.Zp.+xOƋɻT}T9Ɇ)tbvF~T;]T.wTT9wT[l9@$ISM٨#tCk[+>}۲(BBRXΜLyStzNkte]N39ω4Sz>)j!i1 ^MMwwE/[Tis47^5<6۫6ۏѲ"/4S.ڜy~qKԑ;4F]e2RQ*粒?M;l`BL ekؗ[Wl3po >('>)S.zDqAw NŔoAN I%'.\I%'.]x[:Z>!gT%T}sT/~'Ui)Sy-o9R@ t"o            ܇dA@@@@@@@@@@@> u}BPʼn@@@@@@@@@@@@Q,E `HQ @@@@@@@@@@@> u}BPʼn@@@@@@@@@@@@Q,E `HQ @@@@@@@@@@@> u}BPʼn@@@@@@@@@@@@Q,E `HQ @@@@@@@@@@@>M4X1gS ggb'q4uң򢈸); 5%AΒcgl.1Afպ KcƆOкi{]Qa]1 q1keˋ[ vKRO#6J̐-Ih]~!٠ `KH-wn=|8=i'V{Sͩ?ximNeڅj1<#]n+h<D;vIycb~lg1ӈύY~KXS{w|jr?k+d[hf'5wݷeMq,xq3Sb[|%SEZ$Ki~bԓ}3Uܫۏr }OrC[70OÎD+ew+uN5ui[o85]mbG)&iҹ)m(zfB@g1>{dCt(X9}qE+M2ϬlϪ.Ŷd}9|aӻ:V]/[!^;NÖ)wflSX`:%hqL ۧsfp5䪲\#CSv=U=oN{+THOL p?ae ͻBz3[=ipwbK2i;y{*..UUNJqW Y|f̌G˅.)U"Γ1u=xmsc'/Hm_5S[hb?~^A%װ=K:e/A<`D4tT9K\\JxY&d?^?|`ږ7_i<*v:aj7}\[;yͥ~05% 8y+Rb:bkXmVY)z~c>o˵ӥ7Bp6-2w}aÌظ>?{UӊȞx龣24/ڼn/@[teJ:Ge>1&ĊLbPDVd_|{2 &r7{y$ZU㿟\U7G \xvAku?_nq>@tk9A8OAM3>q?}ok|$Ƶr GFy\Srb/{m=sL翆S_~uC>t/Vh:qܦ[7hR'z$Rm抚ZG|5lcsCUIGWܨ悾 y뺆5xޮ]Wg(~uD>DNS@s4#^M o=rCᴃ*K Շdt/"@}n3N|Q#Z>Qv9`y3R1>/I !"JDCQq=Ψ + gYLOڵ[ogOi-.;9F>Ppf{Ae|FE":se;qx++nY[Ԟ<4;h\ "Eg;s4̥۰CR5R[ިiy>ݱ?iL6Ac;n{|/z &oY3k/+Bg6/ӄ2e({X ۻ?{ h`>g6.ulT~zvp{V @W~&hqcꖏ"II/U_1pkQ(~i_*_Ξ9߈dKٕJn;#2^umZtiٽ}G,Y)e=!!-sՓD":ɫn''zءӣNO&;T+d'-(x|6T]R>zw#%1 =7.jrT?Di1b 9(hIr b/:ZJ]ܿL얱ÄUolmNrvERy e.C' 6h:ZclPr0,?ٽk٠2e GMsj^'Zؾ! Ƶr|n'ଣ('T}.ITu mCGn1;v[/?ڵzPjzOZ:rұ{ٜԪჅiλ=e B %§a''U.d@G `FdEe]ֱ_R8&;"v)2OuL{–sejFj}.rG-t gRANpƿn?g;BwIYC8?6;Dh3);G~ N*G'~9I `_Sqe͘ t4 8 nKENiLYBY)-5@MT) uĞ|+ }cL<Ʋ"s.vt>έ}YLDyuceM]M2igHS]{$}ԸV[ʓj'J/,34^syU_˹(\_Sdoz䇌Z2KxZHNqM_TT9*5>vz:WNߒZXєyR]] ]ӳ}n,HI{n/EBjG3~)Gc8Nζq@_N^/)kHVT9nd|$I9@TV.@I_2 Lдd};Xt6)EN2 袧Fa7pLymz~vM /*t,VXP\WU]ʹs"1nV\ɢ͕+}Z ݿAgdjѤDbZUldsϯ:ecVOM|w6&YgrAPQZm9'w;ʓ+o]kÆ dl5o:KYsvHMCp3 ֊F(G7=`^±ǥF/nNT,RT}T<>#tǽgO>YPgZQ``Wkׇ3`x!雁 S|<7R{w=jk>r>{{ElF7aC晛l |E{>.󖓾G*qF-tQΔXP2*9TcS㧗gu[|O1ӳ)?O&мTtOY4-]Cs}h3l`wJdk/~oJo:FY\MMb#[!kPl08iKPʈ9ኅqCGoe &jZVot,# = C5U፮/n8fMj=x;;Gm6U_E(Rׇ\vtᙙ}#L.aU둁]d 煅 :8TT9j٩rjϘ.@cʾhkkɦlU6rlN穑vv)X}zn,zS)>Kwtmj<r>{Dd&dC1'I^Tc<[-gۑ%qT]j6Ys R$E{QG9'+}1w ˫Pi ޑp<.eaX0Ek?Tn_k(}yYR'X;Wt^jkb>϶a%`q%cX.NA;>Rn6^n֊"1Xim+ojp.eaɇoλw^`G@b>w=f;.hcmef_`%5>P._@Qr jvJ+?010u˱@2cUl=418+u9Y !XRx?`_ȸw[sp[u]cQ\<\NG>q摣fo)&i dWBC IIm_$-n)&sw wU86EgHD#+ +ZR<^ǺjKv*.IoAT.jrv9~0,3F;y'ʔևgI~w2疓A*qF ^6۔.tN) Dͬoj-Vmss-6.e@Z|z!%_ʊ;i2No' z|s6E~RSlUPB oеw \'^s1_گKi]M +"5X/&_ ۀW U~X%ӵ2s ).@!iK+::?\G/4s"[9V[hqMD9tt lϒlFg^ΒFi'3ae_? ϶,z]|vИ%ƈ.bZ_L Hׇ~8iGvvhcbYQ;pj=3ؕ_)8c7p"'}!2tc x@ykI)1RgwfﲞT]T9vI֕Em}T..GC 晇yOQ)n3ah;fudeQCw3%=_ iOv=Ψ!9Dخ%3ޒuz0X#3fiέq/?{zΤ˿Y#PL#]TWv̥wm;W ?+miCI:ZޣWܸdrL"m2F~twXsW^TӜMu3G[%KC\oj^0mY4KL*gf2-ʛ#QZm9/XN/$ta()K^{ՀۿBGیwl!SKTwmt} ѲFNm'MFsM냽f5z}csS?Y};ͅRͿM*{jK[bm:;[~uJIR.c*'4SeewcUXkD) ~/2w(`h:Xz~(|3':->ew] v_J(??vR8=|] iZ^_2^ɛ/om/N:{V l\bOH_MgLjr+ kVOfs&ͫ)ھt9oIߣO3 ͑~L|%sCDfy*qrugNol/Ty镻Cӡ qF-\<-SzzcSR𷑫>Ϋ[Z9 r%ywA0ZK=_~w8B6eW6ۻ'7 _2wZT,"U>"++jIa!sۓڏL5OJj|+S_X󭫪"3 ȞX6o?^XW˦,skլqyooKi$zrv0I;Q̈́[+&JL죙Fn3y`H>j\$g 'ΨH/8Rr͐tG.>A;o YT}.bEs{x}ˏ~%lbÏ|o!$eΫyvԺU/G~/ >T9Hֱ8ц!顒ʓ*\SQ*=L}7\~^Pv{%O͗]˞vQ\#w e\..œ|qQwy~(R$˧b|R{w G kT}kgs#{9j݀|zޙ'}g߰2e6Mo7"tgUdMZ[|Td~̥4/{Uxݙ(SO]jgЈl8@Şzk^YqV]m5h4ZxS.=odWQ-a@VφonaۯUKќiX0A/,[i]3.\"|C>y5Gsi3_/bɘ>YlK9ET'~Vk\Jߧ\k؎Ջ-%6Rf߷u'IPP5#*' xN{\P{ѓFں`PE$ׇ?9uoκRC0%q=AE"Y'=ױo='~ng*d''U*}lP=GtYzk3H3zID2niFvkKb$%]ߥ<7Wy:I6|rtxK9'E> LyMfkzq<&U.,/%+ K?MJK)R\\|b@LSmd?"ɪg2Z >ٵL YEqo2hl6IAFӗF _S^6%`Irٸ_+<8H JKt,%ۊ ӈ|%cuMˮ=}˲ )i&j3Wv1FPd{kfGV6#JuIrL=I {F MOy|CSc]gߵL{𥶎QQw`DRPj-,A&d25CgSkc5lyP4Ah}r<.FtƧݥѢEm}T..GS*~0K53n-}0}-ŧ_ #7]̗E|`~=W};%e.&GowKQ프x 8@c˦\|xg{>eq4q+q*?Ǥ1i'Z75L- }n2^wcVtP̀<]RY鬟cI#ݗ$XMVu + ?#q&}|gbOjC` $ĵxuOb`JKEwcbz݉brgw j`D@Q $qF-J {UXM6K:VN~fvp=5S[tNXz͞9s'3{/P$$S$q!ϔH,73ν֏Lb|̂G. y߳}g̞4}IX825n,4$U0ѻ"lV>ap}E_hmqn[?;~s MRZ)%vN|xQG 6ԸG5ՔEssڣ;b8ض{T^X=o2nMģvc,C唗zc\[9F̊J+#vp=HyN0򇼥O;%c3o]]quE=3Vx[ ;H~H|0iגkVL7fES.i7MG~.5ٹ?dὭ\q6%n9톛Mu/]{ S|9;^Sh2.,&s:f2+-].97Lptm~jK*_}~R`~;p Ij\QZm9^BO? Oݡ= LF<¼| "k{{ V 1|qʊ;]&UQܔ`r^r|ܙi->wz]vJww/F U rg3!=t R7j krMb;jD“1u.1:ܨP<Hn_._-+/N=r~yAf=Oh ~{/ޢ=W;~?L R"&Ɔ!Q,qF*Ǎ.񢶝SrS7$&5a`yޥWyѱחbtZ^e!>wi=a= 28$M'iރO%PͷZر}/T޲p$px&Ŵ))Yo-d]򹽮ig[_]1NF`kK,e)uP7妍87N׎lIq/sݡ:A78$ڽa+&_ќ) g׎?ˆ2[zm+H(]j}98de-uK>z7쓃].b9FN*OX!QQ$vL}~3ұ7ܷ;ռ>=1,}s~W1Ud&vۥPyD0F'9F}9wvV܊enPP#݆'r~Pk3yFYPԻrjOm}!x*{ڊgT;rH]yOM3T-=/St6j_$$ږJO߶l*( : ]Ό˙=opRVi5ß+)F691Yb6} hf`HZLbBWFSS>b)LJ4iʨabb'T5j/ش1#0(pCY,MSGxbu#'eƵr1&'}Ѿ%g4].|QGJTT9*p>vR,vI1(@ |κ+-'}.rpOG磎RQvB]/j אgT;rl=Ψ V R@@@@@@@@@@@@I@             =zQZ` B@#p@@@@@@@@@@@0@FD=(.`1           =zQ\b@@@@@@@@@@@z 0            "0 aGqA@@@@@@@@@@@Da0@            `9 !@@@@@@@@@@@@Qs8             B @#p@@@@@@@@@@@0@FD=(.`1           =zQ\b@@@@@@@@@@@z 0            "0 aGqA@@@@@@@@@@@Da0@            `9 !@@@@@@@@@@@@Qs8             B @#p@@@@@@@@@@@0@FE+D[,V v,`tF"D~oǤsL @ & n\ @ @ @@ @ @ @1(p @ @ @"7@ @ @ D @ @  @ @ @@L@ -ܸ @ @ @  @ @ @bQl%@ @ @Dn2IDAT @ @ b 7. @ @ @@ r @ @ @@[q  @ @ @ @ @ @K @ @ @ @ @ & n\ @ @ @@ @ @ @1(p @ @ @"7@ @ @ D @ @  @ @ @@L@ -ܸ @ @ @  @ @ @bQl%@ @ @Dn @ @ b 7. @ @ @@ r @ @ @@[q  @ @ @ @ @ @K @ @ @ @ @ & n\ @ @ @@ @ @ @1(p @ @ @"7@ @ @ D @ @  @ @ @@L@ -ܸ @ @ @  @ @ @bQl%@ @ @Dn @ @ b 7. @ @ @@ r @ @ @@[q  @ @ @ @ @ @K @ @ @ @ @ & n\ @ @ @@ @ @ @1(p @ @ @"7@ @ @ D @ @  @ @ @@L@ -ܸ @ @ @  @ @ @bQl%@ @ @Dn @ @ b 7. @ @ @@ r @ @ @@[q  @ @ @ @ @ @K @ @ @ @ @ & n\ @ @ @@ @ @ @1(p @ @ @"7@ @ @ D @ @  @ @ @@L@ -ܸ @ @ @  @ @ @bQl%@ @ @Dn @ @ b 7. @ @ @@ r @ @ @@[q  @ @ @ @ @ @K @ @ @ @ @ & n\ @ @ @@ @ @ @1(p @ @ @"7@ @ @ D @ @  @ @ @@L@ -ܸ @ @ @  @ @ @bQl%@ @ @Dn @ @ b 7. @ @ @@ r @ @ @@[q  @ @ @ @ @ @K @ @ @ @ @ & n\ @ @ @@ @ @ @1(p @ @ @"7@ @ @ D @ @  @ @ @@L@ -ܸ @ @ @  @ @ @bQl%@ @ @Dn @ @ b 7. @ @ @@ r @ @ @@[q  @ @ @ @ @ @K @ @ @ @ @ & n\ @ @ @@ @ @ @1(p @ @ @"7@ @ @ D @ @  @ @ @@L@ -ܸ @ @ @  @ @ @bQl%@ @ @Dn @ @ b 7. @ @ @@ r @ @ @@[q  @ @ @ @ @ @K @ @ @ @ @ & n\ @ @ @@ @ @ @1(p @ @ @"7@ @ @ D @ @  @ @ @@L@ -ܸ @ @ @  @ @ @bQl%@ @ @Dn @ @ b 7. @ @ @@ r @ @ @@[q  @ @ @ @ @ @K @ @ @ @ @ & n\ @ @ @@ @ @ @1(p @ @ @"7@ @ @ D @ @  @ @ @@L@ -ܸ @ @ @  @ @ @bQl%@ @ @Dn @ @ b 7. @ @ @@ r @ @ @@[q  @ @ @ @ @ @K @ @ @ @ @ & n\ @ @ @@ @ @ @1(p @ @ @"7@ @ @ D @ @  @ @ @@L@ -ܸ @ @ @  @ @ @bQl%@ @ @Dn @ @ b 7. @ @ @@ r @ @ @@[q  @ @ @ @ @ @K @ @ @ @ @ & n\ @ @ @@ @ @ @1(p @ @ @"7@ @ @ D @ @  @ @ @@L@ -ܸ @ @ @  @ @ @bQl%@ @ @Dn @ @ b 7. @ @ @@ r @ @ @@[q  @ @ @ @ @ @K @ @ @ @ @ & n\ @ @ @@ @ @ @1(p @ @ @"7@ @ @ D @ @  @ @ @@L@ -ܸ @ @ @  @ @ @bQl%@ @ @l ` @ @ @\62 @ @ @ഁ @ @ @@F~13A  @ @ @ @ @ @zmB DywW?  @ @ @ _>8w @ @ @KˌyG^VÇ @ @ p=nܑ_y?潙v @ @ @4orOQ/8qއy罛z @ @ @4o^80(CPk|VZ 6`ƍXz5}kQ"7ĹsxELLLpڵxᇱn:,[GN'nwp*DD+E"ZH!Ik>͚5kҪ(]s{=\|˗/7tUK ]_TH%"r^g rMi)pY+ (QDia|՜ٜ,at"hJKIBB _r2ˍ7cLg9fQB(>=oٳHOM6G#-=uɉVtͯEFbX$ SY(EQ~ѭn1jߟ>3eSo_w}҂(-BZY^s\F >-:FiYf,nJID"? /gBE "$0,Ѽز9?et6-|B*MHwtF09&Q4ʱҒ(gD*JH[$TIoHk݁T֞nsF:N>k|D ̬و444:aaٳFK%$mRALƍ1KHLMNT~m0t}Ev+ZK'uUWW1taI kb61hLoa+9Y(*33ۼ)-f#-s"R8/ D B-vও&²쪕.f  PKp{OC?9?'6 ''46Nv J^Gխϔ.-pwME ) I7M/D4727=|Ns!aa Lc ?vP/Ǒk,^nP([JpLIIkt_wuuٱhRc&rWfggnc9'?4#-0֩Bb2 rM'*(Fh^LhE#_asi5 (I`VF9^QNr^Dޢ(Jaγ b*j|go[?sOw&ڏB.v_{*uhf`6͹[Ӛ2,ѼI6Awo\i6D~-8|6_ӓaJO ui/BwaOXĺFK&-qlYas W秊_UeTBEa6/WYݭCmk;c V*"""; ܲa?j] 4AǔO2Uxs=8d-gxڤف܃S>؋~N@;U9+[s¨נ},<  ?y-P j:5љX8_ϻ7";yϋkh:WA%NF|DDD4f593>*t<|]hh!oN:a;StdAm/!2${ %0qy] #PW(4)gرh =n8vsgft )a`č\v.d:O?Wq+rΆm}B~K?J?ͥÙ#Y9xrʉv%athx¢< o>ϰ  #g-%yH2Sj>ؠs]Sܑj:Od` ں?fs“9;ħ߉M?s[߿͎ |DDD4Z]x1>=1~eD/t|i]hڃm~O?%|^﷌FZC;]ԂreϖV 8"mAa^,^ъ) q`sf$]/H=m1en]Vm)(,b9l}Y<&gFyh ݃b}a|_mQ߁ym|67r <j֯_{ќZza@ww|FEWR8588dz,԰-DDDD1,)E""""2ŰHwU<^h1,)E""""2ŰHDDDD"2ڵkѝD2SN3\fdP2,L1, 'rDw[JNDu!Rxo"]jU2gRxxG(Peg$ymo!-~===3a @~Q!rt[9WؐQ\IK)6DNSk?y_{UEnln/ V֨Zܙ$ .o1ϟZoEWcS~;֦Pq6$!{̝۬Xs"9RrQVYjY_^Cg oc%]#2nUEk׍HTEÓTjEZBd>EOjhJU/DƗԨ"[Z@e~Bo2myO*?|O;ʴD !ƨaљ #}_>tރ=C=(ڷz?֥`>v -'g܉qGr s]hpRR:[Уra[%daK4+C~E>NTxW$i~49S̩;Ee|=8ӫ~ٓ4]w֯aM?[rwFˡ Q%=W8WbjXxx 9O4XHpT@&'TU%Da*2BmP%4U$xhc.,W_ɒp+{Ɠk ]=noF MLxR dx'#ln7C}O-ܚ6 a,,1.+i$ ttchLSϢ[ͿmYR_E>%d#ŏ-u3~ahF{C3ſ]4mZ&S3_ܛ6mz:ObF+8fZ#YN!-mT튔H}=J{[B&e"-}cw!4Ƴ/:IG2Aɼ<҂P<iVE/֘4QڝNYF$wEP\U˯B`.@-ʴhvE>.^4?̽焧`/jq ߟmlOc7dB4\hbV۔M(R9R#2_"|x2P7w*]l>/7iɉ}lHؔ`CJA%j Z> Bc 94q fPd,C\\&i>c/`Wi)JCeW /;׾[мpx9]m@|0Έ95?̹igKGIA&\ ط}?avIf!)4ݦH1}O u3/y oGwnP)WE^41D ʹ$D @;Z{4廋PP{SІ3{tם|o<9IW!#p(Ωǖ5~ p웒 +^O<$-f7 II)HIIY WzS(z;ѩ6z.tb& +Q3+:IsC=Aty_ siws#3/yyQ=9Ґ%ϳ6&_d95 ;cYz{ ,/j0go؏ָgOUKH{g+18J -O%aW12I e3kY0zie#}Ažt=UxhOZC'ф]R7;fQHcɊeIi} CjTh=-8\Ո>YY8Vum[{2lˌ(H7/hS[6&j؊@{B- ˖JTVGcKC[i}h:)--8z5p)/+ۊ"[:СO=1,gKƓE(* \jř<0/cʏ%b@Aavm/1j"9^DYc?rىrRL*+&-֮eB sB[zUULs ?y̟c"KXrbi'V{IK MDD4cr/ sM9Fj yHw/ ^{DDDt7Kk\%g pT@inUL[M\OѬĂah6$(I7L oҍ-H7~ 嵹$!Y[;"lH ZiXI*rF|Tn@ߠ%,](xm1y9+KN1DDDD4c DDDDdʲDAIW:ӂv e;J@V^R{Bi q[V2Oq$[&vDfulv؇eQ\[ bOApbK kIC;RzmqZp|?q޲:fWwI::@;{L_/ݜ\fZӋGG/St1<.%Ů.nsUk꣺ģ;86z}osrKr2c:RM*5d"äP X޳_ӌ\"/G/ }%}$Nφѻm두GH616.=Pq0FOrzQ4v%Q;2 Q 틡MQVíhhS[-tu~ <C{KgHfF-kE%aw 7c;\@`S_g(j ,_ j(E7t?2Q;hd}qd"7ǁΦf S'9iTn@S(27Xf3bJ*2}c,>JHTE"=rYֿ̖ṴnG/2â֏]eseqVk:w_$w6%{VԷ.=j7^=.:11?_{/o={?5y}iCwئ$e*lgSUSa{߲+N}BZ;O}%ćkY Qp[ ϡ'`?ŧfWV /6)(,/wbǎ]8:غiw ]-Nd>pG⺵mgmW:8XPz7_vA،di~#6=[& z}qa/;f|{rwDUUMXcqh=܃.]w]'?ʕ+qa>G}W3$bGћXz{ ^cޢ.d+N}իU 7xMl/_§5׀w)~=Ix;'4A`g`2YֿU{wM@{;GNGϙ^ӌbŖ.JPWWڊ\Y(ݏN{[RGq{qdgY?x6#l|c0!F0~&'ե=1Qz8G>KBgNߞrqEqt\ٳry#D&&&⭷•+WURRR066w}^x]Xm_^T tx۶mGitc=Eq%ל*Wϫ g_/|.?6SԶ=0jJ+]@o.ӏaq|jcoNmXÇպt)PJbtK5ߖT?Z? ,쮫,w ca:'[Ym5ʂmrW2:8߃S]dgXI7Z~n2ۻn~ߒc~o&;YPy1aŊr݇~7AƍFp|W0<vorFs(2 鲫' %H<H @A|e mhob:&nf}xm$,^S|nkP:{x3OS^8gՇpa}P 4-X.ӏ,:P5GB 4kF~+[CFr2-UN4_MCyyV~$lTU"olj\(.Bۡ1ہc4j'Ņjn1w6##y%;6޶mr8o ̿?%ߙ%|SF[QYdz|p8͜%%%7Q@ggxڵkx~[?uU-ybG7Oea\gh(2&墤4 oMKLLۯ4AY<ۄU'_x]65k%ĸ*Dč{8 }‡w߸q/^cd䫏M=(p݆a\[qR'O Y'a>iL7N1tfx;RזO7 W/_^ke&1jBWG[ppO4Lj>41 /~IzeA/6|^]C#K// u%&-E/&)zz8-0~XqzwUUz]ΩXb-[q+ŪJԳvWs\݉š{^%ۑTrXoYVbx 3k^[U{6~UVW'Ŷ|Ʃ[n 7M===]W?P|;3E`۸7'ۗ87퓦Zu]h*x؍xG8gUo8FoOS?Fy{pv(y8 N ymnۗx5ʂX<޺:"'U=?$W$k\ W?~oI_[?G*Du߹ؐV\'ONsTD nOKE7t ˋj+رc| w6v4nŮ#񩍤o삖ɱ~|ʃ5W_6=[[Acj1OtQˌ6>TCji-?q"YOd?K..~!QBr Fi"f=y|ᇐ_3K eBBя~d7ڣM,AM}=j=O1opx%b_f}>ZCVm͗K,]i|`ڀ'?_ ?o;dcxg'Cma'hY r{ӧ'zXte =ӝSxzv;XZfm򠤺uuuȅۙȍtP|qlQab76a$"). pc ӿu.<;kؗx #'={Vκ#)lذhm^rъRl۶GЍϤ]f}XnD{vz|֏׏7[166.]TaQQ#؜).>T)a[H7br;xLJ0O[baVGY‘u5ؙposaZء~QJVwp!`>ލ|`Q?pxՆe6W2:צ~]1r ;?8FM!s55,}oj|Vˏ6Ύx8E5{Ʉs&'^YH˟5%1_y {gwF>Хs$D~;SO=eF B?(ˉ4ϟ7dn~?.dŁI9(̶8flۗq*WծO|& }RMdTR=~?6?j#SVA%ؘXng%GYtxk~?؁c¯s%G-W@ d!P+ch:ֈ[[R!ʝh(]݂X4p8V@QRRo|I-/iG9177hE|~[BRRg/o6x c];ň}{׾fN677ϓkOEIi><.4 :A)e}Y bX?g}RVbCWMX|%r#Xցݸz*#>ȏW;H3΢:vAEbZ S u-"\f9PHwP.ey,Z(!sGnݺK .PC8u BC. ^.]yZq?`^\uNL\qr;q1E6alh"%A1t(N.#Y#VX> CbժUFi;wn2(#%$k $+^B1oo_ƲOl¶&+4YF6~MDsjٙ|g!at-˙},X^ Ҳx}Vi1 #AR 9K:J״E BB(gZm9mjMeơCWϣXf˴嘸X+WSNJ6 D4[YHˡ=!Nb蹐F99銖aB_(\R#kz^vhnZ@rtC-#kDŽ,Я\ëƏwށkm At7`X$yj&~(-!prE!Q_Or(-Be:*:tw:qe+X+kԯj9\e\JTaa{Pn.y [#3, HDJ+.hb]+QqrfwUQEȅe(< <>% Jk- ҍ-2t ݽnס~ڄO: rϕpE,#7_KimhɵCJˡJ{wפQJPw'#dI1"tX~$¥8J0H"]6+jT\,KIR Ǹ6 > [#WHsBEMr¦i;֊[_䄷e+'paD//? IkN܈.AkPtδ2UK_f}O׾fOZhL3٣gP[h^fнeuzN24@ܛR5 ??? MGZ(u?U𦞘;m֗mo{sOH6[J}xUR^[He*9瑦^g7pK?Bd9~i ?ɁӳE7w}3acb! Ӂu~z$$7b\ (zR_mLO kc8CV,?#k;lc6yI[hhS[-)Gd5Qt<ځQd< c\?n]~FrٜL9sFt' _C=dsi&}M--򸸸_/GZ%?__e㚌 955qĿۿbJ:dRwn=OxhL.zR˷_ǵW¾?_S,4rѹGyEypnt&359#U1V|H^̚ṴnG/23;fіǍnY*)j |iqKI/vqo,={W[2w@u-jPWkmjlI+HGOs+Fd3^s{Z:T!%4,OP|&J$̨"6"m wq , 9Prv"^~eWl躈'~_QQNh?m̗^z2M([_uc6o:bzPfYl9]Gnn77`^%.]JJ$|?sޫp<޸B"zD׮Q!*=Lt*ͪD? Tź~ZL/N?xO=,\A.ZQzNof4tEw8?Н\VEօf4v~ $ hy)JKѬelb10c; <+Μ{*} ӎȲ.L;[gb|cgSm<,X[f<(3j+rvfv?rBO+a |jsɝ41=,^ÄlIř~ gC[ٌE__Lo|qrG+JmvA7C0?sdOc#F"¶8F[S2 ӄy( \.`162~2ҢËx]!BiPnwEIiu;v_Rz8\zaw! vZ9aԅ1OaO@3zL=&*:jA14}[v|e(r?xmrW2:,쮫pde X[|w< 1Ʒ\>\[)rl{c,4Nu9e)oO;3PэcGp K`5-,(@ˮ/ %H<H +ĸY01,⶗oYdE ?ae*êa}T\Ub=E XJ9RUrο[Yt`m 24_uz4ڊʊ;TŖTr'Juiy(ޚt ~ vX3ӛ2 w(;YmKm(ay/`4E_WQӇi,6<(.T?f44v`jxf pzXo5Hã|;ʛdVd0aOEIi><.4 :A߰~8RW*kcmkѦu]{0#h4*̆U9o#'R0r -m}ШŲYPl@o*(cWJ*R%e=uTno ՛J*2lXhQ?1_T~t, uE""%i_aX"Qsc?: DDDDdaLE"""%,Ѽ`X$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""S DDDDdaL1,)E""""2ŰHDDDD"bX$""""St]> ֓v+DDDDt' >4x<˂/زHDDDD"bX$""""S DDDDdgC-1qq$rc MDDDt>j`X$"""Z|>ϦeE"""%,R`5( E"""%.m6ی`X$""" H@cg"bX$""""S\gQHDDDDwHXâ|HDDDDK\(L"Y$""""S DDDDdaL?#AnFIENDB`python-can-4.5.0/doc/images/wireshark.png000066400000000000000000015002241472200326600203250ustar00rootroot00000000000000PNG  IHDREڧa pHYs  tIME0kٔ IDATx]y\LҤ"lMʚk+e'".ME7EB"r-2N)y>=>g{y'6nZhvX|ܝSgshh Erw03Ľ뾭cұL0w89E٭b& r搯P.rq-w0P: VPf˽j>|! dӷ-NFwX-U3F~'(ȣZ"u t PDM*l0? %+Tn뉻a=2yI v~E2 >?~jkIo{kTJ+Zw~\iNoXQS,VDNSօ=NY?AR~_reH .GZXwY;1xèWY QwG]B_fϼt+{MXt xpl~<}5yQfڞYl)/վ:xG͎ǹ Ht<հW ޓ!n~"LqRH,ڭuqPQ{mm=3}S̎|ow-.ZNSniUJ(﾿;RnKR^It3"'Qs^'J,}NP`5}i ;NP aSRemuNw׷n \'+6*(I [EV̫X>|s⦕}4%vqbՏ P qK>ư^($$ݻtDbZk7boRӴ"g*7N.#*ll:ZU|eE/xJ5N*rMϟe=~:Sz2)p`g*}[9Mk?9}tС]~:[52`9Q=!u?*Gԝq횯>E@Bh27=gmY75G}sNݜ%mf0G:v[䳡Y7tŪͺzY7mM#)*@7gr33 [HQzr}4o MDwm]q|IQHHHWѣ|.33CK&{~`ӽT*co#4ڣVD{zK=& >?E^a^s.}?J?odzw)j@NvdLPTlݺ7@ObJ.ZxْBnMTMIϟyhIi[=={VJIľ=b}Cw7{ivs]jbR~}s7oD92n"'Ě:%r pZtn8,VvvfI 0LqItY,Bb˶RWkD,/1BBiTR?$-VH7ߤ(xz@ urJD 7@r҉,zw?v-oX*Kfdbi˼֭ݽȏUue[bLl)KI]F&2ǷK9im}@%M2I+vC4cMc{V2 c#[VaMeb)0d0m>lL,em{$}ϔ'"hMd9Qd1qέ.lr?W­چ| @4MSv4G"9MuvVx- WS/JSA xyjsj % 4E)NjX!(޾Isi&!_)!ջ~R]w[s;rs0}WL2yvĻئXO$alds8ao= Ȟ%Lwwat:N<\E.6[̻p{iVC9] &sl b2| wO- Bkwߢ{ݺ9×vʒo"im'G*O5[-'70y<\k/RS] ;yHWxdmWMrAi[[ȴlSs̲d N{Vu 9}FߍNMO9:ϯN*dyՆt_^oaڹݲݻW>4QZz*l8D7vWP]k۱w n&c33? wN=o/٭;-Q2S5,+;3׎m4r1^0M'P~Ri|<"M2+xҥ}h_>?kjcB[@FG-;ayC\Fȟ]#z~]LiȟoNS狹X;taCU,}/kX,hs:u$Zл,gQ(w\RXpVq{T2`id*AU5 c)1f'S)Mv"zrt\i}B} YM]O[ cAF|oo4Ռe6S}mL, @3QnkIQv~ȑ:>6szQnrIRKAIvV< W\ ֑j cc%wVM-pC[o:Vn!+x\q⩲Y]|)N8|#k9w֏Ýis&OVlfT͠@()~yEJVf,/]<ԑ%O'2s-+w2ϑlja[iT.\Ew[.C0сC0kss:c9,ڸE os7b0&6:=&Xqz}?2NV{ ldG{X#F0J@q }n!RT2LRII Fnn Ӑ_tO>7pkb[;#zڻwv>o:GdF4p`R<767:WV=[_}|lxo%ZxԐL>>Ļfc %wگ㑿D-=i;rè kq[sМmj<#t>s>^DD$nL/wu{p-tzk͝db%hMf+Vl'+zJ}- -3L:hiĶhzXF{`R{t&Z4K&?/)ҫ}{_9,a"|y>+5D]agEXIRʝr{0)YSCS'6D!v'Ƶm%eݶk\R%O獝4%ubZ٫".!7CTC[A򌘼i`4}qP=.C>oٍq⩎O$9G-({ܩng|o.赥)}|l8C:ʹh)KXT}-N5DQo-J{MԩןC/җV.3AU¿Q  N%\|;ٶhy.Ltr_ns&"]65TaNv-#O86C^^^Ȕ/43yḭ}02t拤U!2T&n0̴OԐ mdbLldGZA&z9۪u~G_aRrXzm+dDa9vde՛X嫻 4Us*oRNM+)';-gEeTΊ>qE1bK/WAqe;ݵ+ N!Ek& n/a/ c?Cks 4N݆LO}IMC0Ux2l4aݣg÷~nN_Q eONM=oYRg'cn={RS~.4 :Cw>(}ЩNfC~shZ[Ϟ='S>w*~מ*ߩ4%O/.Nu30hvCQ2Q,B3 Vgrrr7C$@OXiafvcsHrYh)V#|]\tSJ'퉑ۜNw ?;5s[Ae?D:D:BK#*K"WVPFc:NlPy@iYQg\(nM>KdT*@hl@3E?|d` :?ݪkФ?}a"#X;=272E;.@   (2׎3EEF|d &b&H&|_b=n?V 3] mKˏ|#Ub~?UiH]OeG6eBcNUf θ6P!TtBn2(iS ]Kb!0 i\}cPsSm86S~gt;t<:%H:oʹ KIGqkjWi*(/O#wN[}VU w<F4@Зt~'슖~'}vvwZSNJwc'vB[/rҾ786R-ngrnCA87űQ0nq*kbN9 YQ tQܩ4LQ]leއqָ5ᔉ%w,D0!VH(!T}-xz\Sn yc+S14u5VQ7~=r@,L*+Zbf!F=aAsSS6 s5̶Zu5[NSk}WHS$nkwFո9v+q7po떿|5GԬYs]TlhGkS8BIuZ֗g>;LK9ziwQΊG}X%s)[UW ,|8*N9)kb>+bۭ̝ŝraoX#DZ[txߚ3-4Sa56->;@< '< n:=VgA7s&-#Kn MVt3gsjlN! yKNcΎ[/>szPviq *`ٷZuNC.T/Csrz9'C'#q7U 'cwW ՞n{ewOObPȋ|#$@^*Э-T>2]$YnyRTiG=%EsB8gaNR^)5e*ĂXVR4td9M!y绋׎ݲA[V=涘6/7HGuya"VrB^ ih/%ÊjdVBg'1{Y[|f괯},h`=j]B4&Q1Ek 9܍DK&~fաM5cܬ=կ"{'֙w z5>=B Y.p7[%';>c.$V?`jc0:j{U &D0஗eE:Z{ҦQK6HkPi@־gVpv59\_U._wa<$  tX hsBcoP[ea1 ZgT@N'ylZs9^rU|Uң wUfXY7l0kٺg6{!Evk]'I 1ì ֩Zx-¶gSctykMMt%M %HxZ҆ z=0MyS-& ٙm,ܩ6Ƶv!4N͏40֒xmzFp]5k]4f9MY cƽ_)29AylyڗFOPn5n&'aUiiIk4.*9?nk[;jԗCN_,*Yb@a- ٯӿ. T-HN-ů!.8}tL@|zO6e[@oFAˬ9'a3CBBܽ_踉~C*I[e`u3'&qǿ?T︟z1~=82ff+՚cS1`-'Aߜm""37l]E9*4M;u^Z7_W*Dnݺ#v>|z|Ƹ ݾYg̞5}Lx9ijqo4iw&{i??t@ x5Ir[w[to{e6"7g o$f&';s۶m[nd,[|2`٪ )Dȥ;f/mnԹ<={VJIľ=b}C7oDkkYHHI IW{pzCQq%40bPGFŁ-z"U~$^!OkcV=~\Nj~zkW j臨2CӤģ &ʮCС'O~HsR.@RAhdǎZ51/wl>pgv_Td.5Vh_"ED1mSw.j؟'17wSFLa4O'\}?P{DqM>^%-^] V̷+x+U3`xk-0|~J_?Q% ;-\V̷@T*en⛏0h^{i  tX-sxޠݖ^^I0@+ 4]00yZ0 `yKA 9}9mv_v!)|pJ]6?xz=9Mg_SzK}[j2bUZw!`V4\*UQ܏P#::|~ϹӢ;i#q{2>Z<w-jWDftzz5KUV(rZ|px ؀'[mW}tZɐVxm/8&ٺZ`_"s[%Skbrg\޾gttLv(1LVelCIG'*|ׂ|֤Y-EvNV&-@X:Do[ I3~LF]%˝Jn#YxD }V-Q Ӊ?9294haBn:P !"$5lTLU)N/dZ7 M"J4lb3}v+{9@N4H_wiIX5WRWqvxp w֩ 5jT*ҔJJV** * f]I5&`˲}.]FDw/]gk؝7JE.1FEf((1u"]\F;YRstuƥ.?+ ez2Ù|NPnM@!tk"=r>SJ;1xiOoX|_רөi5dDwpB>ʈ]Ο Ǿ>Mr㷐KM5FؐMn6μ-Dto'i!S'gu9wǩBd.m%^YTO,--WQ߾޴nϱeb?$M䲓30n՚3 ws_XH/.Bww-y«UwB"|Iwz'n3"&6haoF~,oJTlm4 L*P M٢X;v:bypkθhtӁ@rˣ4u/#˵z3NͽOYWtD_S.qرq6^$>GX'g2 g_ kq9H=7t2O }lyM MKdhY:zpTX*%pٞ WnOYqΈw+T?>}3xǿ[+ lRqx蛾;_;';{٦[wZ*rǗ ]V[[X~i+C7e}VnqXn ?fP)koO.p-r.);4.)% ߝ+$]}~89[$]Mh4MrcAr6E;ۺwڐqf>0Kխ_85!Ԥ߀/~/*};%vNKq9b^uWF&p1aq_B֓Gu,rM!#F }=}?4پC?lnLJ͎<>˾ݓ3kl]x9yQkMN9~DK2X~ۂԽby[,; #[ҖbtXsҞ2tmF&{L,}Jm.zŐ&qdbiFeb+as2RFyS&i%KENl29?MS;1 #K+楍} e!Kebif!~]ѺIN}C[QZen\zk^/m1AzYªASTi g&Y:r~ߥJj'{slQФ:7?wgMkD|DIu.VTI _z:6ޤC>~+h)"󛔸RX]q.N-ٱѹ 'v:ILk =2ºKz[HRn=rXay!m.ȟ|l* 6F?jPҫa`$) , =\ֱ.-\ؔ<{ώդ3 [ " mUu뮸=ʰQM!+)avM,Qؔ?t]<ySv[Y5ZI"c֍s `[$ӟ}I =g!|_ !HK;֏Ý:ҼTH -p.UFbP.-.lD6kL 6A* ƶ$=F|I_@ zx+nEEmQ|߭3sX-o OӰa幖]Dr<:4s,NG ˗Ğ*zLj s1_BX&}qWsLXJ @NyMuoznXOc`c J3̰-`K*5ly8m#b༇45@5T Ŀ|#skbI?wzgJ*xwcJۀ8#f_3c:|c;ԄBOQV,kI3kOj \ݙa.6jWK~Ms=*G}DNǶnEҕni[]vzcp 'ot*umd;R#C'FGG=[5r>I֚65}fQe˗גݳXCXރDT[gb>0sv@?UJYd{:h6Nbҁ%6@(` lԩQ;}^{KR,6Q"^tPf?mihVeXknqX˗䱓 =+pɹwr$N_aһ۳ϭ[%wjkդg/bZ0` \)mAk GӔ|%5j6Jy6XtȄE֫դ'ܾ߆oT.c~lK?EiJ0ؔ o ̇Vi[%ʝ= ΰ˷>J~0^>6i1}eAl|l;/%wJ`ua LtM5^I2FL/{'ξ}KW;ǝrW ;-w BuxA7Hw#:e_ߩAg7)ov|~6O;vظVîԚ=72^i `% ڨ꫟k{jֺhUr6j2C~{ DSdlsxy[K3 k_e?9Cv[KzMo[Njwz1kh!NN9)i?+ xßN\;tSj83iҬX*YW}Ίγd@.X58Jz1ѬՀZ ?j&b9?*Ų|k艑̝ ,x[żgS);%ErބF)'>*߳i˝Nwu!4rcԗpׄ$R"m踀vKNR2EFuI$2yԨގ5Bv =4Es-vvv"cc碾dLܧq: خcYQoYQ۳k?j4AR  ZN,z~,&nxx>m玑;Ft5fggox̓X'!J*N9)*  Vtve=iiX 9MyO7>J XVRAme/vvvO fsA:$8-NnH{KHXI6Up{?-y9I%8`7{> zr*IJ>{S/AOGG[Ay(5DrV IjpadbKXR,<.Kw_ڳT&RzTRC$nL,mhL,}ż:=bORX(o$dbisT݉Y&2dzijv'adb)ü~ża;d?vbL,gg0LBz&IߛۅͿ+022kF{,F6bi w=lآ<މLq'{;ў|HzBlS$͔?1UzϾ﹧Oָ1V6uaGwd IgG#~~tIo#KApgEqV>SKzJz-uψ6u0nǝ~B d?E IDATN8;-gEqR5sOrqnqFFU(N.)*iGOb'1SSVxӑo$ =U=Uw?淁oW4 RI' 6@~sUxkX»fVRZ۶>❢8NVlʓ_<1m [wa-oqURωwI ǝrcqwZ.̝J\$T-˗YXcYOY:,xS#^mc۰O`@/$gͬn$o uR<ܑ8/͚YS%r{ `[~I֦2n+e۪8PFIe o<{ώ>uT` d,"'qSnp 'e`!imRM &lm ?vpJ45.`;G=JQW94^!y?s '_ > bIןq:/w9ᄓo\ϯJ*|v#M]#mmݻ* J\) A צ])8jtsY+TfN݉k{cߩk/Mz\C:IZiJva=+x`_}r[=VkWϰI,¢ |mæX gj(u<>^a48ᄓ28Ca 9o4xOTLsDNǷX\VEGG]\N

a:( Jl.+ rݥPi;+M3Z*@GQVD,dԍ:HCN߳4(zPH q]'XwJlze=ӑYO4};mo޾M2;3#LמfgfyUzrDFFZ1f{VfFvfqWfguvfidO#QC2ToJ}gu1\GDSJ|\blo:J.ܰf6];[R070foR~՞j6/Į]]7|ʼ:ct5_)yNk9iz3gZn6XʺԷ)r)~k0iĩ\<쿎`+YF{q>r1yki(rIL` U2֮CCINHa`ӆUAFS 6񻓣ֆI+ Ԗ2Fʪi=v^鑛+xVz;c8U[>7WhrW΍@htKx3 ;5-Ȣq8F, X!<??3k't>z+{TR}^z} Wy [Eחk|,RYnNv2>?C_>5-Og& Q BP=rO9ࠐŅw}KrG+J-ڕ-,..^zT*566^huk i FU `ԨQA3g ii+ˇL>rJ}5wY:fkρ: "?K,r;ƕo!jp+{.J.ZVH'{Zr:bLfaaa~6pvWԑ+4ov"q2:'-clKKⰴ8l-G+S}jiL{+EM4r!OT )Ϻk+D/]t"W۷WK;մcBTR U_΋v*͞h {OjzQ_s3٩2>ĻeϩcJxs/7|$=V9O,&@?LX~^: p >1L"!OcJSGvy"x)FA?`+L.I^rEIR}g1\7> dSK dh`Q /aEbbkLsz.-EF< Z^.0Ys3nf~wӊ$ije׬fhUI>ƹ+$d8l-&%IZ\PPaczLD&Np};N`fH51m`0ZZ JLNKLBbQ![#IR>r`2lծ3dLL۠)I%jBYhz2)d!@rdBKq W(r)A(kpSȥr)+djk(RB+dhRӴi$O㋵ޮ?aqxi6?2kiܺcRJh7~+Oob <6H Ym9ŔMG@Cת5cN212#ˈB#uU o@C4 10濊msw^Ęlnua1tw!+㮽pW [s <0.yK^R\Tߙ G`Z;Fe]89m0+$BVyhB.΍qEQNW9%Wy,qE1ናrOCg  $.}KF Y8t-%rܶ$i Ր~W``+~*s .(9yipJ !.$" /'{=擢3ޏ ^ȾKl v)PɆGё'A _F Uc")<"9D+ՕP"BPsujaq鸻# <( ύ' "uv :F <{U%wVÄӀmU_ZNTyMIIz --^u'Yci}ukC__ڥ77qny?.D̹IEMהZl:Q:u[+8Ğsvgɒ:BF=mGd j3g%%NO'E7#+sbk6-5 4nXnr +OK-M tsȅ.z-  &DE}M0E+>aeޕp[ml>Ba16Uu;Xl_&D$>/ ITsU9y.3ս ] XH^{:į>JS*Jݗ-͵`XLu7lmOgz#t%'H];Ln^x {ly7_V#mU)'n)Ӌ]J~a;'(: :juľszlM\Wr4*J|&IW1`nso>4ܑ䯕CS]-P\FN{▂@,X7n 7{ԏ֤OpD[CCޚJ7]ɕԑf"L~RhuMiw*lm]xQFBǽOpZ (E7WIԖЩ}OˌPp٫ B!LJech%k oAxnEce_Q{{ivJJ*/¡Е}DŽǺ!m֗ضpt"<Ύ?aZG;'YZlOI~Z֩&yN5'=|(Fo R%!ڬ%_na._{0M σl'ߑ':pJ/|.l_jV\~6A \2kߎ\~~\vCDQ_|l !I.bEO" I-qAo^6v "ǭRB4.$i!?#Ad_hzQ_) z# Mpgۗ OAx?&=s8Wg._?p|.?s(klgX@;@Р nZ![t/Ҷ{s/E y}7*6;o/jםImZuC ~ƹ3hyo5ߨm]4]W`ãH 9+2r\M Oq"9!&,eGb XEj[Gnbh7!$ &ƩuZtʅxƕUj~aEv*_gLO54:mnJG7FemѠ-$_-:i0t$߃[_8: ')>w%<͏J9QLW3$(9.ֆ8I3nN㐃ChSwbR2S8DʳbBW%7?hwt9V0pDy (%i/ 'E)& f6mzRoE~c"ݔ!/u7wjXuwSW߀N4WM3X6~;H*đ<k]u(}E\@~M7 Ps7}p˹ ~ eeM;]UWXu!GV>N)OZGXXt^ mu7I$>GVMڬa7"밀6&B9|A&nn*[M1uwĩIm}-E/[~?| nzj\}QyeQ톝j4$~~ 5Fn.`u:GB7m=YWh$mu9 Qd/>VItx$ǟ4&iwIEi[Ӊ=;\xHw9Ϗ(av[J:F1W٧G$ƀiϟJzn1g*䙒 bOۡ{8Iw` IDAT @i`׹2DQN۷-Ui8MԄ/xL~{28>,QuF:@T.04$cnHo7bji2b,&#C4i(*xkQ[14?@-=m'*71 ™kQ,yATShͪov<$Β5Y3*еsgLFA˿]">Gt;49)- {EVFkƦ<譢:5Xۢ@tSZۢ񣴭ni=*0zD];-QI z?m͢QA8YV\[ݴѽõuMA]%+"w?H|C$Q%jP%c?hX% H(\@di||pw{8lslyU^JROҬVI&yߘjj~{ o M^rvS&BoM⯿I<-ƀaD5ɋmqv>xSWfLm ͐IuXmt-vǞG[-W7loѪ.$*I,-I2٧ek*{5dmٚOAzŪzAs ߛ^E@g;($w_ 5!KK'LP@i&nz}ĭ`"fGoϙH wR DR@ ?Gcɾ6Slp?`:(5%ϴZFq!0nzm>= t)iShm|b& Q_=FcO? Řps?z$O+7ɓ]Ifs7I^M,durY4u3䲨 $OwռIMM"IM4~Ewr&ydrĹN;Fzd mӻVmEg(*i_7ݗg^&Yq 2;_x}յ{eFS֙Oz[3yUu&Ju 7(H"d mm WF,³Pb?s:} ]Pk{i5m&lx'w볩-ZΜ$4]9`vr8S$l:8Ӑ9hz`|L}#$F@e?)&i)5F'OnіֶhnZh,g%I*෌%_W#yb(<2N)<0BMRbTTqJMH]PqWSZjW@uM/"s4}O-1l/ HT#΂2+R V`Ȅ:LgCfDO}HFU>K=mOJi9jTҕ dvHy]FPYv!'>*tzW:ݔֶhXmF7ֆIqQGlsE\(\!::;[pf!$ E\YP[Gm}SIx<-5Keua: C詑$3ޮ+,@OtF d"TfkдSKsĸlB5nN?C_|A !bwjiyOѧLE } !HҬ^Cw% xVB. 歅Mfr_$96\yNPIYDlGm5f/ # W$>L'$tYnSfڨuw lΥ\G܏+(=QH6*CFQ m9t&>|[._)/{Cd; y7&p♍jzQ_ωY / b"ҧ'7x˗U "r(-ЁG4.yO(wo_'h$IB$Nʃ>H>XMM1ʼ6M/ظ=IPj[MI3Gy[t%-0]i!Xb (@(pE/neQ%1Qy>`S~z _AA=$|1TB@{H^&Mq,P*jSװfC @vWl=ZFĐILZ$RoȜ]6LImѨݔnݵ-JzW&y2~ƦWʓ,t_}^g5{?*?9$4yGUiw`k?}$z,9" i[}]'BڦX 2sv^{zu8 l ׇG <:wuP[!z33(' ]@e>4ov"q2X/cܣ_Եj`60<\2%e}_kTB6@`C:u 0jI)s( y]ՏкM v74vis^;/eB<&@a.2 z@~ZՙFE˖Uz*h,t5w'ܼ=2IdN_+ʺe"K/9sW;yM19z19z=^c9 e&GѮM˃v7WmtxM3c,cY7aeb|uVظuVֳazT1ͼۡ<qVVu!> Oq/`7C\V&_C~tG-pAE6H㗇]z6@&qז<-n`݉z䏟 2182 c! bhŹ4*bzCyn*+4ܲ5}~c!3-^ `H:2$Ytwe"a`*! d<Gy7n|MF|[8`hve!)3y)},h,r"*f9~ Wgj6a-ٗڙD8ӆ z wbb?0X還_x} Ã?9dwiquh}[|ǞPLMB{]˥Ά>@]7J|l*4Z9~ۢQ]һҨY,[cl?ԻaiԾdG6aɔ_5LZu^ﯠ+jJU(b)} &8M9U;EZN#[vߏ8c@LL(AY~ = GlQ$Wi#4x[-[=n;!Š˿r깂D1st?2%- f-$fv sj}߃ąlB e+> \>eQ|o;pOf;{TyqN. oGe.{(s!P.F FԗF.+.["&Qq +zJh$  A&e2 1Ⱦ(s*34Ae+XRPDmG.$l4Mql_*<".4#6=J#H*.z-  &DE}M0E+>aep[ml>B:5jamL@$>/ ITsU9y.3ս1'Yzj*~T~r8Is758iL{+,7'G՗F.\eeE\$Mw* W5l)Nk5CbJ *{OjzQ_s3٩2>ĻeϩP6j+@ >L@*' } &,?)Goj9xQ|`ALohw_IFuѶhwS8TKS\Kf:Mr|zhTmom%! >tW ;NT6Pzڳ}e+ZtrzҒ[No9PQݾ^$V?w=7\U&JPClKomԸIShmG3@ 0}LǺ8 ǷZQW3/h2 M?n߲yCIv-D_I=9Ar?\,;d$>zni6zS[m4~|yZ4^IQ!)#%Ѿ^۶[7 Ʈ73G{ d±qז<-n`݉ze?9-e[C&ъsiTĚLTWh8ek>!Cry16g Z&Rhж$YtUۓg,cPd1 V%>>;~WitYzKt!?qyk JAssyφLFd~lOiT *mF+!?fWX?Ror[摉Q ;/~촍%@ry}qJQh/4]t)F\n1}ι%RG1BʳV'üMM9V[-۩^muXB&da6?"pղ~Q* "RgBh)Izj^8ڈk-S"d[03S_m6*=E^S7+Lkn9^W.?L lz"h|j p9^hBb*x2߄gڽ 24q6ت  `(V֙(R6.ߤF'7hl:8ӐldJ<K`O 2;_x}U ~fLQ8F4k4`glKT)L 5ֻ߬ET p;ku~|g ngk;qS<X+ҎSgEþtaW+,cs 2-?] _ɒ ^C2)e{-;IXv嫕Y9mikRޢkpdM Qs8^'/qwk D)J2H'*>Ia)`lO6 ^[Vv=ގ a&PMyS~m kmrV߄%W%s Dž@r)r0H/R[{OϪO}Ks|5@s!p 2\&ɤkM_=v(Υ 41A4170LKye"qqQgYqaqQIQgLgN0sn)fN NH{⣸(\&)*G,S 20G$+ew"Lٹ0eLW0,&y)3f!y~kysz.^ <._r&/d63kupg`Ǧ9;&7l6hkvlYۗĮ-V_1p3mHmG}L}_yzA^[-Xѿ02O2N>P@J0Hޗ0نƒxQclX\%թ;~#m1:'sd,4eHp &[QX5\ w-V-\w/ XZ,pGȥzޱU:YC=:Xr;R/gڼBޘbl>`q >3:V="HB]13*"qg><^K3[/Y]+p+tixd0ϣJ|^/ڏj1\(w,su@#dga(Ȕ5Fh-qwz:”O<~RHB9 K?s1+o/6zbgሤpd#Fh-92U<~q} ][zw#*䙮9QʧQB5@rjN+gL:y^vx֫YbB鹌I]n gDnl*e_.H!}2Ji $)lz3O_"=+͍%kҠ^#(Rmy8_NёJL,OkVB\hi֬vcw 4Yh1ַ8ӆŀn$X#;Ϟ;Xid҇peTzg C'KN>8B"a+6FE$͵fQg̙ R*5g2bd]@K`W2}N}yO\~|9!v[OKaHx˱]ПJU6YQX^öT4s񔽨A;G/;aA zjz111GuNDĦ +*O* rɴ"?h"I,b;DDRTZnuVUTXpvQT9~}/(|c Ruyږ%~_-r4qm= _VO5נiؐxvz $m]޻wpw6P߳l}}}BޥK*ܔeć\ \"}x'OJc|xW(.Dڵz,ݓz[ ql@/L-,&=lذB`q8njv6T.ŲAleLf}]j3Zs+qӚ4W+|lˈgaVWĎ|Oy}`ċM  j*>]"8pY^^^Hvʌ tzմqc5(Oxe!aWMۨ԰}΅6fUѧY5F4*0┮Y۶{W3SSP80r*ϋ' @Pt(WϞպ8pY,eYSJ}*-=+))ictbphٻ({ K"`IX~v#(v {MFE%[4Fc+lU"EUǝf r3pݙg=?,Yj!_F׻g3L֮5Vy<":ujʤ :y!n3?szܲkRgm+\\  `槚e"Nx񫖝ߑevKLL{bUu։?~G6Y,ˮY֭[$93[ct,#ڠpYy[ʢ4o@ &fL˖vZh3Bx yT7 ]my\qc)wݚ97o4֠i/_$?{\eE+ k9 ՔkJyOR>aa!cZF,Lq=R-ҿhQM)ՔJSmH$%Y~ 1~c&E<@aṯ'?0 2nFھԩ#`o|O=e0t/F}\< 0'Dbqrc0/RG_y$6C{//.RDHh ،3?g"`u3/ВE5Fܳ1{#TlegI5X3OfUR.RvփjeY"eYH,VsfܖU_ Z=gVU~ ҿg\.%6_z-q֬M{ 7n߹v'gO:hO&S1:m޾$ ,,KٙD,e@V86Yá\2zXY{p{[Gt+5LW 򎫔zXŋT?mݝM4gM]RGm. YIwZ a+m+׿$RiïF^ >L a˛J4c&R*)LխJlIJ\!J)o@E++b2.kxYFTcpR=ƽ/PYpO_ \ OuX\S"(>ΓTaF |JC5o丛r4-;X]2˲`}lO_0M˲!AӅ)?,&g5e*O-2qoA>jYʪ4e&3NI=ߒ5&v@ȋj2 >K8x8C(ѫ)Qb\,^5KCD?yɡWr&%^>Ň$ IDAT}gj"2x:!^gr-"U:gG =,ҍ4ؾ,e,ߗG߿}߾^fp׿y5c2(e2 e_g}Yw߻ԯ/I_#ؾw7,}\_~FY,336wa,2ڱoFmβ >z?N`FGz'͎roSXr}T/O̾ʪ4>N=j(ŞC͖S'Op>}{OHa @`7 -tZNϗQ}&fUt epGNdyT4{}g_WE6#ozikFӞSNi}:Ux:ʯ:~1_X3 kHzKQ:C%<WgtP~$ i&`tU6²N%}KduU\K~HwR,44dU)[1¶7e׉Fcu6+{^֮-]5'?B5sAI۷o78S.؉?~8g3'm3ҔB).)7M) l)6jx?zķth.qKow[{iHNڷ]K$YJyB6,4%s45n;Jb8o;߼%/%w2 >K8x]L m"΋!}~{?d$ 8q|{IJ|wp)M.#WSu@o4 2/xެ_d&zEbȇŭm}~wR\H.$} ZYa4RgA'.%#yA%}fm.ۅ:l*Eɑ Cf?kgwrG1"3)ӗ#}; 3JGr+SH ՔZ[Tm%]8Mi\\[]1duĤtGD A(ue.NEWS+A"ĤtGRGzl `yo!~t[d߁Mٛ,K Ymn/R҂0ȹoa![)Is)üy vdLq?EQk. r9 ;_UM*5`Lv9PZSWoW@.->2CPxӞɵxezAra Ts"?w7̲[(YHa)mpu^T[W8xS"7O}P!TYtu2<95v\{H)I}wxMXJl, 8ڻAꒋ)=Igw޺9˴NR?.rf6콠QC{>~-[?js-gʢ"qOV%#8).2S;`~SN{/KE{RkNQz WC;.UfeSy.OV {Jn+)WU\TrO\ԧl 1 S݁etgN"9uZ۹sseJjߌNˈ\_hwyѽMYbkete+}Z1r_1NTeQ9FqGqG~bTom6]Fe+}{ )ԧ4,gEO-'箆q@+a45}i#v]jN0-So أYz|ͮKփSẈ] *Wj{}siDdqS/ՈmtK=$sm8:dzx}֟sU>C$-ِ6kLN0칕lךWYeI:U2ǹVCOO[9s ӰE*Pr9 ;"pSJ}J regO-p)O))-(*PqqUrQ_v\Ϣ(aY~C/ၸr7ŕ#?8sۘU yq/×,A.4 >Z#ԆM]->ʋɏj<;O[S4<|Gf?'ƶW9)c=y|KcWpS=OA tᵠ|&>B12/n"obR:9S* o퓣b:me%K -L1}]6k{,l<c:`M_tyÀd6h6bMh))˩^;m#Ll "ww'i)D֮*Z)WN',!Q≾d]].YuMuUv?;Ւ.([.yO>ue}٫Ka1̘<|*IW`ZscO|wThLBl1?"\[@Z>2tR^\R^Oňmo7@:x"wU}JE9RUp=ߗi-qt|{{R'(nR$#M*zY[dF=jHdwG&38uI d* +hC `_JخmJ۠pZ{R][哋6hَrp^ll&D\trFP>\1Ҧz),|StC߀. @L̘-7>,&f2ǁhzlsשg#Gqzu^)WOQ|ࡋxRPin7Ђ< K 2S*vo6CnwhJrgF*ДB*ޖה(}*<ՔR#<:x=oO2<%b&T&~0qm/~ԧuoQoyXp3P{ѤD,K<ʠOҡm\IÂޟV o;߼%y;%ȩPB͈9Է+(|VYԔ#'*c/weHjYs#M)V?WsbNy F-@jX씉]*N_}|Ш)zQ Wω9Q9~^;sےs5Ͳj@f592}|.(w<ʠ@_N2z}kt"}ūC[u{$8Mؚ2 :T_K,J]ǯŜ;=h0ǔU^}_ PɵyUJyF 9^ϭJB/j}ނFض?v*^TqGz\$EBՔ*K Z8Mּ=<8xS=sf_ >jJ*?ZP$M5ƵׅO.M.%UP>k{o[]1duĤtGD du7/Qn]Fx7!2ϗ%jpbWוS|:iN]MiP3okTw%<Kv>+A"Ĥt!KM_A0%;:ƾFjJA5e厃RPMi1C#4mBHپ;PpdՆL&s{n;rҹRtYAT+ۥ꺰3Zt*G|੦jJA5eՔRMi1hJ)OY)%4ɵ42Eacyq\\ Jeyx6&+ElU'%b}.HIA\oB}5Nm1ķloF'ݭW)8>_f?'ƶW9gmy~M珞r.UW sq Y-\,0A|vgv~Γ=W[!ٗ+VuAZ9 ՜ˣna;n/'J]5 ֪(k|Or* UȫϸMɂ-Yibd|p`p[^ S@b\,^5KCD?ydF=jHdwG&38uI d* +hC"k|)aҶJsG)pbBkZ궫||r U=^pbˀCC]^ғmd ipѕC BTBsK^* OA>$11cZܰxԲD `ա]my\q )Rg9MAO4P۝n6^nJ -of ,%ޖlUOϢY>ɱgQP9'i#v&4# S y(=YM\eJeѧy!~y͑$x+lŸj]zCiZuɮY|ECrnso>l.$)?^9x)@5ł:umFvI P![ SpM";ܑVK ]WXvW=yg r:zj0%RglzNRo5r$+y,}yK@Ξ KgnЋABnLʈ+T̳̖)E5TSJAQV@5/|ѠiJ }jf]S fU^n1}Wd7V9R"fR7bK;O}J˭OyPl>Q}`"ql?^47eˀiض5ugWK<ąثQ+`Qq7-DG)丨bk>7J=yw 3W=+Scp9+7$N2a+oFw79lr껲/ ߞtFߗi~Ȼp<`ۓ".ZQheQC]͹O.tZfƝ;ؑ!h MɵAbҗ 8˫[}yU)RAzKp/"n'~#lJJ` A1Uɼ$mޜ8y۶4`U-ׯiQع+h[ R[ԚAUZǽJ K۳aG(SJ,2ggPRr'kYԔ*{Rc VK}+rԡ&ܧޮ"^]Z8}dҡҧ=k)=M>U}z/MsX ¬)M-/}ąU-PYvk~43qR"ftCLw3}0seQS*u*K r2ūC.7NjQZ-4f, pX CU%qV xQ 1?o{aO̶II&Zc/nMԱ߄)]_5n*ʌ6= 2ūCGLwGj5w1DJje\7gw2w<Ϛ<4?9yLyw~}o /ײstDz5Aw{#-d|9ȷ.~ ?ok5}#?r=uA(oKdd_2z-rFLTtV)XO5DRNI;jpʖMf^]WaȚ~u5GF4%E9x|)єΞ޸\k/W,XҔh]vi.Xr[ EA9ˢԶV< GH0hݸa^I-ܷ m:ߝ# `Y'W>c'75ik7IJAP `w| "U} &w0cF_P#}4Iݮ=<8xȤ2 AQ}J55n;JY8E(U E)ʢj;1|BDrʨnfLe(2rEM}V*47l\ߟg G)8EmQ ESEM9xM|ϸk#x)6l)=//S,jJb޵Xnsr]?z)$X\"zM^kȹ)/w3rD) ~҃a7*c-O}J (_S|AQ|J a ~%AAQ)āԊ`(8W`7$/qyݢkEYDQ5bf3 >v' H`*YϭJ[%:OID7"ĺYȹ)2]sL<箬_jJ){fnnnAd^F a#yDn2+r `l Xb'zu]>˧~Ք5vK5|W³_]KdɑN} ILJԐ,k=6(' `K>wtȳ}y["ljyk,5du.1Iy9=rXǠrR52̛`L,fQ)84jQRPE}J˫O)kYL> 厃>էzb@x|J;M:1/u/x֐O>DOf l&x@9x/^P>y9_ Z/ ?t3l =X%fπ |qryirR'{pۑΕ*4{z;YARlxi4f ?a'/)Sk)>e)>ņrx)ӛ2gQç )J'O)gC)5ՔRM)E]jJA5eh͢R)EQkJ)( TSj)=p7eCQR8ה憱] K6d"5Hڮ i fI \TʇlӨGZ ,R%mő $b]JsC)W4j\Q{YRk|)aҶJsG)pbєV-~z\1;,Ą3~'۾ G0]pSnGRM{ʯ!q1BڙӲ"UwNkՏ:Rk ^K-E:VدxVKþO-۰ajsdIljTfJ~֤UIRe"F_ذ-tx?te؜&/w0tҏrFįi#cưQ%"wkWO',!Q≾>{As߬RUǺ*j?Ɵٱ_٬v>2`ҳ1#ֶmQmNEL5GFR@K< EIsRkJ[8:OH_?@P+F9ޔsS53;jH q)]zc}{) 8E}Nn%YSԓ*qбޕ*Xp*wV%_u`y |>@ O9Ai-kx@;ve`Ia[\#1ydh^eK:ׄo˰R.gǝ#.OU}_T3'''/MgG^k 6l[ҽ,ݍ7{t TVŪe]:һwsBc\9ZOe R(k*/7߲NY]3Mk4fv1JN~cV5\;!֪L^M#^ǩǟqט Kp6l܄$ޏa CNZGIˤ/Ћ|=RRSZ8T|tѿ)Q^>[ 8r֭ }H.A 1<")$_~o4eB)NZN%t izϘ7W9u0Ҕ@S=/7Mi;0)e~h}@sה2jJlҐ:jo;4\%6viJ JW^S:72A455n;3gʽd}J7(1$ħWo#R2x]L BҎMFg[?q G$s[ځ) ] ;+|_dݪ. x9>F\rdʀO*qq:.BR RKNa0cvbq,j֬IlH5eڧtm\-L$w֙ ]FMr_WxDr7ٲ0W]c<lpzM _Ew ^ S%eڧf M$g 'hcrnY=,Ked,.X6眔وѬ3r$c㳨O)ʉO( M~FeGks7%q_(Ms_Wx,\n- s%X ξĤ@-931~cNLI |3?j™. a"wkWpjȻoEC$n{ՙ5P`MOxM)TSJҋ2nKÙty筛ӸLk$sGڼV@n:HMةQ9#ϵx%Mx 2 + CǔɵA@kb>k݃?ML+QQwpPNoehOjC$U W# jзsgx]U)ԱM}AUy2|׬ W-0E ?RrSSJQJvԧ4BR|nty\l%Ƽ;!7ϟXMZ&eYGI JTy{7הrU(M.Sۥj%M_0#bC`mU*tYƽ<@AsS&,S^ђ{1;{x\V42|.nCZAYՔ#'*c/weHjqRqiJs[4j~PZf|-_?f"ٹkDU=7]Ԑw{Ur֚7 Nt{K/\Vi">6'8|Khi '/F^TaYٹ^ Scb\,^5KCD?y2`Z~f_K}4:.)P^8x=6^\$ˢoM)Ͻ&,Y' :K$I iBЯΞ9|?K@"wk&Tsa}g-gNiڼ΍3cIe*z2rSGD7ilw$zj}WٳSc m2k+N*|cǣO}c$t 7rJѕ>==Ri[˽~KSS4W^~Ãkonːym>/n>ErYW㮣W—=~d @4oZ6-Zڌ8pDבgije:zj7R"L_kK~ڷǧǁøLE,M}׷Gq_W?z4po2ea>1EDN< D0OӾCVgOmݾOGmgQ>jJK)TusTmK,Ұf'M!̊9KWR`卉md ]Z)āԊ`񨖛U\ORoXrz%sĕu~(L0':#z/5 &>`BO)`S:nm&o&"Ԯ}|A5Y+'>%sR86P۝n6 Y|@fm<* ]L ؚyv@To+W) p0u)o:x¬#9U!YϢ(gò)pN 8G>K qjIy3i# !OڦX:Z skVYԔguHc`y R@9YbGދł)8EmQIa.h)*,yh^M&0fEҬإw(~ۜh”`_},\b (E!@c› gBFB^:qdU -1";:cjɻ+ ׷zqS>g<^ZP5ra_H_fk~{vI-#)Ү#}-D*~wE2B2/~c*[q\kUF 9VV_ gjVnXދ*hP h`.K\M 3ؔVVV*vx6峟?gPE ޿hb k1딐UIq]e#WXM(!EPgCSʃ?omǢ-`(d8$A+kDaQQ"dQx͢>>eh͢>>t1 IDATeTSJ_P}Ex9߃2B)W˲oR7ƹnIQ. Oi9)&fo) EɯY>ӧT"`%*O)f(;A)MI__B}TSZ,( (@`!UYût'PR(>>,>oTn @HM-|)->@Ķc@}J@ Hx;9sڑ5"ߨo9?)tHeֿ52`2 Zk)->V VX/+O)~0=Bm@㊩0ɺC#Aѩ{qGvjl-'En(u G[lGQYkBjJ A}Jsʿ>>ԧTOQ,SJ}J)(SJ}J)(>RgC lH}JyPR 2fQRPR 2fQRPR 2)/(OQ>V+ߠ>Oi)%HU8٬++5`(JŚSZn|JI-{|r:A Nȼfui3zTKzPw>D)5ʣ{^KQ|]( S58\=Axo#&=3B),?!nmFy3_%Jpt( E B cMi G'w SjC Z`(xy곆b&~G <4Soo6Ttqϩs"í$V`pz5c+U+ThKbZE4ЪSκhJ-jx۷+oK!ފ64%gd )ՔH[ t`^dv.Y08pE3&6ff `^.?5}F|x^! {O^aTSEMN3g{vČn^f_fX=<̩pm(3:_`}zYedxDns?asj] GDܚr߁:oEM.^`d`ʺ;nF3/mИM].1t`]0$֙n Q\s譤!=_aMj5xߐt͌_3X䄦~qu .+q)^aoy R.?\tl/ k)أoj* QÂI`|$֬e%s%,ps:P)Mj5$2;ڊ#Hĺ$2arEF!W;X]gâv$W;XPAP9E;"X7T,`/;ObADc6gD]; 98DEѐݙ$ܛGɇ-4Z>0:&wpl$ uHKay.׹09pas&;O>eO GD;@;G{,z^ {gDpoK%w/P⦙Pq:spp;_X,+5wGƅ˛:}s$EKn_mm2Q{^G/IenQ_ Y,Y^M3i{B+LQq&C{K>V轐~ xK]Ld!+vo_m-p?$>,_0@ue\`lMcI'P`\fZeG7{S6z:""S^OIN^A#ك̵z ~懢aӆʂM:t_ֺ :xTӿ/?{P&b8PrJ- :/ ¡k5h ثR@8-6vm:ٌ,iWP_76vmhA1LP?;^tzTY2cem^zcEPNNiJr/eWᒜҎ,6Kq!.r sJ_).PPrJ\甖^d9lJNier㋥&JdQ_YCl9lԖ98GVSwhd:R" j`3t޴89=/Y;U 7_QRKa/X?&.A&ĩ%!lsuw?0YՂ2>yuKH GGrVĀABZ1` 0}`{G33 0`iu:44iZʖD+P!&tk Mof#pG%>p!?p` 4^9y=| l݃?*Ԅoz"T.ݲH El)]9e{UŌ. 0`FDZޢ,]@|u-)Etut80hBN ~; cn ],gOh:ڂoQc5u٩ 9ne]ҷG_m Am劺YO/,~nei;r.p‚@' 0`ߌ/:i%Uvqw'y1$3 0`?7b 0`Po gȾ7FA wv Xk`)Yѣ3}^!&u3ņzi(/ݮU/۾ecB,ϫw}_Źn?laLy 0`PHƞ.nbW$+/}Viכ̻z_3o)tGZc=_7-ǨKᘨLrP䏃/U .x 6tRN7dU͌rdR>-q^a\8=EBr^XF*Þ`7 ~'e2E Rr8X.aB 0`wDZ]æxz&\Yp6[_`|-}v]3/䟾Z/1~;`=(r>,>XNt9-ﷴp K@0y|ȮK)(W{O=RcsgR:.ze!+R3`o~e߁z5eC4"}d!&њ/VCxG.-uVۮtTh&m,|Ԫm+)B7Nٶbp^ePneyM ̺l;\H:kӾ8xҿc΃=N̊FK 0` i+˚/(b937IvH1 0{#+@AHoKSCbe|)Z/P~v0'?c8I$r' 0G0s!,@YK- 8GoF+j鳪ޮދ5n[,G_nu=(<̌Bp_kcpoĵ |Ǣih=/<=uD-:F_0F5Yf2tOd%'~#3ł8s I|ۡR -vaO#rgOV٨8j^>שH^E ᩳPD u s5J~1#rq)hUuJ&{iynVKYq}ohQQ//aUxj Q$+gz[vdd7cg*/ ߃,a9àYRthy{ BYֳ+sP`h5UV{pDSX/+:߄IS`#GD12 7-ql5s|®4#c[H6iHq;Uy;nZwlMA:\ڇHIAas$;yvs񒍛.̼iZk͉P$oঞK6HZW]p+Gq|g.bI 4 Â^k~xx냨\4}S8w)S`=5.$ hYXk. 1#8>̹{:h6k/4p)}ҽ wϙf:7#6VD^N `=mU7\JLI!xי9 WoXqu&mKHŲi-sg%;0\{3Oܸ% 0퐯i|WSϋ` YqG`˸n 9)A[ϡia'3?p~_g5@xڴl *=\J7"x`W+/?~<;;^MKEX~7~fȑ/ݷ~QU{\Dai*=CX?efI#7a9_2s(TMuN vmJ74 -j`H*T%Ι9_dqD_1FɌ-[v&%׭DZ v<pkyMymN SQ<hАۛUq{ߐ~|AqUթȄ ń2wc+TԚU3ܢl,5myěfgܪ%xBZ=ݧx0be1  ,F' 9cMZPRQ%}Rg|G~)7ʎig7wS{e{ƊĒ?Zfc~?^\ :X{G21@^xt <yoɫwMl4zwh"nuW8 t3翬j1Y^Z]wMLfكy9GΝիo,JqD"qT8FNLT,4kSGl@Q,"P_>hTMwK(&B'ء>s:2D(Pާ^tMyEê0j. 1ю9`BL?%t\&,liv.M$}j@E;I=+*Dx̥ ([૸O텘HjBUjY\P[>M fv 1ڙd:,whF#YSs'z ^T|n2B-~(hkCQ-(WmqxUĥ،b*hAI!*((0,777+++###-- L5F/eT Sl_лoH;e˸8Q|˸‡Eʤnݺ׭D"VLUv(l N ;<}Duuۇf|jVVVFFFM41Is17oyt cMC>w7'f_JH K{+u^[+(䔏)7)\?~ֶ+oپ眨S}U~ mȻ7o2ӾT];FL<61ܓSS!J=j ~>NW䝟ai6O%>x9'&=%իRǥTR$W7kYULdj&&EU_*^TT qӖ7m u֠_жe*SxSkP켼+~ so`.UkФ#{6'nQ9sW*-WgD%ۺL;&yd1#od/T N*ޤTϣTMG)SH Rs~@ va"Bury~+j[0,Ԅ),cSU jlԖ)Ę IDATP SN=i,+BvCWt'NC d T ^WfX1SJr`n[A13Tuu9p?A;ݸqcΝilP*7opDqܹƍSoe 3ЦA**gi  ,R0~4]]e:::_>;m̗P9vc<50/.8uR[G;4<ϟ0lܨvA2xPmIq2QFj8@l\A[h刓*~l_~'O/_&Mhw5if3gee5nj\Aefm j t" ^/ist*Jy%#rkkk u)ʐdfJ1iENHI Pn_lp\$HR) (Pf*].0wH>٩CoJѮBQ𑍵Si1s`5[?uhFY5J>C3_v4`YT5+UnO]3##q n]37z|Q儭ZA.0bȏW\+EZ2^.˪gV,u\>.Y(IA01xjj*m߾]R[Jqqq-[NV,HĴ̹󶶶y׀(," BÐ<4/7e\ AYg_0(cccUUU]U6|bxܹ, d\p ⨰X-[~E"`#5y6#rsdUUU3R@ݻiۥ{RA.\8pP*S UbKq֠n. q]EJJJ$x|UU5]{$YTxK+EAvgԢӚff*UMϒڰ}]ˢ"^-*`Ν;]OAʱ]v*,(DrR:duhڮ)Y8qvoJԲTTU52 455233ܹkcc]λ‚ڵk5o!MkZ}LO8NJ$d-(Bh~!sz">77D*֮];`ũ͛7Q7oB@| .Uy\.gX 'kmnT׶ue/eWWֵZ\Çϟ+#ٳGu_9am UrB.?7HOFu6lП0V/x^Ri_sIGS4tIMgH*Uv6K"fl4Nve״$RR E#ɓ@*(0Siח8Rfff@&|$I~2RK敓"Ittj]|N__$oJUKKKKo;#-ARu=h7^?a9\LCN/AyPGG0褕 p:'`H԰W&Ɗ &(dϴMsմHy1F%谟vT dRRrvC閲יwnmn;_ Ku:j,]Ms ?y5皹m=4,\7,*R6TO8R1j[@,$ DCØh_ GZ~!ͧnoe XGi1dí1, <||J]C$#IB$ J(,63.FVMu򍘨 Է sb_=̥ص ^N~mqo+1-pX!mp4&`[ׅ8 8*]J I tQ:'Q-X?8ߺ̥ Rp8*Wϟ%\\Uo?F{hm0ܾ_U Z]j ,[ħRRP9/ljw 쟼ϭ]g WHUUkФ+%̥% 9Eof:'AB-x ə0  P%&Z"=0|8`[xI].Ce(+:JBz(̥z$g.{SVDo &Y F$eVYR(3^Ex eڏsDarL9iAR![\JnLCeB< |8'8ˊxu93;pq HBNNAp  HP̺'Vȭ^d|R$g\ 79ׁK^Qc֠C{ $Y,vɽ0An=`P PNT_1 I {|  m}}#Yj2e_jkcI>x;GL<`3Ӈ C. ; 1>9w$EѢmqcL$A6ᵞCGH?Zz2U^㶇Lw_h,d+G yst󐸓af$Ixc"Xh[(q :fCNƿY8Ȗ/ 7H?ۣ2.1( Am""җN ~tZ?2;6s.ʜ 9OkZ{~OGE?`ER#RK|Pi?Uдx8˟ Ⱥ??c-IbX,@HH@H HH p MoYEسk׮Bd܋|$s:]/O"蠵*Jkd3 tU1bw|ipz  jFf?3LS~/}J}!)9VMT>ymFE|("HYEo =<PY7';Pkls!Xl&ʼ-Hlfm' Zw]MQk=wlyq';v<أ p _j۪#%+1̊Fd̥=JK7v7P(bRE)sPOc(f.uc:?UtuYϩLSKΝyՋ8vmFv1Ge݋WHzRom`a%R 1b#f5U{QZ[59%̥{Y@ސw]`eS'A͌(*/r&)џq~lzkH%b$d[3,r|>OC9Tx Þn#!>cjT7iw|0Orlu)1Q&yeKtRTct՛WzuQQ\ZRs Y$IMcjT7 6_jsOdY0ޟRץ]\JE2j8j PHhH?+J)xqk\AW=J!R~:Y}\\\\L%<1$; 8[~7ty7I$)..$a۹'y..S$tr}2h"M8bKcqĮ3\s$Q.n4c..äe.kNaGV$ zR$I;u..|W\\\]\\IiF(jII3`i_E  KVC8E rR? C?I~/X$CN&~]dE͜=r<1~Up|E]6ٝX^jmsS) g/n՜{vx~mT;Ck%?īETo-4o r6#lj ]'*p `N5F&O;aY:@U4F;wA}"tӺL 牸rjʘ!AL9-#0Z.5_2نB{)GH일<FwQ{}Rc0nH,yI -D{GϞ<.-~:ʐ$Qb1]?mڂKiʕS.+M$~QvVN'uΣ 5zăL_/a.-jb.-|v,) 6 *ML?Hi ˕*tS`.a .} iTeP("=lc\8V^ Uk$!EyZPAay{ Y8 *<ܺsl!.=WJ6T47ϱ'5}Gg:eNqdOUo+#o lXO#gfݻt&־R` 0/IR#fZ^۵j{=c"w S&&_Ą٠wV}2 f}W=U6c(Ӳ}`;96]?||;;;~*svHrOO::.V]葼?mSa+d0̥ *>{O 2IsS2%#?]/;<7%&&6M?S 9voDLP8^%tT|+Y1OvmG :-|ƐQ_ވ[O-1'8!Zu|:lNIʸSE'7R:O}t}kgiRÐj "TAh3}kG6AC¯_V2Cqm6ڌ&=ZWU#xCwm{;?3̥!~%\Z_|Yb?GZ56#?ϱKHK]Co[JYUotojF苼ѭGXufFZiv!/1yڄO08E57Nj/QM3x7js;hǖy;ǤnĊK4Au&01 IDATvj✩^&։/dBUjYY zdD+E }+nQkliwrx؜ !YE'z9@L4/ qs<>d ed[_t}uhr%Aږ~H?v5q@72A.iԀj(Eݰl*uK˒/ҽC(+n(<ݲ8*5(XPȳ8¸ lr ye@umPd>~߬aM] '.([ _NhU"˥ϯIl$':BY驯{twڽ + @C)V.5-Ҵ:ȹZe8QSRӪϘͿñK  ,W{[Y E;r1nQ؋%-k$] FsyA;ZL98rQ5Etf7{FY @^}|Vne]ҷG_m2pV|VU3hEttH l=o",ZKFaSk,K`}nz8U-u.2TmL!_*@*j:eA{SVDo &Y FIgqE6jPfJN˴Le`ei;(f=45RR CYT{}&|݂ccֽ1A$);&ԺS+GHH}SNAE LX|J)5oէWswaF\Ֆ豈:Zo>>< pD@3}RoʟoL踪jBL[uezEoUVK%6۬j8O'ڤZz~&g)@5[?uhFY5J>C3_v4`YT5+E}{hput_mNt:WWy: =xXMrS7/XP_y8oP3HŭkI̟^zrzTOO3yOՎ)[<IB_rćҤv̟0:՛{Ӛ`T*P@<%\Suf9elT:[tksE9yt ;{KLټ$i3'Fq#kɞ7R Aj{OZ}6cY 59wV>Ju+yP+q~i4*]t:4G)CzZעeC/77(^=w; /Kk{̇r1t-zC;>j"|;":AE'v4to 8|b_p@ D {hb(`BDbWMKc4[n? fgyE2)YjC@u&>k׼RNǹ&KA~dӄC\u Q2\+ǿ؏ J}w~1(qKd8+_Y\sfW02`oņ%I~rݣXCy{k)a(q<"Coj+ʪCn u~A5|= :Y>vn\3ޱ㺸o\dozSmb.-dڹuUs̥6Sus?q#yHudޓΞnt]w[-T*h٦-ٿqԷCǻ]HH~7NVflWGg2=2W+)\e==usie?*d,/ T ӌ㿸XT0. >k~ˆm2љ5/ةp\N/;[;aF$yTUe״/闂Bvi36iSZ!IJH{SVDװ9PȖ/?^\چL+WҢvff6 Ս mi9X6+2ɜeR^w4|62f$$$5gLܥ ~Fr1̥ s)\Z¿rළJKU e0yZ 0̥ SK织Ac ^Um#wٷ́QM(5 Jl=jtcɂ ] ;sp+/:5)_m?U{⩺:6ۨk!VYԖ7ήcg<-2D{eAiHfZrҏݼOǛ֍[bvFqVwLVGuJ4iҘӜ_u(MIl7Q#mUBϨ&Kw rִ0 80 6(6|~pt ߗ\Lj.SvVG|GL2@gNTEaqj$pQgfݻt&־R` 0/IM õ6kjr=c"w S&&_Ą٠wV}2fwSe3{پUtX>]uJPf{ m:Gڪr/K쿕O1іnb"X2N^ඞЉ!B N 1Ѫim~laLwe*m_%FF]/|]}zHp1z(soc`5ɉ¯wz4bw5ENmT0<Ҥi%yLF7.LF_,u&#j= D̥-N+hZ$[(pf"pAWre-$9\`v#f8?cuXWl%Oht ;amz~ W[B j nN&m: \^_?FsmZY*1Y'dxɃ"W565 ޤg"ki:8jW09&οf7Ls%vXd!CnuL6˻^pYOW r7n\sed;Hf{.{rߋ)9D녆e*4T_7P kkbWdl}9X3QKM]'*Dӻ 2%K2йdЦo%eF @K}7UzHHxsf-ArO^I[|Aգ:eJjThvHVPmþ'zX C¨2ʥ4ʥxwZ8UcK1 r) r)ۭ{NAN:M~v#|d3jg;QTlW?%Y-ʕ#zew> 3&<:u޹qթ+Pr\~h2>XЄC@QeS rʥ?Jyߥ0)t{10#FM3NQ39YOn~W_vTr[Lm;w]r6mh)%Ѡ ygB :Q hA1{d  w:Rzg0r0a,F|n(m"Gze8xRح73뻇 N0Ŕl+]eµk70R43{%|7ϑ@3(ku{333 ѪS33?hvZXX)f_m/^35Ir^ib4$iQA&dHS0Kcn h#4EM#9[xݮbR j8p4y-j3Cyds$ 6X4,#S–A e8XuMnH?.ψpQ.Y۽]P5=hwZЦk,]{t]dwŎO;'4,z/6]un3Ar)~o&?{O/n"ҶC[=d>W 9*vۦ=#H2O6^ö1 Hj H@u.CY )6MF=̯5/9(slNk978?.ιYF0aѥy!3S6j RP-߾Q *41׈Ѯ]uihHo޺ssd'w5].Yi剢E;t據qc);^A5PVe , IDATf2i Lr:4/ 7~to#NI̋<uklw [*ue4)t8Md׮?q/ֈW_5AI;X# 1#r)RO䈁}%8j~:#Qʥ ʥjVn8ӟmfʥ\ ZWk7ntp& EUH%L%MiqToTM _&|%S13?hvZX!;,w ]4U\@&I~׏>ÝqFc%=vD6_3M3]8TPVe"WsNn#l8\ayݠ4QG5fGk}E6c9v u| >0#0koTP&{jVci|Ÿ&:qY6F&Zh>r'fīfCUg+{0#G000*6B$>_=qd.C9~˜̈)yJěLwRڧ^|fqa,v?%qiubp?|r~꺰 Q߂G4)^3aYd6ߍ% P@h׭KmDnG^P.; xHHTBrv({۵6yOK~2sށV W=$pľϥW,f#];¹>+Hi4Jv]u5fЮک<-r;_w U(Q9;-\Zf|r)@Hrnw6*ͥBM 沜Q |2ʺ:~{pi;j (y-#& &>Ws4T&9-%OP5pX?QjiMLCeE)Q!Ro8rie+2?ؽyНtu~8-. *&( Թd KK6B6dt.NA z{}Jٖ] oKuu>8w#a,huvGU5(_)3nLe%[YMFFNkv6 -.#K3Ӣs{>>iP|"~]ԣ6J/D&\ʓ¢R_BhbT;8)^bzzƜ]jy '{NxTR5qŻ67+Y-{~.3⡪3=h#yZXT Ovp / Y-ίNd܍?%!v:[,먊iV$r3"\ܧ#̈_MT*3Y&7B4煜ώZKtb̈vZU/Q9;-\Kr)FE+bR\Q5nf%Wv R1^·Ϳ92ڱ5հOZg3U" fӁyZV1ڌv, Hnw7|4HfRGԯζLhqί)ml>Y k<{MH ߒvד/xQ_._DT_A4h7g:0&v0>~/mرyݑQ<_l;Ԭb%D]<(ͻk}*9nz]!Q_v*ieCT|hqrrL`[l0|l>?/,w6yj-3g'Vm8wvl~mYg|\=]_ysk@nWX9Z {]\p3*~7qܔO}bF&: y)@_xƧ͒f{{T`dOސD9lsû?zIT9-zA*GICZ -pOky]f6ϙ6'%>&s Ju:U7 *h+Nkt䡘!^ʐ_rGǨ9,\Qo~WsYNySWÊO4)շŕ}oᶱ\jQ)ʧ$yB/{jN!1kz_'u~U` z9Ҕ< Fn~{ڡT פ$ovҿR( ڶw 1#OUzO +>r)`Reqaҟ\Q%Kr)V.%ʥr)BkIG8]Fxi=_~fėfī>_U01#O*J/M7II?-ΉJ ޤ̀ZY=S17{]V꼾"41xKsI,Q2lH*-x#n`nob@*f7羔onkǓuCiy?J?{LvDUcz[[֟r_=R`^ojzUfZ/յzTzr%񧴎L[{<$ :YΥeh/|J{lJ}s_uAћ[sK~y-oHJ9^k\ɨ0i_/?<Qaئ`QFِQH Qn7GW./S.]kT.%G s253.ƱwrI]Я]<{b&c&$+]gg!̠ grG_O87nv JBtTP!Dk+&m&ĭc00P I~I ߴ?nhax%.gCf^t>b!Ka,&dR4s]<9ׄFandϽ_uǂ47up1ԯChd"%.Tt6se[["ΪEYͭydHzUfZ"[i |ה9%h:Zq,1jxJ7ux.z^U-3H듰>Mʡ -|% 6fYJ/"1`````T`Fn:f[/8P<-m:s:y?F'e4-}=&mg# 1Rlz}#F< Y׽D㳺覭t9Wo3ڌr yi_Z}kW?8BVHٺ/޲#a6F2׎B_ӟz#6nՃ];6^в ="pc-ƑjQVHScE>ǵP囊>>;bi$4kgtSŕK5SʥY(fU:3&yIj `b{'͚<=%`"Ԅu &6]/~y0so@{=˿?*]E>+ T5-J]? ƕZK.ϓ!q-!#`L:tL $VLm_^)Nٝt+n+cӑDؕqτOąLgI%k'魩~(MSjZe1ꯆt)$;/9U)x?ʫAXÿWvUAٯBb8+A:^%7yB;F9sGXӖظ ^{"d[>\ı.oFyn؞whۘ mS>i64zA+ w؎ZfvJ!,ak Xpu^T|nx+k"GU_p5),| Onwm6e9е؎jb&[ؖs\?s> /s _Ws^D1A6jwU.#_Rq#6H1!1=($s";ŔBg$ d<6Po *HA)FQA+PAʥEwV׏3qT޾ՙkFXKhwީ={Mq!V”'5-)Zʲ! ,`R e4ز0Q#gvo#<ffxX}r) U.Ge)(r韖9U9 VF<rf*9*`F< ̈Έ׀\?B9? @x@Cƈenj̽P7{ɹDV޽- }[@U{ {al%4mN}$_Ӭ`,>ا. CҼ32!@dÛ/YNr:?nКQ3eƯ[3yD 7q]xSn8u@Hѕid`-ڀp[#t&UB $GY enߛB*axD ĹKǬȥiK[[ *UJ"j+Ƹ?qǽwSB"Կ5t*AeCn@H,sQVD!! Nt;ӝjsunl/h=!0^R~:qXc J`9;ANkUJՄqfpx_1#|2ƂQ½sn$vXb/YDh-%\4вT.M_3OK/͠lP> qausz 9R)GBF+Vk2V1+=4R4.y)W r*׆l_ i= ~r)faŌx!ia`XN 004iaR 2+m}# y*ϥJ>}f|u7fD3Fs:uu9TyJb[n {F;?i՟Y]|CDQ_+-t D<}t{yL"=.`zzzaG[;Qqk@%P5>EcG_,hUAﱣϪTriʥ!!ȹ٨L6e74rF(%9v5SNN`gǫ W鼖V͢ROɱ;(Upz_'$,0<2pX?QjiMrҦiZJ Cnl߬Z}-,DHQedăJ68R ḧ/̈*Έׄ]ߵjL;mqA,m:t|t4hPw3Uj4m󰇩i64f.&!Jdz}G򹡴EÇUM|QK]`b̬268T_ vTS0?ؽyНtu~8-. 犠+Bu.7od#Sm\:̳-g)|w3tS#*ؗKjv y07 _={:L4w}8|pY(w˂GPp./۶/pͯҖ":5Bbmae.GyEݰD]B&9G&9sy59 9SCNY_; :Ds~LO F K=zbq!"{̬(qcz_3\ņq#.y.aHӀ9\_N]OrK]z yfi<"QMf%UNc,jt4K/2#iv@Q Z)U=jZ~0bFE3ʥ*V}TW7lϬ _:DSn7ki𬳹ej*Œx;- \@V.UAjfUrf9_~oaP'-J[jW|x͚0lLvlt gvcU1#0#w`+{Ф<`F<XK IU_rq}ڤ\t5QJQ(@(2Mo tQD(R2jL?`#9<: Z/- IDATP'J9Lw^>ˆD1+}e4pS@hLZzK"O U[+ ^&VI`U&aĄDŽ O.m =.&񚎢Rͨ3fFffM;3ݩ[FM0#GB%+ʈ52 mVQn7'bYRw{l ȕK;ڑ9@FMZ3b{'#:>9:#ɽ?wEH\1ɱ[?[vÕ.ֳZfPV3Q 'Gtf7riyܠ4,DG5Z!#e R\ڵsxɃ"jp9v q : (B}ݾ^S͇w7k!p+uU|is%fႡc(}yQ(7}xQɟDo"\E;ӒE݀\Ll-Eb4̆ޏ3 9| ,CU(_ShReH:zUı&| ojN!C.,QJdsx6_AOn=wL&h$5z)ęqN=o>Hl㵺==C;̶Fhs -{Q& H??ޭ<>xr|b3řs3 $8#[&G ~7`urG&FȌ82{h7 P @'p;5߰ٱ0-ki<65vwZ$#>%+.QNsS.iF,ԯ% WˆOxrof<\8dG$E(]! ɭoFW/~ˇg;pßmS~ѧj_D2dIUAW>XTEI>vA~Ne0 nq5/ؤG'L= ܜ+,cS uia<-‰*j^o^ۜ[0000?XTs|ri) 8N!^Jku*)H63KAr)7X|*Ҹ]qB޽eeģdb;c 3x #^Yr fīiac\}@xI$-K\y:z1&QO Ԉ YX6uV_**(jzav]ܧCŔ qh W"8[fP3O ]4{vRsmB :$/M.H!Te EMu,RºARJݑ/)ecS:Z<چ*Qb5fėfCg+Of-}?G]3Y]  m׵fwg0Ǯ7w\ nt|[-ˣ)2VOiy!Aʽ+uψ:̓}AL5|R268gݻKp(z:xm@CtF)ȚJz_k`_0osQ6vgrnqaqg]vsq?`+@K8NgCgӝld^g^S[}'T:Jic]Ҍtu焏ɐ3vOkBgum]ݹE+%CwdT'5Rw$j(xI%"&A<%dt F'̙wU">˕ʻ+n\h{ǽwr4vֿ4fIz_EJ4)O,bE(#l~E\*S.]Ju2#ߜp'~\"&9Xv.hсD+2L&nLnUUN0000000T+Vr)rfW{p ϕrθV]pj}(zF phRX0#^N ri)TriѝLիu&麑//8eǒWXFv(Q45i:(f2- V.rpQcJ_ Vw  wO9F˜_UÄ<-\o#T,#~Jxל/"2QZS31#S̈x)zEeCUfijLB763%Nc?~^ȹm|A..ik9Ow,;**ẌǨV.lR#ka!u[&ɓRB:SJ(n$󺭮6ãӛ* ZU~/mرyݑQ<_l;Ԭ5e)JxJGWHԅW>>]JyZE!=ǐ?1U?_~!SX8$ҫFe,ʃ4S9 =zIU4^Ӵ_ͫ-ksT>?|T{/N7۞2/u2Kd cqa:_39vԉRZq>i,e+w@Kc]bvZ8v& q1Ϲ]s]8>Yh>xr|b3wMIϒŌMtJ#/Ee-~Cf{{E$E(]! ɭo.i`-1U+yO&Q2Զ%Kr(CEJ•]嶣 ǮOvɭf0B3yZ)Q>ҙü%O-*JK/Š(]/Gcү6yJ:ܛGA$ 6X48#S–A e8XuM>,+Ι?|OPmF+VriK8NgCgӝld^g^S[}'T:JiTSM[wzN 9cN&t6H4qG֝N|R=|M T Պ$xI%"&A<%dt F'̙wU"op?K14F=ǗzZd&G (P*Ih.Ǒ~mNc?ǚ\?>y۬~ } UX+bR\ XbwZ$r) \Kr/\Q5yZUkV.(Z^f\ı.oFyn؞whۘ mS>i64zA+ w؎Ϩzgp5,BX^WqV>75hێ֚[˧W7;v~?zLlG5{|1rJ-Ld rθVO/W$/Ew$"Bb{PH /s"b^D!<4!h֢]Rpq?cbg.ZV3u77c=U7h7*^2^}Rrm.ߩ*YV.|Ң;ݙ8Wo_Lֵu#__qˎQFv>GsBȿBj**4ZS"b7%NT+,C$.wH\,|BnH\L!?x'O(vD_-yqDeD;,khi}"q?"yPoчI}BXԸ(y(?H7Ŕy1Rj<*d|܊}{<}08RQѫNEN,~T$.sߦ2ml}tnSqI*~g g9 Cj{LnЕK`ZM-q4,i~qsyWgy| EY;Q*Юk.gelRـ[f8 Żp@$al;~Ϩm"7wYo|."aMםCǨQlHMyBnN)$.шۚv۷?zæ>r\u6z<  A[j#i}+ z:Vw딭QksHeM#Mb=;d*-T7ft8N;n_:K݃3#ACF!<׀[d5ѮBb,[c!8G)$8= MIy=o}_ oN̒Ki ҂{G>qΆȧ;OȪ5H? IDATl5kj˷DJG) w*ٿz] !giׄ4)R'VJه&ȺөOjǙR $jZD7/Dd0CĂ9 ꋂͻ/ȢSj%?"@'k§6ֵJ]Dm*]L^ X"ES? ?1Ɵ̈Rʥ+C)9"39b`@4=,7&8mt6Β"3 66eh-X͊WqU+ȣF6Cm^m@NCw ]y>߭Xf6/ ]{9W;-k1pڟȤOۨX[wq(&këNjo?5TͩǺo>;YE ;\mYÂ:QbGnC}o!N٫Ӫb`nry7f4s?ӟh0Xk9g!;㶵 7F+z^cMdlE+{|V1g wcӬF/ˀM\[mz+`BP@uE 69:ukh ,h7o- ,H<}.y5uaM! ر+"  bo;ػ  (j;+X(CO1! ee<:sϙLNnnYfy7K*U9Px{VNulli`>*0Y3ܫԴ,:;;'#ߨT- ɓT*U^6\PR_mihZJR"40w_zARvh^1ҠNlVv;CsKVgf?n_ܲ*65Slu⳺cCRTLUm***d O}oOlmi`޴׃֖OUl ;ZT)޶di?|3Tȧ iml*a+DJŖ yb=/3$Kki ȌWry6fgǽH g:jBh:)K*sfp 9Op> r}gKfSÂ(^*C ue V[<q/e0nÖaޫo!g_{NPWn"wuhY _PߨԾ Ϥbl1T^Um+N( iռ*P}yH>8d᷁oݾZ~)F\z|ԟ0X:E밨JhGvٍV4ldTrw$$II1Z`GfXUpV nq9@~lCaogF /Ϊ7Y{g@o{P:;/O.* 'F}vdC 21yܴ8u3yY#•9nx)*M.sy ٖC>pAm2'JN dBsi7/uoCV˹s>6޼wRcӡvEj+k/-԰hc9zIL?3r:j]sgy77~vv K,m 9rgP򨧇}HNɅ=Eݔ82}dhƽn )80.6nw|H,[vM>;6I۟V`|/hw|A;NS'>ޱ0ԹMW8-Tg#5τٖg{XT@vL @rd71JYtR$G ^0i0j{n"̆^Sأ[~fJ+(!O?49ZIxed7_Rό|V>8Uz.[Acm2*Vxq!غ4~8M+./nvMyi9V=7(,c1?2& BK&Aobr{ O'FsCЩ;xsy}Xŋ2Qى3/QV_(>VC'߽bldrMXjtjʮW4)aeN=c*,f\F&_I>M:{[tkkߊBZ;?|E0z.qrAǬSOl+Ƭ +-B?SH:>Xy!Ds-g`aDMa:UUȪ|IƵbţ]tiC=,O'0aeIdI#n\wdKi2|TOKHwn,(#(,cKsVSϥasG~.m,g]ew~[:zseWV^amU(smm;kan/gn]:sUdIc y28c)O_&^ER'9qkT`rymRVyCYPqS\d -:yFBo$Kf%d<[xLg_~CDn M*RQ:,j+#-u~ dO({+K,\9msKgCUBQΈihFTEM3c| ʊ )*٪pծ 9 qclw3{w<6)*Ok>lĥdFי;뙻י2;da3a_^3IG'81> H8^ɪW&d.Mwļ@FP5Xe/za;o%-&[Xv@{nPwyix6^.K.nV;s%n<KR?0{effpƩ;<`ͯ$9ۨy7(NQr)>]U@lTlai!kL_*s] |(y<=/HF/Dnۚ \Ԃ!O,9E€!QyBK뀘 Sb~f{]Q]vX7b?MQ%[w~{TQ)sCVoʒNv,[PT;yxaYSF={VJ&FzЭ{urvs(H .#׵G甊s\TźUʍ$M_ƥҭa ?|wF{=wg=s:s\f':c&l;Y8c)NV*$9jOv =<@%*^YG*ɴ%*~yYdV#-4:rՁg6dAOzl#X> td/ rcHߔh)I6>'cE~fN^A5Lk6RdƢffƽ5Ɛf|oیA̢eV@((ιzTn gd0$3Mv> ƭ\ZDRmȕ=NgF!&;e!hMs;X͛٪ߍxە/  ɴVbwMyֆKFFA7R(zf(o2$\<-2l]og.XgʻQy@o̼ĉ5zKdYgLyH2ZlMOy0k6րi%ς 9KT;5rW9kk)8$i'Ϻo b>]tK#͓ۯ{!َV}ZBrؘ$"3-EFbI_Y666~> riFRB>cWSZ{oƙ~{{Tz"OYkQ|U7  xFiꉳU7 F:hc[_V(a|+CN\;ᗅr)!; ({+F&)hH@\ AyٛDѣ\7xJ#qVl7gOA7 zz[F'Y'CZ2 5[;%m[o;2ge҃ɣ&|nyOGZh+KۭoxM*xL^GLnmv"jvsvF3u&vl0|dCZ;[C~Kֿ?Ci/OhdO%N# %:ZsKm 3&8d6bɂGUt>:փh㜫-v˾3xiqnfIcp27W0{h:XW?kpyKoX ᷓZe9iݮ%]:SN.Y vUYOmDICeT3yeHw:X ئa1NќjȨ@/C5f'(|7ޟo7}V^JVP:͓+=tק.^bov_fZ SÌt_\Ky:k|謋;Ƌݾ8D9g^ZG47(,cOﴐ)3m٘ĀyN/D SIwe e!g^).mQjp꯴{䚜Ԕ]o,)aeN=|`ċUX>tQM<|n? T,Y]Zr/UVPǐUG>FI޼yfYNf@4 ]6|]<_g^Ri25|ٰWOjُg_oUJ_ܪȪ.uG,uхCi(桄Ki2|TOKHwșcIAa0uBuY@`6Yy:,UPjܔGFn; ;vcǸP%eVzU[F_"N_̏ZF_Ӕv2oPj1uKa9\,n?_ͬf?輑lV"]X,yRvPp*D ~pN~C7\Yl e =/C](~󊅘QeYXFmR25@Y՟Z@x);.,}'IoQ'QE eJ-z9:u+N( iռ*P}yhhR{dƒy>_ q9XdG+= C ¿ O|,0j|n_iyU#.=~OzJ|k UĢuXVph{#NYhکz9uP@C=x4l#S?蕯~6_ywPڀ֏|=]d<^{āUa?.~=]tv\ٰ 7d殳˷>yoIE ?d@ i-'j=>`E#%?sycBFɗ7nsJnP*Rsl:?2=jt$o^Oi%ig-W-B͎{*tSualTNk@ \KEӗՎh"k<8I ݑt6%!Q}5kZq&2^:ci#? Ҁ2'z^:&W寺I M:KAVPǐUG>FI.dZjr)R.ɌUPkiSn77?E@ !;\Zd G5Y%?ϖ;*~(6Y7xQcE[BTTڊ#ޮKJ"x^?{df^_iM3!O٣DߋXߢ`Ԍۗ' 3ӪP2yE z?ntC(`DGv%g2jTYvN'R=z='ZgfhX&&QϏUKPIU[Ͱv˹WS9ʥF7䊅sZ=5}{k+P2Sl0Nޣ V|1Lvx7-VHyf+\+{ת]?4x<ߴNWxKB TttlAJ\3ENZKŝ>NPX0ʮuv۟IF٭~ Pe^z~M?No$4˹hB,f{BVUe*S yq:dz&_o.I7uBl>`&/ӽS4YshWg@ ž/= NY|NrU{]Iov~y|IuG%_w?] /tߝ95$ZZcV#o@׎ݡ +@|IIjl0 :<#-3XEٿވݺf}*NA|:Z=:<~:8ct!ha(˖/T}=twNu[g?c?4qF{6%4gQ.ʥ+rpiGzJ\^Y"bɓ„3V!zs+H}xeׇQ(sUPyB u˘W,$+˽(Ͳ0nÖaޯIPC'^E KI[ITe5|QBR&(Ӷ'j^J<}FNrp@s[zS쳃u^4201ԊyqA*ai@W]dWmSNQpU*U|^~ c ԰Z~٧Em5}ּ_ʪfq?' =%V*bQ:,j+r]K[{`oX5-kxҔf7/1_1.>MмMٓՁa̔K~:d&/ &{\jHe[S߫ej\s>k 󕠊Dيo6-UCT=E)jbEҮK`+tR6״OִfƓ'P"Ǭ(XdMYmi_Ј7}Co ?+RZsKm 3&8dzoJdA#*:nioܧ%?>Źr$~jS9[j6[%8t[ǦCuv/RN*zw$%z>tӺW|>w[u=bpiB!͎= yM!57Xq;%'d s9F \KEӗՎh"k<8I ݑt6%!Q}5kZq&2^:ciA^9=/zWًgW$PL D4%\y׷\;vQ/qRkū]v'fmoH;aD!w(X SSִdiu ٪@  ٧R{Sާ-m;B"Տ[cC9m*v'/@ ;JD_SC]cXkQ[-v$zarbA[I9|.(Vzݻ#G5fݢ5</%Xh7w׹CAr#> 4ѭ'qs=z5Sl0Nyk~e'%7BTmW(Vt.*O{If%c4Qtc)WMHe_s[qk`O_-J%I2J>w<yhF μew]Fwyk}z= ]r%\Ky:k|謋;Ƌݾ8D9g^ZG47(,cOﴐ)3mIЛBӉu\7}t5=Nx,^r^DrTv|;KTU5ץ- WڽbldrMXjtjʮ(bR<˜zANjUĆSd1ҍG5p6.Nh*'^Z>iƺe7LD.dZ#\ʡKi2|TOKHwșcIAaS. 3ce>siMgy>*'9&;1ׇb+c*^6}r5 \3.Vs_9-Hi3vGf;_&^"X"^̜85*t09s]E]䍗6hP.֥=E'',@CvDjK2~-wzyUJQm  6䭗U.V.#5C+P%s?"+\>..uA Oέ'98QKTb56=>iFb=#Ca5T%Nhf\O \K>3YOk>.^l3XE2JZSbkt+.+ۭ_f[¥1/d^Z[6riCV$tj?ɚCkvwa:=)\,qoFA\oh0 Z=m;%'/!ibbr L:t UT9XKkTGgml]ҡL"\b e"acXDJZr5-}'̠?,Ĭ^r_4H,QP}6Z (4 ZExg\N5 J ]j6o_$w-$뵴1H2-%R"9 ĒTi{XߤfZΖi#nɳO| B:("v+4 欄CԗH3I:@9*Cž2uϪ?77RoSG\Mk Z,%e rǙvu7O,yJ26t7ye=8%{H0J,& {p.ªURRR^~mfff`XG f$RnurvYt)eN. pm|'g1v.Ꙭ;9>-WNz쒭:m( uqrI(Gsrr~gG@V$736xzʆn#?1hԥ顟=!qz'gݑN8mHUQ\')~!` wt11*`wY 뇻89̻A8G Y K.@lݮ4/ vYo~?:9Y @tmUnd9ʴG~1^y%tW'syiɂPР0SUN.A;Oc99=8u%q3bwDh#\ܦRyduj؛*pGTg̓\cEdLga&aٌXag0ɮQs}(`KF߃̘rTEo|=J{=Iq/ `82b6X7-_ض}ˋ2E;U\VY)#vgʓX>'G$=5̳Xy%j~h|`]Q.}VQιpP@5矒[mV v8e^KyGQcj@5^zdA2\k ͱPN9{iU[׌"#vihkIև2Vz b~sXN(h2>E3;o[N tg[Y_.aoC突@"u<~[A iܞSF/q15z<ɡ~^t#,_ťl C%160cҌ#@F=Q/% b9TptO[~c/>-yoQ=-#cjS$ͽ5_oMg}s^Y;ouCPt 6Ckl |b,Iv-ֱȯ|rrɪT%d_0ߴ{Į:wT}-t :rgPː _t@i˳4):N!iI-C5f痠"X8Mt[+Ҿ[&] v=kԵ|2h𐄄Oz5DV҇ߘA~)biP e'wݥ?Y~ngs oXmr>F9V/.zGD(2 ʥy:$ʥS.½L#w馔cW#J* (i1~tqbYF414 ^_ŝNuf^пJ! [YM7n]siVzΎW*;֤~ #B>LJb+iU(sV.5~u:Śט>+gn <*׉b<%K.?(2:E]E69ZKW6A 7].6]&^"X"^H+_v[th̼E]eqM+0K>xomS`َc]{Yu|^e;y pe]OwSmke4ޓU ڴo!;ߴA[V֛ԬjW^Sj݊7m2aq,@( <<]NB  @ @ wOtkq(Mj8?  ZT"@ [iAn^xB}/:}67-'Q֥[U:@ BSusvI2ew]FwynV;s%<yhֽ vE>d|W=W(Vy;VՉFߋj"iy278@BK H=19$_<;n> {}ANٓ)RHSmzf먖ETne$k_W{X2 JR$*BVƹĨol?Vqy]w>Z'k$@ ~~|wȮc dٸqcmCxH[Qv[\ jH`aE hRs%6k3>GBUgZ'[U#_5<[@ ?=n[?$WiX8L^Z4Sĥ]{b=M]nRDS3os.- ^Mګq>sthT<^ʍm2$s͜ՉOA UoYЮKoS$@ ~*iw,}KI'%fs×_K!%~p7WAO_q HE  Y@ MV&6&mwIf^1q.@ )(˨RBw.9ErCoA~C>y u{OoWrRcӡvEATC'yx~E8ӆDUݶ5_;o@4+[* ܓ;2d,FRD*BV4us$J yKf .wGFj=7(Ey7jx6^.K.[Ԕ!]=0{ey!Η(fpˡGWv:~i&E)lmU.*O{FD&ƃ\U"k(y

PjYR 7ЃˮS9ڪb>Kn4ct\٭4KoQ3/5z>C*x_Ƴ1aD*8XK*ko^r6f*f#6-nriN4B7ȋ@ bYxc[ثY=tLTAusvӕB1lظ1RU_]֮#jiri*Rj3UפiN3U_\^ذL:̄'VtyͿװV"`ݨHéɜY{QŒO6X;-dTZHZ +I8Ǭ}Hg?4c @ seް.` iPCr hg3a68< 2JNb Gz*O(_J:wIل fZ#j_N D-PFm̯2_\ʳM~E2 zVWWAH >E*)NӁڪ_66Xւq;(}8D ONbN<_Xc Zp34ҧ J(RARA@ YY"%cTJRBJR¿>x<r5f;ӆGnm %:d׺:nhd+!͂JT@P)7_HB ;4G x*!~9[2?ެ$eq'O nWLܧпPc Vx&z}S?궭\JE:KJI|Jӓ;$(=}PEí=m=ӡǺY;T!/Tk,m'WƔ-*82PӬ?8Mt[+X*J*[8@ xթ~is|ֲ笸<+IbIaQ)}wXKi)iwf}w.*D2D2k; &q\EX{b)W%Akbۧ仓Y9 8Exav{B3qь|tk\:=BN- WﶔRܸR  =XP)X_.!k>L* ,wߔƉ6wΆnLM1wSO>.J4}g)@b {OK@$S쒳!`>bMNsgNu<: 朗֚լ7`1[eeO~3H,p]N7r|zG>^BWJ^Y+;xBV@5)b;j6dJC.9z>C*~sXL# WC~fNV@رwC%_<:q8 0el'{Yp$QJKRY|`k^2HZ_vw';5畻TtyN=5crKy"7Ys\:D?_ʙN֎R4PYV$.h,mQԸ7xΔY"f8fp*ؽA[ƻ:J{xSWU{Ғ"w u_@pm Ĵ^Ҍ\79222$Q=PJOW y?켝`؂u* ?yOnWݙ2К5 ۴⩲3tJoRuBJ I ʬl%/)2_HWΠ?{/~D<|  l-7@`I|.\e$yW OI%jmn|lLW ¯f)UlUv*V-fᕟ]G7ݻx.rC }oZYN5YT :޼@E)tQJYc+'{N]vF3p#XfŌis٥mW{՛6qykWe 6rن:>\(tґgn\aTJ%>x-fB>rDŽSVwOCӨJc!jK2~+#5EYNzoLW5v|vzZF4JRJEY7K?a$U>^PLNO eMP0PO?ӯ `O6G&e9xSn|[dxd_SM_.5ܗ 0y:jº65ɱA˾32M#kJ >Zd :gA, T,}?=$g:ÚȺIB "b" g]{׵kaU{Q (;ߏ 8 I,*9s̜s,j>7fIq)I3tu%l#.5x| &8NQBM.r8tAtQ_mǍ ^6<ܦFZ螢E'5/npVSu#nex{ɫ@zF^QjMhQ?WG)KXy%3gQvr^D@Oy,B.=g+a=l mxs`ML4/үA߀ۤoy^m ۢ4K}XŲ<)1ֳӻq$ptѴ-¹r s)pU !DYմ0 !Tu-[PjViڴ6IIݰtusNlX^'lٜEc{U Y4o݂Kz~k"+.:?k`s"If>.;Sn_&KִZujMO*`& y͛k-$|9ͣsfa-+\b`ÂnV.䰣Y,XT ` pEOD|NjʪMOep?x$9N0@vٶ}ΥR]tu}]!%!QL _!]vmuRI{sRqdY:@nN8#D9@Z԰UdjfwY-`u!*^VF,X|{gI2=)I֍  ?mB>UBuD|ML)_L$2 "5T@U 2 Dп̋m{EH#BP/u썉zt>]4̌C .>=5C#Չau; B8<0(PCi"xHgJl ݃\jl4Vf.^\>雡FσWzH0O&g!SI#b&İedBO` ^F>PKjW,:˂zғBӓu-#aƫQ= o ) եt`8Ȋ0.b>>:KK2a sy@Je* ,=R|jM,d!S$l_֩p}{R9q[ v=},Hb*/8w 6uY,Xgғ`Qn \9ƺ: Mk8Y==)팻܂5ڰ9 ,_dR3ss 4 Q o7o]1}Ւ ,X|xqVF&XqaFphRa)6ғI]ӓ~Y;Y@YTϞIBP* (RIzQܜu 0`F'L%ĸX(0^q8pGJ]q|A$$5{$bQh|ZpYI_-Ef߱\ק;+{y$UXø?ND\ň$ⳗWg{/'Z(.ɤwkyv.]kr`Oo={6ncQ"xƆSwW (nKmQdqgڪ<}Or[3~I^loQS*]m׺70wP(RD")((Φs7N:]WrF#Wj~_Ԍ3)O?ƾ\Wz/tIfgh7R4RI8 WX ;:...mcW_ɕUoٲKWν%*t?4=SE*er YX`SSRR <'#cҮ6tli01J||7H!bexڥ=ח]7PaL!(  JR )ID"BK٧4'Uw'zVOSāmAhPDwFuiP2;Eju1I;l8X"Q)5P=ꞟ1~n_\ w^\2\$nLJMzUb4>)&>HB5.R5 oP}XtF2J%89HA] sWs-5Y=ElV;ڃq] TS*_ukA-z9M$[.JLL}]ɑ+hͱ=Rň>|xI_*>&¼1PNI%J6mӥR E L)&ڵkה Wb$''yxxKH D37qOOOYԬYe0iI%>nwi3n{^f~ӧOcu}UR̋Y^H OPލ[=JMLMLdZ$O/5mQEՍ8hHf'fߞ(WG;Hv乶n" I-oix&M..74jdccCQ$EQ8+-k׮]v^=А*#%y&j$yd4!oWrK3m)bD' yFOzgzNfV]*Ga;=Gf6P m) C3+ [4ea۟PNcwi~qҧ ]LpW "5)ID; /7wP Q|8tض:4 aDhH`ғu7,M_] 1N8 F].p-)` t78tm] wMruݐAPQj 'ǵ}2}H^ɚ>ԅ riGR.gOlca_k)c;/ӽj!^V<.&Mh(7'8|2c,iZpÍ\H(XS*]ʹ߃!OzиSE-AM0A3 ---[7(#z^~+Wrp٥j~}***\jccݢ绝P(&+r9Q}srrBaKRR47ɢLIJ@ nꌙFN.]O^P89=v~Z>fs5x[ܲ2aaao߾}08&ܺmKGO=t%w\}ٸUw 4Zcd9z22055-?2?)zm2;1x&UaeյVV|A Z:rڠ!4EJ(t1qg j`v:rr>4A-߫Pv];-EVǒs 2r 3s2):ZhάY.?KaFQT7M2Rp^xIQuGRR9$T!fYʕu/UJ$I\NtU($ApZR,ˤ0+i,∟TexM+, 8L4I)"ɕ IDATjf_@J1.yW$CR9q icΝ<ѠAP*O1q㸤 Ͼ^岱K (3^N޿s8>ߘՙPVsq/|~8={:F^*V@ Py{mn˲\Wu>HB IђXt>dgg׮cՓcEn@9=`^Ӝs0zhFH#͊~PרQ0@76nѴ;{?umd__Q;e]{$DQ6ԙNd^CF PC,c a;;&ě/5Q8EIecF.EEa*a8E )RB )(,,T*7orW۰i *iypKT9RP˥Y,=)co> 7|oPqӰ=Gb )TҰաVN5Ek %srrr~7ыgYdxY&/jkk>xC/Z,NxPI2Irlԯ_OT5j8KR$ cx &r O qIR2j+* %XAah8K3}\.wuu/ճM8IJ(k+KڵuuTjp8uV?-Ֆ /{3}r>lzS-Xըa8@9{{oCڥ#ܤ 8ř8^n#aw|*,#5UNq{7h[ЀclZcT 5/}yV ^>oS765Ϋ7uw7oՖ֨Y;---00ӆ &Hr¬)5kĉsrr/^\M>{v|䴩3.A JMLL2{g&VVAGG/eee֮]G~\P%$y,=)'Չybn{|z},79}s/˫&9Ȣ `{*OxO+uƶ[[ZZ6k4%5٥cEE} !T2TPE \85nT*JBϸǤA †L PPJ",* ȼ,7+diYիW<''سQFG\.t]iւEK To1,_&:.TJdBfY_9)sKnݺ/H4[czʵkZF>YO PFU+N+R_ 9 &80:O %dxL 4j\o^ %Pfi^7Hx#0Ʌ[(c4"EQƼ>Uxi)l˖-s̙3gN͚5RS$N7I.qf'y0LT(y0)~JI>ER~)Xp ͙6bIaPjX"\%c}(JjL(s LLFdƦ3fm-ԇCR { D}C~#WOmK1r88p%W1M4%EYZXd 3) (ժa8Q LX+#m|RLOO733w$SRRӧOE2m칂jf -r\/Yjᓏ\gǍUƧJ2 uLjje1WY!/s-xk܌<)F!"~Ǹz0;8$A <(*@Zw^yu}$s(+Ȳ=G)HL&(^aXrξ Iafo) qS{.E$IҘ5gزe˖-[?%HGNNqvv붶')H 1F]{#*zry3O366WY,=?Z rBI6R$5OT*{ѭ{Л+EYiqL)[2(ϧ߀PrJR.#>|ٮRJCT"%)NQ&W2&Us d250Ho ]\\n~-Jr.T(] Clo9[~Mɷ1׶01cf|Q_)m߮T :0tBg3 ~RD{[ZF-9{m>}NeǾƱB %[ 4;#p\ nn5 mwF[IQY E /RpS7xnvM(Ĥ )Q3+;C a0Y̙̙h7?(!JKi hmܸs0ǡN5d2H֤eqyCvZ;);Cʽyk òH=,ܺ=1c).mvI#vUO7.K"Z^Mroҳ݂=\TY_<<ܐBU=τ"DJӁM <=lc+ʿZ $H S gwl;ޞk0ӻ?_]&{~͠4˩}7“;;KkZdIH2770nL>TvvB$IerִV-ʒ:UkP#ILIb BU޽hccCed2{u]188iT3oڲųQ]xh`*K!\TJ׫Kog޻go~4/f9 "f<_ܜy mۺ;aN|=vC?uT)/=)A"8gp~a  p,4(ϊݳQFujSHK(a/OS_~ 7M A?dJ{ M:T0 ߿w, qA.$٩Cysg1RJs` ]:/k.|*B!tɋfjiVEן[.Yv.l{L kZiەɛ߈?8Upu]dpXA-ڧ߇Y%zR]'mx{i\7a<^$o =;}@xEՊp(]MYG1@gr_SSiyn{ñ Ƽ |tfa ̀!kHޮLK6"?~aC~K/O v(hF y4EY,X xzIHF[rc8,ѹs I,6&] |TwpA-4)@OHP<~^n" ,~o{NFϏ,4c:TBI_Ӳz VUJm4IMf;mlQK]tLhr& ,Xס}^a/e8ՈzrTFn4=eIY`Q`_`*,X`/6bIY I _8o& 6ͪ Xz2a(Vwf?xū-a~[4HK.Ƃ>:/+fzgp#(f"B)pb)v™=i6:IA8=^xw|,+WkCeBeׅW?MnmSx~aM ' d/W֮s=?hm;xyiC\y l:H1 bbζs.{۝])j1㷃kޣ\N\)dݶ^dvkg{ g,am;)D!J(TEQ$zŗ@1Qx ,N :z<ڸWspGwzvz6RX]^5PL`cBqdm#v8XV"xw I70}=R iT8jfDqN[X$"8h ^&%xtwݗ"XpTWB`9eLiyVpD$^?ԌȿClWJ i8pFn$]4 k $)Pk}3D&aJ2(]Lϊ\Q/vGm.ALs&Ռub4>)&>HB5.R|oP-tF2J%8D񲑪&29ǫyhh{ؚx^Ww{\w4jsfT֎Zr^[_En$()UF/ 5ެ[lK$-=IφԼK7}{ ]x huiaQ'3a+ۮ *岇@cϑ 7uz] lsN1vs[9aCt䶟Bp{MC> ] P.,+̄ w}@xP/RD rs%*s |Mm CС찑N=F wbxy4>վ>I4 `Dۅҝf]yKwzY.CB~90CWkxg=Iw8>ӗC>M.TK;JBvI_>}b;]ӿ}-=zV1cW-DՋ;¤iEDOfLޱVF1f53\q7۸iΣWT1EO; =7hDZS?w?lcyuSrz0!_>>s[B?B/{vH !aB0Du0q.Q 7I#n~؅8R$N<JuaO~=2gg'}rB] :/`(gysB.?gOPy> DQ̱8, HB! S( =Js͊"8Vƒ IDATb2=^9)oL# <$@ Զ?4[ZFlf5EhbI*c pP>%4iAaQ}OP$*1Uיy(S _l5Db2']mEHuP>!c9GQ OHIk%d˶d~zgOY*zҒgbgz} -wiNU%c6~IE}(ʒө|raӆixǺOB*ip+7\wwk89/_>]|To_dЕ_>C8(9r~'P)M p$ z$`ЀM+-gS~|t-n)R ^Άy5֣3ɫL@ JI/xhT}nݜ8VMKE5|iғJO5YLJt4 NxF|B8ᒄ!"I^}$\R!.ψ9νNfCs =$0;a8w(f{r(KC?B)9e@(s<:>9.P8R$A?(r8Ip`RAFkޭ1E*1KO;q.!B9<(BR$? l=@JrB[[#DQ$HBqL!` $8\Пy"5(Q%wP)?w5nC=E@4G-,MEf߱\ק;X}/O=ސxeDJʟN <#.c+HĶ\0}v'*zK$M7ܠ;uw5e/,*Jd(EQE'^RAߓޣ,иxF'Y ! Im0"(ͫ]AO:ER!gn@(z:8~]X$I(r ( :P1EI$I JZ!ΧD$!TR$($T5RPʋk7^,SI$PeTcAXt[hhK$imtғ~mz>_]z_8-9:ݶ٭(,0Rg}˓OW^K J&!Z+I"y5Y`CFS#10TbwcY!x`n1\Fo71FTF'Dӓ~ؖ"yu4=!8?VzRIgzVf1\\Nl"wA1.IU |gm?~b5Ncw5=diL1c溼гd%AsZ1F ,Yl'@ }(r-eöVCa#SH~@?f~pWfFU[U/IjZi[g[ Y#Ed[.ؕ#ĕE~Gۅ1hԪS?H*$e}R+ˤ-t_;488A%=%^|~n⫤Yvn6wpx>~7_׹H-zdQEup{s%j}/HJiHOEOݽv72I'qk1MORPqZ8',&ꔡLP(MOܒl$` uXCJ~]ve}6&5zjPdYu9\R!V*Ya8}cZ]klbB4hԄH`dX1lkYՄn]ǵp]v4=iQ6'5_Iu,'}lV䖙TZbщ($AEnSo9EŎ)2gˤ'JI{9FTrީ7BŭI{.[TܡỸ7P(L9vfzҐY#Οsu9~F^MŔ&LE'=BλG6WGj At Kr=ԥ;vؖ]@_<>79E4*°޳l Zȋ3EU!X`]'eE0|iFaؗ/]EU0J5W&ߩcKYP_zLDgJkRg`QOrBU*Ld;ulu)4KE\ɺ/t w*urӬ/Tf.Y;m:"`M/4r9Vwy?/*ntV[8zя Tf~ _GT^E"ۿi|QjGmǝh[>o? .>p*ZNUPUa`2lHq ^vՀ f{G I͸NﬔMU:pw6ʫ (;QӳJ|oհ} (y4zV!*vʮΰ-#TEf{SpPoB xa|iqƜ"MQn XzүNOJRƪ0&Se+TO'L1TȔy&)nJXTePd^9f0Nՠ(d'Zϼ(PgIeWSnohЮ27&Q|t~6h235vZH" 8lHzre8 B8<0(PO$e#`+)wV_$ZCYoOy|wг=&3LoZgφndٳ >@v/h#&S=:򫷷J|f4Є'Ά̜͒=lZ^N= M2{X?oa$'{{{2mP6#r CBl+kTh-'zfB@> fII"yy p Pι>֡YPvH#BGPw<„Lya<ݛUdBuLpnڦi(TyXؼ@h,kWk0G[Sj 9 F].p-mԺR Rzq*|'th!tRWCo:ݮpz␩;d>o@q\8 Elq4Bl95¦tlNw0iE; eg; Br 9ͣP@Q%bPi7/NYeuCLXpvԐS ơCicOi7i!bȸ- 2G 2/ʐ#XރhhHșK>v?ho&PEu:rfQPBBΨFJia?o7Dș?XkN_|l <}zͨ(MG1δW:oL% 990":>k!gva? fefɚ7^C!Ejc 񁧶]6vUu%\J~@+5R[O;!аn=:hSt+5;a6vtK+aPOk4d8d*uS{R-Ce?߾xDL`sߑ  b*=JDU!")B(DazB<'f/iȣ6-*x%+!N(KCX%)c;B$(1vaY SNzy>(@BO8鏟GKLnr8,.S1U3B2**aw7<Ƽ2tpdM /!4:{tު}1(0sr SRk{qu\f;~Kc">JJ7?J,ٳ9 fۙNh'nZ:r>݊+d~6k`rnғ`Q \*'/L v\-Tf -)V]XC˥,Mpp`yXzR_ngܵԆͱ ﻊ`Lj&3:V.p]}.铪.eo;łʾ2F}eyFUg%/gLjf 2Rj֮:jſ ѬN<7Bv 4_R:[S9/˯}$p2ë-a~[wѨ[N=FgXL!|G;wl>e7¥. yydn FeIdA+SѫIM{^z[: |a_jqCp"%q]:-њ2vR?被{z/O }`toe(002+ o h[':/F||3,.&KOғ,=) ,)doNvI${KOғ,=wBORBM uћ3e4N@bðL i0z ђ}g[0rM$CT|||΢xqRN?_>G=/uzdCV{ Ð,8Ag/$gz ؔ:6iKOKOғKOZ%Q>i݆72)]n%y™qp)pdmōuHXid/=tj.2x1uqºn'7O/NoK%,/xzxSNtr+I5#IWoC>{aoCĔt+D`Ozzjan,qC"/L1|4''eI 1yR/?eQEiv%뼓',:^s* x F\Ȇ9~uY[w^\=6Sc5鋺j-$6L;n}ߜ:wp!Tʖ#6^M='c.QaC r0'x0b]Lue_ XB, T?xy3զyd;?u/H  3Rk ]VM=ʾpGc6GN㟻2$ZrWlehj=(qxк܇mm0#a短g>sD1un1ud{=6_`(mS{nymW8Ѓ55~H.1.|-gc םxU\IJ<|+ۚqʥx &n"da \9bQ,=7y\LQ(ү&(L -dϕ5.D\q/*Zr+ʖ,"bcٮ%>cjzϙcJ>>{f93󝛡mQ?-}k/F}rƇzd;%mo .fˌ{ ks6~xd SZhf :#{͉e3:>4sVSУBڶ*ׄ޳݆,(kOyQxy29o@ FtFVfffffK8nw05nQDM%XJE!x]F0_`0L͆#y<3i@8=E),PGtw&{H^~p}mҕ* 7CּN匰=E-gW׀~U3& 83AzǸ]]5`{'OrH*JZ[D!lT.:jNQN!^GdZHSVPE,}7ە}(YJNZ~)wjJcb"q\j= 19󘘬8v ɕ\s}ľ1G~ĭՇ^a5F1-Fp]{n#;Otzr3 '9WB5)3I#EwN38C$?6t)Wr-B[mqSB#YSb:L[h\ QcLFm?)[Idf{@`T?іdIy6a#BFgoj]P٠0@bId̀ˌYKC.)Y,KUܪ Ǫ5,nUF@_PO kjʔÊҫRR&5莲BIrSrZ>E2̑ IDATM4CSBրK1RYMa` u$q鎛iƫ8Sy6LAdtms ltz0l =1m3F"CiuHĒ}Huoo"6?X-֚]Hcj=1cjѐv@=v%xygKYm_j23sDU/tiir*G"kV ۨF{_8(jNW³'9pk./UW"饁Г#kmxל}2=$Cpa,X2ԩW'Ol2TAb;%|[}>e}8v)AC1vV^J~(k9uD,m 炤lG>UWW]Aѿnkb`KOӳk"ĩ~X,KKlڃ9D,)>+jt\ٟ1>& /n}Po?ɕ]w A"̌0h7Ҭp/ˬ4OѴ3GSnƢ)*ީmBk. ڶ*Gs5ѝ^2sDrU$NȢ'9đp H@8Sw.jo^ڗMwvo/^b<&| XRƌyx۹SNΩ{ˢR?.؝_:A +F>u7`s4,~oYAlqjsu9PaDeH/NzA?T'N'=:%M{m|/+m[qH@k?]v !b^񶼗?&u* Y+Q6[ăm(*;x>Ұm姤B 8&}N N^ŅJ/K8f{yWij7~՗$~{', mlkq mo`=)֓1,նv;ƪQ]ӾZXw'wS5 ~>qYaAL۵}D,mKb <˕Ņҗ_8Ozwe.%{ DFZϙw7K" cp&£s$b1^.tiҺ$?Rٽ(Aoɔn~ I.JDϼ#Rv^8(3KCM i4t4t4,4> ejlIkY(1˛qHjO ۬vv Ze;ib{MEJ"M΍A!<Q䡸jtYUs&S%WmT8o4=sWn23 o7;|lr弧{}nWA9bsKxL_4N+d 4ɸ.ș?"Q+:M _ۢ-PJna.p"W3I#h,la%ohR" "$G# j^곸Rfu/,X{CL;=q~ߐ lyh>7LoQ䢟ӊ)y1m4i7Q#t4[Iu-Mtڊt7^*[Ҥ(C|/YUr(C/Z`0UOh@ encj:XO*]O)Fn.t2j1Op.a+YѥdDxHt( !'K6hjM R_7L!;Eu /ݏnH"p~ȳׅ'\Du}U kM"WGY ۖn+Fůb0edu5L@?<!1Edf(A/2;XY5I(3FMs^j&f`=)do>{Q5WMN=8{l{3\yFeq>^9L\k:F_wŸˋ>c|1gPK<?a4nƄS:,cgƃ6cu(mlzǂ'mc7mih,7v#FHP RGpe6_1|lx3d q uco֓b05dƿ6* c?h@+;'+'.CqVkrQV!T;\r0[/hg>@Im+z&/`~1 :ʜ}#ܒz5rUfNWkZyy*E1:7,|t R/s/a5LvkïUe֓b05s./d*WSDZ.sv_Dr޾*8G48v$y0BIk>Ж% N#ǫϖ,>$=ˤ}D{_*x_DO3tFF?eSv̖o XO B[=t[΢ş wnr:ەͪq%Fyg>}P:q2 =~aΏXW kDw#u/MA|7^ vG׺e;'GE"Xv.@4gvP 6s|^8cA=F}s too=s܇N8zk:O.УWOr433l} 4e-;\_2Td SZhf :#{͉e3:>4sVSУBڶ*ׄ޳݆,砳]9ڬRvKX8̴5tMj77C ?ρ;ڳ-/ H?نןnqfS׽ ;:m{scmuַaUXgg%GA?oڇȄשҖ(d{^-%^P@p# 8_kd΁bq Y!+-<;iӑG mO=^w/x$=ՙZ-Dye;Lr'9ᦰˡ:λ?43U.Je$m&. hvXY FzR˲*Փж#&cL =(ȭ֌Cׅ:HBVm|](h0.;ٞxq& a&5#Qoϼ4m|+^xEˢ[LQGOKV波 x0 =i cAfi>a9@蠋!{zVhCV[^V Y Sa"Փ@+RD zR1ғ,HOwLO*˵3q+(1arѺDIp;-7I @'mu깡`1nӒ}_+Փrv5ӂwOpG$ !Z)*%r-R#%b 9F%>\𷰱7 /=?ݝOp'8b]Gx4OZmߑOpVJM]5)>" F] < cd޳e4iyJShl|>atCe5Rܴ֯=o=/w:hziS~V'ӎj|κU:=`mxf;}&30YǺo+"J\3=]z)PsikꑛgoY Y%`=)JIg60=,d4,}VRدpEjeF]^!\L\('JY@_PO kk4ū*Ky] _:һEkCKRіc=G*` ֓bj Υ_K=L1lw j[-cwH, OMY.ٞgq\8nթ$t5lo똡[gLݞ$̻#yq eG.V^6jT!{J[ 3ל̸09c[}xdBڶ*FoLj9砳]+H9h+R*q>۽+ 'ea,3UcKoG^Fyؕ&jR/` jXU-,` a[k2 I+BΊ51DxU׀[kT0c ,uLr4 N]ƞ"}a u#;t=|$/?8TۮZte0JuC˦u4(w ET4MD)}xEh@ICx.)Cq~-,yu3:;Dxyz}-5[@ ?ϡN cb"q\8˿JQhv{EO-r8=wA[0s:9]M܊z_}V)h~ڢa׵G3wdiNCO.4zyƣS@!=\N=οfeBFIYIm{qD:qRgu#(~h0pWSBZ$짆~C2ߢW.e9YESkb0 2 IQzM)6:puU|mSzRzR qiFѝ"Gp D~t{Mɢ[\R}{Fs2###Lb8*|&bbq[w~>mfȻbd~P*F  C\FVW>]q$rvTf|8`~]O1@3YAff".{ z|K[%*t[mTsdWAE&\9"vg bqTi8 zR Q8u4q3;kdoN~6U-|"C٨6]+^UCL&7)ɴ:ˡ׋Y$n+_)PS6Uá=DWpI=ؑPbL2 m,[U1c wtݹ+oIF_7GwM{mr弧{}nWA9bs<%xL_4N+d 4ɸ.ș#3WBtzAE[.`ɭna.p"W3I#h,la%ohR" E;8EHbF(gq _l]"2mmGpM&#ǘ0z&QD[T"*Wz2:(thu-uCD~nN+vPŴ]:refE[ Ҳ{x@_Ng{ř.3*GɆ6>^ƛ/з & xAk4,2!aFI/c8Dfܞe+}ezh+H8^'78V|²s5O)* %[)n;xY-\_[B,ЊXi.iɧ#F3 too-,lxU4 QzMvw^C KxU^hu*|L.uMcblCܔVO.@чLc >T?5R̡TV*Ec~C8`j3XO*]O ︌t%*<{rS硾R.^h],RSyd`պ Җgi˳2fdm}†}efA#rk]FY>QSOwk >lacd y!804ū\zdiuiPJ)BPNdV`lYFx;8]bkmcݨUfJ%-7;{ld1LmI15qү}^~s|a5ԭC1;^rz~aII٘Mejjln_cU(i_-k,Ȼ㻩܇ X0 Jz-DmLM/UܲxV-ɿ{-{ycWVśqH^"(tyWQ<xv^YR- \v."*䬈ZJ+bGmbuUc <X` |,Tu*'ИyXDϟ@DDH-s#i{ϥ;Ey(n]V\DT; ]wb^x+qʹTXZzO|q ,=!|8?oHJjc4Kr0<|ŏ-`7+bGm%y[ٞH[3_92X )Hv]Q䍑:mǜC}| EPkgV ~oOp8u,'_@~Op@dօa͸]=7,mZkf']' niAHs;x'#lŖOp)wIs|v@].[؛JL(aҴ AӶ =0uW:ˑR4^s8fл 0}G>5ZyT ѝ9klS֣F<'x҇B_C&8uvQnX1' ]wT1}-5[+ #.O/c,{>c+=QOˈO7QvO \KZ!NiY9[#.d4Fvi}J+ߐ̵[{v 2:8D]v"^-$K x4yY_E켛/2Kj6/=4-@+7 ì]5%*nH ?>yD{{7E#%t-EJܛ]2|\2ˡަYMyU\B?ճb'6J\1.PsJCn'IÖdn0`T jXP&tzReT4ySʭ=~]d:f$c.%l%]Wr;.5E&&zu#@zT!dM`0 "wT"nV`XhX gmEs, `0֓b=)`lˬ'*:HkZ]5Qr w8:Zy}ܨ5V-R^YR- _),'-fQr|u -7 83AzǸ]]5`{'OrH*JZ[D!lT.*MQN!^GdZHSVPEPӰlܵ-)JRyݦJ^ZYYq7yjzU1c <xlYXOJKIIM[hA1&脞IQ`k!BACEj6C.ښ|нԀ>p%`?/$t!,vߤƷ`P9M62|Qm0Op buXPe-p(r狳3=.yhe+]3 =i cAfi>a9@蠋!{zVhCV[^V W#WQAտ{԰jJb~x09-*I\O*'/g6|;i}!b8HiB KMf0JHMPѳ0F=[PK晧4q}23Pk, _i7z߶{duPe7sqS-/Y2;n3 =4ôݩ3Ԟ:}_QW"O遤VKцUK[S܌>{c vR^ < jzU1c <` aWlemwewdݙOpML bqv&fu'8Y|!cG.:DYܷmwB0û]û@+x[/}ї6vHa)}v; ct=Fی٦+\ '?}| گǟFOd %Cpܜbė*^n @[6EcC>0Ե_ ;)BP؎!c;X/lZ֧ѯS?-] گtw_KԫOU,%? K=my U[}OilPBK+7&2ZLC{$ MIT([] @(\ {{9X=&/1߮J>q>+fY71괝6 3Um LTmI9 % lGHZ8 'ƝKڛ{z oA|P^ [ $zY IkXOPc;}[310,նv;ƪQ]ӾZXw'wSKa0L`0 U{w0j,'Ei\?R#R [T|TXO)z[aTA"~WXը+]T''J%MQqj7_36  mE,ݿWp٫ݰ` *l[` |,*ՓJV:,`I_ Hg[Ɲ:,ỌLaoIێ dN`WAOc(DB_֟n-?"6C<ޭ+Gӄ(Fy(YD'61lQz6JޓwvKŘut]Lkɼ~P75lִI:V"x1k4(|[oW -*I\O%ků0+Ljf%H6'[iL%m풳$ SԷ y̥%64r]E oi&C&8uv2d6^t-gDѻLh@Ns`|<TOXJ_0dKmI9ܠA7*~T |Ca! ] <-ۿyuw@qˬ\1-?` |.o V8GE/|v_ (gSA̤6_.4;_JBU:N>1tn nưK62^Eb0E2m-].J3P5l&\: o{#뷪67d…oXf E,&U_  D[ٞH[Aя+W2ˀk6~jR=i[+)TO6M]Ĉ3޺D:)GG#'3XG@VJ'ͱI:up"F@XjϤNuyDz%#, e(Г r uu?q=;snC2 >(R:y>Fl}|iwDXqpw.fBF@RZ/X睞uhOG5͠xG&BBqߩ-?B /QM;NR*(zi H]zw_C*iO]zs5-Q 0Ե= S)8PǞяʶ}C+bGwWK75[` |B DS%`C 06Y}[ѻòn> F!Z&D0  M\)zO  ֓:TMt7`0 B,'U I@cba>b{MEJo*H-s#i{ϥ;Ey(n]VhUU w^|`07[e֓SRzR>Eyvi ΏR4-'w:؝BvIDM ApOe 3>A2qJM3eddA첝hc9[G+q]fa=)JIu-Mtڊt7^*[b9goHH.-A3M_-]`0/]xuIBp% [Y[+kʨt=i[{t̪I\>uKJ-d!Gi:t A.3DWt(K6,X2ԩW't>A첝hv5[[ Wl0XO`*֓;b~ˬlo&B"fyΡH@?.Rv橃nv>̯8r1k2=ȁA۸~.b89a({lpo0f0D?tn섳[%lqPgٮ:W`@BLpmNQ(>821&ƿ`OJfx O4:DB9L(.|=:{n ȶ*7Ç-?7~dv?mnOjh_4Q:ۑVxyaszļ*v77nczR -02 (\fa=)֓b0 DUiM5"W n>e֓yz(h|h9sVTuz]zљ_ =cb̎ƚ۽߀joncxKz--Ȑq6MŊW(/StMJ2-)+r"ꁿc}h6 EI.K1Gnr/0L_fa=)=%U'5mmGpM&#ǘ0z&QD[T u2.) hk$ARZZv{x lO{[8tЅ}bfC7gxEq@z>/amC񊦗E'+&%|x4+ x}xֽ"4cur#lu=',;]xq4܁]hCV[^V Y F^fU's3nww>>㐊u1AʥRh3u%P&Y#-(ISFsXU}>atCe5Rܴ֯=o=b(^diu͍|9[k6,m/fzhiSgɩ=u&"D5ؙM^jGZhCRʪϥGnFQPYUY"xI^u^Yl[` |.`γ I2^>+)Wb8HAa"5e2.z?F?qKh @V&\dtY.Qo, /`wЄiw5`hWEUxVW)·t*iL@wzHJ%(il'g5[WK1RYMa` u* U9+TQW:.llW lx?_Y,'UFIqQԜSgO.r<\_pKEj*ZY,myVƌmO`|Bp64Fֺꍲ|>ל}2=$CpaiW*N] 30RЅ>٭>kٲ>vq{mtGWi{칯z)TR*rcUP\ < Njԅ-*-?^+WƝKڛ;jǰ n:I.!Ty6:?3"w֥;s޾kT?qDٶ_:פ&?@ek>o0ct+m>@oAukUYwr~ f+?s- nļXؤHY7eꑤ[Әvsᶃzc5*ӓ:s؍} g.Q(Yoz潳{+upPO3;mU@7;~Փ_JW~b?ʹl34W U?UzoCsۊdH]dYfNƖ趛.Q5VՌr躞ղ;9}H^ uph0煓r,};}g쌞neffffkRWs=օm5[io:X 7 VGacjF}rƇzd;%moP}o3g\{5Ϳ~ e< K[-i3 Dt t狲bxpw@A: IDATT9O)U!m[kBunCsޝ_!.uLK9ڌGH*.o|7 T`0w"*䬈Z`0 ֓I4&FQ'4Q4:7\SDeUͥJԚ `ʱzRzJVO},-='> >N@w7$ehh5[1Op V%C\ 9iGRD]זSkb0 2'r= w7e?L6l֡^W\RR^s8fW2~ ԭ>Oʩ51 S` |Xfa=)JIu-Mtڊt7^*[Ҥ(C|/YcP.e9YESkb0YY\9,5Ej- |e[/T' pޔrkc_:Y5ɘ'n~[IC,RSdh2mw[n<O"t( !'K6hjM U9+:+7zsGӳ6,sOٌtx[; Jx36_F.xe'D8p @Tr 8PfK̸KJJ9\YHS&{ \R9k7;a WWJCw1D; wQٙ7CR ]\>s٭{DFph/"~iK\_g , 8``gƈx^z2b*k% rJ  _CӝJovLBE աXW9(_8-wye۵3/oLe?^|@w'X࿓d.'6*Yk>{Je˶I֓,KXg~|@t :(}ΐs7Gl9'W1)G@~y[v/hc2`os>zReDSsq^8Ѯrwj5XOsy!SؑPbL2  +;~o/95dyg>}P:X`&&ٙbmv^ƛ/з & xAkW4,:Y4E./Uiv}YQ>cC^F1:l8@p?VGRd+[-q`/,ˬR:z}{fݍ'VgR.6(tк@ԔQ m12ق2^4T?`!rՅ^9Jehb ;od1U|ʨt=)pEN=KRzziuHM5QW6QSOwk >lacd y!804ū\zdiuiPAb;%|[}>e}8v)鎮s_RCTX{gϑ3* U9BvWnh4jGEdFFFљlWXUc obj'Υ_K=L1lw j[-cwH,UE4O_w`o_iz!x۔q.%|89z- n<8{l{3\yR;4HFX?GqYkp/Z޻bes@8q>^ZuU|Ӑ:T˝>&ҝۆzN}}̦v6;)9<Wv)Hgٮ:W`s=\>c|1gPKWzL<?a4nƄS,cgƃ6u(mlzǂ'mc7m`BfO~1jGb] PvE0.1sоx#waM,]>N*bCMP*- `/ŮXk{bxm( v`"PlXd *2Ξ93L6ɛ2ɮV](}{ ^\7Hz:z/0ϡ'U4(3WZ–h>ݾk6dYl*,~Z\cYiʧ 32w"h7g4i>gd]t>Φ,|^Qⰶ+3GVsh @7i':+/_{`7Vxk,LoW$סOϐEթ~36{6*~9fդ!Ͽ=nY4~F"rgQleKZ$>Ql]8^I`=)+E/lImnen<{_x{E8Te~֏ Qd^iuCAP#<* g7oo/D-Jl,, l|L^sF;"jL;GcgNto*gT.Jw@ݨ QQ8&O}>a8~xuWt *ul/j9Yɷ^lI}4ܺ/_ؿ+@:iAM@~sGx/5XQM'[Z3QDIb|譝5[~"$xM Py6d;IO'ftJd.xx%2Zq*T4QP3Ι8bq欄 ~M/_ܣ,x!9*ˌyF/wػ >)wzeP5wYgAoIh*@ͥ]F"`0lSzR ĄH(%G?岤}k{t2EVB=5kQ_]جYPZڅ,,4# 1@hfz˕/(o<:sՇ{lVcϢk^ro &q8}gϜ[+w.0h R.2O^fxY6R|v)a~c7c36uFpb&]2ke5hOxmx[ւpXO㳂7uQZ;CbQ֞ V޹Qjr;8,۷z}kCs?<D+)h I_$ Xe07时|<|Y^rҧlx1%[+pPh/I]O:|,`Ô/mwddw[ei[j,y|q]EgҳϢ[LhAOljy`0 YXO2ד9mqg}9[1P ޣѼ`醜\?MќReheƔs_ XO`٬lVY8}؆cDmL٬J%JO'|k׉iR ~~p\P=ݟ.#fn7\A^xrG\fyt*K qg+/ڮz+ؾx3d]6ȳ+ @_Axe1G !2)))t$)H2D܉Ƿ$W_2pb֨u]jի zH_XjR%‡42 I1 @6#l ?vFZ[ZZ՝J, ,s O'/sԕ kO^8z8(&&A(15gRgg n%l_x͚5.lLX~'r t^2w{wM ]u.%lF@,lQ繌mU@?ś]QJˠW26+,øT.U(|(P1^fq(G)5l.Sc?sJ~_X < F+b++--X3;rzRuI&z+VzZȓ݌JMQ#Qsg%( }Dr,TThZUGFyv4,eb-('-̋ xP̎-𥽛Uжv®RBǡY-ah[p'ڱW<ܪ L{}#\EvU\p'%bP>W34)^"%vWpPUkʭ-r n;NYWJjIl,Ml~Q$Rp@([^T{.":TƽN}PVZЕO VKғ_2ٞt&qDO ѳ83m?9 ZK#$_./sXD\>3QDIb|譝5[~"$xMfzg1^L>t{bFڬDCV@)q^g3XiV4ϳn1@\Nkn0%Rbo3Љ rV~VA=hye^lee~nW̎-YZGU]rpؿ+`Z}aO<N;pRYξM||UUu=6Yy3"ϋyHi"\?Rb_Lr^$L$n8BgӄMylqsG΃puw= ZL&`2,_WڬY;WՒ<.2[&ەrϗ j%]EOZ8`]׾v%xa;`o j6D:XF.<l &W:x?l:~Nw6R "ͰG)ݤ%1.NƿYqGUJzeѲ[^fʳBvȵ'Gɸy/86 E%E̜h>!mNe#;9! 뱄@W(z(u:{Y( žwͅ B}l?M!XORNzw:˜B\}ʑZ* 4}zcƣpdn?9,2HgKեPZ(hFu񪆤$_.ƫ٩۷R- o6n IDAT n|}fwTJoǥ$R"`jEfW)* Eյ1HeqN"MVCm;vIb%YcHfQ./F 1DxK9`,i<?Q[K{7 I))W=)7д {#[Ax)B"%N7cR.2&P=ò}7{{a(C9OyԵ3 ́ot o׿;/H!laᩃck*'lhS&8 ]u"{JIj| "<|rI/k8ER@Ե!}>kCkz{бo6]rg>CIb %Y#zXul6o]9>xدaKd["98_V[5[U1c |ij-%mc'QEQSg+=^Kv;":t ݸVXd =JOHQYZS{ {S7O x괮VznL6X Ҩ%K 휂9^rU쨗;"=AB^vk= u/JO`l)w)=/W2g5xչZAd~RuY nQ kPh]ٔ`}J'6 be`ՕFv7Է_`G=*Cx,C%eD/=Ak*r+r5IOH5Kpe1Eu6~C@Ӷ]BY-Yymw\jGQcO~!=zU~k(n1wXT]1EΦgţv8za(EQiKW}Vpv4DY/:;(Fpd?|NjW'+/oSDzí 'G NxOQu)ul*/qj!c-Ҧ!)JQE vtttt5 ?Х7IQ{7ޓ*!TFtut.:0%0jvΛ] Ianmj]TZؗ#|y2+)^?G6+SeU[~Q' C |)^)[O.?iFÝ!jRNSn!%9%_Yqri9Vn4 zs@O=i)>ݷ(xGD#P(IDsoeMQدbip'q^6(,к@ټ^+ȇ?<ѯFYu٠uujV~L@^)+~Vrҍ2~A[NˈYa0_^WQzbԥVv_fKo ?^!*ƷFvow@ǔ:fj"m+\s[I>GEU{vP*;+uGtkY ԵSh:NE;n3Aj·RPfjHyͺbnCeਘ[QP7&fjsБ-ødFI,b^E?"ZLx/`穀mt? Pit༬ HuOh? \eg&\qj8Yi!֓*}!v|V'\>?ߡS?&R.ye(^h],=)7fjܴYV}AO p}D8ڡr7K7J0׭OV@_d81dQ|'ǟ3wۢ~|zeSuQ0 FZ+ +:.2Yҹ&KNz卷?[ BL7q8 0  ÈG7}3hKd/[EaKvtKx֓VIIFeJ_cҧۗ~U>旬 7M%>Te~T, SY|7^7=qa.cSwXh<֓źUgq6JFx;+_Q, \z>ob=)֓b=ГV%V~RF Lt-'X` |,'zR' ]ޠo[5xlOzQ#<[+ql/eV!7 !nޔHۃ<-Ec|a."6r ߜ mmMn <=D|aWntMns 8#=a{ǵ7#`ĦE̹ūgtr_C/x`b%u>l) ¥w S3z53 #>P{?F ,lLD\~/r < =/lrc'Xz! .٪/4\~Rލ/t|ypmp_,deޣMn0ƫg.sR|d+m#ko07(:wuZhzSyQ$l)k|NZ$[۬BM6ny˥OZf]J2MthhgWRA@CqV׶ƝH|;<S=V};yewy4m0c <xt++-?2[t7R@ж"z֓Z@Z|w]>=7G'koN/0mdR'+ zoiY@`RxEC m*C4ɡ' &rYӓUl]:lľC'l=i--IvUWǪf6_V??qs E]ˡbJ7R'2z Ը+T. 1꒽VO[jc; =DeH5EFj"mmc/j綊׿H5GU>Kn(vaSVY=Te&>Kɇj/ 2|tbxvU 2X|z!}hDY0PHogⴡfSDSMۨ }ba&,DiGw{,E{zlڍ~x&M&>B#J e(Гruwhv3C8/A7}!*C7|3IBB%LOxZ:gED)CR9E}Jme$UR=iLJ3j\~!#nۀ>;v(-ii J)dCb'֓>|z$گMv0zҰ\{bŧAw$+ӓ9ғՕLIklZKMK:GqN Q>% z|³f 1y't*b]ުX72ވK5@WaWn~xof2Ͼ1֓2a=)׃ XOJuT)XO ֓b=)pfa02ͪBl.`8bI&{SzҒzRuIZ;,QXQXUӳEzRfJC[**C_-jtղͥT̒Ӕnkݶ+F-` 09-ezRfJUOO `Id=M∞3$g\!z׾LZ hnjD-'v~lH5Y*TV@`&>ѹ6+Q.P<{DJ ŚutB B'6d_Dʙ!m/xtV]zukUS6[` |ŷ+L` |, zRrדf:7J7M2(( dtm-f^e+ mo߱mHf{j.4tgS%j2IWMqڛ6Ю@;8`](T4m @O$qyMӎۧ Y0ko1^F >l4jNhWwڊBBU@q3g%W#hzрei *T$Q.3!Pb.Ciޑgʿ%Jsu4:7N ]F"`iZ|i*F-,lxla)1!ľ|3 'B XPyB ڵ4p@R~\?[0 ȴj_5`){a0ŐRO  ֓V:Tjq3`0 懖YXO:?'1%GsƴP7H b0QCx,-攢 m*p3WO^fa=)3%'M-/v]3Z; pY 4-m" {uY(eT4l )q^:iMeDj| "<|2<0PAMv>Exn^bW -e*I]O:6L~G@vUU -HY ^4xl]5 Q{7йl,'=wuZԎ}@UΨ]*x 7rt@W| Wl/e֓=ըmjB"}Vw})uנ5qkY,h;mcF |x_OeWH E] +lڨ=UGXAɏ+` |,Te'st.s0bl/t|G#z wyCb&jahzɺ2^m2m٘3wۢ~s29{x +I*ill#,zR SƔ43!cE} G-{@ec={$ IfN3gv;4d~uQd>un`tuz:z/ڀuӻu ;AAnkc\p)ۅj8g9I5 n?-&f/i!cqUеd.oPLֽ4U@sp`YļZ4\Gf߰FoY rԏ]8^B#;E۹ovVcݕ5FTe-tȨ90 # 5A^%S] /,o`0 YXO `'t`=)`0?o `JzRfJI1 槂-2'zR'*gwU,8Q <ۨ#[xe+^+2 IQ}@U %XQg4jgz <: W| Wl/e֓*I1 eb-3ZJۀ8xeW ؛`ʘrfe?Z{\Bc`?Ko(-N/?9uaCi_]gI&c{mOǣ56:40qGc*Xx_+%4DoM6p)SQU#eQ9D4ӹ͜ۏ __HO5(ŝ h"Ɗ fϦ^l~iʝXOKJF ; ~Fh%n+Ba/fJ;Ta]6^8?ڵx)t-oV8!GfBcML,_k ]c=R,Ϻ Ly DSMۨnbj4'Ň nTgܡiCʹەtxx5, MJ],)3 к8ȚqssSKsSKsfuX=φ/e)duCP;\_n[QOTb܇qn}}&C ,,!N8_>MczC IDAT>C*^e:YF]`ƮkѹqR]V'!WNYu\dsM]:_ob.7^PRY!N8y jTVU_b/j+%AZ% YJZ@|ܖ^w yrۿuwi>qo'$ɎeeX֊={pɒ-AQ1Uyғf?\Ks8XF_ ݏΐKݝ\QtB=i-q:cl Ahnw*jX}Qe]:_O4t^(a֓oVa=RED_c(%k,ȺgS.`Ur>K؝}j;#&B B7/&&k5- 93J2y-Υw˾ hnjD-'v~lH5Y*TV@`&>ѹ6+Q.P<{DJ ŚutB B'6d_D ˬBNMj< b2ݬE[bq>2ϐd,ja&oyZnê}![Z3QdI X݄|}G;8Vֹrd1^LD vܴ፬D1̘h@^}D|+ g3ykJ*ߔZmΕ(RS({Hmi.Uva0 ~)2 IQIO.WgSˡ9/8_\@9rBSkU ̔dF`-%T֐LͤS*JV'Z._?!+L8Qxc%e6nF/1m< )1mNqX{$ӈ},,iO|aK[fF0?CI,N@L^fza_BRWx 'ahaaһ;-׎!&pzg40rzoo)vF.yun>^СY) }tQP x`'?vw[AVv Zu=zɽ_.3 ]>snYܹ[xn,H! $R"iqk5`p?Gp;S:LA@DM.cĽI|Zqm ړ^=R4`#}횩:㳀; zK[Saپs^sm#ZZckҖoO9KNB0? 'T6?L'z҉ӆ)_4Ҷj-5-$O3U]J/ E|\9&P$~gл f\|BVſ/s*jK<!dH+_}4zRrғMZH}`-^RS~.=,ZUr(K/Zaz闝E]K|{.PVLsv Le2n>*841)\J o]J_yiT?2 IQzR?M7l<a+B{4}7,ݘRSdh4qW [Gs>èLwrrd`~[xBFBaSCm oN_}ˊ%n~2-#_FzU3s 'g<92=Y慀^}95+"3.ONV 3D=yƧ]y.~.Q2 s恤H O4]C6^bzmԐދ Z uKE!>klfp0[EpG>:>}K8L#; a7tU`Kf/p)rlugtȯE( 5yn5V%IoX$6.uh<-.yVa.ħvZ<E@m$H)]Z}ͣc'8;XO󣌆F9@"Ϛ$ 4h|tKSG>:a}ow(JP[g1;|{cot ?PIIID=|Դ)k.tL5*@0.jgo#o/6j)5$I Hm2tҌi"~_E򠬿T#UG/4#rw4q*gpC }d[G] F^%H^PS嶾i79d# ` &=H,&dF$IRqn'&IO5HVYK4(f%\Y蕨uY4wz /NpޘSY{xxOnKR@bDv]k`7k[y4KaӮѝ<}wW%6&DhΘVF)A?@I # лՋ;)q,lN)}`,7s=|`~ "T_q yuoS*ҘV[û)aI]g$CcAZж"}QדNڱW<ܪ L ."բFĿD(/Sד8-T"%.h=rk\@Ck)vP%ؗC6QEݓH >t.":TƽN}PVܗA{nh=)ᢳIwnKQaپR꽽tҺ?,>2{ `-W?i&_x!2!?7?*_36hHI_yCZON:IEzR*_{sֆܸ<D.<'C'8#?_LOԙ`4BgϜzҿ<4[fa/ѾZuI>SOi~Y.,tP>2)`t`f36f;{td|=_vp,g<P_B`0J@*`=)֓b=)B3iz+ Z |a0.XOZ XO bX,v~m#_f5|ד6Z UDqo[zg$.$m`0 MC ;e[)SxUViiio5_8qc8^$}qoɳZG>U=!+o۟zV-$ϽGجaYtn':E yEoC<]vnޞ.rk.#mSsB*l3qUտr]O "m.Kpָ3so3xAI1EKKK[[[MMMK^Sjzj]XդA/tAsP˗:m:HvA+yjNEWgw˱stUjgNo }e&'95Tzqu5`?{I,`CE\BS*QW@,خ k*6T슽Ył"+mbE|)&yuϙ9sM&䗍6laCV6lqoBdgF̗BF,˞su@ݿ]s֑ZdyϋffV$}Ch*e>;vz>1n;ErvM.X'ƎK+(]{ϰzX{|s#bI21144G^A~Y_}2p9/*[Xʴ1e;x_uT0`*I&kYcCDA6^pYbNL|Lt{1_Nm ȚMk*F6%F HߠI|6q3(pľݤK<*2 QS%>ӣ.+|q_qaNYE_JVy~ds7zzK*SΦ(y%*No>)UxR(F sߘX NlvE ?!]Ps~'ųܻ\J.Џ 'E2N%zfbL&^b7Q}Ío1VjSгg1󕰭qwͯ@l!JJ?zYytˬ!^ rnnlr(|{;dXq>) _\qDrFVzY[sg$þ=%g\{9yPczћ.;i>ݎaV䜝hm|=[N۸c=kl[ c)[MV;uN u>6Dּ%T @"OJw8~Y7KI:l|K;i 5^VKZKYEodlƅhrY+n !gi¼iƮc)X%w, %<)*'#s !d>+YI漾9dhɢŒ'PnC]SfRFgg g:,`/**Zeڪ,y=Wj i_^Mhns|OI}+ƳP)O3}8yL̞[RUz{ Y(O*J' Q3:'D]<3cW{Ow#y)3d(- cISk-M,M,ML2m O:U9ɍjtc?vp7b9 4>²!GI#bώtzK(104He Ud-O%M+/2sYli:w^խwp 8^}C[I/pm읇4OYA~gPiu)!e^Fal͖W7ُ.[SѪIDyr eNb.SH ʓFfJjDOtXUʋֺTK937|+J?؞xF>:M9D?QZ%k ,JJJJJ~ FT:`U$OZ WTc$ /'=~9ҭ϶rǑ_*Ԕ׈!O^j<<zSSSS֯::JA'W7$(K)fzoч7O=Ew,~>P<<ʓ*_f)k_ir1dLJJ"IT׻'AP+.RC?B1))|yZ|$o?Rg)eU?v VXfܘnFs[`ߐnCWqUU-'@@WKQ7cbΎGJ7_rC#[i"pb`w5^jA\,y?y_u !jt9<ـIs0"xL=!9ϛ+OE"xy3|~/pp (͍6sl'C _y*a C&g6`؜y+p G͖EPvg<Bĩ%ه~/}90B.уHKl<š}{"} ^}Q( Z<D;^Vӡ!]/xz.Pi>>i,z{, (=󧩻SpO|kʋ?r(r2+R2ϫ$zLwKuFC^!_[@2+UX- 9nSI.zњ_m[jƢ;F P8)GӔZZO S[MiaD3cytbk]ZS܄O#Cf юƦ*?|˘|A+܁fjb$|~ǧ]~?,'⯙U] wI(,R߯\ Tu451o1]-5qmaJ^JruLM9D#SSSӺLy#B;MMv88~gv ܂A˟sd+ *`#]+`ۚjѩc=f6uܠQVa*2D_oq{=On~8+99*eʓ2SR"͢A]lyR o8d-a yP@Sa"\SIvJD _X fXKI'rrlRqj,)TC`-n&@U(֔P0{"Uk"}]|#Q{%b fq]\: zN/"me*}V ^#^C{͓m"@Q\ux/%dlv(y|+UlWGvV4ߞ/W+Z~v@A!{q .*`eV1K6i-)HZV{byg3RK#`OO3:s !W5u6'!ڸI9 I]'x4TF4H< İbmB,LЏ(骷MaBH6fHu)VZoY@V{YJ8d2ƴe;ף2efn'xˏ/nim.hzkDM áJa 1XJ"B4ǘju>3, X/&&]ǧo47(}z>{eё0 >64`0]5PQ(]'kX=z>K;i"(QVR^-O/y%gr҉ק7] J˃NUb;V)tOZ! c0T|=_͹QA*Lu{V%yP4*0TF$*I56JoK%ߓsص[i 0_~ё%|a iuDYuz K%F i jQ  X 4:6/eԭփ@ٓ4i'\Яm +x:J:-%FfҬ^hɝZSyC{zO-^y[ϚCaXhҺF_=&eX|+DH^XIKo0-˕ϟ?||Dq~_UMZzg^[]zuz?!^oX/B>;>qBe9)_Фމf,ɶ\5.Ա5A(ޢk5صIQ%a|tq <_(*GyRyT<7N;bX:xwqǼo)|+"Kj-}ΧG]wWc|K :(R5$2~^CR%ibBeN'[>C?/ro_)EbpoЕKOlvE EXJp7 LSwARD0Hl ^.+5U. A&@>+ KZQi6$<']GY Ǟ?xUh^]M9)o{+jua0]&#;[1~Qm.ۅӧ|P?uI |'̼8v xC˯[wuT}Ox eusc9=f<) Xf!TI(O  HeV<)AAI;PAd#JӀ H%Y(O<(O U24DJ IDATm?T r^S^ϣ5CD2 I)AyR'E7MT5UJ}G*p"}c8Wa &Fv@J8GSf}C]d(Wf(OHT.Qf(qۚjѩc%?@d)t7j4yxN\#3܇䩄2vVfZLY|ZBo|CX2mu+{ m3&gC5ظLaȃe۪!wԱ.D?5>m'u5hGӼ|!UTouo]WY:X1>OFo*)a =6=ЬO <;L8\⨪H3jnɚ;.ؽ-^HV.oJ~X,K \d~'d61T e JDyRA*^rP'EebByRAA*Fbߝi(\=pzB/7OF3A iΜ4h_osyPA~Pϛ' JRSʘiƷOI,ƭک)m:wU͟ȞNm @|# S{J%h"FY(]dK*E5=\ ' FkP(<;MV do0\[Rؠw:YT5/}s{m?*>= R9eʓ2SR򤆍լ ހqdV.D )rt3Vc5HY„1QS:0bOmbҥ@=]C8o…)?0=v6X9O6=Y2вN< CbIӫ@NVR\oS2<(65T׼gi[R_]ہŖIx|7׿ZUފWwob:oJX2. ޾hмPj~+~ˑY߾M$^2$JHDʬbPI%cN~Gv/mхO.Od5)9K(Lc|QS= X)fS\mt[Oz hڪnΡ<ʵ҉1w\ٶ|<K╞^rz>K;i 5^q-37xIk1kM [͸MNpV7}eM!u3lv3ImhyM_[1Ri[b x'N8ѪU+++gJQmm@Ly`\}.@55nШ.A伀V6J)d\{3QL@'Ó ]}͒&Y3 Yf<)*'#s !d>+[漾9K(,c)5U0eX.5l$F"qâ[) "kR9MVY:Q:;zL_=!*+2cPCj6g+Dzћٷ2j<Rpm=C|AǻP{)YW[׬움H/hhh@9VyؠnǩyOة.k9uǒMr|]:!on5́]/ez;ϙ:݋k+ H%Y(O*J' Q3:'D]<3cW{Ow#y)3K^JSk-M,M,ML2m O:3k'45T[UNTbqNF,yAf[Xv41a;= i Wʓem;7&?V=&oYSIǭ\7[ګA컻-hu]!\z73f%rMwU@!7n 5FCvzϸw70k^~Zm#m9O;k3 IQbz?^赊;o4lyu5a˓Jiإa$P9g4 ۍ:f7Wz XsMuiym9Oj;e[U{G {la\wc+M(_ žU ˱y~]f1t$Aܼ}oW/,PTHҫnݜ~P]hq?~;ZŻ\^]9E {6a)*Lm׎1WkΗv<-h` ȏw)A~2 IMPIeahhW( C(OZM'EyR'IA~PIQIVA.AyR'EyR'dyRe(H {.!b?RYM ke*my*hզaR_zs[y5Bu,'(O (OZJgؼ f::JY|36ѧHFmgN7X/X^P;)G,V?~':_A_ByRy<) 'Eyrȓ*.d>sԜ/Ye)Z12J2r5Ȥg_ޱ|P:1>vm e8@ ȯ : ? (Oʼ#ʓʤ %tZ<\ZEW@$&*bznbptPj?<@ԜuuH=܃cػm\W%~Î]G5cyKJ&.3{|r>V#w*Pɠ<|~(yҴv寱fu rf alMsXV8OpUNwThɈ]YQ%b<t2!QQWr1C^-oAfWNSPWvWf!Rɔn /ng ]b񔛞Zuf>N.A)7(OZ6*.O 9"P1R}i w Py.!ES)+\*GZAA2Y(OLIʓ8 a|;]+邜yx|3)Ԥ5A@U ʓT<_WմR3?yp]ei[X?-jZL5g94[P.\d cLN5y|:_JZAMaŜR(ő`A*/PNɓ~5L [ NaNovY[R2DMx: \}WFY ŐDȀm U ~̩2M)ϣ]Qu0TetyR_8k^G+ M%%lҍ)jJ |zdN'wI%ȩ  iM^Y]-y} JN16>u,Wa &Fv/W)8Z5#R ]O!3z+Ic.]o:B"%U3ʮAMR@84RWnۍ'uڛ:Y?7GoDڿipnDup$r. ʓV.0bCMQ*]z:Ə h .z/O2 ޜs˸&g&A)|W5T;℥@ kJ\,xloHո* Q'H+x<4w T#Ro V3mW.z8=%Z s4R"ot~9R~gV;WHGؘΫZE] ƅC)UGyRAyR*t:DE)YcW?jzW~.c H(O  RUeV1(O (O  UPf<)'EA,'ʓ" R>PAJ4D@ybPAA*ˬ"yRL  O/XSH_q,xf>NUuΜ#eʓyABkMkߒ*r&4-t>'LEVv{}-?" K|uV., }U ʓT"eV(OJI"B >)~V9o / X DML5K 3ɟQ+H@zȚTeSUNa[P+#f_\ް gB(o^JyVՇV5k, Xf<\*]&Ϭ)sFRnVY:Qy;y_ glarȣ3$^+ON1sߘXJ&oYSIǭ\7[ګA컻-hu]!ß.>VK)*dq{!͓AA'E~F]Ji Gz*Ѽj[z#T>e˓>[A^m#.f,%H LidifέH4ZM+U5hMX0}cҭ;y8^r@ H%wJBS[''+|x~Jx}w ps~ EX?Y|[/ǖ;sbӠ5/#;m&~9MH~ "C߈Vuh4Au5X~FH|ͬH:08<юUNy*aWUϛ6`P;r#IPbKcWz-K6v%xa|{,n9ӥ[&E S 7"O{7hx.$Xu gwԿt;y٢'JN>w". Gv &۽nFh7?\w{P"eXK)J gIA}o2rS#oǔ/RH~:IQIQI,ꂲrKO%gu9Dm q٠itV/\Y}>&zKjQ`lQ뽼FYSE&5|/PIQTC'~gM4tVUg3.<>_@LLa eb>sk짠'ʗɏeKxWB~@g@ie HY(OJIQP2I?ԉދۮӭw˪ZOj#Arj-U1of/E Oz_f1v._'iSˋ'{لAgubQ&#HLQGU}*Pt#*P<\ի5eʓIY<)ʓUT('s e孚wҏhj e/#j:@fdxt/2zUse_ki.ۧ"9//3];ՏnwS%g9:uA(4<)!O~aWL7!ÿy,ﭰg^:x35ϕ븥IN̬ϜNrhL9qM>!*p,oPx˫%)J؃|d*ȏ~2 ߺ~'xVz[#WcND娖 ʓv(+YiMW1|G7z\0|[n]g!E VNQq-|44/|&o$?ġSU7M ϲQn}p|B9>T )vQ*h߹2@ %y8bN9oPK\AP̔TB*NMJZAA YŠ<)@˓u_M+}ny}WY(5[Դk>8rh@%]%ɘǘju&  Pf<)*'#jR $7&xe ttxR-" 9̋!  H,'G˓ouo]WY:X1>OFo*)a =6nLQSZhӣm's$"<'5lfM#CFP_t!RH[AZ&ҡ!=&e~r|o.ϝ 4&w.L!m5 uﱳʡ|ʷ'ϒ퐁tbWWK╞^rRU"zJyFWHW7Ƣ7}K˙`;д0iq^J[.8=2X 1XA̢U.O*u3>hk.|7vy"IɱXEaB 䋚a:O;7/2VдUV?cMCyk%!c^\m!y ++= dc|wP +jZ0fo֒ T IDATczћXcћ&:q!nʺBffR*i^^d n9SA,JAyR:U$O FB|VMy}s,)}QXR j`ʬ\jHX DLESE֔r(t9 tlw<׿zECUVze61*&6*m!6g+DzI5ɶgl:|Jo 2K9+ Y(O*J' Q3:'D]<3cW{Ow#y)3K^JSk-M,M,ML2m O:3k'45T[UNTbqNF,yAf[Xv41a;= i Wʓem;7&?1[Er Bo*dq+3͖#zy|n ZzW(chhViZE4qpW~y*aWECkLbs@%JL'X7S<aFT[W"+оs{ \8/ٵli:~ӆ˥ad^#H%$ YyC2Qz<܀w9k~wx#ۉV Sz7/;kK]N= |[;3cA1t u%3n2cYxl=4saNsAgW뼹Эo>ӡsNӷ^rs厥} R@HǼG' .KҲye(PONoS2tnfD6!QjON/P~^g@F-sghfF Z1aPp>1R?֔q1=>:.ۻz]], ?[kykq#vu;r_` RQ, Z?y)3lE*=ˬb$/ּ%`Z#^Dt"`Yʔ'KIu+lSPI?7=F\, $% I:i /y{ ƣ6>'Xn'3hPrk|aH4-KVcmBJf &x7HQn̖gxOUo= 7^<_WqOpYˍ]ei[X-jZL5=rzl10 0uIsݿ-`^T1EMiO \.9d! kfnڵ(JCCc߾}v=D=$ t~[k<(F-OyTxĐk0dM״EO"de#~[Im "L˕q>'~ ǺL Rwϑ,z_^Cͼ&"7cnio܊Ot$T\P@wϲ%Aޠߚ+R5x̴9 izYŒGs{9o^}u.T6M}͚*),,DN O ~7'а ȩy׺:Boo{OY3kN31~A~vou4p_1,kw[1i mZNF1qv^˅VxhkO1w]fl FO`!ff.Ǐ^q?%m^wTުZ?Ѫw7Uw,0}A 8suNZzHS[r2K lsw/2pTچtA}ƿq-szZ/܇@xcF?ݻnݘ>[C@|n.~]\ ׎ >R*M}E4tކz{y t tPql/&f_p ^ݵp9.p]`"yg3;vq }̶Nk[dq2 oxS=REs^zʩ*c ;m ^U~KuKlC8kֵJ. lҼdu?Rz>lb'3BJvQH>9n9xӁ#.lʓ<)T-iiչ\.QUUUWW/ .7)}>ed]T7;(+nnxah ,X0|M᭼ PNצٿaֹ5Lo؁9g0*]l 2bΑ{yx7¼_c$5-=.[B]3{#o ƃ@h؁ zv' ;P5$ݏI, r]iEObqKwݶ mWVz ;tTlT6>oWpXwN7,Ir\%anxؾOf#ŦmvBptINicII77\E lK݇-Ś2y8V˴yosL^{|¡,[ƒJ8"H._ tAlG ^_.&A*}z3|2x؁MJ^)y%osrrq (H)Tmpz,&zg[j\sTkg}{D@zJ+@ɧ-eޥZ]sc~W"c٪osk?߾|ꔴЃr3ŸT45hjl|!{WyA͒o漎kʘ qٗ=f36{ufIDmMչ:"H*oZ&Q4,k!_Rgycn\-Zd Պ#/6НDjࡿN Y=lXIDp|&ӓdNK :fجqN8Y6_၎:Ԩ_Yvn4gŻ!|"Ʀw`)v̕7w,u{Mgc]k 7EmVmh>1ws =_gZe(yAmC˒ZRjs+ͱ`Ц!!=9x?زOc}HSo: ~+y|B&VhlGn׋n >M7;۹,ɴG?U w*bG)]CRѯ8?IOio^śǙ%KO`o?XNYjGݏ;~%?Ďn%Q}oQdԌfW=mco/z$gߴ.;lyʒ-dI.83YC>;08ll2E!M$|>`jUSj!ZVG9Kum&Dp|lӼ]O>WQ*5(Ve8 V??똳3ԲaUj֢Z6TBzŮY#vYP4˹8ecvǙ*OZ͒WRW4K [)y&Vf V®ڑ3]{bzUuθͿ,R _?x=:"&[iҡYZezRvALﵮtPsxn oP|P6Q1wnUSku )2 YϚWyyzұ&!=v)'G5PdI=ց<=il6ZJғ6A P5Fӓ>ULO 'm~կ R:U,5 oî+W)P4 viC| GY,)FIR~S| Vwތ)VٯV?a4k)BvF3CYhyؔ)_Y#k]sW Y<oʀ,k0X^dWݥHAxEܳ )4jٳz ⤊?鯆 ?J}u2h+!l3}}CL$l^kӎmlHX4_I;_%ԯZr~RG)Qԣ72dG6ԶwT&b dY 00 S1|iӡ^{, D &ғjILOqٛK٢ ni69Mܟ0=JetXVzӓJU't].ޘ4BS<_Ot-[%%BeNnKPCu',t/'2=i\i+Iz@MLOzN*`ڳ}zldVdb'v?5ϼzv$+xݯ_^֬֠;Rڵ&5,bqMc6lUEͼ֨mCi:QΦ " *HSBsv%Z\`}\oCz*c/Wj/Kz! Ӝ.z] Lwl˲:$ѕoxR ۹GߣۇuZNn/O!05Cv4}ilJxjƲ{v4{51p;X5@ @ h}֗nVލWu7vfWt?q^]bps[:' N;8{>(,!G @ @ xsE(͋2cxk-MMۥ _C1l{l޳U}dZFm]!˝ש6sMkw>Qsi%V0zѕu̶#P!`-;Cڳ:^I]sKn,Kf  rՇ_܊7Oq! y.@ @ * k}膝. @k~=dЍڴt^?K-(Ovb.^/7֐ )MBBVjsi49J`jfCf-s)x/]TLPt?% @Z5e;a=`:}۵cT*_s7HHi3@ѱ_i! 8t%5Q~ \whL)"@ @ (Ƴ!xVBܟӮz镶fTkڹ:!i$rZ@ @ ix@ @ $л@ @ ?(EO\5_c#[V}KblhsKkiT@ @4,uؼsigC7sa"w ݽQc p̿ݙ֦ʂ|.]|ߜ p /b0=Bz[]>Z~.ɲ.~eh֓q-k_Vnڲ+ rl@ @t. '9SKDljZ( IDAT2IkC/sEIyx;')G? y;;~.‘#W|slQPTyzZuU GygOũĉ#k˨@Ǟ:?#6_Gb֏ڷ_?`y3l꽅Vt8]pu>uw.y!kicjX }׵K_Ccu.q.O?ŞmFLMߺUK+]tZlcݲ:2Y*jn٦ȳ𓥜䋶sr" UJFOɁ |p{׆u^?Ik~ǹ{mȰ>5bz=Ҵ&br1}yɰGO4]Ǩ25~lF @ 4i'(tc)+cSôUDE^bZm"i1Ғ\1XQ#goRK!YϹ5;D9c]Ltq^~zDYOp^Xxoyy+GqGr< G~*:q3GPHŋ0搾{` B맋pwqb qF' ih(=7I}qؼ`{,8" 2LuOZ㄄qv5WRê` [_{_Ilfh;˞t?l͋ ~{ƭ}upCsp̻Dyvi}}fRA?7L|2g.u#ϵSww9IBΫ Q ׋Sݽwfr-2fԹ=qk:7~.,ʓe!]bjW_GoԮEhn,DPM'Nuvl:,0kfҤ@|O3=R,HH$_w8 @  |]MYCpkrC߆>8޸nY9e5KKH)S/&-FO7o긍#ahPLu>c sP)h!s󋞗’s4C$ץ38+뵛fT qn ֺ9g<Ňf+%0V=p נ{,q{&S2mfKUBfeoi եRe @$?;&m hP4ؾ-܌iHM{CX_ 5b@P\U##L]Da{fT<-1\|K-J:~5}^C)ZCu3yRjN\AePE񖗷xy齎w޿(k/yٔ]E 4q'x<^eJChj%i%V0zѕu̶#(LL*)8%)9_RJeJ 7' TA"oմW.꺏Mxv^~E0UJۨpQr5T4OQ_W@r9uk iNat%L>f?wUy[ #xO)6+߭ .ːV&NM;R\Ө!{Gq9ዟ0=3E疦X0+Lt@g3-x; i3d@AH cE"nA(=V_$PcjR5 Dȗ|n!]Ž׼,ikj4],ܒҢB:K X4:lR~(ZtjEJ|Ҙ,*.!.|.%ե_fLJSJH1!qY|み6liżrTB AJJ1LypJa#zW.J+L-5Q(':̶/~̘=ast 6H=_*z~ N*YP IP8K"e& *' BblPHEH5L0+'.uj x\¦X"U@"dAL Mf4*#%>άj5.SJei}}Tm+!8Zh DEAῼ26fB)h;/];#bƤ,ֶK@otٿ+rҦŽS)sF3;=7IyzȝR#zJ%D* d8,8V[LR,̏yzyL&W1x,::'*Ɉ U&y'c1NH/Fps %`4tA-4)(@ J $Q#(=Y0+" 9q7/la[ÿA-.^Q^PQXg2 ˴{?,ӧ$/i.Ê"ƀ]/ق -xUXu7O8mD_Hgr rr0e/7+A˻/w Z@ ٱ!*SEz@w)ȸ2M7MrYjAyB5Ū5R-՛v]ym#+;Œ -K Cݕ*\bd\SR$)^Pid_M'?ȵ%,L.ZKJ,N6! 8BT&If(z@5oi_&44h4Z3c« |!Z jLVRgOD}{U -zFa$z2HdiaR$$|nyE?}3xĚ?kğ^T Bn ]OmYp|0e =[ogЕ0Y>FJJ+ ~X׀"-e_CPθXyEBn̛' ջjWO3U[Ҥ^r#^| 7R1k̼}p)fIo )%q5pc[zh PpLJ bIJ";)E;RsL5:fFR$*>2njͪ#%eǎ=LP VߖMץS߲ǎ>&VNSs(wnÉwbWD ^C ބM *ȏGvwy:.dhga@ )/  A 0 kd'feFr>@H$$a,MZHqK  0 $OKɒ|U$IH$MA&7_IzrE34D1Y$`2IIYԩ͞bоV @Q[Af+!p@>%r =& mB 2wɊxNs]t& Bˑì0aBJY)`ct16u{[Rhjek=((.H%eo`Je|0/9[ 5u!KH@ a)~aT_cQ{.E\:}`"oJi}L)lh($$u@ >2@ 0gM4З4>DyI>riyYzH(tt5uE B0Y:z :CcVާHzu @1otU3qIqzZL{s6N]gعvcN/'IWy5!jPu|`vzq.8BL=rc[Efӌ:'n=V:yn|?v#+W, r]5YGGZC3Y|h|Rcuzkp ^w׹g2~+fV4ZjY.iiVFP]J,S5[ӌ[clq~cFzkK+"J>@ NPʲy*WGv,)xh.-RX[tIV|C&T1{F_w(0f9pr;zM£TJe0l @԰SߩwZ6q{Nؔ6vG6D TU:3VNms;L<5B iB 圲+0gغGݟz0>Zؐg58TBQcZye IDATqp0 @F,V+%:\R$IAR$IP_goߦN1z&K >zXL@ xœ64giwlVvc/fO+mlz~䪒jdc ó+om2qKl-fKR6sMkw39c]Ltq^~z+|e?}xc5v孪oqGg*:sp K^8Ɠ)0A,q{Y(MsuԑzxNL1 cB z,ŏiEހ.Z3P^*QekOeCx jL-5wSLq{G|OU%Qх0tիY5?mXx%^FJ@uN%@uUf+W:nO U2 qp1G$ѢwC OPeο$8/_=i""U9Я־׵EQ5_ AT}T.DīgQ[mxwہIZzIپΫ`:R.S8߉C-̘yfUݻqC*aPbHƾLPU!BI+ IO MAbnt:=JŨT*`_*0y]!va^瘔ޙo=tޫT&F#r]g {j0^VKy0CwCjXh3RR Ň9@iv!QFt B> C dzDEɂA^Q^ ̉ yag ť?s8 J\KxEDy@Ealc=k3,Ӧ=NاG +"vr^g $oTa qkȻx `^nKWswq_@@.rcCy )senm ʋۻ` d\צk9,QoG (hJáIMd\SR$)^C#Q*FxOeϏf3rlѴ~w K{hشcYۨek(4|Á\;]\R$H#; fæ sȽ5ЩpY4@Ta"hCDF@ yǖo9mKuӛ0ǔl@@  Ĵ\gdVu\A4e73x,2@  [ZV!j1 Wc2kj+5Bn~!}P@ 83q_^ޅL@ /gܲF0 Y2  KPRN*d{k!-)FuƲeP< @ @ rn J8 `$ &)U&j%I:UHH H`4 r,f6銢N,;?)Q<1}gnz/yzkKk:]&NLr_mr64iw/.[)KŮ&9[dCr4D5NEvHs=Jf ϏveմRM㧒вd2 Zse92ŲJ5ϑU'*j*jm2dUY ,ިK EswҺ^)Jqs dž$I 3 ye~0*Jd^^^IqQ#,[ɔJب?S_dX:Xj9qAq(aN΢4<ť|?&\} YJ"Q+Qݾt+1y@A1ڔ*!% I e%uFӻ_^U!$A䷗E)⇏g&9[[go~NfR^sm]BPL#3pUD]R%ޜS&v.☸IzǕw^Mw]Yfwt_n|˵lQĭ{ z[]>;Yʕ'Bv\}n4+y1MsĎ[;OͬA1t%tYaz3oU2ꋷ8; jT?3T !AHFÁ'f]X;CrG{\J~G'YBJy/.?hC 5b x m9RjTfo@yC$NsoqqnaaھdM 0$aXU&"˭Mѣ͛7'NiM%ѣf&m[$II$AAM^A Zǿ`!VETT9TEްT$-ѣ秇ޝ4i2KNN.--kcd7A\yy9A޽ZZZeY.|[uӊANNqAA̙ WW^[侰M{ӳfw2'Gds_' yeQmXݭ4Zjq>Ogg6Ƭ&KpۿXP}qWW!ϾŀX(-}[RSJr)T:o׽@g1vƭ(::1*W#!?<=[su33nb"}MREBobf^x 8:Xè#L)@>^- J5̧0_!9wAd$ ,_AAq]|7ו͊JxΙ❅H$ BIJ1X%%%%%%)A. ԙR$ٚ@ҷ&'D$t:]GK%JCx͉-~<~ߗBxHJyxN͹tKKH)S/&-/0{8hhPb}B無Q\M8sΕx~RXrfh}gqv׬=\ -]Ǫ|ܨۂn:=Q+NyMJZ|$(kI__x%?9[~Xv a÷5l>Ňf+%0V=p נ{,q{&S2mfKUBfeoi եR:K[^չײhg?PB$ ΛCaT|ND*JKًH,-Y:QA} }qdr[^ N-5pכ`\kdt5M֗g]NʇC"eMر{\}۷I.N]vqfO$$I+ ZwMM~ya$Iʦ޽խ7x4YU$''~ԩ7o\paFFZf> 5ME $̬Z@j"$ʺ5FKѨϟsy|ňR-6*53_}v|ɮ3n4Y QUP(sIgΜht:`48$$ı{7 %e-Z`XUސ8W555SSS]ǼFųH<ɎK.:vxgfp^K]uluԛO{Mc] O.ckâhZ AY Jn9v-EB&R6EUsϛ4i▤۷oh+V۳+b8*/C܆$_8omBKMvz:FR$1)a7s'/{\Ҿ_V~'af{>@' WIB ::Y uu}ύЕ'Dj_):DH,&C=deOHC$?Nrb&;ؓ$bPR~A^nzMKk$;g)\1hc@xymy3ާW޽mlz~䪒jdc ë^h̨y[bdk-6[juk^3?u^9c]Ltq^~z+|e?}xc5v孪oqG>rv~kݘL&Lu ?_jbbS(y*~ ~&HH  5555Ư,fx.B!..jm 4L{$I 윜6.nEEƦ͓_GE8 zʕ:ضMH`bj+==_ ;.㋿+W $*F-5gObרI콥 P9;h*Uc>ݙ͛3o߼y%=z?ÿ]g^BUVJ c)#WBWoJJJ?ccc+!4iR.w|gɱLq%^9T__S֫琾X*2QUB Աzv~)W VR u( @ϩKk MVP~>Z89d@ 1|gDML52奪U)󥖛-E< ̙ډ˛/+P)@ UUW]&&61͉7 ol4\xyޣOU]L_m*v 7_垞lٳc.Lk?W2?AƞKSZ{VF04ѧ@PG=ܑf_*8|x[N])M.f.KwiM'Hٔo:f35)0K7Rt;l/Fgw I)-XkZZn[NI23@bڵkW+442YuvAkٴI+OhN,>{䙻gN%׮ܸy;$ĉ==\8`;w@uUPV B<>_TGBl DB!GD"\]r֖W&9yyI۵euDdOǏBȋC&iZєRcRjF4MM"/7#22~a#IoD:[ [a]=&d 899V[w[ r LB999\ʼB"_.+.$Cvon*51Fii1g$nV@pR"HUϾ^[xyyyyyݿ_ x{{`U6n$5bnf>QihM8hz9x}xP}&nȒwdžDfEؤOס~5놁GFZj>s(,3bB?WTaű5CRNh 32qCa@a~# D w^+ln)&;֮=?8';s"p-LyW&CEq'B֏.=>_ ʹվ~Sfu`̈c- *-h?{mW=Z/Ji,;-0rOk!ٱ7ܸ%)'Ǜ-^}: ZrF7eJfzM|dyzzv3p{+94v%GGnrѺ}ab;!0Q\\Q[TGrR9GLt>1@Z c/N!uOg6{Ga~GKW'ʻBlkO2n޸qgÐOPxeڅSNM0yïzyYHXWJQuǫ{VWJU/ z~^S IDATS9 <)*_ l Y5 g_|zck6PlA3-J>6UެgCθpAZU;AbnRvQSGm+yO}FVMGw)wPRH"Gv~;)V˪5)*'|-JKe jSwZeζٲi]F'LIL;kwmZv%'X W6LIB/,j۶mJJP(dY\XXg)[ØYwFE@Nhˋnݪ%XYZԓVh44MSUt ji9E<bK ) s,+cBRiУC۱Ǟ>ס9ouqT{!3:g [h(Lfbl\`ɢ !~3cc *,IL֡}6~Yg'˙,ow 4`^^cMj۝W jcxcm %|Ų촩:tHx7O{7n xQӴ\RZSTT!=-S@Ԅ0eatT*}};jxU˝:uBOv2ɏC<°e@5Մ`ZMh5iPMFjt7ղX* 2-%@o.PFK[-L&S*)ɞ{JjrysZZB@ pwHO.[@_nQZƤ,_'㿓(37n\QQ8իW[Rj{1퀺qzMi! l]Aݨ[Cr]0 s=DH`K?qF>i=t>?`[#&tF*O, rM7ó cUbx>_\\leccjV_wLVH`bJ&u.,pAd2|W(D626tVq =|l[>m ghx=J\1Z9&I79y=B 7 3ma#2hT5*]wZ )>!Vr\.6AP(JDi .=ԙvHJRgg 6,^޽kjjڦMhmjq/LOMMmt1r um.BhȱYY;'ס+Ug!iU |>| >wbȠZ6C^칤$,{'j 1*'&*Nm uµ3uʇ-K-}?'7~3ְ ˲$@Jv~Nh~-" |C#=Ihhh!~nܡgJˉКu3_A>RYx-:@.?ѤU}h\7nP( --L 4N]SCBcP62ܼ\##R(mZh1aBǻ Z7jh4 Br)!! f4Z5j4 a PFU̫ l@1Dšϧiaq]V,K( @lAAvW#B033V,Rk*\!OO(k֙2lBBEQFWQU+!qrP'U0N]7\/sT4 xfuɸP(d2֟%-%aS3baUȷ{N&AeGl{Xx(U'𨗦<3^IwܩA?xC=Zulq,FQH,C!wk0XAh`1"JB7^Uq\yƚVTHI#LN-lXQP&oFQB pК%{ „lu,-- RiBڭUѪ 4dfe lߺ6?闙Y0CYI*4  ,,JB^Y]}h#G ˯S\->9p20MQᏽ~ofVhjR*1ΐ䑖.6əJܳ&b8rاAwCΙU#Zo{Pme )&I yr02c:Z{~bbHTc GUH Dz nvL,͋&"JRjVk5300P(`dd$ʴɶre]]]IK@,k(Z5g4 ˞GSQ Z`1bYaE 4,ҰHð2aYr\rHd)[Q`Ȳv3iG!DӴIbbb[dSc3iavE` BzFo|ݚ562274wBBia@(T?MIHO灍+c~V(J2''G.[kXO7 236|rfq~-yQzN =->hg~o(2 AʤMwY\vg ~ ia !|zBXwڶ͂Rj1"V7?BmdTUFBD/woF#Yr &DkIxe!W&Ųl^~>T&42,Ʋb zBB40Lqqq~~>Xd$W'4hR9,9 .-ňeaqLqvQw)??Ν;fffFFFB̥ bYV: EZZaհu׮ sC|>Q,+eX;x`_Zzc1Y+OGZO~yhh2c-Dq2D(;3B|1hVd ckLX[gvU|:7q&˲,Mk4ZT*UjVU*V]vf_`fj^Ze?d޺kvF-&З7<*<]n}X3)HNJSȣu* vXڌ<[׃t]=н_lE縝Wzg\ٳtmۏ?69[6㙾7>'fH&&];cv98mݹ khSN<{Ĉū(ۡhE׆^r{{n-~uWÑ+7~/[ylh/"/e8p#C^!]_$ ]?][x܍c|W11Z9}GnR LLR)IQLMMԴO>-c\+{efa]*B999 jrCP]iScBP^5:xY38cciӦk4B,BeYJ=mc0JiZ]bHշ\BM:oݪ'u~7n IHL,Pl⬬N|@Rikc;ٍdѦeK:Q[i}@,-dg!w3>@qڋXs!3M)5ȅƱ3)?e;dx>%% BVðݲ3ìgf~|3k{+awD,ZLX8љOznUFY}Pe̥RvX@6;o׻d+4vvLP \TDtsX KE{ό2c" DmyhmȬzL]˹_Kenj`R2}JCwh`olfk Дb({$[}_>25y^IՂZm(!<˔?+xf|,򟳟?n ZQXze͞?;MThdcWnPt̐:u%Ϫ᫛]\#0AkL &WQ!sO{/IrX?] 9po nM^' riu}:uztc oR1xd>]7BPd!2ׯњ VFF5_i6ջW ^Xn!D! !AKK ̾f,0 MkF MݧIQ6/ |>wmxx#-6mY +e3WnUB8p`Q['΍-}_@ E"FFתCf7|>0pX*sT6p0&4A!  :*PT2@ Qi#S @@!vko!.>iq/J*p|l=3$R"GGGG;@"Dx9?YˆL^R>4igU+ir4@G8>fZodh_e _Ѷ<`I=ގc5=Ȱ[u0jѭׁUQ_|TB ԱzyWJ\P&oaP,hA3la@Բxy|YvZa.BcnqKRN7[tFw( :įtn˼W&w0kr'/ݝMۻXi|ȡ>/#9hݾ0 g. .zGҨ-U9Z)#&: ]-1Xp:z٧3ܽo%jT]ɤkzЙGf&'&֍"dW"ܒW2ٷ+F1f6?ZVQ?U6: l|a;SȻ|T`~_0E/_q r8pჇ˻ݫBVyKNmEзr2x;^Y/ӕ8p@}0`L0 !xuÁ8pݫO-x|N*'&*8p8p8pxWP9ɉqrDƦ]m9"$V+2e\-B B.E*YMҎE Q IDATcZ?[@Ym6"ǶݽۑCft_u쯡-&~lI2-9zg2ohߺetjj.`;>Q.QA~Qɺ߃bF智way\Ȭo:lJ9G[$PjyC'|M8pPd%?{ܡsO䘘[ZhO$P)qe Tn0K~BVK L-9 s<_e?\rn_n}>!?]]r ~<[|\ Rgxnծ oFa.˟-;Xbs/9Fvi۽Eg6rR&xfuy" iv 6[e;\JyÝy{j?M#XVT`ne=8pPOhZO_֙@z춲e"Pz(@PBHÁ"tfo$""՝+&w\2=O^J|^),<)..2*6"*66'\ CW6ߧ׿' kqt햽[W~٭ǨQMKڠ;O6ub08r];eX0ѳUAŹmQv=A;FX0з)Kki:9,mҳ/NY(.rf>>~%?1nM=ܚz8iӶGi.U-x5m?/۸n HrV%z'$ Jt ett?X!9[:īTgcښ2~oQuZGRշSN6=0pހ/?>T-gí\E+cTVwƽNih4&- k\; zt3_CTh?uΜ~}L.V!k]o}_uЯP=\?7Uȷgߵ6Bs8)Zo7gؖ~meĔb9p0M *=# cL@XBD{B0!Z8pg9QG怏K[lIťORgagU[vMșL]Tm=m264]S^OYx7JyNs?yzGsvLb3EeOV``nZ;F&奪U)Z0@V.xR0gk'.oJXhtKC5TU] /ftp${xn6'n(:+ZճpCOzJ>Ue+w1Lʪ%l;ި$f˞U<s10e_Yܽ M} 2\ze)a>;EYFaJ`hO Wϡzn5zʼn#;;Tp=BMS4]U]JүӚDO)~uxiWV^ ZMuvto/]v6|fP!5j0R>`ZnUw^U)t^ziYPƞFSvB2t P3?r莟|;:ŝ%́[!XkhS@l!.7P*:@`mj-NoX-E(gQxs;,g|xKws @]͗q~_YTDFJi,;̫bD,siH3wdpݶ =!%uq*ap}uarv_|$iSOO4Y>zbD,l<_N:h_X.ՔzgҨ-U9%5fH%b٣Jҵ:z٧3/~눣%jQ*ʊRlkO|$G ol4\pʩ 3%⌝/?qMe.սE8/{ɼ3%U"K.{et)*]%bA)kξWe4Ķnf:bMhM—gC\$yWtfNP!.=ĊS[n+17n|35#.բ,D>K/etY۹!I̩v~;~15j2R>\ZQ;;~sO4pNq[|ӭrbi_e R34< _CIbs릌XM=k })*Ѧ)cl}=>nrA{0L aYcccݷO~~eK(bLKJv);6j˳),tԑsŪKL|_(B#^<{ti#G}wS~̲)g猚'gg|ͤU#GqI'w9 rk"=ۢ#T9:#$ (>ޮ[+N<2V'O_<f|)L.,(O>w; 03cRs2sh>B}k˞#FJ Jsh+MH{~EJQ5ʝYNs 2w34ڧ+eJ ;;5wkڻ)b5UN{.SΝ rlL!Z-9ܬ ^Rb:ebp"M͠g/Əs?aȲ?1)֦7[x^HuụU+``0CcZ80Vhm(I:5cաKB}>AH옾? .C3^_w)<]ZCM٫T ;;Z58p3KC <ߵ Ѥtpr%"** _zK]a5ڳ K"GwtҖWj[ځoܨAQ3v>c|gu/ (qZ`!c^ǺA׍gUfvcW{vۯ_Nn<[?V$G%4 p!ZK»l>уz4%=.mې`iˑ!} t#ٹߧHΣo$xh7,)2ERT Pn?؊E\Lp;y6hih/]B+gr.f ɩ&9[t}Yo7~N؅?TDtBD {ZѽYʋJĢ JuG~H_Ⰰd+̝xoCЕ?].CBAɁ6::~햾RըEr%+ 7JZ uTߊ \KH")R*7s*_^B! m)fɞnAo}xo2cr=xo,&FVLQ~oWB-+* m9##r56d?{!;6Eǣ~Gupk˨Jdiyjgr' |=yB D{R7Mg>k݁-,(-n޵m˳riJEUʚk wnG9L:&" Y97)|p=ł \Ge4$,0Fl9gֿ?8p߃yWv-Ǭ63՛V=/e۱˿dsL O3vŢުof[GK7}hf8COKE[K( +xKWE}PFu Y _o:njz @#̝펹  / [h[>[t̚&^RLg3iٴz?Pa$6YubqD?ůۘ|5-={-:ҳ:?.,)M(}:z{ش}x{6Q+ K]\~҉*<=#kcoS$Q% I2j?mcCl_۴ nYu@ZR^Dmr5/+4ǒv}#3CZ#\l)x4">%PȪWZ 0[\Xsf5H߹rrG7|_;kan}e$c^LU3L:PQVW4]/-c٢w&3f8pPl6 ?TPmsȁ h4i v.("@{6R =POȲ/ nabtg;ߐRUnfq@&tN#q[~m@{e sP#%wCӥ%y3.ٲ`@:ǫuv}?_x3B\w>[wYda)CCbzBqH#vL٥o-ل3|BlgVw'f`sԻvf <ɖ_5ɿs߾}zeS}9L9ϣ47e;\"?O80+\7[w[,>{Lѿ9pCUx<̝h΁0&LivDƤ$B Rv! dan_7!1C]1(iq{?Ճ[CTʬݐ#:Zju|#++f] hΙo7I""B^ݹb}Dž)~ ڣخ%iʲMyR\\dTlDTll.OJ+f*e;dxe}Zۙ{␠6nWhn+X.~y9?ߑKn_Y9a\ߞz5Ȼ<}'|:iեǨ_džIđy/򀉞z |(=5woڴwZیL;no`oAS˃vz&trX Kg _Xq$j>P\pq^}|vKjc }Kw7Ws]ݚz5pӦz~]*Z:5pk^qEݰKJN3I؅NAU6_~[-f|(իbٚ'R/ӬoE# M5cWF/Q 9<ө}Noc+?p{EAխ+yV==ˮ;!W5Z>/=_oN iP}8p 4j. ENfEij,ѨhڔB(@J".)D!!^pH0j Y XFhԕfq|jXfkΟE9i%dغe3H*.[gK"Wmy'6!g2uQEͬzׯLz4+ -mg2߱6 I{lQx͹fF IDATh!޿Bh G}2:p21uUWnzuPts͟YHʳ75'u\, ac3;P6ZO6Ux\cPU׵/u=3-/׺EoJaa׆UYemc;IXձs3R3R3R"?cSኃ4wP;VtpB ssss}Qoۯ;^b7dξvtu5y'o/PZWn=^Xǁ/n me^AپC/jNxkd5 ~~f<\b^jCG7L㓗@b݁ &~#XEAg@AJdUeA+z.s]în='swDeH\D!v Y_-]o=H.pߥ2:~bӇ'wE2 K~c Ⱦu<{I4ZOC2gM4bRwzXynj:o7/̹'TĄލh~龻d:SD%ڶUrPYauYnÀ7-V@k^&a4Gu|m~+fx4O'S(c~B^lғ+|sY+XH&)HxFV[Gdn5BG#XkO3bԁ$:)\Y"'Uzw0==g I"ѷOoE0{_O .5m > -t.4יvֽ/XvrpIh8aޙD%T۠jz.G)ytϏ[{ F 0~ s χ#LS6Z$96MJUS,v: l%0GE%$=OX! )};IQgQ`SA|a>i7qpQN6Ь "\%dZ5 .dǝABeB2b4!24L=#XHFs;Z6vziGa*7H(k "RW2,l:WNzԛVd΅-;/"ӫ[؜H`Չ,^OLKzułqUe8E|P45Bo̸kYf&ϟ|3&c-*oBDnG޾y[$da?0<똰g"B9BN]{!#Zo Qڎ!yzV@cVKg\?^7nG%h7Z#02 UK:6}QQU˺]R_[j3O3ה,xQ/'z'_V l-4 Jb_E rT*[jSӇ 21X"yxw4@Al:?hc OoU(yam֌D$O&Q0b(sn:̾}sw 0EQ~Ͼ ބ}'I!ZR6qҡe#sK84 dskD !6pu%~VWt<&(dvo,=3Ś^{/m*֣(U +dsGeľh]nOnދ˰тX3?Y>J@y4 !??3L<\U6t"( ;@K]-lQHʰ/fe"gbn^d*;@/P:AO| N߉V;%/yIZ rb(5 @( P JePPBy a>_L0Qkf/qw{[4͊vVjQg9JX(ʚ]! ?5z/if\D>Y3 BF;'O,~h`IXnqf M EЄK`*0E+\~Xܩ) tws`1QmD"ߐ8nm_k5I, P CE7nдJP.V4EuR$ׅ4۔1 ['Jhf%G'$H|$;3;6x)17.突Tָ7(`7:>̹(AYG;6IqO4e}A229]yk Æ7AW4! RT_ݮ[mW|nb0dʖJ+svr?R( *"ґfMy5vc4U%95>BȹffiQ/J l8^aɼcǼe,ߦC]=}UGz/X~1vO8`+v.E%I9(Nh)jK<{6Ude)9}V/uS'UO^hSqNZK9X⢵ gFr G7~xI'rSCV|:zLS-UC}6ҜED?۾xce9{0l:܆اguZH%eHis u:l]Ǜ[xv~yƫhBDʹѿ5- AQ1v4 d˪|0 Qۮ{u:W v*Fs4MR~u~S})ir `(Ծ?]W])|1'+ VeЗ9e NM$kV}!N^-dU P`_ @GW]UNG-q]mr~mMtGYIŞZ۝ &E_k hzY,ҖD1аݹcm:7onm8ܩy@Bi*bDD ^CXrM0" tSq1=~ No;dZVq3+R\zrBjo ؈D!P;}[Z$H!y\8şRZ}T%!#CJ$Hh!d\(i8oPԆ!NHj[cPCE 1= [2lR!I{# Mv"GʎqA]ZRI '"ލ% сF'uA$z AD:⹔/c|*!=/d!Қ8̉]kkjeeP A"A $H#1}IΤIqH/%Ağ24c'JLlC1Os:) Fۯ7qkO">3sDoknMyf G8yoܥim:mHHJw(7 iMR]EKI ۠}IԮ" u &]fvޱZW;,4nIS X5d9eUݜߗCڭ_e-,ֻ O[2{獛 _;sR/E8l2o̢,*=/P$7b0Ss^+-Dv"A $H A=W߾ juĺg"Vj4|*]*;vǩJKKHJ}Zd7<6Uk[gIzCV xΊ 9oɴfYanxfpܹSwA,SQ:=ؤ' @ Jb>9zʬX,$Fɩg4o|}utM&X؏Votq4<ݿv>c!.F(M2B%Α;(:{BYw93x t޾; (R}VdCw>5PR " OOOioݻ1NotrpIh8aޙD%T۠jz.G)ytϏ[{ F 0~ s χ#LS6Z$96MJUS,v: l<(* Y|j IH)wI>˟:FI=ufMz/!Qp!;L 4/3rQYv!A`eByB"730A؁+z[6vzEaf4 ;'̵%[xJ3k7wۿEK97FwN#T{ےqnС}KE#;:7/n쉈H);yT{ (N$XիsMONзRRR 2JMXNj=h9±= \iwv12[އ G>I)t6Ѩs<0arȎ;vk$N4c'nUu,]va pAA艃*Zv9j|1H6{leUGW'GX/-z~nwSZ9kɊ[[O rF9z/yJLf%&j|I bN4gB?]!I{e {1F&)T)9Flh+wێPkЄoAk>Ah꥙Y[g@A+C+pg0"(T -ԕ՛?L3[z82@uI{=BJL6a "]zRQ1?:kl֧aj (轢f j8ĒB@^YXY #սCO\XUn!YkJA^b.O1k;C~~gP<\K'R JPC<dh)j"Hv~}Yyh'N#}⨘IOY`͸AJkX%jFP-pBK}!xȥP+b_ IDATñ+|#ɦOL ٮuzT6= T04. iƌeW #6ܸ4oǖNYaw֖1-Cx)fMdz>yiDDă+:XsO9bƳҀW,aDD;ܰ1l;O"""‚ܙv|hgӦc⚥\=|WTye66SY wl<ŽNsȁmɍAϣBҳceѡޱ$<٧}e+"@mzYB/ӎjZ\`hhYH<ޛ{iJCY:R"]}cΡak8|q7RTXHH YW&MG5m0pn ;[V]H'v)(,flú[ߐ*3툈 o-NŒ˂ . uFE~˵ZQNq30˴: uUg'( X<>'eG*xxFWLj=S {akN%FaT?6~y\8şRZ}TơO29"$H 2LP4 Wm7(jOMta ("@zʞl)3 {YS jiiϒ./$m>5y/ >\Z5z^h}=ruI>d3"9-Y;oU[U.M7h?/@UGM Ayk\PwzѪ'㏲>NgYt̎wm\yӢ կUk]~xൢ,תM#yE.oqZu-UgeqT> 7#\_\5ĢQ'9 Lw,]e~ٺfkH\_;3ZtbRm99jbe>.68H;')?+_v['zFCG$~{jҐm~L?Oc=pvzg޲ ~~ܿd::| $H AgR35Iu^J| $H A $H Av2@ 1oMGdSv%\:K,\ѓiGEWXͪ`8n-ٌ];iu#h/tgN"ܶѸ9їXwk5[\;u]"A $H {a"@:2M~W8Ety:,Ch] >$:d!xaivVξvcl[%ZzzufJ 9㙮5_ ˳y Ŷv3ԆEo>GHH ;0؄v|XEAg@AJ^ZPfxg?`zB뀱7m$W;"[j]7,9l㕛S"L GDG]@6'"lcH3ܼKiuuQ9 uD2L$\޸˗O95%#]"  W-_cg$סy[ZV3j>v5{BNE=ʪH/~͐^! L_خd}Y&2<万IЋ3GkgUyR_~-f}P!]SPͬr)6=k]פPzq$O `_x5:P*~rzO'+* ޼alX1X:K7Rvr392t3(:Oez<VIE奇y|JGn9|'eقz}=a1:)6 /8q{t9 @qȝW*\"ܷ[ފ `(ۧ#A|^[]I3&^qz4ŋ~jHh8ozyp ("@>$'FYb?=˭"6brȼ^5)D6ڀ&j!y9^b\0vOw\|aRۭS_eIӧ> j]T.l[qFikT*)8A\r ]l~JbjW~[Z=WѼJS7׹)97Cάiz,Y@`?hzNgfwjڎ9 ʼnw{s4a܍Fښ}x=+ \qؽg)L"'Rh柬"$HmVPz* g2h@2]VUM DRtuOyԱ">FM$:gLowBZj^:Ksem.,M%3ʹ*R*(6!S+/ɽJagF<?lzo߽t1F^Y]@c= yҵ5w%bG.tt#-cvbUo8eQW^8}URJ?H$mKi@~3mD?orr?3w6`yGed 65TĞ6B擻㪗^m,W@DxY|H\>_= +L)%/Խ# o?g]w g C|%MaK{hͷ!8Ow;nM v4s$#^EL5Yj~ǟ Ǭ'θ^:sbD@"9dַ`Box~ރCL#gz DF`6lF+ROћV=DJ 0=|D7X$, D܇/¾FNj6D/p=k8J-*W4]ˋ9{)0{& nm&&|j]=rE.9q[GkArԿn jgnaMG^/ 'o"5kY1E?JYftuu 5羒_\'Soi{R(|s 8m5}7 Y _ .uBᆴR6>Rb"5Qs-e{ϱK*'ѦAYc?S?{{J8;_*||]r=W՗UMLwf'b.! kL;ݸ8"E=XOcrvް1e(*%' ˆl`5rl݅oa.~~y_䍎U2G*d_3ԈPP)¶N˯v@^|63C82`i|9XRb kpz1U,zTpG&eZ\Xaj>H+}q#iYuYM%|ݴpeiP٠x,hݑwڸ}膘 f( ϔqϘV:O⫷im'5=2ƏW;Wd)6KB{gV*_URo; >y~9+ jIV 3Ҵ ?(&= p2 RTDfH`Dy\8(żߪ|:"MTr6kGMvn)şEIMžqs,2 6k4ZVj\Kt|ZLI0 mW["5H,}dTGES)}^^@{ɭ퇸%ZX^rj:[zfʍ n^oWT8ExǷ7i?vWQEa#-qkkJLW3sDRWT)qKOKFn&bAE,Y(糖ՋT0]o-:ÛD _K噀Ory@ϼ ?^OFW*>}Ԫ-gy:Q!!O E}ԔYn(; D)8NjT$/Ku!6)HSw9*!2򶙻K]"edH2Q?mky=8m4 ~AdEe Uo:Y.j;Q_Gw9pԚ-QߦQP`*=;F@PVYH£}WfR 4ߦ,2(0$㽹ߐO) YhgvHHv3);=9H Pvc#!%lf}:_6i?bۢ)v&  ag),*FfM8mŋ9NVSMM5!y;'evXėӆOqMnҤ{,BOURFiڣw, d*b}t$m%ʾb=_JX=ըq2e""ߺ,"?Dr G,$ѓG LT>l÷>P,K狽Gf-bӴ) 9an m^ H2ߣDeJKz~u)Qi;V^զ:Uj'X$WaC0K5RP4oiy}̰J:C1( 5e~R'P=۟IhPR^ʽ,VRa< <-Cj35j6[Y{m'e,M_VߐJ SPavp,ԒJm0^#8{p;I:Ȩ]t; D[އ0/ϐWqB6[NH߮9e1@t˪|\ \jʭ7NX}RB[VƕW ?)8|\$q ;"+4~Qm5,>(Tž {nTz^? > ܄jÂ*龫F["U> #hPTHSŬ!#C.'AM3!wuxamz3JTO}[x'5QU0PCE 1=^^m7$["@Սh#OB5bVTe%v~=+8w_h=rU%jUeԧ/oH8lTYqqBŜ!8Q#K?tլ \|JM 6jƢ gYʯyYUgW;e^g^Y qNm*{%{lך5Xۥu\uFmetᗫ)Xa߼y_ WV"a?QSwYPP*Q-X|.2pnBя@P_=q!XU*|~#D _d::h['1φoJ!4{?NA0(~EOO~gH?- l yoj]F*芼w8/ y% C%LQeP1Xnd&c3lN.KW^(OGǣ(-- hS=_%#8 ef;1J|g5ߖ|L;#_E]<!iYRXT)vK޳.rzY.;%K܈u;ޒcE1 JiḍRσϲ*sWߒD.q*0Q~Srb@%ifɀγ~:Uw-UDP,H/cj9k?TQxnKy)LD~Xz^Z>^8ƌDE-2PaKF˾E`myUHRos&*+5n -)#izs)Z .A+֣CdSi4HӜ*c>ˍϨRKSN_7rܹJ*> $H o}K]tӐ&=4CZOVW{R$HwDYE'uPEB+[x*EP#-aӄ"QY':U\[zd_fk죱" PTL)a}t~ޗ\2Xձ'z'Hc A $H A ?ߔeѹKʜɬdVbۆd 9lv»gdƴ Y%OWV[c'rP].~I㳛:Pd)Xm;B֐ɏ[ Z L0+ot4̬3{ KnD(P a[K[+7ޛFg*طpGe)cU!%&KȰk U̮xT=TF^5FNSWnSx|VwFs謦K(+~+ Zaww| * 7T4KQbMOv)NjMz_{;CnJ}}k7jRl:0YK-w B̕I;ؑBV7@T! $HhhqU 3L{HiA#NFLÏ݄wr*ʮFL&Zn b{2e2S,[&qrqL&9rgX@T}i L&lOryI֡Rw14u+W;&06GMLDL&7G 8;p?́ZBX}a$&ijnϑVd9S }ιf]EτZZ.VϸI.J+& ڪ[As-E0Sͼjod~gK38c㽁WsZIe>=vÜ{.|DMc[LËo+P<^Yև=i+3@9q+%(w}пf =l. *y_\oҐvs)/n׭?Bⶫ>7qshn2||{euaak9; )PxIHmєG]c7FSUS; kVavR0͆# 6 .wwB^txgϰ^H/&֮'us;"’E'mM%|{6Ude)9}V/uS'UO^hSoJaa`N7Ӂ”& ?++ܲw~%Xrȕ*I' tHt7۲j(muy*/?!0BEʔhkdjjq$H AIPW_U/eW*y>Eaf4 ;'̵'O"7hc yG,o4]lso}qbE;S-lGoRieuAn,ʽrJk!J %;J3k7wۿWK97FwN#$UY:<?W-o^8@/{{'" Qli W{Y} (N$XիsMONзRRR 2JMXNj=h9±= \iwv12[އ G>I)t6Ѩs<0arȎ;vk$N4c'nUu,]֬2+o/=~U&)knrԖ׃ӷ3j j)o\\֪YsOVܔzZgE6 Ia|^b>-*hm~x6 ̧;|{{Ħ{'XÃ#s!ͫ i6ݖ8jC<*xc3{_^dj5hFT:qxؼۿ6(RZ}TPD:m|׽81@XEẋu BE UozfZ_'LspZf#;i6-< ݲ+-)DO]?dx j q?Cȥ> ;9v:#S;K xzkCH Zh-q1}> 洌OKyY`qY.;֐*?< gfUiM%Fa--$!iQ٥$KhB2Ee-d+m0rWu9yss3jl9:欚ֽ A@G[>4o M7rmq<dԅ̭9^amva#]BRQ#!yұx}˴\5>H?E,Kzt}f}ʮUEXsuKF>Ai, Z;doQs!Y6ipkU0Bmv6Nݙ^:PXuj@~Eg<ܭ#x~[ &bh5~Q>۱̇Gpwr!mY%.1Kbxqg EМupC єo+I"y)rB1x^q1nzey#}NaTǟ+n.&OѾRs*#u*A٣SI. :ώWDWLS#Q'nF Y:ʕc|-w M%9u3/]qXg/哤hEz?h-%Fo4;oP?@$0[[}ǠAjk54&ۙ^>s7fO#GRvu. afe3enzyS[[/oy߯5۸d/jkkEEE;őB6M"Ծ(`zvpK+C:]6޼[Xs`à}(͛- 8ܴ^rO5Ie9ewO{}+ Z[(,hKh.{Y|_j|#=5_(E]co8zaŒk rsy{‰C[6lذaьSǭX$ZsX]x؇|dg';G>[=Z__})ekD;#Tmj떍0ܾN噫 I+xzݵO> l_׌Z<qqm&q[8qsx;]+.5FMBS2",},!v6S#KO(J/~"pĨ5$Xz}L@gʙ# o{?UH \L{8aٱ-c+rLXw6E-Ǐb ' ޽x\kK3@^h&SuO:mUYȮⓜ:-QvMUG]͏m>-=ԼT;rᤍ_Q/6N]8M͓Ѝ;kOÝg68O·  3'=vb~Tiwq)>uE0,& &0ژ K ;b^^ԩS l?Y]@+@ u_g |[цm"eU @w?HmKEo[8Uzf5q +Uٳ䖬vxEmv2s8qeޮxQowHsj$3#%9e%6{YO--\".@^3(gl487~0D;eY0C/_ !i@g}kjU5woMdy>i9A%6+`G㓖z*krZeZ~܅gΚ#p.G47}a;k|uktK8'%Gٯ\NR6<*9Xg4aHb%axCt^>ҺEe˫agbл@umlKO ާNv(܁""u 11(54@jtᇓ3GHn)(1q麑;^gmzg|n 8#p6}t@}anoTe11mVMF= >[iRӝDc1/0ͦ'/;lHU𠺝sbe{ r >5RuJJ?#{]޻ޯ4VSW|ڢ dL9ǎhVe}uUwq%mi`XA"lƶCleCv/l,ڋe39.X&]{%etb-@v*:zu;+Z0,ҿ*AɁQf/Q5tZ9jdpͲ>%HYu}vYBcFSC}ےZ"@ :q6eޙMKPPd} fR~81rta {QV[\ȉv.[ 1ԔA뻰i\j)t~ψs r bfw\^̬ۢZ0uKT= HΚ/2.3P8<Dz,@K5r| kמY׀D;LY!אJkM+if)a{VlPZӽݚm~={*Ku]͍-_[w 6CT}ի]ӖVg`~pgƝ]a'_jJ[+y|sGȯ2<*E,۷W/ZAjSԙ̅U=ͦ7mF~oJ;|d;0}t1OͻE,vQ(,ڋ0nͅ;6j8]mw ˎ薨#NŢ^?=^YCiIFݵ9 87/S 7ˊѼ]PQYw]Ik|ܲ+,+2>J#5Bs5] @`< ZWRH JDg7N)7@]KJпTT2g;vjFY)0>F`?"/=fjh\bt7a(BEiyY۽/AxDD3)bC*WU!<;~o1۔5cө(BO u%6إzs\jULBm|N oq ='GV]-4>{S&_s$e[IՕ\*P+?dd (U_o6iEZ2.'xod[,XPQ$ut_lJ͢Q41n{EUmQ;w$4"Ԇ0w, j/ 1T˲HU PQ#N_S2fzͽp+}zde?[s]Gc~LSp[uy $ 8GG XuΑ_&>]cヨ++gԦzl[F"Hl\g g_^޸BD"im<]m#KHEu^eN"ևjՒ?̀{.Vwv&DR[ny6k;LH$f;+[I)4ܬ?+7K.k!u+Ys2{ş[,ɮo=mῧ^$[o+ϰo0nDy~R2Z?}(,1 l&LM$bFw*V}sb؆L:կphn_z+u_iD*ߝ5֊_!Yr&)"_17/v֢Jqɇ68՗-97g \3丯B5 0`Sr =ߩQwfkz%sݎ-8=O @{EqLqGCr^2xs8?ܶ0qO$Qsw{2]:{yw7 u>rLnlzyx+ݠM;iG^!a>Vcc)-g=$^eg2G%FCvgܺ6^:֎V|:f->$Džqze%bz6ر@U_zEϙ BSMvLdzo1 y~UA`Fr!䏆\cwa0 ͆wV (-1NfE֪8$  ,կ_em2>/z4dG)}JZ%pxyegitvO>7p,[!I<7-;i'Xaa ID_`h+<#leN*Ze!86&{jڡ{j_HL)Nt3%/V&M]sc>ן?$eԅ+K"Չ>^P~ύwh[ynֱ+}Pi$}ҒisG ^(lbʣp );oTX<@kh"],Xy,]I.$a"Ky}sȱ)k9!x^q1nzey#}/'[@;WuNe$V;n[V%os\t#ia K'dr۠gG+mkkC[F !5$Np?-8~+F1&[m_l*ɩ9GX,U[:q k$iZZb5Ə$ ̖Ơy<9)`g5X4&`pq"{Iq|rښD&-̇^vk{US_XSa~RTGgҪ>!şL|ګ(x#nYDY.飾? >&k7f!o]9Mc֧x*wu3o/+q nc?Qm\uS#^")ΡU2GbS2s WMbYA&YmF_+:+0} \dWMlJJCӁ.И:ؤm]K* GF9R,R>fD:iƼ?hSw mYtFެ'3^GZȂ$3xيn<h)˰ ˚ %V^xqӳ@=E?3''RQ3%aAO*Rr/N]35/Uܫƫߙ!0? p l?Y]@+@ u0jDRSw(5I}\6}|>|y5Z./]^^ c}3~_nf]|BMMN>ug0W@BEs{N_E;,VPQ36^>Sqʆ eg)_LO)е0*e$EC8*Y/|<^ϩ5׶U[VNcEc:F{P;t, }LE)#p}JU -Ǩ/oÎv}-=H|ZsKw8nɓ& QhIӤ&M/ѱ/TXeC@bԤiq7\sI8=86Fc,m6xqv_&/c4!!Ё`5fg^#FVgwFC-W{[ԻR"(~g?J"6o߅`VPSD5B2m&$]` #ClnK+/T\"w@ Z?b~-Ϯ ҢDбb3wK7um+')-=Z"\}Tjd%Rťq+_A٢p1(54@jtᇓ3GHn)(1q麑;^gmzg|n 8#p6}t@}anoTe11mVMF= >[iRӝDc1/F\ZGMXi}:/koa^+3.rvZ<.If+8CVK^3_)[D^0Hm,5T9|lekꊓ/&|G3C@+) !@ `~JzDwfSҬh)d~t(h-S6"1ԔA뻰i\j)\! AD7mY`:^<P 6(>d 9kLD̓ p,]s*-ǧvɛE~ HDh1y ΍)ܴfv@'j5ۭ)Q3A_5+Fk+N!F葪zF+ 1Z3Lή Jޓ/Eڔƒ3)hDivSE{-n6}ю"){7Q䱺t$Ԕ 32@ @ cZWRH JDg7N)džV n>_R1'(=EhS3B J!1b^ Ty1#TCzէW KLE*}eMx³M,̗/&"qx,}O'2TQ hL1恜{BBEwYh,B'ȢTU4THōsWDBEs~ڵvvGhE$91jg/ϒE񌲈#z"T4kBoNEj}Z+i~ȴ.}ջ0Vi@/ɝMВs@:NҪfѧzoʶ4+rp|rBEEN8Ŵ|')-(-(}tbV}jzqb]fαZ||"Tyww3]QjJF53!4+ETVxI@P !@ |6nDy~0Z?}(,1 dޗݿjx:>pΟ`y9O\eL.8eT}7;=D嗚Y' H!u+Y`xAQH&[` 6oRVI}Kv.qMA3 S* k4l}W @ Ȑ!S#DZk7$k]J"zs9~Ӹ| :^9Prٴ#dZmBr: H 76:ԼIs"ڸ-=ngS cuwovu-'䜎r 5GvN&t:49iҒzsma._w`C`ܵ2w^CQ.=GT;2. 8!frf˓ƚuB7M, x8릴1w,24$zYEWx߳C Qasx%X;Zb`$Džqze%bz61}AH9U:Ah5?l ٮ6m$}/Ƙ!g̮<<@  !ĿT-3w 7z KۧU"\wh/W}Vyd ɧv+$՛ϋ ٿ`Do/2IH#p{I$l8myح/d8jDdiB[Ia+{uVD. qϰ4)tܛP5ScBz `JNAv)Y67E5J?ڤknZ W$%uOC/ZW'xA=77ܡmYǮFAx}HKΩQ({8},^LןByn58e %ϬOqޒF:DP]?:b:AkY%>P(1Hk,1O{MKD"i'1LC/:pHKG Z'Iik%-&k7f!o]9Mc֧x*whRKJ:ǯN9^GSà}*CLZՇ<3Qw0O;r(Ea)Z%zdoחĦd"CSfI LzCͳیvV8tZWGacf$p]U6))MCcpBjcmv.,/xYpJkppJ/Xk0gҿ66mbk᭣/́z,>Z8W=p;wQi~Gܲ \w3XzfWaɷ񣍮"έ>38!g'\\{A aխg%LWnw <34xyc5 ٻo=[t kW z= ZSd睜߈!1~bएFȻ߹-w6z:',$nʒZ{/D{ZZ~^'dz=eƽ>l_֎XUƪB^l ƭ 9b$t&0wZGG8.%_dlO?{3?}!_(}jcjJۋ؁F]٨t2apD,R'P>AƯڶX{cPx96G:}7A)NEes,V*q$"ferKZG̕d51,C;8dڕusY2~IJưpfmڤq p l?Y]@+@ u*5t@\ZGK+xqM?㿂ExvG+? *٢j/` cc5cnjŠZiL{GiirAFœYOh_:pȘ#sHR:N&Ow{~8I>s4+ 䖂sE3϶3z]7oz8\[ig:07iU*̲ƒƊFAiȨT7!g5-W*sSh:%Pt /t] Yj>g,zY ׫pc+[#UW| 4;_0{f|*O牖ڢ dL9ǎhVe}uUwq%mi`XA"lƶCleC6BE{?=8`¾l&ˤb͈qDة,L/F@j`lOGH^R%FOy(k,v*FKKH;K(r^`i,C;L&&IJX1ƌhᧆ%1j['Ե1@ sq6eޙMKPPd} fR~81rta {QV[\umXB5c?)waӸ.S! AD/3ju!lP|@r<~v vq8eZ9LSX"U$a" d{qJjb 7PɢZAiMvkv{&(0T◖&}3[!2m*WỦ-h3"ck0;'p[+yOZCՔVJ2 _e)> Yo1^ 3{=,f{1zM,9ca_[p5Nb)َ,h,oE6]Sqn@xh~!{Fۢf:߻ch/¸5ۨmt㶵ށ,;nJ#5Bs5] @`< ZWRH JDg7N) n]%%_**s3QfO;5#ĀR#O0u BE3L54W1hx]}:yͰTWִˬֈ <|o"IL~!C劫PFsmȹ'!T~ Pn|U^",t,zKE*^9kkm0JMT8wET TӘPEh6kb &ODW1`;[>91lCTg CWJ8e1`&dh6&;?kςM @ ?ӽgA g1i>>pu?-|yʮe dN{^<ʝ'*PϘu7ڤ6(N{G(oݺw'۵8jx:<0jS&eԷdY^<Ƕ<, YÞꚻw㼺6N]9ЗG o'DXQF'YK&u^6GZ&/1#C]D5c99m5σ\R(fu/Fqa%*-z=y,.R_7r)|ED~nv^>> 1MkQمIv$BaS* @ {h+ \pCV Z^|xu_4.6a#ĴWΆ7\6ߞu1C=׿QEsz;e>/Cϖfe)B1; 7]xӺ܀irNG9]|߄is# #Nip3Mz丯ޜ9èke/{-wN݇]:{ywd\pB#w̦'5nYL;ZW q MiI?cXdi$I(;ч8*g߇0<<ֵJձvR壈1uw$Eч0@LCLVZcQAH9U:Ah5|0Xc`X88~^^YYZ%汥X'^8ڭ$={OC32D"c}p[̣gg$^A"o)ʥ%D"4zx,[_pg>ӫP[Ia+{uVD. qϰ4)tܛP58 g+h1Jh/.\XԵ5"$Z_Khq_܈qfBF!-:Z~DPEzتQ/O< @c pH`g֧8hoI#HP(~J1,f}Z]CbR(WREu `Ao@2f>yZB7=W?>͝68s9o<”4޿FS&Փ/0 ulY=]j\RF]EN&򎞡qBYmwz:s1@jAܼͿj4i&^OW('Y Ǹy|~+ln>Y=?8Xc lшkbRk=YP/>[q^}@2,ò)BɰU^\?,Pp ;?ʼnǵn paAO*Rr/N]3A+C x!^1g ˅|L9RfP|?DZCYrg1Wǚ(OXa4eJ>p!ձ[ /NrN.`o(5YtrGYs獶s 282@uluDMyX 71O w/H aPzح`Әlgz!Ozi>T? OwԔCJM(vR4}|>|Ge˫acƏ׻@umlKO ާN f(B({n>QQbe 5cB 5RR3DESkaTH:vqT*^e<^ϩ5׶U[VNcEc:\n>:CGyrT:ק]r6hW4'{"\6%p"R'M"ɓIM&5^c_ ֩b!<Idun,f ?Jb=pzVqJmzj Y* lz_L^'(iBBˡ],=}kζF+ܛ;f5[|SXweDQ-Ό,El߾ ,V<$jdPۦM3=I  N"[,VPq4OB kxڑCa/ K*Ջ%{|{Cٿ'80#q}QZ${zcC/c =돪)/jfir/'畕S&֋yMy&Cs[UNzmvZ,,iQ_AOV]c3˦1S]< ])ğ!x|>ǯN9^Ơ}*CLZՇ<3 +bf$p]U6))MCcpBjcmv.,/xYpJkppJ/X&3_xN6vQt@îY<߫֘Ph$_]!db*h|c}VL~")lcդϛ:g1\;ftK7w"\w 3}VZYI^UUĻU8N|r{lY>s4bȴ@q\j;xKvOOf2i;'h=` ,)|WcVSwee#u/&OTr2;'gl3oE>4WwNc9 VB- DeR[A%KISvgzgu -j"/}wyvRS;XU/@PRȽkJXq /;]@~|?( 7RqQPQ܁,vQZQIN̮dQ<,bk޼}bAk&SQZJ2mK_e.|UbjpwrgxеS899jAYޛ,<܀PQEQ(N1- /=s s s s$X8uǡUb߅^X`ٰs12_Ezcx+j@M(f&|fu(" /C!@ @ oB]ن-6u(ϑF4&T@~{?!lbXv 9. l}W @ @ _!]y3c-iSV%1dkFBV6*BiTvR$De!$5f~Ts/#[3p<}s{zKl*h@]Fb9'ȦUa_Jm|}z]ޒzxr-l*|9Vznۚ%AULruʇU'l3^F TVZkEΠ=gkpV}:||Va^2kƍ%Cw6,&-2 *Eaڜrָ$e[od:Wvi ulMFTc7]˙}gyF3餽msGtջQng͓[~דa#EC˴1$zs] ^?+srkm<2E}UQxF=룝Ϙhۙ wQV  tT62?6l˖x[3YY ~:O1MɼzABk:p#WCK4] T rgZ]=YnscZ!EE:aD?g#鮾es{ŞMN6+Y|/MqD|~z9!{!D-CFfKG*:#hFt"Z!ΙŮQ X )Vs)('ۄ[LGXa=ҏ5W;9{#:`xg V?L,VigZ6$X~]7ooMwcƲC:} twD"=:2 >bEVHm'nD"%p匦Tm GC7MͯʇͲŋin M&H)1^T{ N s~b oHrX~-kHY{gJvvZjq.IR_/~xW@y)5Ae9ٹMR nc+qع¯[n_) Qqˊ9ReWݎm1:( 'fJf QQHnPI^^Ži5^V>.zKr˃#N]ֳ3 m`1X$BC^^׸U-{Pñ>?8މӥ$KuQ_!ʁdܯ3pk1_>x 8cD: B1aqiPbspmn*ZW6G "H$ғ˄8nnnf_ܸtCû{{޲kr>.63㽯]|=`!DIEE8iU-:<9V/t=S&7`Ò0Cj!1a LU)yDJ o\S_7؜VW'ߗOA"EYKAYWߊ[/ˏRc5T浹e"egʇͲp۲-hʩ xP{923z6-Z2W[} 1iQ5HJb_~@1hL*ќ6m䣓~J2fu&Q?^JkqX*jNdˑ}H#,Z$Xhr;qݣ$i1tZ{!Q1WzCW$aɳ N 욦v1ʻWQ xP.`!QACc`/jǤAa+,+e-X.]{5s6%!@?@B)}7fB_{zT !@)&nToEoJڿL h݃SRw^0zWm Sw ުPj}  ͕8= I[.(Mavo 7ݻ뚻#a+_򥴒&W&EX67Sp? ;u.+*U׽G9@}Q'cTIߞ4{gd(~Y-G7"xoڼުN{:WSKRaaW4qC05hks> 'vM|kYx5*7g}}^iKEۼff[ %3!队vYc͐T9a\Zy7?fl^P_DaW2\/Qw qB6hb]ػoP݉OL%qo010X /@GA˴sx.7MS%@K^}"{fa'cbUQގȰ$gBmN=j>=C[Mp ~#_bJo~O\lqyw>N^swe40EvZ7 Md?LGށi ߸EiE`$Njw0?2ӯwfڠ}OLRi>cx+8Skvr\Kۇ2_iEĂ/"oU1^d2zՐVD~h~w>#YJޱ9Z+mۗm5%V6fǍo'ݏ.>t_yi1[L'Iu Ĭd*MVl^0_%ޅI S {H2do* %*1UL=|_1uɺwf%µ瞿{~?K[N>Nti]d(VcƫI"d#C'7Pj1mLЏ٭7H[{0^br`ʇ 0G)OHC=gc;l}gQ#oN T,(5Ĥi+m Qȟ/-X]CQPΣ3CCCd~)d0ut>PA4|QPuo>}[[:Xt˃|Sp%]R!~}OB QZrAi_8EnJߑ! *|r__Ipeo2D!=w%ʫFt#2rRZD_" c镑fU{ QP72_3Ɲ9g $CrSFavĴ*9ޓs]Y`mL3"܇6Бo]A_H^PQMv^';0"T@ji>K^@A/wf@WKx{ӆHZ*)dr_::(:`5 QЍQ(z56am -rsfhh`[GA2DyUeuJdkB"?@ me/+eΤB2(y mZT N{<,XVW&+). G؈&}\xxi:9Mc&6Ob:\} fVW|ϖxzzuyE? (P@? LgG;@^@ſ"$~ 51:T(P@#'n@ (P@ (P~zȞzS D9m+9cU4ߥp 5o_q D;LbcO)*pg=Q hI維YB K ReONU!lN?p?qڠJ ^ e}BfPNm1 +,O5Б3KNlU%jz>I;r"药ZR%VGn}աe’}/ꮎ6'fa]@+h|_s Fې#Tixe|0UAcvj)y j+M{XqŔʎ!?`Dce4n_Ar2L1iZ]=%9koVytu/eYf]s[IeiݻNcʱtpl?}JcWfK_\^ ]݌'quBIY]svI_/E ^\phK=s[Pp4e^tB9 ;>3j#ZN}_DL:iMv)uyq~+pwKe#Vԍ!:_Ӗx8=gn;)oLѼ"k] ww>N'w1v4S rc[fevSBkl2ZWp1n*`sY mwp,:eȹf xCw`Gt:=+t,?5 ~_<5`fZok8+ "7N,I :) vpFck5έ jhh<X&vMNޥ"Q۵~xW@y)@ sr s ex/VTHyE9&D5ؾv2&,#_nG֐uNq r]|X/9iK od]+iˀ˚6f~8RZ5gBu@PQs"{F_CFrg)%F).@I*Y_GCd%M.`6D+û tc/>ϱWunR E7⓿ l,YW 3_Ӳ-)ϟ?iyp8h +#9C6ޛ8n}N~.S`6g1$oT'Yzy)7<(xa]CYPaX }cHZ# ľ?cyTOZyo)GZ0l> 'D8<}7`r3UdX%T,3gZ>X7׵3'8b"Oܢ":~;=.uO}'{VnVޟŨ*iFn߻.6S>-2t5}l|b}'Ja0V KZ)1Ze*V[)({ +y&¼|, ǿ-AG8FGK}HZuA>sć>\vAt-=|ZRMpT{r5x7_K;||^rn{JY_1R6봡 %3!队vYc͐T9a\Zy7?fl^P_DaW2\/Qw ~8! 4g~ݼwUee7ED|\ΧrĂNGQlL7` b_i}W,/7si*O/Z3 ;vD= 9W]hm ]wQ¾oژ xq`Я@}1%߷A~Poqyw>N^swe40E15%=֜`J{۷d>`8p+0r# "6$+#n:t ɱEiE`$NjwڔG!JT9:53Wpf|z\m92^XC 2Ȝe/pagYW g/a{^]gf0:1+i`F`T ̘&Ζˉ6E_QtY!)HlY5Kavo 7ݻ뚻#a+_&W&_~W w&Pvagn9KCŽl!w/G~Js5$ͳ4酧og >߫Ꙫ9W[, rҊƶZ[RX@3yͤ}۫T'S^KkY-ŏ8 hO/P8p}(lryMWl컳6 %j Pue괙cPgӇr'K}doj1>i+?kF;vEc7$xsK&i!a5#ax KrײO"ߞ4{gd(i>%0yc\:3n^pfncO^Yӿiڙb?iS:s?=)eyWYhzzφ367؉3zXFG<t7Vtj@ɵxC?pNвܲPY~<7ٿ%3OYju%QJFGK{݁l7ň=\&:s?_OG^10`:A.?COZAAilߡԚ^?7np_YRi`8EIs5w~,)2P@5pOX?JvI5.#{9CPAՎ ?K>  `֡z@Ű-J`~/2b (P@ (P@ş46qC&zS D9m+ cU4ŁLjZ_Dv (P@ (P,8Prc/QVIyj =plcН I~rQ6gܲ5.I[%svA*}y`26i`6PGdXNIe=vSQ0 Ϩ'.fI{uNRj)w綘k*.[z}&e\\&9gT_j +w ǵ}ޛD'޹91z颭hނFlFa+@ D0KNlU%jz>IHo+U'U6 ^r:KC !i}HCxQ͠7;hJ@ Z]WKx:ڜ@ Ut́oC8R!^~ˠǝbA TWXJk{NW{L+U}i-&7GcƽS*;_Xє}re[Z[ۻ,- #tnۚ%/[,ɡJ;AG@ 4 Z<|:|E %;;ogI|z Pàœfz0.]iwaEuϓ݄<+xPzmпܾ(}ʚY}oÉ񍺬IO >&:sTڇ6,ӽyԏeP{߿uஔ:\->sRgu)-QbS_\{(y{L3qHib 3Qi7ȺEXj%NցN!c^`DkCE5+ /l|b\xA5}o1:ozvtإ_[o]~^Qnսr/Ζp_iKݚAlsO^ {iiW=,.D1z8]dC2%NXj©UM|~F`U)MxAT u(P@]aVf7.D>&uØ]뤜[1EJtʮ]w1a:ܝp1Hr:KC٢- IDAT'' Fn3|0,ߜjxhcą[#?Xavπ;dz;z.oYd{12};|l&Xũ]|^ pV@DܱofY@;tvS\0+EWj[j-M7yLڑyܵte_x@vwbzsN߰.ny:v*c0u1z' z5Ngr0\jR/yMDVNM |vU'=y-uY# '0Ue ^,r٧-] 7F=Oq6z'7s8 V,GH錒So/oM@j[v'jHŴ>$l+b=cGQXa=O5n H_` mI\9w`Rs[ivGڙ^pbtq]7oжTaKH8r0/g40=i 7$:1 6δlb!KskOzC{Š9CvNˀh,a5 A?tW߲6.w^Ҟ649‡Z<||^rn{JY_Z*53 UP(9]Mcn|T ʻ13g& azi¼t0XN^n~~'a&̾wUee7ED|.9rĂNGQlD]7` b_iwix>hɫOd,pLZ*3L\]HtReey9U?q(vh頳,^kcpao IО}@kydnX֠ðpbzA$=၃ފw=|b-϶.>gggCy1U683 h5Y <8>3D8?DM6_B(/ 3QWQK D÷ai \]&.ӗ|[:X("l=_!Ĕ=jIWTlg$@!C "hPi}]*}G(d}ݺvv~%]+MwDVS(QӍKցZkAqsKUv(WFRWk2D!COZ|3猵ݱdBn"Ž6X@}1'߷A34 li[^{:S8+I0^*)*# zS)d4Mm Yɯ|><|xڶ"$Z_x,t| }摳s^A2Dy˥Z,XE ɔScT۫dB.yD ihCrYMˎz/v#3T۽iRH:F=:%ƢtA/f%_?`7r|/ <};}[yž,|p*)ӷX3qO=SlW[t$ܗЩ ( 4R f:Z;k҉nh{uPPljV~1ub赴VRsAk*M=d+EacvS.-QkҺOkÅ<*0v:m)*zU+Kmp N+P|؎0. ӧO,s19QD_I),$]Hl%cYzFY]}od_<䝍%LZa>%0yt&!bzǒ**U (P}` At: ] ٟ>iӦ&;4Z_Vן2KQ@#M\  `֡z@ (P@ (Pэ (P@ (P@OU`!BNc)gl?o1*bs=׸Q&ebY̫LBn&'<71vm[@ :Tj}HCxQ͠7;hJ@ Z]WKtWG._ZC4^9|aPNm1 +,O5Ϻ_c\@ h2XMc4%o!0u\mEi?7R1FXRK>(=`opLrq'65={mxgI{ſ_j0ڪrꀂ0ެ7q̌ zwKŻ>L8Ÿ;WIAUTZC-bPGcWlh#,}VҖ5mͮq㥴6k'ZDه8R RK&S5'I w R3^o+\G&(;ZRY;jm%yǭ~i @dn_^s퟊޷L%6Zŀ%Z [F. gDhz3܂lcl耛(X4gTSy**;jE^~ ?d5 B h~h Et8H?ls ۼ˥+cAC"J oECWY~~-$rSn$m̈́@&CHS.GM96 ߊޔ@к p { a:83$M4$yEiT0}5 DƏP{?UuNnk* ~_<5mX)oLѼ"k] w/._>INgi޲5[%(9z͍Ak'tet-k,?Y8sk\NoY?xQ͹G ;F\{kDA2x !usy#N % &tUYũ]|^ pV@DܱofY@;tvS\0+EWj[j-M7yLڑyܵte_x@vwbzsN߰.ny:v*c0u1z' z5Ngr0\jR/yMDVNM |vU'=y-uY# '0Ue ^,rjìn*\|<zMF .sb;`f>RTHީV^ISzF gj"ׇYjᐽ ɲem{7^Z; 9UM@\:bn1Z坫č!e4:^s:ȗ@lv4]QGcmՈ[C<9׸uwB}7!jn\#$zbk+AJs#goS,$f V?L,LO+ӳ - 1*bwݼ6mKo:7o_<z 59azc ꌃ6YH"=p@ 3R]m2I$g45ZF+/+$6D"xR-.P:Y&,#_nG֐uNq r]A?Jjlm|:ʋYyQ)kIV }O (ϗ@k ̂(d(Q!!݀!lI2DyQxffZ:]n KL(d(꺎7qYLc SG R^+dC-骗m|(dBԒ] jmcNhŨwdB ׭;mgW"D@ٛ}Gd5 QH]p{5݈ T9:WeXze(}U޾&C2M#[gkc3ܔE1mbNwRghdL!!7AG޾vq }%# ׋zb@E17ec"{yO# W4o\ScT۫dB.y=݆6~y7myZ`uB()^\/sȬ2I[ X!Oe`[cFۖB!7g(F6.1utD!CWQ9[W\-BCz[YJ3 ?JBۮ$U3H>mx" G~JK)6bDIgLJAV^UK(d(;yٳk0r+mۯ{/*xvPissՓo_$ ]?ne>Ak.jޅM#us5$ͳ4Nlo[{\j]¥P[ivZqYp%b2 OY&tR.m5{Fs8~=akN( IDAT5U9 |픇Nׯ 2:n\wM|Ԉݗ`aclFfS.o4Ftճ30,ZeiUgPgӇr8ccWei~%1l~,g@oΘ?dj1>i).3uSIFuDCtN:t#%JN Q)Der!$ק@T~o5gf^|>ᬟ>y2wzYl<³0 r+vXv`U?4z .5s$8@ 2g@rz)2E@ @ FEq"G%*L-_OUg<ֲY0? @ @ 5EbЯTWTU?N{:c#khrl8yϨX.*LVA1<)[c_-Bu^Ug>kM(j̷߾$п ֞$YQ{dzZ~ʀ*:V_u_b摳{z*&Ysj(m|ӐBKl"` 4$E_eִ Mڠ4g2@ @ ?oIq{( :u-iz6S8W8//3+7#+7][}FinLq<(Sy] **Ciݶ#I/c,VSV\|.Q8y3 4:Fe^2`c DY8k VseJU;}0@$$q6P&1 jL*,kd۰s>qәC$+x0/!7f(12 3zr#dڷzxr+ߌ>5 3#;WnkzZ8PnC6:jʓCܳKCQm7uXdl{cJbWl H`i!g3 a&?qV`U%IR Z)FM.+Ao#ۧUgΚP_M^!@ `0~.Ϭ*d߱~FDv„iVcγrd>q5-j JUZ^M-<6o{T"c?GʻK&nɆٖ8"K*Dw1"RueyS;ާ>6Qňӑats}9~tX=rQ+~k )RS`w? rNC+ !g 2y'ihQg5Cf2?k 4"k +sRUwƅo{Np @[8viyi#ٺ7~wGEAe6fй'hϮMvd5=a7Q9K3֒^rgӓiYP}J^Fh,:@MjR[0 F؇Hw B B SӛPQvV d %mo-vFԳGn>/onܾAC((md)EÍ;7j!t(thLj2$ypΓ -0[ii9Y̏$C=:zpwzoskx%iX޾xD? /lϢx<~!"h۰p$M\Ig },c:7"8YԔ3AkVT>ͳ;T:g$E!IY8[)Kwk[yzG0L܋&{Ld'Oy7b@ В{eʕni6o.H)kv7|I-iUI]R7\1$VA];\2GX^I3ss+JFήX=Q|MK+ W_uQlclmj8-WXY_kPY!GrrɂGgn[}sUMO}ᯭmzГn_HBGU\C*@jq`]!AXan&ބ_&dD*Jy#em4gq}zJ;=/ȫfdJX :]MRrũVt(*lؗH3-vD=R--[} @+ 3ΑvH_7fХN:Fg7HR 9Zr*ퟝ _t59ߴж0+=&S`,v]%G,bTo֕!hST*90#b T;?ݠ,ۍnmuu-֣pnH;b<6 ̲H۹VxY^fEZ Efdv/W?WC?|>ПG[82|BwWt~>#=5$-Ӑ'^6QsZ4{NCNN 3־V5 K'#tyaE_$>e™:_}$=iPEpBC@#ij4Nژzcy$fF!tQq;;6إbj^[Vi̅S]YJ% WYFQ5EIʱ-e1uI"]+|߼!tuBAkuV:>5eY[zʋQ:fePmT<[czd2Q3*ePx+4( ^)]/}b r r r$?S0yR:g:@{68=)+%B!g9DӻSOU.b!za{(i/^6 B aUܰP(Ύgʶ ͇ \հ[[}e]FDIt _K)a K XHREtă/ JQ#cJQXh}T T*5 -.3f1K|rC*(% t5}0_1G#6=);0n*pΈw3yE+ /x,3q? Gbwsm*7/ U횛xUZXSKهuWxRZ#ct_#4}j:'eү>Y(@ PK/e¼\z'& ^o%~+H+I;(,z) ^JiX,r ~%Њ+s4vz>sEWE4$8F?'Idr&vfwA5yUl@ھv(u綋i/\ݗޮ+I=/BHyϪVkNDȥMwx`&O)Ptأ?Ϥ/ AXKz Jr1Nݷ'yA)=)#jҲyCӾЋMRDqQfCy!MJM[_9b(M0A1>sTG'%ź43;@CALnX¦'b3K*EDr滌]Bd&Ep xӌoQ6a@n-%K:G?WC?BkK3@YL@EQ l6w߽hƍUgXlߥ8K!N k\x YȚiq"G%*L-b'j싪 3eGKT,BtYgGj/:|"_/fy/#j/sgNp^*=;i9Wb?5/@ ȟI|V̵ꊪjTi\gtlMbMCn'5T+EeܔJ<Ƞ5X9;ek ]U3!]T3|֚QR՘o}X'Gx'X{F_0gGGW8j..m|tS4Mim>-&R~~5HCV_NE^0Q7qJxMdܭm|cyFue!y%Q/u*ꯂ]a&$4@pF?H)&ii LC{ :e}nӍcl}Lf[,@dnT)k̡U8yX_|b y+|C"\`' @ FzFFrR!.r]?6K)|uR[î-~_i4m)g#В>t*A]EtB7۩;$ezbŪyj˗/y$p%:*'\~&<ݡ"@o陻-6XC!Q=>(>\ٴRUE%Fkm& P>I!G T/ }LCi ;K!Y6li!ŃOwtn? ^3 Kȍ=J bƌ/7zm5ca#GDf7ć8L==[/N5RN{~']3LeH1/N~/IR Z)FMj@=KˏdW܊n3w.L1|$,;zxG#w(pZeO~dϫ1#D^N 8@ 5@E^ ;+NQ~j,}[۳'.#EbBAxJK{Mp ZDݠgHyob-0Rf$ѡrzkMt'#"M, >[W7s}j#OU( 11Aݩ@*o߹u>B֐"5u1qӼ\ ߛ >O!Pr@/wjFu\31to&c]AN/^,̼PL񠭱"0f®0.4t[dZ6T1۴3):҂ g~Pµ|0LW?`[#4z &5 =]"snz?mkYH \AR̔|,ʩ`+>l @2*닯-KX6%fl?vQZ~ܓr J|`Ne O D~aAQnjHjV]3>qf2/A8{Ѥx“0'Xl@ yZrWRYЍ"MQ<0ޥ)emَ$є9%͸ ۡ:K+6۪#^w)PP~ray%gcs+JFo8|liEQ.ڛmslM 'Է6PXU xk;gAK.(/58'A,OR `o2YdSL?m8Y\ן~z3,OƱx[sė ;*KY Tܸ[ep*~?Rz(6 1B" _qe}{ \73}{!}& O|:"X8RhӒ^9e}$Xq?U9Ah-5nSa?8ݍ9>XM߼~~|I@^h\{~МskYbӴw)@\>hHBXuڼ)r'V61&3M BYj"c\sd&B2~PP̊vF w\zy\ Bt PFA̱Kt(BcɵlYVtNnփvީe^]HBh9p? /ͧPL|0p6˰6`]R /Fr9܇<;|BYdw|u1U,^3QYKj^_.pWc/rGz'=c믳O3LQ5 *y44O6|ClEQ<CN*(7Up܂ohI(-7m Vg-p{,#VEQݫD:F EQmTVߊr\Vb4/Q ),M-C3lx}zTA9ICkQEQvsCۻ6妊j}IvY7( %b{a^UKGY?<>f'@ʦ#(gQ72$Odշ6 ;UPnk/ ;QOtRyRW'[^~Uނ(n,̼ M6VEYm4|%46QJ 8UQ[3P͆!4`^/S\˩jCQEuÍU Rp>?m_.Ozg 4g IDATEQmv WS>M̯p*o3EQ}{\dcևTPm \SoQVpPn&V'^br#)祵گyg8^v|@{bĒy|fG݉=jAVK5Z{R ZQEϕq55Z%y) Mf$}NNNJ1!,ER&/?],j?'''5R&,l'YYnV7mVВo EQ(d%KlJSұjU;@c($B,Xwl֤:6p vmBz2cfM 7+8&z7lwK5{;Ň_}Fwjf[jdUYnՌpS a攅n|ᡯL~W6mM>61{TYlZ^Ap'}(>5K}?0gUݶQy?c(ZTGUUUcFV 0Z~@SUUU}}Y>ӛQj z0_ɇR,#~F}AD +Mhmi()jRFK 9PXP@ mzߘy\V}jafVPfjW6!5:zhϙBs%eaW\~h2"fEJ1i{at6\Il3,W&Iby_p{]^>T~@s։eS f9slucpB^y[o}3ۀ'![E\!#9>+֥] 7Y[<0rhW7R߅I[>~ed~6a)ûƧ&>3&z!=#ȜV~(əDڽ5TI^̅Czmɪv@) l +m_;᪮sbL~1즌 - N'ܖ{G$cL5k9/L;~ktTnnfn~kk`TB"aH3ƫM@X7,93'h8Do c1A3uAYBLWjW:B(K:nǙT/ *H kǔ? J`W ,n&2?X[wgm:qa11Zs~Gx6鸇OgUiyJ_ث@DFbRJ<,7&ziQa`\?brd9Q'DP^H}j*{J"GP> 4YReҴKL61MWa ֟,,߂/wʙ2jv I+{_q-M3fx=cG?l~LoŸL[;h=c#٣Q c#4P#,xd͞>tݨ6?J1-51yx˷˭m' s>vpqd)?cw!s"`v%mGsߺ]J&n ~ 'Ylߥ8K!Η &CLCꊪjTi\gt?HbMCn'5T+EeܔJ<Ƞ5X9;ek ]˱E3Bh|+L5==g%Uw؍5 ֞$YQ{dz~dQYU a/A9XV>UÉ z뽿u @ ;FzFFrR!.r]+޿ͥg|YՋeffdְkޗ(mt6kdzOhɏ:uzҮ": ?xt2=1b!TWvBB5m_>qәC$+x0/!7f(12 3zr#dwixr+ߌ>Wo 28SO;2xֽEq 6lFyg0? 5aa"GXA{N'~^#ik׷OG^zJ{ U$I/hlҧ_77&(Vx陛_bؘA3zud}/<6B}ZO_u *o`V &BsS8yu}4$`֢he\Dgx?!Q4PvE#H<}F?;~ `l=c鴦+F98䐙?b^5=@ o{?xsyf5P$"S&?سK7uYW@e>״H^5(9/Wiyi#7j QAT3?Ls)M,ys%f[^~Œ$:TNo!bDgvΑO}Cm飊!&#<_DB҃|}qh/`0|6͡o]0sŭ=oP = U!Xɵ>kpT(:&0&9^ө^{G/`_}uދ7eS^fF$ٳǍ϶,8B?GN-9B^kkk?cX/޽1E/u tܼg| ̴e\ysܳ# Lds[B*/OxaH hɽ2 Ds nK2ޥ=emY޴V9R 'Wa;T'y.6}7Zq~-w)P}I%K]_} Fo8jE".ãuQbſ}SÉR腡8V%c>-{Ts75$ȹ^?BU[ zZҌ '/>2nUb5>961?9̛m09[q(V7U@| bzy? W1摼Ͻ{17h˜zg `d2W [˳R<Ͼi)=Q],@$?P_M's@ tAj-)`(#g1 -* : |:;Gf]E%{jHZ:B!SOl>r _i;^П睜f} @kZԗ OF4$Š1ܡħpZ8SK"b>񏞴k^8!F4JqtB}VmGz36ǼH#: {|רoaps15/-W+4N}Bکy˳(ښ$X$]vrʮo^: y:Z=̓nJ!tZ}j g31K-m۵]v1*{}\,= m򒊆I_r83z &}Uyfsqҋ2IVD% /CI~jGCɑ홁nYl#ĎlKeH;aX␽ydzx nwskĽo̶9Wl#|8|?HEݦSpvcK͇i+ >o9]swIW6O `[_5ɯ+'آЦo~enLG6,3WGmI;jZ,0"/{u;>!3OÜ6Aiv"ru:㣹 G#\feYO˛y]d` };"$Z.&ܧӛz>zpGwUzzs?BꜬlڛ>Yd sE^{<5*S{z8aUܰi_԰fmYhNWH*(V'ND7n>Ҫ1mJbl[ OTVZVZn)wvb[ >s\$KƼK~Ex_㓫/&@[I{FxЂ> \+URc ׽/R3յ9Y&JjpO\fhtZJ l“+:0Ko-i1V'*o~s{g}/o3m@K-n'R{'4aE(eD&ێ$c*GI`I.R)&Pk'\P ﷚,/YFwT꣨${3VЧ1o"ޤϭ>\?AΘ@@ `H2Tc´Mۣ\WS$:^ֺ"$ERÐygW +槳p7V8UT'qg$[9)#qK %Hx0xu-=l :``q 4QyR220tnh.c ;`T밸V#myۤo>eV})}^-zhDdD;(ɣV_B> ܍?\O#G13;hY+}TdeD5EeֲY0? ) rELQUK/DYUJL}6356pWY2}%IhX9Gc3QD;`[N(}Pm ^n/i]WC1Lj$Q,=̲[5Ďn<&?7K~+?QBʻ}Vz\s"E.mڽ 3h}Ogly&}y{ E=^Ά}NGCr1Nݷ'yA)=)#jҲy|~yi_ptRzNU|&rO(&%&ӭU1&bEmf9[죓b]v陝f\! j7,a%""[p^9]F ^.!ye 2"Ni Gw (sC };pkg9z8~9z\g>D?8݃ć$TFIgBiz^4cc:;b}*)~CL/=nv2Q{JQEӰwmSJY>+0!Og\2vxW>uQ~E9YzL%ZTKȚ=}RQm~|#QsZG^4vO>u$3mc[Yn}3ޢU̞wЁH{لy3K6l?-@6)82[2wg'6(w]5}[$-)4[3zײي۵W7@ ҟdX~@/@ ;g532|X Qv)]Hu뚹lz6Sj8//3+7#+7]['~i`*SF<%?;|dҮ": ?mCzcfj gtr?\u,s4I!Owй0*sKch4$Rm}jlZ|5s/% P>I!G T/ }LC=*,kd۰o>qәC$+x0/!7f(12 3zr#dڷmxr+ߌ>5 3#;WnkzZ8PnC6:jʓCܳs2 OovSEͶqdn6RӐϧsD`UT#ty?&s.WUˑ3|iyɉaת5A}dNC2/hyf<$)-;b}'Nhh|ng[4\L͋w : Sv+k^d*2((I9V,>.Id]|W4NCrV:h<4Bէ 0kpKOyq:fePmT<[czd2Q3*ePx+4( ^)]/}b r r r$?S0yR;LПe3OΗL9OU.b!za{(i/^6 B @ v~q%a~(,(gDu&MoB_dֲFم[1ZB,Nv^*ӱ }$dԷ lOm.=y(0.cYJp΍C}R¬xVGT|G!d>Y5B.yUc~:!ܧ~}󿗣?s^]8WbGr,̓UicnmXJY,8SctQ\\ƇcuV m5.e\A[wMq}wS')gag>'Xm+Z0Kl 2Y@K඼\9ziT'=w?Vϟ—Cz s ;aU}Vd{/[wEv/ۨ/#3ꦓi^f.4x5U\MZavcyTJ?r#ȀY|jp*Iδ=Ӣ=Q+%V]>o>tx ̡O=kĽʙ{}k >䶟3Mx _t|R6Etoma61Z篹!k. p%WJhSIby_p{e fx*b?@^Zs0$tݘlXHmg (&.C`Ec[ҎZ-?c*K̲HGC^p#>nȌ<0MPf 6uGsŹJ@h=J|0+z|^#hdjվ VxЊU^fEы^1Ibc]M&O9IJ^҃7r^?"@ @ZB-?%ΙzbUܰP(Ύg< Չ>0Qfm[6nu}H{Z%1{6í'*JLIdbuWe+Gq,p~E^:t'W_e˹[V%n뤥Q&# }VnaA vX%5Yy"5;?3\]!;Z9RvIFZ E0Ko-f/"*o~s{g}/o3m@K-n'R{'4` RATjbH?Rq{V8Z䢿!rjJMu L~Yet7J>:AI>CoC: nk=b)Wꮠ,Y+}TdIXnlQ}Qu{&w' sKCn/%7}v.Du"H|Lubk0I2T*J}|jKۣ7T$8^],6X@H Cb睻$[j +}' 2W3:3NeQ"ܦ^2:_L"(-Ϭ)o]cd{/>\rN 6ˇv݌) &x8hC 7Y~ŠR&myJM m73psZmhy+TjėZEd(#ch(JĻ3sIvTirfUVq9wB2Vx, Wz%ַ IhKc O8qvz/=\?|>84_۩6aHuFݤ!4٬q QC"\TM4˃ ZӍcxS /U;sBu^Ug>kM(j̷߾g kψ F,h Y-?%ͮ>U)w?7P֧zlߺ ҿ rELQUK{߷lʪTRƷ}᜕IhXA"IiiJih2*W(lB:>~o<|~}Vi#CG J.eO*{{"Ǭhc8yTN@O8RiuIMow /d&x_!m&h![&^T6is"h(U<޽L*z.trx!5tkcz+yn_ˢ2ݼTܢC(-i:ŧA<\IGrN k8O;v$MQ99k`,Vwg݈H-" &:[Zqh#gǣart\AilomDRRcΆ9T`58mYeՕa0x mla蟜_(+)7}66n3fL*z|559L=wk `d|)2tǣ)t(he6? `{Ԧ' L-D'FGiC\ێevhFP iT:8Y=箹MO/ו$淢LjILys's1JOZu 5}_WTb3a/9\ ]Ol}4Wuc'ψw"V/.z%,5iJ_V'sy+ j,X~1ܸ7WP]:Q|WVܟ &ڪlײ hEA wu iӧ3˰k㽪ۇzµ[ozF!zN"wK2~n::o@ŦC&sl+:{jޢ:2g`< .*956Hѿ(I0uE56L_.^݅a{=}J$'(MH"S2d7#XxNzg72>^Z#TV &ۂ~q:0Q}yG;^xpPLUHy3mqpQ7@tsNxjT" uwl_8j~y<;@ 7ihVן2K @cσgWfe(_|,@OkҶw^q9 w{]CˌGQ*ל'ï$nW>[j'*s^2}ojm_d~UI:[hϜ(ԶEi}N(_c45~sO>aq'OBoΗp }* Hy'hCD8Hrt6u؏^08k(xjf,jG}AaF5v&mItV,]9&r~L!$ k:!( m'(9GPmk] ^|RBCd\mē9ߏߕ/B%hlZm Á=.ZpĪPtIm[|(L@,NmS)(:.VݡYsJ=e$C0 l,^WRh"ԗ{ gJ Ѐ2@1Ī@Sd*̀G#pP0Z&&"TG8ӻY @ 倬gᤌ؊Uxlhhk-iacJ9:%u K}iGA\9ԕ5@ק[iVN*+IkBsp #a6:nʌz|XHjIx**  Й8=k-.چ狩)E@GIS#= 9e(M+e aU>ݴ`u7(L)dx5tZFg{ٟ`PbJ2E_Z*YPBg-s0 E15%svEj+^S͍bm^?<5;6̧0$T4E^u DÌeʰ n]\.ғ|ջMzLT• Haw/֙q&XĦGgۇB2d7Xef9aGKyGʐ_A3̩bStsWzB0N¦w` @F3UʊrjAԳSptGD4^[>?=B&Q8?3j=YD!ɿ83TS~ 5d ZA2u{]燨#tМfv+}Hy{Tbj/aWj&VWi (dҗ*2 (:t,BВ$ DiP{ƻzy{UdLLu"(T2;QK", v.)JD{c+yg'Q$J-ەuqœ I29-?Ӵ*9@mf2M'qm3Y&J=݂>RQnT2Yug:>8Agh2u8!.C救Ҥr(d%y%8yM 96? v."?n]_* DE ` <3,n-(dɥc_;@5 )9Fs3\q8n>. |oeONWIٕpva̪ʒk+d!=oY=ɤm~6V*^q]"?qQj›,3%uB9iW +tw2t&01HFGd+SF41wF(O ǿq k^/nZ@iG>MϮËǢY!tz;iv ",NqXqZT:"z*=^Ę# ќ䨻= ^H|q3ۥ4,|qbڻ"Dbb;>* 9]%[M.8` m>Y3Ra>>#g$}S?DWQ]i~#1#HNLč<}leƎ񙤶륰9(N YgWWk^[+-=p,[SqyF >uTȅ0KQa/IAk%~^q/f⦀4ʇkk>deNMjwxe-ѵ +0FLH3yM s0VU\Q:卫+ V41zkoZ_-hud&++{tSO_ȤL;)Ο4Wl%fvye~jKy:9)e`j.t斓lݛT虮A$ė7QW- p|6ES[/Np?kγ{yvSҳm )A1=|^~@ /u{yAG bۛ: (ړUF+q[C :Q VlՅÂ7 e% qf Zc{dpkZ_Z@a7mS:p4C+Z%Q0,WLe9 rs=.K9Sih"Ҿh.j1Box_X IDAT8GX񢸹atvYӚJNmFFMtE9y+\~>}bcF?ӧm?摳>ş)Y2?J?7Ӈ*1\!LX+U8''i9CLFbxè׈ #섽.^YߙzPۤ_6z^,GO1ଡ଼yٱU'okڥ뒹pLJ s iq_]W0Q9^uFi&zY"gos2Rǖ1ODkH(ZܹLFl坛`m9ڔC]~q`z18bzg|7s/=ƥyÛŐXg\^nC@bi(?\@cPkȥ41썈7 wXL<{{"Ǭhc8yTN@O8RiuIMow  oWHIߚfwk"ZXt|ڱ#AoI]kf/A1=|![&^T6is"h>^ȑ(a]#??}/hNv*Rq&!-쉲Dp%}4iG7hw؍";m`Wxqh#gǣarc||ۈh6g Ws s@ ;LUV]YZ 3.ɤM Z<,+>e%fw@?=yƌcp]E1ؑïi8_}A]: b.,Z6|9k3Rzf_7DO4KvPNSqAVY֒;R0G/uz{ZG$jk˕C1s=N1XwNny7{RL){2JvYлcR/2fBӻhϜ(Զ:D~V|džb[|/x._JϴM\OhNnyi@hU7R]TzoQ{GmҘ.I³pxC_f͢G\qml/ȱ$~X13BUI Y e (joIqוXLT4Fn^E |*Fi~Zת|\]M&892ߥ*$yZh2vDT}Y߹ww L1J;~1 满m/*3VrЊkoK-6G8,a:}ԮtʐVxS癹ܑa%X^}?uݳ}JW]S} mXe5 ;k{v2dkCD%cƯXv$-$J욺&1"&F~ !8)b>mm9RvNfi۠)(drǬvMt @͜ʚiSЭ4NkgDxٝPʌz0}&g]̻fŶ@S Й8]FWyKZȏaQ%M񾓎SܴX :Dڞ{HGaJ!T^\OF#uol܂|ij,@t/Œ𝖞tӨ+7_iN؟2,EAz<^qk)] #FCyQ "=)Sf6̴3/ۮ~'z?\prƖBqӥU#K=!ca!kFڣߦc0LYwyN'dDvGD f^߃KBESuٰJڻ- x_0|S2mM>c4Ȩ+j+*\8P?h{}3u IcqKX\f?vFT8V^ U*j*"O)@ ej(+gPȩRN%gЃ=L!-TUM럞A!(Y XCXPO5L_dy@ld DɺCTF5'9-,V,fYۣW{ &PC&5qmgLD!PiY@ֳ!^ZD!(/# j/xw@/6T{LC%X;jI%^AԮ%]y^q|s:$ D}ấ-[WŭO&\O'QȤ@wOj,vεQכ[Ȥ7&qַR>%uox-TsS&R>x(j\8V z1r󢗇 Z.%QȤ*oo=~0̼:_;NpKǬI:W+$ 91R|=.YeH)H2rx(C{ V2%=Su,lWOY=[Ò)d"tm֮jw+Tyt/8?&D!(v(iV*/2r˂Uv5Uernv1K/+`$:ɟ[G~aE%kc}򞧲w0=r @0j~CDDKpw}).2!@fxZqFfΏjQSu ڔa3!g X `0Ng{A 8o112D @#7DY @:z(RBLLQ; @0ڲNؙTfdPXWl>Dz}E zx<ehS{U%$6u<3 =uwNX-#xmzҪ=m x  mvZaUֶEG30kO:(}Qx<^ģ; _r(-<hE׌ ѐZ.Ax<^k*9a FՔj!ĊN/}V?Rr=K;=+mlmk&:@{8Kr rm|: _7h+5['a͹ֱ~uxNog.=Rs""↣ C* ;W̶ݫAVy  L Zr>31" Ƽ$̛o[J|yUaDEn{vÓk,:E$K]o^92QEn kr8jY  lH|*t8r|Qo׷Jguj9Hg[̮G%O\إ-CѸ1n ѽ-B#?qhZ~_=лjا]:0%+OjCޒuĖx<~wbM7VxSzGkɍuֺeR˻xն>"/*sx<^M6:[r r;rY6>pkw׳z73UT<_E&w kDe>&q{:*O3[ac|&z),cξ(ʻ*-CfZrF *jlySSpZ_>wgj}SOuPȏ\2VѼ2m`&n H|麆!AVT<ФvAWFA@a"ۻ`] 2 cd1>IuaJ =Jqo%֊&FOcMSmw:2^Cco/dR] #G|,KdՖ uS⫻]D~ySaJ{/8X%d, ԾxƠ5d<|\%T;3]E:v+qKE8yVR_V=hEw0]%[M.8`0n{sѴ տ(/1ZwYp!k bEi뵱@L]ArZDN[T-N\RF,Q| 4bK봲{6Wo,hm[w6="/f5ЙVL'/DЋ;as ZT:"z*=^Ę# ќ䨻= ^H|q3ۥ4,|qbڻ"Dbb;>*9Ls;r^kg|m}|֟/gϒ33bXLTr,GzT er9vB_c|Μ|GL|3K Eƥ*&&+Z g/V7G> @+魵"(2CjSIu!UW\f`dRajE"n=e+6$QNj} g!1xxOŒh)*+k@tj2&8iR3%gHK>jX0bHKΐ׿(9]m&ǜw[?#tаsd< :g1HW yyaD4ޒZ:;@OjC RCϋE)&s6/;VuMt]24C)aN!-9Cr 2 &*C֋q(#yD/ 1K|ymNF3:ɳ`z E;W݈͠sW>gv||_r5~Ƚo<.,T/y"}zVL|8uǸ42owx54kܫtvmp(lq ܃6egk3SNܭA$ė7QW- p|6ES[/Npޖi{nqO) @JUx :"umOw@γ{lMmz%6j~ux#H&8 L 3%r-{3 )1ȑ(ړU~F+q[C :Q VlՅÂ7 {9Kr"1)psִ~1nڦ"1tP#p7k*abkph/i;7qYSih_-}).\>bߞqJEqs l湝U9y+@?_ $2(ˈ@A sd{{Z?%22rN!1NE+4%VÍ:^f<6p)+ەOfh'*s^2}ojm_d~ILٲF{DA4-H믑}NK":J W2G>aq'OBoΗp }* Hy'hCD8Hrt6u(^08k(xjf,jG}AaF5v&mItV,x0t^}@KuEWk{`53Ry-8zbUWo:Ĥ6-c˅2nme^UyxKʈr|W)/ɭIA<P2KF˺UR8P. cB, y؋%5A+,*䰊K-TDΡ x$"^4yq;OjN|?~W:e SB3x?fء#Jн X,&BC[i2t=i &*{KpX@)Rz>^_5 LWѩ_JʛKh jMq~w)7;tb~l35얊3{(@~8WA3q.S9=B$ga* .^x:/_ 45E7F*Tw:uL%q`K١w\TЩ>NE,oic)nvwvn._͇H#G{7 0O HaH}r [>^` `}W-[8n.zK,U2卛W[酽7Z䖤_F);yhWQ3wgh+L In]%-{ƊAk,&ܤ<#wǣ!s2uon4& ꗮob- d~>ǵ ]%6IC๭cvi·qڹFPzc4tsO1c\Wh؉lȡ7\fYmV!ΏBɁ^[zNXǶOm-a:mJ9:%u K}iGA\9ԕ5@ק[iVN*+IkBsp q T_fݕ`M.][#U>U0" B qһPr|%E|10(isw8,Vie {At '\l:) ]Nڙm][ij,W.J¼:艋M;-=E/t:`>wZ P)ݩaJoh*y#V z1rB$8|rKLmi =/ۮ4NH= YrƖ[zrӥU%lfF8 퇬W:Gi4x`9 C{eԼhmd3o;QOs&w+zZ%yt/8?}. MQƧer6*i0u߶ה{xgoO3o9c4c12jʖ6Mtp}2F1Sg{XT{FT8V^ }[WI .T 2%93Sfkqkߒ7!ci:?3[Yg77 ?GI滃iBilcNig뿩'(:=} 95.I\ha?rCgO))*pn H,%box}"9yt*9g~yH.6.<"iq;u}[b\ t;OmH-9&]I0o!s N^7a=(^]Fٹrw{LpRkŸ+V'(dRsZ;~ iU 5s};(C-dR`OB@gMQ/{xû}n1r6CQ/-q;4u_w]̓UxJ=U4Fs<l)6GkզC~l?*YH0,YgV˪a{/ $rmM|Uh[U">_kek]+Չ$ɡu_~D3qZ[s_cYg*htsx8gLͽN vs88c)9E<_ͿkN2_̯=ym%n .w߃C-tsFV]_DBE'wxmk a2*{/fp@yh]_?D0" /0u񦊀[8 7>s7y.`@MڷqVz8/x \{<SħFQ-';B>99\sxfꋗs__]7$7g4.5WoDZ(oތ] PCqFۻ&ZJ Cihh_WT,񩋍k O7&c(.ai)i8&%9CZrX6{AC/ y,|zfr}~!9=BW (HS+1|\X3MvF|0@+3}>PF! v\IXkv#6M_=o@ŦC&slf[Rx=[g ڣ 3\_fd(.RP+)7 䲥|V2d7  %~i󴽉$J•RIӀ2@f/Ix Wᱧv `jE:m#3HmIb a9#9Oo\q8ڱF;?낇Ϟ{/8X%d, ԾxƠ5d<|\%T;3]E:v+q\s]WO0#x<$bo\.!%uE t%D[xX%Qhj+>',]P~i@hNxXaԵOS1=vȘI\ΆG49_FCV3fQ; 6(7iX H򠻵*`fj"_NbȺܰ ߎl ݦ:{"sĹ )ٶ\ݕ5?'%1AuoV@L<9᎟]B ,TƦ2sGO U[yMԦwQ0QNYb2S8Pu\CzˢIP F'|+{aX4 ߥ2;,Eȩ/[SΔĠce8bU=T*p6F$!ab+R%7ML?EƩpw!3A+l ϹvWr8зs2zq(GG3zNXǶwh-a:mJ9:%u K}iGA\9ԕ5@ק[iVN*+IkBsp FXumt/}ݕ`M.][#U>U0bC(4 ;3/qzHٟ7[] S S8Gz~sbuQV˰Dª|im?oQRZk p.-4m z 5S(]y1uM;-=E/t:`>wZ PSS>gWtV5%,:Zh|aKBESuY@4XX kJpf˱`"=)7Y۔GD%\xmAagRUMlzt6}<{/$C6}Xfv,р2W *sjԲ*6O '9͕Wv6 oNRR^@cPkNġ 7;\;@_uSupix:^]ɻLԾ k%3x@^45E7z~+JuC^'9A]A^=Npq\=cf(oq487Yt,A Z;4l 6; :鹈-m4xU7-U:q˶V%63KmLyU}wEmV;%Sv03H8a Gۛ :?YvKř$ߝuz* 3w'Mtl1Ak,&ܤ<#wǣ!s>"'Am{{DKt6_UC3m*w+Sd$ 9.k6u6xnk]km\|Ak9vgAg4tsO1c\Wh؉lAa{PrX3?×>85"8G{7 0?_ TWg@IP-}$7 Ϊܴw~𩩷tJ%K-[sBwvzҭ:`D]8A0t|J9}bjCa8Z`V: ^zʳ}V^uU =g65ty2#d;*MP3rLliKmw9`tV]loxذCuYl+o[mY5'/p`:Fxi2ԻϸUn9ڮ6U'8E_k- 閱vS"v0B l)N Î[;}$C'ӆb GGb3hyX{@d31)}Xx=ImOҠ-)rqrˆ< :ǜRm^hRR \ŐyVqo2w !G\R WYƔ0eױ9/}ntQ m"wJs=Y޵#ޚw Ͱnk{_P rَ`_#~'uZxJE|6 a! Cعu{I s1S[U&=]G_F0`Πz7pJg>ӧ#ՌC35G ^?oo,D)(."?V]_* DE ` <3,n-(dɥc_;@YH= 112D @#YXDQF &-< Q  @FhD @ @ _t2)F(^q]"+ ɣԎ7eʼnO5 9lV  @ @ ֳ)*v";De>&q{:*O3[ac|&z),cξ(ʻ*oO"}- 2֒32-VPQ[f{?AC-򩸿l4*uʶGEXSTVK/f13?ȚyT*U푤*-wZ*j1CJζ#ũʅC,1l鄒dz*85N|C]}}ck?^K9ݪ3]=rğ#rE#--ËXwȇi< o^0jðZ SV.zSed9ӓt}341m f܂Qx5b' z_'*6 6zS@emR _pݜPNFcyqV0'!K^aH 4zix8%nѨL)ɾ6r2|1v6ITjkQdo1`JV}H䐡MZ󦸌 ΅W|rr3lv }L| 'ޜJ+,nU|E Y]fh|tثLk"mbϷnwg:;6f6oCmNT|.JD>bLO:YG=2vW x8;yQQ/)ŗ}7@@t bKwx-mXOƴιoKve=ldikiX`Ou¾e˭.k 7\ E93OO+Q_ \v[т5ƒ[@3e F"UtzႬ!IDǻX bYUw1u/W7eOVw-q7#dj7{ [{tTf5>4tq-Vѓ]r"~Rʮz(/18m2h]:bmIr։BYA+WӊUU5h5v.\x| ?J"mw$1Dص^wZDǙ99sT+}9%Bh_ b露_q>TD?yn#:nþۖ rMg6V潪6rZr뀏YjZ}sŬؐkB9#\WDެb.FNYNT@p%nJZ[ {S5AS;"ZUI.lJ9Ow"Ɋ +(ʓ,]J_sϔV PTL)gNsOX·w7 fޥXgD D DRם BV#n-=u1 *qRuW巾k֟[9|p;Yf?}$\?N-U /N.<ܥZޥ<|/%xwW뚓˵Ғ߮R"QC pV僰qxJ߯>:|oo. IDATɺdV" $G=l 88 bYo?x\P\ۻ8[To`_,cW$0aLcA|$6tTBav"\ 46_V˄/6Aqdh)m Z2b}xj) 7 =cڧC95;f'8~qMR6!-|;f{sm*Nr::dycaNcp=t-9~uaˁ*@b %u+9Z *^װ93\r3[5LfBb"BOΒ%FS:6wo)MMK CC|9nb60D5 ԵD&P'Ph-m?R|(k HmAe!;u#m=q1!tё;UlW^qew~m#%>6C\]Uk@]Ց6.emk(l tŖ[5ڙ~GՅJJ X󷾢y'~~hW2bİ-ai rܨ+Gd%T-O^~.'v @ .oˣV;5+&iM_f7O7)sis]ua̔vVEvcuEy8 {krK9pWpжFlٮa,Nf읫d޶'ʈVzT= &#J!ҫz!s ޖֱAyw 퓛92sH[IH),)|c( 0~WqMpl4k.ߵR4;tݬbѮG0VL]KtQYJ[ qƄ0ٜ9 졶$=CS:JBu̢f)-pYm#hfpOfœ!u Wez P)&v?p|XɃkwn9%8~9u^3RR1㼨#+tjt{u>$.{1X#t'M` ޷t$sdl7&Нa1XEM/S+?IzRDĞ*%mM$u ǫ{V1_.3-Vk]. _1˽ӴwA 8BR]`cX1h(t5C߶J1,:F~q{| ;~oǧn  c1l"mNof*GdZJjg>_M2o傧OC<`20 XFk\s 9%%q5$\wڽ/ӓcNrOWAEo u߻]B*+VIw>#QO2R^ܹ-Kv0:;K41Ή2d< HM޲?m )O7ߑb$C&N<BTcfˏE=aLut&&W9L}s(4'ј';Y%$~Ԃ-hxvp.| I$m9Fƣk7ypo,½V6JLq>}ɩ,&xC$l7Ť~[87<^PnZn[&?=1oF#n7uǑjm0|^v=cqz,5+z(z&8Zs!C<%FАTE IHVEQbvR&@ ?8 l6 'pyVzrrrTTT kU@\:bP.r@ @ yH@ @ _Q \ $:Pp/7|Iz"CvD],axcD8 @ @ /|gaKtմu9(cП;Oh@7(:vmDeߐ\N*cFO[Fx(=۫qW t ʂKO3RӼVOR֛fe0-3WF =c0%-2+xVS*zS"d9ӓt348 @OJ9DEԷ28O(r b\,N6_p愎i3Ԝ6zSí w̞A2McV]F ռ+B.VKM`)m^4:vi3m1^[dPŽVH9GT wھ8.v45e/@P!/9dp%J mm ΅W|Ô}yJ5k>&jG(c)2݆,gz~34F{{{aXosOژEۚ|q[E(G Ԙ{)RDz^BQ=3%E )֞TS+Gu@16Ӎ'hOԚi,uhSrJ )m30?T*u\{RͿؕIkST|cEJ :TuDYNwLR)z'%NLֲkoHo,TJ՝O㧧@.~|>J50]jGFN=٬[Xg.Ɯ##kRTGnZ*j1CJζ#ũʅC,1l鄒oy=[pU9gy*wnI]8 堩,ygQh+~YTߡvсʪpCE}*,Gb$4~i47@}`_R=kzGdD+̌F$c e4P #F&n4aBBA`7abfx>sT+}9%Bh_ b露_q>TD?ynw":nþۖ rMg6V潪6rZr뀏YBbC 5:Fp_-z "w9e93;(*8|J~%-l=Ϛ)ϮJrdSjyҼ_dV!OVLP_AUd~`U|7{TzdJ=sfzY 0.: JX @ : .ᵴ)c?&V5O.~m֥zjm)c?A uϯn+ZXrkV{RcQ[e᷎%2z\ۃW.yD*e隷pINv5ٸe@3e F"UtzႬ!IDǻX bYUw1u/Д?Ye!ߌ^G=V,lQAИӹZEO?wɉ5y~|CiGM67k+$HNoTN O Z9VD$DΘ0)尵s³^Uh$W!M ]""?y^6+inD)^Ww &9tF>Y5S{gTUwE,NFy.7GX]xjS9|?K9<#^?t{_iuiY,zFVڧ`` FqH7Cʢ*A >#GA4gK?U}FYHLDB4iKǽr&ԴJPYbeL̹`VlbtTjqul I$º)'IEY]TsW N2: `zۯ!Mҗ׍A[+c\kT[{_=zŁRyĔE;~1NSC3K}&wFw蔫 16cҢTG:x-̶=~vm1ɵRWS6g\x*4VM/_f#Ta%%J!a Þo!cϵmc$mY9[ZKxI)yIy e2˔C"+dTU%]OyⰪR\o}L/{ZvŶm֊Ag㵄 OweBc6GߵԥRT݅QZcճMzTem+f?NћRP{Zޥ<|/%xwwr߲ǨQIaOAjJ߯.Z^Z~8_# Z%ibYo/{\eǹ[NvzMGdV̲^GliL'Kݎ+x*zf7w}Wy|WwLMu hgr{4V^>z,;ˏFz0/ߧh;Յ/Sd*/6Fwi˖Xx̲7Zw,0.FK7&owTWAEdj 3Ԙ6 IOn>ޱ{0o}\FަX.a8{cUJLZ֭k],+s;]! [w ܄mѥ,b ?FQ6(;G֍t$̪m՛3Ώ, Zd͹UB+RRdDN.ϜWd}\z/6eE|6?Lq.݆F;ZM Nh*MOz4C]j)yOkÎ*1 {\ @ DXmK ؍,um+8"?ן6UL9zmxJ 'm_Ienxo[_ISƴ/'WPSm74]oj9,1+dQq*Sݘ |dD)d2QzuS/;d9Y!:6?;]t@Ƨȶ9#mڳk&!~Rݺv(]6Ӭ|UHu۔GN~kqX[myWwCҖuGB1=L6gBo{F5 rOԿΪP+zy>YJK=N2:~ӿeoHg>m$^~ s9U<ۥM;y>ӹ[[Bjnj IU?dUeAwLR@ ~p8l-OSr ^mޗeo״}g?]8JRt#`ˆsrrTTTڲ5o!}D_? zPJP ZXc[tz^Zϗ@3w鲈^w:T"ĸ^x?t!|t1 R 4i' |x5r,mzE߂OYEWX^u[M~||ѹ֊v&Wu1N'Ltpqv+=4o/+'Vx\ƭ |Q晠|KPXtbe}1qjٕ.q5B1۴-( :ߐ<@qᐊ9~kTsyp٢ ;YN QymlwƢ/"B8}qU1έC~y|#~+GxCU`44O]̕qok^eY>}^xZdp߇WOoWm]*. ]~ מs޷55G؄_/HgpOq֟#?s%N|@A,@ (" H3ȚH]Ds"_,2@ @ ų~|0Fg%T76-K"/#qw̃0ף' CGѮst׺@ܠ̛s,W]B @|g\ڦ3aw@yB~ص%UUC ;r;i>mS|| 0fS4lHG ">] (/=UHMZa>I][o]:"0P/96l..7Ƹ;jśg?S~Ԯ_ Je؋nffoW]ʂ^YXFWCTY]B}QK×*;J$^~ƈqg=q3Eo D߃ֺjN57Z{fMj#yz;ݖϸGwDl>Epu[Stl| @ :FZZRbN~NKO>=>2+xi;W(77=3'-3']UU$ݥ 1gГb}?iSQ :Ƹ3(r b\,N6_p愎-.i3Ԝ6zSíw̞A2McV]y?W]4S]wzixؽLʹ Ohzm7ivB9e9Ijn4r.|HBJCH䐡Õ+)ȷ}j8^]SR6j)9awJ+_0RȄ"r ++F&S5O^\߼][{SML-viwCW]GɃ̲d޳#h~_J@EJKrQzeW~{0[kpP%'zV@\!"|X Ddrٺӌ-Rgf}M|^`_Ʀ\^wzx!X\@e'[=DOƈ;D2^e02rԶ~Q] `vl}u 8@ ?58 堩,yQvZWXt*&(%/P$5srD)ˑ9ɥ{#fViMo{ȥl{*-_(03O׷@ٛѴ: e f惃,"0}x )~NƗj>kz ە9z?iZb ޝo2#ʢ:")* |)i,.c ^@j@o]Te|)~V~ѕ: @hg؋.~YL%|P.:xTb{2bT?o:ܽpzu)(܏K0bUw5tkŮ1Mѭ#B7ڽ;vYkv' wܬ5[V㞏Ӣֲ[sWG+_)TvC>asZUvlоk]fR]׶a*_<Xg737p3O=M+jbrvME9GƦk=x R\DTVq;$PA"dŔwaԴJ@>4wlz9gM4澼8~+Hd.h uR mei{6FhG0]`@Jn3/ ɯ %l^wN !ܩb4zc,5onzfſ65w~U]\hiĥE)PIH^mڲɵP)IsD=4B-ޝl۵߮]伤V|︟Nv5ٸe~d ow@?(Uuy{CO]w)nC!U۽{Xڣ6Z#.ᵴ)c?-m'DC`W\=oohƣiGM67k+$HNoTN O Z9VD$DH3?)尵s³^Uh$W!M ]""?y^6+inD)^Ww &9tF>Y5S{g{oB3^{Yeq_^T͢Hllr843x Op}n |wycYk-Xc,5(H'/ozT HI󢎬d2%ӥ{Yׂ1ÙI겷)I>02LwR`Y+߄Y }AOr=G[\zhysiqI_":MEjRKGwz=uU=K>.A iI6Wb1v]fZp"=txK:Ơco.h`XH l +c1e~VB ?ީ0 l겎.ѕ!K¬zin/]1X~6{sJ}R^tڋݴ+]])|1Yvv/d`a^Obw ^:&xU2u_mv-ҿeq/oX`]yhoss95ۅv}+p[<ͅA+^]:FАTEAHVEQFn~W_ !tS:-X:(MEԦp ^jQ#%uq8p8l`9V`p>ϳݖק9n&4]ukN=s_>i,C%Ur{L]>nHd6TDwVʲbLܯRZ5`KԄ1 Y/ʑy' GEVao6{)k%Ac89^.m#z$2,iUz3)BVo6/| Wez 4$uW&Us v!DzPӻt:p#]|D龧Xwgk"xS@ kw _!iL]8Ǯ^/*he˖-[f>2fOɏr, 4=of֋GY|WP]A-ZYc$(D o}3! nZ8J n:"ɵ$]E2|;-Kf(df\D9ґ/|  ,ګ%EdIxpv9v{0S9;yXb-+/oR#}gNeG2Z@ />WЊI3Ț~ /E :g.~m;$:Pp Wl7$ ]!;G wŧ pIǮHo/؟0<1"2݁{+)ߴo+n@ @"3iLsPƠ?w!kJQtڒ*ʪ!4UƌV)>^eP>){W[ t ʂKO3RӼVOR֛fe+.=0#l1SK;lohF Yf%3ڝn"Ŧ{:ֺjN53;t{:@ +:FZZRbN~NKO>-?2+xi (77=3'-3']ULO]x&=)!6aQ^Ƞc>:}Ǝ"}q/rTm gN@h[KÓF`=mm}:FsERŽVH9GT ݱITm_ ;qF[2IHZiI~2Tqp%O=@m«@x>qaJf_<rS5؃N@i FJPQnf5PĄkȈ bd0M9@>6[0[w\*,r>oJ3Ǧ>ڳ֘jbj{z%S}H,枽1n=A{LcN뽄7pXF4:h2qcD 2<1ڱ-KG}\3m|c ``dzZL_^KFR>[؝A/NA4y)mP^2׾A|m7u/;֬TVV*fNV9e93'cI{#fViMo6w/r)5zJb3D+̌F$c e4P #F&n4aBBA`7lx=gj^~#0<\^[{Eu/DKa@R !֧#ꞈg9O_cND!`z״#QrAʼW?U~vOQ.ܺ8CK]Yn1˙_V~sŬؐkB9#\WDެb.pݦmnEK?qQ ʰj)|L< vWNo./_iꡕ;̱v39\o'@KYۻK(^TQ{t믯1TI̼K1w?/{ $쎳hb]}Myh#x~G*&A`e"Og {3$-oDSx敄:g.ri_w󔖞D@ gq-%L+N⶝6V`tڋI \|WYTe(I[bP͙璗;٪9ngr5D괎"dŔw-i0c|h/L0( &BvFB ۠ם{br}C#wx͛._fwFw蔫 jĥE)PIH^mڲɵPWSd4;5(jA>~;4edTS v!mYN׍D 둕 MZz伤e`nգz%Q'-;E_}[`rJI} {q$왽u_&%la~8:˺([:@c:Q7ַW ua1S9Yj^{]@ 3!575*şB*2_Ʊ29HE7D,]W#I)ݲH1vqrᰁ[a> )@ !K‡t4gg#= oFC9$GB*A @ 2R@ @ ų~|0Fg%T76-Cۍ/#qw̃>~G#O]7;W'uNA7Yօ@ =jMWM[g2OScזLTQVm /L2fNi+MسwU@yrFj Izl& 6w{ ϩ7Xhfwuq0UQ',|?!x v"0mP- L^u356[NV%5Ӵ/vHDp菂]DTYށoT"I/ 4F8쉛a/k ֕TsZ?kH9gm{tGKPO]zX;I6OЛ;' @ ,_Ϫc%%zT4Q7/s7̏aE9i99쪢75B '.}ghq<s`O{)o/d1]qBe1ŸX.mp [Q Ҟg9Qm$nӱ[5:=e21N"V~L]X[/5ui˧o&ѱ{7hOitќn%hsD 틣bg8NSh˙X I+ U"C*WR  \xu'.?LIq,їt^@cr{pt (|H2"*mToO`n,O!r{8U@`0"ͭXs§ X-nW|_ӮVp)"x)WǁQ'itϴM_skC*o-TȒRXʼW7=K ؽJ٠@ ~xbٸcU[YX σ|vl8-me< %٥ Wi` (Ω;j˻HHI҆&3d! -q%oK;z7_epm @R(>U? -qKyuת76e&ȍ4Pip1 %*T·zu~ctC{2^fP`xӧOܘag}7kVڒWi "#Io +]TyZogTe %ȕh>f0#Q!yV۵6="tf dF`~|I4^'`}_2)r6Ir*xzƓE HvaNi;pNDCz]xhZjJoeM;Dۤ)߹Hp)v}Ӯ`en_ F)`Mܣ;j&{j~YPR扏Y;^U\Pœf0 R@ ߥĂ]Y;-: Sn{/VׇZ\S^7ps W7M{erz\)Jq<քAm :*+}WmKYd E.?~ӊ,v`+-BǍTCVeU[YZv,U 1^{lWMf͑-+h SSGm,B^PzcGr g*ߊ]lyMӮOw8sKWlw k$\B-Mr N(m|G MU=U`)V[Pl3ˢ{3S7ޖ4{k?lBaӧϏ̙KR!yT۵ Ͻa"Lg^Y, '^~|vqcL? .%ż3]*)ʬ^֚)m{nov_E>⿎Sڎ(z :B%_VN!rKt3(-ΩM/GBh>SMH(A', |9)IY3*zrUapGn؊+veq$}¿ 2 & (Alۋ]m~Dxm''o(eյtc^dVg|%@ _cA_.bG ߮a}P)Z =Rq6fT92ifqEBL&g]]<24g|aW,e)IdT SwF_N~Q Uzɞ}&ʲ+Q;C [Xuk{duzfmд*jWE޼'lJב/= rC+r Z!cRΙ_Uvwj1Y ׸("2j֧xy)AHwQ PqScC~3^¼;@H}MLSf %8l s.WxUi6. 呏ۅ̴ow2`s+i!JfD y E' ;J{%me0zM\: *`z*1)!a6*D JWi9d@ z> 5dД2dЀ5-u$*0aiEњz#|>4(QMI\f>аyơ/JitbNZ 3 5XQ5u U|G<  -Try 6CA˧w5zW|oS"`}H]J4qv69'4ڳJںneeed$k(

]i6d禅n i(<ʹ )W֦Gdj1_JT@ ?WTVV7_jp-k/tZCj^Z|Xr:J-^ зZF6 AŢ.O{S=p20OKOc rCfXZ=^Q;Uj#7o,_*Ũ'QZVbRbmN4/O>dqyYh+762ZpAn#+<Ψ8gqrtNiW.7lko33<=}"@mwM ;5;O] ɬ$]U/k\lM&)JBx]f^gnzYAu[&u`KV<4BB[8xcW_io}` Ѓ!6%Q]]$ָOFx;fG *%>g_"#`|4G2Ɍ߻mWWiuq WxYjWִkӹYgZsv[1J&/PgnzsIO![]Լ8u,nX U p mMqaOAK}_< 0_z2Ac'8:vc;,^rںhe7gKs2k'EǹA}aJmc=%whZ]jsKNy-: Jb/׻_4mrq B8*[ _+蔞8<_\ 9W =d %M돰] 7RY {7[Unfiq!T]J*H Oڕf+&3Ȗc4))NY\6 }f~/IN Go#`+>"kxwӴ-N܇UG-[]cꀝL%($W+ dҖ,7X/zT_^jey96,?S;ZzcXm+y7gpLOWpR¯=0ܖ^$7p9pqYܪ1GmM:fؐ pn5UP \WN>VNSsoH=ٺ~r4nEVl W@ ?yn_u56xO"roK1Nʓ 5]OߵlO0?5Ϸ_U|m 1xÕ#K+]`l곑s sYWW^qڜ L vֽSNS5M9977u5kSDAl-|Pv֣??<}a 'v L-*Rxg꒜6z:V]rbMc\{ӫnۅK/O9ou xyv&}`OdYji ]|@Yଢ଼>mQ?dEp]Bt[S>lT9d⼣$f(3F f_[2Ϗ<.]I; ,>Yb&/hd_x5T'1eQ@lpR?+@"qHNIg׮޵/%"?Օo'm9jr5GM$.Cku5M)Yk6PfbֹQk}s LMp r݅ '.!/3ٸ[V2iׁwcܴĚj(h[og}oS3.`+ꥆm0 '63B'$q "t`cu\kE}n$W=o4Ah Q?'Z[iU<;_ с[8  n˯:y`{dzy<Ǐm gs&D%N)9DxSR=ֵQƻ "xYΜrr8yCprZ&\N}~Ǯ*UWB|xNC A4KAp*u{_r:tM ;ޮ-4?b;S(=-5S lm>feOzty\^Y9i/\~Ҝ3QJjnsg 2^FqiCۭ*6,؛6iA-EhzYAr+k<ۡ.wLyF &uVp+zk'}9'&M4hӧ te2,a*Ukا5'-nv(i- g| CSr^x^zV!;WJg(_-=5w*Xg1mSokqQmqI|·[j'8N7S+7K9C HՒ. yѮRsh!W=Kh<qLt$nCюxxtLG췽w3Mc!pj]_#b@ .AKg=wWRG`'#ѡf[wAfj?r,5k.-:<ؑRTm]םg RCeVt(*"CJ׏Tqq%~v V"<heɉ 20r"A۽ y^n@ Cw\sbku u 1ml1a$~Nx@ ~mZYTEXvvZ1mrd~$ĝMnYNv~-(-Ʋc.T#izv2BCv{8nX҈kfՃrH·n09u>|BVTo4 l}M|9(xe\]Q3>W 0ϫ_e2$2c*p;/'*=d> D6.ؿ3 \7IPgnF6 MvUT͋xt*nS(,PoMq8 װ2.,YiU@jw͜}*vpK"",[ j}W[XHk$Dyz> in+?|f*?Ùeza &&`U)K 6T9+sx` *˴owWBfZ7;^l9狿 %3`qϢkyCN&kxrvC3A^oi -o/+x )izJH ңw? @ |V DMABhk2)EeȠ kZxUMHU44aҊ52:3'G| @M*hJ82;|\5C4}QJcÜu:׊N$էo$EŊy c"մ+?*Qx@dhjES8)g ]>}CO ;h;pt-D>D}Kc0FeW;79<ԞU'֕gTw+,+#3'Y@Y@[UVڢ/w >i&1\oXRH %8l4()-{vqin'f6eJ#l.NYK":f4q1._nk筋Kx@bNRhs>@xXY!Ԧ Z NZ!@ @ ?52Ts>W|*7w-,rYĄA>;662QܒR%U` (Ω;j˻5(@HI҆&3d! -nŗ@e1?{|%1RP*B* SoX^ȝ*Ywzj3^fH-{/)Q9u6EO{[T[2c$4ě>}r4D} D$>SY=*Ֆ,7:.2⟝$6pܕMvFUP|]NpiWے24VH{4NT]OuxGլWkbo7Krtϭ?lڂnZ3[FśSQH )W֦G 5ab 7Wb_}npONuL W %wg9noFU++9. 39c:=28?ƪ\FA"{إ9Cݢ0֫'omr(mBk]S^-:c1QG5=,Ӵq,&ƊIs260 ߚ0@oỉ^A%տ}WXs9E,,x8{7 YLʊGox6?־b"ۗOR񼛦]m ޽ab,&ˊA-v]Yuخf? !G M]**Y̪o&'̲xͱ 7EJnMcd11Vm' ̔̔̔wO\uBRW1Gm%6$0:l&Gb|Q(~`4b׮ =g<7a Թ`YPǴ=/[%[Ul<Ц JN@B5DY/4 40puμ1CTn e ?f=Hj^n˔To1ѳOI| ݭ/V٭fm;n"KvkaDyNӟ~޹ )L`2+:(%@ 'GvqqYjՕ+W^CPVTK_w3KR(] Q`qNj] *,!UmbhQf~Ɏ= rETywwQVsl^=VGn.&iH 4M0zz9E-18{#y!b/!@E&@ /(Ee=^qw;Weg&|?%G[v /:we`=YwRi߼Wנ{C@ oMϢ*-n;nʆn,tӔ0{F,Jm*co`)w@hȎGC F1[54_,bşYh]- !Ch>bY', J,[̲˺vJz5.ߞ: ^٧ @ݡ[w` !@ #_ްLiUKLekd6/DzRRS6N*̹n?M{>/g9.E1-,& 9;LMaYɺ/Y2sDDz.=z1\|"wkUE {gO3cGL&g^C^1olZ;ZS欚ܐotĞ:Cw̕ן3pk65ljoyׁQ'(C1ZLmr亪 ("2j}zK1![O>Um /%H.}:ۓZKqxlpc yˊ~0]Jj(a$D@{_T ]BR_mWPѭgbHCڰaޔ&AqdmĆSy$ lg,MΘc|`$z =Kج. j"Ƿ !`_1u:3hDTƆ@D|ҥ(,bpCp&qW_1 #Wˤ p𜉅5{1c{-qJdSukNމ;79<ԞU'֕gTw+,+#3'Y@Y@[UVڢ/w >i&1\oXRH %0lҊJ]dG<8=e qvT̊K67[`#t˂F`w\`gp.G:ڹ[(8Hze4&Wu5e]zvt<#r %3Q~' *G/ :Veiѯ_rVȲ^z^jBM.$jx,iHw~m^Hi8*l}*"W,}01whngD@ Pv>xbڸcU+\X e$&D۞KKA,-" yPSvըU֗wkUP"b"1d-[fH}t-c; .-3Pq#yy}˪3櫤@\p`3*B* SoX͕W*Dm ̪x SGE@rSGa[靿U]=R.e1]̂okmJװATJ⳯5CRm|SftAUr +{=:a#kSvFUP|]W?O™ަ-c]zӞ 7аI9udRId;eRW N65="&Pi|#W eRS5rPl?zj@_72- >5H,` ɟ`rW_nomQ-8<(* J@|@?in@ @iAbduX\S{gɭVhXӧ._nq%<:9_og!F8=.g11VLO87քz M XL,ݨ't+L6%ݢ"_u32A/t~Z_ PueC`)]uӱv}WMf%LҞ):eas mYbbWV=z##հyTZrl_>'Knvն)0rzX.+Γ~wh&bVD]?=+Y%G M]**Y̪oڠ>f#gn]x-1,4I {cGSug>G~= όd r XL,iz6ݑ|>*?+w6{DJH&b:idƲ0j(^I78qHڷa2$^&9%XC.~ÜXL,IN_~hs ;́qYL,t|^!~]8sǎ՛NgVdtyh2[ر&yܶ+K{;n?SϲsC!~yPٗ_T o`ّ>a΄_G\8vYUضIӬgUUWڼ:!W} @ɠ"AD_Yѯq8cVfA-L@ :XKWg(zv zsEGxO:Pëר3nf|% ;x=o+Zxmb+F=dH;+T<}7#q͏.+b2X2utK{8.?w7.7tEE~k{5u?]N~b"5]&CMvoK"5N^|ҙD\^nsXry+Ż x+-1)iqzƱ'̬/rf 8yWx*w#~W&: I))aam/ !>&>@ ~^G<~C{PW|')%DgI{+4:"&-(IFGG8dnj۞RG*nן^DgTR]s,^x|&BßxlA$Qf5}]7?u`m3솓<;OŸ{-~tg6̼ß^/:~i @B1,%eu2K6<ZWn45m@}fy݆Wz?7?O8w/rPIj:'Tr__(M},r!L,f1 t(E\Ek`)'x(Gy{Kre廡'Əh]nfY mߵto9DZvS:)?Y> br-~ =lZ{: !CA+_gzDu>5ggQ]c ۭ:F(jM\hG5Gbq=wFW5dׂҢk,;A5bJۭi++4dǽ&hx[F\[444;Eڎ( p>t3(-ΩmgEX7%Ez~c-4$b;,OLekd6ŀe%$'x[)9ix3"4#YbZ6YLrvHfcg&'dM驻`衃nj_r ܭ-8f1Ξ6VgƂLT=D:bشTwC Y5Ia]*Hp&Sf[waS+d }jfdA6v:C IDAT(K1ZB)^Vv<-O:Ko&ǽ| 1(H@DRMڰaޔ&Aq>٣[ϾžF-#"`jW;BtĽ1D݁*;/ѡ[lX}o*LNZ| ON|xzϬ@ ?DMABhk2 ]T ΫM̮nZ"1V)љ9A>T JmjTWsNPBZ^(a:kE_'hSW7TbE<1TjʑG<  -TrtmmY`ÃfOi߿jzC{گ7<] O=?QD!X0l=uY78szsJYy}Bm]IzFu2pʲ22S~5J ETUiU@-jA @sofl]e֍/ALPæ/^;@e, E C!J匑maGGwNC>p5sŖAaWL{Uwf \CҮ\ 0YqVhq,|+{틯i+^wvGo إ,͌~1l4PN|cY$)YRXj'xIW)у@ ߯3Ts>W|*7w-,rYĄA>;662QܒRմa C^sTpu5JxW(QE$$EZiCgHE܂̀ U;z7_epm @R(>U? -qKyuת76e&ȍ4PiEPrSGa[d EU?1Qu/3(p\0FBCKӧenLװATJ35CRm|Ӵt|Go +]TyZogTe %ȕh>f0#7[IR!yV۵6="tf dF`~|I4^'`}_2)r6Ir*xzƓE oHҎ8?)mGΩ|>hH/ZO MKWMuw޴Tﭬi(uv42;7 .EҮ/Q5#"slk}b~W/El{t?pG%xE<||%h]D^V;zee?vL7'5`zC @,viNfP8*5zijc m¦݂1._nq%<:9_og!F8=.g11VLO87քz M XL,ݨMPhC0h6stX~XhSi})*@UaZ,&b~tխJOڵC_Y4Ѷ|'21K{?蔅S6g/f!_YTJ+jm,}:,ϻiUۦ&bb)ߺ2ec'81%QOJn(m|̹BS sJ*A1 6,i?vskaѦ4{k?l…>>7SaV(ؼmڅ0=Rk>NY, owLJ ?k>$VyR0z1XScfaȜfGLnTRNi;Ol b1+u#mH4-cobby> 6ԿVȔ@$Y5ay0KyMwJ͛S &vws_k[NWg+:E=瞥օBOYt4&z'UYmʎJr-r/ `?:M1@ ?K~@GV)%YPK !Pشa˭1^i2'94Om ,myYKH , pys o<0A{ %YmaL+3D ^UM򸺴[]t56uousvԤ9qjpzkb o.`7ǃY@ ܏ёdttCv8[a•éyվT^=HޭK` '#w޾a:3聡}޾;R ;YDENݸi!xiІf.n*ømp6xуys7Sza[@T!ǸK!>zcEckc Eͧ 77نy+/\7WڸAsGq}?ɹiar'\rlu*4 .z/AG6Lhx#{ahM8X;[I 풜xcnIì#Eut4bV2#k ԟN+S]oS#OTm*u`2jt,:Ws:T$.Z*[_\ 4@ :⧶ooH7p oŘsfMQ5=B|JfK5a҆\oJк*uPa-D(V iMz]ƍOJpS|HSN5qg-UhwPnofKDRQEi/7$Eۍ(J{()*궐%dl/S"ka2W{?x9yss=94Ns]3Z*sޱGȶ/..}r&ׇ\v{0< eS Lo`=Mfn07n_r׾5M> #n[OPS*'@H⤫6zִN1)X<;>O .9W'X`4GIE«aw8d Eϥܾg!RQ={ a5Զ VW,BgisniF^YS?}[zE.7=IuRZ,.1i-+mD~TWqw9@5 }zTXf5d~]kg2M]zؼlaTx֓.d{D|@A- IpcrGD^x";FpndxR *hXc #ob+c\쌊~ޏ֟ͮ( mMr @{neKca5'GsꍙpH4lSj^3!Y'V~^igv {Y'1iΉo7)n+Ii9nk{>ZkJ?䩸"Qg]z5k E}(YgpOnKm^I+ 7N^n(nCwj?ql#['Y.n%/r :,ĺJ]^9j6W,0#{ғ+ߏ$U"v}'x再XMulaE' GM0|eSo;Xn*bgpoN.4GQ x$C%iw[['('\rcQz6|sY師Y!/%;ׯ6'r%{W["@ /af^` @ :~ UCNbGjZWc.$:h,7D*>l: *MiJ(xi X/-ՀSTE*g.[vImUfeEDE}+&doX0Q4L᭟wi_cI޹s?$7BF{=WҮO2۟Q=c)YLC82z Om GΘw\ݱrݩM9>mn^fd T\eq*-ueuXմ>|͜Sk<0E{5Ul4"1Wicę'/@m1ռ˕7~ߘ!s$5#pՙA]^ޏt,dȵ\Ai%UI/7Wg*+6x)57,)-cJP(I-}GV !I}5s"`ݦY݅[=tP0 @:8]yLe`iXZFJg0 "O}8:uL7MAKeA ձa ]j(aPXfzoߤ ugPwц&nt>[7ZJS*Aڴ+ϺbQB~#Gs= B!YANjZBo|41YHʷ%7d$IL(l"հ &# @M{Ln? )Y.Jϟ(%Qs *zyLՓn0&L©]gŶ8:wũKj=%PܪO#-5K}H2$1oj|r5瀤,ep-r!6Y\?V@ȵɔgfލ.2Zo$Ʈ-/i2*Pv3c_"nF|:"H% rFl>Eh=#jCcvqָ9k_7Cs z*(++A'b}(k6F 2`0 ƃ࣫t^bV5j%&󜰣 QHX]ϲ݂ xaVuY9WdLj [M/kvI2Eqq15 RrD2mr@t=iθY~kMR)=: 8Fܘ-MGUJ~w:M 2DېWkQ[rW_'p_O8jiK? E|:"Px8Ljײ똄V^\zvPjT!]zS̠52FCN=ޣ=Bx[`ɂlcvc#V7Yc;C7گCٺZ|ʻh̯nl|}W)"2}ߠ'w#@ ^~ÌP]n.ƞ$v}zK%l9%Sʯk \N]0H851k;3\*/|-/?@b^ěʺwwOS2u5iŃ-JTU5PoU.7Cg0K[?}TpZt]_j5VkIx_WnS=XdM?T}ua[ϻ^cw2p3M [́{ ܥ| yW7s@˻ħ1Dڳ+/1ר+6OUS_T~)!tpיEܔMgG&n`~`WE[h[76\bn} ٻmr#t8n ;pkm 8_:t3w6v^1aͣu>fD˯, 슰nWl@oJ=ٻzL/ݖTDXxU.뷹5iHޞ' ~I=ڭ? J Ev^ݖ \nQ6~V+s-/zW^?KDžwY|v60a/4O=|7t ,!B 9~x IDAT|,w ="l`0.z7ާ0Ŕ%%T1SMtyPO9Co}o.xVuxJJ|\.[Y%U:ח?oK:٩Y)YYU%5=lK6?9Cw !OبÈe= M؇ 5jjN3_9E}QDt30ѡͳ^q1F0\E(=jQ q3ooA}0Y[WC"4(+ ]tLV?䩸"Qg0%u9oE5e@Wߐu:4xUVfQI m؍e 4a~ UAJԖT3{r擯o! +׭k4mҋs$=[yUI4 SXݽf2ٳzD~B{Yԍ*|@,Ok9snLqf4TmkD\TJД D1f \?k\= eR #s &M4v #;XP49Xo$/5.LZɼ|ȣۯ5OiY d{VTd P"0X]}w{KVg4'ꤴY.]b&ZVTۯ['_`;"cK9jpG~s )*IU5lΫ^4#ϟ-O}'7|vY**δY~#OcJkdoe{Luy6o+zSP m;]|brWnWxj7w[H9m-Ljl = Զzf5g昶4eݗ=:Vb޲_~-̆ˊRzA)3-f3xCxⱷ~^3,qf˧=tઌ;w˩o2>>6|ƶ(Ϙ+~Wٔ,]65F0.mr?#f~yrkKGNmiLh{A,|Gr 5_C4\Tzp}FcKBmφƺOt ;[!in/|O\ï^Dߕ7awY~zIL$EF*{7tp=qҦ#GsWܕtu/W̞awYt\5oԾų"oLԃP=|HyA\knqӐ+:b|}eZ:I_uzRKizI$ʲEi;=2[zGU~Frw=õ{I-s]Cfi{9j$/ÔOKsI+4 ;4sJPy҇JA qSw3*OVw`B4t 1 5 yNՍv @׳ Lj ٿ%&8F>H88hY+K)DWc}|xl&Fi #R/[5H4B^btb*/v+wB1:I=EucpwSr^=VM|7cDpco[E6[pϝh;t&jGi3В\_eQ# *8xkw+1"kW'#p&ᚇY${#|jCkq.)˨}6hpr՜vP\&;.tփY̦7'eH7rvi({Q@X%Q{:ܕ 4{vO7ܗ _ ?xyX: ..Zq>puSLn:l[:w;=hGӹ\io;p>^djB]Jw.8FyVY#Wb^ThLl6[hS+̬d]Mqwi_+QUbBvW4d{dFv'el -=OPߤ]zz|uuٕҗ kД Ѱj ?Qҹ!b6t'iy!-#.vlS 7[_W<. \nߵ\F|kg5{󽤎\|\>qg s&ק{cpM'6{_)ǂ]nm2"^e\OI:>.ꤧi M^٩`pjAIM3~YM| x{Z!w #i O\2c^{WH΍ OaYX% TKwl["bbz }ab+c\쌊~Hu~gdlvUDFahkҕ%Ls+[w K89So̜?CaG)͜φ,fXay "~Ϟy+eĤ;'`ڧ/,'5TtѡJZSRT OM<d3.(lUjS켹RWeuRWqPϽS| OuKimguy\ 6v_>QAFqR-=I@~Yu)]`C1|G<)jaԶ{˲ߧ0$۳o@m$4U%:Gjh1ZAhMH^Uu@Çfq&RC9iUiJcnBiGOc߽'?u>EU-!U.g.3{1q:BEr*W;`6$;ޒwoXAbbO?+'b*^8mL KBF{=WҮOn|TEiK'ԃqD A#zvCԆf~Ňv]{ʜܼY67/3'/3A,CK]YvN^V~EB÷<xj?E{uUlRC41ٺ§uVV~wÚk:aWŻNyyWEJ}ҪqZR+˜c<٬#NrC* wb2wpWwE2Ɯ!IϞSz{iSLW1vl"e ;g3 jryErf4 _WZj :+|7U|ȣۯ5OqmX򨿞PUj[3nq*/-v>++xo~S_'@3|2 NJk%f2eE*6浞vN~%yq!ngҚ@ zQOR2j2Q9&yLMWYPiI [.5|0|(a3=7a˅ڻ*@.5Ď[JS*aLPP]YRKjc>d5=C~t6m+y[ܫ|~]Gr>g= G:Ex0F?wܘދ|7Ҋ$7vSD`TTMÑ7L3io-fmZ?ٽ>r | U5dʧЗ0)2D??y̪¼FMdvtu!j2lhYcDㅉXe_ #R$oa4ѿ%m"y߫1O>><{6@K  #˴_G$W~z|@nz1Bح0j q'm:g\0nv1]ԯKO!zfVM|7cDpco[E6[pϝh;t&j)Иfg%˂FTvq2]R+WcD>v#;W 1SO"FXM5wF_IVʷdz*",؇X]RѠ[s"twP6=l)6R7Jdl-xR~٩M/ODf-L\]_ !wSWӓ_8F3dž:=PJvQRQ31ʼU߾xl"@ Ap^z6l```0<}k@PM#:jJ_GfwoJ9Dgp86v+[8~?@ KWY;iL@v@ @ 񏇊L@ @ Y? yD2!yGI+6;Rr1xR3Mz(m钩-=mX9VSt)rm6qF<:y+=K!vBz=oP4Gc9L5sHr0 (*JCE@ Ӷmy*nH 3LEI]s[Mf*5\ uhT~'PϽSkS~On%eZy OwvmՎ3[ԽQXJGϒBo?pz0T&awLY6{V,%CsnLqf4T9C \nڱV.>#Fr;̛ZϊvgåFF\w@ o8 *A3Gh )omHOLFClr2ҪҔƬҎƾP5RۧT+\2+\j3@9,̆ˊRV3M舑$<`<-Sxg@1'5f1k{;wCB}s+ds%$ 3J*e40#.ӭ IDAT 0̏1(ckS*sr}dܼ̦dpUZʲ뀧K!lDZdq4.K1b41ٺ§u\ɵצx`ϮwaGreS_$N)Ii N\pp5_ټxbe+_Uk{oWo̠^f97B"]u"@ ~2(VTY43Hstg@ lJ3|/N45)'ٞ5U}O? 7r/)ɂ`|@D ҁ@Ke;$ٶH@W o˧A彰$"ls'BДrh>eh@p,ѕ:;9ܹB]QJZet(7CXw(Qtd)TTaZѥ5,`&M]}F _Xp mhKՑ&TˉϺbQB~#Gr ԵBPHe@sVno9 h}:H!'IBfBam06Ij%cX[yrrªZB ŸݬIVwsDqnrCym"]q҆Zm 2TULv]/jw`=d%uy=wSgtLϭԽD+;+n|B\ARC*ss@Ror7!N2D 0VCm`u"tf=/Yظz9gƅxIkAVPTTTTTT;6hND)\O0;05g//me"N(Zg7}3j[awDLZ/:981j5JOWe W3/:o~\ 9Ĝ>QY՜?z2TURl9J!#%Td2|=HwjKՑV9]7Wt)c}%O6df;XL0 \wqcqRC*EAKEmy9$ H`!@TЕVŭЄ#>D?ՑpdaC{P(n[cc)2$G@{w|$aߡ31!FPM#:jJ_c3߽+m !aa>_1S}z_[ͨl>Y݅[v,Cˤ@x}y*k' s 8M@PxFd@ ? (@ g@ 'LNı(}OJך9yĿSZkϵ,!o#g@ ?/g))qQ޻leugW||Z_& Ok-itQ]UPR)`~ssB`Q'89r1xRXVz(m钩-=N CO֝9jEcSwΝa7k #B8~}ڰX_s4S xth,iW\{ I7s^&`!ǻnuu͜/<$/L PQ:E*+((ȴmA,y SQRלsV_S.sJ:WGw*_a3Ũ"smFF2t7k 3eh'1/Io^. 3 fY=⋪^dn6\sX-Xٞrrʳ|3-א=8fَ!yʸ!%%%%١Luzsf-S5Eyt񉜎HSma6L0#i c +lqS/2rI˼ӛtN;@ ?/jpJS+@WU9Gj [w<_WUI4|hכm"RN&UZUҘP13Fw_j0ۧT+\2+\jF@9,̆ˊRV3M舑$<`<-Sxg@1'}ؘy+lcw i+^ϕ'6@|ԃDϘ+lJ%3gL)x'h~jP0?roݱrݩM9>mn^fd T\eq*-ueuXm6sO 28n%BWAЈP\ݦg<6Ow6q3x&`:3k܋}ný "(*E2?Uy^H))`N,lSH&~EIj9W'?k/+Ӑ[h0nSHv3T.g>,\tDSHwwtD`5w1ޟtS{_;FJiU=&f4ыVgh ;jKPy=kYU=gYr{W[|Rbړ-]TUUu߾}ÇOKK۵kׄ zp)1fK]y3{H!kM?ޝ?fʻ86ŗlj\'<ԁӻ>M׋U %<9[Q_KirJ%Y|a+,Ga8r}4uV6P ,h rRS]|-<@RT- !'IBfBam06Ij%cX[yrrªZBι1 ŸݬIVwL[DqnrCym"]q҆Zm 27jKͿAFs I۳߮\99 7Kk1x$٧?DO<3ntz#1vmyaMTMI*~7~dQlV DC :{wYʙn/lX,;v3>c_"nF|:"H% rFl>Eh=#jCcvqָ?avܪdN~E죬B8]UTT'L㓞~ҥYmj$/O7br7 @8M5M$+!ZZ:e4*)))))Iq)Yav<,(D,ggV\9(yHMkf\:f<{R,YRC1Q%ʉ۠J9;*-mP+]CVs {@ Y̪¼FMdvtu!j2lYֱ[0c/L.+gꗘL p e. ^h9^5xR67޳ZJnhYM'pHl:"&6zNjuHح0j q'm:g\0nv1]ԯKO!Z{cne'8Fd?78]h#jɻ5 ܉HgRPy+ƂavZ+ܞ,8j$Ae/u-N{8Fd>~c!/3j;$b$\0~7kdu|̹"rO}h-%z79uCWsݢGCu_]kOmJyq|<,r qe|#Wܠ|*qn->=6E©1g‚wM |J >/Y+(0@ [gM; -֟<'M@~P:D9 @cyB;@ ~;m,JbMQ-Xygc]]2GKZ*d.ɫ7m_8a,W!envGqU3hT_Qyn;)C@ d}n_cxC/n`ꙚzG- YSTݮ"ؿN:-LjR1 |ӑK 5DcaL۶-4@,.C/w47]nHQ^+pꏥ<{=}9 +>c -^&)j0J v}'5(E`/N:0o@*e1u旭b2ƫ2V~*']EPz/H5!R~{/bO/ోO;W!ہLb`˄q͇ *1kyJ"CY*.'n$igh1yAٰ_B |f0MOOU#,O5Eke塙-w{/3ҳʢwո!oPTXϣ1_o{$E0NAS)л7\M62?avyCMX3&X?| gHS*dl9x8bӅj$B#oekln},gg!kE PVlP?=tڀYw}| $.>B #4GiҼs?Z YʋHT7&Iw i-q'zNLh(uNNZE T(Ė'0/A^oE*)¾l1_Fp 1lZCxX_"+aq޶F8i``Fw۷ݰl٭W?5O_%k(/PyedNK!:ψ>M0;z%$6͌ ,@MƇx"мvߺmVcOvWp;3l{f.]$Ieu& ʋ E^'U+_Pzq(8D HXvGXg!jbKҋz"=j"hWc l@|"R}Mפbmc4cmH:qaCΖ.uKՉme/70nCKTڢYd!.\ÞȪFe5ŏ{+JW6}dDq0:_l|3"ήР6&}:yQUEvDg_m)rSab맘M'wV<HфЈ\[ ITTtu#/Z IDAT$)E/j;4yɛz3jsCު%嵡h*;9¾/~@ 0_j[Z8<,F$3IoOf(q/!fT9+ok% VU -Ju@&Иu2EzF$xm(.F%i@JS-v(⠦;Njgm<$,@C9_->pKGj˾#DzTZޒjbrSj ˛`R{s\s{_=z%HizؘP$d>SϬ:&S[-X=.b=ŝ `)5]2 WLJsNu֓! vq4 ; lr)άA.ѿzx9*ƪD7^KxKcz2oyJLg$pJ{SGT1 ]pevNPWtj@|՞.H|:"%Lt)8~$+;/E麱⽸A6wȢU >W=6\adu즬뷋i& %lAR@c?QVBa#T5WX`Fuk4uV["'{-MАi{c~fs䷄PCuʎDECnsUV(ؼvoϹȚฝ*E0) ~.7K?l:AOu56#T>+d)QCOy2pڦW7&p"C!`+)(y+|(R| aoC/ޛH?^>+fřn E|:"+Xg} EwV5aj41}R|S.3iܗH4O45[ zKiAfUMaܕݓ:􏇓v={py¡ /$n6Vuiۧ1}]݈@R O9+fW4is'v}Z @_Yww[>bX{'M{5I>·rfMrF9f@eH #Yh;$;?.( 0lE9/R{bG:4Wmv`BoiZޏjTjط8\>]i8I 8mvQʽ.<:Oo˙IpS lY kupyGm<(YDJ~4>rw8+4Ttfi饁ަa-8yU]D` P?lvMiPY*Wy{?&?jxJoD;=\s{sU(@ ogɷ>/Y+(0@ [Y@ o,24@ @ ;m_DSӷU.wt1/:\_hojazz!!dWKy~ kj>{X(upǵ-,4T$cfkj<4"Ӡ)ͫKF iz`ww1&Nw\cҏe@ @/Gl4==~|WU#߷<5'ԾC3[8mbE/_fdgE徭,KV? y4m$7 3=:Eyށ{zˢɦVO7l.O|k=]o! Z'Fel9x8bӅf $B#oekl|i>3vҳѢ93Aak+m6^ʊoBD۞|nm`ج;z>l@P!KT4GiSi^A\J,EUFjқqr?:Y#+M;\m;Q/ԫvrdBC j4@D˻l kQ^{Uu3ֻ x͍“Ow6%2nM}j̑jC̕ȧ6/;i+tTDPPGW.bgjh:x9]Bx@ /_וd] [HnNoj[':D]yJ1*ӕIbfV22!_/ ?af3eMQdkNY`3ZU򀜱hJk]ʝ8I^G88 D, ۛhƕ+"cbO7rf{*;5$t[iDY}qxo=)a[38j~jM^*ȭJ *%P_.}"2ΗBt}`9`wM!vJH2 %8l$Nn{uDlpNJy\:iت+8Mq4E;EH)4cТ+-埅8KCȕ[jI=u|y.V8cn:@#@~4jznv֔x^>4Uϖjh;oUo~SŲΙ<:9}ΰOJ ֭Pi!/t[a2@>Q5[RVv6=3A2sۓY-fdeKY3UZE@f~@TѺRMuh̎:"fyt=}FY\JRF6ysW#7ܒ dg:i}y8鎓c po>$2D( АwWOc,\Rő8E#+#ņX*ܔ76f*DE\k-W^Aɾ9&Rt}ZޚP$d>SϬ:&S[zi]Wz,ً;=s^5?SjdN]٘}#_F;d'rP(M5tqÞ}yV}ҍOMdX&g-:@ ȏj|SыHMW^9Tk_N;R"X騉4MU_"R02nt`D7 CoV>yotŏ8w'j3fZG%$c(eZ".퉌"7vOUĞn;{ꄤ:|d %N1X7E0pܔbNeQ')B0^޻n_=5myΗR60,Ͻjؖu^]bYYL2^|~Ʀ E{/-iO4f[zeбTCꔳf}f"LVʷ<]|-EO2[қ!g 8o3Tg2yFR `«~Dmx؍[ |U,uUdxϧ. Cl8 C }kSU #WoϹȚฝ*E0) ~pB O oXqq=UԒl~^Լ ?"^鰥=+RFX8Sw`X,֑`MT.+̬Wޜw={"SysU @ Cjl@[܁ uA M@ >|8"+ Rg:u}Fg W 9>'9PG-q1ɅCDa{=nn]]97wYzI}C@ ϒo}|_WPD9 /.v@ DOe]髼Yr5MMW/ٸzȧWOo%Vجwo՘Ƹߜ,jJߺ/ʢdQMO8ĝ#ή \^<`rƾ5Io'T+#K>@ gA ƗV孺e@ 仠,24G?!攊"6/JY*| V$cWlUF1pQ 1T䛭@)o_dl {k3333k#,ǥ;0@ {=EVu<70Lm=#JQFÖ)nW_ Ztc` rk_X]}yH"ѱkg$ #$ˇ.EдMM-lW[>R:3D:c)ϯa^mgOvC@\Er1?`8ZUͥ1Uƥ"k4[/d٧1Mi^_0I v]׎1p+; bk/C kǭ9mź}Fr?@ 蜍'ߏjvfߓ̖;NfX˗yyyxeѻj!!~B2zy$'Ň8}/EIM(w :lCQw޽h [j:ǚ1AOr[p=C։QgN5363AtY H9z[/49F``wwL3=cml{PVlP?=tڀYw}| $.>B #4GiҼl{?Z YʋHT7&uFVvߩw+^WȄ6deR;hwmٴ֚eZwLǽnǶ;^scSMDRa# ?nLynOH"4T\O4:YCT!@ @ N~c^Dj>ο ΡZrRZOGMh*}q;%ht$wwaz E0}.~ྻ8Q97:*!C,Ԃqiܶ\EK\<]kW{u4u*B ;`uJPqSvvz> :Ew$0 P{y}w\β!;_"zK4G0r8D=uИoAR ES 50Y+ ^7wja?lIoM3[k7{&%,sWr3G~<}#E0I`J6?FNAn~An~An-Z>[*~}}S~†!6N{> 5v)ת+\dMwpN"_?}b8 !@ wH #Yh;$?.( |>{4ĕg~1y/5gX%] f o)؋L;[/3;xY`]eF43\KbhtAT#IװHyFf쾜= @:.>}U7doݝ_|;9e],K3 lX/d!@~ZFӓLJnqU5}SwYyhfmw3ˌ켼r]5.,`21h$9)>,h{)Jo*D f YVyށ{zˢɦVO7l.O|k=]o! ZKFy-4UO~٤ݤFXvw4SKC0Fx*+|Mo{隟a,!I\^SC},PeGhT9Ly~q)AV>JoqL8́4p5S%D5W@Sɑ E6IC\=wS %((XE>E7yٙMw_1 IDAT na?@koW7''Y'TKA8Ԑ)5uQRCKw8~2ۖ'eQ߿4'͈B"JAGDPJ(<sȦkRQ̱NzG$!^gKz]:XͶ܌ORh7ޡ%*Dn!NEYjz$Kv@|)~^Q8 r@ ?)_j[Z8<,mzfd椷'[ʸcgp+*HKJu@&Иu2EzF$xm(.F%i@JS-v(⠦;NjgBD$2D( АwWOc,\Rő8E#+$ĆX*ܔ76f*DE\k-W^Aɾ9&Rt}Z>675 Y3ktTaK#V| VԔ.fcؠSY vx/1+?= x,\b }tn(Xö ZK=;~u͸/pKmmh)4x5NNf̋kطU*X*" }}Y}d"LV7[/x*^{^h4}PLo \:wea""`bivOxKP!y{ߞskT"n7P '^?l:vqO KY!c <'fz<{5S&J [~(y+GFng >u_E!DV 35Kg"RgR&E¹?Lnvu=żh9{a)Y LcqVܟ{{rWvG3vv8Ӥw=ް,|@  56du-~@WRW|=ækiL^"..ġSMC}զk&DK}+|>pa-̆1y(kT_W8'!ƥN"Y@u`> @gA H|@ @}inN]l@՜\enfQM -n݌rwnʓd,F; x/%c}4|:o?5]"Woھp(Y*C想8 ҏN㪎gY=驾ܞwD}љSx^ n56qƧw FpT^ee#ǙNbzd@ n~ pj,Y gjnQ"7?HQu`:MТ#K[K2MLGj/-]= 0m۶H0A|b J_t MK߿Dv#NE"dWKy~ kj>{6s@V}C/1N[LSԜa>ON7kPTL^t`9xzTb"//[4emWe.T%1N4^$kC<#m 3v_NŞZ }[Q_cWy>Vw̝ZuCmY)Sz[ѥRq9q$9O;C BφN[$IeknغNDTy@ o/7dC]tپ5z<4xӶ|/VeFv^zv^^9^Yb? y4ƶoHo*D fa{t0 ?EM-ϟn8 CMX3&X?| gHS*dl9x8bӅj$B#oekl<},gg!kExPVlP?=tڀYw}| $.>B #4GiҼs?Z YʋHT7&Iw i-q'zNLh(uNNZE T(Ė'0/A^oE*)¾l1_Fp 1lZCxX_"+aq޶~U[ATJ,@] D8e/<#>s6_︛BJ8dJ`43&p5fJ,Z>g@U0~Y13w<ך/ر^Mϰev$銢_ `* sK4*/*7d W]t{T;~"Cǡ%+#aq^B`iH/CqKuD &/J/z*#\]͟ӎ&HG6]L\bu;"ą :[ .W'n=f|ߔ@-Q!Bt;qJ/ʪ$P#YCM^ᕍ`0w3sP; f{ݚvȖ;*tsQiJӉ (Så>j 8_#P% /JP\L\'r6,u>z/,Á8^7[vn{kD)K'MoE?naOh:i Ry_SSνٜ.'ax-N[P(jHi@ ?";tƼuUڗdMA2s̜d{YbzLVnEQ%Ui(SNY]hyD E3H/ nIZzrRi}y8鎓c po>$2D( АwWOc,\Rő8E#+)bCM,AnʛzB ay3l"RjoknoGd)>Pog5BRdu|밥@ZE~Uǒs38U3KJ@8@irz2d!W.N~r>P4!&yæ3kKt^Jj8ҘL9jyD= ҅877ywL!(CrjS6z-T=_!)߳Hp)v}0S+E~7] 6/* Khnx/nhE%^Յ.n2gyV-8V4䝿3Z\A Ԗa:`':~ နRcHR.'#ko7kݮ@dP$o2Ie1kp JVAQ$KQIS-;kt~ЈyO)i-bTY|8s4OBQSMچGq:~ouz]Y#74 }Ctx)EtD/i`SFG:0M"Kyw~p7PC7:Gs312N-hxVѶ/En%.˵=:Lw~ Iu:l!Kc:Qo|%`();;=kʢO;S`(wݾl{k.hgY/E e=l`Y{#԰m)L_ѳѳ dM% B3#`^[ ؘoAR ES 50Y+2)X[l De7ACΦq5Λ=B չ+;!}UJX` Wڅ="kv0P4Sû/jx?!O| P1n.F M> irz S&P8]M9ExD[FԨ {DjŅd E0Yq̜5-Δp{h("{ .Ex]:c(޾S1蕺{jt9I,E16^xrx4Jfw_Hn.8du재q<كLxWK$)+U8yJTǙNMaͬ=si:߭Ϻ@dnCD楆]7?4lČ[M=̹~(żS,M)ǀJTkOdqj ϣ97pTPJUW&FVå# >zWYwIJ-^1cڝ{yM֓VzUd=+ʸ_nrY Wx?}m_^nW5VzK?Jܲ0q#b^G志M o%d|@ o!o =ZVc?!,24@ @ ;oVszxǷrmEI4_,)w3y]+Ol,xVO }e룥Y-}6+@ 9Ȫg1D)JsJRh Rd(/"2RS}ތ$16FwĝhU;92 YTN5~ "a|][6m{(*:G{mwƂ ɧ܇v2%2nM}j̑jC̕ȧ6/;i+tTDPPGW.bgjh:x9]Bx@ /7וd] [HnNoj[ߋ:D]yJ1*ӕI֭WĴbdddC(E_~Jg0 ֜f,+9c5єٻ;q,6Jpp6Y7f=+WEĞon/-d傥T 9vjH護4T45gm;I^#[OnX6 N}Eځ@ӧׯ r뿒5J R+xP=rmc,fxR`ݯ5_>NŘw:+@ @ Uc޺*aimo3s$3'=ޢhFVƽ;S嬼([QT dF+L*֕o@cv1ˣ-3R2țx)OᖤW-'+N'ؗwڡh81: p@"H y|4…;.ZA/QP4Rky;Rl"MySOa3,oM^DJ͵rc"Eק9 u@BV3|lu IDAT]C)a:U@ZE~Uǒs38U3KԕѧOο ΡZrRZOGMh*}q;%ht$wwaz E0}3{33fdBKol/oV$TJvْ*I-Mb-diD H3sγsys-.' /h2'mlHtԠ#>m.Mz-"IUfE7(8g)e|1ÙOiMFJ)~am#g_iY*I}i0XiIԥz3 ÙXEكFof^[o($&y476ymv~n v ە oΛ 0$FL%Ù#gș'FFNfvNfvNfڭGs4m#Y*q$n†FosWOq&3dtk\RCuɕDAb)lיĊ}5r8ߠB@ @ J]m I AAHNSMV Q.7E&B  .% .q.J ! @= B,}J:츃XS\/ܐ|#2ۯMQ^mR|r(=-wBH %W2Uf:}wb >2^ߣ;b؍xkDI+D @ \';7>ce`iXZFJK2 &3E &H+O _yC -1(ͯS񂣏oozBj% MxB⛆2\hr"qSYyf)k l<>f\*Bf:jj/Xkcˎ}د瀜rӑ*FAf|~ Į55?_usXF}k^=~Ņ"٣4tD?j9nmuK}&7FGhUx Qˈ~v[ڋ<0}5M6WvP<]Y~.8IẖMRma۞zyOue9zIg X{ydϭ;lڃ3wZ1]G=(+q g0:Qa d"@ }ak&$'VhNuhy=x|8@}y[d᩷ :3dԙb1f9Ù/X]|O4^3dNؐ8AkG&5|H]djZ3}dc6>6蠩NW&3w' Y)8#6Mf˱/0/_Qak@2ds'&zCrH%u`6ґ/qy4,xs~s|ߴgRWX]L"A#I7Fgi-7BDYPa^XLқP<6ROFsNg7Eʆ7M|f|#]3yp##'3;'3;'3Σ9͑,uUk}aC8ÙO2} _:~r.J tnML gbžIILoP!@ @  [ށ $&+ef{S"!@\D8tf|Oa}@ ~zhBG s 5%%@ @ ~x@ @ .&0(s*L%K/h֣UnHlԙ+];յH:ɩjMq 31y0RUOt@ @]b=fXXzˏU/iMBy/_gge +R 3`.=='?:g-Rq~pCBǙn,^4xyS7.OHpXZF;z mh-=uIFSfGbDݛaam'XijMl\ƻuw,ۻnᄩK-4R 1rEM'(ڻ\l;X֛!~j}@OmuUmۻE"buN\w\eT)G+Q{⎡93!b4Pdafd$cBqNa H#}Nږ'%T»gn&e$?1!E.|"Rl&&/>?7M5XYw96>1aB`"׋_)JFi6}S}|P1 v"& )UO!|ߒ!@ 7אCvbqC6ޠsX/{U J+ WL#U  ;+@+Rs֦R+ .2aF>q,AJRTʌYaXbY}BܙdagcjuCkgOe7+j까va6EN|n-Jdy>[&.)y'b/?enOeהe?ɬN +ŷ-._ᡁUvF7ZN:* rF6|o42SLfpN __Y] 3;bT/Ѻ^*z6,jW/Mufq}?3{D=Ġ4CN >IyF UHLDB44nVz -S@sKMgm5LlJ:*Bf:jj/Xkcˎ}د瀜rӳ*FAf|~ Į55?_usXF}k^=~Ņ"٣4tD?j-scnmuK}&7FGhUx <٩keD?;-_mEXFʃ&+;d(~6j.՛c-AmB/Ӌ rS_We)^ ы1G]scU!22E؛Ho+8&ㅋ/]]ő)ѝNi?zχc IESڒ&۹mY@uIn)uu426/EW[T ;ll]@]ɷ˧ET&6jMa[߭yĴ:L҉:пZ[i`5-@ j~ak&$'VhNuhy=w ƙˋ&$L OgЙY&s(<7˙L|=({☁&sƆDL O _<2$]z"VSGH'mW՚#cQAMuvwt>01;aJO9϶h2#]~L y[['L g>qm6C*u}yLděK$=p&g`aTVW{$`8?{HͬYkM39K*l++ISzj3Fowh즰hP]z?sD}+e:i۾_O/ tnML gbžIILme jvm'j:?&6&}pfӆOqn=|uRj L 8UC9"_2~]U&x mH?Xkp&3 0tQC E&|"␨{fᙷﮪ_o7&mRcNP#LD;9>X([?`\ 33t߽;u)d#k 9 gb(A%pf\y\i4WK!@Pjk@OjBrj_N)w9El.2M7~ߖ-|=c껪>=:d[͵+RݩWGpr èD4p@ ~zhBG|@ Y.q.B }hϢ"s @ @ ~"P>h|#{xRDu&Ւz"a#<ܶhOoeq*?l@ @t )U4]}yk \a;)\~s`V>A4ӝhkt ,ޯm1tÕs95[dxcO[dCc#7'C\8 Mm2& ϭogv>3@W_hLj[`o C ^oKC>2OZZPqs7> 2<Ѿn-6?Bo8->,&du *{1Hgg!; @&~o\j 3%%Q<? _d1?q}ejp)*%4 +ڙSf\dvMŬo<":a㩹쒛_MSvҜxp'9lث+R1ҿ*=x捋DƘgGwo)*iڤYA-AX^ׁb^}9 (z^׎.kNѴtA8yʮ .ZȘ; [Pf "~9Ц,˜]c" "$ӯ|_):ХSNEaA1Nkj tI']eѯ}. Pآ2 .>]A\gVpRw0T@ ?!B"b߄*OޝCאCs>K\y萁kZ8yUHD54Ҋե2SDž%kUACΊ/jNt R-q-W5GíM)'W,9]eŒ&K}Y惔xI ̕ FƲβNagcjuCkgOe7+j까va6EN|n-Jdy>[&.i%ߚ0cR :=]Sz*';Y߶|VKKm!_h9-ߪPw F? bBb(a@ag[kl_X>= B,}J:츃XsFCoQf)ʫ-RYNegтeN(Jv׿LNU{tB`g_ qo-(i:t/ÒR "++c8j*q1{@0#w,zNvo%tVAJZe$/`2S_ޏ۰zoڱ4h(u7zK9/8&PBb"B/wShN(9 4)ҿW;+xV޶YZp. B!m jB55tD,}JeǾWs@NLєQ5Z2kH%l$v9J7[+.y%#Qkpk@XJ35:BRSU^ .#Inyjk/2~'T4_!Cte]$16Kvn{=jԕ!%aV}ZoU&}n%aӊ2>6GOA!5IDATT\Ύ9)xlL։ [ @ ~ ޽7y7EKr˗Xbe]hw*g?p_]vZ:ż&VrGڜsao^}9 |э&5o3b\?2[kOYl{<݅u9 l+^]`ͥ†_/~^M9Z_qV/λ;M*IW}FW_V>&$Bmqf{U4[/,f~o @)KOsdɩJ>y=|oC,ͼ| ‹cFXa* )/x\iТI]dxE.zGxvUKcz6Vǘuv>K pkYEx6"Mf֣4P>뉇(8g)cib>P2&t^dQ=z&uQu'"m[:6|ӂtp*:ִJ.{{@Ȁ'mc+5:K{mDI%{+w҉6P<Ͷ>UAOg7ўxy׵gk&IS{v[72>pyt͹N+wM3i7lH`6ߺrATڢ'xP%5T\I.gnk뱚r4ܤL耿:]!@ٻC۷JVV~'<<<"""6n3}lwn憿4*^}Q_pqá=W N[qF 6o}'v.m+Q).=NڜȽzTlVE9E^R JWũ ;c E|N _?˴DE/J1t`8şm 8| YnA칰ĚQN솊R.:smhTat%^.[~l`zǢ wnĿaeHwOM.r|9c~ _vnG'f˨ Ћ(PWEѻ |C=!w i ^{SFl@|y\. ġ3L'Vf͚'> 76G)T;Eζ>@%CriQy 8KP)?nPȀ X™#U>&?]1f:Xfa=xBNvs+FJPQ6PѣgOn&-I|\KR)Բ9E|j߳#:lb$Z3ϭwܠ3c,vqC+MVbk:\e~9Pz [,l(6sueVSZ^ܱ 'k7^Z}d(jŭalѠ?w 񆏕Z#[@^v,$Jc>U/G?XCEy9%7לYuyh#T6;j x _| r4>@}\cV)Fת%!5s<5A tp7pYԗ\^Rwx; iQXs U]}&lcf/׵ Ԥxt8A׋QQv!KIRx+<Դ$: ԴWgK7dv 7U3hv-R"8ɩvnz塝TGFK·7h/Pm}Ǣt[)8ompgۡIo3 Sy%7.-_(4d$3@ ?!rrl6[RR;rrb^~:'W@eHViLhiqB8ޥGjh" {N_雽hc`5HYM 5+I2o7=xTP7@] PzJS/$.-$MC٫\i}V HTQQiEiSfI=gQ_ψgQ.;FⱫ8g)19@pK\򖽿_+syQ!XC-#y'3@,|h;d,'MN{Cst$(| ^굼z7ZFMu$TT- m =yc?'F𹃭-v |3i}i@- ě0D*ȑˠj Bګ3і=ƒHoa:0Tg.z^v +A<3en{3HqW ];8m {v AgH.7dVJ8H謦rkku殞4@{kzDҎyRzPm]ʶpy÷G||Q}wU_\Ht᳡2ie[|@ gOҪشiST$SM] ֓=^ :My}@-? 0d5)S.5yʵkj|:T_]rNz*_ nOדQ9TQ:k$UU(RlxzϮb# 5' jtebKMw996wrnGR>aF4?[@A] uE%ۄx׃ 6[/R]ox%:gՖq+t}\)GAnI 1}(&o pD\dT/o<O _ y f6PYES'#Д%镼[@}i=-SW~]ICrSJѶkk5ؿLeɋs+ j;6t!:rc<T~ZZыS^T (48SՅyu:]i 1j)Y!c"!,*lgu; uxB4E)j6K-tz"a̕yV;_:zI=^lo~ïdK@n#H9U]tzm{~uZYP8to?Ho@-sZ{v ySs=DpS^]kktg[b#>7,{0{9rd=ǡn;w<~utgɣUr!&i (RF;&]ڼY>4qN+GZN=Ø:K ~W NLwQ@ccnދڟ0Y:}1`^lѼcU[9 ψ۱(@@J]$RDV]#LyLe:jXr[Q 6m[lWYh},, pW1l^ rZ\PAKT`Ĩ+,ڽk~_>1[}Dylݍ2_es v}[zKM<teZIGWiIRh""5dRxkJBhI@K^TPmyWA0d?4'}f,߾~|{%%Gn z9 =5G׶ bhJ9.!-DUr)v~h$tVS妯K 'VhaN!F'9w\[m5.l|1o֩Jzviv$tac~{[RIx9Pѕ-W>n\_-uIyEGƧ 8ߠó?YT%LwAe:cm.prds΁>A4ӝhkt ,ޯm1tÕs95S^"G.ËoO*+1Bqeݯs۵nٛTM&/o=Uaf`^}˜#*Eg)X^foϗ5^4.jt-cu˂SbF=i"SU>8U||EO Wb4op}kXn_s`.{Ak{[ݯ,++H[;q#-WE҉HWn d&8Y֤'@yмIO{&+5ɯ\ݙ\Kqo?뗃[mOnٿBK}dͽtSe0q ӎX>H^[SPY_r5W =/?׼o ?sCڦY̔Q۬ Wog _d1Ӛ>Oo7^LMJI*%>Vf\zzNjE+ۋ')_`Yxͤ7_4?LɅ@DR]t^=d'ɶ==\_Ͻ$paP'X~W fQxZM_%?0T},󲉲3CȑV=&!P)_>"_}d|aϮYy'"(OoV@}ď]eOѮ/Pxb/@ Hr1A*7V,"W:d` :U"y\PZQV`d\  ;+y@+RrUcn}4ڔX<%_&h'.e>HITq@\ߜ#+xS8ˢO;̆C^w]{_zLXF}EM\=.6ߕ wm6YLB#8=cą=?%Dg4ڼl욲WU=>CW95d+<4_@]łӢ 5?q _it"&$6 *S!|Fo%W+gv׿LNU{ʼ^w /Ջ"!h)-{Y\ߏ =Oy[wvuJY~ð"6[ok!%+ⰓtSH,5 I漻Ĭk o=IMDPFW- ߕzNf@n1wOqXkcN^5d[JuXHQqgoFv@  FguS|' R2*yݣ0"5yk{]Y#K]~WC]zȩe[o_NPBb"B4 ͩO|O'kJ5%fmflYU=RΪQ]g$8D_uyYwB!b>@.gHxY*FAf|~ Į5Uf?pP+dYsP'Y.ϖ98vEHQE/!db -Fzٶ1Y'@jm jRzxbώu۸%3qFPc5\f:^M0zMp[;~8 ,E k!Ga8YPuCRmuPlj#vpOft-]kH6+H 4~ /=buH(yxr٧;#E֋Q']m^'mW .:XoSlupȊYkg{+ꃺ Uwh$mdF:^k=JCuﳞxmsR1&v/6#U/cB7EՓKi~amaia9Dߴ >N5MRWX$` h~1YoLDY'r'(u1w}. ӆ~XNtYj ~]65wď#xmg\X'm=('_gkW$aΨV3L DKqHV=Hz3.){P誟y 2ypdS8Rd,RQ: X`z{/]߉:ҿV[:Y+4V$/X[= \ ;F}'?Ĺ@^yjtƺe.>W 屬6*XzݵT>8| F=m P=8B<T_t79ԕGVr^' bߡMn>wLmVZ r$^˥ R^@ =P[PZ,{urs\d" !o"ǿ-)?Z<{94wU}{tɶkWSN` qe? ]mu놹s~R7`__, providing common abstractions to different hardware devices, and a suite of utilities for sending and receiving messages on a CAN bus. **python-can** runs any where Python runs; from high powered computers with commercial `CAN to USB` devices right down to low powered devices running linux such as a BeagleBone or RaspberryPi. More concretely, some example uses of the library: * Passively logging what occurs on a CAN bus. For example monitoring a commercial vehicle using its `OBD-II port `__. * Testing of hardware that interacts via CAN. Modules found in modern cars, motorcycles, boats, and even wheelchairs have had components tested from Python using this library. * Prototyping new hardware modules or software algorithms in-the-loop. Easily interact with an existing bus. * Creating virtual modules to prototype CAN bus communication. Brief example of the library in action: connecting to a CAN bus, creating and sending a message: .. literalinclude:: ../examples/send_one.py :language: python :linenos: Contents: .. toctree:: :maxdepth: 1 installation configuration api interfaces virtual-interfaces plugin-interface other-tools scripts development history Known Bugs ~~~~~~~~~~ See the project `bug tracker`_ on github. Patches and pull requests very welcome! .. admonition:: Documentation generated |today| .. _Python: http://www.python.org .. _Setuptools: http://pypi.python.org/pypi/setuptools .. _Pip: http://pip.openplans.org/ .. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall .. _IPython: http://ipython.scipy.org .. _bug tracker: https://github.com/hardbyte/python-can/issues python-can-4.5.0/doc/installation.rst000066400000000000000000000103621472200326600176040ustar00rootroot00000000000000Installation ============ Install the ``can`` package from PyPi with ``pip`` or similar:: $ pip install python-can .. warning:: As most likely you will want to interface with some hardware, you may also have to install platform dependencies. Be sure to check any other specifics for your hardware in :doc:`interfaces`. Many interfaces can install their dependencies at the same time as ``python-can``, for instance the interface ``serial`` includes the ``pyserial`` dependency which can be installed with the ``serial`` extra:: $ pip install python-can[serial] GNU/Linux dependencies ---------------------- Reasonably modern Linux Kernels (2.6.25 or newer) have an implementation of ``socketcan``. This version of python-can will directly use socketcan if called with Python 3.3 or greater, otherwise that interface is used via ctypes. Windows dependencies -------------------- Kvaser ~~~~~~ To install ``python-can`` using the Kvaser CANLib SDK as the backend: 1. Install `Kvaser's latest Windows CANLib drivers `__. 2. Test that Kvaser's own tools work to ensure the driver is properly installed and that the hardware is working. PCAN ~~~~ Download and install the latest driver for your interface: - `Windows `__ (also supported on *Cygwin*) - `Linux `__ (`also works without `__, see also :ref:`pcandoc linux installation`) - `macOS `__ Note that PCANBasic API timestamps count seconds from system startup. To convert these to epoch times, the uptime library is used. If it is not available, the times are returned as number of seconds from system startup. To install the uptime library, run ``pip install python-can[pcan]``. This library can take advantage of the `Python for Windows Extensions `__ library if installed. It will be used to get notified of new messages instead of the CPU intensive polling that will otherwise have be used. IXXAT ~~~~~ To install ``python-can`` using the IXXAT VCI V3 or V4 SDK as the backend: 1. Install `IXXAT's latest Windows VCI V3 SDK or VCI V4 SDK (Win10) drivers `__. 2. Test that IXXAT's own tools (i.e. MiniMon) work to ensure the driver is properly installed and that the hardware is working. NI-CAN ~~~~~~ Download and install the NI-CAN drivers from `National Instruments `__. Currently the driver only supports 32-bit Python on Windows. neoVI ~~~~~ See :doc:`interfaces/neovi`. Vector ~~~~~~ To install ``python-can`` using the XL Driver Library as the backend: 1. Install the `latest drivers `__ for your Vector hardware interface. 2. Install the `XL Driver Library `__ or copy the ``vxlapi.dll`` and/or ``vxlapi64.dll`` into your working directory. 3. Use Vector Hardware Configuration to assign a channel to your application. CANtact ~~~~~~~ CANtact is supported on Linux, Windows, and macOS. To install ``python-can`` using the CANtact driver backend: ``python3 -m pip install "python-can[cantact]"`` If ``python-can`` is already installed, the CANtact backend can be installed separately: ``pip install cantact`` Additional CANtact documentation is available at `cantact.io `__. CanViewer ~~~~~~~~~ ``python-can`` has support for showing a simple CAN viewer terminal application by running ``python -m can.viewer``. On Windows, this depends on the `windows-curses library `__ which can be installed with: ``python -m pip install "python-can[viewer]"`` Installing python-can in development mode ----------------------------------------- A "development" install of this package allows you to make changes locally or pull updates from the Git repository and use them without having to reinstall. Download or clone the source repository then: :: # install in editable mode cd python3 -m pip install -e . python-can-4.5.0/doc/interfaces.rst000066400000000000000000000020771472200326600172320ustar00rootroot00000000000000.. _can interface modules: Hardware Interfaces =================== **python-can** hides the low-level, device-specific interfaces to controller area network adapters in interface dependant modules. However as each hardware device is different, you should carefully go through your interface's documentation. .. note:: The *Interface Names* are listed in :doc:`configuration`. The following hardware interfaces are included in python-can: .. toctree:: :maxdepth: 1 interfaces/canalystii interfaces/cantact interfaces/etas interfaces/gs_usb interfaces/iscan interfaces/ixxat interfaces/kvaser interfaces/neousys interfaces/neovi interfaces/nican interfaces/nixnet interfaces/pcan interfaces/robotell interfaces/seeedstudio interfaces/serial interfaces/slcan interfaces/socketcan interfaces/socketcand interfaces/systec interfaces/usb2can interfaces/vector Additional interface types can be added via the :ref:`plugin interface`, or by installing a third party package that utilises the :ref:`plugin interface`. python-can-4.5.0/doc/interfaces/000077500000000000000000000000001472200326600164725ustar00rootroot00000000000000python-can-4.5.0/doc/interfaces/canalystii.rst000066400000000000000000000026111472200326600213640ustar00rootroot00000000000000CANalyst-II =========== CANalyst-II is a USB to CAN Analyzer device produced by Chuangxin Technology. Install: ``pip install "python-can[canalystii]"`` Supported platform ------------------ Windows, Linux and Mac. .. note:: The backend driver depends on `pyusb `_ so a ``pyusb`` backend driver library such as ``libusb`` must be installed. On Windows a tool such as `Zadig `_ can be used to set the Canalyst-II USB device driver to ``libusb-win32``. Limitations ----------- Multiple Channels ^^^^^^^^^^^^^^^^^ The USB protocol transfers messages grouped by channel. Messages received on channel 0 and channel 1 may be returned by software out of order between the two channels (although inside each channel, all messages are in order). The timestamp field of each message comes from the hardware and shows the exact time each message was received. To compare ordering of messages on channel 0 vs channel 1, sort the received messages by the timestamp field first. Backend Driver -------------- The backend driver module `canalystii ` must be installed to use this interface. This open source driver is unofficial and based on reverse engineering. Earlier versions of python-can required a binary library from the vendor for this functionality. Bus --- .. autoclass:: can.interfaces.canalystii.CANalystIIBus python-can-4.5.0/doc/interfaces/cantact.rst000066400000000000000000000002661472200326600206450ustar00rootroot00000000000000CANtact CAN Interface ===================== Interface for CANtact devices from Linklayer Labs .. autoclass:: can.interfaces.cantact.CantactBus :show-inheritance: :members: python-can-4.5.0/doc/interfaces/etas.rst000066400000000000000000000017641472200326600201700ustar00rootroot00000000000000ETAS ==== This interface adds support for CAN interfaces by `ETAS`_. The ETAS BOA_ (Basic Open API) is used. Installation ------------ Install the "ETAS ECU and Bus Interfaces – Distribution Package". .. warning:: Only Windows is supported by this interface. The Linux kernel v5.13 (and greater) natively supports ETAS ES581.4, ES582.1 and ES584.1 USB modules. To use these under Linux, please refer to the :ref:`SocketCAN` interface documentation. Configuration ------------- The simplest configuration file would be:: [default] interface = etas channel = ETAS://ETH/ES910:abcd/CAN:1 Channels are the URIs used by the underlying API. To find available URIs, use :meth:`~can.detect_available_configs`:: configs = can.interface.detect_available_configs(interfaces="etas") for c in configs: print(c) Bus --- .. autoclass:: can.interfaces.etas.EtasBus :members: .. _ETAS: https://www.etas.com/ .. _BOA: https://www.etas.com/de/downloadcenter/18102.php python-can-4.5.0/doc/interfaces/gs_usb.rst000077500000000000000000000052431472200326600205150ustar00rootroot00000000000000.. _gs_usb: Geschwister Schneider and candleLight ===================================== Windows/Linux/Mac CAN driver based on usbfs or WinUSB WCID for Geschwister Schneider USB/CAN devices and candleLight USB CAN interfaces. Install: ``pip install "python-can[gs_usb]"`` Usage: pass device ``index`` or ``channel`` (starting from 0) if using automatic device detection: :: import can import usb dev = usb.core.find(idVendor=0x1D50, idProduct=0x606F) bus = can.Bus(interface="gs_usb", channel=dev.product, index=0, bitrate=250000) bus = can.Bus(interface="gs_usb", channel=0, bitrate=250000) # same Alternatively, pass ``bus`` and ``address`` to open a specific device. The parameters can be got by ``pyusb`` as shown below: .. code-block:: python import usb import can dev = usb.core.find(idVendor=0x1D50, idProduct=0x606F) bus = can.Bus( interface="gs_usb", channel=dev.product, bus=dev.bus, address=dev.address, bitrate=250000 ) Supported devices ----------------- Geschwister Schneider USB/CAN devices and bytewerk.org candleLight USB CAN interfaces such as candleLight, canable, cantact, etc. Supported platform ------------------ Windows, Linux and Mac. .. note:: The backend driver depends on `pyusb `_ so a ``pyusb`` backend driver library such as ``libusb`` must be installed. On Windows a tool such as `Zadig `_ can be used to set the USB device driver to ``libusbK``. Supplementary Info ------------------ The firmware implementation for Geschwister Schneider USB/CAN devices and candleLight USB CAN can be found in `candle-usb/candleLight_fw `_. The Linux kernel driver can be found in `linux/drivers/net/can/usb/gs_usb.c `_. The ``gs_usb`` interface in ``python-can`` relies on upstream ``gs_usb`` package, which can be found in `https://pypi.org/project/gs-usb/ `_ or `https://github.com/jxltom/gs_usb `_. The ``gs_usb`` package uses ``pyusb`` as backend, which brings better cross-platform compatibility. Note: The bitrate ``10K``, ``20K``, ``50K``, ``83.333K``, ``100K``, ``125K``, ``250K``, ``500K``, ``800K`` and ``1M`` are supported in this interface, as implemented in the upstream ``gs_usb`` package's ``set_bitrate`` method. .. warning:: Message filtering is not supported in Geschwister Schneider USB/CAN devices and bytewerk.org candleLight USB CAN interfaces. Bus --- .. autoclass:: can.interfaces.gs_usb.GsUsbBus :members: python-can-4.5.0/doc/interfaces/iscan.rst000066400000000000000000000005031472200326600203170ustar00rootroot00000000000000isCAN ===== Interface for isCAN from `Thorsis Technologies GmbH`_, former ifak system GmbH. Bus --- .. autoclass:: can.interfaces.iscan.IscanBus .. autoexception:: can.interfaces.iscan.IscanError .. _Thorsis Technologies GmbH: https://www.thorsis.com/en/industrial-automation/usb-interfaces/can/iscan-usb-interface/ python-can-4.5.0/doc/interfaces/ixxat.rst000066400000000000000000000127441472200326600203710ustar00rootroot00000000000000.. _ixxatdoc: IXXAT Virtual Communication Interface ===================================== Interface to `IXXAT `__ Virtual Communication Interface V3 SDK. Works on Windows. The Linux ECI SDK is currently unsupported, however on Linux some devices are supported with :doc:`socketcan`. The :meth:`~can.BusABC.send_periodic` method is supported natively through the on-board cyclic transmit list. Modifying cyclic messages is not possible. You will need to stop it, and then start a new periodic message. Configuration ------------- The simplest configuration file would be:: [default] interface = ixxat channel = 0 Python-can will search for the first IXXAT device available and open the first channel. ``interface`` and ``channel`` parameters are interpreted by frontend ``can.interfaces.interface`` module, while the following parameters are optional and are interpreted by IXXAT implementation. * ``receive_own_messages`` (default False) Enable self-reception of sent messages. * ``unique_hardware_id`` (default first device) Unique hardware ID of the IXXAT device. * ``extended`` (default True) Allow usage of extended IDs. * ``fd`` (default False) Enable CAN-FD capabilities. * ``rx_fifo_size`` (default 16 for CAN, 1024 for CAN-FD) Number of RX mailboxes. * ``tx_fifo_size`` (default 16 for CAN, 128 for CAN-FD) Number of TX mailboxes. * ``bitrate`` (default 500000) Channel bitrate. * ``data_bitrate`` (defaults to 2Mbps) Channel data bitrate (only canfd, to use when message bitrate_switch is used). * ``sjw_abr`` (optional, only canfd) Bus timing value sample jump width (arbitration). * ``tseg1_abr`` (optional, only canfd) Bus timing value tseg1 (arbitration). * ``tseg2_abr`` (optional, only canfd) Bus timing value tseg2 (arbitration). * ``sjw_dbr`` (optional, only used if baudrate switch enabled) Bus timing value sample jump width (data). * ``tseg1_dbr`` (optional, only used if baudrate switch enabled) Bus timing value tseg1 (data). * ``tseg2_dbr`` (optional, only used if baudrate switch enabled) Bus timing value tseg2 (data). * ``ssp_dbr`` (optional, only used if baudrate switch enabled) Secondary sample point (data). Filtering --------- The CAN filters act as an allow list in IXXAT implementation, that is if you supply a non-empty filter list you must explicitly state EVERY frame you want to receive (including RTR field). The can_id/mask must be specified according to IXXAT behaviour, that is bit 0 of can_id/mask parameters represents the RTR field in CAN frame. See IXXAT VCI documentation, section "Message filters" for more info. List available devices ---------------------- In case you have connected multiple IXXAT devices, you have to select them by using their unique hardware id. The function :meth:`~can.detect_available_configs` can be used to generate a list of :class:`~can.BusABC` constructors (including the channel number and unique hardware ID number for the connected devices). .. testsetup:: ixxat from unittest.mock import Mock import can assert hasattr(can, "detect_available_configs") can.detect_available_configs = Mock( "interface", return_value=[{'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW441489'}, {'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW107422'}, {'interface': 'ixxat', 'channel': 1, 'unique_hardware_id': 'HW107422'}], ) .. doctest:: ixxat >>> import can >>> configs = can.detect_available_configs("ixxat") >>> for config in configs: ... print(config) {'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW441489'} {'interface': 'ixxat', 'channel': 0, 'unique_hardware_id': 'HW107422'} {'interface': 'ixxat', 'channel': 1, 'unique_hardware_id': 'HW107422'} You may also get a list of all connected IXXAT devices using the function ``get_ixxat_hwids()`` as demonstrated below: .. testsetup:: ixxat2 from unittest.mock import Mock import can.interfaces.ixxat assert hasattr(can.interfaces.ixxat, "get_ixxat_hwids") can.interfaces.ixxat.get_ixxat_hwids = Mock(side_effect=lambda: ['HW441489', 'HW107422']) .. doctest:: ixxat2 >>> from can.interfaces.ixxat import get_ixxat_hwids >>> for hwid in get_ixxat_hwids(): ... print("Found IXXAT with hardware id '%s'." % hwid) Found IXXAT with hardware id 'HW441489'. Found IXXAT with hardware id 'HW107422'. Bus --- .. autoclass:: can.interfaces.ixxat.IXXATBus :members: Implementation based on vcinpl.dll ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: can.interfaces.ixxat.canlib_vcinpl.IXXATBus :members: .. autoclass:: can.interfaces.ixxat.canlib_vcinpl.CyclicSendTask :members: Implementation based on vcinpl2.dll ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: can.interfaces.ixxat.canlib_vcinpl2.IXXATBus :members: .. autoclass:: can.interfaces.ixxat.canlib_vcinpl2.CyclicSendTask :members: Internals --------- The IXXAT :class:`~can.BusABC` object is a fairly straightforward interface to the IXXAT VCI library. It can open a specific device ID or use the first one found. The frame exchange *does not involve threads* in the background but is explicitly instantiated by the caller. - ``recv()`` is a blocking call with optional timeout. - ``send()`` is not blocking but may raise a VCIError if the TX FIFO is full RX and TX FIFO sizes are configurable with ``rx_fifo_size`` and ``tx_fifo_size`` options, defaulting to 16 for both. python-can-4.5.0/doc/interfaces/kvaser.rst000066400000000000000000000025631472200326600205250ustar00rootroot00000000000000.. _kvaserdoc: Kvaser's CANLIB =============== `Kvaser `__'s CANLib SDK for Windows (also available on Linux). Bus --- .. autoclass:: can.interfaces.kvaser.canlib.KvaserBus :members: :exclude-members: get_stats Internals --------- The Kvaser :class:`~can.Bus` object with a physical CAN Bus can be operated in two modes; ``single_handle`` mode with one shared bus handle used for both reading and writing to the CAN bus, or with two separate bus handles. Two separate handles are needed if receiving and sending messages in different threads (see `Kvaser documentation `_). .. warning:: Any objects inheriting from `Bus`_ should *not* directly use the interface handle(/s). Message filtering ~~~~~~~~~~~~~~~~~ The Kvaser driver and hardware only supports setting one filter per handle. If one filter is requested, this is will be handled by the Kvaser driver. If more than one filter is needed, these will be handled in Python code in the ``recv`` method. If a message does not match any of the filters, ``recv()`` will return None. Custom methods ~~~~~~~~~~~~~~ This section contains Kvaser driver specific methods. .. automethod:: can.interfaces.kvaser.canlib.KvaserBus.get_stats .. autoclass:: can.interfaces.kvaser.structures.BusStatistics :members: python-can-4.5.0/doc/interfaces/neousys.rst000066400000000000000000000010121472200326600207230ustar00rootroot00000000000000Neousys CAN Interface ===================== This kind of interface can be found for example on Neousys POC-551VTC One needs to have correct drivers and DLL (Share object for Linux) from `Neousys `_. Beware this is only tested on Linux kernel higher than v5.3. This should be drop in with Windows but you have to replace with correct named DLL .. autoclass:: can.interfaces.neousys.NeousysBus :show-inheritance: :members: python-can-4.5.0/doc/interfaces/neovi.rst000066400000000000000000000021411472200326600203420ustar00rootroot00000000000000Intrepid Control Systems neoVI ============================== .. note:: This ``ICS neoVI`` documentation is a work in progress. Feedback and revisions are most welcome! Interface to `Intrepid Control Systems `__ neoVI API range of devices via `python-ics `__ wrapper on Windows. Installation ------------ This neoVI interface requires the installation of the ICS neoVI DLL and ``python-ics`` package. - Download and install the Intrepid Product Drivers `Intrepid Product Drivers `__ - Install ``python-can`` with the ``neovi`` extras: .. code-block:: bash pip install python-can[neovi] Configuration ------------- An example `can.ini` file for windows 7: :: [default] interface = neovi channel = 1 Bus --- .. autoclass:: can.interfaces.ics_neovi.NeoViBus .. autoexception:: can.interfaces.ics_neovi.ICSApiError .. autoexception:: can.interfaces.ics_neovi.ICSInitializationError .. autoexception:: can.interfaces.ics_neovi.ICSOperationError python-can-4.5.0/doc/interfaces/nican.rst000066400000000000000000000011521472200326600203130ustar00rootroot00000000000000National Instruments NI-CAN =========================== This interface adds support for NI-CAN controllers by `National Instruments`_. .. warning:: NI-CAN only seems to support 32-bit architectures so if the driver can't be loaded on a 64-bit Python, try using a 32-bit version instead. .. warning:: CAN filtering has not been tested thoroughly and may not work as expected. Bus --- .. autoclass:: can.interfaces.nican.NicanBus .. autoexception:: can.interfaces.nican.NicanError .. autoexception:: can.interfaces.nican.NicanInitializationError .. _National Instruments: http://www.ni.com/can/ python-can-4.5.0/doc/interfaces/nixnet.rst000066400000000000000000000005671472200326600205410ustar00rootroot00000000000000National Instruments NI-XNET ============================ This interface adds support for NI-XNET CAN controllers by `National Instruments`_. .. note:: NI-XNET only supports windows platforms. Bus --- .. autoclass:: can.interfaces.nixnet.NiXNETcanBus :show-inheritance: :member-order: bysource :members: .. _National Instruments: http://www.ni.com/can/ python-can-4.5.0/doc/interfaces/pcan.rst000066400000000000000000000020711472200326600201450ustar00rootroot00000000000000.. _pcandoc: PCAN Basic API ============== Interface to `Peak-System `__'s PCAN-Basic API. Configuration ------------- Here is an example configuration file for using `PCAN-USB `_: :: [default] interface = pcan channel = PCAN_USBBUS1 state = can.bus.BusState.PASSIVE bitrate = 500000 ``channel`` (default ``"PCAN_USBBUS1"``) CAN interface name. Valid ``channel`` values:: PCAN_ISABUSx PCAN_DNGBUSx PCAN_PCIBUSx PCAN_USBBUSx PCAN_PCCBUSx PCAN_LANBUSx Where ``x`` should be replaced with the desired channel number starting at ``1``. ``state`` (default ``can.bus.BusState.ACTIVE``) BusState of the channel ``bitrate`` (default ``500000``) Channel bitrate .. _pcandoc linux installation: Linux installation ------------------ Beginning with version 3.4, Linux kernels support the PCAN adapters natively via :doc:`/interfaces/socketcan`, refer to: :ref:`socketcan-pcan`. Bus --- .. autoclass:: can.interfaces.pcan.PcanBus :members: python-can-4.5.0/doc/interfaces/robotell.rst000066400000000000000000000021611472200326600210460ustar00rootroot00000000000000.. _robotell: Robotell CAN-USB interface ========================== An USB to CAN adapter sold on Aliexpress, etc. with the manufacturer name Robotell printed on the case. There is also a USB stick version with a clear case. If the description or screenshots refer to ``EmbededDebug`` or ``EmbededConfig`` the device should be compatible with this driver. These USB devices are based on a STM32 controller with a CH340 serial interface and use a binary protocol - NOT compatible with SLCAN See `https://www.amobbs.com/thread-4651667-1-1.html `_ for some background on these devices. This driver directly uses either the local or remote (not tested) serial port. Remote serial ports will be specified via special URL. Both raw TCP sockets as also RFC2217 ports are supported. Usage: use ``port or URL[@baurate]`` to open the device. For example use ``/dev/ttyUSB0@115200`` or ``COM4@9600`` for local serial ports and ``socket://192.168.254.254:5000`` or ``rfc2217://192.168.254.254:5000`` for remote ports. Bus --- .. autoclass:: can.interfaces.robotell.robotellBus :members: python-can-4.5.0/doc/interfaces/seeedstudio.rst000066400000000000000000000025711472200326600215460ustar00rootroot00000000000000.. _seeeddoc: Seeed Studio USB-CAN Analyzer ============================= SKU: 114991193 Links: - https://www.seeedstudio.com/USB-CAN-Analyzer-p-2888.html - https://github.com/SeeedDocument/USB-CAN_Analyzer - https://copperhilltech.com/blog/usbcan-analyzer-usb-to-can-bus-serial-protocol-definition/ Installation ------------ This interface has additional dependencies which can be installed using pip and the optional extra ``seeedstudio``. That will include the dependency ``pyserial``:: pip install python-can[seeedstudio] Interface --------- :: can.interfaces.seeedstudio.SeeedBus A bus example:: bus = can.interface.Bus(interface='seeedstudio', channel='/dev/ttyUSB0', bitrate=500000) Configuration ------------- :: SeeedBus(channel, baudrate=2000000, timeout=0.1, frame_type='STD', operation_mode='normal', bitrate=500000) CHANNEL The serial port created by the USB device when connected. TIMEOUT Only used by the underling serial port, it probably should not be changed. The serial port baudrate=2000000 and rtscts=false are also matched to the device so are not added here. FRAMETYPE - "STD" - "EXT" OPERATIONMODE - "normal" - "loopback" - "silent" - "loopback_and_silent" BITRATE - 1000000 - 800000 - 500000 - 400000 - 250000 - 200000 - 125000 - 100000 - 50000 - 20000 - 10000 - 5000 python-can-4.5.0/doc/interfaces/serial.rst000066400000000000000000000143341472200326600205100ustar00rootroot00000000000000.. _serial: CAN over Serial =============== A text based interface. For example use over serial ports like ``/dev/ttyS1`` or ``/dev/ttyUSB0`` on Linux machines or ``COM1`` on Windows. Remote ports can be also used via a special URL. Both raw TCP sockets as also RFC2217 ports are supported: ``socket://192.168.254.254:5000`` or ``rfc2217://192.168.254.254:5000``. In addition a virtual loopback can be used via ``loop://`` URL. The interface is a simple implementation that has been used for recording CAN traces. .. note:: The properties **extended_id**, **is_remote_frame** and **is_error_frame** from the class:`~can.Message` are not in use. This interface will not send or receive flags for this properties. Bus --- .. autoclass:: can.interfaces.serial.serial_can.SerialBus .. automethod:: _recv_internal Internals --------- The frames that will be sent and received over the serial interface consist of six parts. The start and the stop byte for the frame, the timestamp, DLC, arbitration ID and the payload. The payload has a variable length of between 0 and 8 bytes, the other parts are fixed. Both, the timestamp and the arbitration ID will be interpreted as 4 byte unsigned integers. The DLC is also an unsigned integer with a length of 1 byte. Serial frame format ^^^^^^^^^^^^^^^^^^^ +-------------------+----------------+-----------------------------------------------+-------------------------------+-------------------------+---------+--------------+ | | Start of frame | Timestamp | DLC | Arbitration ID | Payload | End of frame | +===================+================+==============================+================+===============================+=========================+=========+==============+ | **Length (Byte)** | 1 | 4 | 1 | 4 | 0 - 8 | 1 | +-------------------+----------------+-----------------------------------------------+-------------------------------+-------------------------+---------+--------------+ | **Data type** | Byte | Unsigned 4 byte integer | Unsigned 1 byte integer | Unsigned 4 byte integer | Byte | Byte | +-------------------+----------------+-----------------------------------------------+-------------------------------+-------------------------+---------+--------------+ | **Byte order** | \- | Little-Endian | Little-Endian | Little-Endian | \- | \- | +-------------------+----------------+-----------------------------------------------+-------------------------------+-------------------------+---------+--------------+ | **Description** | Must be 0xAA | Usually s, ms or µs since start of the device | Length in byte of the payload | \- | \- | Must be 0xBB | +-------------------+----------------+-----------------------------------------------+-------------------------------+-------------------------+---------+--------------+ Examples of serial frames ^^^^^^^^^^^^^^^^^^^^^^^^^ .. rubric:: CAN message with 8 byte payload +----------------+-----------------------------------------+ | CAN message | +----------------+-----------------------------------------+ | Arbitration ID | Payload | +================+=========================================+ | 1 | 0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88 | +----------------+-----------------------------------------+ +----------------+---------------------+------+---------------------+-----------------------------------------+--------------+ | Serial frame | +----------------+---------------------+------+---------------------+-----------------------------------------+--------------+ | Start of frame | Timestamp | DLC | Arbitration ID | Payload | End of frame | +================+=====================+======+=====================+=========================================+==============+ | 0xAA | 0x66 0x73 0x00 0x00 | 0x08 | 0x01 0x00 0x00 0x00 | 0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88 | 0xBB | +----------------+---------------------+------+---------------------+-----------------------------------------+--------------+ .. rubric:: CAN message with 1 byte payload +----------------+---------+ | CAN message | +----------------+---------+ | Arbitration ID | Payload | +================+=========+ | 1 | 0x11 | +----------------+---------+ +----------------+---------------------+------+---------------------+---------+--------------+ | Serial frame | +----------------+---------------------+------+---------------------+---------+--------------+ | Start of frame | Timestamp | DLC | Arbitration ID | Payload | End of frame | +================+=====================+======+=====================+=========+==============+ | 0xAA | 0x66 0x73 0x00 0x00 | 0x01 | 0x01 0x00 0x00 0x00 | 0x11 | 0xBB | +----------------+---------------------+------+---------------------+---------+--------------+ .. rubric:: CAN message with 0 byte payload +----------------+---------+ | CAN message | +----------------+---------+ | Arbitration ID | Payload | +================+=========+ | 1 | None | +----------------+---------+ +----------------+---------------------+------+---------------------+--------------+ | Serial frame | +----------------+---------------------+------+---------------------+--------------+ | Start of frame | Timestamp | DLC | Arbitration ID | End of frame | +================+=====================+======+=====================+==============+ | 0xAA | 0x66 0x73 0x00 0x00 | 0x00 | 0x01 0x00 0x00 0x00 | 0xBB | +----------------+---------------------+------+---------------------+--------------+ python-can-4.5.0/doc/interfaces/slcan.rst000077500000000000000000000020371472200326600203310ustar00rootroot00000000000000.. _slcan: CAN over Serial / SLCAN ======================== A text based interface: compatible to slcan-interfaces (slcan ASCII protocol) should also support LAWICEL direct. These interfaces can also be used with socketcan and slcand with Linux. This driver directly uses either the local or remote serial port, it makes slcan-compatible interfaces usable with Windows also. Remote serial ports will be specified via special URL. Both raw TCP sockets as also RFC2217 ports are supported. Usage: use ``port or URL[@baurate]`` to open the device. For example use ``/dev/ttyUSB0@115200`` or ``COM4@9600`` for local serial ports and ``socket://192.168.254.254:5000`` or ``rfc2217://192.168.254.254:5000`` for remote ports. .. note: An Arduino-Interface could easily be build with this: https://github.com/latonita/arduino-canbus-monitor Supported devices ----------------- .. todo:: Document this. Bus --- .. autoclass:: can.interfaces.slcan.slcanBus :members: Internals --------- .. todo:: Document the internals of slcan interface. python-can-4.5.0/doc/interfaces/socketcan.rst000066400000000000000000000204431472200326600212010ustar00rootroot00000000000000.. _SocketCAN: SocketCAN ========= The SocketCAN documentation can be found in the `Linux kernel docs`_ in the ``networking`` directory. Quoting from the SocketCAN Linux documentation: The socketcan package is an implementation of CAN protocols (Controller Area Network) for Linux. CAN is a networking technology which has widespread use in automation, embedded devices, and automotive fields. While there have been other CAN implementations for Linux based on character devices, SocketCAN uses the Berkeley socket API, the Linux network stack and implements the CAN device drivers as network interfaces. The CAN socket API has been designed as similar as possible to the TCP/IP protocols to allow programmers, familiar with network programming, to easily learn how to use CAN sockets. .. important:: `python-can` versions before 2.2 had two different implementations named ``socketcan_ctypes`` and ``socketcan_native``. These were removed in version 4.0.0 after a deprecation period. Socketcan Quickstart -------------------- The CAN network driver provides a generic interface to setup, configure and monitor CAN devices. To configure bit-timing parameters use the program ``ip``. The virtual CAN driver (vcan) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The virtual CAN interfaces allow the transmission and reception of CAN frames without real CAN controller hardware. Virtual CAN network devices are usually named 'vcanX', like vcan0 vcan1 vcan2. To create a virtual can interface using socketcan run the following: .. code-block:: bash sudo modprobe vcan # Create a vcan network interface with a specific name sudo ip link add dev vcan0 type vcan sudo ip link set vcan0 up Real Device ~~~~~~~~~~~ ``vcan`` should be substituted for ``can`` and ``vcan0`` should be substituted for ``can0`` if you are using real hardware. Setting the bitrate can also be done at the same time, for example to enable an existing ``can0`` interface with a bitrate of 1MB: .. code-block:: bash sudo ip link set can0 up type can bitrate 1000000 CAN over Serial / SLCAN ~~~~~~~~~~~~~~~~~~~~~~~ SLCAN adapters can be used directly via :doc:`/interfaces/slcan`, or via :doc:`/interfaces/socketcan` with some help from the ``slcand`` utility which can be found in the `can-utils `_ package. To create a socketcan interface for an SLCAN adapter run the following: .. code-block:: bash slcand -f -o -c -s5 /dev/ttyAMA0 ip link set up slcan0 Names of the interfaces created by ``slcand`` match the ``slcan\d+`` regex. If a custom name is required, it can be specified as the last argument. E.g.: .. code-block:: bash slcand -f -o -c -s5 /dev/ttyAMA0 can0 ip link set up can0 .. _socketcan-pcan: PCAN ~~~~ Kernels >= 3.4 supports the PCAN adapters natively via :doc:`/interfaces/socketcan`, so there is no need to install any drivers. The CAN interface can be brought like so: :: sudo modprobe peak_usb sudo modprobe peak_pci sudo ip link set can0 up type can bitrate 500000 Intrepid ~~~~~~~~ The Intrepid Control Systems, Inc provides several devices (e.g. ValueCAN) as well as Linux module and user-space daemon to make it possible to use them via SocketCAN. Refer to below repositories for installation instructions: - `Intrepid kernel module`_ - `Intrepid user-space daemon`_ Send Test Message ^^^^^^^^^^^^^^^^^ The `can-utils`_ library for Linux includes a `cansend` tool which is useful to send known payloads. For example to send a message on `vcan0`: .. code-block:: bash cansend vcan0 123#DEADBEEF CAN Errors ^^^^^^^^^^ A device may enter the "bus-off" state if too many errors occurred on the CAN bus. Then no more messages are received or sent. An automatic bus-off recovery can be enabled by setting the "restart-ms" to a non-zero value, e.g.: .. code-block:: bash sudo ip link set canX type can restart-ms 100 Alternatively, the application may realize the "bus-off" condition by monitoring CAN error frames and do a restart when appropriate with the command: .. code-block:: bash ip link set canX type can restart Note that a restart will also create a CAN error frame. List network interfaces ~~~~~~~~~~~~~~~~~~~~~~~ To reveal the newly created ``can0`` or a ``vcan0`` interface: .. code-block:: bash ifconfig Display CAN statistics ~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: bash ip -details -statistics link show vcan0 Network Interface Removal ~~~~~~~~~~~~~~~~~~~~~~~~~ To remove the network interface: .. code-block:: bash sudo ip link del vcan0 Wireshark --------- Wireshark supports socketcan and can be used to debug *python-can* messages. Fire it up and watch your new interface. To spam a bus: .. code-block:: python import time import can interface = 'socketcan' channel = 'vcan0' def producer(id): """:param id: Spam the bus with messages including the data id.""" bus = can.Bus(channel=channel, interface=interface) for i in range(10): msg = can.Message(arbitration_id=0xc0ffee, data=[id, i, 0, 1, 3, 1, 4, 1], is_extended_id=False) bus.send(msg) time.sleep(1) producer(10) With debugging turned right up this looks something like this: .. image:: ../images/wireshark.png :width: 100% The process to follow bus traffic is even easier: .. code-block:: python for message in Bus(can_interface): print(message) Reading and Timeouts -------------------- Reading a single CAN message off of the bus is simple with the ``bus.recv()`` function: .. code-block:: python import can bus = can.Bus(channel='vcan0', interface='socketcan') message = bus.recv() By default, this performs a blocking read, which means ``bus.recv()`` won't return until a CAN message shows up on the socket. You can optionally perform a blocking read with a timeout like this: .. code-block:: python message = bus.recv(1.0) # Timeout in seconds. if message is None: print('Timeout occurred, no message.') If you set the timeout to ``0.0``, the read will be executed as non-blocking, which means ``bus.recv(0.0)`` will return immediately, either with a ``Message`` object or ``None``, depending on whether data was available on the socket. Filtering --------- The implementation features efficient filtering of can_id's. That filtering occurs in the kernel and is much much more efficient than filtering messages in Python. Broadcast Manager ----------------- The ``socketcan`` interface implements thin wrappers to the linux `broadcast manager` socket api. This allows the cyclic transmission of CAN messages at given intervals. The overhead for periodic message sending is extremely low as all the heavy lifting occurs within the linux kernel. The :class:`~can.BusABC` initialized for `socketcan` interface transparently handles scheduling of CAN messages to Linux BCM via :meth:`~can.BusABC.send_periodic`: .. code-block:: python with can.interface.Bus(interface="socketcan", channel="can0") as bus: task = bus.send_periodic(...) More examples that uses :meth:`~can.BusABC.send_periodic` are included in ``python-can/examples/cyclic.py``. The `task` object returned by :meth:`~can.BusABC.send_periodic` can be used to halt, alter or cancel the periodic message task: .. autoclass:: can.interfaces.socketcan.CyclicSendTask :members: Buffer Sizes ------------ Currently, the sending buffer size cannot be adjusted by this library. However, `this issue `__ describes how to change it via the command line/shell. Bus --- The :class:`~can.interfaces.socketcan.SocketcanBus` specializes :class:`~can.BusABC` to ensure usage of SocketCAN Linux API. The most important differences are: - usage of SocketCAN BCM for periodic messages scheduling; - filtering of CAN messages on Linux kernel level; - usage of nanosecond timings from the kernel. .. autoclass:: can.interfaces.socketcan.SocketcanBus :members: :inherited-members: .. External references .. _Linux kernel docs: https://www.kernel.org/doc/Documentation/networking/can.txt .. _Intrepid kernel module: https://github.com/intrepidcs/intrepid-socketcan-kernel-module .. _Intrepid user-space daemon: https://github.com/intrepidcs/icsscand .. _can-utils: https://github.com/linux-can/can-utils python-can-4.5.0/doc/interfaces/socketcand.rst000066400000000000000000000107761472200326600213550ustar00rootroot00000000000000.. _socketcand_doc: socketcand Interface ==================== `Socketcand `__ is part of the `Linux-CAN `__ project, providing a Network-to-CAN bridge as a Linux damon. It implements a specific `TCP/IP based communication protocol `__ to transfer CAN frames and control commands. The main advantage compared to UDP-based protocols (e.g. virtual interface) is, that TCP guarantees delivery and that the message order is kept. Here is a small example dumping all can messages received by a socketcand daemon running on a remote Raspberry Pi: .. code-block:: python import can bus = can.interface.Bus(interface='socketcand', host="10.0.16.15", port=29536, channel="can0") # loop until Ctrl-C try: while True: msg = bus.recv() print(msg) except KeyboardInterrupt: pass The output may look like this:: Timestamp: 1637791111.209224 ID: 000006fd X Rx DLC: 8 c4 10 e3 2d 96 ff 25 6b Timestamp: 1637791111.233951 ID: 000001ad X Rx DLC: 4 4d 47 c7 64 Timestamp: 1637791111.409415 ID: 000005f7 X Rx DLC: 8 86 de e6 0f 42 55 5d 39 Timestamp: 1637791111.434377 ID: 00000665 X Rx DLC: 8 97 96 51 0f 23 25 fc 28 Timestamp: 1637791111.609763 ID: 0000031d X Rx DLC: 8 16 27 d8 3d fe d8 31 24 Timestamp: 1637791111.634630 ID: 00000587 X Rx DLC: 8 4e 06 85 23 6f 81 2b 65 This interface also supports :meth:`~can.detect_available_configs`. .. code-block:: python import can import can.interfaces.socketcand cfg = can.interfaces.socketcand._detect_available_configs() if cfg: bus = can.Bus(**cfg[0]) The socketcand daemon broadcasts UDP beacons every 3 seconds. The default detection method waits for slightly more than 3 seconds to receive the beacon packet. If you want to increase the timeout, you can use :meth:`can.interfaces.socketcand.detect_beacon` directly. Below is an example which detects the beacon and uses the configuration to create a socketcand bus. .. code-block:: python import can import can.interfaces.socketcand cfg = can.interfaces.socketcand.detect_beacon(6000) if cfg: bus = can.Bus(**cfg[0]) Bus --- .. autoclass:: can.interfaces.socketcand.SocketCanDaemonBus :show-inheritance: :member-order: bysource :members: .. autofunction:: can.interfaces.socketcand.detect_beacon Socketcand Quickstart --------------------- The following section will show how to get the stuff installed on a Raspberry Pi with a MCP2515-based CAN interface, e.g. available from `Waveshare `__. However, it will also work with any other socketcan device. Install CAN Interface for a MCP2515 based interface on a Raspberry Pi ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Add the following lines to ``/boot/config.txt``. Please take care on the frequency of the crystal on your MCP2515 board:: dtparam=spi=on dtoverlay=mcp2515-can0,oscillator=12000000,interrupt=25,spimaxfrequency=1000000 Reboot after ``/boot/config.txt`` has been modified. Enable socketcan for can0 ~~~~~~~~~~~~~~~~~~~~~~~~~ Create config file for systemd-networkd to start the socketcan interface automatically: .. code-block:: bash cat >/etc/systemd/network/80-can.network <<'EOT' [Match] Name=can0 [CAN] BitRate=250K RestartSec=100ms EOT Enable ``systemd-networkd`` on reboot and start it immediately (if it was not already startet): .. code-block:: bash sudo systemctl enable systemd-networkd sudo systemctl start systemd-networkd Build socketcand from source ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: bash # autoconf is needed to build socketcand sudo apt-get install -y autoconf # clone & build sources git clone https://github.com/linux-can/socketcand.git cd socketcand ./autogen.sh ./configure make Install socketcand ~~~~~~~~~~~~~~~~~~ .. code-block:: bash make install Run socketcand ~~~~~~~~~~~~~~ .. code-block:: bash ./socketcand -v -i can0 During start, socketcand will prompt its IP address and port it listens to:: Verbose output activated Using network interface 'eth0' Listen adress is 10.0.16.15 Broadcast adress is 10.0.255.255 creating broadcast thread... binding socket to 10.0.16.15:29536 python-can-4.5.0/doc/interfaces/systec.rst000066400000000000000000000047331472200326600205450ustar00rootroot00000000000000.. _systec: SYSTEC interface ================ Windows interface for the USBCAN devices supporting up to 2 channels based on the particular product. There is support for the devices also on Linux through the :doc:`socketcan` interface and for Windows using this ``systec`` interface. Installation ------------ The interface requires installation of the **USBCAN32.dll** library. Download and install the driver for specific `SYSTEC `__ device. Supported devices ----------------- The interface supports following devices: - GW-001 (obsolete), - GW-002 (obsolete), - Multiport CAN-to-USB G3, - USB-CANmodul1 G3, - USB-CANmodul2 G3, - USB-CANmodul8 G3, - USB-CANmodul16 G3, - USB-CANmodul1 G4, - USB-CANmodul2 G4. Configuration ------------- The simplest configuration would be:: interface = systec channel = 0 Python-can will search for the first device found if not specified explicitly by the ``device_number`` parameter. The ``interface`` and ``channel`` are the only mandatory parameters. The interface supports two channels 0 and 1. The maximum number of entries in the receive and transmit buffer can be set by the parameters ``rx_buffer_entries`` and ``tx_buffer_entries``, with default value 4096 set for both. Optional parameters: * ``bitrate`` (default 500000) Channel bitrate in bit/s * ``device_number`` (default first device) The device number of the USB-CAN * ``rx_buffer_entries`` (default 4096) The maximum number of entries in the receive buffer * ``tx_buffer_entries`` (default 4096) The maximum number of entries in the transmit buffer * ``state`` (default BusState.ACTIVE) BusState of the channel * ``receive_own_messages`` (default False) If messages transmitted should also be received back Bus --- .. autoclass:: can.interfaces.systec.ucanbus.UcanBus :members: Internals --------- Message filtering ~~~~~~~~~~~~~~~~~ The interface and driver supports only setting of one filter per channel. If one filter is requested, this is will be handled by the driver itself. If more than one filter is needed, these will be handled in Python code in the ``recv`` method. If a message does not match any of the filters, ``recv()`` will return None. Periodic tasks ~~~~~~~~~~~~~~ The driver supports periodic message sending but without the possibility to set the interval between messages. Therefore the handling of the periodic messages is done by the interface using the :class:`~can.broadcastmanager.ThreadBasedCyclicSendTask`. python-can-4.5.0/doc/interfaces/udp_multicast.rst000066400000000000000000000047361472200326600221130ustar00rootroot00000000000000.. _udp_multicast_doc: Multicast IP Interface ====================== This module implements transport of CAN and CAN FD messages over UDP via Multicast IPv4 and IPv6. This virtual interface allows for communication between multiple processes and even hosts. This differentiates it from the :ref:`virtual_interface_doc` interface, which can only passes messages within a single process but does not require a network stack. It runs on UDP to have the lowest possible latency (as opposed to using TCP), and because normal IP multicast is inherently unreliable, as the recipients are unknown. This enables ad-hoc networks that do not require a central server but is also a so-called *unreliable network*. In practice however, local area networks (LANs) should most often be sufficiently reliable for this interface to function properly. .. note:: For an overview over the different virtual buses in this library and beyond, please refer to the section :ref:`virtual_interfaces_doc`. It also describes important limitations of this interface. Please refer to the `Bus class documentation`_ below for configuration options and useful resources for specifying multicast IP addresses. Supported Platforms ------------------- It should work on most Unix systems (including Linux with kernel 2.6.22+ and macOS) but currently not on Windows. Example ------- This example should print a single line indicating that a CAN message was successfully sent from ``bus_1`` to ``bus_2``: .. code-block:: python import time import can from can.interfaces.udp_multicast import UdpMulticastBus # The bus can be created using the can.Bus wrapper class or using UdpMulticastBus directly with can.Bus(channel=UdpMulticastBus.DEFAULT_GROUP_IPv6, interface='udp_multicast') as bus_1, \ UdpMulticastBus(channel=UdpMulticastBus.DEFAULT_GROUP_IPv6) as bus_2: # register a callback on the second bus that prints messages to the standard out notifier = can.Notifier(bus_2, [can.Printer()]) # create and send a message with the first bus, which should arrive at the second one message = can.Message(arbitration_id=0x123, data=[1, 2, 3]) bus_1.send(message) # give the notifier enough time to get triggered by the second bus time.sleep(2.0) Bus Class Documentation ----------------------- .. autoclass:: can.interfaces.udp_multicast.UdpMulticastBus :members: :exclude-members: send python-can-4.5.0/doc/interfaces/usb2can.rst000066400000000000000000000113171472200326600205640ustar00rootroot00000000000000USB2CAN Interface ================= The `USB2CAN `_ is a cheap CAN interface based on an ARM7 chip (STR750FV2). There is support for this device on Linux through the :doc:`socketcan` interface and for Windows using this ``usb2can`` interface. Support though windows is achieved through a DLL very similar to the way the PCAN functions. The API is called CANAL (CAN Abstraction Layer) which is a separate project designed to be used with VSCP which is a socket like messaging system that is not only cross platform but also supports other types of devices. Installation ------------ 1. To install on Windows download the USB2CAN Windows driver. It is compatible with XP, Vista, Win7, Win8/8.1. (Written against driver version v1.0.2.1) 2. Install the appropriate version of `pywin32 `_ (win32com) 3. Download the USB2CAN CANAL DLL from the USB2CAN website. Place this in either the same directory you are running usb2can.py from or your DLL folder in your python install. Note that only a 32-bit version is currently available, so this only works in a 32-bit Python environment. Internals --------- This interface originally written against CANAL DLL version ``v1.0.6``. Interface Layout ~~~~~~~~~~~~~~~~ - ``usb2canabstractionlayer.py`` This file is only a wrapper for the CANAL API that the interface expects. There are also a couple of constants here to try and make dealing with the bitwise operations for flag setting a little easier. Other than that this is only the CANAL API. If a programmer wanted to work with the API directly this is the file that allows you to do this. The CANAL project does not provide this wrapper and normally must be accessed with C. - ``usb2canInterface.py`` This file provides the translation to and from the python-can library to the CANAL API. This is where all the logic is and setup code is. Most issues if they are found will be either found here or within the DLL that is provided - ``serial_selector.py`` See the section below for the reason for adding this as it is a little odd. What program does is if a serial number is not provided to the usb2canInterface file this program does WMI (Windows Management Instrumentation) calls to try and figure out what device to connect to. It then returns the serial number of the device. Currently it is not really smart enough to figure out what to do if there are multiple devices. This needs to be changed if people are using more than one interface. Interface Specific Items ------------------------ There are a few things that are kinda strange about this device and are not overly obvious about the code or things that are not done being implemented in the DLL. 1. You need the Serial Number to connect to the device under Windows. This is part of the "setup string" that configures the device. There are a few options for how to get this. 1. Use ``usb2canWin.py`` to find the serial number. 2. Look on the device and enter it either through a prompt/barcode scanner/hardcode it. (Not recommended) 3. Reprogram the device serial number to something and do that for all the devices you own. (Really Not Recommended, can no longer use multiple devices on one computer) 2. In ``usb2canabstractionlayer.py`` there is a structure called ``CanalMsg`` which has a unsigned byte array of size 8. In the ``usb2canInterface`` file it passes in an unsigned byte array of size 8 also which if you pass less than 8 bytes in it stuffs it with extra zeros. So if the data ``"01020304"`` is sent the message would look like ``"0102030400000000"``. There is also a part of this structure called ``sizeData`` which is the actual length of the data that was sent not the stuffed message (in this case would be 4). What then happens is although a message of size 8 is sent to the device only the first 4 bytes of information would be sent. This is done because the DLL expects a length of 8 and nothing else. So to make it compatible that has to be sent through the wrapper. If ``usb2canInterface`` sent an array of length 4 with sizeData of 4 as well the array would throw an incompatible data type error. 3. The masking features have not been implemented currently in the CANAL interface in the version currently on the USB2CAN website. .. warning:: Currently message filtering is not implemented. Contributions are most welcome! Bus --- .. autoclass:: can.interfaces.usb2can.Usb2canBus Exceptions ---------- .. autoexception:: can.interfaces.usb2can.usb2canabstractionlayer.CanalError Miscellaneous ------------- .. autoclass:: can.interfaces.usb2can.Usb2CanAbstractionLayer :members: :undoc-members: python-can-4.5.0/doc/interfaces/vector.rst000066400000000000000000000056031472200326600205320ustar00rootroot00000000000000Vector ====== This interface adds support for CAN controllers by `Vector`_. Only Windows is supported. Configuration ------------- By default this library uses the channel configuration for CANalyzer. To use a different application, open **Vector Hardware Configuration** program and create a new application and assign the channels you may want to use. Specify the application name as ``app_name='Your app name'`` when constructing the bus or in a config file. Channel should be given as a list of channels starting at 0. Here is an example configuration file connecting to CAN 1 and CAN 2 for an application named "python-can": :: [default] interface = vector channel = 0, 1 app_name = python-can VectorBus --------- .. autoclass:: can.interfaces.vector.VectorBus :show-inheritance: :member-order: bysource :members: set_filters, recv, send, send_periodic, stop_all_periodic_tasks, flush_tx_buffer, reset, shutdown, popup_vector_hw_configuration, get_application_config, set_application_config Exceptions ---------- .. autoexception:: can.interfaces.vector.VectorError :show-inheritance: .. autoexception:: can.interfaces.vector.VectorInitializationError :show-inheritance: .. autoexception:: can.interfaces.vector.VectorOperationError :show-inheritance: Miscellaneous ------------- .. autofunction:: can.interfaces.vector.get_channel_configs .. autoclass:: can.interfaces.vector.VectorChannelConfig :show-inheritance: :class-doc-from: class .. autoclass:: can.interfaces.vector.canlib.VectorBusParams :show-inheritance: :class-doc-from: class .. autoclass:: can.interfaces.vector.canlib.VectorCanParams :show-inheritance: :class-doc-from: class .. autoclass:: can.interfaces.vector.canlib.VectorCanFdParams :show-inheritance: :class-doc-from: class .. autoclass:: can.interfaces.vector.xldefine.XL_HardwareType :show-inheritance: :member-order: bysource :members: :undoc-members: .. autoclass:: can.interfaces.vector.xldefine.XL_ChannelCapabilities :show-inheritance: :member-order: bysource :members: :undoc-members: .. autoclass:: can.interfaces.vector.xldefine.XL_BusCapabilities :show-inheritance: :member-order: bysource :members: :undoc-members: .. autoclass:: can.interfaces.vector.xldefine.XL_BusTypes :show-inheritance: :member-order: bysource :members: :undoc-members: .. autoclass:: can.interfaces.vector.xldefine.XL_OutputMode :show-inheritance: :member-order: bysource :members: :undoc-members: .. autoclass:: can.interfaces.vector.xldefine.XL_CANFD_BusParams_CanOpMode :show-inheritance: :member-order: bysource :members: :undoc-members: .. autoclass:: can.interfaces.vector.xldefine.XL_Status :show-inheritance: :member-order: bysource :members: :undoc-members: .. _Vector: https://vector.com/ python-can-4.5.0/doc/interfaces/virtual.rst000066400000000000000000000035501472200326600207150ustar00rootroot00000000000000.. _virtual_interface_doc: Virtual ======= The virtual interface can be used as a way to write OS and driver independent tests. Any `VirtualBus` instances connecting to the same channel (from within the same Python process) will receive each others messages. If messages shall be sent across process or host borders, consider using the :ref:`udp_multicast_doc` and refer to :ref:`virtual_interfaces_doc` for a comparison and general discussion of different virtual interfaces. Example ------- .. code-block:: python import can bus1 = can.interface.Bus('test', interface='virtual') bus2 = can.interface.Bus('test', interface='virtual') msg1 = can.Message(arbitration_id=0xabcde, data=[1,2,3]) bus1.send(msg1) msg2 = bus2.recv() #assert msg1 == msg2 assert msg1.arbitration_id == msg2.arbitration_id assert msg1.data == msg2.data assert msg1.timestamp != msg2.timestamp .. code-block:: python import can bus1 = can.interface.Bus('test', interface='virtual', preserve_timestamps=True) bus2 = can.interface.Bus('test', interface='virtual') msg1 = can.Message(timestamp=1639740470.051948, arbitration_id=0xabcde, data=[1,2,3]) # Messages sent on bus1 will have their timestamps preserved when received # on bus2 bus1.send(msg1) msg2 = bus2.recv() assert msg1.arbitration_id == msg2.arbitration_id assert msg1.data == msg2.data assert msg1.timestamp == msg2.timestamp # Messages sent on bus2 will not have their timestamps preserved when # received on bus1 bus2.send(msg1) msg3 = bus1.recv() assert msg1.arbitration_id == msg3.arbitration_id assert msg1.data == msg3.data assert msg1.timestamp != msg3.timestamp Bus Class Documentation ----------------------- .. autoclass:: can.interfaces.virtual.VirtualBus :members: .. automethod:: _detect_available_configs python-can-4.5.0/doc/internal-api.rst000066400000000000000000000116231472200326600174670ustar00rootroot00000000000000.. _internalapi: Internal API ============ Here we document the odds and ends that are more helpful for creating your own interfaces or listeners but generally shouldn't be required to interact with python-can. BusABC ------ The :class:`~can.BusABC` class, as the name suggests, provides an abstraction of a CAN bus. The bus provides a wrapper around a physical or virtual CAN Bus. An interface specific instance of the :class:`~can.BusABC` is created by the :class:`~can.Bus` class, see :ref:`bus` for the user facing API. .. _businternals: Extending the ``BusABC`` class ------------------------------ Concrete implementations **must** implement the following: * :meth:`~can.BusABC.send` to send individual messages * :meth:`~can.BusABC._recv_internal` to receive individual messages (see note below!) * set the :attr:`~can.BusABC.channel_info` attribute to a string describing the underlying bus and/or channel They **might** implement the following: * :meth:`~can.BusABC.flush_tx_buffer` to allow discarding any messages yet to be sent * :meth:`~can.BusABC.shutdown` to override how the bus should shut down * :meth:`~can.BusABC._send_periodic_internal` to override the software based periodic sending and push it down to the kernel or hardware. * :meth:`~can.BusABC._apply_filters` to apply efficient filters to lower level systems like the OS kernel or hardware. * :meth:`~can.BusABC._detect_available_configs` to allow the interface to report which configurations are currently available for new connections. * :meth:`~can.BusABC.state` property to allow reading and/or changing the bus state. .. note:: *TL;DR*: Only override :meth:`~can.BusABC._recv_internal`, never :meth:`~can.BusABC.recv` directly. Previously, concrete bus classes had to override :meth:`~can.BusABC.recv` directly instead of :meth:`~can.BusABC._recv_internal`, but that has changed to allow the abstract base class to handle in-software message filtering as a fallback. All internal interfaces now implement that new behaviour. Older (custom) interfaces might still be implemented like that and thus might not provide message filtering: Concrete instances are usually created by :func:`can.Bus` which takes the users configuration into account. Bus Internals ~~~~~~~~~~~~~ Several methods are not documented in the main :class:`can.BusABC` as they are primarily useful for library developers as opposed to library users. .. automethod:: can.BusABC.__init__ .. automethod:: can.BusABC.__iter__ .. automethod:: can.BusABC.__str__ .. autoattribute:: can.BusABC.__weakref__ .. automethod:: can.BusABC._recv_internal .. automethod:: can.BusABC._apply_filters .. automethod:: can.BusABC._send_periodic_internal .. automethod:: can.BusABC._detect_available_configs About the IO module ------------------- Handling of the different file formats is implemented in ``can.io``. Each file/IO type is within a separate module and ideally implements both a *Reader* and a *Writer*. The reader usually extends :class:`can.io.generic.BaseIOHandler`, while the writer often additionally extends :class:`can.Listener`, to be able to be passed directly to a :class:`can.Notifier`. Adding support for new file formats ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This assumes that you want to add a new file format, called *canstore*. Ideally add both reading and writing support for the new file format, although this is not strictly required. 1. Create a new module: *can/io/canstore.py* (*or* simply copy some existing one like *can/io/csv.py*) 2. Implement a reader ``CanstoreReader`` (which often extends :class:`can.io.generic.BaseIOHandler`, but does not have to). 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). Besides from a constructor, only ``on_message_received(self, msg)`` needs to be implemented. 4. Add a case to ``can.io.player.LogReader``'s ``__new__()``. 5. Document the two new classes (and possibly additional helpers) with docstrings and comments. Please mention features and limitations of the implementation. 6. Add a short section to the bottom of *doc/listeners.rst*. 7. Add tests where appropriate, for example by simply adding a test case called `class TestCanstoreFileFormat(ReaderWriterTest)` to *test/logformats_test.py*. That should already handle all of the general testing. Just follow the way the other tests in there do it. 8. Add imports to *can/__init__py* and *can/io/__init__py* so that the new classes can be simply imported as *from can import CanstoreReader, CanstoreWriter*. IO Utilities ~~~~~~~~~~~~ .. automodule:: can.io.generic :members: :member-order: bysource Other Utilities --------------- .. automodule:: can.util :members: python-can-4.5.0/doc/message.rst000066400000000000000000000141411472200326600165260ustar00rootroot00000000000000Message ======= .. module:: can .. autoclass:: Message One can instantiate a :class:`~can.Message` defining data, and optional arguments for all attributes such as arbitration ID, flags, and timestamp. >>> from can import Message >>> test = Message(data=[1, 2, 3, 4, 5]) >>> test.data bytearray(b'\x01\x02\x03\x04\x05') >>> test.dlc 5 >>> print(test) Timestamp: 0.000000 ID: 00000000 X Rx DL: 5 01 02 03 04 05 The :attr:`~can.Message.arbitration_id` field in a CAN message may be either 11 bits (standard addressing, CAN 2.0A) or 29 bits (extended addressing, CAN 2.0B) in length, and ``python-can`` exposes this difference with the :attr:`~can.Message.is_extended_id` attribute. .. attribute:: timestamp :type: float The timestamp field in a CAN message is a floating point number representing when the message was received since the epoch in seconds. Where possible this will be timestamped in hardware. .. attribute:: arbitration_id :type: int The frame identifier used for arbitration on the bus. The arbitration ID can take an int between 0 and the maximum value allowed depending on the ``is_extended_id`` flag (either 2\ :sup:`11` - 1 for 11-bit IDs, or 2\ :sup:`29` - 1 for 29-bit identifiers). >>> print(Message(is_extended_id=False, arbitration_id=100)) Timestamp: 0.000000 ID: 064 S Rx DL: 0 .. attribute:: data :type: bytearray The data parameter of a CAN message is exposed as a **bytearray** with length between 0 and 8. >>> example_data = bytearray([1, 2, 3]) >>> print(Message(data=example_data)) Timestamp: 0.000000 ID: 00000000 X Rx DL: 3 01 02 03 A :class:`~can.Message` can also be created with bytes, or lists of ints: >>> m1 = Message(data=[0x64, 0x65, 0x61, 0x64, 0x62, 0x65, 0x65, 0x66]) >>> print(m1.data) bytearray(b'deadbeef') >>> m2 = Message(data=b'deadbeef') >>> m2.data bytearray(b'deadbeef') .. attribute:: dlc :type: int The :abbr:`DLC (Data Length Code)` parameter of a CAN message is an integer between 0 and 8 representing the frame payload length. In the case of a CAN FD message, this indicates the data length in number of bytes. >>> m = Message(data=[1, 2, 3]) >>> m.dlc 3 .. note:: The DLC value does not necessarily define the number of bytes of data in a message. Its purpose varies depending on the frame type - for data frames it represents the amount of data contained in the message, in remote frames it represents the amount of data being requested. .. attribute:: channel :type: str or int or None This might store the channel from which the message came. .. attribute:: is_extended_id :type: bool This flag controls the size of the :attr:`~can.Message.arbitration_id` field. Previously this was exposed as `id_type`. >>> print(Message(is_extended_id=False)) Timestamp: 0.000000 ID: 000 S Rx DL: 0 >>> print(Message(is_extended_id=True)) Timestamp: 0.000000 ID: 00000000 X Rx DL: 0 .. note:: The initializer argument and attribute ``extended_id`` has been deprecated in favor of ``is_extended_id``, but will continue to work for the ``3.x`` release series. .. attribute:: is_error_frame :type: bool This boolean parameter indicates if the message is an error frame or not. >>> print(Message(is_error_frame=True)) Timestamp: 0.000000 ID: 00000000 X Rx E DL: 0 .. attribute:: is_remote_frame :type: bool This boolean attribute indicates if the message is a remote frame or a data frame, and modifies the bit in the CAN message's flags field indicating this. >>> print(Message(is_remote_frame=True)) Timestamp: 0.000000 ID: 00000000 X Rx R DL: 0 .. attribute:: is_fd :type: bool Indicates that this message is a CAN FD message. .. attribute:: is_rx :type: bool Indicates whether this message is a transmitted (Tx) or received (Rx) frame .. attribute:: bitrate_switch :type: bool If this is a CAN FD message, this indicates that a higher bitrate was used for the data transmission. .. attribute:: error_state_indicator :type: bool If this is a CAN FD message, this indicates an error active state. .. method:: __str__ A string representation of a CAN message: >>> from can import Message >>> test = Message() >>> print(test) Timestamp: 0.000000 ID: 00000000 X Rx DL: 0 >>> test2 = Message(data=[1, 2, 3, 4, 5]) >>> print(test2) Timestamp: 0.000000 ID: 00000000 X Rx DL: 5 01 02 03 04 05 The fields in the printed message are (in order): - timestamp, - arbitration ID, - flags, - data length (DL), - and data. The flags field is represented as one, two or three letters: - X if the :attr:`~can.Message.is_extended_id` attribute is set, otherwise S, - E if the :attr:`~can.Message.is_error_frame` attribute is set, - R if the :attr:`~can.Message.is_remote_frame` attribute is set. The arbitration ID field is represented as either a four or eight digit hexadecimal number depending on the length of the arbitration ID (11-bit or 29-bit). Each of the bytes in the data field (when present) are represented as two-digit hexadecimal numbers. .. automethod:: equals python-can-4.5.0/doc/notifier.rst000066400000000000000000000044031472200326600167210ustar00rootroot00000000000000Notifier and Listeners ====================== .. _notifier: Notifier -------- The Notifier object is used as a message distributor for a bus. The Notifier uses an event loop or creates a thread to read messages from the bus and distributes them to listeners. .. autoclass:: can.Notifier :members: .. _listeners_doc: Listener -------- The Listener class is an "abstract" base class for any objects which wish to register to receive notifications of new messages on the bus. A Listener can be used in two ways; the default is to **call** the Listener with a new message, or by calling the method **on_message_received**. Listeners are registered with :ref:`notifier` object(s) which ensure they are notified whenever a new message is received. .. literalinclude:: ../examples/print_notifier.py :language: python :linenos: :emphasize-lines: 8,9 Subclasses of Listener that do not override **on_message_received** will cause :class:`NotImplementedError` to be thrown when a message is received on the CAN bus. .. autoclass:: can.Listener :members: There are some listeners that already ship together with `python-can` and are listed below. Some of them allow messages to be written to files, and the corresponding file readers are also documented here. .. note :: Please note that writing and the reading a message might not always yield a completely unchanged message again, since some properties are not (yet) supported by some file formats. .. note :: Additional file formats for both reading/writing log files can be added via a plugin reader/writer. An external package can register a new reader by using the ``can.io.message_reader`` entry point. Similarly, a writer can be added using the ``can.io.message_writer`` entry point. The format of the entry point is ``reader_name=module:classname`` where ``classname`` is a :class:`can.io.generic.BaseIOHandler` concrete implementation. :: entry_points={ 'can.io.message_reader': [ '.asc = my_package.io.asc:ASCReader' ] }, BufferedReader -------------- .. autoclass:: can.BufferedReader :members: .. autoclass:: can.AsyncBufferedReader :members: RedirectReader -------------- .. autoclass:: can.RedirectReader :members: python-can-4.5.0/doc/other-tools.rst000066400000000000000000000074431472200326600173700ustar00rootroot00000000000000Other CAN Bus Tools =================== In order to keep the project maintainable, the scope of the package is limited to providing common abstractions to different hardware devices, and a basic suite of utilities for sending and receiving messages on a CAN bus. Other tools are available that either extend the functionality of python-can, or provide complementary features that python-can users might find useful. Some of these tools are listed below for convenience. CAN Message protocols (implemented in Python) --------------------------------------------- #. SAE J1939 Message Protocol * The `can-j1939`_ module provides an implementation of the CAN SAE J1939 standard for Python, including J1939-22. `can-j1939`_ uses python-can to provide support for multiple hardware interfaces. #. CIA CANopen * The `canopen`_ module provides an implementation of the CIA CANopen protocol, aiming to be used for automation and testing purposes #. ISO 15765-2 (ISO TP) * The `can-isotp`_ module provides an implementation of the ISO TP CAN protocol for sending data packets via a CAN transport layer. #. UDS * The `python-uds`_ module is a communication protocol agnostic implementation of the Unified Diagnostic Services (UDS) protocol defined in ISO 14229-1, although it does have extensions for performing UDS over CAN utilising the ISO TP protocol. This module has not been updated for some time. * The `uds`_ module is another tool that implements the UDS protocol, although it does have extensions for performing UDS over CAN utilising the ISO TP protocol. This module has not been updated for some time. #. XCP * The `pyxcp`_ module implements the Universal Measurement and Calibration Protocol (XCP). The purpose of XCP is to adjust parameters and acquire current values of internal variables in an ECU. .. _can-j1939: https://github.com/juergenH87/python-can-j1939 .. _canopen: https://canopen.readthedocs.io/en/latest/ .. _can-isotp: https://can-isotp.readthedocs.io/en/latest/ .. _python-uds: https://python-uds.readthedocs.io/en/latest/index.html .. _uds: https://uds.readthedocs.io/en/latest/ .. _pyxcp: https://pyxcp.readthedocs.io/en/latest/ CAN Frame Parsing tools etc. (implemented in Python) ---------------------------------------------------- #. CAN Message / Database scripting * The `cantools`_ package provides multiple methods for interacting with can message database files, and using these files to monitor live busses with a command line monitor tool. #. CAN Message / Log Decoding * The `canmatrix`_ module provides methods for converting between multiple popular message frame definition file formats (e.g. .DBC files, .KCD files, .ARXML files etc.). * The `pretty_j1939`_ module can be used to post-process CAN logs of J1939 traffic into human readable terminal prints or into a JSON file for consumption elsewhere in your scripts. .. _cantools: https://cantools.readthedocs.io/en/latest/ .. _canmatrix: https://canmatrix.readthedocs.io/en/latest/ .. _pretty_j1939: https://github.com/nmfta-repo/pretty_j1939 Other CAN related tools, programs etc. -------------------------------------- #. Micropython CAN class * A `CAN class`_ is available for the original micropython pyboard, with much of the same functionality as is available with python-can (but with a different API!). #. ASAM MDF Files * The `asammdf`_ module provides many methods for processing ASAM (Association for Standardization of Automation and Measuring Systems) MDF (Measurement Data Format) files. .. _`CAN class`: https://docs.micropython.org/en/latest/library/pyb.CAN.html .. _`asammdf`: https://asammdf.readthedocs.io/en/master/ | | .. note:: See also the available plugins for python-can in :ref:`plugin interface`. python-can-4.5.0/doc/plugin-interface.rst000066400000000000000000000066651472200326600203520ustar00rootroot00000000000000 .. _plugin interface: Plugin Interface ================ External packages can register new interfaces by using the ``can.interface`` entry point in its project configuration. The format of the entry point depends on your project configuration format (*pyproject.toml*, *setup.cfg* or *setup.py*). In the following example ``module`` defines the location of your bus class inside your package e.g. ``my_package.subpackage.bus_module`` and ``classname`` is the name of your :class:`can.BusABC` subclass. .. tab:: pyproject.toml (PEP 621) .. code-block:: toml # Note the quotes around can.interface in order to escape the dot . [project.entry-points."can.interface"] interface_name = "module:classname" .. tab:: setup.cfg .. code-block:: ini [options.entry_points] can.interface = interface_name = module:classname .. tab:: setup.py .. code-block:: python from setuptools import setup setup( # ..., entry_points = { 'can.interface': [ 'interface_name = module:classname' ] } ) The ``interface_name`` can be used to create an instance of the bus in the **python-can** API: .. code-block:: python import can bus = can.Bus(interface="interface_name", channel=0) Example Interface Plugins ------------------------- The table below lists interface drivers that can be added by installing additional packages that utilise the plugin API. These modules are optional dependencies of python-can. .. note:: The packages listed below are maintained by other authors. Any issues should be reported in their corresponding repository and **not** in the python-can repository. +----------------------------+-------------------------------------------------------+ | Name | Description | +============================+=======================================================+ | `python-can-canine`_ | CAN Driver for the CANine CAN interface | +----------------------------+-------------------------------------------------------+ | `python-can-cvector`_ | Cython based version of the 'VectorBus' | +----------------------------+-------------------------------------------------------+ | `python-can-remote`_ | CAN over network bridge | +----------------------------+-------------------------------------------------------+ | `python-can-sontheim`_ | CAN Driver for Sontheim CAN interfaces (e.g. CANfox) | +----------------------------+-------------------------------------------------------+ | `zlgcan-driver-py`_ | Python wrapper for zlgcan-driver-rs | +----------------------------+-------------------------------------------------------+ | `python-can-cando`_ | Python wrapper for Netronics' CANdo and CANdoISO | +----------------------------+-------------------------------------------------------+ .. _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 .. _python-can-cando: https://github.com/belliriccardo/python-can-cando python-can-4.5.0/doc/pycanlib.pml000066400000000000000000000024601472200326600166640ustar00rootroot00000000000000/* This promela model was used to verify a past design of the bus object. */ bool lock = false; inline enterMon() { atomic { !lock; lock = true; } } inline leaveMon() { lock = false; } typedef Condition { bool gate; byte waiting; } #define emptyC(C) (C.waiting == 0) inline waitC(C) { atomic { C.waiting++; lock = false; C.gate; C.gate = false; C.waiting--; } } inline signalC(C) { atomic { if :: (C.waiting > 0) -> C.gate = true; !lock; lock = true; :: else fi; } } mtype = { clear, set }; mtype writing_event = clear; byte critical = 0; Condition done_writing; bool live = false; active proctype RX() { end: do :: enterMon(); if :: (writing_event == set) -> waitC(done_writing); :: else fi; critical++; assert(critical == 1); live = true; live = false; critical--; leaveMon(); od } active proctype TX() { end: do :: atomic { writing_event == clear -> writing_event = set; } enterMon(); critical++; assert(critical == 1); live = true; live = false; critical--; writing_event = clear; signalC(done_writing); leaveMon(); od } ltl {[]<> live} python-can-4.5.0/doc/scripts.rst000066400000000000000000000036031472200326600165720ustar00rootroot00000000000000Command Line Tools ================== The following modules are callable from ``python-can``. They can be called for example by ``python -m can.logger`` or ``can_logger`` (if installed using pip). can.logger ---------- Command line help, called with ``--help``: .. command-output:: python -m can.logger -h :shell: can.player ---------- .. command-output:: python -m can.player -h :shell: can.viewer ---------- A screenshot of the application can be seen below: .. image:: images/viewer.png :width: 100% The first column is the number of times a frame with the particular ID that has been received, next is the timestamp of the frame relative to the first received message. The third column is the time between the current frame relative to the previous one. Next is the length of the frame, the data and then the decoded data converted according to the ``-d`` argument. The top red row indicates an error frame. There are several keyboard shortcuts that can be used with the viewer script, they function as follows: * ESCAPE - Quit the viewer script * q - as ESCAPE * c - Clear the stored frames * s - Sort the stored frames * h - Toggle highlighting of changed bytes in the data field - see the below image * SPACE - Pause the viewer * UP/DOWN - Scroll the viewer .. image:: images/viewer_changed_bytes_highlighting.png :width: 50% A byte in the data field is highlighted blue if the value is different from the last time the message was received. Command line arguments ^^^^^^^^^^^^^^^^^^^^^^ By default the ``can.viewer`` uses the :doc:`/interfaces/socketcan` interface. All interfaces are supported and can be specified using the ``-i`` argument or configured following :doc:`/configuration`. The full usage page can be seen below: .. command-output:: python -m can.viewer -h :shell: can.logconvert -------------- .. command-output:: python -m can.logconvert -h :shell: python-can-4.5.0/doc/utils.rst000066400000000000000000000001071472200326600162370ustar00rootroot00000000000000Utilities --------- .. autofunction:: can.detect_available_configs python-can-4.5.0/doc/virtual-interfaces.rst000066400000000000000000000204161472200326600207130ustar00rootroot00000000000000 .. _virtual_interfaces_doc: Virtual Interfaces ================== There are quite a few implementations for CAN networks that do not require physical CAN hardware. The built in virtual interfaces are: .. toctree:: :maxdepth: 1 interfaces/virtual interfaces/udp_multicast Comparison ---------- The following table compares some known virtual interfaces: +----------------------------------------------------+-----------------------------------------------------------------------+---------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------+ | **Name** | **Availability** | **Applicability** | **Implementation** | | | +-----------+-------------+-------------+--------------------+---------------------------------------------+---------------------------------------------------------------------+ | | | **Within | **Between | **Via (IP) | **Without Central | **Transport | **Serialization | | | | Process** | Processes** | Networks** | Server** | Technology** | Format** | +----------------------------------------------------+-----------------------------------------------------------------------+-----------+-------------+-------------+--------------------+---------------------------------------------+---------------------------------------------------------------------+ | ``virtual`` (this) | *included* | ✓ | ✗ | ✗ | ✓ | Singleton & Mutex | none | | | | | | | | (reliable) | | +----------------------------------------------------+-----------------------------------------------------------------------+-----------+-------------+-------------+--------------------+---------------------------------------------+---------------------------------------------------------------------+ | ``udp_multicast`` (:ref:`doc `) | *included* | ✓ | ✓ | ✓ | ✓ | UDP via IP multicast | custom using `msgpack `__ | | | | | | | | (unreliable) | | +----------------------------------------------------+-----------------------------------------------------------------------+-----------+-------------+-------------+--------------------+---------------------------------------------+---------------------------------------------------------------------+ | *christiansandberg/ | `external `__ | ✓ | ✓ | ✓ | ✗ | Websockets via TCP/IP | custom binary | | python-can-remote* | | | | | | (reliable) | | +----------------------------------------------------+-----------------------------------------------------------------------+-----------+-------------+-------------+--------------------+---------------------------------------------+---------------------------------------------------------------------+ | *windelbouwman/ | `external `__ | ✓ | ✓ | ✓ | ✗ | `ZeroMQ `__ via TCP/IP | custom binary [#f1]_ | | virtualcan* | | | | | | (reliable) | | +----------------------------------------------------+-----------------------------------------------------------------------+-----------+-------------+-------------+--------------------+---------------------------------------------+---------------------------------------------------------------------+ .. [#f1] The only option in this list that implements interoperability with other languages out of the box. For the others (except the first intra-process one), other programs written in potentially different languages could effortlessly interface with the bus once they mimic the serialization format. The last one, however, has already implemented the entire bus functionality in *C++* and *Rust*, besides the Python variant. Common Limitations ------------------ **Guaranteed delivery** and **message ordering** is one major point of difference: While in a physical CAN network, a message is either sent or in queue (or an explicit error occurred), this may not be the case for virtual networks. The ``udp_multicast`` bus for example, drops this property for the benefit of lower latencies by using unreliable UDP/IP instead of reliable TCP/IP (and because normal IP multicast is inherently unreliable, as the recipients are unknown by design). The other three buses faithfully model a physical CAN network in this regard: They ensure that all recipients actually receive (and acknowledge each message), much like in a physical CAN network. They also ensure that messages are relayed in the order they have arrived at the central server and that messages arrive at the recipients exactly once. Both is not guaranteed to hold for the best-effort ``udp_multicast`` bus as it uses UDP/IP as a transport layer. **Central servers** are, however, required by interfaces 3 and 4 (the external tools) to provide these guarantees of message delivery and message ordering. The central servers receive and distribute the CAN messages to all other bus participants, unlike in a real physical CAN network. The first intra-process ``virtual`` interface only runs within one Python process, effectively the Python instance of :class:`~can.interfaces.virtual.VirtualBus` acts as a central server. Notably the ``udp_multicast`` bus does not require a central server. **Arbitration and throughput** are two interrelated functions/properties of CAN networks which are typically abstracted in virtual interfaces. In all four interfaces, an unlimited amount of messages can be sent per unit of time (given the computational power of the machines and networks that are involved). In a real CAN/CAN FD networks, however, throughput is usually much more restricted and prioritization of arbitration IDs is thus an important feature once the bus is starting to get saturated. None of the interfaces presented above support any sort of throttling or ID arbitration under high loads. python-can-4.5.0/examples/000077500000000000000000000000001472200326600154205ustar00rootroot00000000000000python-can-4.5.0/examples/asyncio_demo.py000077500000000000000000000030371472200326600204510ustar00rootroot00000000000000#!/usr/bin/env python """ This example demonstrates how to use async IO with python-can. """ import asyncio from typing import List import can from can.notifier import MessageRecipient def print_message(msg: can.Message) -> None: """Regular callback function. Can also be a coroutine.""" print(msg) async def main() -> None: """The main function that runs in the loop.""" with can.Bus( interface="virtual", channel="my_channel_0", receive_own_messages=True ) as bus: reader = can.AsyncBufferedReader() logger = can.Logger("logfile.asc") listeners: List[MessageRecipient] = [ print_message, # Callback function reader, # AsyncBufferedReader() listener logger, # Regular Listener object ] # Create Notifier with an explicit loop to use for scheduling of callbacks 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() if __name__ == "__main__": asyncio.run(main()) python-can-4.5.0/examples/crc.py000077500000000000000000000042231472200326600165450ustar00rootroot00000000000000#!/usr/bin/env python """ This example exercises the periodic task's multiple message sending capabilities to send a message containing a counter and a checksum. Expects a vcan0 interface: python3 -m examples.crc """ import logging import time import can logging.basicConfig(level=logging.INFO) def crc_send(bus): """ Sends periodic messages every 1 s with no explicit timeout. Modifies messages after 8 seconds, sends for 10 more seconds, then stops. """ msg = can.Message(arbitration_id=0x12345678, data=[1, 2, 3, 4, 5, 6, 7, 0]) messages = build_crc_msgs(msg) print( "Starting to send a message with updating counter and checksum every 1 s for 8 s" ) task = bus.send_periodic(messages, 1) assert isinstance(task, can.CyclicSendTaskABC) time.sleep(8) msg = can.Message(arbitration_id=0x12345678, data=[8, 9, 10, 11, 12, 13, 14, 0]) messages = build_crc_msgs(msg) print("Sending modified message data every 1 s for 10 s") task.modify_data(messages) time.sleep(10) task.stop() print("stopped cyclic send") def build_crc_msgs(msg): """ Using the input message as base, create 16 messages with SAE J1939 SPN 3189 counters and SPN 3188 checksums placed in the final byte. """ messages = [] for counter in range(16): checksum = compute_xbr_checksum(msg, counter) msg.data[7] = counter + (checksum << 4) messages.append( can.Message(arbitration_id=msg.arbitration_id, data=msg.data[:]) ) return messages def compute_xbr_checksum(message, counter): """ Computes an XBR checksum per SAE J1939 SPN 3188. """ checksum = sum(message.data[:7]) checksum += sum(message.arbitration_id.to_bytes(length=4, byteorder="big")) checksum += counter & 0x0F xbr_checksum = ((checksum >> 4) + checksum) & 0x0F return xbr_checksum if __name__ == "__main__": for interface, channel in [("socketcan", "vcan0")]: print(f"Carrying out crc test with {interface} interface") with can.Bus(interface=interface, channel=channel, bitrate=500000) as BUS: crc_send(BUS) time.sleep(2) python-can-4.5.0/examples/cyclic.py000077500000000000000000000076641472200326600172600ustar00rootroot00000000000000#!/usr/bin/env python """ This example exercises the periodic sending capabilities. Expects a vcan0 interface: python3 -m examples.cyclic """ import logging import time import can logging.basicConfig(level=logging.INFO) def simple_periodic_send(bus): """ Sends a message every 20ms with no explicit timeout Sleeps for 2 seconds then stops the task. """ print("Starting to send a message every 200ms for 2s") msg = can.Message( arbitration_id=0x123, data=[1, 2, 3, 4, 5, 6], is_extended_id=False ) task = bus.send_periodic(msg, 0.20) assert isinstance(task, can.CyclicSendTaskABC) time.sleep(2) task.stop() print("stopped cyclic send") def limited_periodic_send(bus): """Send using LimitedDurationCyclicSendTaskABC.""" print("Starting to send a message every 200ms for 1s") msg = can.Message( arbitration_id=0x12345678, data=[0, 0, 0, 0, 0, 0], is_extended_id=True ) task = bus.send_periodic(msg, 0.20, 1, store_task=False) if not isinstance(task, can.LimitedDurationCyclicSendTaskABC): print("This interface doesn't seem to support LimitedDurationCyclicSendTaskABC") task.stop() return time.sleep(2) print("Cyclic send should have stopped as duration expired") # Note the (finished) task will still be tracked by the Bus # unless we pass `store_task=False` to bus.send_periodic # alternatively calling stop removes the task from the bus # task.stop() def test_periodic_send_with_modifying_data(bus): """Send using ModifiableCyclicTaskABC.""" print("Starting to send a message every 200ms. Initial data is four consecutive 1s") msg = can.Message(arbitration_id=0x0CF02200, data=[1, 1, 1, 1]) task = bus.send_periodic(msg, 0.20) if not isinstance(task, can.ModifiableCyclicTaskABC): print("This interface doesn't seem to support modification") task.stop() return time.sleep(2) print("Changing data of running task to begin with 99") msg.data[0] = 0x99 task.modify_data(msg) time.sleep(2) task.stop() print("stopped cyclic send") print("Changing data of stopped task to single ff byte") msg.data = bytearray([0xFF]) msg.dlc = 1 task.modify_data(msg) time.sleep(1) print("starting again") task.start() time.sleep(1) task.stop() print("done") # Will have to consider how to expose items like this. The socketcan # interfaces will continue to support it... but the top level api won't. # def test_dual_rate_periodic_send(): # """Send a message 10 times at 1ms intervals, then continue to send every 500ms""" # msg = can.Message(arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5]) # print("Creating cyclic task to send message 10 times at 1ms, then every 500ms") # task = can.interface.MultiRateCyclicSendTask('vcan0', msg, 10, 0.001, 0.50) # time.sleep(2) # # print("Changing data[0] = 0x42") # msg.data[0] = 0x42 # task.modify_data(msg) # time.sleep(2) # # task.stop() # print("stopped cyclic send") # # time.sleep(2) # # task.start() # print("starting again") # time.sleep(2) # task.stop() # print("done") def main(): """Test different cyclic sending tasks.""" reset_msg = can.Message( arbitration_id=0x00, data=[0, 0, 0, 0, 0, 0], is_extended_id=False ) # this uses the default configuration (for example from environment variables, or a # config file) see https://python-can.readthedocs.io/en/stable/configuration.html with can.Bus() as bus: bus.send(reset_msg) simple_periodic_send(bus) bus.send(reset_msg) limited_periodic_send(bus) test_periodic_send_with_modifying_data(bus) # print("Carrying out multirate cyclic test for {} interface".format(interface)) # can.rc['interface'] = interface # test_dual_rate_periodic_send() time.sleep(2) if __name__ == "__main__": main() python-can-4.5.0/examples/cyclic_checksum.py000066400000000000000000000034121472200326600211220ustar00rootroot00000000000000#!/usr/bin/env python """ This example demonstrates how to send a periodic message containing an automatically updating counter and checksum. Expects a virtual interface: python3 -m examples.cyclic_checksum """ import logging import time import can logging.basicConfig(level=logging.INFO) def cyclic_checksum_send(bus: can.BusABC) -> None: """ Sends periodic messages every 1 s with no explicit timeout. The message's counter and checksum is updated before each send. Sleeps for 10 seconds then stops the task. """ message = can.Message(arbitration_id=0x78, data=[0, 1, 2, 3, 4, 5, 6, 0]) print("Starting to send an auto-updating message every 100ms for 3 s") task = bus.send_periodic(msgs=message, period=0.1, modifier_callback=update_message) time.sleep(3) task.stop() print("stopped cyclic send") def update_message(message: can.Message) -> None: counter = increment_counter(message) checksum = compute_xbr_checksum(message, counter) message.data[7] = (checksum << 4) + counter def increment_counter(message: can.Message) -> int: counter = message.data[7] & 0x0F counter += 1 counter %= 16 return counter def compute_xbr_checksum(message: can.Message, counter: int) -> int: """ Computes an XBR checksum as per SAE J1939 SPN 3188. """ checksum = sum(message.data[:7]) checksum += sum(message.arbitration_id.to_bytes(length=4, byteorder="big")) checksum += counter & 0x0F xbr_checksum = ((checksum >> 4) + checksum) & 0x0F return xbr_checksum if __name__ == "__main__": with can.Bus(channel=0, interface="virtual", receive_own_messages=True) as _bus: notifier = can.Notifier(bus=_bus, listeners=[print]) cyclic_checksum_send(_bus) notifier.stop() python-can-4.5.0/examples/cyclic_multiple.py000077500000000000000000000072021472200326600211570ustar00rootroot00000000000000#!/usr/bin/env python """ This example exercises the periodic task's multiple message sending capabilities Expects a vcan0 interface: python3 -m examples.cyclic_multiple """ import logging import time import can logging.basicConfig(level=logging.INFO) def cyclic_multiple_send(bus): """ Sends periodic messages every 1 s with no explicit timeout Sleeps for 10 seconds then stops the task. """ print("Starting to send a message every 1 s for 10 s") messages = [] messages.append( can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) ) messages.append( can.Message( arbitration_id=0x401, data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], is_extended_id=False, ) ) messages.append( can.Message( arbitration_id=0x401, data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33], is_extended_id=False, ) ) messages.append( can.Message( arbitration_id=0x401, data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44], is_extended_id=False, ) ) messages.append( can.Message( arbitration_id=0x401, data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55], is_extended_id=False, ) ) task = bus.send_periodic(messages, 1) assert isinstance(task, can.CyclicSendTaskABC) time.sleep(10) task.stop() print("stopped cyclic send") def cyclic_multiple_send_modify(bus): """ Sends initial set of 3 Messages containing Odd data sent every 1 s with no explicit timeout. Sleeps for 8 s. Then the set is updated to 3 Messages containing Even data. Sleeps for 10 s. """ messages_odd = [] messages_odd.append( can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) ) messages_odd.append( can.Message( arbitration_id=0x401, data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33], is_extended_id=False, ) ) messages_odd.append( can.Message( arbitration_id=0x401, data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55], is_extended_id=False, ) ) messages_even = [] messages_even.append( can.Message( arbitration_id=0x401, data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], is_extended_id=False, ) ) messages_even.append( can.Message( arbitration_id=0x401, data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44], is_extended_id=False, ) ) messages_even.append( can.Message( arbitration_id=0x401, data=[0x66, 0x66, 0x66, 0x66, 0x66, 0x66], is_extended_id=False, ) ) print("Starting to send a message with odd every 1 s for 8 s with odd data") task = bus.send_periodic(messages_odd, 1) assert isinstance(task, can.CyclicSendTaskABC) time.sleep(8) print("Starting to send a message with even data every 1 s for 10 s with even data") task.modify_data(messages_even) time.sleep(10) print("stopped cyclic modify send") if __name__ == "__main__": for interface, channel in [("socketcan", "vcan0")]: print(f"Carrying out cyclic multiple tests with {interface} interface") with can.Bus(interface=interface, channel=channel, bitrate=500000) as BUS: cyclic_multiple_send(BUS) cyclic_multiple_send_modify(BUS) time.sleep(2) python-can-4.5.0/examples/print_notifier.py000077500000000000000000000010161472200326600210260ustar00rootroot00000000000000#!/usr/bin/env python import time import can def main(): with can.Bus(interface="virtual", receive_own_messages=True) as bus: print_listener = can.Printer() 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() if __name__ == "__main__": main() python-can-4.5.0/examples/receive_all.py000077500000000000000000000015221472200326600202470ustar00rootroot00000000000000#!/usr/bin/env python """ Shows how to receive messages via polling. """ import can from can.bus import BusState def receive_all(): """Receives all messages and prints them to the console until Ctrl+C is pressed.""" # this uses the default configuration (for example from environment variables, or a # config file) see https://python-can.readthedocs.io/en/stable/configuration.html with can.Bus() as bus: # set to read-only, only supported on some interfaces try: bus.state = BusState.PASSIVE except NotImplementedError: pass try: while True: msg = bus.recv(1) if msg is not None: print(msg) except KeyboardInterrupt: pass # exit normally if __name__ == "__main__": receive_all() python-can-4.5.0/examples/send_multiple.py000077500000000000000000000020651472200326600206440ustar00rootroot00000000000000#!/usr/bin/env python """ This demo creates multiple processes of producers to spam a socketcan bus. """ from time import sleep from concurrent.futures import ProcessPoolExecutor import can def producer(thread_id: int, message_count: int = 16) -> None: """Spam the bus with messages including the data id. :param thread_id: the id of the thread/process :param message_count: the number of messages that shall be sent """ # this uses the default configuration (for example from environment variables, or a # config file) see https://python-can.readthedocs.io/en/stable/configuration.html with can.Bus() as bus: for i in range(message_count): msg = can.Message( arbitration_id=0x0CF02200 + thread_id, data=[thread_id, i, 0, 1, 3, 1, 4, 1], ) bus.send(msg) sleep(1.0) print(f"Producer #{thread_id} finished sending {message_count} messages") if __name__ == "__main__": with ProcessPoolExecutor() as executor: executor.map(producer, range(5)) python-can-4.5.0/examples/send_one.py000077500000000000000000000020301472200326600175620ustar00rootroot00000000000000#!/usr/bin/env python """ This example shows how sending a single message works. """ import can def send_one(): """Sends a single message.""" # this uses the default configuration (for example from the config file) # see https://python-can.readthedocs.io/en/stable/configuration.html with can.Bus() as bus: # Using specific buses works similar: # bus = can.Bus(interface='socketcan', channel='vcan0', bitrate=250000) # bus = can.Bus(interface='pcan', channel='PCAN_USBBUS1', bitrate=250000) # bus = can.Bus(interface='ixxat', channel=0, bitrate=250000) # bus = can.Bus(interface='vector', app_name='CANalyzer', channel=0, bitrate=250000) # ... msg = can.Message( arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True ) try: bus.send(msg) print(f"Message sent on {bus.channel_info}") except can.CanError: print("Message NOT sent") if __name__ == "__main__": send_one() python-can-4.5.0/examples/serial_com.py000077500000000000000000000043341472200326600201160ustar00rootroot00000000000000#!/usr/bin/env python """ This example sends every second a messages over the serial interface and also receives incoming messages. python3 -m examples.serial_com Expects two serial ports (/dev/ttyS10 and /dev/ttyS11) connected to each other: Linux: To connect two ports use socat. sudo apt-get install socat sudo socat PTY,link=/dev/ttyS10 PTY,link=/dev/ttyS11 Windows: This example was not tested on Windows. To create and connect virtual ports on Windows, the following software can be used: com0com: http://com0com.sourceforge.net/ """ import time import threading import can def send_cyclic(bus, msg, stop_event): """The loop for sending.""" print("Start to send a message every 1s") start_time = time.time() while not stop_event.is_set(): msg.timestamp = time.time() - start_time bus.send(msg) print(f"tx: {msg}") time.sleep(1) print("Stopped sending messages") def receive(bus, stop_event): """The loop for receiving.""" print("Start receiving messages") while not stop_event.is_set(): rx_msg = bus.recv(1) if rx_msg is not None: print(f"rx: {rx_msg}") print("Stopped receiving messages") def main(): """Controls the sender and receiver.""" with can.Bus(interface="serial", channel="/dev/ttyS10") as server: with can.Bus(interface="serial", channel="/dev/ttyS11") as client: tx_msg = can.Message( arbitration_id=0x01, data=[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88], ) # Thread for sending and receiving messages stop_event = threading.Event() t_send_cyclic = threading.Thread( target=send_cyclic, args=(server, tx_msg, stop_event) ) t_receive = threading.Thread(target=receive, args=(client, stop_event)) t_receive.start() t_send_cyclic.start() try: while True: time.sleep(0) # yield except KeyboardInterrupt: pass # exit normally stop_event.set() time.sleep(0.5) print("Stopped script") if __name__ == "__main__": main() python-can-4.5.0/examples/simple_log_converter.py000077500000000000000000000007301472200326600222160ustar00rootroot00000000000000#!/usr/bin/env python """ Use this to convert .can/.asc files to .log files. Can be easily adapted for all sorts of files. Usage: python3 simple_log_convert.py sourceLog.asc targetLog.log """ import sys import can def main(): """The transcoder""" with can.LogReader(sys.argv[1]) as reader: with can.Logger(sys.argv[2]) as writer: for msg in reader: writer.on_message_received(msg) if __name__ == "__main__": main() python-can-4.5.0/examples/vcan_filtered.py000077500000000000000000000016411472200326600206040ustar00rootroot00000000000000#!/usr/bin/env python """ This shows how message filtering works. """ import time import can def main(): """Send some messages to itself and apply filtering.""" with can.Bus(interface="virtual", receive_own_messages=True) as bus: can_filters = [{"can_id": 1, "can_mask": 0xF, "extended": True}] bus.set_filters(can_filters) # 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() if __name__ == "__main__": main() python-can-4.5.0/pyproject.toml000066400000000000000000000127251472200326600165250ustar00rootroot00000000000000[build-system] requires = ["setuptools >= 67.7", "setuptools_scm>=8"] build-backend = "setuptools.build_meta" [project] name = "python-can" dynamic = ["readme", "version"] description = "Controller Area Network interface module for Python" authors = [{ name = "python-can contributors" }] dependencies = [ "wrapt~=1.10", "packaging >= 23.1", "typing_extensions>=3.10.0.0", "msgpack~=1.1.0; platform_system != 'Windows'", ] requires-python = ">=3.8" license = { text = "LGPL v3" } classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "Intended Audience :: Education", "Intended Audience :: Information Technology", "Intended Audience :: Manufacturing", "Intended Audience :: Telecommunications Industry", "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 :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Embedded Systems", "Topic :: Software Development :: Embedded Systems :: Controller Area Network (CAN)", "Topic :: System :: Hardware :: Hardware Drivers", "Topic :: System :: Logging", "Topic :: System :: Monitoring", "Topic :: System :: Networking", "Topic :: Utilities", ] [project.scripts] can_logconvert = "can.logconvert:main" can_logger = "can.logger:main" can_player = "can.player:main" can_viewer = "can.viewer:main" [project.urls] homepage = "https://github.com/hardbyte/python-can" documentation = "https://python-can.readthedocs.io" repository = "https://github.com/hardbyte/python-can" changelog = "https://github.com/hardbyte/python-can/blob/develop/CHANGELOG.md" [project.optional-dependencies] lint = [ "pylint==3.2.*", "ruff==0.7.0", "black==24.10.*", "mypy==1.12.*", ] pywin32 = ["pywin32>=305"] seeedstudio = ["pyserial>=3.0"] serial = ["pyserial~=3.0"] neovi = ["filelock", "python-ics>=2.12"] canalystii = ["canalystii>=0.1.0"] cantact = ["cantact>=0.0.7"] cvector = ["python-can-cvector"] gs_usb = ["gs_usb>=0.2.1"] nixnet = ["nixnet>=0.3.2"] pcan = ["uptime~=3.0.1"] remote = ["python-can-remote"] sontheim = ["python-can-sontheim>=0.1.2"] canine = ["python-can-canine>=0.2.2"] zlgcan = ["zlgcan-driver-py"] viewer = [ "windows-curses; platform_system == 'Windows' and platform_python_implementation=='CPython'" ] mf4 = ["asammdf>=6.0.0"] [tool.setuptools.dynamic] readme = { file = "README.rst" } [tool.setuptools.package-data] "*" = ["README.rst", "CONTRIBUTORS.txt", "LICENSE.txt", "CHANGELOG.md"] doc = ["*.*"] examples = ["*.py"] can = ["py.typed"] [tool.setuptools.packages.find] include = ["can*"] [tool.setuptools_scm] # can be empty if no extra settings are needed, presence enables setuptools_scm [tool.mypy] warn_return_any = true warn_unused_configs = true ignore_missing_imports = true no_implicit_optional = true disallow_incomplete_defs = true warn_redundant_casts = true warn_unused_ignores = true exclude = [ "venv", "^doc/conf.py$", "^build", "^test", "^can/interfaces/__init__.py", "^can/interfaces/etas", "^can/interfaces/gs_usb", "^can/interfaces/ics_neovi", "^can/interfaces/iscan", "^can/interfaces/ixxat", "^can/interfaces/kvaser", "^can/interfaces/nican", "^can/interfaces/neousys", "^can/interfaces/pcan", "^can/interfaces/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 = [ "A", # flake8-builtins "B", # flake8-bugbear "C4", # flake8-comprehensions "F", # pyflakes "E", # pycodestyle errors "I", # isort "N", # pep8-naming "PGH", # pygrep-hooks "PL", # pylint "RUF", # ruff-specific rules "T20", # flake8-print "TCH", # flake8-type-checking "UP", # pyupgrade "W", # pycodestyle warnings "YTT", # flake8-2020 ] ignore = [ "B026", # star-arg-unpacking-after-keyword-arg "PLR", # pylint refactor ] [tool.ruff.lint.per-file-ignores] "can/interfaces/*" = [ "E501", # Line too long "F403", # undefined-local-with-import-star "F405", # undefined-local-with-import-star-usage "N", # pep8-naming "PGH003", # blanket-type-ignore "RUF012", # mutable-class-default ] "can/logger.py" = ["T20"] # flake8-print "can/player.py" = ["T20"] # flake8-print [tool.ruff.lint.isort] known-first-party = ["can"] [tool.pylint] disable = [ "c-extension-no-member", "cyclic-import", "duplicate-code", "fixme", "invalid-name", "missing-class-docstring", "missing-function-docstring", "missing-module-docstring", "no-else-raise", "no-else-return", "too-few-public-methods", "too-many-arguments", "too-many-branches", "too-many-instance-attributes", "too-many-locals", "too-many-public-methods", "too-many-statements", ] python-can-4.5.0/test/000077500000000000000000000000001472200326600145615ustar00rootroot00000000000000python-can-4.5.0/test/__init__.py000066400000000000000000000000261472200326600166700ustar00rootroot00000000000000#!/usr/bin/env python python-can-4.5.0/test/back2back_test.py000066400000000000000000000400651472200326600200020ustar00rootroot00000000000000#!/usr/bin/env python """ This module tests two buses attached to each other. """ import random import unittest from multiprocessing.dummy import Pool as ThreadPool from time import sleep, time import pytest import can from can import CanInterfaceNotImplementedError from can.interfaces.udp_multicast import UdpMulticastBus from .config import ( IS_CI, IS_OSX, IS_PYPY, IS_TRAVIS, IS_UNIX, TEST_CAN_FD, TEST_INTERFACE_SOCKETCAN, ) class Back2BackTestCase(unittest.TestCase): """Use two interfaces connected to the same CAN bus and test them against each other. This very class declaration runs the test on the *virtual* interface but subclasses can be created for other buses. """ BITRATE = 500000 TIMEOUT = 0.1 INTERFACE_1 = "virtual" CHANNEL_1 = "virtual_channel_0" INTERFACE_2 = "virtual" CHANNEL_2 = "virtual_channel_0" def setUp(self): self.bus1 = can.Bus( channel=self.CHANNEL_1, interface=self.INTERFACE_1, bitrate=self.BITRATE, fd=TEST_CAN_FD, single_handle=True, ) self.bus2 = can.Bus( channel=self.CHANNEL_2, interface=self.INTERFACE_2, bitrate=self.BITRATE, fd=TEST_CAN_FD, single_handle=True, ) def tearDown(self): self.bus1.shutdown() self.bus2.shutdown() def _check_received_message( self, recv_msg: can.Message, sent_msg: can.Message ) -> None: self.assertIsNotNone( recv_msg, "No message was received on %s" % self.INTERFACE_2 ) self.assertEqual(recv_msg.arbitration_id, sent_msg.arbitration_id) self.assertEqual(recv_msg.is_extended_id, sent_msg.is_extended_id) self.assertEqual(recv_msg.is_remote_frame, sent_msg.is_remote_frame) self.assertEqual(recv_msg.is_error_frame, sent_msg.is_error_frame) self.assertEqual(recv_msg.is_fd, sent_msg.is_fd) self.assertEqual(recv_msg.bitrate_switch, sent_msg.bitrate_switch) self.assertEqual(recv_msg.dlc, sent_msg.dlc) if not sent_msg.is_remote_frame: self.assertSequenceEqual(recv_msg.data, sent_msg.data) def _send_and_receive(self, msg: can.Message) -> None: # Send with bus 1, receive with bus 2 self.bus1.send(msg) recv_msg = self.bus2.recv(self.TIMEOUT) self._check_received_message(recv_msg, msg) # Some buses may receive their own messages. Remove it from the queue self.bus1.recv(0) # Send with bus 2, receive with bus 1 # Add 1 to arbitration ID to make it a different message msg.arbitration_id += 1 self.bus2.send(msg) # Some buses may receive their own messages. Remove it from the queue self.bus2.recv(0) recv_msg = self.bus1.recv(self.TIMEOUT) self._check_received_message(recv_msg, msg) def test_no_message(self): """Tests that there is no message being received if none was sent.""" self.assertIsNone(self.bus1.recv(0.1)) def test_multiple_shutdown(self): """Tests whether shutting down ``bus1`` twice does not throw any errors.""" self.bus1.shutdown() @unittest.skipIf( IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server", ) def test_timestamp(self): self.bus2.send(can.Message()) recv_msg1 = self.bus1.recv(self.TIMEOUT) sleep(2.0) self.bus2.send(can.Message()) recv_msg2 = self.bus1.recv(self.TIMEOUT) delta_time = recv_msg2.timestamp - recv_msg1.timestamp self.assertTrue( 1.75 <= delta_time <= 2.25, "Time difference should have been 2s +/- 250ms." f"But measured {delta_time}", ) def test_standard_message(self): msg = can.Message( is_extended_id=False, arbitration_id=0x100, data=[1, 2, 3, 4, 5, 6, 7, 8] ) self._send_and_receive(msg) def test_extended_message(self): msg = can.Message( is_extended_id=True, arbitration_id=0x123456, data=[10, 11, 12, 13, 14, 15, 16, 17], ) self._send_and_receive(msg) def test_remote_message(self): msg = can.Message( is_extended_id=False, arbitration_id=0x200, is_remote_frame=True, dlc=4 ) self._send_and_receive(msg) def test_dlc_less_than_eight(self): msg = can.Message(is_extended_id=False, arbitration_id=0x300, data=[4, 5, 6]) self._send_and_receive(msg) @unittest.skip( "TODO: how shall this be treated if sending messages locally? should be done uniformly" ) def test_message_is_rx(self): """Verify that received messages have is_rx set to `False` while messages received on the other virtual interfaces have is_rx set to `True`. """ msg = can.Message( is_extended_id=False, arbitration_id=0x300, data=[2, 1, 3], is_rx=False ) self.bus1.send(msg) # Some buses may receive their own messages. Remove it from the queue self.bus1.recv(0) self_recv_msg = self.bus2.recv(self.TIMEOUT) self.assertIsNotNone(self_recv_msg) self.assertTrue(self_recv_msg.is_rx) @unittest.skip( "TODO: how shall this be treated if sending messages locally? should be done uniformly" ) def test_message_is_rx_receive_own_messages(self): """The same as `test_message_direction` but testing with `receive_own_messages=True`.""" bus3 = can.Bus( channel=self.CHANNEL_2, interface=self.INTERFACE_2, bitrate=self.BITRATE, fd=TEST_CAN_FD, single_handle=True, receive_own_messages=True, ) try: msg = can.Message( is_extended_id=False, arbitration_id=0x300, data=[2, 1, 3], is_rx=False ) bus3.send(msg) self_recv_msg_bus3 = bus3.recv(self.TIMEOUT) self.assertTrue(self_recv_msg_bus3.is_rx) finally: bus3.shutdown() def test_unique_message_instances(self): """Verify that we have a different instances of message for each bus even with `receive_own_messages=True`. """ bus3 = can.Bus( channel=self.CHANNEL_2, interface=self.INTERFACE_2, bitrate=self.BITRATE, fd=TEST_CAN_FD, single_handle=True, receive_own_messages=True, ) try: msg = can.Message( is_extended_id=False, arbitration_id=0x300, data=[2, 1, 3] ) bus3.send(msg) recv_msg_bus1 = self.bus1.recv(self.TIMEOUT) recv_msg_bus2 = self.bus2.recv(self.TIMEOUT) self_recv_msg_bus3 = bus3.recv(self.TIMEOUT) self._check_received_message(recv_msg_bus1, recv_msg_bus2) self._check_received_message(recv_msg_bus2, self_recv_msg_bus3) recv_msg_bus1.data[0] = 4 self.assertNotEqual(recv_msg_bus1.data, recv_msg_bus2.data) self.assertEqual(recv_msg_bus2.data, self_recv_msg_bus3.data) finally: bus3.shutdown() def test_fd_message(self): msg = can.Message( is_fd=True, is_extended_id=True, arbitration_id=0x56789, data=[0xFF] * 64 ) self._send_and_receive(msg) def test_fd_message_with_brs(self): msg = can.Message( is_fd=True, bitrate_switch=True, is_extended_id=True, arbitration_id=0x98765, data=[0xFF] * 48, ) self._send_and_receive(msg) def test_fileno(self): """Test is the values returned by fileno() are valid.""" try: fileno = self.bus1.fileno() except NotImplementedError: pass # allow it to be left non-implemented else: self.assertIsNotNone(fileno) self.assertTrue(fileno == -1 or fileno > 0) def test_timestamp_is_absolute(self): """Tests that the timestamp that is returned is an absolute one.""" self.bus2.send(can.Message()) # Some buses may receive their own messages. Remove it from the queue self.bus2.recv(0) message = self.bus1.recv(self.TIMEOUT) # The allowed delta is still quite large to make this work on the CI server self.assertAlmostEqual(message.timestamp, time(), delta=self.TIMEOUT) def test_sub_second_timestamp_resolution(self): """Tests that the timestamp that is returned has sufficient resolution. The property that the timestamp has resolution below seconds is checked on two messages to reduce the probability of both having a timestamp of exactly a full second by accident to a negligible level. This is a regression test that was added for #1021. """ self.bus2.send(can.Message()) sleep(0.01) self.bus2.send(can.Message()) recv_msg_1 = self.bus1.recv(self.TIMEOUT) recv_msg_2 = self.bus1.recv(self.TIMEOUT) sub_second_fraction_1 = recv_msg_1.timestamp % 1 sub_second_fraction_2 = recv_msg_2.timestamp % 1 self.assertGreater(sub_second_fraction_1 + sub_second_fraction_2, 0) # Some buses may receive their own messages. Remove it from the queue self.bus2.recv(0) self.bus2.recv(0) @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 messages = [] self.bus2.send_periodic(can.Message(), period, duration) while (msg := self.bus1.recv(period * 1.25)) is not None: messages.append(msg) delta_t = round(messages[-1].timestamp - messages[0].timestamp, 2) assert delta_t <= duration @unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan") class BasicTestSocketCan(Back2BackTestCase): INTERFACE_1 = "socketcan" CHANNEL_1 = "vcan0" INTERFACE_2 = "socketcan" CHANNEL_2 = "vcan0" # this doesn't even work on Travis CI for macOS; for example, see # https://travis-ci.org/github/hardbyte/python-can/jobs/745389871 @unittest.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)", ) class BasicTestUdpMulticastBusIPv4(Back2BackTestCase): INTERFACE_1 = "udp_multicast" CHANNEL_1 = UdpMulticastBus.DEFAULT_GROUP_IPv4 INTERFACE_2 = "udp_multicast" CHANNEL_2 = UdpMulticastBus.DEFAULT_GROUP_IPv4 def test_unique_message_instances(self): with self.assertRaises(CanInterfaceNotImplementedError): super().test_unique_message_instances() # this doesn't even work for loopback multicast addresses on Travis CI; for example, see # https://travis-ci.org/github/hardbyte/python-can/builds/745065503 @unittest.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)", ) class BasicTestUdpMulticastBusIPv6(Back2BackTestCase): HOST_LOCAL_MCAST_GROUP_IPv6 = "ff11:7079:7468:6f6e:6465:6d6f:6d63:6173" INTERFACE_1 = "udp_multicast" CHANNEL_1 = HOST_LOCAL_MCAST_GROUP_IPv6 INTERFACE_2 = "udp_multicast" CHANNEL_2 = HOST_LOCAL_MCAST_GROUP_IPv6 def test_unique_message_instances(self): with self.assertRaises(CanInterfaceNotImplementedError): super().test_unique_message_instances() TEST_INTERFACE_ETAS = False try: bus_class = can.interface._get_class_for_interface("etas") TEST_INTERFACE_ETAS = True except CanInterfaceNotImplementedError: pass @unittest.skipUnless(TEST_INTERFACE_ETAS, "skip testing of etas interface") class BasicTestEtas(Back2BackTestCase): if TEST_INTERFACE_ETAS: configs = can.interface.detect_available_configs(interfaces="etas") INTERFACE_1 = "etas" CHANNEL_1 = configs[0]["channel"] INTERFACE_2 = "etas" CHANNEL_2 = configs[2]["channel"] def test_unique_message_instances(self): self.skipTest( "creating a second instance of a channel with differing self-reception settings is not supported" ) @unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan") class SocketCanBroadcastChannel(unittest.TestCase): def setUp(self): self.broadcast_bus = can.Bus(channel="", interface="socketcan") self.regular_bus = can.Bus(channel="vcan0", interface="socketcan") def tearDown(self): self.broadcast_bus.shutdown() self.regular_bus.shutdown() def test_broadcast_channel(self): self.broadcast_bus.send(can.Message(channel="vcan0")) recv_msg = self.regular_bus.recv(1) self.assertIsNotNone(recv_msg) self.assertEqual(recv_msg.channel, "vcan0") self.regular_bus.send(can.Message()) recv_msg = self.broadcast_bus.recv(1) self.assertIsNotNone(recv_msg) self.assertEqual(recv_msg.channel, "vcan0") class TestThreadSafeBus(Back2BackTestCase): def setUp(self): self.bus1 = can.ThreadSafeBus( channel=self.CHANNEL_1, interface=self.INTERFACE_1, bitrate=self.BITRATE, fd=TEST_CAN_FD, single_handle=True, ) self.bus2 = can.ThreadSafeBus( channel=self.CHANNEL_2, interface=self.INTERFACE_2, bitrate=self.BITRATE, fd=TEST_CAN_FD, single_handle=True, ) @pytest.mark.timeout(180.0 if IS_PYPY else 5.0) def test_concurrent_writes(self): sender_pool = ThreadPool(100) receiver_pool = ThreadPool(100) message = can.Message( arbitration_id=0x123, channel=self.CHANNEL_1, is_extended_id=True, timestamp=121334.365, data=[254, 255, 1, 2], ) workload = 1000 * [message] def sender(msg): self.bus1.send(msg) def receiver(_): return self.bus2.recv() sender_pool.map_async(sender, workload) for msg in receiver_pool.map(receiver, len(workload) * [None]): self.assertIsNotNone(msg) self.assertEqual(message.arbitration_id, msg.arbitration_id) self.assertTrue(message.equals(msg, timestamp_delta=None)) sender_pool.close() sender_pool.join() receiver_pool.close() receiver_pool.join() @pytest.mark.timeout(180.0 if IS_PYPY else 5.0) def test_filtered_bus(self): sender_pool = ThreadPool(100) receiver_pool = ThreadPool(100) included_message = can.Message( arbitration_id=0x123, channel=self.CHANNEL_1, is_extended_id=True, timestamp=121334.365, data=[254, 255, 1, 2], ) excluded_message = can.Message( arbitration_id=0x02, channel=self.CHANNEL_1, is_extended_id=True, timestamp=121334.300, data=[1, 2, 3], ) workload = 500 * [included_message] + 500 * [excluded_message] random.shuffle(workload) self.bus2.set_filters([{"can_id": 0x123, "can_mask": 0xFF, "extended": True}]) def sender(msg): self.bus1.send(msg) def receiver(_): return self.bus2.recv() sender_pool.map_async(sender, workload) received_msgs = receiver_pool.map(receiver, 500 * [None]) for msg in received_msgs: self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, included_message.arbitration_id) self.assertTrue(included_message.equals(msg, timestamp_delta=None)) self.assertEqual(len(received_msgs), 500) sender_pool.close() sender_pool.join() receiver_pool.close() receiver_pool.join() if __name__ == "__main__": unittest.main() python-can-4.5.0/test/config.py000066400000000000000000000033641472200326600164060ustar00rootroot00000000000000#!/usr/bin/env python """ This module contains various configuration for the tests. Some tests are skipped when run on a CI server because they are not reproducible, see for example #243 and #940. """ import platform from os import environ as environment def env(name: str) -> bool: return environment.get(name, "").lower() in ("yes", "true", "t", "1") # ############################## Continuous integration # see here for the environment variables that are set on the CI servers: # - https://docs.travis-ci.com/user/environment-variables/ # - https://docs.github.com/en/actions/reference/environment-variables#default-environment-variables IS_TRAVIS = env("TRAVIS") IS_GITHUB_ACTIONS = env("GITHUB_ACTIONS") IS_CI = IS_TRAVIS or IS_GITHUB_ACTIONS or env("CI") or env("CONTINUOUS_INTEGRATION") if IS_TRAVIS and IS_GITHUB_ACTIONS: raise OSError( f"only one of IS_TRAVIS ({IS_TRAVIS}) and IS_GITHUB_ACTIONS ({IS_GITHUB_ACTIONS}) may be True at the " "same time" ) # ############################## Platforms _sys = platform.system().lower() IS_WINDOWS = "windows" in _sys or ("win" in _sys and "darwin" not in _sys) IS_LINUX = "linux" in _sys IS_OSX = "darwin" in _sys IS_UNIX = IS_LINUX or IS_OSX del _sys if (IS_WINDOWS and IS_LINUX) or (IS_LINUX and IS_OSX) or (IS_WINDOWS and IS_OSX): raise OSError( f"only one of IS_WINDOWS ({IS_WINDOWS}), IS_LINUX ({IS_LINUX}) and IS_OSX ({IS_OSX}) " f'can be True at the same time (platform.system() == "{platform.system()}")' ) # ############################## Implementations IS_PYPY = platform.python_implementation() == "PyPy" # ############################## What tests to run TEST_CAN_FD = True TEST_INTERFACE_SOCKETCAN = IS_LINUX and env("TEST_SOCKETCAN") python-can-4.5.0/test/contextmanager_test.py000066400000000000000000000021711472200326600212120ustar00rootroot00000000000000#!/usr/bin/env python """ This module tests the context manager of Bus and Notifier classes """ import unittest import can class ContextManagerTest(unittest.TestCase): def setUp(self): data = [0, 1, 2, 3, 4, 5, 6, 7] self.msg_send = can.Message( is_extended_id=False, arbitration_id=0x100, data=data ) def test_open_buses(self): with can.Bus(interface="virtual") as bus_send, can.Bus( interface="virtual" ) as bus_recv: bus_send.send(self.msg_send) msg_recv = bus_recv.recv() # Receiving a frame with data should evaluate msg_recv to True 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: bus_send.send(self.msg_send) # Receiving a frame after bus has been closed should raise a CanException self.assertRaises(can.CanError, bus_recv.recv) self.assertRaises(can.CanError, bus_send.send, self.msg_send) if __name__ == "__main__": unittest.main() python-can-4.5.0/test/data/000077500000000000000000000000001472200326600154725ustar00rootroot00000000000000python-can-4.5.0/test/data/__init__.py000066400000000000000000000000261472200326600176010ustar00rootroot00000000000000#!/usr/bin/env python python-can-4.5.0/test/data/example_data.py000066400000000000000000000147111472200326600204740ustar00rootroot00000000000000#!/usr/bin/env python """ This module contains some example data, like messages of different types and example comments with different challenges. """ import random from operator import attrgetter from can import Message # make tests more reproducible random.seed(13339115) def sort_messages(messages): """ Sorts the given messages by timestamps (ascending). :param Iterable[can.Message] messages: a sequence of messages to sort :rtype: list """ return list(sorted(messages, key=attrgetter("timestamp"))) # some random number TEST_TIME = 1483389946.197 # List of messages of different types that can be used in tests TEST_MESSAGES_BASE = sort_messages( [ Message( # empty timestamp=1e-4, ), Message( # only data timestamp=2e-4, data=[0x00, 0x42], ), Message( # no data timestamp=3e-4, arbitration_id=0xAB, is_extended_id=False, ), Message( # no data timestamp=4e-4, arbitration_id=0x42, is_extended_id=True, ), Message( # no data timestamp=5e-4, arbitration_id=0xABCDEF, ), Message( # empty data timestamp=6e-4, data=[], ), Message( # empty data timestamp=7e-4, data=[0xFF, 0xFE, 0xFD], ), Message( # with channel as integer timestamp=8e-4, channel=0, ), Message( # with channel as integer timestamp=9e-4, channel=42, ), Message( # with channel as string timestamp=10e-4, channel="vcan0", ), Message( # with channel as string timestamp=11e-4, channel="awesome_channel", ), Message( arbitration_id=0xABCDEF, is_extended_id=True, timestamp=TEST_TIME, data=[1, 2, 3, 4, 5, 6, 7, 8], ), Message( arbitration_id=0x123, is_extended_id=False, timestamp=TEST_TIME + 42.42, data=[0xFF, 0xFF], ), Message( arbitration_id=0xDADADA, is_extended_id=True, timestamp=TEST_TIME + 0.165, data=[1, 2, 3, 4, 5, 6, 7, 8], ), Message( arbitration_id=0x123, is_extended_id=False, timestamp=TEST_TIME + 0.365, data=[254, 255], ), Message( arbitration_id=0x768, is_extended_id=False, timestamp=TEST_TIME + 3.165 ), ] ) TEST_MESSAGES_CAN_FD = sort_messages( [ Message(timestamp=12e-4, is_fd=True, data=range(64)), Message(timestamp=13e-4, is_fd=True, data=range(8)), Message(timestamp=14e-4, is_fd=True, data=range(8), bitrate_switch=True), Message(timestamp=15e-4, is_fd=True, data=range(8), error_state_indicator=True), ] ) TEST_MESSAGES_REMOTE_FRAMES = sort_messages( [ Message( arbitration_id=0xDADADA, is_extended_id=True, is_remote_frame=True, timestamp=TEST_TIME + 0.165, ), Message( arbitration_id=0x123, is_extended_id=False, is_remote_frame=True, timestamp=TEST_TIME + 0.365, ), Message( arbitration_id=0x768, is_extended_id=False, is_remote_frame=True, timestamp=TEST_TIME + 3.165, ), Message( arbitration_id=0xABCDEF, is_extended_id=True, is_remote_frame=True, timestamp=TEST_TIME + 7858.67, ), ] ) TEST_MESSAGES_ERROR_FRAMES = sort_messages( [ Message(is_error_frame=True), Message(is_error_frame=True, timestamp=TEST_TIME + 0.170), Message(is_error_frame=True, timestamp=TEST_TIME + 17.157), ] ) TEST_ALL_MESSAGES = sort_messages( TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES ) TEST_COMMENTS = [ "This is the first comment", "", # empty comment "This third comment contains some strange characters: 'ä\"§$%&/()=?__::_Öüßêè and ends here.", ( "This fourth comment is quite long! " "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. " "Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. " "Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi." ), ] def generate_message(arbitration_id): """ Generates a new message with the given ID, some random data and a non-extended ID. """ data = bytearray([random.randrange(0, 2**8 - 1) for _ in range(8)]) return Message( arbitration_id=arbitration_id, data=data, is_extended_id=False, timestamp=TEST_TIME, ) python-can-4.5.0/test/data/ip_link_list.json000066400000000000000000000040021472200326600210410ustar00rootroot00000000000000[ { "ifindex": 1, "ifname": "lo", "flags": [ "LOOPBACK", "UP", "LOWER_UP" ], "mtu": 65536, "qdisc": "noqueue", "operstate": "UNKNOWN", "linkmode": "DEFAULT", "group": "default", "txqlen": 1000, "link_type": "loopback", "address": "00:00:00:00:00:00", "broadcast": "00:00:00:00:00:00" }, { "ifindex": 2, "ifname": "eth0", "flags": [ "NO-CARRIER", "BROADCAST", "MULTICAST", "UP" ], "mtu": 1500, "qdisc": "fq_codel", "operstate": "DOWN", "linkmode": "DEFAULT", "group": "default", "txqlen": 1000, "link_type": "ether", "address": "11:22:33:44:55:66", "broadcast": "ff:ff:ff:ff:ff:ff" }, { "ifindex": 3, "ifname": "wlan0", "flags": [ "BROADCAST", "MULTICAST", "UP", "LOWER_UP" ], "mtu": 1500, "qdisc": "noqueue", "operstate": "UP", "linkmode": "DORMANT", "group": "default", "txqlen": 1000, "link_type": "ether", "address": "11:22:33:44:55:66", "broadcast": "ff:ff:ff:ff:ff:ff" }, { "ifindex": 48, "ifname": "vcan0", "flags": [ "NOARP", "UP", "LOWER_UP" ], "mtu": 72, "qdisc": "noqueue", "operstate": "UNKNOWN", "linkmode": "DEFAULT", "group": "default", "txqlen": 1000, "link_type": "can" }, { "ifindex": 50, "ifname": "mycustomCan123", "flags": [ "NOARP", "UP", "LOWER_UP" ], "mtu": 72, "qdisc": "noqueue", "operstate": "UNKNOWN", "linkmode": "DEFAULT", "group": "default", "txqlen": 1000, "link_type": "can" }, {} ]python-can-4.5.0/test/data/issue_1256.asc000066400000000000000000003721161472200326600200010ustar00rootroot00000000000000date Tue May 27 04:09:35.000 pm 2014 base hex timestamps absolute internal events logged // version 10.0.1 0.019968 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100 0.029964 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100 0.039943 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100 0.039977 1 11 Rx d 8 4A 28 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.049949 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100 0.059945 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100 0.059976 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.060015 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 0.069970 1 11 Rx d 8 84 29 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.070001 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100 0.079951 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100 0.089947 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100 0.099951 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100 0.099982 1 11 Rx d 8 BD 2A CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.109949 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100 0.109983 1 10 Rx d 8 10 27 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 0.110014 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.110032 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 0.110053 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100 0.129997 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100 0.130036 1 11 Rx d 8 F5 2B 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.139949 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100 0.149954 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100 0.159951 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100 0.159983 1 11 Rx d 8 2C 2D 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.160095 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 0.160132 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.169955 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100 0.180007 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100 0.189956 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100 0.189991 1 11 Rx d 8 62 2E 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.199956 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100 0.209970 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100 0.210004 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.210026 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 0.210084 1 10 Rx d 8 F3 28 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 0.219957 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100 0.219987 1 11 Rx d 8 95 2F 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.229990 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100 0.240004 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100 0.249954 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100 0.249988 1 11 Rx d 8 C7 30 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.259976 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100 0.260138 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 0.260170 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.269974 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100 0.279956 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100 0.279989 1 11 Rx d 8 F6 31 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.289959 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100 0.299963 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100 0.309957 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100 0.309989 1 11 Rx d 8 22 33 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.310012 1 10 Rx d 8 D5 2A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 0.310075 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18 0.310097 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 0.319936 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100 0.329956 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100 0.339973 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100 0.339993 1 11 Rx d 8 4B 34 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.349918 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100 0.359922 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100 0.359957 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 0.360032 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.360102 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100 0.360114 1 11 Rx d 8 71 35 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.380012 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100 0.389965 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100 0.399981 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100 0.400017 1 11 Rx d 8 93 36 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.409967 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100 0.410001 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.410024 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 0.410086 1 10 Rx d 8 B5 2C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 0.419955 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100 0.429968 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100 0.430001 1 11 Rx d 8 B2 37 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.439986 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100 0.440148 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100 0.459928 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100 0.459970 1 11 Rx d 8 CC 38 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.459991 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 0.460048 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.469963 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100 0.479988 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100 0.489926 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100 0.489959 1 11 Rx d 8 E2 39 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.499927 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100 0.509930 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100 0.509963 1 10 Rx d 8 91 2E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 0.510027 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 0.510057 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.519946 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100 0.519980 1 11 Rx d 8 F2 3A 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.529932 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100 0.540023 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100 0.549926 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100 0.549958 1 11 Rx d 8 FE 3B D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.559975 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100 0.560137 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.560200 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 0.560231 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100 0.579988 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100 0.580022 1 11 Rx d 8 05 3D DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.589973 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100 0.599972 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100 0.609972 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100 0.610004 1 11 Rx d 8 06 3E F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.610115 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 0.610151 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.610162 1 10 Rx d 8 69 30 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 0.610188 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100 0.629991 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100 0.639991 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100 0.640026 1 11 Rx d 8 01 3F F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.649975 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100 0.659977 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100 0.660010 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.660144 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 0.669992 1 11 Rx d 8 F6 3F F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.670076 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100 0.680000 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100 0.689982 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100 0.690146 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100 0.690159 1 11 Rx d 8 E5 40 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.709978 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100 0.710013 1 10 Rx d 8 3B 32 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 0.710079 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.710100 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 0.720010 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100 0.729980 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100 0.730013 1 11 Rx d 8 CD 41 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.739981 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100 0.749983 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100 0.759997 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100 0.760030 1 11 Rx d 8 AF 42 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.760052 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 0.760113 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.769999 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100 0.779998 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100 0.789982 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100 0.790003 1 11 Rx d 8 8A 43 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.799976 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100 0.809974 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100 0.810125 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.810139 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 0.810168 1 10 Rx d 8 07 34 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 0.819974 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100 0.820007 1 11 Rx d 8 5D 44 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.829974 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100 0.839975 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100 0.849974 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100 0.850006 1 11 Rx d 8 29 45 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.860028 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100 0.860065 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 0.860138 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18 0.860201 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100 0.880033 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100 0.880170 1 11 Rx d 8 EE 45 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.889961 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100 0.900008 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100 0.909943 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100 0.909976 1 11 Rx d 8 AA 46 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.910071 1 10 Rx d 8 CB 35 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 0.910101 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.910113 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 0.919950 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100 0.929950 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100 0.939955 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100 0.940093 1 11 Rx d 8 5F 47 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.940105 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100 0.959949 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100 0.959982 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 0.960053 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 0.969947 1 64 Rx d 4 5C 03 00 00 Length = 0 BitCount = 0 ID = 100 0.969981 1 11 Rx d 8 0B 48 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 0.979953 1 64 Rx d 4 64 03 00 00 Length = 0 BitCount = 0 ID = 100 0.990010 1 64 Rx d 4 6C 03 00 00 Length = 0 BitCount = 0 ID = 100 0.999991 1 64 Rx d 4 74 03 00 00 Length = 0 BitCount = 0 ID = 100 1.000156 1 11 Rx d 8 AF 48 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.009977 1 64 Rx d 4 7C 03 00 00 Length = 0 BitCount = 0 ID = 100 1.010014 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 1.010037 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 1.010107 1 10 Rx d 8 86 37 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 1.019975 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100 1.030073 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100 1.030111 1 11 Rx d 8 4B 49 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.040047 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100 1.050030 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100 1.060044 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100 1.060181 1 11 Rx d 8 DE 49 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.060194 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 1.060223 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 1.060283 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100 1.080019 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100 1.089998 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100 1.090034 1 11 Rx d 8 68 4A 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.100056 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100 1.110002 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100 1.110041 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 1.110098 1 10 Rx d 8 37 39 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 1.110132 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 1.110145 1 11 Rx d 8 EA 4A 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.110155 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100 1.130015 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100 1.140014 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100 1.150012 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100 1.150061 1 11 Rx d 8 62 4B D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.150160 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100 1.150173 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 1.150238 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 1.169969 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100 1.179968 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100 1.180014 1 11 Rx d 8 D1 4B DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.189955 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100 1.200037 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100 1.209998 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100 1.210021 1 11 Rx d 8 37 4C F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.210151 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 1.210196 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 1.210251 1 10 Rx d 8 DE 3A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 1.220057 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100 1.230061 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100 1.240062 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100 1.240099 1 11 Rx d 8 93 4C F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.250061 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100 1.260053 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100 1.260090 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 1.260113 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 1.270063 1 11 Rx d 8 E6 4C F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.270179 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100 1.270193 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100 1.289924 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100 1.300009 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100 1.300037 1 11 Rx d 8 2F 4D CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.309979 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100 1.310113 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 1.310183 1 10 Rx d 8 78 3C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 1.310217 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 1.310249 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100 1.329968 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100 1.330021 1 11 Rx d 8 6F 4D 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.339962 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100 1.349962 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100 1.359966 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100 1.359992 1 11 Rx d 8 A5 4D 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.360009 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 1.360074 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 1.370015 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100 1.380032 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100 1.390025 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100 1.390058 1 11 Rx d 8 D1 4D 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.400054 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100 1.400094 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100 1.400116 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 1.400176 1 10 Rx d 8 06 3E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 1.400209 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18 1.420035 1 11 Rx d 8 F4 4D 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.420068 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100 1.430012 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100 1.439963 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100 1.449976 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100 1.450009 1 11 Rx d 8 0C 4E D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.459963 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100 1.459986 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 1.460057 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 1.469973 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100 1.480036 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100 1.480071 1 11 Rx d 8 1B 4E DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.489976 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100 1.500035 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100 1.509978 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100 1.510010 1 11 Rx d 8 20 4E F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.510100 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 1.510131 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 1.510142 1 10 Rx d 8 86 3F 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 1.519982 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100 1.530039 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100 1.540022 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100 1.540051 1 11 Rx d 8 1B 4E F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.550046 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100 1.560021 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100 1.560057 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 1.560181 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 1.570020 1 11 Rx d 8 0C 4E F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.570138 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100 1.580022 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100 1.590043 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100 1.600024 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100 1.600055 1 11 Rx d 8 F4 4D CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.610019 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100 1.610179 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 1.610191 1 10 Rx d 8 F7 40 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 1.610217 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 1.620027 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100 1.630024 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100 1.630055 1 11 Rx d 8 D1 4D 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.640032 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100 1.650055 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100 1.650094 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100 1.650119 1 11 Rx d 8 A5 4D 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.650135 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 1.650190 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 1.670103 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100 1.680104 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100 1.690059 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100 1.690097 1 11 Rx d 8 6F 4D 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.690212 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100 1.710033 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100 1.710070 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 1.710139 1 10 Rx d 8 59 42 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 1.710189 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 1.720025 1 11 Rx d 8 2F 4D 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.720057 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100 1.730027 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100 1.730186 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100 1.750028 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100 1.750060 1 11 Rx d 8 E6 4C D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.760049 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100 1.760080 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 1.760095 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 1.770032 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100 1.780053 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100 1.780087 1 11 Rx d 8 93 4C DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.790032 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100 1.800013 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100 1.810033 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100 1.810068 1 11 Rx d 8 37 4C F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.810178 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 1.810211 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 1.810263 1 10 Rx d 8 AB 43 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 1.810292 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100 1.830028 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100 1.840037 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100 1.840069 1 11 Rx d 8 D1 4B F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.850034 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100 1.860036 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100 1.860070 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 1.860091 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 1.870040 1 11 Rx d 8 62 4B F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.870173 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100 1.880039 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100 1.890037 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100 1.900074 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100 1.900111 1 11 Rx d 8 EA 4A CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.900134 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100 1.900151 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 1.900246 1 10 Rx d 8 EB 44 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 1.900276 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 1.920089 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100 1.930090 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100 1.930127 1 11 Rx d 8 68 4A 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.940093 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100 1.940132 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100 1.960058 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100 1.960096 1 11 Rx d 8 DE 49 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 1.960118 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 1.960187 1 12 Rx d 4 01 01 00 00 Length = 0 BitCount = 0 ID = 18 1.970038 1 64 Rx d 4 5C 03 00 00 Length = 0 BitCount = 0 ID = 100 1.980044 1 64 Rx d 4 64 03 00 00 Length = 0 BitCount = 0 ID = 100 1.990002 1 64 Rx d 4 6C 03 00 00 Length = 0 BitCount = 0 ID = 100 1.990149 1 11 Rx d 8 4B 49 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.000000 1 64 Rx d 4 74 03 00 00 Length = 0 BitCount = 0 ID = 100 2.010062 1 64 Rx d 4 7C 03 00 00 Length = 0 BitCount = 0 ID = 100 2.010100 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 2.010143 1 10 Rx d 8 1A 46 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 2.010182 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.020003 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100 2.020035 1 11 Rx d 8 AF 48 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.030002 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100 2.040047 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100 2.050003 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100 2.050137 1 11 Rx d 8 0B 48 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.060035 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100 2.060072 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.060166 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 2.070114 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100 2.080034 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100 2.080071 1 11 Rx d 8 5F 47 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.090050 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100 2.100064 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100 2.110044 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100 2.110208 1 11 Rx d 8 AA 46 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.110260 1 10 Rx d 8 36 47 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 2.110289 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 2.110316 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.110327 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100 2.130053 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100 2.140028 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100 2.140060 1 11 Rx d 8 EE 45 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.150070 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100 2.150102 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100 2.150125 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.150231 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 2.170064 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100 2.170211 1 11 Rx d 8 29 45 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.180053 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100 2.190065 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100 2.200058 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100 2.200095 1 11 Rx d 8 5D 44 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.210053 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100 2.210089 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 2.210158 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.210227 1 10 Rx d 8 3F 48 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 2.220056 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100 2.230054 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100 2.230215 1 11 Rx d 8 8A 43 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.230267 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100 2.250051 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100 2.260053 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100 2.260089 1 11 Rx d 8 AF 42 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.260111 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.260127 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 2.270057 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100 2.280069 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100 2.290077 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100 2.290234 1 11 Rx d 8 CD 41 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.300073 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100 2.310053 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100 2.310096 1 10 Rx d 8 34 49 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 2.310167 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.310239 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 2.310268 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100 2.310280 1 11 Rx d 8 E5 40 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.330060 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100 2.340074 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100 2.350083 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100 2.350234 1 11 Rx d 8 F6 3F D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.350288 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100 2.350300 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 2.350330 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.370081 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100 2.380078 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100 2.380119 1 11 Rx d 8 01 3F DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.390063 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100 2.400062 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100 2.400097 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100 2.400120 1 11 Rx d 8 06 3E F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.400136 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.400236 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 2.400266 1 10 Rx d 8 14 4A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 2.420096 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100 2.430063 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100 2.440083 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100 2.440116 1 11 Rx d 8 05 3D F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.440230 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100 2.460066 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100 2.460101 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 2.460172 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.470079 1 11 Rx d 8 FE 3B F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.470111 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100 2.480072 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100 2.480232 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100 2.500067 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100 2.500100 1 11 Rx d 8 F2 3A CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.510082 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100 2.510117 1 10 Rx d 8 E0 4A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 2.510181 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 2.510237 1 12 Rx d 4 01 01 00 00 Length = 0 BitCount = 0 ID = 18 2.520174 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100 2.520211 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100 2.520234 1 11 Rx d 8 E2 39 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.540030 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100 2.550072 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100 2.560027 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100 2.560060 1 11 Rx d 8 CC 38 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.560141 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.560196 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 2.560227 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100 2.580070 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100 2.590075 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100 2.590111 1 11 Rx d 8 B2 37 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.600069 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100 2.610070 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100 2.610104 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 2.610168 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.610189 1 10 Rx d 8 96 4B 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 2.620075 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100 2.620108 1 11 Rx d 8 93 36 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.630077 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100 2.640065 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100 2.650071 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100 2.650103 1 11 Rx d 8 71 35 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.650124 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100 2.650141 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.650240 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 2.670078 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100 2.680091 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100 2.680126 1 11 Rx d 8 4B 34 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.690092 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100 2.690126 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100 2.710092 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100 2.710128 1 11 Rx d 8 22 33 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.710154 1 10 Rx d 8 37 4C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 2.710214 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 2.710259 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.720071 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100 2.730075 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100 2.730236 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100 2.730256 1 11 Rx d 8 F6 31 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.750078 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100 2.760095 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100 2.760128 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.760150 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 2.770101 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100 2.770139 1 11 Rx d 8 C7 30 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.770253 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100 2.790118 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100 2.800075 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100 2.800113 1 11 Rx d 8 95 2F CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.810084 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100 2.810116 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 2.810185 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.810272 1 10 Rx d 8 C1 4C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 2.810301 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100 2.830087 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100 2.830122 1 11 Rx d 8 62 2E 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.840105 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100 2.850098 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100 2.860076 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100 2.860114 1 11 Rx d 8 2C 2D 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.860136 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.860153 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 2.870100 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100 2.880131 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100 2.890145 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100 2.890182 1 11 Rx d 8 F5 2B 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.900088 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100 2.900125 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100 2.900150 1 10 Rx d 8 34 4D 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 2.900213 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.900283 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 2.920086 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100 2.920229 1 11 Rx d 8 BD 2A 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.930105 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100 2.940094 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100 2.940128 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100 2.940151 1 11 Rx d 8 84 29 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.960131 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100 2.960153 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 2.960199 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 2.970084 1 64 Rx d 4 5C 03 00 00 Length = 0 BitCount = 0 ID = 100 2.980091 1 64 Rx d 4 64 03 00 00 Length = 0 BitCount = 0 ID = 100 2.980263 1 11 Rx d 8 4A 28 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 2.980317 1 64 Rx d 4 6C 03 00 00 Length = 0 BitCount = 0 ID = 100 3.000106 1 64 Rx d 4 74 03 00 00 Length = 0 BitCount = 0 ID = 100 3.010158 1 64 Rx d 4 7C 03 00 00 Length = 0 BitCount = 0 ID = 100 3.010195 1 11 Rx d 8 10 27 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.010218 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.010234 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 3.010278 1 10 Rx d 8 91 4D 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 3.020129 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100 3.030126 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100 3.040128 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100 3.040267 1 11 Rx d 8 D6 25 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.050098 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100 3.060086 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100 3.060119 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 3.060193 1 12 Rx d 4 01 01 00 00 Length = 0 BitCount = 0 ID = 18 3.060269 1 11 Rx d 8 9C 24 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.060280 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100 3.080052 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100 3.090013 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100 3.100011 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100 3.100139 1 11 Rx d 8 63 23 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.110014 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100 3.110044 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 3.110104 1 10 Rx d 8 D7 4D 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 3.110134 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.120011 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100 3.130110 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100 3.130139 1 11 Rx d 8 2B 22 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.140015 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100 3.150013 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100 3.160153 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100 3.160290 1 11 Rx d 8 F4 20 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.160342 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.160395 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 3.170068 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100 3.180094 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100 3.190101 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100 3.190134 1 11 Rx d 8 BE 1F 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.190157 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100 3.210102 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100 3.210135 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.210157 1 10 Rx d 8 06 4E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 3.210223 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 3.220098 1 11 Rx d 8 8B 1E 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.220219 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100 3.230103 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100 3.230137 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100 3.250083 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100 3.250118 1 11 Rx d 8 59 1D D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.260090 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100 3.260122 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 3.260194 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.270103 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100 3.280074 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100 3.280241 1 11 Rx d 8 2A 1C DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.290127 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100 3.300105 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100 3.310145 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100 3.310178 1 11 Rx d 8 FE 1A F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.310201 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.310217 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 3.310282 1 10 Rx d 8 1D 4E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 3.310326 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100 3.330117 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100 3.340106 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100 3.340272 1 11 Rx d 8 D5 19 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.350133 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100 3.360104 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100 3.360136 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 3.360206 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.370111 1 11 Rx d 8 AF 18 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.370143 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100 3.380073 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100 3.390099 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100 3.400128 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100 3.400301 1 11 Rx d 8 8D 17 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.400355 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100 3.400367 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 3.400394 1 10 Rx d 8 1D 4E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 3.400422 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.420127 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100 3.430111 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100 3.430143 1 11 Rx d 8 6E 16 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.440133 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100 3.440164 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100 3.460133 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100 3.460291 1 11 Rx d 8 54 15 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.460343 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.460396 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 3.470130 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100 3.480128 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100 3.480160 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100 3.480184 1 11 Rx d 8 3E 14 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.500107 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100 3.510123 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100 3.510160 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.510183 1 10 Rx d 8 06 4E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 3.510246 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 3.520133 1 11 Rx d 8 2E 13 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.520256 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100 3.520336 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100 3.540135 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100 3.550123 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100 3.550154 1 11 Rx d 8 22 12 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.560132 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100 3.560163 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 3.560234 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.560312 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100 3.580132 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100 3.580289 1 11 Rx d 8 1B 11 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.590114 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100 3.600140 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100 3.610137 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100 3.610168 1 11 Rx d 8 1A 10 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.610191 1 12 Rx d 4 01 01 00 00 Length = 0 BitCount = 0 ID = 18 3.610209 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 3.610267 1 10 Rx d 8 D7 4D 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 3.620042 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100 3.630075 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100 3.640082 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100 3.640213 1 11 Rx d 8 1F 0F F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.650082 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100 3.650118 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100 3.650142 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 3.650202 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.670166 1 11 Rx d 8 2A 0E F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.670202 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100 3.680120 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100 3.690128 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100 3.690160 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100 3.690184 1 11 Rx d 8 3B 0D CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.710127 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100 3.710271 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 3.710304 1 10 Rx d 8 91 4D 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 3.710332 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.720163 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100 3.730129 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100 3.730166 1 11 Rx d 8 53 0C 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.730283 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100 3.750126 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100 3.760173 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100 3.760210 1 11 Rx d 8 71 0B 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.760233 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.760249 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 3.770116 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100 3.770268 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100 3.790150 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100 3.790184 1 11 Rx d 8 96 0A 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.800152 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100 3.810126 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100 3.810151 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.810225 1 10 Rx d 8 34 4D 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 3.810258 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 3.810288 1 11 Rx d 8 C3 09 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.810300 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100 3.830121 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100 3.840131 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100 3.850150 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100 3.850182 1 11 Rx d 8 F7 08 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.850304 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100 3.850318 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 3.850348 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.870134 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100 3.880155 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100 3.880187 1 11 Rx d 8 32 08 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.890156 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100 3.890314 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100 3.900151 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100 3.900184 1 11 Rx d 8 76 07 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.900206 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.900308 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 3.900337 1 10 Rx d 8 C1 4C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 3.920150 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100 3.930141 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100 3.940146 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100 3.940177 1 11 Rx d 8 C1 06 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.940289 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100 3.960155 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100 3.960321 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 3.960353 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 3.970143 1 11 Rx d 8 15 06 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 3.970175 1 64 Rx d 4 5C 03 00 00 Length = 0 BitCount = 0 ID = 100 3.980155 1 64 Rx d 4 64 03 00 00 Length = 0 BitCount = 0 ID = 100 3.980187 1 64 Rx d 4 6C 03 00 00 Length = 0 BitCount = 0 ID = 100 4.000161 1 64 Rx d 4 74 03 00 00 Length = 0 BitCount = 0 ID = 100 4.000193 1 11 Rx d 8 71 05 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.010222 1 64 Rx d 4 7C 03 00 00 Length = 0 BitCount = 0 ID = 100 4.010262 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 4.010319 1 10 Rx d 8 37 4C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 4.010350 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 4.020171 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100 4.020337 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100 4.020350 1 11 Rx d 8 D5 04 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.040172 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100 4.050149 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100 4.060159 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100 4.060196 1 11 Rx d 8 42 04 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.060314 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 4.060367 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 4.060397 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100 4.080148 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100 4.090164 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100 4.090198 1 11 Rx d 8 B8 03 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.100207 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100 4.110147 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100 4.110185 1 10 Rx d 8 96 4B 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 4.110267 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 4.110291 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 4.120175 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100 4.120209 1 11 Rx d 8 36 03 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.130179 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100 4.140158 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100 4.140314 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100 4.140327 1 11 Rx d 8 BE 02 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.150166 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100 4.150201 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 4.150271 1 12 Rx d 4 01 01 00 00 Length = 0 BitCount = 0 ID = 18 4.170105 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100 4.180168 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100 4.180203 1 11 Rx d 8 4F 02 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.190075 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100 4.190107 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100 4.210114 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100 4.210252 1 11 Rx d 8 E9 01 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.210265 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 4.210307 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 4.210336 1 10 Rx d 8 E0 4A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 4.220109 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100 4.230170 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100 4.230205 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100 4.230222 1 11 Rx d 8 8D 01 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.250120 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100 4.260111 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100 4.260144 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 4.260213 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 4.270185 1 11 Rx d 8 3A 01 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.270317 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100 4.270392 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100 4.290125 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100 4.300176 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100 4.300208 1 11 Rx d 8 F1 00 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.310134 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100 4.310165 1 10 Rx d 8 14 4A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 4.310231 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 4.310286 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 4.310347 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100 4.330186 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100 4.330343 1 11 Rx d 8 B1 00 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.340151 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100 4.350181 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100 4.360178 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100 4.360210 1 11 Rx d 8 7B 00 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.360232 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 4.360249 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 4.370158 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100 4.380181 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100 4.390169 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100 4.390287 1 11 Rx d 8 4F 00 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.390342 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100 4.400204 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100 4.400241 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 4.400310 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 4.400388 1 10 Rx d 8 34 49 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 4.420206 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100 4.420243 1 11 Rx d 8 2C 00 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.430163 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100 4.440152 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100 4.440184 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100 4.440208 1 11 Rx d 8 14 00 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.460180 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100 4.460345 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 4.460388 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 4.470164 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100 4.480182 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100 4.480216 1 11 Rx d 8 05 00 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.480327 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100 4.500107 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100 4.510152 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100 4.510187 1 11 Rx d 8 00 00 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.510205 1 10 Rx d 8 3F 48 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 4.510259 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 4.510290 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 4.520188 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100 4.520347 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100 4.540146 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100 4.540175 1 11 Rx d 8 05 00 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.550167 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100 4.560189 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100 4.560224 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 4.560343 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 4.560373 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100 4.560385 1 11 Rx d 8 14 00 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.580185 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100 4.590144 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100 4.600191 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100 4.600219 1 11 Rx d 8 2C 00 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.600293 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100 4.600307 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 4.600335 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 4.600346 1 10 Rx d 8 36 47 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 4.620191 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100 4.630170 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100 4.630202 1 11 Rx d 8 4F 00 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.640194 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100 4.640338 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100 4.650180 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100 4.650215 1 11 Rx d 8 7B 00 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.650237 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 4.650349 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 4.670204 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100 4.680196 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100 4.690196 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100 4.690216 1 11 Rx d 8 B1 00 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.690288 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100 4.710150 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100 4.710187 1 10 Rx d 8 1A 46 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 4.710208 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18 4.710224 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 4.720179 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100 4.720293 1 11 Rx d 8 F1 00 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.730133 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100 4.730167 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100 4.750198 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100 4.750232 1 11 Rx d 8 3A 01 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.760140 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100 4.760173 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 4.760240 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 4.770140 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100 4.770174 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100 4.770197 1 11 Rx d 8 8D 01 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.790183 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100 4.800136 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100 4.810198 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100 4.810231 1 11 Rx d 8 E9 01 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.810306 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 4.810320 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 4.810345 1 10 Rx d 8 EB 44 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 4.810374 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100 4.830181 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100 4.840222 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100 4.840254 1 11 Rx d 8 4F 02 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.850262 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100 4.850399 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100 4.850413 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 4.850442 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 4.870188 1 11 Rx d 8 BE 02 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.870319 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100 4.880199 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100 4.890186 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100 4.900200 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100 4.900232 1 11 Rx d 8 36 03 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.900255 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100 4.900274 1 10 Rx d 8 AB 43 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 4.900333 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 4.900379 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 4.920200 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100 4.930189 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100 4.930226 1 11 Rx d 8 B8 03 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.930343 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100 4.940195 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100 4.960198 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100 4.960231 1 11 Rx d 8 42 04 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 4.960253 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 4.960364 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 4.970188 1 64 Rx d 4 5C 03 00 00 Length = 0 BitCount = 0 ID = 100 4.980200 1 64 Rx d 4 64 03 00 00 Length = 0 BitCount = 0 ID = 100 4.980360 1 64 Rx d 4 6C 03 00 00 Length = 0 BitCount = 0 ID = 100 4.980377 1 11 Rx d 8 D5 04 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.000197 1 64 Rx d 4 74 03 00 00 Length = 0 BitCount = 0 ID = 100 5.010190 1 64 Rx d 4 7C 03 00 00 Length = 0 BitCount = 0 ID = 100 5.010223 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 5.010289 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.010310 1 10 Rx d 8 59 42 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 5.020239 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100 5.020276 1 11 Rx d 8 71 05 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.020367 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100 5.040200 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100 5.050214 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100 5.050249 1 11 Rx d 8 15 06 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.060174 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100 5.060206 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.060338 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 5.060374 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100 5.080201 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100 5.080233 1 11 Rx d 8 C1 06 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.090209 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100 5.100201 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100 5.100357 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100 5.100370 1 11 Rx d 8 76 07 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.100377 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 5.100404 1 10 Rx d 8 F7 40 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 5.100433 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.120199 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100 5.130213 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100 5.140201 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100 5.140232 1 11 Rx d 8 32 08 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.140341 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100 5.150214 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100 5.150251 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.150351 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 5.170256 1 11 Rx d 8 F7 08 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.170292 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100 5.180234 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100 5.190222 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100 5.190259 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100 5.190284 1 11 Rx d 8 C3 09 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.210201 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100 5.210237 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.210344 1 10 Rx d 8 86 3F 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 5.210373 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 5.220196 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100 5.230215 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100 5.230373 1 11 Rx d 8 96 0A 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.230426 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100 5.250211 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100 5.260116 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100 5.260139 1 11 Rx d 8 71 0B 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.260154 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 5.260198 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18 5.270165 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100 5.270200 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100 5.290162 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100 5.290299 1 11 Rx d 8 53 0C 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.300164 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100 5.310223 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100 5.310257 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 5.310301 1 10 Rx d 8 06 3E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 5.310340 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.310404 1 11 Rx d 8 3B 0D 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.310415 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100 5.330168 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100 5.340163 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100 5.350165 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100 5.350300 1 11 Rx d 8 2A 0E D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.350353 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100 5.350365 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.350373 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 5.370212 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100 5.380203 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100 5.380237 1 11 Rx d 8 1F 0F DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.390213 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100 5.400203 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100 5.400242 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100 5.400266 1 11 Rx d 8 1A 10 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.400283 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 5.400347 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.400410 1 10 Rx d 8 78 3C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 5.420202 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100 5.430213 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100 5.440203 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100 5.440238 1 11 Rx d 8 1B 11 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.440352 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100 5.460203 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100 5.460238 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.460344 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 5.470230 1 11 Rx d 8 22 12 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.470257 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100 5.480180 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100 5.480355 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100 5.500216 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100 5.500252 1 11 Rx d 8 2E 13 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.510235 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100 5.510269 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.510291 1 10 Rx d 8 DE 3A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 5.510349 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 5.510403 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100 5.520203 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100 5.520240 1 11 Rx d 8 3E 14 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.540232 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100 5.550219 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100 5.560240 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100 5.560273 1 11 Rx d 8 54 15 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.560385 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 5.560417 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.560477 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100 5.580241 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100 5.590253 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100 5.590290 1 11 Rx d 8 6E 16 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.600261 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100 5.600398 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100 5.600412 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 5.600439 1 10 Rx d 8 37 39 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 5.600467 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.620237 1 11 Rx d 8 8D 17 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.620368 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100 5.630250 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100 5.640286 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100 5.640323 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100 5.640346 1 11 Rx d 8 AF 18 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.650275 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100 5.650312 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.650411 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 5.670301 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100 5.680244 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100 5.680281 1 11 Rx d 8 D5 19 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.680398 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100 5.690241 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100 5.710227 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100 5.710266 1 11 Rx d 8 FE 1A F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.710289 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 5.710355 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.710408 1 10 Rx d 8 86 37 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 5.720247 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100 5.730242 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100 5.730400 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100 5.730413 1 11 Rx d 8 2A 1C F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.750231 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100 5.760248 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100 5.760281 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.760303 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 5.770228 1 11 Rx d 8 59 1D F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.770350 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100 5.770373 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100 5.790239 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100 5.800150 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100 5.800188 1 11 Rx d 8 8B 1E CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.810189 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100 5.810224 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18 5.810337 1 10 Rx d 8 CB 35 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 5.810367 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 5.810396 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100 5.830234 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100 5.830270 1 11 Rx d 8 BE 1F 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.840299 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100 5.850194 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100 5.850341 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100 5.850354 1 11 Rx d 8 F4 20 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.850362 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 5.850392 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.870185 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100 5.880196 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100 5.890274 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100 5.890311 1 11 Rx d 8 2B 22 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.890425 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100 5.900288 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100 5.900326 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 5.900394 1 10 Rx d 8 07 34 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 5.900429 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.920258 1 11 Rx d 8 63 23 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.920295 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100 5.930234 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100 5.940252 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100 5.940284 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100 5.940307 1 11 Rx d 8 9C 24 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.960253 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100 5.960285 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 5.960389 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 5.970257 1 64 Rx d 4 5C 03 00 00 Length = 0 BitCount = 0 ID = 100 5.980257 1 64 Rx d 4 64 03 00 00 Length = 0 BitCount = 0 ID = 100 5.980424 1 11 Rx d 8 D6 25 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 5.980476 1 64 Rx d 4 6C 03 00 00 Length = 0 BitCount = 0 ID = 100 6.000260 1 64 Rx d 4 74 03 00 00 Length = 0 BitCount = 0 ID = 100 6.010259 1 64 Rx d 4 7C 03 00 00 Length = 0 BitCount = 0 ID = 100 6.010291 1 11 Rx d 8 10 27 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.010316 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 6.010375 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.010396 1 10 Rx d 8 3B 32 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 6.010437 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100 6.020264 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100 6.040254 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100 6.040431 1 11 Rx d 8 4A 28 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.050267 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100 6.060287 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100 6.060329 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.060459 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 6.060490 1 11 Rx d 8 84 29 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.060501 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100 6.080264 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100 6.090264 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100 6.100264 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100 6.100426 1 11 Rx d 8 BD 2A CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.100478 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100 6.100490 1 10 Rx d 8 69 30 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 6.100516 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.100527 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 6.120259 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100 6.130240 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100 6.130278 1 11 Rx d 8 F5 2B 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.140262 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100 6.140294 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100 6.150266 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100 6.150298 1 11 Rx d 8 2C 2D 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.150320 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 6.150390 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.170276 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100 6.180295 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100 6.180332 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100 6.180356 1 11 Rx d 8 62 2E 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.190293 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100 6.210248 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100 6.210286 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.210397 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 6.210437 1 10 Rx d 8 91 2E 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 6.220267 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100 6.220301 1 11 Rx d 8 95 2F 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.230228 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100 6.230392 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100 6.250275 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100 6.250312 1 11 Rx d 8 C7 30 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.260277 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100 6.260314 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 6.260390 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.260402 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100 6.270300 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100 6.270338 1 11 Rx d 8 F6 31 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.290288 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100 6.300282 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100 6.310272 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100 6.310305 1 11 Rx d 8 22 33 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.310417 1 10 Rx d 8 B5 2C 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 6.310449 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.310511 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 6.310539 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100 6.330273 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100 6.340288 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100 6.340319 1 11 Rx d 8 4B 34 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.350201 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100 6.350238 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100 6.350258 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 6.350266 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18 6.370218 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100 6.370357 1 11 Rx d 8 71 35 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.380217 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100 6.390218 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100 6.400189 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100 6.400212 1 11 Rx d 8 93 36 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.400228 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100 6.400243 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.400303 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 6.400334 1 10 Rx d 8 D5 2A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 6.420281 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100 6.430259 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100 6.430419 1 11 Rx d 8 B2 37 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.430471 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100 6.440281 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100 6.460278 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100 6.460316 1 11 Rx d 8 CC 38 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.460339 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 6.460410 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.470284 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100 6.480284 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100 6.480317 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100 6.480340 1 11 Rx d 8 E2 39 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.500285 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100 6.510262 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100 6.510296 1 10 Rx d 8 F3 28 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 6.510359 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 6.510415 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.510427 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100 6.510436 1 11 Rx d 8 F2 3A 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.520278 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100 6.540273 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100 6.550261 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100 6.550299 1 11 Rx d 8 FE 3B D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.560258 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100 6.560405 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.560469 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 6.560500 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100 6.580285 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100 6.580323 1 11 Rx d 8 05 3D DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.590290 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100 6.600283 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100 6.600320 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100 6.600343 1 11 Rx d 8 06 3E F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.600359 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 6.600416 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.600427 1 10 Rx d 8 10 27 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 6.620260 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100 6.630273 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100 6.640290 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100 6.640326 1 11 Rx d 8 01 3F F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.640439 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100 6.650286 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100 6.650319 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.650449 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 6.670354 1 11 Rx d 8 F6 3F F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.670389 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100 6.680295 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100 6.680459 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100 6.690274 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100 6.690306 1 11 Rx d 8 E5 40 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.710273 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100 6.710311 1 10 Rx d 8 2D 25 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 6.710380 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.710451 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 6.720295 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100 6.730276 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100 6.730308 1 11 Rx d 8 CD 41 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.730420 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100 6.750294 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100 6.760296 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100 6.760330 1 11 Rx d 8 AF 42 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.760352 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 6.760414 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.760434 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100 6.770281 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100 6.790280 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100 6.790318 1 11 Rx d 8 8A 43 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.800301 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100 6.810282 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100 6.810447 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.810510 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 6.810539 1 10 Rx d 8 4B 23 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 6.810567 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100 6.810579 1 11 Rx d 8 5D 44 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.830296 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100 6.840298 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100 6.850278 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100 6.850309 1 11 Rx d 8 29 45 D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.850404 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100 6.850417 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 6.850450 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.870283 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100 6.870861 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100 6.870893 1 11 Rx d 8 EE 45 DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.890260 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100 6.890295 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100 6.900196 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100 6.900215 1 11 Rx d 8 AA 46 F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.900225 1 10 Rx d 8 6B 21 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 6.900260 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18 6.900326 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 6.920201 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100 6.930203 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100 6.930351 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100 6.930363 1 11 Rx d 8 5F 47 F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.940201 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100 6.960203 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100 6.960231 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 6.960293 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 6.970202 1 64 Rx d 4 5C 03 00 00 Length = 0 BitCount = 0 ID = 100 6.970229 1 11 Rx d 8 0B 48 F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 6.980204 1 64 Rx d 4 64 03 00 00 Length = 0 BitCount = 0 ID = 100 6.980229 1 64 Rx d 4 6C 03 00 00 Length = 0 BitCount = 0 ID = 100 7.000335 1 64 Rx d 4 74 03 00 00 Length = 0 BitCount = 0 ID = 100 7.000507 1 11 Rx d 8 AF 48 CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.010287 1 64 Rx d 4 7C 03 00 00 Length = 0 BitCount = 0 ID = 100 7.010320 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 7.010343 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 7.010400 1 10 Rx d 8 8F 1F 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 7.010457 1 64 Rx d 4 64 00 00 00 Length = 0 BitCount = 0 ID = 100 7.020304 1 64 Rx d 4 6C 00 00 00 Length = 0 BitCount = 0 ID = 100 7.020339 1 11 Rx d 8 4B 49 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.040311 1 64 Rx d 4 74 00 00 00 Length = 0 BitCount = 0 ID = 100 7.050320 1 64 Rx d 4 7C 00 00 00 Length = 0 BitCount = 0 ID = 100 7.060309 1 64 Rx d 4 84 00 00 00 Length = 0 BitCount = 0 ID = 100 7.060477 1 11 Rx d 8 DE 49 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.060529 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 7.060560 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 7.060620 1 64 Rx d 4 8C 00 00 00 Length = 0 BitCount = 0 ID = 100 7.080310 1 64 Rx d 4 94 00 00 00 Length = 0 BitCount = 0 ID = 100 7.090297 1 64 Rx d 4 9C 00 00 00 Length = 0 BitCount = 0 ID = 100 7.090329 1 11 Rx d 8 68 4A 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.100292 1 64 Rx d 4 A4 00 00 00 Length = 0 BitCount = 0 ID = 100 7.100312 1 64 Rx d 4 AC 00 00 00 Length = 0 BitCount = 0 ID = 100 7.100328 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 7.100387 1 10 Rx d 8 B7 1D 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 7.100419 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 7.120312 1 11 Rx d 8 EA 4A 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.120444 1 64 Rx d 4 B4 00 00 00 Length = 0 BitCount = 0 ID = 100 7.130326 1 64 Rx d 4 BC 00 00 00 Length = 0 BitCount = 0 ID = 100 7.140333 1 64 Rx d 4 C4 00 00 00 Length = 0 BitCount = 0 ID = 100 7.140370 1 64 Rx d 4 CC 00 00 00 Length = 0 BitCount = 0 ID = 100 7.140394 1 11 Rx d 8 62 4B D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.150338 1 64 Rx d 4 D4 00 00 00 Length = 0 BitCount = 0 ID = 100 7.150376 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 7.150480 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 7.170318 1 64 Rx d 4 DC 00 00 00 Length = 0 BitCount = 0 ID = 100 7.180302 1 64 Rx d 4 E4 00 00 00 Length = 0 BitCount = 0 ID = 100 7.180442 1 11 Rx d 8 D1 4B DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.180498 1 64 Rx d 4 EC 00 00 00 Length = 0 BitCount = 0 ID = 100 7.190295 1 64 Rx d 4 F4 00 00 00 Length = 0 BitCount = 0 ID = 100 7.210313 1 64 Rx d 4 FC 00 00 00 Length = 0 BitCount = 0 ID = 100 7.210347 1 11 Rx d 8 37 4C F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.210370 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 7.210430 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 7.210497 1 10 Rx d 8 E5 1B 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 7.220304 1 64 Rx d 4 04 01 00 00 Length = 0 BitCount = 0 ID = 100 7.230303 1 64 Rx d 4 0C 01 00 00 Length = 0 BitCount = 0 ID = 100 7.230336 1 64 Rx d 4 14 01 00 00 Length = 0 BitCount = 0 ID = 100 7.230360 1 11 Rx d 8 93 4C F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.250319 1 64 Rx d 4 1C 01 00 00 Length = 0 BitCount = 0 ID = 100 7.260301 1 64 Rx d 4 24 01 00 00 Length = 0 BitCount = 0 ID = 100 7.260335 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 7.260357 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 7.260418 1 11 Rx d 8 E6 4C F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.260497 1 64 Rx d 4 2C 01 00 00 Length = 0 BitCount = 0 ID = 100 7.270301 1 64 Rx d 4 34 01 00 00 Length = 0 BitCount = 0 ID = 100 7.290306 1 64 Rx d 4 3C 01 00 00 Length = 0 BitCount = 0 ID = 100 7.300306 1 64 Rx d 4 44 01 00 00 Length = 0 BitCount = 0 ID = 100 7.300341 1 11 Rx d 8 2F 4D CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.310325 1 64 Rx d 4 4C 01 00 00 Length = 0 BitCount = 0 ID = 100 7.310486 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 7.310548 1 10 Rx d 8 19 1A 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 7.310577 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 7.310605 1 64 Rx d 4 54 01 00 00 Length = 0 BitCount = 0 ID = 100 7.330337 1 64 Rx d 4 5C 01 00 00 Length = 0 BitCount = 0 ID = 100 7.330375 1 11 Rx d 8 6F 4D 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.340304 1 64 Rx d 4 64 01 00 00 Length = 0 BitCount = 0 ID = 100 7.350330 1 64 Rx d 4 6C 01 00 00 Length = 0 BitCount = 0 ID = 100 7.350362 1 64 Rx d 4 74 01 00 00 Length = 0 BitCount = 0 ID = 100 7.350386 1 11 Rx d 8 A5 4D 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.350404 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 7.350467 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 7.370323 1 64 Rx d 4 7C 01 00 00 Length = 0 BitCount = 0 ID = 100 7.380343 1 64 Rx d 4 84 01 00 00 Length = 0 BitCount = 0 ID = 100 7.390331 1 64 Rx d 4 8C 01 00 00 Length = 0 BitCount = 0 ID = 100 7.390368 1 11 Rx d 8 D1 4D 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.390484 1 64 Rx d 4 94 01 00 00 Length = 0 BitCount = 0 ID = 100 7.400313 1 64 Rx d 4 9C 01 00 00 Length = 0 BitCount = 0 ID = 100 7.400345 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 7.400406 1 10 Rx d 8 55 18 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 7.400462 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 7.420308 1 11 Rx d 8 F4 4D 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.420344 1 64 Rx d 4 A4 01 00 00 Length = 0 BitCount = 0 ID = 100 7.430307 1 64 Rx d 4 AC 01 00 00 Length = 0 BitCount = 0 ID = 100 7.430471 1 64 Rx d 4 B4 01 00 00 Length = 0 BitCount = 0 ID = 100 7.440263 1 64 Rx d 4 BC 01 00 00 Length = 0 BitCount = 0 ID = 100 7.440286 1 11 Rx d 8 0C 4E D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.460271 1 64 Rx d 4 C4 01 00 00 Length = 0 BitCount = 0 ID = 100 7.460305 1 12 Rx d 4 01 00 00 00 Length = 0 BitCount = 0 ID = 18 7.460395 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 7.470270 1 64 Rx d 4 CC 01 00 00 Length = 0 BitCount = 0 ID = 100 7.480274 1 64 Rx d 4 D4 01 00 00 Length = 0 BitCount = 0 ID = 100 7.480307 1 11 Rx d 8 1B 4E DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.480398 1 64 Rx d 4 DC 01 00 00 Length = 0 BitCount = 0 ID = 100 7.500279 1 64 Rx d 4 E4 01 00 00 Length = 0 BitCount = 0 ID = 100 7.510321 1 64 Rx d 4 EC 01 00 00 Length = 0 BitCount = 0 ID = 100 7.510356 1 11 Rx d 8 20 4E F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.510371 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 7.510413 1 12 Rx d 4 00 00 00 00 Length = 0 BitCount = 0 ID = 18 7.510426 1 10 Rx d 8 9A 16 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 7.510455 1 64 Rx d 4 F4 01 00 00 Length = 0 BitCount = 0 ID = 100 7.520273 1 64 Rx d 4 FC 01 00 00 Length = 0 BitCount = 0 ID = 100 7.540273 1 64 Rx d 4 04 02 00 00 Length = 0 BitCount = 0 ID = 100 7.540306 1 11 Rx d 8 1B 4E F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.550279 1 64 Rx d 4 0C 02 00 00 Length = 0 BitCount = 0 ID = 100 7.560279 1 64 Rx d 4 14 02 00 00 Length = 0 BitCount = 0 ID = 100 7.560427 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 7.560491 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 7.560522 1 11 Rx d 8 0C 4E F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.560534 1 64 Rx d 4 1C 02 00 00 Length = 0 BitCount = 0 ID = 100 7.580317 1 64 Rx d 4 24 02 00 00 Length = 0 BitCount = 0 ID = 100 7.590327 1 64 Rx d 4 2C 02 00 00 Length = 0 BitCount = 0 ID = 100 7.600338 1 64 Rx d 4 34 02 00 00 Length = 0 BitCount = 0 ID = 100 7.600371 1 11 Rx d 8 F4 4D CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.600492 1 64 Rx d 4 3C 02 00 00 Length = 0 BitCount = 0 ID = 100 7.600510 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 7.600518 1 10 Rx d 8 E9 14 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 7.600546 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 7.620313 1 64 Rx d 4 44 02 00 00 Length = 0 BitCount = 0 ID = 100 7.630335 1 64 Rx d 4 4C 02 00 00 Length = 0 BitCount = 0 ID = 100 7.630369 1 11 Rx d 8 D1 4D 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.640340 1 64 Rx d 4 54 02 00 00 Length = 0 BitCount = 0 ID = 100 7.640372 1 64 Rx d 4 5C 02 00 00 Length = 0 BitCount = 0 ID = 100 7.650332 1 64 Rx d 4 64 02 00 00 Length = 0 BitCount = 0 ID = 100 7.650364 1 11 Rx d 8 A5 4D 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.650386 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 7.650450 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 7.670400 1 64 Rx d 4 6C 02 00 00 Length = 0 BitCount = 0 ID = 100 7.680343 1 64 Rx d 4 74 02 00 00 Length = 0 BitCount = 0 ID = 100 7.680511 1 64 Rx d 4 7C 02 00 00 Length = 0 BitCount = 0 ID = 100 7.680525 1 11 Rx d 8 6F 4D 64 16 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.690328 1 64 Rx d 4 84 02 00 00 Length = 0 BitCount = 0 ID = 100 7.710318 1 64 Rx d 4 8C 02 00 00 Length = 0 BitCount = 0 ID = 100 7.710351 1 65 Rx d 3 01 00 00 Length = 0 BitCount = 0 ID = 101 7.710419 1 10 Rx d 8 42 13 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 7.710479 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 7.720345 1 11 Rx d 8 2F 4D 99 05 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.720376 1 64 Rx d 4 94 02 00 00 Length = 0 BitCount = 0 ID = 100 7.720401 1 64 Rx d 4 9C 02 00 00 Length = 0 BitCount = 0 ID = 100 7.730346 1 64 Rx d 4 A4 02 00 00 Length = 0 BitCount = 0 ID = 100 7.750372 1 64 Rx d 4 AC 02 00 00 Length = 0 BitCount = 0 ID = 100 7.750536 1 11 Rx d 8 E6 4C D7 18 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.760370 1 64 Rx d 4 B4 02 00 00 Length = 0 BitCount = 0 ID = 100 7.760408 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 7.760431 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 7.760479 1 64 Rx d 4 BC 02 00 00 Length = 0 BitCount = 0 ID = 100 7.770375 1 64 Rx d 4 C4 02 00 00 Length = 0 BitCount = 0 ID = 100 7.770413 1 11 Rx d 8 93 4C DD 2C 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.790333 1 64 Rx d 4 CC 02 00 00 Length = 0 BitCount = 0 ID = 100 7.800344 1 64 Rx d 4 D4 02 00 00 Length = 0 BitCount = 0 ID = 100 7.810334 1 64 Rx d 4 DC 02 00 00 Length = 0 BitCount = 0 ID = 100 7.810496 1 11 Rx d 8 37 4C F1 14 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.810548 1 65 Rx d 3 19 00 00 Length = 0 BitCount = 0 ID = 101 7.810577 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 7.810629 1 10 Rx d 8 A8 11 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 7.810658 1 64 Rx d 4 E4 02 00 00 Length = 0 BitCount = 0 ID = 100 7.830335 1 64 Rx d 4 EC 02 00 00 Length = 0 BitCount = 0 ID = 100 7.840350 1 64 Rx d 4 F4 02 00 00 Length = 0 BitCount = 0 ID = 100 7.840382 1 11 Rx d 8 D1 4B F6 07 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.850350 1 64 Rx d 4 FC 02 00 00 Length = 0 BitCount = 0 ID = 100 7.850381 1 64 Rx d 4 04 03 00 00 Length = 0 BitCount = 0 ID = 100 7.850410 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 7.850429 1 66 Rx d 1 02 Length = 0 BitCount = 0 ID = 102 7.870348 1 11 Rx d 8 62 4B F9 0A 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.870470 1 64 Rx d 4 0C 03 00 00 Length = 0 BitCount = 0 ID = 100 7.880353 1 64 Rx d 4 14 03 00 00 Length = 0 BitCount = 0 ID = 100 7.890353 1 64 Rx d 4 1C 03 00 00 Length = 0 BitCount = 0 ID = 100 7.890391 1 64 Rx d 4 24 03 00 00 Length = 0 BitCount = 0 ID = 100 7.890416 1 11 Rx d 8 EA 4A CD 11 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.900349 1 64 Rx d 4 2C 03 00 00 Length = 0 BitCount = 0 ID = 100 7.900381 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 7.900501 1 10 Rx d 8 1A 10 00 00 00 00 00 00 Length = 0 BitCount = 0 ID = 16 7.900533 1 65 Rx d 3 32 00 00 Length = 0 BitCount = 0 ID = 101 7.920354 1 64 Rx d 4 34 03 00 00 Length = 0 BitCount = 0 ID = 100 7.930328 1 64 Rx d 4 3C 03 00 00 Length = 0 BitCount = 0 ID = 100 7.930488 1 11 Rx d 8 68 4A 4D 22 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.930546 1 64 Rx d 4 44 03 00 00 Length = 0 BitCount = 0 ID = 100 7.940352 1 64 Rx d 4 4C 03 00 00 Length = 0 BitCount = 0 ID = 100 7.960354 1 64 Rx d 4 54 03 00 00 Length = 0 BitCount = 0 ID = 100 7.960389 1 11 Rx d 8 DE 49 20 1D 00 00 00 00 Length = 0 BitCount = 0 ID = 17 7.960411 1 66 Rx d 1 04 Length = 0 BitCount = 0 ID = 102 7.960498 1 12 Rx d 4 00 01 00 00 Length = 0 BitCount = 0 ID = 18 python-can-4.5.0/test/data/issue_1299.asc000066400000000000000000000011661472200326600200020ustar00rootroot00000000000000date Thu Apr 28 10:44:52.480 am 2022 base hex timestamps absolute internal events logged // version 12.0.0 Begin TriggerBlock Thu Apr 28 10:44:52.480 am 2022 0.000000 Start of measurement 13.258199 1 180 Tx d 8 6A 00 00 00 00 00 00 00 Length = 244016 BitCount = 125 ID = 384 13.258433 1 221 Tx d 8 C2 4A 05 81 00 00 15 10 Length = 228016 BitCount = 117 ID = 545 13.258671 1 3FF Tx d D 55 AA 01 02 03 04 05 06 Length = 232016 BitCount = 119 ID = 1023 13.258907 1 F4 Tx d 8 8A 1A 0D F2 13 00 00 07 Length = 230016 BitCount = 118 ID = 244 End TriggerBlock python-can-4.5.0/test/data/logfile.asc000066400000000000000000000067631472200326600176170ustar00rootroot00000000000000date Sam Sep 30 15:06:13.191 2017 base hex timestamps absolute internal events logged // version 9.0.0 //0.000000 previous log file: logfile_errorframes.asc Begin Triggerblock Sam Sep 30 15:06:13.191 2017 0.000000 Start of measurement 0.015991 CAN 1 Status:chip status error passive - TxErr: 132 RxErr: 0 0.015991 CAN 2 Status:chip status error active 1.015991 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% 1.015991 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% 2.015992 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% 2.501000 1 ErrorFrame 2.501010 1 ErrorFrame ECC: 10100010 2.501020 2 ErrorFrame Flags = 0xe CodeExt = 0x20a2 Code = 0x82 ID = 0 DLC = 0 Position = 5 Length = 11300 2.510001 2 100 Tx r 2.520002 3 200 Tx r Length = 1704000 BitCount = 145 ID = 88888888x 2.584921 4 300 Tx r 8 Length = 1704000 BitCount = 145 ID = 88888888x 3.098426 1 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273910 BitCount = 141 ID = 418119424x 3.148421 1 18EBFF00x Rx d 8 02 1F DE 80 25 DF C0 2B Length = 271910 BitCount = 140 ID = 418119424x 3.197693 1 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x 3.248765 1 18EBFF00x Rx d 8 04 00 4B FF FF FF FF FF Length = 283910 BitCount = 146 ID = 418119424x 3.297743 1 J1939TP FEE3p 6 0 0 - Rx d 23 A0 0F A6 60 3B D1 40 1F DE 80 25 DF C0 2B E1 00 4B FF FF 3C 0F 00 4B FF FF FF FF FF FF FF FF FF FF FF FF 17.876707 CAN 1 Status:chip status error passive - TxErr: 131 RxErr: 0 17.876708 1 6F9 Rx d 8 05 0C 00 00 00 00 00 00 Length = 240015 BitCount = 124 ID = 1785 17.876976 1 6F8 Rx d 8 FF 00 0C FE 00 00 00 00 Length = 239910 BitCount = 124 ID = 1784 18.015997 1 Statistic: D 2 R 0 XD 0 XR 0 E 0 O 0 B 0.04% 20.105214 2 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273925 BitCount = 141 ID = 418119424x 20.155119 2 18EBFF00x Rx d 8 02 1F DE 80 25 DF C0 2B Length = 272152 BitCount = 140 ID = 418119424x 20.204671 2 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x 20.248887 2 18EBFF00x Rx d 8 04 00 4B FF FF FF FF FF Length = 283925 BitCount = 146 ID = 418119424x 20.305233 2 J1939TP FEE3p 6 0 0 - Rx d 23 A0 0F A6 60 3B D1 40 1F DE 80 25 DF C0 2B E1 00 4B FF FF 3C 0F 00 4B FF FF FF FF FF FF FF FF FF FF FF FF 30.005071 CANFD 2 Rx 300 Generic_Name_12 1 0 8 8 01 02 03 04 05 06 07 08 102203 133 303000 e0006659 46500250 4b140250 20011736 2001040d 30.300981 CANFD 3 Tx 50005x 0 0 5 0 140000 73 200050 7a60 46500250 460a0250 20011736 20010205 30.506898 CANFD 4 Rx 4EE 0 0 f 64 01 02 03 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 64 1331984 11 0 46500250 460a0250 20011736 20010205 30.806898 CANFD 5 Tx ErrorFrame Not Acknowledge error, dominant error flag fffe c7 31ca Arb. 556 44 0 0 f 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1331984 11 0 46500250 460a0250 20011736 20010205 113.016026 1 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% 113.016026 2 Statistic: D 0 R 0 XD 0 XR 0 E 0 O 0 B 0.00% End TriggerBlock python-can-4.5.0/test/data/logfile_errorframes.asc000066400000000000000000000026121472200326600222130ustar00rootroot00000000000000date Sam Sep 30 15:06:13.191 2017 base hex timestamps absolute internal events logged // version 9.0.0 Begin Triggerblock Sam Sep 30 15:06:13.191 2017 0.000000 Start of measurement 0.015991 CAN 1 Status:chip status error passive - TxErr: 132 RxErr: 0 0.015991 CAN 2 Status:chip status error active 2.501000 1 ErrorFrame 2.501010 1 ErrorFrame ECC: 10100010 2.501020 2 ErrorFrame Flags = 0xe CodeExt = 0x20a2 Code = 0x82 ID = 0 DLC = 0 Position = 5 Length = 11300 2.520002 3 200 Tx r Length = 1704000 BitCount = 145 ID = 88888888x 2.584921 4 300 Tx r 8 Length = 1704000 BitCount = 145 ID = 88888888x 3.098426 1 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273910 BitCount = 141 ID = 418119424x 3.197693 1 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x 17.876976 1 6F8 Rx d 8 FF 00 0C FE 00 00 00 00 Length = 239910 BitCount = 124 ID = 1784 20.105214 2 18EBFF00x Rx d 8 01 A0 0F A6 60 3B D1 40 Length = 273925 BitCount = 141 ID = 418119424x 20.155119 2 18EBFF00x Rx d 8 02 1F DE 80 25 DF C0 2B Length = 272152 BitCount = 140 ID = 418119424x 20.204671 2 18EBFF00x Rx d 8 03 E1 00 4B FF FF 3C 0F Length = 283910 BitCount = 146 ID = 418119424x 20.248887 2 18EBFF00x Rx d 8 04 00 4B FF FF FF FF FF Length = 283925 BitCount = 146 ID = 418119424x End TriggerBlock python-can-4.5.0/test/data/single_frame.asc000066400000000000000000000006361472200326600206220ustar00rootroot00000000000000date Sat Sep 30 15:06:13.191 2017 base hex timestamps absolute internal events logged Begin Triggerblock Sat Sep 30 15:06:13.191 2017 0.000000 Start of measurement 0.000000 1 123x Rx d 40 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F End TriggerBlock python-can-4.5.0/test/data/single_frame_us_locale.asc000066400000000000000000000003401472200326600226400ustar00rootroot00000000000000date Sat Sep 30 15:06:13.191 2017 base hex timestamps absolute internal events logged Begin Triggerblock Sat Sep 30 15:06:13.191 2017 0.000000 Start of measurement 0.000000 1 123x Rx d 1 68 End TriggerBlock python-can-4.5.0/test/data/test_CanErrorFrameExt.blf000066400000000000000000000007041472200326600223660ustar00rootroot00000000000000LOGG>0LOBJ LOBJ @I""""""""""3333DUfw"3DLOBJ @I""""""""""3333DUfw"3DLOBJ tLOBJ 8s""""""""LOBJ <s"""""""" python-can-4.5.0/test/data/test_CanErrorFrames.asc000066400000000000000000000013311472200326600220700ustar00rootroot00000000000000date Sam Sep 30 15:06:13.191 2017 base hex timestamps absolute internal events logged // version 9.0.0 Begin Triggerblock Sam Sep 30 15:06:13.191 2017 0.000000 Start of measurement 2.501000 1 ErrorFrame 3.501000 1 ErrorFrame ECC: 10100010 4.501000 2 ErrorFrame Flags = 0xe CodeExt = 0x20a2 Code = 0x82 ID = 0 DLC = 0 Position = 5 Length = 11300 30.806898 CANFD 5 Tx ErrorFrame Not Acknowledge error, dominant error flag fffe c7 31ca Arb. 556 44 0 0 f 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1331984 11 0 46500250 460a0250 20011736 20010205 End TriggerBlock python-can-4.5.0/test/data/test_CanFdMessage.asc000066400000000000000000000013211472200326600214760ustar00rootroot00000000000000date Sam Sep 30 15:06:13.191 2017 base hex timestamps absolute internal events logged // version 9.0.0 Begin Triggerblock Sam Sep 30 15:06:13.191 2017 0.000000 Start of measurement 30.005021 CANFD 1 Rx 300 1 0 8 8 11 c2 03 04 05 06 07 08 102203 133 303000 e0006659 46500250 4b140250 20011736 2001040d 30.005041 CANFD 2 Tx 1C4D80A7x 0 1 8 8 12 c2 03 04 05 06 07 08 102203 133 303000 e0006659 46500250 4b140250 20011736 2001040d 30.005071 CANFD 3 Rx 30a Generic_Name_12 1 1 8 8 01 02 03 04 05 06 07 08 102203 133 303000 e0006659 46500250 4b140250 20011736 2001040d End TriggerBlock python-can-4.5.0/test/data/test_CanFdMessage.blf000066400000000000000000000010641472200326600214770ustar00rootroot00000000000000LOGG>44LOBJ LOBJ xd"""""""""3DDDDUUUUfw@  !"#$%&'()*+,-./0123456789:;<=>?LOBJ xd"""""""""3DDDDUUUUfw@  !"#$%&'()*+,-./0123456789:;<=>?LOBJ tLOBJ 8s""""""""LOBJ <s"""""""" python-can-4.5.0/test/data/test_CanFdMessage64.asc000066400000000000000000000015401472200326600216530ustar00rootroot00000000000000date Sam Sep 30 15:06:13.191 2017 base hex timestamps absolute internal events logged // version 9.0.0 Begin Triggerblock Sam Sep 30 15:06:13.191 2017 0.000000 Start of measurement 30.506898 CANFD 4 Rx 4EE 0 1 f 64 A1 02 03 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 64 1331984 11 0 46500250 460a0250 20011736 20010205 31.506898 CANFD 4 Rx 1C4D80A7x AlphaNumericName_2 1 0 f 64 b1 02 03 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 64 1331984 11 0 46500250 460a0250 20011736 20010205 End TriggerBlock python-can-4.5.0/test/data/test_CanFdMessage64.blf000066400000000000000000000011441472200326600216500ustar00rootroot00000000000000LOGG>ddLOBJ@ LOBJ e"""""""""@DUUUUffffwwww  !"#$%&'()*+,-./0123456789:;<=>?""""LOBJ e"""""""""@DUUUUffffwwww  !"#$%&'()*+,-./0123456789:;<=>?""""LOBJ tLOBJ 8s""""""""LOBJ <s"""""""" python-can-4.5.0/test/data/test_CanFdRemoteMessage.asc000066400000000000000000000005171472200326600226600ustar00rootroot00000000000000date Sam Sep 30 15:06:13.191 2017 base hex timestamps absolute internal events logged // version 9.0.0 Begin Triggerblock Sam Sep 30 15:06:13.191 2017 0.000000 Start of measurement 30.300981 CANFD 3 Tx 50005x 0 1 5 0 140000 73 200050 7a60 46500250 460a0250 20011736 20010205 End TriggerBlock python-can-4.5.0/test/data/test_CanMessage.asc000066400000000000000000000005671472200326600212370ustar00rootroot00000000000000date Sam Sep 30 15:06:13.191 2017 base hex timestamps absolute internal events logged // version 9.0.0 Begin Triggerblock Sat Sep 30 10:06:13.191 PM 2017 0.000000 Start of measurement 2.5010 2 C8 Tx d 8 09 08 07 06 05 04 03 02 17.876708 1 6F9 Rx d 8 05 0C 00 00 00 00 00 00 Length = 240015 BitCount = 124 ID = 1785 End TriggerBlock python-can-4.5.0/test/data/test_CanMessage.asc.gz000066400000000000000000000004251472200326600216470ustar00rootroot00000000000000 atest_CanMessage.asc]j0 FrSM /,ZvJ7at8Ҡ :>#%|O^Rek8'}Ckh23&xvyv;\15LOBJ `LOBJ 0"""""""""3DDDDUfwLOBJ 0"""""""""3DDDDUfwLOBJ tLOBJ 8s""""""""LOBJ <s"""""""" python-can-4.5.0/test/data/test_CanMessage.trc000066400000000000000000000022531472200326600212530ustar00rootroot00000000000000;$FILEVERSION=2.1 ;$STARTTIME=43008.920986006946 ;$COLUMNS=N,O,T,B,I,d,R,L,D ; ; C:\Users\User\Desktop\python-can\test\data\test_CanMessage.trc ; Start time: 30.09.2017 22:06:13.191.000 ; Generated by PEAK-Converter Version 2.2.4.136 ; Data imported from C:\Users\User\Desktop\python-can\test\data\test_CanMessage.asc ;------------------------------------------------------------------------------- ; Bus Name Connection Protocol ; N/A N/A N/A N/A ;------------------------------------------------------------------------------- ; Message Time Type ID Rx/Tx ; Number Offset | Bus [hex] | Reserved ; | [ms] | | | | | Data Length Code ; | | | | | | | | Data [hex] ... ; | | | | | | | | | ;---+-- ------+------ +- +- --+----- +- +- +--- +- -- -- -- -- -- -- -- ;Begin Triggerblock Sat Sep 30 10:06:13.191 PM 2017 ; 0.000000 Start of measurement 1 2501.000 DT 2 00C8 Tx - 8 09 08 07 06 05 04 03 02 2 17876.708 DT 1 06F9 Rx - 8 05 0C 00 00 00 00 00 00 ;End TriggerBlock python-can-4.5.0/test/data/test_CanMessage2.blf000066400000000000000000000006641472200326600213140ustar00rootroot00000000000000LOGG> LOBJ pLOBJ 8V"""""""""3DDDDUfwLOBJ 8V"""""""""3DDDDUfwLOBJ tLOBJ 8s""""""""LOBJ <s"""""""" python-can-4.5.0/test/data/test_CanMessage_V1_0_BUS1.trc000066400000000000000000000025131472200326600226310ustar00rootroot00000000000000;########################################################################## ; C:\Users\User\Desktop\python-can\test\data\test_CanMessage_V1_0_BUS1.trc ; ; CAN activities imported from C:\Users\User\Desktop\python-can\test\data\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 ; ; Columns description: ; ~~~~~~~~~~~~~~~~~~~~~ ; +-current number in actual sample ; | +time offset of message (ms) ; | | +ID of message (hex) ; | | | +data length code ; | | | | +data bytes (hex) ... ; | | | | | ;----+- ---+--- ----+--- + -+ -- -- ... 1) 17535 00000100 8 00 00 00 00 00 00 00 00 2) 17540 FFFFFFFF 4 00 00 00 08 -- -- -- -- BUSHEAVY 3) 17700 00000100 8 00 00 00 00 00 00 00 00 4) 17873 00000100 8 00 00 00 00 00 00 00 00 5) 19295 0000 8 00 00 00 00 00 00 00 00 6) 19500 0000 8 00 00 00 00 00 00 00 00 7) 19705 0000 8 00 00 00 00 00 00 00 00 8) 20592 00000100 8 00 00 00 00 00 00 00 00 9) 20798 00000100 8 00 00 00 00 00 00 00 00 10) 20956 00000100 8 00 00 00 00 00 00 00 00 11) 21097 00000100 8 00 00 00 00 00 00 00 00 python-can-4.5.0/test/data/test_CanMessage_V1_1.trc000066400000000000000000000022541472200326600220420ustar00rootroot00000000000000;$FILEVERSION=1.1 ;$STARTTIME=44548.6028595139 ; ; Start time: 18.12.2021 14:28:07.062.0 ; Generated by PCAN-View v5.0.0.814 ; ; Message Number ; | Time Offset (ms) ; | | Type ; | | | ID (hex) ; | | | | Data Length ; | | | | | Data Bytes (hex) ... ; | | | | | | ;---+-- ----+---- --+-- ----+--- + -+ -- -- -- -- -- -- -- 1) 17535.4 Tx 00000100 8 00 00 00 00 00 00 00 00 2) 17540.3 Warng FFFFFFFF 4 00 00 00 08 BUSHEAVY 3) 17700.3 Tx 00000100 8 00 00 00 00 00 00 00 00 4) 17873.8 Tx 00000100 8 00 00 00 00 00 00 00 00 5) 19295.4 Tx 0000 8 00 00 00 00 00 00 00 00 6) 19500.6 Tx 0000 8 00 00 00 00 00 00 00 00 7) 19705.2 Tx 0000 8 00 00 00 00 00 00 00 00 8) 20592.7 Tx 00000100 8 00 00 00 00 00 00 00 00 9) 20798.6 Tx 00000100 8 00 00 00 00 00 00 00 00 10) 20956.0 Tx 00000100 8 00 00 00 00 00 00 00 00 11) 21097.1 Tx 00000100 8 00 00 00 00 00 00 00 00 python-can-4.5.0/test/data/test_CanMessage_V1_3.trc000066400000000000000000000032641472200326600220460ustar00rootroot00000000000000;$FILEVERSION=1.3 ;$STARTTIME=44548.6028595139 ; ; C:\test.trc ; Start time: 18.12.2021 14:28:07.062.0 ; Generated by PCAN-Explorer v5.4.0 ;------------------------------------------------------------------------------- ; Bus Name Connection Protocol Bit rate ; 1 PCAN Untitled@pcan_usb CAN 500 kbit/s ; 2 PTCAN PCANLight_USB_16@pcan_usb CAN ;------------------------------------------------------------------------------- ; Message Number ; | Time Offset (ms) ; | | Bus ; | | | Type ; | | | | ID (hex) ; | | | | | Reserved ; | | | | | | Data Length Code ; | | | | | | | Data Bytes (hex) ... ; | | | | | | | | ; | | | | | | | | ;---+-- ------+------ +- --+-- ----+--- +- -+-- -+ -- -- -- -- -- -- -- 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 python-can-4.5.0/test/data/test_CanMessage_V2_0_BUS1.trc000066400000000000000000000027611472200326600226370ustar00rootroot00000000000000;$FILEVERSION=2.0 ;$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 ; 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 ;------------------------------------------------------------------------------- ; Connection Bit rate ; N/A N/A ;------------------------------------------------------------------------------- ; Message Time Type ID Rx/Tx ; Number Offset | [hex] | Data Length ; | [ms] | | | | Data [hex] ... ; | | | | | | | ;---+-- ------+------ +- --+----- +- +- +- -- -- -- -- -- -- -- 1 17535.400 DT 00000100 Tx 8 00 00 00 00 00 00 00 00 2 17540.300 ST Rx 00 00 00 08 3 17700.300 DT 00000100 Tx 8 00 00 00 00 00 00 00 00 4 17873.800 DT 00000100 Tx 8 00 00 00 00 00 00 00 00 5 19295.400 DT 0000 Tx 8 00 00 00 00 00 00 00 00 6 19500.600 DT 0000 Tx 8 00 00 00 00 00 00 00 00 7 19705.200 DT 0000 Tx 8 00 00 00 00 00 00 00 00 8 20592.700 DT 00000100 Tx 8 00 00 00 00 00 00 00 00 9 20798.600 DT 00000100 Tx 8 00 00 00 00 00 00 00 00 10 20956.000 DT 00000100 Tx 8 00 00 00 00 00 00 00 00 11 21097.100 DT 00000100 Tx 8 00 00 00 00 00 00 00 00 python-can-4.5.0/test/data/test_CanMessage_V2_1.trc000066400000000000000000000033071472200326600220430ustar00rootroot00000000000000;$FILEVERSION=2.1 ;$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 ; 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 ;------------------------------------------------------------------------------- ; Bus Name Connection Protocol ; N/A N/A N/A N/A ;------------------------------------------------------------------------------- ; Message Time Type ID Rx/Tx ; Number Offset | Bus [hex] | Reserved ; | [ms] | | | | | Data Length Code ; | | | | | | | | Data [hex] ... ; | | | | | | | | | ;---+-- ------+------ +- +- --+----- +- +- +--- +- -- -- -- -- -- -- -- 1 17535.400 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00 2 17540.300 ST 1 - Rx - 4 00 00 00 08 3 17700.300 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00 4 17873.800 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00 5 19295.400 DT 1 0000 Tx - 8 00 00 00 00 00 00 00 00 6 19500.600 DT 1 0000 Tx - 8 00 00 00 00 00 00 00 00 7 19705.200 DT 1 0000 Tx - 8 00 00 00 00 00 00 00 00 8 20592.700 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00 9 20798.600 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00 10 20956.000 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00 11 21097.100 DT 1 00000100 Tx - 8 00 00 00 00 00 00 00 00 python-can-4.5.0/test/data/test_CanRemoteMessage.asc000066400000000000000000000005571472200326600224120ustar00rootroot00000000000000date Sam Sep 30 15:06:13.191 2017 base hex timestamps absolute internal events logged // version 9.0.0 Begin Triggerblock Sam Sep 30 15:06:13.191 2017 0.000000 Start of measurement 2.510001 2 100 Rx r 2.520002 3 200 Tx r Length = 1704000 BitCount = 145 ID = 88888888x 2.584921 4 300 Rx r 8 Length = 1704000 BitCount = 145 ID = 88888888x End TriggerBlock python-can-4.5.0/test/listener_test.py000066400000000000000000000134741472200326600200300ustar00rootroot00000000000000#!/usr/bin/env python """ """ import asyncio import logging import os import random import tempfile import unittest import warnings from os.path import dirname, join import can from .data.example_data import generate_message logging.basicConfig(level=logging.DEBUG) # makes the random number generator deterministic random.seed(13339115) class ListenerImportTest(unittest.TestCase): def testClassesImportable(self): self.assertTrue(hasattr(can, "Listener")) self.assertTrue(hasattr(can, "BufferedReader")) self.assertTrue(hasattr(can, "Notifier")) self.assertTrue(hasattr(can, "Logger")) self.assertTrue(hasattr(can, "ASCWriter")) self.assertTrue(hasattr(can, "ASCReader")) self.assertTrue(hasattr(can, "BLFReader")) self.assertTrue(hasattr(can, "BLFWriter")) self.assertTrue(hasattr(can, "CSVReader")) self.assertTrue(hasattr(can, "CSVWriter")) self.assertTrue(hasattr(can, "CanutilsLogWriter")) self.assertTrue(hasattr(can, "CanutilsLogReader")) self.assertTrue(hasattr(can, "SqliteReader")) self.assertTrue(hasattr(can, "SqliteWriter")) self.assertTrue(hasattr(can, "Printer")) self.assertTrue(hasattr(can, "LogReader")) self.assertTrue(hasattr(can, "MessageSync")) class BusTest(unittest.TestCase): def setUp(self): # Save all can.rc defaults self._can_rc = can.rc can.rc = {"interface": "virtual"} self.bus = can.interface.Bus() def tearDown(self): self.bus.shutdown() # Restore the defaults can.rc = self._can_rc class ListenerTest(BusTest): def testBasicListenerCanBeAddedToNotifier(self): a_listener = can.Printer() notifier = can.Notifier(self.bus, [a_listener], 0.1) notifier.stop() self.assertIn(a_listener, notifier.listeners) def testAddListenerToNotifier(self): a_listener = can.Printer() notifier = can.Notifier(self.bus, [], 0.1) notifier.stop() self.assertNotIn(a_listener, notifier.listeners) notifier.add_listener(a_listener) self.assertIn(a_listener, notifier.listeners) def testRemoveListenerFromNotifier(self): a_listener = can.Printer() notifier = can.Notifier(self.bus, [a_listener], 0.1) notifier.stop() self.assertIn(a_listener, notifier.listeners) notifier.remove_listener(a_listener) self.assertNotIn(a_listener, notifier.listeners) def testPlayerTypeResolution(self): def test_filetype_to_instance(extension, klass): print(f"testing: {extension}") try: if extension == ".blf": delete = False file_handler = open( join(dirname(__file__), "data", "test_CanMessage.blf") ) else: delete = True file_handler = tempfile.NamedTemporaryFile( suffix=extension, delete=False ) with file_handler as my_file: filename = my_file.name with can.LogReader(filename) as reader: self.assertIsInstance(reader, klass) finally: if delete: os.remove(filename) test_filetype_to_instance(".asc", can.ASCReader) test_filetype_to_instance(".blf", can.BLFReader) test_filetype_to_instance(".csv", can.CSVReader) test_filetype_to_instance(".db", can.SqliteReader) test_filetype_to_instance(".log", can.CanutilsLogReader) def testPlayerTypeResolutionUnsupportedFileTypes(self): for should_fail_with in ["", ".", ".some_unknown_extention_42"]: with self.assertRaises(ValueError): with can.LogReader(should_fail_with): # make sure we close it anyways pass def testLoggerTypeResolution(self): def test_filetype_to_instance(extension, klass): print(f"testing: {extension}") try: with tempfile.NamedTemporaryFile( suffix=extension, delete=False ) as my_file: filename = my_file.name with can.Logger(filename) as writer: self.assertIsInstance(writer, klass) finally: os.remove(filename) test_filetype_to_instance(".asc", can.ASCWriter) test_filetype_to_instance(".blf", can.BLFWriter) test_filetype_to_instance(".csv", can.CSVWriter) test_filetype_to_instance(".db", can.SqliteWriter) test_filetype_to_instance(".log", can.CanutilsLogWriter) test_filetype_to_instance(".txt", can.Printer) with can.Logger(None) as logger: self.assertIsInstance(logger, can.Printer) def testLoggerTypeResolutionUnsupportedFileTypes(self): for should_fail_with in ["", ".", ".some_unknown_extention_42"]: with self.assertRaises(ValueError): with can.Logger(should_fail_with): # make sure we close it anyways pass def testBufferedListenerReceives(self): a_listener = can.BufferedReader() a_listener(generate_message(0xDADADA)) a_listener(generate_message(0xDADADA)) self.assertIsNotNone(a_listener.get_message(0.1)) a_listener.stop() self.assertIsNotNone(a_listener.get_message(0.1)) def test_deprecated_loop_arg(recwarn): warnings.simplefilter("always") can.AsyncBufferedReader(loop=asyncio.get_event_loop()) assert len(recwarn) > 0 assert recwarn.pop(DeprecationWarning) recwarn.clear() # assert that no warning is shown when loop argument is not used can.AsyncBufferedReader() assert len(recwarn) == 0 if __name__ == "__main__": unittest.main() python-can-4.5.0/test/logformats_test.py000066400000000000000000001101341472200326600203470ustar00rootroot00000000000000#!/usr/bin/env python """ This test module test the separate reader/writer combinations of the can.io.* modules by writing some messages to a temporary file and reading it again. Then it checks if the messages that were read are same ones as the ones that were written. It also checks that the order of the messages is correct. The types of messages that are tested differs between the different writer/reader pairs - e.g., some don't handle error frames and comments. TODO: correctly set preserves_channel and adds_default_channel """ import locale import logging import os import tempfile import unittest from abc import ABCMeta, abstractmethod from contextlib import contextmanager from datetime import datetime from itertools import zip_longest from pathlib import Path from unittest.mock import patch from parameterized import parameterized import can from can.io import blf from .data.example_data import ( TEST_COMMENTS, TEST_MESSAGES_BASE, TEST_MESSAGES_CAN_FD, TEST_MESSAGES_ERROR_FRAMES, TEST_MESSAGES_REMOTE_FRAMES, sort_messages, ) from .message_helper import ComparingMessagesTestCase logging.basicConfig(level=logging.DEBUG) try: import asammdf except ModuleNotFoundError: asammdf = None @contextmanager def override_locale(category: int, locale_str: str) -> None: prev_locale = locale.getlocale(category) locale.setlocale(category, locale_str) yield locale.setlocale(category, prev_locale) class ReaderWriterExtensionTest(unittest.TestCase): def _get_suffix_case_variants(self, suffix): return [ suffix.upper(), suffix.lower(), f"can.msg.ext{suffix}", "".join([c.upper() if i % 2 else c for i, c in enumerate(suffix)]), ] def _test_extension(self, suffix): WriterType = can.io.MESSAGE_WRITERS.get(suffix) ReaderType = can.io.MESSAGE_READERS.get(suffix) for suffix_variant in self._get_suffix_case_variants(suffix): tmp_file = tempfile.NamedTemporaryFile(suffix=suffix_variant, delete=False) tmp_file.close() try: if WriterType: with can.Logger(tmp_file.name) as logger: assert type(logger) == WriterType if ReaderType: with can.LogReader(tmp_file.name) as player: assert type(player) == ReaderType finally: os.remove(tmp_file.name) def test_extension_matching_asc(self): self._test_extension(".asc") def test_extension_matching_blf(self): self._test_extension(".blf") def test_extension_matching_csv(self): self._test_extension(".csv") def test_extension_matching_db(self): self._test_extension(".db") def test_extension_matching_log(self): self._test_extension(".log") def test_extension_matching_txt(self): self._test_extension(".txt") def test_extension_matching_mf4(self): try: self._test_extension(".mf4") except NotImplementedError: if asammdf is not None: raise class ReaderWriterTest(unittest.TestCase, ComparingMessagesTestCase, metaclass=ABCMeta): """Tests a pair of writer and reader by writing all data first and then reading all data and checking if they could be reconstructed correctly. Optionally writes some comments as well. .. note:: This class is prevented from being executed as a test case itself by a *del* statement in at the end of the file. (Source: `*Wojciech B.* on StackOverlfow `_) """ def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) self._setup_instance() @abstractmethod def _setup_instance(self): """Hook for subclasses.""" raise NotImplementedError() def _setup_instance_helper( self, writer_constructor, reader_constructor, binary_file=False, check_remote_frames=True, check_error_frames=True, check_fd=True, check_comments=False, test_append=False, allowed_timestamp_delta=0.0, preserves_channel=True, adds_default_channel=None, ): """ :param Callable writer_constructor: the constructor of the writer class :param Callable reader_constructor: the constructor of the reader class :param bool binary_file: if True, opens files in binary and not in text mode :param bool check_remote_frames: if True, also tests remote frames :param bool check_error_frames: if True, also tests error frames :param bool check_fd: if True, also tests CAN FD frames :param bool check_comments: if True, also inserts comments at some locations and checks if they are contained anywhere literally in the resulting file. The locations as selected randomly but deterministically, which makes the test reproducible. :param bool test_append: tests the writer in append mode as well :param float or int or None allowed_timestamp_delta: directly passed to :meth:`can.Message.equals` :param bool preserves_channel: if True, checks that the channel attribute is preserved :param any adds_default_channel: sets this as the channel when not other channel was given ignored, if *preserves_channel* is True """ # get all test messages self.original_messages = list(TEST_MESSAGES_BASE) if check_remote_frames: self.original_messages += TEST_MESSAGES_REMOTE_FRAMES if check_error_frames: self.original_messages += TEST_MESSAGES_ERROR_FRAMES if check_fd: self.original_messages += TEST_MESSAGES_CAN_FD # sort them so that for example ASCWriter does not "fix" any messages with timestamp 0.0 self.original_messages = sort_messages(self.original_messages) if check_comments: # we check this because of the lack of a common base class # we filter for not starts with '__' so we do not get all the builtin # methods when logging to the console attrs = [ attr for attr in dir(writer_constructor) if not attr.startswith("__") ] assert ( "log_event" in attrs ), f"cannot check comments with this writer: {writer_constructor}" # get all test comments self.original_comments = TEST_COMMENTS if check_comments else () self.writer_constructor = writer_constructor self.reader_constructor = reader_constructor self.binary_file = binary_file self.test_append_enabled = test_append ComparingMessagesTestCase.__init__( self, allowed_timestamp_delta=allowed_timestamp_delta, preserves_channel=preserves_channel, ) # adds_default_channel=adds_default_channel # TODO inlcude in tests def setUp(self): with tempfile.NamedTemporaryFile("w+", delete=False) as test_file: self.test_file_name = test_file.name def tearDown(self): os.remove(self.test_file_name) del self.test_file_name def test_path_like_explicit_stop(self): """testing with path-like and explicit stop() call""" # create writer print("writing all messages/comments") writer = self.writer_constructor(self.test_file_name) self._write_all(writer) self._ensure_fsync(writer) writer.stop() if hasattr(writer.file, "closed"): self.assertTrue(writer.file.closed) print("reading all messages") reader = self.reader_constructor(self.test_file_name) read_messages = list(reader) # redundant, but this checks if stop() can be called multiple times reader.stop() if hasattr(writer.file, "closed"): self.assertTrue(writer.file.closed) # check if at least the number of messages matches # could use assertCountEqual in later versions of Python and in the other methods self.assertEqual( len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages", ) self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) def test_path_like_context_manager(self): """testing with path-like object and context manager""" # create writer print("writing all messages/comments") with self.writer_constructor(self.test_file_name) as writer: self._write_all(writer) self._ensure_fsync(writer) w = writer if hasattr(w.file, "closed"): self.assertTrue(w.file.closed) # read all written messages print("reading all messages") with self.reader_constructor(self.test_file_name) as reader: read_messages = list(reader) r = reader if hasattr(r.file, "closed"): self.assertTrue(r.file.closed) # check if at least the number of messages matches; self.assertEqual( len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages", ) self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) def test_file_like_explicit_stop(self): """testing with file-like object and explicit stop() call""" # create writer print("writing all messages/comments") my_file = open(self.test_file_name, "wb" if self.binary_file else "w") writer = self.writer_constructor(my_file) self._write_all(writer) self._ensure_fsync(writer) writer.stop() if hasattr(my_file, "closed"): self.assertTrue(my_file.closed) print("reading all messages") my_file = open(self.test_file_name, "rb" if self.binary_file else "r") reader = self.reader_constructor(my_file) read_messages = list(reader) # redundant, but this checks if stop() can be called multiple times reader.stop() if hasattr(my_file, "closed"): self.assertTrue(my_file.closed) # check if at least the number of messages matches # could use assertCountEqual in later versions of Python and in the other methods self.assertEqual( len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages", ) self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) def test_file_like_context_manager(self): """testing with file-like object and context manager""" # create writer print("writing all messages/comments") my_file = open(self.test_file_name, "wb" if self.binary_file else "w") with self.writer_constructor(my_file) as writer: self._write_all(writer) self._ensure_fsync(writer) w = writer if hasattr(my_file, "closed"): self.assertTrue(my_file.closed) # read all written messages print("reading all messages") my_file = open(self.test_file_name, "rb" if self.binary_file else "r") with self.reader_constructor(my_file) as reader: read_messages = list(reader) r = reader if hasattr(my_file, "closed"): self.assertTrue(my_file.closed) # check if at least the number of messages matches; self.assertEqual( len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages", ) self.assertMessagesEqual(self.original_messages, read_messages) self.assertIncludesComments(self.test_file_name) def test_append_mode(self): """ testing append mode with context manager and path-like object """ if not self.test_append_enabled: raise unittest.SkipTest("do not test append mode") count = len(self.original_messages) first_part = self.original_messages[: count // 2] second_part = self.original_messages[count // 2 :] # write first half with self.writer_constructor(self.test_file_name) as writer: for message in first_part: writer(message) self._ensure_fsync(writer) # use append mode for second half try: writer = self.writer_constructor(self.test_file_name, append=True) except ValueError as e: # maybe "append" is not a formal parameter (this is the case for SqliteWriter) try: writer = self.writer_constructor(self.test_file_name) except TypeError: # if it is still a problem, raise the initial error raise e with writer: for message in second_part: writer(message) self._ensure_fsync(writer) with self.reader_constructor(self.test_file_name) as reader: read_messages = list(reader) self.assertMessagesEqual(self.original_messages, read_messages) def _write_all(self, writer): """Writes messages and insert comments here and there.""" # Note: we make no assumptions about the length of original_messages and original_comments for msg, comment in zip_longest( self.original_messages, self.original_comments, fillvalue=None ): # msg and comment might be None if comment is not None: print("writing comment: ", comment) writer.log_event(comment) # we already know that this method exists if msg is not None: print("writing message: ", msg) writer(msg) def _ensure_fsync(self, io_handler): if hasattr(io_handler.file, "fileno"): io_handler.file.flush() os.fsync(io_handler.file.fileno()) def assertIncludesComments(self, filename): """ Ensures that all comments are literally contained in the given file. :param filename: the path-like object to use """ if self.original_comments: # read the entire outout file with open(filename, "rb" if self.binary_file else "r") as file: output_contents = file.read() # check each, if they can be found in there literally for comment in self.original_comments: self.assertIn(comment, output_contents) class TestAscFileFormat(ReaderWriterTest): """Tests can.ASCWriter and can.ASCReader""" FORMAT_START_OF_FILE_DATE = "%a %b %d %I:%M:%S.%f %p %Y" def _setup_instance(self): super()._setup_instance_helper( can.ASCWriter, can.ASCReader, check_fd=True, check_comments=True, preserves_channel=False, adds_default_channel=0, ) def _get_logfile_location(self, filename: str) -> Path: my_dir = Path(__file__).parent return my_dir / "data" / filename def _read_log_file(self, filename, **kwargs): logfile = self._get_logfile_location(filename) with can.ASCReader(logfile, **kwargs) as reader: return list(reader) def test_read_absolute_time(self): time_from_file = "Sat Sep 30 10:06:13.191 PM 2017" start_time = datetime.strptime( time_from_file, self.FORMAT_START_OF_FILE_DATE ).timestamp() expected_messages = [ can.Message( timestamp=2.5010 + start_time, arbitration_id=0xC8, is_extended_id=False, is_rx=False, channel=1, dlc=8, data=[9, 8, 7, 6, 5, 4, 3, 2], ), can.Message( timestamp=17.876708 + start_time, arbitration_id=0x6F9, is_extended_id=False, channel=0, dlc=0x8, data=[5, 0xC, 0, 0, 0, 0, 0, 0], ), ] actual = self._read_log_file("test_CanMessage.asc", relative_timestamp=False) self.assertMessagesEqual(actual, expected_messages) def test_read_can_message(self): expected_messages = [ can.Message( timestamp=2.5010, arbitration_id=0xC8, is_extended_id=False, is_rx=False, channel=1, dlc=8, data=[9, 8, 7, 6, 5, 4, 3, 2], ), can.Message( timestamp=17.876708, arbitration_id=0x6F9, is_extended_id=False, channel=0, dlc=0x8, data=[5, 0xC, 0, 0, 0, 0, 0, 0], ), ] actual = self._read_log_file("test_CanMessage.asc") self.assertMessagesEqual(actual, expected_messages) def test_read_can_remote_message(self): expected_messages = [ can.Message( timestamp=2.510001, arbitration_id=0x100, is_extended_id=False, channel=1, is_remote_frame=True, ), can.Message( timestamp=2.520002, arbitration_id=0x200, is_extended_id=False, is_rx=False, channel=2, is_remote_frame=True, ), can.Message( timestamp=2.584921, arbitration_id=0x300, is_extended_id=False, channel=3, dlc=8, is_remote_frame=True, ), ] actual = self._read_log_file("test_CanRemoteMessage.asc") self.assertMessagesEqual(actual, expected_messages) def test_read_can_fd_remote_message(self): expected_messages = [ can.Message( timestamp=30.300981, arbitration_id=0x50005, channel=2, dlc=5, is_rx=False, is_fd=True, is_remote_frame=True, error_state_indicator=True, ) ] actual = self._read_log_file("test_CanFdRemoteMessage.asc") self.assertMessagesEqual(actual, expected_messages) def test_read_can_fd_message(self): expected_messages = [ can.Message( timestamp=30.005021, arbitration_id=0x300, is_extended_id=False, channel=0, dlc=8, data=[0x11, 0xC2, 3, 4, 5, 6, 7, 8], is_fd=True, bitrate_switch=True, ), can.Message( timestamp=30.005041, arbitration_id=0x1C4D80A7, channel=1, dlc=8, is_rx=False, data=[0x12, 0xC2, 3, 4, 5, 6, 7, 8], is_fd=True, error_state_indicator=True, ), can.Message( timestamp=30.005071, arbitration_id=0x30A, is_extended_id=False, channel=2, dlc=8, data=[1, 2, 3, 4, 5, 6, 7, 8], is_fd=True, bitrate_switch=True, error_state_indicator=True, ), ] actual = self._read_log_file("test_CanFdMessage.asc") self.assertMessagesEqual(actual, expected_messages) def test_read_can_fd_message_64(self): expected_messages = [ can.Message( timestamp=30.506898, arbitration_id=0x4EE, is_extended_id=False, channel=3, dlc=64, data=[0xA1, 2, 3, 4] + 59 * [0] + [0x64], is_fd=True, error_state_indicator=True, ), can.Message( timestamp=31.506898, arbitration_id=0x1C4D80A7, channel=3, dlc=64, data=[0xB1, 2, 3, 4] + 59 * [0] + [0x64], is_fd=True, bitrate_switch=True, ), ] actual = self._read_log_file("test_CanFdMessage64.asc") self.assertMessagesEqual(actual, expected_messages) def test_read_can_and_canfd_error_frames(self): expected_messages = [ can.Message(timestamp=2.501000, channel=0, is_error_frame=True), can.Message(timestamp=3.501000, channel=0, is_error_frame=True), can.Message(timestamp=4.501000, channel=1, is_error_frame=True), can.Message( timestamp=30.806898, channel=4, is_rx=False, is_error_frame=True, is_fd=True, ), ] actual = self._read_log_file("test_CanErrorFrames.asc") self.assertMessagesEqual(actual, expected_messages) def test_read_ignore_comments(self): _msg_list = self._read_log_file("logfile.asc") def test_read_no_triggerblock(self): _msg_list = self._read_log_file("issue_1256.asc") def test_read_can_dlc_greater_than_8(self): _msg_list = self._read_log_file("issue_1299.asc") def test_read_error_frame_channel(self): # gh-issue 1578 err_frame = can.Message(is_error_frame=True, channel=4) temp_file = tempfile.NamedTemporaryFile("w", delete=False) temp_file.close() try: with can.ASCWriter(temp_file.name) as writer: writer.on_message_received(err_frame) with can.ASCReader(temp_file.name) as reader: msg_list = list(reader) assert len(msg_list) == 1 assert err_frame.equals( msg_list[0], check_channel=True ), f"{err_frame!r}!={msg_list[0]!r}" finally: os.unlink(temp_file.name) def test_write_millisecond_handling(self): now = datetime( year=2017, month=9, day=30, hour=15, minute=6, second=13, microsecond=191456 ) # We temporarily set the locale to C to ensure test reproducibility with override_locale(category=locale.LC_TIME, locale_str="C"): # We mock datetime.now during ASCWriter __init__ for reproducibility # Unfortunately, now() is a readonly attribute, so we mock datetime with patch("can.io.asc.datetime") as mock_datetime: mock_datetime.now.return_value = now writer = can.ASCWriter(self.test_file_name) msg = can.Message( timestamp=now.timestamp(), arbitration_id=0x123, data=b"h" ) writer.on_message_received(msg) writer.stop() actual_file = Path(self.test_file_name) expected_file = self._get_logfile_location("single_frame_us_locale.asc") self.assertEqual(expected_file.read_text(), actual_file.read_text()) def test_write(self): now = datetime( year=2017, month=9, day=30, hour=15, minute=6, second=13, microsecond=191456 ) # We temporarily set the locale to C to ensure test reproducibility with override_locale(category=locale.LC_TIME, locale_str="C"): # We mock datetime.now during ASCWriter __init__ for reproducibility # Unfortunately, now() is a readonly attribute, so we mock datetime with patch("can.io.asc.datetime") as mock_datetime: mock_datetime.now.return_value = now writer = can.ASCWriter(self.test_file_name) msg = can.Message( timestamp=now.timestamp(), arbitration_id=0x123, data=range(64), ) with writer: writer.on_message_received(msg) actual_file = Path(self.test_file_name) expected_file = self._get_logfile_location("single_frame.asc") self.assertEqual(expected_file.read_text(), actual_file.read_text()) class TestBlfFileFormat(ReaderWriterTest): """Tests can.BLFWriter and can.BLFReader. Uses log files created by Toby Lorenz: https://bitbucket.org/tobylorenz/vector_blf/src/master/src/Vector/BLF/tests/unittests/events_from_binlog/ """ def _setup_instance(self): super()._setup_instance_helper( can.BLFWriter, can.BLFReader, binary_file=True, check_fd=True, check_comments=False, test_append=True, allowed_timestamp_delta=1.0e-6, preserves_channel=False, adds_default_channel=0, ) def _read_log_file(self, filename): logfile = os.path.join(os.path.dirname(__file__), "data", filename) with can.BLFReader(logfile) as reader: return list(reader) def test_can_message(self): expected = can.Message( timestamp=2459565876.494607, arbitration_id=0x4444444, is_extended_id=False, channel=0x1110, dlc=0x33, data=[0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC], ) actual = self._read_log_file("test_CanMessage.blf") self.assertMessagesEqual(actual, [expected] * 2) self.assertEqual(actual[0].channel, expected.channel) def test_can_message_2(self): expected = can.Message( timestamp=2459565876.494607, arbitration_id=0x4444444, is_extended_id=False, channel=0x1110, dlc=0x33, data=[0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC], ) actual = self._read_log_file("test_CanMessage2.blf") self.assertMessagesEqual(actual, [expected] * 2) self.assertEqual(actual[0].channel, expected.channel) def test_can_fd_message(self): expected = can.Message( timestamp=2459565876.494607, arbitration_id=0x4444444, is_extended_id=False, channel=0x1110, dlc=64, is_fd=True, bitrate_switch=True, error_state_indicator=True, data=range(64), ) actual = self._read_log_file("test_CanFdMessage.blf") self.assertMessagesEqual(actual, [expected] * 2) self.assertEqual(actual[0].channel, expected.channel) def test_can_fd_message_64(self): expected = can.Message( timestamp=2459565876.494607, arbitration_id=0x15555555, is_extended_id=False, is_remote_frame=True, channel=0x10, dlc=64, is_fd=True, is_rx=False, bitrate_switch=True, error_state_indicator=True, ) actual = self._read_log_file("test_CanFdMessage64.blf") self.assertMessagesEqual(actual, [expected] * 2) self.assertEqual(actual[0].channel, expected.channel) def test_can_error_frame_ext(self): expected = can.Message( timestamp=2459565876.494607, is_error_frame=True, arbitration_id=0x19999999, is_extended_id=True, channel=0x1110, dlc=0x66, data=[0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44], ) actual = self._read_log_file("test_CanErrorFrameExt.blf") self.assertMessagesEqual(actual, [expected] * 2) self.assertEqual(actual[0].channel, expected.channel) def test_timestamp_to_systemtime(self): self.assertAlmostEqual( 1636485425.999, blf.systemtime_to_timestamp(blf.timestamp_to_systemtime(1636485425.998908)), places=3, ) self.assertAlmostEqual( 1636485426.0, blf.systemtime_to_timestamp(blf.timestamp_to_systemtime(1636485425.999908)), places=3, ) class TestCanutilsFileFormat(ReaderWriterTest): """Tests can.CanutilsLogWriter and can.CanutilsLogReader""" def _setup_instance(self): super()._setup_instance_helper( can.CanutilsLogWriter, can.CanutilsLogReader, check_fd=True, test_append=True, check_comments=False, preserves_channel=False, adds_default_channel="vcan0", ) class TestCsvFileFormat(ReaderWriterTest): """Tests can.CSVWriter and can.CSVReader""" def _setup_instance(self): super()._setup_instance_helper( can.CSVWriter, can.CSVReader, check_fd=False, test_append=True, check_comments=False, preserves_channel=False, adds_default_channel=None, ) @unittest.skipIf(asammdf is None, "MF4 is unavailable") class TestMF4FileFormat(ReaderWriterTest): """Tests can.MF4Writer and can.MF4Reader""" def _setup_instance(self): super()._setup_instance_helper( can.MF4Writer, can.MF4Reader, binary_file=True, check_comments=False, preserves_channel=False, allowed_timestamp_delta=1e-4, adds_default_channel=0, ) class TestSqliteDatabaseFormat(ReaderWriterTest): """Tests can.SqliteWriter and can.SqliteReader""" def _setup_instance(self): super()._setup_instance_helper( can.SqliteWriter, can.SqliteReader, check_fd=False, test_append=True, check_comments=False, preserves_channel=False, adds_default_channel=None, ) @unittest.skip("not implemented") def test_file_like_explicit_stop(self): pass @unittest.skip("not implemented") def test_file_like_context_manager(self): pass def test_read_all(self): """ testing :meth:`can.SqliteReader.read_all` with context manager and path-like object """ # create writer print("writing all messages/comments") with self.writer_constructor(self.test_file_name) as writer: self._write_all(writer) # read all written messages print("reading all messages") with self.reader_constructor(self.test_file_name) as reader: read_messages = list(reader.read_all()) # check if at least the number of messages matches; self.assertEqual( len(read_messages), len(self.original_messages), "the number of written messages does not match the number of read messages", ) self.assertMessagesEqual(self.original_messages, read_messages) class TestPrinter(unittest.TestCase): """Tests that can.Printer does not crash. TODO test append mode """ messages = ( TEST_MESSAGES_BASE + TEST_MESSAGES_REMOTE_FRAMES + TEST_MESSAGES_ERROR_FRAMES + TEST_MESSAGES_CAN_FD ) def test_not_crashes_with_stdout(self): with can.Printer() as printer: for message in self.messages: printer(message) def test_not_crashes_with_file(self): with tempfile.NamedTemporaryFile("w") as temp_file: with can.Printer(temp_file) as printer: for message in self.messages: printer(message) class TestTrcFileFormatBase(ReaderWriterTest): """ Base class for Tests with can.TRCWriter and can.TRCReader .. note:: This class is prevented from being executed as a test case itself by a *del* statement in at the end of the file. """ def _setup_instance(self): super()._setup_instance_helper( can.TRCWriter, can.TRCReader, check_remote_frames=False, check_error_frames=False, check_fd=False, check_comments=False, preserves_channel=False, allowed_timestamp_delta=0.001, adds_default_channel=0, ) def _read_log_file(self, filename, **kwargs): logfile = os.path.join(os.path.dirname(__file__), "data", filename) with can.TRCReader(logfile, **kwargs) as reader: return list(reader) class TestTrcFileFormatGen(TestTrcFileFormatBase): """Generic tests for can.TRCWriter and can.TRCReader with different file versions""" def test_can_message(self): start_time = 1506809173.191 # 30.09.2017 22:06:13.191.000 as timestamp expected_messages = [ can.Message( timestamp=start_time + 2.5010, arbitration_id=0xC8, is_extended_id=False, is_rx=False, channel=1, dlc=8, data=[9, 8, 7, 6, 5, 4, 3, 2], ), can.Message( timestamp=start_time + 17.876708, arbitration_id=0x6F9, is_extended_id=False, channel=0, dlc=0x8, data=[5, 0xC, 0, 0, 0, 0, 0, 0], ), ] actual = self._read_log_file("test_CanMessage.trc") self.assertMessagesEqual(actual, expected_messages) @parameterized.expand( [ ("V1_0", "test_CanMessage_V1_0_BUS1.trc", False), ("V1_1", "test_CanMessage_V1_1.trc", True), ("V1_3", "test_CanMessage_V1_3.trc", True), ("V2_1", "test_CanMessage_V2_1.trc", True), ] ) def test_can_message_versions(self, name, filename, is_rx_support): with self.subTest(name): if name == "V1_0": # Version 1.0 does not support start time start_time = 0 else: start_time = ( 1639837687.062001 # 18.12.2021 14:28:07.062.001 as timestamp ) def msg_std(timestamp): msg = can.Message( timestamp=timestamp + start_time, arbitration_id=0x000, is_extended_id=False, channel=1, dlc=8, data=[0, 0, 0, 0, 0, 0, 0, 0], ) if is_rx_support: msg.is_rx = False return msg def msg_ext(timestamp): msg = can.Message( timestamp=timestamp + start_time, arbitration_id=0x100, is_extended_id=True, channel=1, dlc=8, data=[0, 0, 0, 0, 0, 0, 0, 0], ) if is_rx_support: msg.is_rx = False return msg expected_messages = [ msg_ext(17.5354), msg_ext(17.7003), msg_ext(17.8738), msg_std(19.2954), msg_std(19.5006), msg_std(19.7052), msg_ext(20.5927), msg_ext(20.7986), msg_ext(20.9560), msg_ext(21.0971), ] actual = self._read_log_file(filename) self.assertMessagesEqual(actual, expected_messages) def test_not_supported_version(self): with tempfile.NamedTemporaryFile(mode="w") as f: with self.assertRaises(NotImplementedError): writer = can.TRCWriter(f) writer.file_version = can.TRCFileVersion.UNKNOWN writer.on_message_received(can.Message()) class TestTrcFileFormatV1_0(TestTrcFileFormatBase): """Tests can.TRCWriter and can.TRCReader with file version 1.0""" @staticmethod def Writer(filename): writer = can.TRCWriter(filename) writer.file_version = can.TRCFileVersion.V1_0 return writer def _setup_instance(self): super()._setup_instance() self.writer_constructor = TestTrcFileFormatV1_0.Writer # this excludes the base class from being executed as a test case itself del ReaderWriterTest del TestTrcFileFormatBase if __name__ == "__main__": unittest.main() python-can-4.5.0/test/message_helper.py000066400000000000000000000033351472200326600201220ustar00rootroot00000000000000#!/usr/bin/env python """ This module contains a helper for writing test cases that need to compare messages. """ class ComparingMessagesTestCase: """ Must be extended by a class also extending a unittest.TestCase. .. note:: This class does not extend unittest.TestCase so it does not get run as a test itself. """ def __init__(self, allowed_timestamp_delta=0.0, preserves_channel=True): """ :param float or int or None allowed_timestamp_delta: directly passed to :meth:`can.Message.equals` :param bool preserves_channel: if True, checks that the channel attribute is preserved """ self.allowed_timestamp_delta = allowed_timestamp_delta self.preserves_channel = preserves_channel def assertMessageEqual(self, message_1, message_2): """ Checks that two messages are equal, according to the given rules. """ if not message_1.equals( message_2, check_channel=self.preserves_channel, timestamp_delta=self.allowed_timestamp_delta, ): print(f"Comparing: message 1: {message_1!r}") print(f" message 2: {message_2!r}") self.fail(f"messages are unequal: \n{message_1}\n{message_2}") def assertMessagesEqual(self, messages_1, messages_2): """ Checks the order and content of the individual messages pairwise. Raises an error if the lengths of the sequences are not equal. """ self.assertEqual( len(messages_1), len(messages_2), "the number of messages differs" ) for message_1, message_2 in zip(messages_1, messages_2): self.assertMessageEqual(message_1, message_2) python-can-4.5.0/test/network_test.py000066400000000000000000000061051472200326600176650ustar00rootroot00000000000000#!/usr/bin/env python import contextlib import logging import random import threading import unittest import can from test.config import IS_PYPY logging.getLogger(__file__).setLevel(logging.WARNING) # make a random bool: def rbool(): return random.choice([False, True]) class ControllerAreaNetworkTestCase(unittest.TestCase): """ This test ensures that what messages go in to the bus is what comes out. Requires a can interface. To ensure that hardware and/or software message priority queues don't effect the test, messages are sent one at a time. """ num_messages = 512 # TODO check if error flags are working (don't currently appear on bus) error_flags = [False for _ in range(num_messages)] remote_flags = [rbool() for _ in range(num_messages)] extended_flags = [rbool() for _ in range(num_messages)] ids = list(range(num_messages)) data = list( bytearray([random.randrange(0, 2**8 - 1) for a in range(random.randrange(9))]) for b in range(num_messages) ) def setUp(self): # Save all can.rc defaults self._can_rc = can.rc can.rc = {"interface": "virtual"} def tearDown(self): # Restore the defaults can.rc = self._can_rc def producer(self, channel: str): with can.interface.Bus(channel=channel) as client_bus: for i in range(self.num_messages): m = can.Message( arbitration_id=self.ids[i], is_remote_frame=self.remote_flags[i], is_error_frame=self.error_flags[i], is_extended_id=self.extended_flags[i], data=self.data[i], ) client_bus.send(m) def testProducer(self): """Verify that we can send arbitrary messages on the bus""" logging.debug("testing producer alone") self.producer(channel="testProducer") logging.debug("producer test complete") def testProducerConsumer(self): logging.debug("testing producer/consumer") read_timeout = 2.0 if IS_PYPY else 0.5 channel = "testProducerConsumer" with can.interface.Bus(channel=channel, interface="virtual") as server_bus: t = threading.Thread(target=self.producer, args=(channel,)) t.start() i = 0 while i < self.num_messages: msg = server_bus.recv(timeout=read_timeout) self.assertIsNotNone(msg, "Didn't receive a message") self.assertEqual(msg.is_extended_id, self.extended_flags[i]) if not msg.is_remote_frame: self.assertEqual(msg.data, self.data[i]) self.assertEqual(msg.arbitration_id, self.ids[i]) self.assertEqual(msg.is_error_frame, self.error_flags[i]) self.assertEqual(msg.is_remote_frame, self.remote_flags[i]) i += 1 t.join() with contextlib.suppress(NotImplementedError): server_bus.flush_tx_buffer() if __name__ == "__main__": unittest.main() python-can-4.5.0/test/notifier_test.py000066400000000000000000000035661472200326600200230ustar00rootroot00000000000000#!/usr/bin/env python import asyncio import time import unittest import can class NotifierTest(unittest.TestCase): def test_single_bus(self): with can.Bus("test", interface="virtual", receive_own_messages=True) as bus: reader = can.BufferedReader() notifier = can.Notifier(bus, [reader], 0.1) msg = can.Message() bus.send(msg) self.assertIsNotNone(reader.get_message(1)) notifier.stop() 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) msg = can.Message() bus1.send(msg) time.sleep(0.1) bus2.send(msg) recv_msg = reader.get_message(1) self.assertIsNotNone(recv_msg) self.assertEqual(recv_msg.channel, 0) recv_msg = reader.get_message(1) self.assertIsNotNone(recv_msg) self.assertEqual(recv_msg.channel, 1) notifier.stop() class AsyncNotifierTest(unittest.TestCase): def test_asyncio_notifier(self): async def run_it(): with can.Bus("test", interface="virtual", receive_own_messages=True) as bus: reader = can.AsyncBufferedReader() notifier = can.Notifier( bus, [reader], 0.1, loop=asyncio.get_running_loop() ) bus.send(can.Message()) recv_msg = await asyncio.wait_for(reader.get_message(), 0.5) self.assertIsNotNone(recv_msg) notifier.stop() asyncio.run(run_it()) if __name__ == "__main__": unittest.main() python-can-4.5.0/test/open_vcan.sh000077500000000000000000000004231472200326600170670ustar00rootroot00000000000000#!/bin/bash # Used by .travis.yml (which is executed with sudo privileges) modprobe vcan ip link add dev vcan0 type vcan ip link set up vcan0 mtu 72 ip link add dev vxcan0 type vcan ip link set up vxcan0 mtu 72 ip link add dev slcan0 type vcan ip link set up slcan0 mtu 72 python-can-4.5.0/test/serial_test.py000066400000000000000000000132051472200326600174520ustar00rootroot00000000000000#!/usr/bin/env python """ This module is testing the serial interface. Copyright: 2017 Boris Wenzlaff """ import unittest from unittest.mock import patch import can from can.interfaces.serial.serial_can import SerialBus from .config import IS_PYPY from .message_helper import ComparingMessagesTestCase # Mentioned in #1010 TIMEOUT = 0.5 if IS_PYPY else 0.1 # 0.1 is the default set in SerialBus class SerialDummy: """ Dummy to mock the serial communication """ msg = None def __init__(self): self.msg = bytearray() def read(self, size=1): return_value = bytearray() for i in range(size): return_value.append(self.msg.pop(0)) return bytes(return_value) def write(self, msg): self.msg = bytearray(msg) def reset(self): self.msg = None class SimpleSerialTestBase(ComparingMessagesTestCase): MAX_TIMESTAMP = 0xFFFFFFFF / 1000 def __init__(self): ComparingMessagesTestCase.__init__( self, allowed_timestamp_delta=None, preserves_channel=True ) def test_can_protocol(self): self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20) def test_rx_tx_min_max_data(self): """ Tests the transfer from 0x00 to 0xFF for a 1 byte payload """ for b in range(0, 255): msg = can.Message(data=[b]) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) def test_rx_tx_min_max_dlc(self): """ Tests the transfer from a 1 - 8 byte payload """ payload = bytearray() for b in range(1, 9): payload.append(0) msg = can.Message(data=payload) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) def test_rx_tx_data_none(self): """ Tests the transfer without payload """ msg = can.Message(data=None) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) def test_rx_tx_min_id(self): """ Tests the transfer with the lowest arbitration id """ msg = can.Message(arbitration_id=0) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) def test_rx_tx_max_id(self): """ Tests the transfer with the highest arbitration id """ msg = can.Message(arbitration_id=536870911) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) def test_rx_tx_max_timestamp(self): """ Tests the transfer with the highest possible timestamp """ msg = can.Message(timestamp=self.MAX_TIMESTAMP) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) self.assertEqual(msg.timestamp, msg_receive.timestamp) def test_rx_tx_max_timestamp_error(self): """ Tests for an exception with an out of range timestamp (max + 1) """ msg = can.Message(timestamp=self.MAX_TIMESTAMP + 1) self.assertRaises(ValueError, self.bus.send, msg) def test_rx_tx_min_timestamp(self): """ Tests the transfer with the lowest possible timestamp """ msg = can.Message(timestamp=0) self.bus.send(msg) msg_receive = self.bus.recv() self.assertMessageEqual(msg, msg_receive) self.assertEqual(msg.timestamp, msg_receive.timestamp) def test_rx_tx_min_timestamp_error(self): """ Tests for an exception with an out of range timestamp (min - 1) """ msg = can.Message(timestamp=-1) self.assertRaises(ValueError, self.bus.send, msg) def test_when_no_fileno(self): """ Tests for the fileno method catching the missing pyserial implementeation on the Windows platform """ try: fileno = self.bus.fileno() except NotImplementedError: pass # allow it to be left non-implemented for Windows platform else: fileno.__gt__ = ( lambda self, compare: True ) # Current platform implements fileno, so get the mock to respond to a greater than comparison self.assertIsNotNone(fileno) self.assertFalse( fileno == -1 ) # forcing the value to -1 is the old way of managing fileno on Windows but it is not compatible with notifiers self.assertTrue(fileno > 0) class SimpleSerialTest(unittest.TestCase, SimpleSerialTestBase): def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) SimpleSerialTestBase.__init__(self) def setUp(self): self.patcher = patch("serial.Serial") self.mock_serial = self.patcher.start() self.serial_dummy = SerialDummy() self.mock_serial.return_value.write = self.serial_dummy.write self.mock_serial.return_value.read = self.serial_dummy.read self.addCleanup(self.patcher.stop) self.bus = SerialBus("bus", timeout=TIMEOUT) def tearDown(self): self.serial_dummy.reset() class SimpleSerialLoopTest(unittest.TestCase, SimpleSerialTestBase): def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) SimpleSerialTestBase.__init__(self) def setUp(self): self.bus = SerialBus("loop://", timeout=TIMEOUT) def tearDown(self): self.bus.shutdown() if __name__ == "__main__": unittest.main() python-can-4.5.0/test/simplecyclic_test.py000066400000000000000000000267171472200326600206670ustar00rootroot00000000000000#!/usr/bin/env python """ This module tests cyclic send tasks. """ import gc import sys import time import traceback import unittest from threading import Thread from time import sleep from typing import List from unittest.mock import MagicMock import can from .config import * from .message_helper import ComparingMessagesTestCase class SimpleCyclicSendTaskTest(unittest.TestCase, ComparingMessagesTestCase): def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) ComparingMessagesTestCase.__init__( self, allowed_timestamp_delta=0.016, preserves_channel=True ) @unittest.skipIf( IS_CI, "the timing sensitive behaviour cannot be reproduced reliably on a CI server", ) def test_cycle_time(self): msg = can.Message( is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7] ) with can.interface.Bus(interface="virtual") as bus1: with can.interface.Bus(interface="virtual") as bus2: # disabling the garbage collector makes the time readings more reliable gc.disable() task = bus1.send_periodic(msg, 0.01, 1) self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) sleep(2) size = bus2.queue.qsize() # About 100 messages should have been transmitted self.assertTrue( 80 <= size <= 120, "100 +/- 20 messages should have been transmitted. But queue contained {}".format( size ), ) last_msg = bus2.recv() next_last_msg = bus2.recv() # we need to reenable the garbage collector again gc.enable() # Check consecutive messages are spaced properly in time and have # the same id/data self.assertMessageEqual(last_msg, next_last_msg) # Check the message id/data sent is the same as message received # Set timestamp and channel to match recv'd because we don't care # and they are not initialized by the can.Message constructor. msg.timestamp = last_msg.timestamp msg.channel = last_msg.channel self.assertMessageEqual(msg, last_msg) def test_removing_bus_tasks(self): bus = can.interface.Bus(interface="virtual") tasks = [] for task_i in range(10): msg = can.Message( is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7], ) msg.arbitration_id = task_i task = bus.send_periodic(msg, 0.1, 1) tasks.append(task) self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) assert len(bus._periodic_tasks) == 10 for task in tasks: # Note calling task.stop will remove the task from the Bus's internal task management list task.stop() self.join_threads([task.thread for task in tasks], 5.0) assert len(bus._periodic_tasks) == 0 bus.shutdown() def test_managed_tasks(self): bus = can.interface.Bus(interface="virtual", receive_own_messages=True) tasks = [] for task_i in range(3): msg = can.Message( is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7], ) msg.arbitration_id = task_i task = bus.send_periodic(msg, 0.1, 10, store_task=False) tasks.append(task) self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) assert len(bus._periodic_tasks) == 0 # Self managed tasks should still be sending messages for _ in range(50): received_msg = bus.recv(timeout=5.0) assert received_msg is not None assert received_msg.arbitration_id in {0, 1, 2} for task in tasks: task.stop() self.join_threads([task.thread for task in tasks], 5.0) bus.shutdown() def test_stopping_perodic_tasks(self): bus = can.interface.Bus(interface="virtual") tasks = [] for task_i in range(10): msg = can.Message( is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7], ) msg.arbitration_id = task_i task = bus.send_periodic(msg, 0.1, 1) tasks.append(task) assert len(bus._periodic_tasks) == 10 # stop half the tasks using the task object for task in tasks[::2]: task.stop() assert len(bus._periodic_tasks) == 5 # stop the other half using the bus api bus.stop_all_periodic_tasks(remove_tasks=False) self.join_threads([task.thread for task in tasks], 5.0) # Tasks stopped via `stop_all_periodic_tasks` with remove_tasks=False should # still be associated with the bus (e.g. for restarting) assert len(bus._periodic_tasks) == 5 bus.shutdown() def test_restart_perodic_tasks(self): period = 0.01 safe_timeout = period * 5 if not IS_PYPY else 1.0 duration = 0.3 msg = can.Message( is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7] ) def _read_all_messages(_bus: "can.interfaces.virtual.VirtualBus") -> None: sleep(safe_timeout) while not _bus.queue.empty(): _bus.recv(timeout=period) sleep(safe_timeout) with can.ThreadSafeBus(interface="virtual", receive_own_messages=True) as bus: task = bus.send_periodic(msg, period) self.assertIsInstance(task, can.broadcastmanager.ThreadBasedCyclicSendTask) # Test that the task is sending messages sleep(safe_timeout) assert not bus.queue.empty(), "messages should have been transmitted" # Stop the task and check that messages are no longer being sent bus.stop_all_periodic_tasks(remove_tasks=False) _read_all_messages(bus) assert bus.queue.empty(), "messages should not have been transmitted" # Restart the task and check that messages are being sent again task.start() sleep(safe_timeout) assert not bus.queue.empty(), "messages should have been transmitted" # Stop the task and check that messages are no longer being sent bus.stop_all_periodic_tasks(remove_tasks=False) _read_all_messages(bus) assert bus.queue.empty(), "messages should not have been transmitted" # Restart the task with limited duration and wait until it stops task.duration = duration task.start() sleep(duration + safe_timeout) assert task.stopped assert time.time() > task.end_time assert not bus.queue.empty(), "messages should have been transmitted" _read_all_messages(bus) assert bus.queue.empty(), "messages should not have been transmitted" # Restart the task and check that messages are being sent again task.start() sleep(safe_timeout) assert not bus.queue.empty(), "messages should have been transmitted" # Stop all tasks and wait for the thread to exit bus.stop_all_periodic_tasks() # Avoids issues where the thread is still running when the bus is shutdown self.join_threads([task.thread], 5.0) @unittest.skipIf(IS_CI, "fails randomly when run on CI server") def test_thread_based_cyclic_send_task(self): bus = can.ThreadSafeBus(interface="virtual") msg = can.Message( is_extended_id=False, arbitration_id=0x123, data=[0, 1, 2, 3, 4, 5, 6, 7] ) # good case, bus is up on_error_mock = MagicMock(return_value=False) task = can.broadcastmanager.ThreadBasedCyclicSendTask( bus=bus, lock=bus._lock_send_periodic, messages=msg, period=0.1, duration=3, on_error=on_error_mock, ) sleep(1) on_error_mock.assert_not_called() task.stop() bus.shutdown() # bus has been shut down on_error_mock = MagicMock(return_value=False) task = can.broadcastmanager.ThreadBasedCyclicSendTask( bus=bus, lock=bus._lock_send_periodic, messages=msg, period=0.1, duration=3, on_error=on_error_mock, ) sleep(1) self.assertEqual(1, on_error_mock.call_count) task.stop() # bus is still shut down, but on_error returns True on_error_mock = MagicMock(return_value=True) task = can.broadcastmanager.ThreadBasedCyclicSendTask( bus=bus, lock=bus._lock_send_periodic, messages=msg, period=0.1, duration=3, on_error=on_error_mock, ) sleep(1) self.assertTrue(on_error_mock.call_count > 1) task.stop() def test_modifier_callback(self) -> None: msg_list: List[can.Message] = [] def increment_first_byte(msg: can.Message) -> None: msg.data[0] = (msg.data[0] + 1) % 256 original_msg = can.Message( is_extended_id=False, arbitration_id=0x123, data=[0] * 8 ) with can.ThreadSafeBus(interface="virtual", receive_own_messages=True) as bus: notifier = can.Notifier(bus=bus, listeners=[msg_list.append]) task = bus.send_periodic( msgs=original_msg, period=0.001, modifier_callback=increment_first_byte ) time.sleep(0.2) task.stop() notifier.stop() self.assertEqual(b"\x01\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[0].data)) self.assertEqual(b"\x02\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[1].data)) self.assertEqual(b"\x03\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[2].data)) self.assertEqual(b"\x04\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[3].data)) self.assertEqual(b"\x05\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[4].data)) self.assertEqual(b"\x06\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[5].data)) self.assertEqual(b"\x07\x00\x00\x00\x00\x00\x00\x00", bytes(msg_list[6].data)) @staticmethod def join_threads(threads: List[Thread], timeout: float) -> None: stuck_threads: List[Thread] = [] t0 = time.perf_counter() for thread in threads: time_left = timeout - (time.perf_counter() - t0) if time_left > 0.0: thread.join(time_left) if thread.is_alive(): if platform.python_implementation() == "CPython": # print thread frame to help with debugging frame = sys._current_frames()[thread.ident] traceback.print_stack(frame, file=sys.stderr) stuck_threads.append(thread) if stuck_threads: err_message = ( f"Threads did not stop within {timeout:.1f} seconds: " f"[{', '.join([str(t) for t in stuck_threads])}]" ) raise RuntimeError(err_message) if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_bit_timing.py000066400000000000000000000371341472200326600203270ustar00rootroot00000000000000#!/usr/bin/env python import struct import pytest import can from can.interfaces.pcan.pcan import PCAN_BITRATES def test_sja1000(): """Test some values obtained using other bit timing calculators.""" timing = can.BitTiming( f_clock=8_000_000, brp=4, tseg1=11, tseg2=4, sjw=2, nof_samples=3, strict=True ) assert timing.f_clock == 8_000_000 assert timing.bitrate == 125_000 assert timing.brp == 4 assert timing.nbt == 16 assert timing.tseg1 == 11 assert timing.tseg2 == 4 assert timing.sjw == 2 assert timing.nof_samples == 3 assert timing.sample_point == 75 assert timing.btr0 == 0x43 assert timing.btr1 == 0xBA timing = can.BitTiming( f_clock=8_000_000, brp=1, tseg1=13, tseg2=2, sjw=1, strict=True ) assert timing.f_clock == 8_000_000 assert timing.bitrate == 500_000 assert timing.brp == 1 assert timing.nbt == 16 assert timing.tseg1 == 13 assert timing.tseg2 == 2 assert timing.sjw == 1 assert timing.nof_samples == 1 assert timing.sample_point == 87.5 assert timing.btr0 == 0x00 assert timing.btr1 == 0x1C timing = can.BitTiming( f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1, strict=True ) assert timing.f_clock == 8_000_000 assert timing.bitrate == 1_000_000 assert timing.brp == 1 assert timing.nbt == 8 assert timing.tseg1 == 5 assert timing.tseg2 == 2 assert timing.sjw == 1 assert timing.nof_samples == 1 assert timing.sample_point == 75 assert timing.btr0 == 0x00 assert timing.btr1 == 0x14 def test_from_bitrate_and_segments(): timing = can.BitTiming.from_bitrate_and_segments( f_clock=8_000_000, bitrate=125_000, tseg1=11, tseg2=4, sjw=2, nof_samples=3 ) assert timing.f_clock == 8_000_000 assert timing.bitrate == 125_000 assert timing.brp == 4 assert timing.nbt == 16 assert timing.tseg1 == 11 assert timing.tseg2 == 4 assert timing.sjw == 2 assert timing.nof_samples == 3 assert timing.sample_point == 75 assert timing.btr0 == 0x43 assert timing.btr1 == 0xBA timing = can.BitTiming.from_bitrate_and_segments( f_clock=8_000_000, bitrate=500_000, tseg1=13, tseg2=2, sjw=1 ) assert timing.f_clock == 8_000_000 assert timing.bitrate == 500_000 assert timing.brp == 1 assert timing.nbt == 16 assert timing.tseg1 == 13 assert timing.tseg2 == 2 assert timing.sjw == 1 assert timing.nof_samples == 1 assert timing.sample_point == 87.5 assert timing.btr0 == 0x00 assert timing.btr1 == 0x1C timing = can.BitTiming.from_bitrate_and_segments( f_clock=8_000_000, bitrate=1_000_000, tseg1=5, tseg2=2, sjw=1, strict=True ) assert timing.f_clock == 8_000_000 assert timing.bitrate == 1_000_000 assert timing.brp == 1 assert timing.nbt == 8 assert timing.tseg1 == 5 assert timing.tseg2 == 2 assert timing.sjw == 1 assert timing.nof_samples == 1 assert timing.sample_point == 75 assert timing.btr0 == 0x00 assert timing.btr1 == 0x14 timing = can.BitTimingFd.from_bitrate_and_segments( f_clock=80_000_000, nom_bitrate=500_000, nom_tseg1=119, nom_tseg2=40, nom_sjw=40, data_bitrate=2_000_000, data_tseg1=29, data_tseg2=10, data_sjw=10, ) assert timing.f_clock == 80_000_000 assert timing.nom_bitrate == 500_000 assert timing.nom_brp == 1 assert timing.nbt == 160 assert timing.nom_tseg1 == 119 assert timing.nom_tseg2 == 40 assert timing.nom_sjw == 40 assert timing.nom_sample_point == 75 assert timing.f_clock == 80_000_000 assert timing.data_bitrate == 2_000_000 assert timing.data_brp == 1 assert timing.dbt == 40 assert timing.data_tseg1 == 29 assert timing.data_tseg2 == 10 assert timing.data_sjw == 10 assert timing.data_sample_point == 75 # test strict invalid with pytest.raises(ValueError): can.BitTimingFd.from_bitrate_and_segments( f_clock=80_000_000, nom_bitrate=500_000, nom_tseg1=119, nom_tseg2=40, nom_sjw=40, data_bitrate=2_000_000, data_tseg1=29, data_tseg2=10, data_sjw=10, strict=True, ) def test_can_fd(): # test non-strict timing = can.BitTimingFd( f_clock=80_000_000, nom_brp=1, nom_tseg1=119, nom_tseg2=40, nom_sjw=40, data_brp=1, data_tseg1=29, data_tseg2=10, data_sjw=10, ) assert timing.f_clock == 80_000_000 assert timing.nom_bitrate == 500_000 assert timing.nom_brp == 1 assert timing.nbt == 160 assert timing.nom_tseg1 == 119 assert timing.nom_tseg2 == 40 assert timing.nom_sjw == 40 assert timing.nom_sample_point == 75 assert timing.data_bitrate == 2_000_000 assert timing.data_brp == 1 assert timing.dbt == 40 assert timing.data_tseg1 == 29 assert timing.data_tseg2 == 10 assert timing.data_sjw == 10 assert timing.data_sample_point == 75 # test strict invalid with pytest.raises(ValueError): can.BitTimingFd( f_clock=80_000_000, nom_brp=1, nom_tseg1=119, nom_tseg2=40, nom_sjw=40, data_brp=1, data_tseg1=29, data_tseg2=10, data_sjw=10, strict=True, ) # test strict valid timing = can.BitTimingFd( f_clock=80_000_000, nom_brp=2, nom_tseg1=59, nom_tseg2=20, nom_sjw=20, data_brp=2, data_tseg1=14, data_tseg2=5, data_sjw=5, strict=True, ) assert timing.f_clock == 80_000_000 assert timing.nom_bitrate == 500_000 assert timing.nom_brp == 2 assert timing.nbt == 80 assert timing.nom_tseg1 == 59 assert timing.nom_tseg2 == 20 assert timing.nom_sjw == 20 assert timing.nom_sample_point == 75 assert timing.data_bitrate == 2_000_000 assert timing.data_brp == 2 assert timing.dbt == 20 assert timing.data_tseg1 == 14 assert timing.data_tseg2 == 5 assert timing.data_sjw == 5 assert timing.data_sample_point == 75 def test_from_btr(): timing = can.BitTiming.from_registers(f_clock=8_000_000, btr0=0x00, btr1=0x14) assert timing.bitrate == 1_000_000 assert timing.brp == 1 assert timing.nbt == 8 assert timing.tseg1 == 5 assert timing.tseg2 == 2 assert timing.sjw == 1 assert timing.sample_point == 75 assert timing.btr0 == 0x00 assert timing.btr1 == 0x14 def test_btr_persistence(): f_clock = 8_000_000 for btr0btr1 in PCAN_BITRATES.values(): btr0, btr1 = struct.pack(">H", btr0btr1.value) t = can.BitTiming.from_registers(f_clock, btr0, btr1) assert t.btr0 == btr0 assert t.btr1 == btr1 def test_from_sample_point(): timing = can.BitTiming.from_sample_point( f_clock=16_000_000, bitrate=500_000, sample_point=69.0, ) assert timing.f_clock == 16_000_000 assert timing.bitrate == 500_000 assert 68 < timing.sample_point < 70 fd_timing = can.BitTimingFd.from_sample_point( f_clock=80_000_000, nom_bitrate=1_000_000, nom_sample_point=75.0, data_bitrate=8_000_000, data_sample_point=70.0, ) assert fd_timing.f_clock == 80_000_000 assert fd_timing.nom_bitrate == 1_000_000 assert 74 < fd_timing.nom_sample_point < 76 assert fd_timing.data_bitrate == 8_000_000 assert 69 < fd_timing.data_sample_point < 71 # check that there is a solution for every sample point for sp in range(50, 100): can.BitTiming.from_sample_point( f_clock=16_000_000, bitrate=500_000, sample_point=sp ) # check that there is a solution for every sample point for nsp in range(50, 100): for dsp in range(50, 100): can.BitTimingFd.from_sample_point( f_clock=80_000_000, nom_bitrate=500_000, nom_sample_point=nsp, data_bitrate=2_000_000, data_sample_point=dsp, ) def test_iterate_from_sample_point(): for sp in range(50, 100): solutions = list( can.BitTiming.iterate_from_sample_point( f_clock=16_000_000, bitrate=500_000, sample_point=sp, ) ) assert len(solutions) >= 2 for nsp in range(50, 100): for dsp in range(50, 100): solutions = list( can.BitTimingFd.iterate_from_sample_point( f_clock=80_000_000, nom_bitrate=500_000, nom_sample_point=nsp, data_bitrate=2_000_000, data_sample_point=dsp, ) ) assert len(solutions) >= 2 def test_equality(): t1 = can.BitTiming.from_registers(f_clock=8_000_000, btr0=0x00, btr1=0x14) t2 = can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1, nof_samples=1) t3 = can.BitTiming( f_clock=16_000_000, brp=2, tseg1=5, tseg2=2, sjw=1, nof_samples=1 ) assert t1 == t2 assert t1 != t3 assert t2 != t3 assert t1 != 10 t4 = can.BitTimingFd( f_clock=80_000_000, nom_brp=1, nom_tseg1=119, nom_tseg2=40, nom_sjw=40, data_brp=1, data_tseg1=29, data_tseg2=10, data_sjw=10, ) t5 = can.BitTimingFd( f_clock=80_000_000, nom_brp=1, nom_tseg1=119, nom_tseg2=40, nom_sjw=40, data_brp=1, data_tseg1=29, data_tseg2=10, data_sjw=10, ) t6 = can.BitTimingFd.from_sample_point( f_clock=80_000_000, nom_bitrate=1_000_000, nom_sample_point=75.0, data_bitrate=8_000_000, data_sample_point=70.0, ) assert t4 == t5 assert t4 != t6 assert t4 != t1 def test_string_representation(): timing = can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1) assert str(timing) == ( "BR: 1_000_000 bit/s, SP: 75.00%, BRP: 1, TSEG1: 5, TSEG2: 2, SJW: 1, " "BTR: 0014h, CLK: 8MHz" ) fd_timing = can.BitTimingFd( f_clock=80_000_000, nom_brp=1, nom_tseg1=119, nom_tseg2=40, nom_sjw=40, data_brp=1, data_tseg1=29, data_tseg2=10, data_sjw=10, ) assert str(fd_timing) == ( "NBR: 500_000 bit/s, NSP: 75.00%, NBRP: 1, NTSEG1: 119, NTSEG2: 40, NSJW: 40, " "DBR: 2_000_000 bit/s, DSP: 75.00%, DBRP: 1, DTSEG1: 29, DTSEG2: 10, DSJW: 10, " "CLK: 80MHz" ) def test_repr(): timing = can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1) assert repr(timing) == ( "can.BitTiming(f_clock=8000000, brp=1, tseg1=5, tseg2=2, sjw=1, nof_samples=1)" ) fd_timing = can.BitTimingFd( f_clock=80_000_000, nom_brp=1, nom_tseg1=119, nom_tseg2=40, nom_sjw=40, data_brp=1, data_tseg1=29, data_tseg2=10, data_sjw=10, ) assert repr(fd_timing) == ( "can.BitTimingFd(f_clock=80000000, nom_brp=1, nom_tseg1=119, nom_tseg2=40, " "nom_sjw=40, data_brp=1, data_tseg1=29, data_tseg2=10, data_sjw=10)" ) def test_hash(): _timings = { can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1, nof_samples=1), can.BitTimingFd( f_clock=80_000_000, nom_brp=1, nom_tseg1=119, nom_tseg2=40, nom_sjw=40, data_brp=1, data_tseg1=29, data_tseg2=10, data_sjw=10, ), } def test_mapping(): timing = can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1) timing_dict = dict(timing) assert timing_dict["f_clock"] == timing["f_clock"] assert timing_dict["brp"] == timing["brp"] assert timing_dict["tseg1"] == timing["tseg1"] assert timing_dict["tseg2"] == timing["tseg2"] assert timing_dict["sjw"] == timing["sjw"] assert timing == can.BitTiming(**timing_dict) fd_timing = can.BitTimingFd( f_clock=80_000_000, nom_brp=1, nom_tseg1=119, nom_tseg2=40, nom_sjw=40, data_brp=1, data_tseg1=29, data_tseg2=10, data_sjw=10, ) fd_timing_dict = dict(fd_timing) assert fd_timing_dict["f_clock"] == fd_timing["f_clock"] assert fd_timing_dict["nom_brp"] == fd_timing["nom_brp"] assert fd_timing_dict["nom_tseg1"] == fd_timing["nom_tseg1"] assert fd_timing_dict["nom_tseg2"] == fd_timing["nom_tseg2"] assert fd_timing_dict["nom_sjw"] == fd_timing["nom_sjw"] assert fd_timing_dict["data_brp"] == fd_timing["data_brp"] assert fd_timing_dict["data_tseg1"] == fd_timing["data_tseg1"] assert fd_timing_dict["data_tseg2"] == fd_timing["data_tseg2"] assert fd_timing_dict["data_sjw"] == fd_timing["data_sjw"] assert fd_timing == can.BitTimingFd(**fd_timing_dict) def test_oscillator_tolerance(): timing = can.BitTiming(f_clock=16_000_000, brp=2, tseg1=10, tseg2=5, sjw=4) osc_tol = timing.oscillator_tolerance( node_loop_delay_ns=250, bus_length_m=10.0, ) assert osc_tol == pytest.approx(1.23, abs=1e-2) fd_timing = can.BitTimingFd( f_clock=80_000_000, nom_brp=5, nom_tseg1=27, nom_tseg2=4, nom_sjw=4, data_brp=5, data_tseg1=6, data_tseg2=1, data_sjw=1, ) osc_tol = fd_timing.oscillator_tolerance( node_loop_delay_ns=250, bus_length_m=10.0, ) assert osc_tol == pytest.approx(0.48, abs=1e-2) def test_recreate_with_f_clock(): timing_8mhz = can.BitTiming(f_clock=8_000_000, brp=1, tseg1=5, tseg2=2, sjw=1) timing_16mhz = timing_8mhz.recreate_with_f_clock(f_clock=16_000_000) assert timing_8mhz.bitrate == timing_16mhz.bitrate assert timing_8mhz.sample_point == timing_16mhz.sample_point assert (timing_8mhz.sjw / timing_8mhz.nbt) == pytest.approx( timing_16mhz.sjw / timing_16mhz.nbt, abs=1e-3 ) assert timing_8mhz.nof_samples == timing_16mhz.nof_samples timing_16mhz = can.BitTiming( f_clock=16000000, brp=2, tseg1=12, tseg2=3, sjw=3, nof_samples=1 ) timing_8mhz = timing_16mhz.recreate_with_f_clock(f_clock=8_000_000) assert timing_8mhz.bitrate == timing_16mhz.bitrate assert timing_8mhz.sample_point == timing_16mhz.sample_point assert (timing_8mhz.sjw / timing_8mhz.nbt) == pytest.approx( timing_16mhz.sjw / timing_16mhz.nbt, abs=1e-2 ) assert timing_8mhz.nof_samples == timing_16mhz.nof_samples fd_timing_80mhz = can.BitTimingFd( f_clock=80_000_000, nom_brp=5, nom_tseg1=27, nom_tseg2=4, nom_sjw=4, data_brp=5, data_tseg1=6, data_tseg2=1, data_sjw=1, ) fd_timing_60mhz = fd_timing_80mhz.recreate_with_f_clock(f_clock=60_000_000) assert fd_timing_80mhz.nom_bitrate == fd_timing_60mhz.nom_bitrate assert fd_timing_80mhz.nom_sample_point == pytest.approx( fd_timing_60mhz.nom_sample_point, abs=1.0 ) assert (fd_timing_80mhz.nom_sjw / fd_timing_80mhz.nbt) == pytest.approx( fd_timing_60mhz.nom_sjw / fd_timing_60mhz.nbt, abs=1e-2 ) assert fd_timing_80mhz.data_bitrate == fd_timing_60mhz.data_bitrate assert fd_timing_80mhz.data_sample_point == pytest.approx( fd_timing_60mhz.data_sample_point, abs=1.0 ) assert (fd_timing_80mhz.data_sjw / fd_timing_80mhz.dbt) == pytest.approx( fd_timing_60mhz.data_sjw / fd_timing_60mhz.dbt, abs=1e-2 ) python-can-4.5.0/test/test_bus.py000066400000000000000000000011151472200326600167610ustar00rootroot00000000000000import gc from unittest.mock import patch import can def test_bus_ignore_config(): with patch.object( target=can.util, attribute="load_config", side_effect=can.util.load_config ): _ = can.Bus(interface="virtual", ignore_config=True) assert not can.util.load_config.called _ = can.Bus(interface="virtual") assert can.util.load_config.called @patch.object(can.bus.BusABC, "shutdown") def test_bus_attempts_self_cleanup(mock_shutdown): bus = can.Bus(interface="virtual") del bus gc.collect() mock_shutdown.assert_called() python-can-4.5.0/test/test_cantact.py000066400000000000000000000043441472200326600176140ustar00rootroot00000000000000#!/usr/bin/env python """ Tests for CANtact interfaces """ import unittest import can from can.interfaces import cantact class CantactTest(unittest.TestCase): def test_bus_creation(self): bus = can.Bus(channel=0, interface="cantact", _testing=True) self.assertIsInstance(bus, cantact.CantactBus) self.assertEqual(bus.protocol, can.CanProtocol.CAN_20) cantact.MockInterface.set_bitrate.assert_called() cantact.MockInterface.set_bit_timing.assert_not_called() cantact.MockInterface.set_enabled.assert_called() cantact.MockInterface.set_monitor.assert_called() cantact.MockInterface.start.assert_called() def test_bus_creation_bittiming(self): cantact.MockInterface.set_bitrate.reset_mock() bt = can.BitTiming(f_clock=24_000_000, brp=3, tseg1=13, tseg2=2, sjw=1) bus = can.Bus(channel=0, interface="cantact", timing=bt, _testing=True) self.assertIsInstance(bus, cantact.CantactBus) self.assertEqual(bus.protocol, can.CanProtocol.CAN_20) cantact.MockInterface.set_bitrate.assert_not_called() cantact.MockInterface.set_bit_timing.assert_called() cantact.MockInterface.set_enabled.assert_called() cantact.MockInterface.set_monitor.assert_called() cantact.MockInterface.start.assert_called() def test_transmit(self): bus = can.Bus(channel=0, interface="cantact", _testing=True) msg = can.Message( arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True ) bus.send(msg) cantact.MockInterface.send.assert_called() def test_recv(self): bus = can.Bus(channel=0, interface="cantact", _testing=True) frame = bus.recv(timeout=0.5) cantact.MockInterface.recv.assert_called() self.assertIsInstance(frame, can.Message) def test_recv_timeout(self): bus = can.Bus(channel=0, interface="cantact", _testing=True) frame = bus.recv(timeout=0.0) cantact.MockInterface.recv.assert_called() self.assertIsNone(frame) def test_shutdown(self): bus = can.Bus(channel=0, interface="cantact", _testing=True) bus.shutdown() cantact.MockInterface.stop.assert_called() python-can-4.5.0/test/test_cyclic_socketcan.py000066400000000000000000000533261472200326600215030ustar00rootroot00000000000000#!/usr/bin/env python """ This module tests multiple message cyclic send tasks. """ import time import unittest import can from .config import TEST_INTERFACE_SOCKETCAN @unittest.skipUnless(TEST_INTERFACE_SOCKETCAN, "skip testing of socketcan") class CyclicSocketCan(unittest.TestCase): BITRATE = 500000 TIMEOUT = 0.1 INTERFACE_1 = "socketcan" CHANNEL_1 = "vcan0" INTERFACE_2 = "socketcan" CHANNEL_2 = "vcan0" PERIOD = 1.0 DELTA = 0.01 def _find_start_index(self, tx_messages, message): """ :param tx_messages: The list of messages that were passed to the periodic backend :param message: The message whose data we wish to match and align to :returns: start index in the tx_messages """ start_index = -1 for index, tx_message in enumerate(tx_messages): if tx_message.data == message.data: start_index = index break return start_index def setUp(self): self._send_bus = can.Bus( interface=self.INTERFACE_1, channel=self.CHANNEL_1, bitrate=self.BITRATE ) self._recv_bus = can.Bus( interface=self.INTERFACE_2, channel=self.CHANNEL_2, bitrate=self.BITRATE ) def tearDown(self): self._send_bus.shutdown() self._recv_bus.shutdown() def test_cyclic_initializer_list(self): messages = [] messages.append( can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) ) messages.append( can.Message( arbitration_id=0x401, data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], is_extended_id=False, ) ) messages.append( can.Message( arbitration_id=0x401, data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33], is_extended_id=False, ) ) messages.append( can.Message( arbitration_id=0x401, data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44], is_extended_id=False, ) ) messages.append( can.Message( arbitration_id=0x401, data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55], is_extended_id=False, ) ) task = self._send_bus.send_periodic(messages, self.PERIOD) self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) results = [] for _ in range(len(messages) * 2): result = self._recv_bus.recv(self.PERIOD * 2) if result: results.append(result) task.stop() # Find starting index for each start_index = self._find_start_index(messages, results[0]) self.assertTrue(start_index != -1) # Now go through the partitioned results and assert that they're equal for rx_index, rx_message in enumerate(results): tx_message = messages[start_index] self.assertIsNotNone(rx_message) self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) self.assertEqual(tx_message.dlc, rx_message.dlc) self.assertEqual(tx_message.data, rx_message.data) self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) self.assertEqual(tx_message.is_fd, rx_message.is_fd) start_index = (start_index + 1) % len(messages) def test_cyclic_initializer_tuple(self): messages = [] messages.append( can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) ) messages.append( can.Message( arbitration_id=0x401, data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], is_extended_id=False, ) ) messages.append( can.Message( arbitration_id=0x401, data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33], is_extended_id=False, ) ) messages.append( can.Message( arbitration_id=0x401, data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44], is_extended_id=False, ) ) messages.append( can.Message( arbitration_id=0x401, data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55], is_extended_id=False, ) ) messages = tuple(messages) self.assertIsInstance(messages, tuple) task = self._send_bus.send_periodic(messages, self.PERIOD) self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) results = [] for _ in range(len(messages) * 2): result = self._recv_bus.recv(self.PERIOD * 2) if result: results.append(result) task.stop() # Find starting index for each start_index = self._find_start_index(messages, results[0]) self.assertTrue(start_index != -1) # Now go through the partitioned results and assert that they're equal for rx_index, rx_message in enumerate(results): tx_message = messages[start_index] self.assertIsNotNone(rx_message) self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) self.assertEqual(tx_message.dlc, rx_message.dlc) self.assertEqual(tx_message.data, rx_message.data) self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) self.assertEqual(tx_message.is_fd, rx_message.is_fd) start_index = (start_index + 1) % len(messages) def test_cyclic_initializer_message(self): message = can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) task = self._send_bus.send_periodic(message, self.PERIOD) self.assertIsInstance(task, can.broadcastmanager.CyclicSendTaskABC) # Take advantage of kernel's queueing mechanisms time.sleep(4 * self.PERIOD) task.stop() for _ in range(4): tx_message = message rx_message = self._recv_bus.recv(self.TIMEOUT) self.assertIsNotNone(rx_message) self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) self.assertEqual(tx_message.dlc, rx_message.dlc) self.assertEqual(tx_message.data, rx_message.data) self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) self.assertEqual(tx_message.is_fd, rx_message.is_fd) def test_cyclic_initializer_invalid_none(self): with self.assertRaises(ValueError): task = self._send_bus.send_periodic(None, self.PERIOD) def test_cyclic_initializer_invalid_empty_list(self): with self.assertRaises(ValueError): task = self._send_bus.send_periodic([], self.PERIOD) def test_cyclic_initializer_different_arbitration_ids(self): messages = [] messages.append( can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) ) messages.append( can.Message( arbitration_id=0x3E1, data=[0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE], is_extended_id=False, ) ) with self.assertRaises(ValueError): task = self._send_bus.send_periodic(messages, self.PERIOD) def test_start_already_started_task(self): messages_a = can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) task_a = self._send_bus.send_periodic(messages_a, self.PERIOD) time.sleep(0.1) # Task restarting is permitted as of #1440 task_a.start() task_a.stop() def test_create_same_id(self): messages_a = can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) messages_b = can.Message( arbitration_id=0x401, data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], is_extended_id=False, ) task_a = self._send_bus.send_periodic(messages_a, self.PERIOD) self.assertIsInstance(task_a, can.broadcastmanager.CyclicSendTaskABC) task_b = self._send_bus.send_periodic(messages_b, self.PERIOD) self.assertIsInstance(task_b, can.broadcastmanager.CyclicSendTaskABC) time.sleep(self.PERIOD * 4) task_a.stop() task_b.stop() msgs = [] for _ in range(4): msg = self._recv_bus.recv(self.PERIOD * 2) self.assertIsNotNone(msg) msgs.append(msg) self.assertTrue(len(msgs) >= 4) # Both messages should be recevied on the bus, # even with the same arbitration id msg_a_data_present = msg_b_data_present = False for rx_message in msgs: self.assertTrue( rx_message.arbitration_id == messages_a.arbitration_id == messages_b.arbitration_id ) if rx_message.data == messages_a.data: msg_a_data_present = True if rx_message.data == messages_b.data: msg_b_data_present = True self.assertTrue(msg_a_data_present) self.assertTrue(msg_b_data_present) def test_modify_data_list(self): messages_odd = [] messages_odd.append( can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) ) messages_odd.append( can.Message( arbitration_id=0x401, data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33], is_extended_id=False, ) ) messages_odd.append( can.Message( arbitration_id=0x401, data=[0x55, 0x55, 0x55, 0x55, 0x55, 0x55], is_extended_id=False, ) ) messages_even = [] messages_even.append( can.Message( arbitration_id=0x401, data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], is_extended_id=False, ) ) messages_even.append( can.Message( arbitration_id=0x401, data=[0x44, 0x44, 0x44, 0x44, 0x44, 0x44], is_extended_id=False, ) ) messages_even.append( can.Message( arbitration_id=0x401, data=[0x66, 0x66, 0x66, 0x66, 0x66, 0x66], is_extended_id=False, ) ) task = self._send_bus.send_periodic(messages_odd, self.PERIOD) self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC) results_odd = [] results_even = [] for _ in range(len(messages_odd) * 2): result = self._recv_bus.recv(self.PERIOD * 2) if result: results_odd.append(result) task.modify_data(messages_even) for _ in range(len(messages_even) * 2): result = self._recv_bus.recv(self.PERIOD * 2) if result: results_even.append(result) task.stop() # Make sure we received some messages self.assertTrue(len(results_even) != 0) self.assertTrue(len(results_odd) != 0) # Find starting index for each start_index_even = self._find_start_index(messages_even, results_even[0]) self.assertTrue(start_index_even != -1) start_index_odd = self._find_start_index(messages_odd, results_odd[0]) self.assertTrue(start_index_odd != -1) # Now go through the partitioned results and assert that they're equal for rx_index, rx_message in enumerate(results_even): tx_message = messages_even[start_index_even] self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) self.assertEqual(tx_message.dlc, rx_message.dlc) self.assertEqual(tx_message.data, rx_message.data) self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) self.assertEqual(tx_message.is_fd, rx_message.is_fd) start_index_even = (start_index_even + 1) % len(messages_even) if rx_index != 0: prev_rx_message = results_even[rx_index - 1] # Assert timestamps are within the expected period self.assertTrue( abs( (rx_message.timestamp - prev_rx_message.timestamp) - self.PERIOD ) <= self.DELTA ) for rx_index, rx_message in enumerate(results_odd): tx_message = messages_odd[start_index_odd] self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) self.assertEqual(tx_message.dlc, rx_message.dlc) self.assertEqual(tx_message.data, rx_message.data) self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) self.assertEqual(tx_message.is_fd, rx_message.is_fd) start_index_odd = (start_index_odd + 1) % len(messages_odd) if rx_index != 0: prev_rx_message = results_odd[rx_index - 1] # Assert timestamps are within the expected period self.assertTrue( abs( (rx_message.timestamp - prev_rx_message.timestamp) - self.PERIOD ) <= self.DELTA ) def test_modify_data_message(self): message_odd = can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) message_even = can.Message( arbitration_id=0x401, data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], is_extended_id=False, ) task = self._send_bus.send_periodic(message_odd, self.PERIOD) self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC) results_odd = [] results_even = [] for _ in range(1 * 4): result = self._recv_bus.recv(self.PERIOD * 2) if result: results_odd.append(result) task.modify_data(message_even) for _ in range(1 * 4): result = self._recv_bus.recv(self.PERIOD * 2) if result: results_even.append(result) task.stop() # Now go through the partitioned results and assert that they're equal for rx_index, rx_message in enumerate(results_even): tx_message = message_even self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) self.assertEqual(tx_message.dlc, rx_message.dlc) self.assertEqual(tx_message.data, rx_message.data) self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) self.assertEqual(tx_message.is_fd, rx_message.is_fd) if rx_index != 0: prev_rx_message = results_even[rx_index - 1] # Assert timestamps are within the expected period self.assertTrue( abs( (rx_message.timestamp - prev_rx_message.timestamp) - self.PERIOD ) <= self.DELTA ) for rx_index, rx_message in enumerate(results_odd): tx_message = message_odd self.assertEqual(tx_message.arbitration_id, rx_message.arbitration_id) self.assertEqual(tx_message.dlc, rx_message.dlc) self.assertEqual(tx_message.data, rx_message.data) self.assertEqual(tx_message.is_extended_id, rx_message.is_extended_id) self.assertEqual(tx_message.is_remote_frame, rx_message.is_remote_frame) self.assertEqual(tx_message.is_error_frame, rx_message.is_error_frame) self.assertEqual(tx_message.is_fd, rx_message.is_fd) if rx_index != 0: prev_rx_message = results_odd[rx_index - 1] # Assert timestamps are within the expected period self.assertTrue( abs( (rx_message.timestamp - prev_rx_message.timestamp) - self.PERIOD ) <= self.DELTA ) def test_modify_data_invalid(self): message = can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) task = self._send_bus.send_periodic(message, self.PERIOD) self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC) time.sleep(2 * self.PERIOD) with self.assertRaises(ValueError): task.modify_data(None) def test_modify_data_unequal_lengths(self): message = can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) new_messages = [] new_messages.append( can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) ) new_messages.append( can.Message( arbitration_id=0x401, data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], is_extended_id=False, ) ) task = self._send_bus.send_periodic(message, self.PERIOD) self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC) time.sleep(2 * self.PERIOD) with self.assertRaises(ValueError): task.modify_data(new_messages) def test_modify_data_different_arbitration_id_than_original(self): old_message = can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) new_message = can.Message( arbitration_id=0x3E1, data=[0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE], is_extended_id=False, ) task = self._send_bus.send_periodic(old_message, self.PERIOD) self.assertIsInstance(task, can.broadcastmanager.ModifiableCyclicTaskABC) time.sleep(2 * self.PERIOD) with self.assertRaises(ValueError): task.modify_data(new_message) def test_stop_all_periodic_tasks_and_remove_task(self): message_a = can.Message( arbitration_id=0x401, data=[0x11, 0x11, 0x11, 0x11, 0x11, 0x11], is_extended_id=False, ) message_b = can.Message( arbitration_id=0x402, data=[0x22, 0x22, 0x22, 0x22, 0x22, 0x22], is_extended_id=False, ) message_c = can.Message( arbitration_id=0x403, data=[0x33, 0x33, 0x33, 0x33, 0x33, 0x33], is_extended_id=False, ) # Start Tasks task_a = self._send_bus.send_periodic(message_a, self.PERIOD) task_b = self._send_bus.send_periodic(message_b, self.PERIOD) task_c = self._send_bus.send_periodic(message_c, self.PERIOD) self.assertIsInstance(task_a, can.broadcastmanager.ModifiableCyclicTaskABC) self.assertIsInstance(task_b, can.broadcastmanager.ModifiableCyclicTaskABC) self.assertIsInstance(task_c, can.broadcastmanager.ModifiableCyclicTaskABC) for _ in range(6): _ = self._recv_bus.recv(self.PERIOD) # Stop all tasks and delete self._send_bus.stop_all_periodic_tasks(remove_tasks=True) # Now wait for a few periods, after which we should definitely not # receive any CAN messages time.sleep(4 * self.PERIOD) # If we successfully deleted everything, then we will eventually read # 0 messages. successfully_stopped = False for _ in range(6): rx_message = self._recv_bus.recv(self.PERIOD) if rx_message is None: successfully_stopped = True break self.assertTrue(successfully_stopped, "Still received messages after stopping") # None of the tasks should still be associated with the bus self.assertEqual(0, len(self._send_bus._periodic_tasks)) if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_detect_available_configs.py000066400000000000000000000045541472200326600231620ustar00rootroot00000000000000#!/usr/bin/env python """ This module tests :meth:`can.BusABC._detect_available_configs` and :meth:`can.BusABC.detect_available_configs`. """ import unittest from can import detect_available_configs from .config import IS_CI, IS_UNIX, TEST_INTERFACE_SOCKETCAN class TestDetectAvailableConfigs(unittest.TestCase): def test_count_returned(self): # At least virtual has to always return at least one interface self.assertGreaterEqual(len(detect_available_configs()), 1) self.assertEqual(len(detect_available_configs(interfaces=[])), 0) self.assertGreaterEqual(len(detect_available_configs(interfaces="virtual")), 1) self.assertGreaterEqual( len(detect_available_configs(interfaces=["virtual"])), 1 ) self.assertGreaterEqual(len(detect_available_configs(interfaces=None)), 1) def test_general_values(self): configs = detect_available_configs() for config in configs: self.assertIn("interface", config) self.assertIn("channel", config) def test_content_virtual(self): configs = detect_available_configs(interfaces="virtual") self.assertGreaterEqual(len(configs), 1) for config in configs: self.assertEqual(config["interface"], "virtual") def test_content_udp_multicast(self): configs = detect_available_configs(interfaces="udp_multicast") for config in configs: self.assertEqual(config["interface"], "udp_multicast") def test_content_socketcan(self): configs = detect_available_configs(interfaces="socketcan") for config in configs: self.assertEqual(config["interface"], "socketcan") def test_count_udp_multicast(self): configs = detect_available_configs(interfaces="udp_multicast") if IS_UNIX: self.assertGreaterEqual(len(configs), 2) else: self.assertEqual(len(configs), 0) @unittest.skipUnless( TEST_INTERFACE_SOCKETCAN and IS_CI, "this setup is very specific" ) def test_socketcan_on_ci_server(self): configs = detect_available_configs(interfaces="socketcan") self.assertGreaterEqual(len(configs), 1) self.assertIn("vcan0", [config["channel"] for config in configs]) # see TestSocketCanHelpers.test_find_available_interfaces() too if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_interface.py000066400000000000000000000015001472200326600201260ustar00rootroot00000000000000import importlib from unittest.mock import patch import pytest import can from can.interfaces import BACKENDS @pytest.fixture(params=(BACKENDS.keys())) def constructor(request): mod, cls = BACKENDS[request.param] try: module = importlib.import_module(mod) constructor = getattr(module, cls) except: pytest.skip("Unable to load interface") return constructor @pytest.fixture def interface(constructor): class MockInterface(constructor): def __init__(self): pass def __del__(self): pass return MockInterface() @patch.object(can.bus.BusABC, "shutdown") def test_interface_calls_parent_shutdown(mock_shutdown, interface): try: interface.shutdown() except: pass finally: mock_shutdown.assert_called() python-can-4.5.0/test/test_interface_canalystii.py000077500000000000000000000067701472200326600223670ustar00rootroot00000000000000#!/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() python-can-4.5.0/test/test_interface_ixxat.py000066400000000000000000000045051472200326600213530ustar00rootroot00000000000000#!/usr/bin/env python """ Unittest for ixxat interface. Run only this test: python setup.py test --addopts "--verbose -s test/test_interface_ixxat.py" """ import unittest import can class SoftwareTestCase(unittest.TestCase): """ Test cases that test the software only and do not rely on an existing/connected hardware. """ def setUp(self): try: bus = can.Bus(interface="ixxat", channel=0) bus.shutdown() except can.CanInterfaceNotImplementedError: raise unittest.SkipTest("not available on this platform") def test_bus_creation(self): # channel must be >= 0 with self.assertRaises(ValueError): can.Bus(interface="ixxat", channel=-1) # rx_fifo_size must be > 0 with self.assertRaises(ValueError): can.Bus(interface="ixxat", channel=0, rx_fifo_size=0) # tx_fifo_size must be > 0 with self.assertRaises(ValueError): can.Bus(interface="ixxat", channel=0, tx_fifo_size=0) class HardwareTestCase(unittest.TestCase): """ Test cases that rely on an existing/connected hardware. """ def setUp(self): try: bus = can.Bus(interface="ixxat", channel=0) bus.shutdown() except can.CanInterfaceNotImplementedError: raise unittest.SkipTest("not available on this platform") def test_bus_creation(self): try: configs = can.detect_available_configs("ixxat") if configs: for interface_kwargs in configs: bus = can.Bus(**interface_kwargs) bus.shutdown() else: raise unittest.SkipTest("No adapters were detected") except can.CanInterfaceNotImplementedError: raise unittest.SkipTest("not available on this platform") def test_bus_creation_incorrect_channel(self): # non-existent channel -> use arbitrary high value with self.assertRaises(can.CanInitializationError): can.Bus(interface="ixxat", channel=0xFFFF) def test_send_after_shutdown(self): with can.Bus(interface="ixxat", channel=0) as bus: with self.assertRaises(can.CanOperationError): bus.send(can.Message(arbitration_id=0x3FF, dlc=0)) if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_interface_ixxat_fd.py000066400000000000000000000036471472200326600220320ustar00rootroot00000000000000#!/usr/bin/env python """ Unittest for ixxat interface using fd option. Run only this test: python setup.py test --addopts "--verbose -s test/test_interface_ixxat_fd.py" """ import unittest import can class SoftwareTestCase(unittest.TestCase): """ Test cases that test the software only and do not rely on an existing/connected hardware. """ def setUp(self): try: bus = can.Bus(interface="ixxat", fd=True, channel=0) bus.shutdown() except can.CanInterfaceNotImplementedError: raise unittest.SkipTest("not available on this platform") def test_bus_creation(self): # channel must be >= 0 with self.assertRaises(ValueError): can.Bus(interface="ixxat", fd=True, channel=-1) # rx_fifo_size must be > 0 with self.assertRaises(ValueError): can.Bus(interface="ixxat", fd=True, channel=0, rx_fifo_size=0) # tx_fifo_size must be > 0 with self.assertRaises(ValueError): can.Bus(interface="ixxat", fd=True, channel=0, tx_fifo_size=0) class HardwareTestCase(unittest.TestCase): """ Test cases that rely on an existing/connected hardware. """ def setUp(self): try: bus = can.Bus(interface="ixxat", fd=True, channel=0) bus.shutdown() except can.CanInterfaceNotImplementedError: raise unittest.SkipTest("not available on this platform") def test_bus_creation(self): # non-existent channel -> use arbitrary high value with self.assertRaises(can.CanInitializationError): can.Bus(interface="ixxat", fd=True, channel=0xFFFF) def test_send_after_shutdown(self): with can.Bus(interface="ixxat", fd=True, channel=0) as bus: with self.assertRaises(can.CanOperationError): bus.send(can.Message(arbitration_id=0x3FF, dlc=0)) if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_interface_virtual.py000066400000000000000000000021041472200326600216750ustar00rootroot00000000000000#!/usr/bin/env python """ This module tests :meth:`can.interface.virtual`. """ import unittest from can import Bus, Message EXAMPLE_MSG1 = Message(timestamp=1639739471.5565314, arbitration_id=0x481, data=b"\x01") class TestMessageFiltering(unittest.TestCase): def setUp(self): self.node1 = Bus("test", interface="virtual", preserve_timestamps=True) self.node2 = Bus("test", interface="virtual") def tearDown(self): self.node1.shutdown() self.node2.shutdown() def test_sendmsg(self): self.node2.send(EXAMPLE_MSG1) r = self.node1.recv(0.1) assert r.timestamp != EXAMPLE_MSG1.timestamp assert r.arbitration_id == EXAMPLE_MSG1.arbitration_id assert r.data == EXAMPLE_MSG1.data def test_sendmsg_preserve_timestamp(self): self.node1.send(EXAMPLE_MSG1) r = self.node2.recv(0.1) assert r.timestamp == EXAMPLE_MSG1.timestamp assert r.arbitration_id == EXAMPLE_MSG1.arbitration_id assert r.data == EXAMPLE_MSG1.data if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_kvaser.py000066400000000000000000000266741472200326600175040ustar00rootroot00000000000000#!/usr/bin/env python """ """ import ctypes import time import unittest from unittest.mock import Mock import pytest import can from can.interfaces.kvaser import canlib, constants class KvaserTest(unittest.TestCase): def setUp(self): canlib.canGetNumberOfChannels = KvaserTest.canGetNumberOfChannels canlib.canOpenChannel = Mock(return_value=0) canlib.canIoCtl = Mock(return_value=0) canlib.canIoCtlInit = Mock(return_value=0) canlib.kvReadTimer = Mock() canlib.canSetBusParamsC200 = Mock() canlib.canSetBusParams = Mock() canlib.canSetBusParamsFd = Mock() canlib.canBusOn = Mock() canlib.canBusOff = Mock() canlib.canClose = Mock() canlib.canSetBusOutputControl = Mock() canlib.canGetChannelData = Mock() canlib.canSetAcceptanceFilter = Mock() canlib.canWriteSync = Mock() canlib.canWrite = self.canWrite canlib.canReadWait = self.canReadWait canlib.canGetBusStatistics = Mock() canlib.canRequestBusStatistics = Mock() self.msg = {} self.msg_in_cue = None self.bus = can.Bus(channel=0, interface="kvaser") def tearDown(self): if self.bus: self.bus.shutdown() self.bus = None def test_bus_creation(self): self.assertIsInstance(self.bus, canlib.KvaserBus) self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20) self.assertTrue(canlib.canOpenChannel.called) self.assertTrue(canlib.canBusOn.called) def test_bus_creation_illegal_channel_name(self): # Test if the bus constructor is able to deal with non-ASCII characters def canGetChannelDataMock( channel: ctypes.c_int, param: ctypes.c_int, buf: ctypes.c_void_p, bufsize: ctypes.c_size_t, ): if param == constants.canCHANNELDATA_DEVDESCR_ASCII: buf_char_ptr = ctypes.cast(buf, ctypes.POINTER(ctypes.c_char)) for i, char in enumerate(b"hello\x7a\xcb"): buf_char_ptr[i] = char canlib.canGetChannelData = canGetChannelDataMock bus = can.Bus(channel=0, interface="kvaser") self.assertTrue(bus.channel_info.startswith("hello")) bus.shutdown() def test_bus_shutdown(self): self.bus.shutdown() self.assertTrue(canlib.canBusOff.called) self.assertTrue(canlib.canClose.called) def test_filter_setup(self): # No filter in constructor expected_args = [ ((0, 0, 0, 0),), # Disable filtering STD on read handle ((0, 0, 0, 1),), # Disable filtering EXT on read handle ((0, 0, 0, 0),), # Disable filtering STD on write handle ((0, 0, 0, 1),), # Disable filtering EXT on write handle ] self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list, expected_args) # One filter, will be handled by canlib canlib.canSetAcceptanceFilter.reset_mock() self.bus.set_filters([{"can_id": 0x8, "can_mask": 0xFF, "extended": True}]) expected_args = [ ((0, 0x8, 0xFF, 1),), # Enable filtering EXT on read handle ((0, 0x8, 0xFF, 1),), # Enable filtering EXT on write handle ] self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list, expected_args) # Multiple filters, will be handled in Python canlib.canSetAcceptanceFilter.reset_mock() multiple_filters = [ {"can_id": 0x8, "can_mask": 0xFF}, {"can_id": 0x9, "can_mask": 0xFF}, ] self.bus.set_filters(multiple_filters) expected_args = [ ((0, 0, 0, 0),), # Disable filtering STD on read handle ((0, 0, 0, 1),), # Disable filtering EXT on read handle ((0, 0, 0, 0),), # Disable filtering STD on write handle ((0, 0, 0, 1),), # Disable filtering EXT on write handle ] self.assertEqual(canlib.canSetAcceptanceFilter.call_args_list, expected_args) def test_send_extended(self): msg = can.Message( arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4], is_extended_id=True ) self.bus.send(msg) self.assertEqual(self.msg["arb_id"], 0xC0FFEE) self.assertEqual(self.msg["dlc"], 7) self.assertEqual(self.msg["flags"], constants.canMSG_EXT) self.assertSequenceEqual(self.msg["data"], [0, 25, 0, 1, 3, 1, 4]) def test_send_standard(self): msg = can.Message(arbitration_id=0x321, data=[50, 51], is_extended_id=False) self.bus.send(msg) self.assertEqual(self.msg["arb_id"], 0x321) self.assertEqual(self.msg["dlc"], 2) self.assertEqual(self.msg["flags"], constants.canMSG_STD) self.assertSequenceEqual(self.msg["data"], [50, 51]) @pytest.mark.timeout(3.0) def test_recv_no_message(self): self.assertEqual(self.bus.recv(timeout=0.5), None) def test_recv_extended(self): self.msg_in_cue = can.Message( arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True ) now = time.time() msg = self.bus.recv() self.assertEqual(msg.arbitration_id, 0xC0FFEF) self.assertEqual(msg.dlc, 8) self.assertEqual(msg.is_extended_id, True) self.assertSequenceEqual(msg.data, self.msg_in_cue.data) self.assertTrue(now - 1 < msg.timestamp < now + 1) def test_recv_standard(self): self.msg_in_cue = can.Message( arbitration_id=0x123, data=[100, 101], is_extended_id=False ) msg = self.bus.recv() self.assertEqual(msg.arbitration_id, 0x123) self.assertEqual(msg.dlc, 2) self.assertEqual(msg.is_extended_id, False) self.assertSequenceEqual(msg.data, [100, 101]) def test_available_configs(self): configs = canlib.KvaserBus._detect_available_configs() expected = [ { "interface": "kvaser", "channel": 0, "dongle_channel": 1, "device_name": "", "serial": 0, }, { "interface": "kvaser", "channel": 1, "dongle_channel": 1, "device_name": "", "serial": 0, }, ] self.assertListEqual(configs, expected) def test_canfd_default_data_bitrate(self): canlib.canSetBusParams.reset_mock() canlib.canSetBusParamsFd.reset_mock() bus = can.Bus(channel=0, interface="kvaser", fd=True) self.assertEqual(bus.protocol, can.CanProtocol.CAN_FD) canlib.canSetBusParams.assert_called_once_with( 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0 ) canlib.canSetBusParamsFd.assert_called_once_with( 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0 ) def test_can_timing(self): canlib.canSetBusParams.reset_mock() canlib.canSetBusParamsFd.reset_mock() timing = can.BitTiming.from_bitrate_and_segments( f_clock=16_000_000, bitrate=125_000, tseg1=13, tseg2=2, sjw=1, ) can.Bus(channel=0, interface="kvaser", timing=timing) canlib.canSetBusParamsC200.assert_called_once_with(0, timing.btr0, timing.btr1) def test_canfd_timing(self): canlib.canSetBusParams.reset_mock() canlib.canSetBusParamsFd.reset_mock() canlib.canOpenChannel.reset_mock() timing = can.BitTimingFd.from_bitrate_and_segments( f_clock=80_000_000, nom_bitrate=500_000, nom_tseg1=68, nom_tseg2=11, nom_sjw=10, data_bitrate=2_000_000, data_tseg1=10, data_tseg2=9, data_sjw=8, ) can.Bus(channel=0, interface="kvaser", timing=timing) canlib.canSetBusParams.assert_called_once_with(0, 500_000, 68, 11, 10, 1, 0) canlib.canSetBusParamsFd.assert_called_once_with(0, 2_000_000, 10, 9, 8) canlib.canOpenChannel.assert_called_with( 0, constants.canOPEN_CAN_FD | constants.canOPEN_ACCEPT_VIRTUAL ) def test_canfd_non_iso(self): canlib.canSetBusParams.reset_mock() canlib.canSetBusParamsFd.reset_mock() canlib.canOpenChannel.reset_mock() timing = can.BitTimingFd.from_bitrate_and_segments( f_clock=80_000_000, nom_bitrate=500_000, nom_tseg1=68, nom_tseg2=11, nom_sjw=10, data_bitrate=2_000_000, data_tseg1=10, data_tseg2=9, data_sjw=8, ) bus = can.Bus(channel=0, interface="kvaser", timing=timing, fd_non_iso=True) self.assertEqual(bus.protocol, can.CanProtocol.CAN_FD_NON_ISO) canlib.canSetBusParams.assert_called_once_with(0, 500_000, 68, 11, 10, 1, 0) canlib.canSetBusParamsFd.assert_called_once_with(0, 2_000_000, 10, 9, 8) canlib.canOpenChannel.assert_called_with( 0, constants.canOPEN_CAN_FD_NONISO | constants.canOPEN_ACCEPT_VIRTUAL ) def test_canfd_nondefault_data_bitrate(self): canlib.canSetBusParams.reset_mock() canlib.canSetBusParamsFd.reset_mock() data_bitrate = 2000000 bus = can.Bus(channel=0, interface="kvaser", fd=True, data_bitrate=data_bitrate) self.assertEqual(bus.protocol, can.CanProtocol.CAN_FD) bitrate_constant = canlib.BITRATE_FD[data_bitrate] canlib.canSetBusParams.assert_called_once_with( 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0 ) canlib.canSetBusParamsFd.assert_called_once_with(0, bitrate_constant, 0, 0, 0) def test_canfd_custom_data_bitrate(self): canlib.canSetBusParams.reset_mock() canlib.canSetBusParamsFd.reset_mock() data_bitrate = 123456 can.Bus(channel=0, interface="kvaser", fd=True, data_bitrate=data_bitrate) canlib.canSetBusParams.assert_called_once_with( 0, constants.canFD_BITRATE_500K_80P, 0, 0, 0, 0, 0 ) canlib.canSetBusParamsFd.assert_called_once_with(0, data_bitrate, 0, 0, 0) def test_bus_get_stats(self): stats = self.bus.get_stats() self.assertTrue(canlib.canRequestBusStatistics.called) self.assertTrue(canlib.canGetBusStatistics.called) self.assertIsInstance(stats, canlib.structures.BusStatistics) @staticmethod def canGetNumberOfChannels(count): count._obj.value = 2 def canWrite(self, handle, arb_id, buf, dlc, flags): self.msg["arb_id"] = arb_id self.msg["dlc"] = dlc self.msg["flags"] = flags self.msg["data"] = bytearray(buf._obj) def canReadWait(self, handle, arb_id, data, dlc, flags, timestamp, timeout): if not self.msg_in_cue: return constants.canERR_NOMSG arb_id._obj.value = self.msg_in_cue.arbitration_id dlc._obj.value = self.msg_in_cue.dlc data._obj.raw = self.msg_in_cue.data flags_temp = 0 if self.msg_in_cue.is_extended_id: flags_temp |= constants.canMSG_EXT else: flags_temp |= constants.canMSG_STD if self.msg_in_cue.is_remote_frame: flags_temp |= constants.canMSG_RTR if self.msg_in_cue.is_error_frame: flags_temp |= constants.canMSG_ERROR_FRAME flags._obj.value = flags_temp timestamp._obj.value = 0 return constants.canOK if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_load_config.py000066400000000000000000000067321472200326600204460ustar00rootroot00000000000000#!/usr/bin/env python import shutil import tempfile import unittest import unittest.mock from tempfile import NamedTemporaryFile import can class LoadConfigTest(unittest.TestCase): configuration_in = { "default": {"interface": "serial", "channel": "0"}, "one": {"interface": "kvaser", "channel": "1", "bitrate": 100000}, "two": {"channel": "2"}, } configuration_out = { "default": {"interface": "serial", "channel": 0}, "one": {"interface": "kvaser", "channel": 1, "bitrate": 100000}, "two": {"channel": 2}, } def setUp(self): # Create a temporary directory self.test_dir = tempfile.mkdtemp() def tearDown(self): # Remove the directory after the test shutil.rmtree(self.test_dir) def _gen_configration_file(self, sections): with NamedTemporaryFile( mode="w", dir=self.test_dir, delete=False ) as tmp_config_file: content = [] for section in sections: content.append(f"[{section}]") for k, v in self.configuration_in[section].items(): content.append(f"{k} = {v}") tmp_config_file.write("\n".join(content)) return tmp_config_file.name def _dict_to_env(self, d): return {f"CAN_{k.upper()}": str(v) for k, v in d.items()} def test_config_default(self): tmp_config = self._gen_configration_file(["default"]) config = can.util.load_config(path=tmp_config) self.assertEqual(config, self.configuration_out["default"]) def test_config_whole_default(self): tmp_config = self._gen_configration_file(self.configuration_in) config = can.util.load_config(path=tmp_config) self.assertEqual(config, self.configuration_out["default"]) def test_config_whole_context(self): tmp_config = self._gen_configration_file(self.configuration_in) config = can.util.load_config(path=tmp_config, context="one") self.assertEqual(config, self.configuration_out["one"]) def test_config_merge_context(self): tmp_config = self._gen_configration_file(self.configuration_in) config = can.util.load_config(path=tmp_config, context="two") expected = self.configuration_out["default"].copy() expected.update(self.configuration_out["two"]) self.assertEqual(config, expected) def test_config_merge_environment_to_context(self): tmp_config = self._gen_configration_file(self.configuration_in) env_data = {"interface": "serial", "bitrate": 125000} env_dict = self._dict_to_env(env_data) with unittest.mock.patch.dict("os.environ", env_dict): config = can.util.load_config(path=tmp_config, context="one") expected = self.configuration_out["one"].copy() expected.update(env_data) self.assertEqual(config, expected) def test_config_whole_environment(self): tmp_config = self._gen_configration_file(self.configuration_in) env_data = {"interface": "socketcan", "channel": "3", "bitrate": 250000} env_dict = self._dict_to_env(env_data) with unittest.mock.patch.dict("os.environ", env_dict): config = can.util.load_config(path=tmp_config, context="one") expected = self.configuration_out["one"].copy() expected.update({"interface": "socketcan", "channel": 3, "bitrate": 250000}) self.assertEqual(config, expected) if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_load_file_config.py000066400000000000000000000070751472200326600214460ustar00rootroot00000000000000#!/usr/bin/env python import shutil import tempfile import unittest from tempfile import NamedTemporaryFile import can class LoadFileConfigTest(unittest.TestCase): configuration = { "default": {"interface": "virtual", "channel": "0"}, "one": {"interface": "kvaser", "channel": "1"}, "two": {"channel": "2"}, "three": {"extra": "extra value"}, } def setUp(self): # Create a temporary directory self.test_dir = tempfile.mkdtemp() def tearDown(self): # Remove the directory after the test shutil.rmtree(self.test_dir) def _gen_configration_file(self, sections): with NamedTemporaryFile( mode="w", dir=self.test_dir, delete=False ) as tmp_config_file: content = [] for section in sections: content.append(f"[{section}]") for k, v in self.configuration[section].items(): content.append(f"{k} = {v}") tmp_config_file.write("\n".join(content)) return tmp_config_file.name def test_config_file_with_default(self): tmp_config = self._gen_configration_file(["default"]) config = can.util.load_file_config(path=tmp_config) self.assertEqual(config, self.configuration["default"]) def test_config_file_with_default_and_section(self): tmp_config = self._gen_configration_file(["default", "one"]) config = can.util.load_file_config(path=tmp_config) self.assertEqual(config, self.configuration["default"]) config.update(can.util.load_file_config(path=tmp_config, section="one")) self.assertEqual(config, self.configuration["one"]) def test_config_file_with_section_only(self): tmp_config = self._gen_configration_file(["one"]) config = can.util.load_file_config(path=tmp_config) config.update(can.util.load_file_config(path=tmp_config, section="one")) self.assertEqual(config, self.configuration["one"]) def test_config_file_with_section_and_key_in_default(self): expected = self.configuration["default"].copy() expected.update(self.configuration["two"]) tmp_config = self._gen_configration_file(["default", "two"]) config = can.util.load_file_config(path=tmp_config) config.update(can.util.load_file_config(path=tmp_config, section="two")) self.assertEqual(config, expected) def test_config_file_with_section_missing_interface(self): expected = self.configuration["two"].copy() tmp_config = self._gen_configration_file(["two"]) config = can.util.load_file_config(path=tmp_config) config.update(can.util.load_file_config(path=tmp_config, section="two")) self.assertEqual(config, expected) def test_config_file_extra(self): expected = self.configuration["default"].copy() expected.update(self.configuration["three"]) tmp_config = self._gen_configration_file(["default", "three"]) config = can.util.load_file_config(path=tmp_config) config.update(can.util.load_file_config(path=tmp_config, section="three")) self.assertEqual(config, expected) def test_config_file_with_non_existing_section(self): expected = self.configuration["default"].copy() tmp_config = self._gen_configration_file(["default", "one", "two", "three"]) config = can.util.load_file_config(path=tmp_config) config.update(can.util.load_file_config(path=tmp_config, section="zero")) self.assertEqual(config, expected) if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_logger.py000066400000000000000000000225241472200326600174560ustar00rootroot00000000000000#!/usr/bin/env python """ This module tests the functions inside of logger.py """ import gzip import os import sys import unittest from unittest import mock from unittest.mock import Mock import pytest import can import can.logger class TestLoggerScriptModule(unittest.TestCase): def setUp(self) -> None: # Patch VirtualBus object patcher_virtual_bus = mock.patch("can.interfaces.virtual.VirtualBus", spec=True) self.MockVirtualBus = patcher_virtual_bus.start() self.addCleanup(patcher_virtual_bus.stop) self.mock_virtual_bus = self.MockVirtualBus.return_value self.mock_virtual_bus.shutdown = Mock() # Patch Logger object patcher_logger = mock.patch("can.logger.Logger", spec=True) self.MockLogger = patcher_logger.start() self.addCleanup(patcher_logger.stop) self.mock_logger = self.MockLogger.return_value self.mock_logger.stop = Mock() self.MockLoggerUse = self.MockLogger self.loggerToUse = self.mock_logger # Patch SizedRotatingLogger object patcher_logger_sized = mock.patch("can.logger.SizedRotatingLogger", spec=True) self.MockLoggerSized = patcher_logger_sized.start() self.addCleanup(patcher_logger_sized.stop) self.mock_logger_sized = self.MockLoggerSized.return_value self.mock_logger_sized.stop = Mock() self.testmsg = can.Message( arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True ) self.baseargs = [sys.argv[0], "-i", "virtual"] def assertSuccessfullCleanup(self): self.MockVirtualBus.assert_called_once() self.mock_virtual_bus.shutdown.assert_called_once() self.MockLoggerUse.assert_called_once() self.loggerToUse.stop.assert_called_once() def test_log_virtual(self): self.mock_virtual_bus.recv = Mock(side_effect=[self.testmsg, KeyboardInterrupt]) sys.argv = self.baseargs can.logger.main() self.assertSuccessfullCleanup() self.mock_logger.assert_called_once() def test_log_virtual_active(self): self.mock_virtual_bus.recv = Mock(side_effect=[self.testmsg, KeyboardInterrupt]) sys.argv = self.baseargs + ["--active"] can.logger.main() self.assertSuccessfullCleanup() self.mock_logger.assert_called_once() self.assertEqual(self.mock_virtual_bus.state, can.BusState.ACTIVE) def test_log_virtual_passive(self): self.mock_virtual_bus.recv = Mock(side_effect=[self.testmsg, KeyboardInterrupt]) sys.argv = self.baseargs + ["--passive"] can.logger.main() self.assertSuccessfullCleanup() self.mock_logger.assert_called_once() self.assertEqual(self.mock_virtual_bus.state, can.BusState.PASSIVE) def test_log_virtual_with_config(self): self.mock_virtual_bus.recv = Mock(side_effect=[self.testmsg, KeyboardInterrupt]) sys.argv = self.baseargs + [ "--bitrate", "250000", "--fd", "--data_bitrate", "2000000", ] can.logger.main() self.assertSuccessfullCleanup() self.mock_logger.assert_called_once() def test_log_virtual_sizedlogger(self): self.mock_virtual_bus.recv = Mock(side_effect=[self.testmsg, KeyboardInterrupt]) self.MockLoggerUse = self.MockLoggerSized self.loggerToUse = self.mock_logger_sized sys.argv = self.baseargs + ["--file_size", "1000000"] can.logger.main() self.assertSuccessfullCleanup() self.mock_logger_sized.assert_called_once() def test_parse_logger_args(self): args = self.baseargs + [ "--bitrate", "250000", "--fd", "--data_bitrate", "2000000", "--receive-own-messages=True", ] results, additional_config = can.logger._parse_logger_args(args[1:]) assert results.interface == "virtual" assert results.bitrate == 250_000 assert results.fd is True assert results.data_bitrate == 2_000_000 assert additional_config["receive_own_messages"] is True def test_parse_can_filters(self): expected_can_filters = [{"can_id": 0x100, "can_mask": 0x7FC}] results, additional_config = can.logger._parse_logger_args( ["--filter", "100:7FC", "--bitrate", "250000"] ) assert results.can_filters == expected_can_filters def test_parse_can_filters_list(self): expected_can_filters = [ {"can_id": 0x100, "can_mask": 0x7FC}, {"can_id": 0x200, "can_mask": 0x7F0}, ] results, additional_config = can.logger._parse_logger_args( ["--filter", "100:7FC", "200:7F0", "--bitrate", "250000"] ) assert results.can_filters == expected_can_filters def test_parse_timing(self) -> None: can20_args = self.baseargs + [ "--timing", "f_clock=8_000_000", "tseg1=5", "tseg2=2", "sjw=2", "brp=2", "nof_samples=1", "--app-name=CANalyzer", ] results, additional_config = can.logger._parse_logger_args(can20_args[1:]) assert results.timing == can.BitTiming( f_clock=8_000_000, brp=2, tseg1=5, tseg2=2, sjw=2, nof_samples=1 ) assert additional_config["app_name"] == "CANalyzer" canfd_args = self.baseargs + [ "--timing", "f_clock=80_000_000", "nom_tseg1=119", "nom_tseg2=40", "nom_sjw=40", "nom_brp=1", "data_tseg1=29", "data_tseg2=10", "data_sjw=10", "data_brp=1", "--app-name=CANalyzer", ] results, additional_config = can.logger._parse_logger_args(canfd_args[1:]) assert results.timing == can.BitTimingFd( f_clock=80_000_000, nom_brp=1, nom_tseg1=119, nom_tseg2=40, nom_sjw=40, data_brp=1, data_tseg1=29, data_tseg2=10, data_sjw=10, ) assert additional_config["app_name"] == "CANalyzer" # remove f_clock parameter, parsing should fail incomplete_args = self.baseargs + [ "--timing", "tseg1=5", "tseg2=2", "sjw=2", "brp=2", "nof_samples=1", "--app-name=CANalyzer", ] with self.assertRaises(SystemExit): can.logger._parse_logger_args(incomplete_args[1:]) def test_parse_additional_config(self): unknown_args = [ "--app-name=CANalyzer", "--serial=5555", "--receive-own-messages=True", "--false-boolean=False", "--offset=1.5", "--tseg1-abr=127", ] parsed_args = can.logger._parse_additional_config(unknown_args) assert "app_name" in parsed_args assert parsed_args["app_name"] == "CANalyzer" assert "serial" in parsed_args assert parsed_args["serial"] == 5555 assert "receive_own_messages" in parsed_args assert ( isinstance(parsed_args["receive_own_messages"], bool) and parsed_args["receive_own_messages"] is True ) assert "false_boolean" in parsed_args assert ( isinstance(parsed_args["false_boolean"], bool) and parsed_args["false_boolean"] is False ) assert "offset" in parsed_args assert parsed_args["offset"] == 1.5 assert "tseg1_abr" in parsed_args assert parsed_args["tseg1_abr"] == 127 with pytest.raises(ValueError): can.logger._parse_additional_config(["--wrong-format"]) with pytest.raises(ValueError): can.logger._parse_additional_config(["-wrongformat=value"]) with pytest.raises(ValueError): can.logger._parse_additional_config(["--wrongformat=value1 value2"]) with pytest.raises(ValueError): can.logger._parse_additional_config(["wrongformat="]) class TestLoggerCompressedFile(unittest.TestCase): def setUp(self) -> None: # Patch VirtualBus object self.patcher_virtual_bus = mock.patch( "can.interfaces.virtual.VirtualBus", spec=True ) self.MockVirtualBus = self.patcher_virtual_bus.start() self.mock_virtual_bus = self.MockVirtualBus.return_value self.testmsg = can.Message( arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True ) self.baseargs = [sys.argv[0], "-i", "virtual"] self.testfile = open("coffee.log.gz", "w+") def test_compressed_logfile(self): """ Basic test to verify Logger is able to write gzip files. """ self.mock_virtual_bus.recv = Mock(side_effect=[self.testmsg, KeyboardInterrupt]) sys.argv = self.baseargs + ["--file_name", self.testfile.name] can.logger.main() with gzip.open(self.testfile.name, "rt") as testlog: last_line = testlog.readlines()[-1] self.assertEqual(last_line, "(0.000000) vcan0 00C0FFEE#0019000103010401 R\n") def tearDown(self) -> None: self.testfile.close() os.remove(self.testfile.name) self.patcher_virtual_bus.stop() if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_message_class.py000066400000000000000000000112351472200326600210050ustar00rootroot00000000000000#!/usr/bin/env python import pickle import sys import unittest from copy import copy, deepcopy from datetime import timedelta from math import isinf, isnan import hypothesis.errors import hypothesis.strategies as st import pytest from hypothesis import HealthCheck, given, settings from can import Message from .config import IS_GITHUB_ACTIONS, IS_PYPY, IS_WINDOWS from .message_helper import ComparingMessagesTestCase class TestMessageClass(unittest.TestCase): """ This test tries many inputs to the message class constructor and then sanity checks all methods and ensures that nothing crashes. It also checks whether Message._check() allows all valid can frames. """ @given( timestamp=st.floats(min_value=0.0), arbitration_id=st.integers(), is_extended_id=st.booleans(), is_remote_frame=st.booleans(), is_error_frame=st.booleans(), channel=st.one_of(st.text(), st.integers()), dlc=st.integers(min_value=0, max_value=8), data=st.one_of(st.binary(min_size=0, max_size=8), st.none()), is_fd=st.booleans(), bitrate_switch=st.booleans(), error_state_indicator=st.booleans(), ) # The first run may take a second on CI runners and will hit the deadline @settings( max_examples=2000, suppress_health_check=[HealthCheck.too_slow], deadline=None if IS_GITHUB_ACTIONS else timedelta(milliseconds=500), ) @pytest.mark.xfail( IS_WINDOWS and IS_PYPY, raises=hypothesis.errors.Flaky, reason="Hypothesis generates inconsistent timestamp floats on Windows+PyPy-3.7", ) def test_methods(self, **kwargs): is_valid = not ( ( not kwargs["is_remote_frame"] and (len(kwargs["data"] or []) != kwargs["dlc"]) ) or (kwargs["arbitration_id"] >= 0x800 and not kwargs["is_extended_id"]) or kwargs["arbitration_id"] >= 0x20000000 or kwargs["arbitration_id"] < 0 or ( kwargs["is_remote_frame"] and (kwargs["is_fd"] or kwargs["is_error_frame"]) ) or (kwargs["is_remote_frame"] and len(kwargs["data"] or []) > 0) or ( (kwargs["bitrate_switch"] or kwargs["error_state_indicator"]) and not kwargs["is_fd"] ) or isnan(kwargs["timestamp"]) or isinf(kwargs["timestamp"]) ) # this should return normally and not throw an exception message = Message(check=is_valid, **kwargs) if kwargs["data"] is None or kwargs["is_remote_frame"]: kwargs["data"] = bytearray() if not is_valid and not kwargs["is_remote_frame"]: with self.assertRaises(ValueError): Message(check=True, **kwargs) self.assertGreater(len(str(message)), 0) self.assertGreater(len(message.__repr__()), 0) if is_valid: self.assertEqual(len(message), kwargs["dlc"]) self.assertTrue(bool(message)) self.assertGreater(len(f"{message}"), 0) _ = f"{message}" with self.assertRaises(Exception): _ = "{somespec}".format( message ) # pylint: disable=missing-format-argument-key if sys.version_info.major > 2: self.assertEqual(bytearray(bytes(message)), kwargs["data"]) # check copies and equalities if is_valid: self.assertEqual(message, message) normal_copy = copy(message) deep_copy = deepcopy(message) for other in (normal_copy, deep_copy, message): self.assertTrue(message.equals(other, timestamp_delta=None)) self.assertTrue(message.equals(other)) self.assertTrue(message.equals(other, timestamp_delta=0)) class MessageSerialization(unittest.TestCase, ComparingMessagesTestCase): def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) ComparingMessagesTestCase.__init__( self, allowed_timestamp_delta=0.016, preserves_channel=True ) def test_serialization(self): message = Message( timestamp=1.0, arbitration_id=0x401, is_extended_id=False, is_remote_frame=False, is_error_frame=False, channel=1, dlc=6, data=bytearray([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]), is_fd=False, ) serialized = pickle.dumps(message, -1) deserialized = pickle.loads(serialized) self.assertMessageEqual(message, deserialized) if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_message_filtering.py000066400000000000000000000030241472200326600216600ustar00rootroot00000000000000#!/usr/bin/env python """ This module tests :meth:`can.BusABC._matches_filters`. """ import unittest from can import Bus, Message from .data.example_data import TEST_ALL_MESSAGES EXAMPLE_MSG = Message(arbitration_id=0x123, is_extended_id=True) HIGHEST_MSG = Message(arbitration_id=0x1FFFFFFF, is_extended_id=True) MATCH_EXAMPLE = [{"can_id": 0x123, "can_mask": 0x1FFFFFFF, "extended": True}] MATCH_ONLY_HIGHEST = [{"can_id": 0xFFFFFFFF, "can_mask": 0x1FFFFFFF, "extended": True}] class TestMessageFiltering(unittest.TestCase): def setUp(self): self.bus = Bus(interface="virtual", channel="testy") def tearDown(self): self.bus.shutdown() def test_match_all(self): # explicitly self.bus.set_filters() self.assertTrue(self.bus._matches_filters(EXAMPLE_MSG)) # implicitly self.bus.set_filters(None) self.assertTrue(self.bus._matches_filters(EXAMPLE_MSG)) def test_match_filters_is_empty(self): self.bus.set_filters([]) for msg in TEST_ALL_MESSAGES: self.assertTrue(self.bus._matches_filters(msg)) def test_match_example_message(self): self.bus.set_filters(MATCH_EXAMPLE) self.assertTrue(self.bus._matches_filters(EXAMPLE_MSG)) self.assertFalse(self.bus._matches_filters(HIGHEST_MSG)) self.bus.set_filters(MATCH_ONLY_HIGHEST) self.assertFalse(self.bus._matches_filters(EXAMPLE_MSG)) self.assertTrue(self.bus._matches_filters(HIGHEST_MSG)) if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_message_sync.py000066400000000000000000000070511472200326600206550ustar00rootroot00000000000000#!/usr/bin/env python """ This module tests :class:`can.MessageSync`. """ import gc import time import unittest from copy import copy import pytest from can import Message, MessageSync from .config import IS_CI, IS_GITHUB_ACTIONS, IS_LINUX, IS_OSX, IS_TRAVIS from .data.example_data import TEST_MESSAGES_BASE from .message_helper import ComparingMessagesTestCase TEST_FEWER_MESSAGES = TEST_MESSAGES_BASE[::2] def inc(value): """Makes the test boundaries give some more space when run on the CI server.""" if IS_CI: return value * 1.5 else: return value skip_on_unreliable_platforms = unittest.skipIf( (IS_TRAVIS and IS_OSX) or (IS_GITHUB_ACTIONS and not IS_LINUX), "this environment's timings are too unpredictable", ) @skip_on_unreliable_platforms class TestMessageSync(unittest.TestCase, ComparingMessagesTestCase): def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) ComparingMessagesTestCase.__init__(self) def setup_method(self, _): # disabling the garbage collector makes the time readings more reliable gc.disable() def teardown_method(self, _): # we need to reenable the garbage collector again gc.enable() def test_general(self): messages = [ Message(timestamp=50.0), Message(timestamp=50.0), Message(timestamp=50.0 + 0.05), Message(timestamp=50.0 + 0.13), Message(timestamp=50.0), # back in time ] sync = MessageSync(messages, gap=0.0, skip=0.0) t_start = time.perf_counter() collected = [] timings = [] for message in sync: t_now = time.perf_counter() collected.append(message) timings.append(t_now - t_start) self.assertMessagesEqual(messages, collected) self.assertEqual(len(timings), len(messages), "programming error in test code") self.assertTrue(0.0 <= timings[0] < 0.0 + inc(0.02), str(timings[0])) self.assertTrue(0.0 <= timings[1] < 0.0 + inc(0.02), str(timings[1])) self.assertTrue(0.045 <= timings[2] < 0.05 + inc(0.02), str(timings[2])) self.assertTrue(0.125 <= timings[3] < 0.13 + inc(0.02), str(timings[3])) self.assertTrue(0.125 <= timings[4] < 0.13 + inc(0.02), str(timings[4])) def test_skip(self): messages = copy(TEST_FEWER_MESSAGES) sync = MessageSync(messages, skip=0.005, gap=0.0) before = time.perf_counter() collected = list(sync) after = time.perf_counter() took = after - before # the handling of the messages itself also takes some time: # ~0.001 s/message on a ThinkPad T560 laptop (Ubuntu 18.04, i5-6200U) assert 0 < took < inc(len(messages) * (0.005 + 0.003)), f"took: {took}s" self.assertMessagesEqual(messages, collected) @skip_on_unreliable_platforms @pytest.mark.parametrize( "timestamp_1,timestamp_2", [(0.0, 0.0), (0.0, 0.01), (0.01, 1.5)] ) def test_gap(timestamp_1, timestamp_2): """This method is alone so it can be parameterized.""" messages = [ Message(arbitration_id=0x1, timestamp=timestamp_1), Message(arbitration_id=0x2, timestamp=timestamp_2), ] sync = MessageSync(messages, timestamps=False, gap=0.1) gc.disable() before = time.perf_counter() collected = list(sync) after = time.perf_counter() gc.enable() took = after - before assert 0.195 <= took < 0.2 + inc(0.02) assert messages == collected if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_neousys.py000066400000000000000000000076331472200326600177100ustar00rootroot00000000000000#!/usr/bin/env python import unittest from ctypes import ( POINTER, byref, c_ubyte, cast, sizeof, ) from unittest.mock import Mock import can from can.interfaces.neousys import neousys class TestNeousysBus(unittest.TestCase): def setUp(self) -> None: can.interfaces.neousys.neousys.NEOUSYS_CANLIB = Mock() can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_RegisterReceived = Mock( return_value=1 ) can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_RegisterStatus = Mock( return_value=1 ) can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Setup = Mock(return_value=1) can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Start = Mock(return_value=1) can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Send = Mock(return_value=1) can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Stop = Mock(return_value=1) self.bus = can.Bus(channel=0, interface="neousys") def tearDown(self) -> None: if self.bus: self.bus.shutdown() self.bus = None def test_bus_creation(self) -> None: self.assertIsInstance(self.bus, neousys.NeousysBus) self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20) neousys.NEOUSYS_CANLIB.CAN_Setup.assert_called() neousys.NEOUSYS_CANLIB.CAN_Start.assert_called() neousys.NEOUSYS_CANLIB.CAN_RegisterReceived.assert_called() neousys.NEOUSYS_CANLIB.CAN_RegisterStatus.assert_called() neousys.NEOUSYS_CANLIB.CAN_Send.assert_not_called() neousys.NEOUSYS_CANLIB.CAN_Stop.assert_not_called() CAN_Start_args = ( can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Setup.call_args[0] ) # sizeof struct should be 16 self.assertEqual(CAN_Start_args[0], 0) self.assertEqual(CAN_Start_args[2], 16) NeousysCanSetup_struct = cast( CAN_Start_args[1], POINTER(neousys.NeousysCanSetup) ) self.assertEqual(NeousysCanSetup_struct.contents.bitRate, 500000) self.assertEqual( NeousysCanSetup_struct.contents.recvConfig, neousys.NEOUSYS_CAN_MSG_USE_ID_FILTER, ) def test_bus_creation_bitrate(self) -> None: self.bus = can.Bus(channel=0, interface="neousys", bitrate=200000) self.assertIsInstance(self.bus, neousys.NeousysBus) self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20) CAN_Start_args = ( can.interfaces.neousys.neousys.NEOUSYS_CANLIB.CAN_Setup.call_args[0] ) # sizeof struct should be 16 self.assertEqual(CAN_Start_args[0], 0) self.assertEqual(CAN_Start_args[2], 16) NeousysCanSetup_struct = cast( CAN_Start_args[1], POINTER(neousys.NeousysCanSetup) ) self.assertEqual(NeousysCanSetup_struct.contents.bitRate, 200000) self.assertEqual( NeousysCanSetup_struct.contents.recvConfig, neousys.NEOUSYS_CAN_MSG_USE_ID_FILTER, ) def test_receive(self) -> None: recv_msg = self.bus.recv(timeout=0.05) self.assertEqual(recv_msg, None) msg_data = [0x01, 0x02, 0x03, 0x04, 0x05] NeousysCanMsg_msg = neousys.NeousysCanMsg( 0x01, 0x00, 0x00, 0x05, (c_ubyte * 8)(*msg_data) ) self.bus._neousys_recv_cb(byref(NeousysCanMsg_msg), sizeof(NeousysCanMsg_msg)) recv_msg = self.bus.recv(timeout=0.05) self.assertEqual(recv_msg.dlc, 5) self.assertSequenceEqual(recv_msg.data, msg_data) def test_send(self) -> None: msg = can.Message( arbitration_id=0x01, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=False ) self.bus.send(msg) neousys.NEOUSYS_CANLIB.CAN_Send.assert_called() def test_shutdown(self) -> None: self.bus.shutdown() neousys.NEOUSYS_CANLIB.CAN_Stop.assert_called() if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_neovi.py000066400000000000000000000011171472200326600173120ustar00rootroot00000000000000#!/usr/bin/env python """ """ import pickle import unittest from can.interfaces.ics_neovi import ICSApiError class ICSApiErrorTest(unittest.TestCase): def test_error_pickling(self): iae = ICSApiError( 0xF00, "description_short", "description_long", severity=ICSApiError.ICS_SPY_ERR_CRITICAL, restart_needed=1, ) pickled_iae = pickle.dumps(iae) un_pickled_iae = pickle.loads(pickled_iae) assert iae.__dict__ == un_pickled_iae.__dict__ if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_pcan.py000066400000000000000000000467401472200326600171260ustar00rootroot00000000000000""" Test for PCAN Interface """ import ctypes import struct import unittest from unittest import mock from unittest.mock import Mock, patch import pytest from parameterized import parameterized import can from can import BusState, CanProtocol from can.exceptions import CanInitializationError from can.interfaces.pcan import PcanBus, PcanError from can.interfaces.pcan.basic import * class TestPCANBus(unittest.TestCase): def setUp(self) -> None: patcher = mock.patch("can.interfaces.pcan.pcan.PCANBasic", spec=True) self.MockPCANBasic = patcher.start() self.addCleanup(patcher.stop) self.mock_pcan = self.MockPCANBasic.return_value self.mock_pcan.Initialize.return_value = PCAN_ERROR_OK self.mock_pcan.InitializeFD = Mock(return_value=PCAN_ERROR_OK) self.mock_pcan.SetValue = Mock(return_value=PCAN_ERROR_OK) self.mock_pcan.GetValue = self._mockGetValue self.PCAN_API_VERSION_SIM = "4.2" self.bus = None def tearDown(self) -> None: if self.bus: self.bus.shutdown() self.bus = None def _mockGetValue(self, channel, parameter): """ This method is used as mock for GetValue method of PCANBasic object. Only a subset of parameters are supported. """ if parameter == PCAN_API_VERSION: return PCAN_ERROR_OK, self.PCAN_API_VERSION_SIM.encode("ascii") elif parameter == PCAN_RECEIVE_EVENT: return PCAN_ERROR_OK, int.from_bytes(PCAN_RECEIVE_EVENT, "big") raise NotImplementedError( f"No mock return value specified for parameter {parameter}" ) def test_bus_creation(self) -> None: self.bus = can.Bus(interface="pcan") self.assertIsInstance(self.bus, PcanBus) self.assertEqual(self.bus.protocol, CanProtocol.CAN_20) with pytest.deprecated_call(): self.assertFalse(self.bus.fd) self.MockPCANBasic.assert_called_once() self.mock_pcan.Initialize.assert_called_once() self.mock_pcan.InitializeFD.assert_not_called() def test_bus_creation_state_error(self) -> None: with self.assertRaises(ValueError): can.Bus(interface="pcan", state=BusState.ERROR) @parameterized.expand([("f_clock", 80_000_000), ("f_clock_mhz", 80)]) def test_bus_creation_fd(self, clock_param: str, clock_val: int) -> None: self.bus = can.Bus( interface="pcan", fd=True, nom_brp=1, nom_tseg1=129, nom_tseg2=30, nom_sjw=1, data_brp=1, data_tseg1=9, data_tseg2=6, data_sjw=1, channel="PCAN_USBBUS1", **{clock_param: clock_val}, ) self.assertIsInstance(self.bus, PcanBus) self.assertEqual(self.bus.protocol, CanProtocol.CAN_FD) with pytest.deprecated_call(): self.assertTrue(self.bus.fd) self.MockPCANBasic.assert_called_once() self.mock_pcan.Initialize.assert_not_called() self.mock_pcan.InitializeFD.assert_called_once() # Retrieve second argument of first call bitrate_arg = self.mock_pcan.InitializeFD.call_args[0][-1] self.assertTrue(f"{clock_param}={clock_val}".encode("ascii") in bitrate_arg) self.assertTrue(b"nom_brp=1" in bitrate_arg) self.assertTrue(b"nom_tseg1=129" in bitrate_arg) self.assertTrue(b"nom_tseg2=30" in bitrate_arg) self.assertTrue(b"nom_sjw=1" in bitrate_arg) self.assertTrue(b"data_brp=1" in bitrate_arg) self.assertTrue(b"data_tseg1=9" in bitrate_arg) self.assertTrue(b"data_tseg2=6" in bitrate_arg) self.assertTrue(b"data_sjw=1" in bitrate_arg) def test_api_version_low(self) -> None: self.PCAN_API_VERSION_SIM = "1.0" with self.assertLogs("can.pcan", level="WARNING") as cm: self.bus = can.Bus(interface="pcan") found_version_warning = False for i in cm.output: if "version" in i and "pcan" in i: found_version_warning = True self.assertTrue( found_version_warning, f"No warning was logged for incompatible api version {cm.output}", ) def test_api_version_read_fail(self) -> None: self.mock_pcan.GetValue = Mock(return_value=(PCAN_ERROR_ILLOPERATION, None)) with self.assertRaises(CanInitializationError): self.bus = can.Bus(interface="pcan") def test_issue1642(self) -> None: self.PCAN_API_VERSION_SIM = "1, 3, 0, 50" with self.assertLogs("can.pcan", level="WARNING") as cm: self.bus = can.Bus(interface="pcan") found_version_warning = False for i in cm.output: if "version" in i and "pcan" in i: found_version_warning = True self.assertTrue( found_version_warning, f"No warning was logged for incompatible api version {cm.output}", ) @parameterized.expand( [ ("no_error", PCAN_ERROR_OK, PCAN_ERROR_OK, "some ok text 1"), ("one_error", PCAN_ERROR_UNKNOWN, PCAN_ERROR_OK, "some ok text 2"), ( "both_errors", PCAN_ERROR_UNKNOWN, PCAN_ERROR_UNKNOWN, "An error occurred. Error-code's text (8h) couldn't be retrieved", ), ] ) def test_get_formatted_error(self, name, status1, status2, expected_result: str): with self.subTest(name): self.bus = can.Bus(interface="pcan") self.mock_pcan.GetErrorText = Mock( side_effect=[ (status1, expected_result.encode("utf-8", errors="replace")), (status2, expected_result.encode("utf-8", errors="replace")), ] ) complete_text = self.bus._get_formatted_error(PCAN_ERROR_BUSHEAVY) self.assertEqual(complete_text, expected_result) def test_status(self) -> None: self.bus = can.Bus(interface="pcan") self.bus.status() self.mock_pcan.GetStatus.assert_called_once_with(PCAN_USBBUS1) @parameterized.expand( [("no_error", PCAN_ERROR_OK, True), ("error", PCAN_ERROR_UNKNOWN, False)] ) def test_status_is_ok(self, name, status, expected_result) -> None: with self.subTest(name): self.mock_pcan.GetStatus = Mock(return_value=status) self.bus = can.Bus(interface="pcan") self.assertEqual(self.bus.status_is_ok(), expected_result) self.mock_pcan.GetStatus.assert_called_once_with(PCAN_USBBUS1) @parameterized.expand( [("no_error", PCAN_ERROR_OK, True), ("error", PCAN_ERROR_UNKNOWN, False)] ) def test_reset(self, name, status, expected_result) -> None: with self.subTest(name): self.mock_pcan.Reset = Mock(return_value=status) self.bus = can.Bus(interface="pcan", fd=True) self.assertEqual(self.bus.reset(), expected_result) self.mock_pcan.Reset.assert_called_once_with(PCAN_USBBUS1) @parameterized.expand( [("no_error", PCAN_ERROR_OK, 1), ("error", PCAN_ERROR_UNKNOWN, None)] ) def test_get_device_number(self, name, status, expected_result) -> None: with self.subTest(name): self.bus = can.Bus(interface="pcan", fd=True) # Mock GetValue after creation of bus to use first mock of # GetValue in constructor self.mock_pcan.GetValue = Mock(return_value=(status, 1)) self.assertEqual(self.bus.get_device_number(), expected_result) self.mock_pcan.GetValue.assert_called_once_with( PCAN_USBBUS1, PCAN_DEVICE_NUMBER ) @parameterized.expand( [("no_error", PCAN_ERROR_OK, True), ("error", PCAN_ERROR_UNKNOWN, False)] ) def test_set_device_number(self, name, status, expected_result) -> None: with self.subTest(name): self.bus = can.Bus(interface="pcan") self.mock_pcan.SetValue = Mock(return_value=status) self.assertEqual(self.bus.set_device_number(3), expected_result) # check last SetValue call self.assertEqual( self.mock_pcan.SetValue.call_args_list[-1][0], (PCAN_USBBUS1, PCAN_DEVICE_NUMBER, 3), ) def test_recv(self): data = (ctypes.c_ubyte * 8)(*[x for x in range(8)]) msg = TPCANMsg(ID=0xC0FFEF, LEN=8, MSGTYPE=PCAN_MESSAGE_EXTENDED, DATA=data) timestamp = TPCANTimestamp() self.mock_pcan.Read = Mock(return_value=(PCAN_ERROR_OK, msg, timestamp)) self.bus = can.Bus(interface="pcan") recv_msg = self.bus.recv() self.assertEqual(recv_msg.arbitration_id, msg.ID) self.assertEqual(recv_msg.dlc, msg.LEN) self.assertEqual(recv_msg.is_extended_id, True) self.assertEqual(recv_msg.is_fd, False) self.assertSequenceEqual(recv_msg.data, msg.DATA) self.assertEqual(recv_msg.timestamp, 0) def test_recv_fd(self): data = (ctypes.c_ubyte * 64)(*[x for x in range(64)]) msg = TPCANMsgFD( ID=0xC0FFEF, DLC=64, MSGTYPE=(PCAN_MESSAGE_EXTENDED.value | PCAN_MESSAGE_FD.value), DATA=data, ) timestamp = TPCANTimestampFD() self.mock_pcan.ReadFD = Mock(return_value=(PCAN_ERROR_OK, msg, timestamp)) self.bus = can.Bus(interface="pcan", fd=True) recv_msg = self.bus.recv() self.assertEqual(recv_msg.arbitration_id, msg.ID) self.assertEqual(recv_msg.dlc, msg.DLC) self.assertEqual(recv_msg.is_extended_id, True) self.assertEqual(recv_msg.is_fd, True) self.assertSequenceEqual(recv_msg.data, msg.DATA) self.assertEqual(recv_msg.timestamp, 0) @pytest.mark.timeout(3.0) @patch("select.select", return_value=([], [], [])) def test_recv_no_message(self, mock_select): self.mock_pcan.Read = Mock(return_value=(PCAN_ERROR_QRCVEMPTY, None, None)) self.bus = can.Bus(interface="pcan") self.assertEqual(self.bus.recv(timeout=0.5), None) def test_send(self) -> None: self.mock_pcan.Write = Mock(return_value=PCAN_ERROR_OK) self.bus = can.Bus(interface="pcan") msg = can.Message( arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True ) self.bus.send(msg) self.mock_pcan.Write.assert_called_once() self.mock_pcan.WriteFD.assert_not_called() def test_send_fd(self) -> None: self.mock_pcan.WriteFD = Mock(return_value=PCAN_ERROR_OK) self.bus = can.Bus(interface="pcan", fd=True) msg = can.Message( arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True ) self.bus.send(msg) self.mock_pcan.Write.assert_not_called() self.mock_pcan.WriteFD.assert_called_once() @parameterized.expand( [ ( "standart", (False, False, False, False, False, False), PCAN_MESSAGE_STANDARD, ), ( "extended", (True, False, False, False, False, False), PCAN_MESSAGE_EXTENDED, ), ("remote", (False, True, False, False, False, False), PCAN_MESSAGE_RTR), ("error", (False, False, True, False, False, False), PCAN_MESSAGE_ERRFRAME), ("fd", (False, False, False, True, False, False), PCAN_MESSAGE_FD), ( "bitrate_switch", (False, False, False, False, True, False), PCAN_MESSAGE_BRS, ), ( "error_state_indicator", (False, False, False, False, False, True), PCAN_MESSAGE_ESI, ), ] ) def test_send_type(self, name, msg_type, expected_value) -> None: with self.subTest(name): ( is_extended_id, is_remote_frame, is_error_frame, is_fd, bitrate_switch, error_state_indicator, ) = msg_type self.mock_pcan.Write = Mock(return_value=PCAN_ERROR_OK) self.bus = can.Bus(interface="pcan") msg = can.Message( arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=is_extended_id, is_remote_frame=is_remote_frame, is_error_frame=is_error_frame, bitrate_switch=bitrate_switch, error_state_indicator=error_state_indicator, is_fd=is_fd, ) self.bus.send(msg) # self.mock_m_objPCANBasic.Write.assert_called_once() CANMsg = self.mock_pcan.Write.call_args_list[0][0][1] self.assertEqual(CANMsg.MSGTYPE, expected_value.value) def test_send_error(self) -> None: self.mock_pcan.Write = Mock(return_value=PCAN_ERROR_BUSHEAVY) self.bus = can.Bus(interface="pcan") msg = can.Message( arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True ) with self.assertRaises(PcanError): self.bus.send(msg) @parameterized.expand([("on", True), ("off", False)]) def test_flash(self, name, flash) -> None: with self.subTest(name): self.bus = can.Bus(interface="pcan") self.bus.flash(flash) call_list = self.mock_pcan.SetValue.call_args_list last_call_args_list = call_list[-1][0] self.assertEqual( last_call_args_list, (PCAN_USBBUS1, PCAN_CHANNEL_IDENTIFYING, flash) ) def test_shutdown(self) -> None: self.bus = can.Bus(interface="pcan") self.bus.shutdown() self.mock_pcan.Uninitialize.assert_called_once_with(PCAN_USBBUS1) @parameterized.expand( [ ("active", BusState.ACTIVE, PCAN_PARAMETER_OFF), ("passive", BusState.PASSIVE, PCAN_PARAMETER_ON), ] ) def test_state(self, name, bus_state: BusState, expected_parameter) -> None: with self.subTest(name): self.bus = can.Bus(interface="pcan") self.bus.state = bus_state call_list = self.mock_pcan.SetValue.call_args_list last_call_args_list = call_list[-1][0] self.assertEqual( last_call_args_list, (PCAN_USBBUS1, PCAN_LISTEN_ONLY, expected_parameter), ) def test_state_constructor(self): for state in [BusState.ACTIVE, BusState.PASSIVE]: bus = can.Bus(interface="pcan", state=state) assert bus.state == state def test_detect_available_configs(self) -> None: if platform.system() == "Darwin": self.mock_pcan.GetValue = Mock( return_value=(PCAN_ERROR_OK, PCAN_CHANNEL_AVAILABLE) ) configs = PcanBus._detect_available_configs() self.assertEqual(len(configs), 50) else: value = (TPCANChannelInformation * 1).from_buffer_copy( struct.pack("HBBI33sII", 81, 5, 0, 1, b"PCAN-USB FD", 1122867, 1) ) self.mock_pcan.GetValue = Mock(return_value=(PCAN_ERROR_OK, value)) configs = PcanBus._detect_available_configs() assert len(configs) == 1 assert configs[0]["interface"] == "pcan" assert configs[0]["channel"] == "PCAN_USBBUS1" assert configs[0]["supports_fd"] assert configs[0]["controller_number"] == 0 assert configs[0]["device_features"] == 1 assert configs[0]["device_id"] == 1122867 assert configs[0]["device_name"] == "PCAN-USB FD" assert configs[0]["device_type"] == 5 assert configs[0]["channel_condition"] == 1 @parameterized.expand([("valid", PCAN_ERROR_OK, "OK"), ("invalid", 0x00005, None)]) def test_status_string(self, name, status, expected_result) -> None: with self.subTest(name): self.bus = can.Bus(interface="pcan") self.mock_pcan.GetStatus = Mock(return_value=status) self.assertEqual(self.bus.status_string(), expected_result) self.mock_pcan.GetStatus.assert_called() @parameterized.expand([(0x0, "error"), (0x42, "PCAN_USBBUS8")]) def test_constructor_with_device_id(self, dev_id, expected_result): def get_value_side_effect(handle, param): if param == PCAN_API_VERSION: return PCAN_ERROR_OK, self.PCAN_API_VERSION_SIM.encode("ascii") if handle in (PCAN_USBBUS8, PCAN_USBBUS14): return 0, 0x42 else: return PCAN_ERROR_ILLHW, 0x0 self.mock_pcan.GetValue = Mock(side_effect=get_value_side_effect) if expected_result == "error": with self.assertRaises(ValueError): can.Bus(interface="pcan", device_id=dev_id) else: self.bus = can.Bus(interface="pcan", device_id=dev_id) self.assertEqual(expected_result, self.bus.channel_info) def test_bus_creation_auto_reset(self): self.bus = can.Bus(interface="pcan", auto_reset=True) self.assertIsInstance(self.bus, PcanBus) self.MockPCANBasic.assert_called_once() def test_auto_reset_init_fault(self): self.mock_pcan.SetValue = Mock(return_value=PCAN_ERROR_INITIALIZE) with self.assertRaises(CanInitializationError): self.bus = can.Bus(interface="pcan", auto_reset=True) def test_peak_fd_bus_constructor_regression(self): # Tests that the following issue has been fixed: # https://github.com/hardbyte/python-can/issues/1458 params = { "interface": "pcan", "fd": True, "f_clock": 80000000, "nom_brp": 1, "nom_tseg1": 129, "nom_tseg2": 30, "nom_sjw": 1, "data_brp": 1, "data_tseg1": 9, "data_tseg2": 6, "data_sjw": 1, "channel": "PCAN_USBBUS1", } can.Bus(**params) def test_constructor_bit_timing(self): timing = can.BitTiming.from_registers(f_clock=8_000_000, btr0=0x47, btr1=0x2F) bus = can.Bus(interface="pcan", channel="PCAN_USBBUS1", timing=timing) bitrate_arg = self.mock_pcan.Initialize.call_args[0][1] self.assertEqual(bitrate_arg.value, 0x472F) self.assertEqual(bus.protocol, CanProtocol.CAN_20) def test_constructor_bit_timing_fd(self): timing = can.BitTimingFd( f_clock=40_000_000, nom_brp=1, nom_tseg1=129, nom_tseg2=30, nom_sjw=1, data_brp=1, data_tseg1=9, data_tseg2=6, data_sjw=1, ) bus = can.Bus(interface="pcan", channel="PCAN_USBBUS1", timing=timing) self.assertEqual(bus.protocol, CanProtocol.CAN_FD) bitrate_arg = self.mock_pcan.InitializeFD.call_args[0][-1] self.assertTrue(b"f_clock=40000000" in bitrate_arg) self.assertTrue(b"nom_brp=1" in bitrate_arg) self.assertTrue(b"nom_tseg1=129" in bitrate_arg) self.assertTrue(b"nom_tseg2=30" in bitrate_arg) self.assertTrue(b"nom_sjw=1" in bitrate_arg) self.assertTrue(b"data_brp=1" in bitrate_arg) self.assertTrue(b"data_tseg1=9" in bitrate_arg) self.assertTrue(b"data_tseg2=6" in bitrate_arg) self.assertTrue(b"data_sjw=1" in bitrate_arg) if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_player.py000077500000000000000000000072601472200326600174760ustar00rootroot00000000000000#!/usr/bin/env python """ This module tests the functions inside of player.py """ import io import os import sys import unittest from unittest import mock from unittest.mock import Mock import can import can.player class TestPlayerScriptModule(unittest.TestCase): logfile = os.path.join(os.path.dirname(__file__), "data", "test_CanMessage.asc") def setUp(self) -> None: # Patch VirtualBus object patcher_virtual_bus = mock.patch("can.interfaces.virtual.VirtualBus", spec=True) self.MockVirtualBus = patcher_virtual_bus.start() self.addCleanup(patcher_virtual_bus.stop) self.mock_virtual_bus = self.MockVirtualBus.return_value self.mock_virtual_bus.__enter__ = Mock(return_value=self.mock_virtual_bus) # Patch time sleep object patcher_sleep = mock.patch("can.io.player.time.sleep", spec=True) self.MockSleep = patcher_sleep.start() self.addCleanup(patcher_sleep.stop) self.baseargs = [sys.argv[0], "-i", "virtual"] def assertSuccessfulCleanup(self): self.MockVirtualBus.assert_called_once() self.mock_virtual_bus.__exit__.assert_called_once() def test_play_virtual(self): sys.argv = self.baseargs + [self.logfile] can.player.main() msg1 = can.Message( timestamp=2.501, arbitration_id=0xC8, is_extended_id=False, is_fd=False, is_rx=False, channel=1, dlc=8, data=[0x9, 0x8, 0x7, 0x6, 0x5, 0x4, 0x3, 0x2], ) msg2 = can.Message( timestamp=17.876708, arbitration_id=0x6F9, is_extended_id=False, is_fd=False, is_rx=True, channel=0, dlc=8, data=[0x5, 0xC, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0], ) self.assertTrue(msg1.equals(self.mock_virtual_bus.send.mock_calls[0].args[0])) self.assertTrue(msg2.equals(self.mock_virtual_bus.send.mock_calls[1].args[0])) self.assertSuccessfulCleanup() def test_play_virtual_verbose(self): sys.argv = self.baseargs + ["-v", self.logfile] with unittest.mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout: can.player.main() self.assertIn("09 08 07 06 05 04 03 02", mock_stdout.getvalue()) self.assertIn("05 0c 00 00 00 00 00 00", mock_stdout.getvalue()) self.assertEqual(self.mock_virtual_bus.send.call_count, 2) self.assertSuccessfulCleanup() def test_play_virtual_exit(self): self.MockSleep.side_effect = [None, KeyboardInterrupt] sys.argv = self.baseargs + [self.logfile] can.player.main() assert self.mock_virtual_bus.send.call_count <= 2 self.assertSuccessfulCleanup() def test_play_skip_error_frame(self): logfile = os.path.join( os.path.dirname(__file__), "data", "logfile_errorframes.asc" ) sys.argv = self.baseargs + ["-v", logfile] can.player.main() self.assertEqual(self.mock_virtual_bus.send.call_count, 9) self.assertSuccessfulCleanup() def test_play_error_frame(self): logfile = os.path.join( os.path.dirname(__file__), "data", "logfile_errorframes.asc" ) sys.argv = self.baseargs + ["-v", "--error-frames", logfile] can.player.main() self.assertEqual(self.mock_virtual_bus.send.call_count, 12) self.assertSuccessfulCleanup() class TestPlayerCompressedFile(TestPlayerScriptModule): """ Re-run tests using a compressed file. """ logfile = os.path.join(os.path.dirname(__file__), "data", "test_CanMessage.asc.gz") if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_robotell.py000066400000000000000000000605401472200326600200210ustar00rootroot00000000000000#!/usr/bin/env python import unittest import can class robotellTestCase(unittest.TestCase): def setUp(self): # will log timeout messages since we are not feeding ack messages to the serial port at this stage self.bus = can.Bus("loop://", interface="robotell") self.serial = self.bus.serialPortOrig self.serial.read(self.serial.in_waiting) def tearDown(self): self.bus.shutdown() def test_protocol(self): self.assertEqual(self.bus.protocol, can.CanProtocol.CAN_20) def test_recv_extended(self): self.serial.write( bytearray( [ 0xAA, 0xAA, 0x56, 0x34, 0x12, 0x00, 0xA5, 0xAA, 0xA5, 0xA5, 0xA5, 0x55, 0xA5, 0x55, 0xA5, 0xA5, 0xA5, 0xAA, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0xEB, 0x55, 0x55, ] ) ) msg = self.bus.recv(1) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x123456) self.assertEqual(msg.is_extended_id, True) self.assertEqual(msg.is_remote_frame, False) self.assertEqual(msg.dlc, 6) self.assertSequenceEqual(msg.data, [0xAA, 0xA5, 0x55, 0x55, 0xA5, 0xAA]) data = self.serial.read(self.serial.in_waiting) def test_send_extended(self): msg = can.Message( arbitration_id=0x123456, is_extended_id=True, data=[0xAA, 0xA5, 0x55, 0x55, 0xA5, 0xAA], ) self.bus.send(msg) data = self.serial.read(self.serial.in_waiting) self.assertEqual( data, bytearray( [ 0xAA, 0xAA, 0x56, 0x34, 0x12, 0x00, 0xA5, 0xAA, 0xA5, 0xA5, 0xA5, 0x55, 0xA5, 0x55, 0xA5, 0xA5, 0xA5, 0xAA, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0xEB, 0x55, 0x55, ] ), ) def test_recv_standard(self): self.serial.write( bytearray( [ 0xAA, 0xAA, 0x7B, 0x00, 0x00, 0x00, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x31, 0x32, 0x33, 0x08, 0x00, 0x00, 0x00, 0x0D, 0x55, 0x55, ] ) ) msg = self.bus.recv(1) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 123) self.assertEqual(msg.is_extended_id, False) self.assertEqual(msg.is_remote_frame, False) self.assertEqual(msg.dlc, 8) self.assertSequenceEqual( msg.data, [0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x31, 0x32, 0x33] ) data = self.serial.read(self.serial.in_waiting) def test_send_standard(self): msg = can.Message( arbitration_id=123, is_extended_id=False, data=[0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x31, 0x32, 0x33], ) self.bus.send(msg) data = self.serial.read(self.serial.in_waiting) self.assertEqual( data, bytearray( [ 0xAA, 0xAA, 0x7B, 0x00, 0x00, 0x00, 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x31, 0x32, 0x33, 0x08, 0x00, 0x00, 0x00, 0x0D, 0x55, 0x55, ] ), ) def test_recv_extended_remote(self): self.serial.write( bytearray( [ 0xAA, 0xAA, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x01, 0x01, 0xA5, 0xA5, 0x55, 0x55, ] ) ) msg = self.bus.recv(1) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x123456) self.assertEqual(msg.is_extended_id, True) self.assertEqual(msg.is_remote_frame, True) self.assertEqual(msg.dlc, 7) data = self.serial.read(self.serial.in_waiting) def test_send_extended_remote(self): msg = can.Message( arbitration_id=0x123456, is_extended_id=True, is_remote_frame=True, dlc=7 ) self.bus.send(msg) data = self.serial.read(self.serial.in_waiting) self.assertEqual( data, bytearray( [ 0xAA, 0xAA, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x01, 0x01, 0xA5, 0xA5, 0x55, 0x55, ] ), ) def test_partial_recv(self): # write some junk data and then start of message self.serial.write( bytearray([0x11, 0x22, 0x33, 0xAA, 0xAA, 0x7B, 0x00, 0x00, 0x00, 0x48]) ) msg = self.bus.recv(1) self.assertIsNone(msg) # write rest of first message, and then a second message self.serial.write( bytearray( [ 0x65, 0x6C, 0x6C, 0x6F, 0x31, 0x32, 0x33, 0x08, 0x00, 0x00, 0x00, 0x0D, 0x55, 0x55, ] ) ) self.serial.write( bytearray( [ 0xAA, 0xAA, 0x56, 0x34, 0x12, 0x00, 0xA5, 0xAA, 0xA5, 0xA5, 0xA5, 0x55, 0xA5, 0x55, 0xA5, 0xA5, 0xA5, 0xAA, 0x00, 0x00, 0x06, 0x00, 0x01, 0x00, 0xEB, 0x55, 0x55, ] ) ) msg = self.bus.recv(1) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 123) self.assertEqual(msg.is_extended_id, False) self.assertEqual(msg.is_remote_frame, False) self.assertEqual(msg.dlc, 8) self.assertSequenceEqual( msg.data, [0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x31, 0x32, 0x33] ) # now try to also receive 2nd message msg = self.bus.recv(1) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x123456) self.assertEqual(msg.is_extended_id, True) self.assertEqual(msg.is_remote_frame, False) self.assertEqual(msg.dlc, 6) self.assertSequenceEqual(msg.data, [0xAA, 0xA5, 0x55, 0x55, 0xA5, 0xAA]) # test nothing more left msg = self.bus.recv(1) self.assertIsNone(msg) data = self.serial.read(self.serial.in_waiting) def test_serial_number(self): self.serial.write( bytearray( [ 0xAA, 0xAA, 0xF0, 0xFF, 0xFF, 0x01, 0x53, 0xFF, 0x6A, 0x06, 0x49, 0x72, 0x48, 0xA5, 0x55, 0x08, 0xFF, 0x01, 0x00, 0x11, 0x55, 0x55, ] ) ) self.serial.write( bytearray( [ 0xAA, 0xAA, 0xF1, 0xFF, 0xFF, 0x01, 0x40, 0x60, 0x17, 0x87, 0x00, 0x00, 0x00, 0x00, 0x08, 0xFF, 0x01, 0x00, 0x36, 0x55, 0x55, ] ) ) sn = self.bus.get_serial_number(1) self.assertEqual(sn, "53FF-6A06-4972-4855-4060-1787") data = self.serial.read(self.serial.in_waiting) self.assertEqual( data, bytearray( [ 0xAA, 0xAA, 0xF0, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xFF, 0x01, 0x01, 0xF8, 0x55, 0x55, 0xAA, 0xAA, 0xF1, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0xFF, 0x01, 0x01, 0xF9, 0x55, 0x55, ] ), ) sn = self.bus.get_serial_number(0) self.assertIsNone(sn) data = self.serial.read(self.serial.in_waiting) def test_set_bitrate(self): self.serial.write( bytearray( [ 0xAA, 0xAA, 0xD0, 0xFE, 0xFF, 0x01, 0x40, 0x42, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xFF, 0x01, 0x01, 0x64, 0x55, 0x55, ] ) ) self.bus.set_bitrate(1000000) data = self.serial.read(self.serial.in_waiting) self.assertEqual( data, bytearray( [ 0xAA, 0xAA, 0xD0, 0xFE, 0xFF, 0x01, 0x40, 0x42, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xFF, 0x01, 0x00, 0x63, 0x55, 0x55, ] ), ) def test_set_auto_retransmit(self): self.serial.write( bytearray( [ 0xAA, 0xAA, 0xA0, 0xFE, 0xFF, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x01, 0x01, 0xA1, 0x55, 0x55, ] ) ) self.serial.write( bytearray( [ 0xAA, 0xAA, 0xA0, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x01, 0x01, 0xA0, 0x55, 0x55, ] ) ) self.bus.set_auto_retransmit(True) self.bus.set_auto_retransmit(False) data = self.serial.read(self.serial.in_waiting) self.assertEqual( data, bytearray( [ 0xAA, 0xAA, 0xA0, 0xFE, 0xFF, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x01, 0x00, 0xA0, 0x55, 0x55, 0xAA, 0xAA, 0xA0, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x01, 0x00, 0x9F, 0x55, 0x55, ] ), ) def test_set_auto_bus_management(self): self.serial.write( bytearray( [ 0xAA, 0xAA, 0xB0, 0xFE, 0xFF, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x01, 0x01, 0xB1, 0x55, 0x55, ] ) ) self.serial.write( bytearray( [ 0xAA, 0xAA, 0xB0, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x01, 0x01, 0xB0, 0x55, 0x55, ] ) ) self.bus.set_auto_bus_management(True) self.bus.set_auto_bus_management(False) data = self.serial.read(self.serial.in_waiting) self.assertEqual( data, bytearray( [ 0xAA, 0xAA, 0xB0, 0xFE, 0xFF, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x01, 0x00, 0xB0, 0x55, 0x55, 0xAA, 0xAA, 0xB0, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x01, 0x00, 0xAF, 0x55, 0x55, ] ), ) def test_set_serial_rate(self): self.serial.write( bytearray( [ 0xAA, 0xAA, 0x90, 0xFE, 0xFF, 0x01, 0x00, 0xC2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xFF, 0x01, 0x01, 0x56, 0x55, 0x55, ] ) ) self.bus.set_serial_rate(115200) data = self.serial.read(self.serial.in_waiting) self.assertEqual( data, bytearray( [ 0xAA, 0xAA, 0x90, 0xFE, 0xFF, 0x01, 0x00, 0xC2, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xFF, 0x01, 0x00, 0xA5, 0x55, 0x55, 0x55, ] ), ) def test_set_hw_filter(self): self.serial.write( bytearray( [ 0xAA, 0xAA, 0xE0, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x08, 0xFF, 0x01, 0x01, 0x67, 0x55, 0x55, ] ) ) self.serial.write( bytearray( [ 0xAA, 0xAA, 0xE1, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x08, 0xFF, 0x01, 0x01, 0xA8, 0x55, 0x55, ] ) ) self.serial.write( bytearray( [ 0xAA, 0xAA, 0xE2, 0xFE, 0xFF, 0x01, 0xF0, 0x01, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x08, 0xFF, 0x01, 0x01, 0xCB, 0x55, 0x55, ] ) ) self.bus.set_hw_filter(1, True, 0, 0, False) self.bus.set_hw_filter(2, True, 0, 0, True) self.bus.set_hw_filter(3, False, 0x1F0, 0x1F0, False) data = self.serial.read(self.serial.in_waiting) self.assertEqual( data, bytearray( [ 0xAA, 0xAA, 0xE0, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x08, 0xFF, 0x01, 0x00, 0x66, 0x55, 0x55, 0xAA, 0xAA, 0xE1, 0xFE, 0xFF, 0x01, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x08, 0xFF, 0x01, 0x00, 0xA7, 0x55, 0x55, 0xAA, 0xAA, 0xE2, 0xFE, 0xFF, 0x01, 0xF0, 0x01, 0x00, 0x00, 0xF0, 0x01, 0x00, 0x00, 0x08, 0xFF, 0x01, 0x00, 0xCA, 0x55, 0x55, ] ), ) def test_when_no_fileno(self): with self.assertRaises(NotImplementedError): self.bus.fileno() if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_rotating_loggers.py000066400000000000000000000237261472200326600215550ustar00rootroot00000000000000#!/usr/bin/env python """ Test rotating loggers """ import os from pathlib import Path from typing import cast from unittest.mock import Mock import can from can.io.generic import FileIOMessageWriter from can.typechecking import StringPathLike from .data.example_data import generate_message class TestBaseRotatingLogger: @staticmethod def _get_instance(file: StringPathLike) -> can.io.BaseRotatingLogger: class SubClass(can.io.BaseRotatingLogger): """Subclass that implements abstract methods for testing.""" _supported_formats = {".asc", ".blf", ".csv", ".log", ".txt"} def __init__(self, file: StringPathLike, **kwargs) -> None: super().__init__(**kwargs) suffix = Path(file).suffix.lower() if suffix not in self._supported_formats: raise ValueError(f"Unsupported file format: {suffix}") self._writer = can.Printer(file=file) @property def writer(self) -> FileIOMessageWriter: return cast(FileIOMessageWriter, self._writer) def should_rollover(self, msg: can.Message) -> bool: return False def do_rollover(self): ... return SubClass(file=file) def test_import(self): assert hasattr(can.io, "BaseRotatingLogger") def test_attributes(self): assert issubclass(can.io.BaseRotatingLogger, can.Listener) assert hasattr(can.io.BaseRotatingLogger, "namer") assert hasattr(can.io.BaseRotatingLogger, "rotator") assert hasattr(can.io.BaseRotatingLogger, "rollover_count") assert hasattr(can.io.BaseRotatingLogger, "writer") assert hasattr(can.io.BaseRotatingLogger, "rotation_filename") assert hasattr(can.io.BaseRotatingLogger, "rotate") assert hasattr(can.io.BaseRotatingLogger, "on_message_received") assert hasattr(can.io.BaseRotatingLogger, "stop") assert hasattr(can.io.BaseRotatingLogger, "should_rollover") assert hasattr(can.io.BaseRotatingLogger, "do_rollover") def test_get_new_writer(self, tmp_path): with self._get_instance(tmp_path / "__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() def test_rotation_filename(self, tmp_path): with self._get_instance(tmp_path / "__unused.txt") as logger_instance: default_name = "default" assert logger_instance.rotation_filename(default_name) == "default" logger_instance.namer = lambda x: x + "_by_namer" assert logger_instance.rotation_filename(default_name) == "default_by_namer" def test_rotate_without_rotator(self, tmp_path): with self._get_instance(tmp_path / "__unused.txt") as logger_instance: 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 True assert os.path.exists(dest) is False logger_instance.rotate(source, dest) assert os.path.exists(source) is False assert os.path.exists(dest) is True def test_rotate_with_rotator(self, tmp_path): 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") 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 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() 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 should_rollover = Mock(return_value=False) do_rollover = Mock() writers_on_message_received = Mock() logger_instance.should_rollover = should_rollover logger_instance.do_rollover = do_rollover logger_instance.writer.on_message_received = writers_on_message_received msg = generate_message(0x123) logger_instance.on_message_received(msg) should_rollover.assert_called_with(msg) do_rollover.assert_not_called() writers_on_message_received.assert_called_with(msg) # Test with rollover should_rollover = Mock(return_value=True) do_rollover = Mock() writers_on_message_received = Mock() logger_instance.should_rollover = should_rollover logger_instance.do_rollover = do_rollover logger_instance.writer.on_message_received = writers_on_message_received msg = generate_message(0x123) logger_instance.on_message_received(msg) should_rollover.assert_called_with(msg) do_rollover.assert_called() writers_on_message_received.assert_called_with(msg) def test_issue_1792(self, tmp_path): 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() class TestSizedRotatingLogger: def test_import(self): assert hasattr(can.io, "SizedRotatingLogger") assert hasattr(can, "SizedRotatingLogger") def test_attributes(self): assert issubclass(can.SizedRotatingLogger, can.io.BaseRotatingLogger) assert hasattr(can.SizedRotatingLogger, "namer") assert hasattr(can.SizedRotatingLogger, "rotator") assert hasattr(can.SizedRotatingLogger, "should_rollover") assert hasattr(can.SizedRotatingLogger, "do_rollover") def test_create_instance(self, tmp_path): base_filename = "mylogfile.ASC" max_bytes = 512 with can.SizedRotatingLogger( base_filename=tmp_path / base_filename, max_bytes=max_bytes ) as logger_instance: assert Path(logger_instance.base_filename).name == base_filename assert logger_instance.max_bytes == max_bytes assert logger_instance.rollover_count == 0 assert isinstance(logger_instance.writer, can.ASCWriter) def test_should_rollover(self, tmp_path): base_filename = "mylogfile.ASC" max_bytes = 512 with can.SizedRotatingLogger( base_filename=tmp_path / base_filename, max_bytes=max_bytes ) as logger_instance: msg = generate_message(0x123) do_rollover = Mock() logger_instance.do_rollover = do_rollover logger_instance.writer.file.tell = Mock(return_value=511) assert logger_instance.should_rollover(msg) is False logger_instance.on_message_received(msg) do_rollover.assert_not_called() logger_instance.writer.file.tell = Mock(return_value=512) assert logger_instance.should_rollover(msg) is True logger_instance.on_message_received(msg) do_rollover.assert_called() def test_logfile_size(self, tmp_path): base_filename = "mylogfile.ASC" max_bytes = 1024 msg = generate_message(0x123) with can.SizedRotatingLogger( base_filename=tmp_path / base_filename, max_bytes=max_bytes ) as logger_instance: for _ in range(128): logger_instance.on_message_received(msg) for file_path in os.listdir(tmp_path): assert os.path.getsize(tmp_path / file_path) <= 1100 def test_logfile_size_context_manager(self, tmp_path): base_filename = "mylogfile.ASC" max_bytes = 1024 msg = generate_message(0x123) with can.SizedRotatingLogger( base_filename=tmp_path / base_filename, max_bytes=max_bytes ) as logger_instance: for _ in range(128): logger_instance.on_message_received(msg) for file_path in os.listdir(tmp_path): assert os.path.getsize(os.path.join(tmp_path, file_path)) <= 1100 python-can-4.5.0/test/test_scripts.py000066400000000000000000000060521472200326600176640ustar00rootroot00000000000000#!/usr/bin/env python """ This module tests that the scripts are all callable. """ import errno import subprocess import sys import unittest from abc import ABCMeta, abstractmethod from .config import * class CanScriptTest(unittest.TestCase, metaclass=ABCMeta): @classmethod def setUpClass(cls): # clean up the argument list so the call to the main() functions # in test_does_not_crash() succeeds sys.argv = sys.argv[:1] def test_do_commands_exist(self): """This test calls each scripts once and verifies that the help can be read without any other errors, like the script not being found. """ for command in self._commands(): try: subprocess.check_output( command.split(), stderr=subprocess.STDOUT, encoding="utf-8", shell=IS_WINDOWS, ) except subprocess.CalledProcessError as e: return_code = e.returncode output = e.output else: return_code = 0 output = "-- NO OUTPUT --" allowed = [0, errno.EINVAL] self.assertIn( return_code, allowed, 'Calling "{}" failed (exit code was {} and not SUCCESS/0 or EINVAL/22):\n{}'.format( command, return_code, output ), ) def test_does_not_crash(self): # test import module = self._import() # test main method with self.assertRaises(SystemExit) as cm: module.main() self.assertEqual(cm.exception.code, errno.EINVAL) @abstractmethod def _commands(self): """Returns an Iterable of commands that should "succeed", meaning they exit normally (exit code 0) or with the exit code for invalid arguments: EINVAL/22. """ pass @abstractmethod def _import(self): """Returns the modue of the script that has a main() function.""" pass class TestLoggerScript(CanScriptTest): def _commands(self): commands = [ "python -m can.logger --help", "can_logger --help", ] return commands def _import(self): import can.logger as module return module class TestPlayerScript(CanScriptTest): def _commands(self): commands = [ "python -m can.player --help", "can_player --help", ] return commands def _import(self): import can.player as module return module class TestLogconvertScript(CanScriptTest): def _commands(self): commands = [ "python -m can.logconvert --help", "can_logconvert --help", ] return commands def _import(self): import can.logconvert as module return module # TODO add #390 # this excludes the base class from being executed as a test case itself del CanScriptTest if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_slcan.py000066400000000000000000000124601472200326600172750ustar00rootroot00000000000000#!/usr/bin/env python import unittest from typing import cast import serial import can.interfaces.slcan from .config import IS_PYPY """ Mentioned in #1010 & #1490 > PyPy works best with pure Python applications. Whenever you use a C extension module, > it runs much slower than in CPython. The reason is that PyPy can't optimize C extension modules since they're not fully supported. > In addition, PyPy has to emulate reference counting for that part of the code, making it even slower. https://realpython.com/pypy-faster-python/#it-doesnt-work-well-with-c-extensions """ TIMEOUT = 0.5 if IS_PYPY else 0.01 # 0.001 is the default set in slcanBus class slcanTestCase(unittest.TestCase): def setUp(self): self.bus = cast( can.interfaces.slcan.slcanBus, can.Bus("loop://", interface="slcan", sleep_after_open=0, timeout=TIMEOUT), ) self.serial = cast(serial.Serial, 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") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) self.assertEqual(msg.is_extended_id, True) self.assertEqual(msg.is_remote_frame, False) self.assertEqual(msg.dlc, 2) self.assertSequenceEqual(msg.data, [0xAA, 0x55]) # Ewert Energy Systems CANDapter specific self.serial.write(b"x12ABCDEF2AA55\r") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) self.assertEqual(msg.is_extended_id, True) self.assertEqual(msg.is_remote_frame, False) self.assertEqual(msg.dlc, 2) self.assertSequenceEqual(msg.data, [0xAA, 0x55]) def test_send_extended(self): msg = can.Message( arbitration_id=0x12ABCDEF, is_extended_id=True, data=[0xAA, 0x55] ) self.bus.send(msg) 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") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x456) self.assertEqual(msg.is_extended_id, False) self.assertEqual(msg.is_remote_frame, False) self.assertEqual(msg.dlc, 3) self.assertSequenceEqual(msg.data, [0x11, 0x22, 0x33]) def test_send_standard(self): msg = can.Message( arbitration_id=0x456, is_extended_id=False, data=[0x11, 0x22, 0x33] ) self.bus.send(msg) 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") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x123) self.assertEqual(msg.is_extended_id, False) self.assertEqual(msg.is_remote_frame, True) self.assertEqual(msg.dlc, 8) def test_send_standard_remote(self): msg = can.Message( arbitration_id=0x123, is_extended_id=False, is_remote_frame=True, dlc=8 ) self.bus.send(msg) 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") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) self.assertEqual(msg.is_extended_id, True) self.assertEqual(msg.is_remote_frame, True) self.assertEqual(msg.dlc, 6) def test_send_extended_remote(self): msg = can.Message( arbitration_id=0x12ABCDEF, is_extended_id=True, is_remote_frame=True, dlc=6 ) self.bus.send(msg) 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") msg = self.bus.recv(TIMEOUT) self.assertIsNone(msg) self.serial.write(b"2AA55\rT12") msg = self.bus.recv(TIMEOUT) self.assertIsNotNone(msg) self.assertEqual(msg.arbitration_id, 0x12ABCDEF) self.assertEqual(msg.is_extended_id, True) self.assertEqual(msg.is_remote_frame, False) self.assertEqual(msg.dlc, 2) self.assertSequenceEqual(msg.data, [0xAA, 0x55]) msg = self.bus.recv(TIMEOUT) self.assertIsNone(msg) self.serial.write(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(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(sn, "A123") sn = self.bus.get_serial_number(0) self.assertIsNone(sn) if __name__ == "__main__": unittest.main() python-can-4.5.0/test/test_socketcan.py000066400000000000000000000330401472200326600201440ustar00rootroot00000000000000#!/usr/bin/env python """ Test functions in `can.interfaces.socketcan.socketcan`. """ import ctypes import struct import unittest import warnings from unittest.mock import patch import can from can.interfaces.socketcan.constants import ( CAN_BCM_TX_DELETE, CAN_BCM_TX_SETUP, SETTIMER, STARTTIMER, TX_COUNTEVT, ) from can.interfaces.socketcan.socketcan import ( BcmMsgHead, bcm_header_factory, build_bcm_header, build_bcm_transmit_header, build_bcm_tx_delete_header, build_bcm_update_header, ) from .config import IS_LINUX, IS_PYPY, TEST_INTERFACE_SOCKETCAN class SocketCANTest(unittest.TestCase): def setUp(self): self._ctypes_sizeof = ctypes.sizeof self._ctypes_alignment = ctypes.alignment @patch("ctypes.sizeof") @patch("ctypes.alignment") def test_bcm_header_factory_32_bit_sizeof_long_4_alignof_long_4( self, ctypes_sizeof, ctypes_alignment ): """This tests a 32-bit platform (ex. Debian Stretch on i386), where: * sizeof(long) == 4 * sizeof(long long) == 8 * alignof(long) == 4 * alignof(long long) == 4 """ def side_effect_ctypes_sizeof(value): type_to_size = { ctypes.c_longlong: 8, ctypes.c_long: 4, ctypes.c_uint8: 1, ctypes.c_uint16: 2, ctypes.c_uint32: 4, ctypes.c_uint64: 8, } return type_to_size[value] def side_effect_ctypes_alignment(value): type_to_alignment = { ctypes.c_longlong: 4, ctypes.c_long: 4, ctypes.c_uint8: 1, ctypes.c_uint16: 2, ctypes.c_uint32: 4, ctypes.c_uint64: 4, } return type_to_alignment[value] ctypes_sizeof.side_effect = side_effect_ctypes_sizeof ctypes_alignment.side_effect = side_effect_ctypes_alignment fields = [ ("opcode", ctypes.c_uint32), ("flags", ctypes.c_uint32), ("count", ctypes.c_uint32), ("ival1_tv_sec", ctypes.c_long), ("ival1_tv_usec", ctypes.c_long), ("ival2_tv_sec", ctypes.c_long), ("ival2_tv_usec", ctypes.c_long), ("can_id", ctypes.c_uint32), ("nframes", ctypes.c_uint32), ] BcmMsgHead = bcm_header_factory(fields) expected_fields = [ ("opcode", ctypes.c_uint32), ("flags", ctypes.c_uint32), ("count", ctypes.c_uint32), ("ival1_tv_sec", ctypes.c_long), ("ival1_tv_usec", ctypes.c_long), ("ival2_tv_sec", ctypes.c_long), ("ival2_tv_usec", ctypes.c_long), ("can_id", ctypes.c_uint32), ("nframes", ctypes.c_uint32), # We expect 4 bytes of padding ("pad_0", ctypes.c_uint8), ("pad_1", ctypes.c_uint8), ("pad_2", ctypes.c_uint8), ("pad_3", ctypes.c_uint8), ] self.assertEqual(expected_fields, BcmMsgHead._fields_) @patch("ctypes.sizeof") @patch("ctypes.alignment") def test_bcm_header_factory_32_bit_sizeof_long_4_alignof_long_long_8( self, ctypes_sizeof, ctypes_alignment ): """This tests a 32-bit platform (ex. Raspbian Stretch on armv7l), where: * sizeof(long) == 4 * sizeof(long long) == 8 * alignof(long) == 4 * alignof(long long) == 8 """ def side_effect_ctypes_sizeof(value): type_to_size = { ctypes.c_longlong: 8, ctypes.c_long: 4, ctypes.c_uint8: 1, ctypes.c_uint16: 2, ctypes.c_uint32: 4, ctypes.c_uint64: 8, } return type_to_size[value] def side_effect_ctypes_alignment(value): type_to_alignment = { ctypes.c_longlong: 8, ctypes.c_long: 4, ctypes.c_uint8: 1, ctypes.c_uint16: 2, ctypes.c_uint32: 4, ctypes.c_uint64: 8, } return type_to_alignment[value] ctypes_sizeof.side_effect = side_effect_ctypes_sizeof ctypes_alignment.side_effect = side_effect_ctypes_alignment fields = [ ("opcode", ctypes.c_uint32), ("flags", ctypes.c_uint32), ("count", ctypes.c_uint32), ("ival1_tv_sec", ctypes.c_long), ("ival1_tv_usec", ctypes.c_long), ("ival2_tv_sec", ctypes.c_long), ("ival2_tv_usec", ctypes.c_long), ("can_id", ctypes.c_uint32), ("nframes", ctypes.c_uint32), ] BcmMsgHead = bcm_header_factory(fields) expected_fields = [ ("opcode", ctypes.c_uint32), ("flags", ctypes.c_uint32), ("count", ctypes.c_uint32), ("ival1_tv_sec", ctypes.c_long), ("ival1_tv_usec", ctypes.c_long), ("ival2_tv_sec", ctypes.c_long), ("ival2_tv_usec", ctypes.c_long), ("can_id", ctypes.c_uint32), ("nframes", ctypes.c_uint32), # We expect 4 bytes of padding ("pad_0", ctypes.c_uint8), ("pad_1", ctypes.c_uint8), ("pad_2", ctypes.c_uint8), ("pad_3", ctypes.c_uint8), ] self.assertEqual(expected_fields, BcmMsgHead._fields_) @patch("ctypes.sizeof") @patch("ctypes.alignment") def test_bcm_header_factory_64_bit_sizeof_long_8_alignof_long_8( self, ctypes_sizeof, ctypes_alignment ): """This tests a 64-bit platform (ex. Ubuntu 18.04 on x86_64), where: * sizeof(long) == 8 * sizeof(long long) == 8 * alignof(long) == 8 * alignof(long long) == 8 """ def side_effect_ctypes_sizeof(value): type_to_size = { ctypes.c_longlong: 8, ctypes.c_long: 8, ctypes.c_uint8: 1, ctypes.c_uint16: 2, ctypes.c_uint32: 4, ctypes.c_uint64: 8, } return type_to_size[value] def side_effect_ctypes_alignment(value): type_to_alignment = { ctypes.c_longlong: 8, ctypes.c_long: 8, ctypes.c_uint8: 1, ctypes.c_uint16: 2, ctypes.c_uint32: 4, ctypes.c_uint64: 8, } return type_to_alignment[value] ctypes_sizeof.side_effect = side_effect_ctypes_sizeof ctypes_alignment.side_effect = side_effect_ctypes_alignment fields = [ ("opcode", ctypes.c_uint32), ("flags", ctypes.c_uint32), ("count", ctypes.c_uint32), ("ival1_tv_sec", ctypes.c_long), ("ival1_tv_usec", ctypes.c_long), ("ival2_tv_sec", ctypes.c_long), ("ival2_tv_usec", ctypes.c_long), ("can_id", ctypes.c_uint32), ("nframes", ctypes.c_uint32), ] BcmMsgHead = bcm_header_factory(fields) expected_fields = [ ("opcode", ctypes.c_uint32), ("flags", ctypes.c_uint32), ("count", ctypes.c_uint32), # We expect 4 bytes of padding ("pad_0", ctypes.c_uint8), ("pad_1", ctypes.c_uint8), ("pad_2", ctypes.c_uint8), ("pad_3", ctypes.c_uint8), ("ival1_tv_sec", ctypes.c_long), ("ival1_tv_usec", ctypes.c_long), ("ival2_tv_sec", ctypes.c_long), ("ival2_tv_usec", ctypes.c_long), ("can_id", ctypes.c_uint32), ("nframes", ctypes.c_uint32), ] self.assertEqual(expected_fields, BcmMsgHead._fields_) def test_build_bcm_header(self): def _find_u32_fmt_char() -> str: for _fmt in ("H", "I", "L", "Q"): if struct.calcsize(_fmt) == 4: return _fmt def _standard_size_little_endian_to_native(data: bytes) -> bytes: std_le_fmt = " None: self.assertEqual(0, channel2int("can0")) self.assertEqual(0, channel2int("vcan0")) self.assertEqual(1, channel2int("vcan1")) self.assertEqual(12, channel2int("vcan12")) self.assertEqual(3, channel2int(3)) self.assertEqual(42, channel2int("42")) self.assertEqual(None, channel2int("can")) self.assertEqual(None, channel2int("can0a")) class TestCheckAdjustTimingClock(unittest.TestCase): def test_adjust_timing(self): timing = BitTiming(f_clock=80_000_000, brp=10, tseg1=13, tseg2=2, sjw=1) # Check identity case new_timing = check_or_adjust_timing_clock(timing, valid_clocks=[80_000_000]) assert timing == new_timing with pytest.warns(UserWarning) as record: new_timing = check_or_adjust_timing_clock( timing, valid_clocks=[8_000_000, 24_000_000] ) assert len(record) == 1 assert ( record[0].message.args[0] == "Adjusted f_clock in BitTiming from 80000000 to 8000000" ) assert new_timing.__class__ == BitTiming assert new_timing.f_clock == 8_000_000 assert new_timing.bitrate == timing.bitrate assert new_timing.tseg1 == timing.tseg1 assert new_timing.tseg2 == timing.tseg2 assert new_timing.sjw == timing.sjw # Check that order is preserved with pytest.warns(UserWarning) as record: new_timing = check_or_adjust_timing_clock( timing, valid_clocks=[24_000_000, 8_000_000] ) assert new_timing.f_clock == 24_000_000 assert len(record) == 1 assert ( record[0].message.args[0] == "Adjusted f_clock in BitTiming from 80000000 to 24000000" ) # Check that order is preserved for all valid clock rates with pytest.warns(UserWarning) as record: new_timing = check_or_adjust_timing_clock( timing, valid_clocks=[8_000, 24_000_000, 8_000_000] ) assert new_timing.f_clock == 24_000_000 assert len(record) == 1 assert ( record[0].message.args[0] == "Adjusted f_clock in BitTiming from 80000000 to 24000000" ) with pytest.raises(CanInitializationError): check_or_adjust_timing_clock(timing, valid_clocks=[8_000, 16_000]) def test_adjust_timing_fd(self): timing = BitTimingFd( f_clock=160_000_000, nom_brp=2, nom_tseg1=119, nom_tseg2=40, nom_sjw=40, data_brp=2, data_tseg1=29, data_tseg2=10, data_sjw=10, ) # Check identity case new_timing = check_or_adjust_timing_clock(timing, valid_clocks=[160_000_000]) assert timing == new_timing with pytest.warns(UserWarning) as record: new_timing = check_or_adjust_timing_clock( timing, valid_clocks=[8_000, 80_000_000] ) assert len(record) == 1, "; ".join( [record[i].message.args[0] for i in range(len(record))] ) # print all warnings, if more than one warning is present assert ( record[0].message.args[0] == "Adjusted f_clock in BitTimingFd from 160000000 to 80000000" ) assert new_timing.__class__ == BitTimingFd assert new_timing.f_clock == 80_000_000 assert new_timing.nom_bitrate == timing.nom_bitrate assert new_timing.nom_sample_point == timing.nom_sample_point assert new_timing.data_bitrate == timing.data_bitrate assert new_timing.data_sample_point == timing.data_sample_point with pytest.raises(CanInitializationError): check_or_adjust_timing_clock(timing, valid_clocks=[8_000, 16_000]) class TestCastFromString(unittest.TestCase): def test_cast_from_string(self) -> None: self.assertEqual(1, cast_from_string("1")) self.assertEqual(-1, cast_from_string("-1")) self.assertEqual(0, cast_from_string("-0")) self.assertEqual(1.1, cast_from_string("1.1")) self.assertEqual(-1.1, cast_from_string("-1.1")) self.assertEqual(0.1, cast_from_string(".1")) self.assertEqual(10.0, cast_from_string(".1e2")) self.assertEqual(0.001, cast_from_string(".1e-2")) self.assertEqual(-0.001, cast_from_string("-.1e-2")) self.assertEqual("text", cast_from_string("text")) self.assertEqual("", cast_from_string("")) self.assertEqual("can0", cast_from_string("can0")) self.assertEqual("0can", cast_from_string("0can")) self.assertEqual(False, cast_from_string("false")) self.assertEqual(False, cast_from_string("False")) self.assertEqual(True, cast_from_string("true")) self.assertEqual(True, cast_from_string("True")) with self.assertRaises(TypeError): cast_from_string(None) python-can-4.5.0/test/test_vector.py000066400000000000000000001463021472200326600175020ustar00rootroot00000000000000#!/usr/bin/env python """ Test for Vector Interface """ import ctypes import functools import pickle import sys import time from test.config import IS_WINDOWS from unittest.mock import Mock import pytest import can from can.interfaces.vector import ( VectorBusParams, VectorCanFdParams, VectorCanParams, VectorChannelConfig, VectorError, VectorInitializationError, VectorOperationError, canlib, xlclass, xldefine, ) XLDRIVER_FOUND = canlib.xldriver is not None @pytest.fixture() def mock_xldriver() -> None: # basic mock for XLDriver xldriver_mock = Mock() # bus creation functions xldriver_mock.xlOpenDriver = Mock() xldriver_mock.xlGetApplConfig = Mock(side_effect=xlGetApplConfig) xldriver_mock.xlGetChannelIndex = Mock(side_effect=xlGetChannelIndex) xldriver_mock.xlOpenPort = Mock(side_effect=xlOpenPort) xldriver_mock.xlCanFdSetConfiguration = Mock(return_value=0) xldriver_mock.xlCanSetChannelMode = Mock(return_value=0) xldriver_mock.xlActivateChannel = Mock(return_value=0) xldriver_mock.xlGetSyncTime = Mock(side_effect=xlGetSyncTime) xldriver_mock.xlCanSetChannelAcceptance = Mock(return_value=0) xldriver_mock.xlCanSetChannelBitrate = Mock(return_value=0) xldriver_mock.xlSetNotification = Mock(side_effect=xlSetNotification) xldriver_mock.xlCanSetChannelOutput = Mock(return_value=0) # bus deactivation functions xldriver_mock.xlDeactivateChannel = Mock(return_value=0) xldriver_mock.xlClosePort = Mock(return_value=0) xldriver_mock.xlCloseDriver = Mock() # sender functions xldriver_mock.xlCanTransmit = Mock(return_value=0) xldriver_mock.xlCanTransmitEx = Mock(return_value=0) # various functions xldriver_mock.xlCanFlushTransmitQueue = Mock() # backup unmodified values real_xldriver = canlib.xldriver real_waitforsingleobject = canlib.WaitForSingleObject real_has_events = canlib.HAS_EVENTS # set mock canlib.xldriver = xldriver_mock canlib.HAS_EVENTS = False yield # cleanup canlib.xldriver = real_xldriver canlib.WaitForSingleObject = real_waitforsingleobject canlib.HAS_EVENTS = real_has_events def test_listen_only_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", listen_only=True, _testing=True) assert isinstance(bus, canlib.VectorBus) assert bus.protocol == can.CanProtocol.CAN_20 can.interfaces.vector.canlib.xldriver.xlCanSetChannelOutput.assert_called() xlCanSetChannelOutput_args = ( can.interfaces.vector.canlib.xldriver.xlCanSetChannelOutput.call_args[0] ) assert xlCanSetChannelOutput_args[2] == xldefine.XL_OutputMode.XL_OUTPUT_MODE_SILENT @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_listen_only() -> None: bus = can.Bus( channel=0, serial=_find_virtual_can_serial(), interface="vector", receive_own_messages=True, listen_only=True, ) assert isinstance(bus, canlib.VectorBus) assert bus.protocol == can.CanProtocol.CAN_20 msg = can.Message( arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True ) bus.send(msg) received_msg = bus.recv() assert received_msg.arbitration_id == msg.arbitration_id assert received_msg.data == msg.data bus.shutdown() def test_bus_creation_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", _testing=True) assert isinstance(bus, canlib.VectorBus) assert bus.protocol == can.CanProtocol.CAN_20 can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called() xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0] assert xlOpenPort_args[5] == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION.value assert xlOpenPort_args[6] == xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_not_called() can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_not_called() @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_bus_creation() -> None: bus = can.Bus(channel=0, serial=_find_virtual_can_serial(), interface="vector") assert isinstance(bus, canlib.VectorBus) assert bus.protocol == can.CanProtocol.CAN_20 bus.shutdown() xl_channel_config = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=0 ) assert bus.channel_masks[0] == xl_channel_config.channelMask assert ( xl_channel_config.busParams.data.can.canOpMode & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CAN20 ) bus = canlib.VectorBus(channel=0, serial=_find_virtual_can_serial()) assert isinstance(bus, canlib.VectorBus) assert bus.protocol == can.CanProtocol.CAN_20 bus.shutdown() @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_bus_creation_channel_index() -> None: channel_index = 1 bus = can.Bus( channel=0, serial=_find_virtual_can_serial(), channel_index=channel_index, interface="vector", ) assert isinstance(bus, canlib.VectorBus) assert bus.protocol == can.CanProtocol.CAN_20 assert bus.channel_masks[0] == 1 << channel_index bus.shutdown() @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_bus_creation_multiple_channels() -> None: bus = can.Bus( channel="0, 1", bitrate=1_000_000, serial=_find_virtual_can_serial(), interface="vector", ) assert isinstance(bus, canlib.VectorBus) assert bus.protocol == can.CanProtocol.CAN_20 assert len(bus.channels) == 2 assert bus.mask == 3 xl_channel_config_0 = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=0 ) assert xl_channel_config_0.busParams.data.can.bitRate == 1_000_000 xl_channel_config_1 = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=1 ) assert xl_channel_config_1.busParams.data.can.bitRate == 1_000_000 bus.shutdown() def test_bus_creation_bitrate_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", bitrate=200_000, _testing=True) assert isinstance(bus, canlib.VectorBus) assert bus.protocol == can.CanProtocol.CAN_20 can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called() xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0] assert xlOpenPort_args[5] == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION.value assert xlOpenPort_args[6] == xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_not_called() can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_called() xlCanSetChannelBitrate_args = ( can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.call_args[0] ) assert xlCanSetChannelBitrate_args[2] == 200_000 @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_bus_creation_bitrate() -> None: bus = can.Bus( channel=0, serial=_find_virtual_can_serial(), interface="vector", bitrate=200_000, ) assert isinstance(bus, canlib.VectorBus) assert bus.protocol == can.CanProtocol.CAN_20 xl_channel_config = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=0 ) assert xl_channel_config.busParams.data.can.bitRate == 200_000 bus.shutdown() def test_bus_creation_fd_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", fd=True, _testing=True) assert isinstance(bus, canlib.VectorBus) assert bus.protocol == can.CanProtocol.CAN_FD can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called() xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0] assert ( xlOpenPort_args[5] == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4.value ) assert xlOpenPort_args[6] == xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_called() can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_not_called() @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_bus_creation_fd() -> None: bus = can.Bus( channel=0, serial=_find_virtual_can_serial(), interface="vector", fd=True ) assert isinstance(bus, canlib.VectorBus) assert bus.protocol == can.CanProtocol.CAN_FD xl_channel_config = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=0 ) assert ( xl_channel_config.interfaceVersion == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4 ) assert ( xl_channel_config.busParams.data.canFD.canOpMode & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CANFD ) bus.shutdown() def test_bus_creation_fd_bitrate_timings_mocked(mock_xldriver) -> None: bus = can.Bus( channel=0, interface="vector", fd=True, bitrate=500_000, data_bitrate=2_000_000, sjw_abr=16, tseg1_abr=127, tseg2_abr=32, sjw_dbr=6, tseg1_dbr=27, tseg2_dbr=12, _testing=True, ) assert isinstance(bus, canlib.VectorBus) assert bus.protocol == can.CanProtocol.CAN_FD can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called() xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0] assert ( xlOpenPort_args[5] == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4.value ) assert xlOpenPort_args[6] == xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_called() can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_not_called() xlCanFdSetConfiguration_args = ( can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.call_args[0] ) canFdConf = xlCanFdSetConfiguration_args[2] assert canFdConf.arbitrationBitRate == 500000 assert canFdConf.dataBitRate == 2000000 assert canFdConf.sjwAbr == 16 assert canFdConf.tseg1Abr == 127 assert canFdConf.tseg2Abr == 32 assert canFdConf.sjwDbr == 6 assert canFdConf.tseg1Dbr == 27 assert canFdConf.tseg2Dbr == 12 @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_bus_creation_fd_bitrate_timings() -> None: bus = can.Bus( channel=0, serial=_find_virtual_can_serial(), interface="vector", fd=True, bitrate=500_000, data_bitrate=2_000_000, sjw_abr=16, tseg1_abr=127, tseg2_abr=32, sjw_dbr=6, tseg1_dbr=27, tseg2_dbr=12, ) xl_channel_config = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=0 ) assert ( xl_channel_config.interfaceVersion == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4 ) assert ( xl_channel_config.busParams.data.canFD.canOpMode & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CANFD ) assert xl_channel_config.busParams.data.canFD.arbitrationBitRate == 500_000 assert xl_channel_config.busParams.data.canFD.sjwAbr == 16 assert xl_channel_config.busParams.data.canFD.tseg1Abr == 127 assert xl_channel_config.busParams.data.canFD.tseg2Abr == 32 assert xl_channel_config.busParams.data.canFD.sjwDbr == 6 assert xl_channel_config.busParams.data.canFD.tseg1Dbr == 27 assert xl_channel_config.busParams.data.canFD.tseg2Dbr == 12 assert xl_channel_config.busParams.data.canFD.dataBitRate == 2_000_000 bus.shutdown() def test_bus_creation_timing_8mhz_mocked(mock_xldriver) -> None: timing = can.BitTiming.from_bitrate_and_segments( f_clock=8_000_000, bitrate=125_000, tseg1=13, tseg2=2, sjw=1, ) bus = can.Bus(channel=0, interface="vector", timing=timing, _testing=True) assert isinstance(bus, canlib.VectorBus) can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called() xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0] assert xlOpenPort_args[5] == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION.value assert xlOpenPort_args[6] == xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_not_called() can.interfaces.vector.canlib.xldriver.xlCanSetChannelParamsC200.assert_called() btr0, btr1 = ( can.interfaces.vector.canlib.xldriver.xlCanSetChannelParamsC200.call_args[0] )[2:] assert btr0 == timing.btr0 assert btr1 == timing.btr1 def test_bus_creation_timing_16mhz_mocked(mock_xldriver) -> None: timing = can.BitTiming.from_bitrate_and_segments( f_clock=16_000_000, bitrate=125_000, tseg1=13, tseg2=2, sjw=1, ) bus = can.Bus(channel=0, interface="vector", timing=timing, _testing=True) assert isinstance(bus, canlib.VectorBus) can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called() xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0] assert xlOpenPort_args[5] == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION.value assert xlOpenPort_args[6] == xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_not_called() can.interfaces.vector.canlib.xldriver.xlCanSetChannelParams.assert_called() chip_params = ( can.interfaces.vector.canlib.xldriver.xlCanSetChannelParams.call_args[0] )[2] assert chip_params.bitRate == 125_000 assert chip_params.sjw == 1 assert chip_params.tseg1 == 13 assert chip_params.tseg2 == 2 assert chip_params.sam == 1 @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_bus_creation_timing() -> None: for f_clock in [8_000_000, 16_000_000]: timing = can.BitTiming.from_bitrate_and_segments( f_clock=f_clock, bitrate=125_000, tseg1=13, tseg2=2, sjw=1, ) bus = can.Bus( channel=0, serial=_find_virtual_can_serial(), interface="vector", timing=timing, ) assert isinstance(bus, canlib.VectorBus) assert bus.protocol == can.CanProtocol.CAN_20 xl_channel_config = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=0 ) assert xl_channel_config.busParams.data.can.bitRate == 125_000 assert xl_channel_config.busParams.data.can.sjw == 1 assert xl_channel_config.busParams.data.can.tseg1 == 13 assert xl_channel_config.busParams.data.can.tseg2 == 2 bus.shutdown() def test_bus_creation_timingfd_mocked(mock_xldriver) -> None: timing = can.BitTimingFd.from_bitrate_and_segments( f_clock=80_000_000, nom_bitrate=500_000, nom_tseg1=68, nom_tseg2=11, nom_sjw=10, data_bitrate=2_000_000, data_tseg1=10, data_tseg2=9, data_sjw=8, ) bus = can.Bus( channel=0, interface="vector", timing=timing, _testing=True, ) assert isinstance(bus, canlib.VectorBus) assert bus.protocol == can.CanProtocol.CAN_FD can.interfaces.vector.canlib.xldriver.xlOpenDriver.assert_called() can.interfaces.vector.canlib.xldriver.xlGetApplConfig.assert_called() can.interfaces.vector.canlib.xldriver.xlOpenPort.assert_called() xlOpenPort_args = can.interfaces.vector.canlib.xldriver.xlOpenPort.call_args[0] assert ( xlOpenPort_args[5] == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4.value ) assert xlOpenPort_args[6] == xldefine.XL_BusTypes.XL_BUS_TYPE_CAN.value can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.assert_called() can.interfaces.vector.canlib.xldriver.xlCanSetChannelBitrate.assert_not_called() xlCanFdSetConfiguration_args = ( can.interfaces.vector.canlib.xldriver.xlCanFdSetConfiguration.call_args[0] ) canFdConf = xlCanFdSetConfiguration_args[2] assert canFdConf.arbitrationBitRate == 500_000 assert canFdConf.dataBitRate == 2_000_000 assert canFdConf.sjwAbr == 10 assert canFdConf.tseg1Abr == 68 assert canFdConf.tseg2Abr == 11 assert canFdConf.sjwDbr == 8 assert canFdConf.tseg1Dbr == 10 assert canFdConf.tseg2Dbr == 9 @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_bus_creation_timingfd() -> None: timing = can.BitTimingFd.from_bitrate_and_segments( f_clock=80_000_000, nom_bitrate=500_000, nom_tseg1=68, nom_tseg2=11, nom_sjw=10, data_bitrate=2_000_000, data_tseg1=10, data_tseg2=9, data_sjw=8, ) bus = can.Bus( channel=0, serial=_find_virtual_can_serial(), interface="vector", timing=timing, ) assert bus.protocol == can.CanProtocol.CAN_FD xl_channel_config = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=0 ) assert ( xl_channel_config.interfaceVersion == xldefine.XL_InterfaceVersion.XL_INTERFACE_VERSION_V4 ) assert ( xl_channel_config.busParams.data.canFD.canOpMode & xldefine.XL_CANFD_BusParams_CanOpMode.XL_BUS_PARAMS_CANOPMODE_CANFD ) assert xl_channel_config.busParams.data.canFD.arbitrationBitRate == 500_000 assert xl_channel_config.busParams.data.canFD.sjwAbr == 10 assert xl_channel_config.busParams.data.canFD.tseg1Abr == 68 assert xl_channel_config.busParams.data.canFD.tseg2Abr == 11 assert xl_channel_config.busParams.data.canFD.sjwDbr == 8 assert xl_channel_config.busParams.data.canFD.tseg1Dbr == 10 assert xl_channel_config.busParams.data.canFD.tseg2Dbr == 9 assert xl_channel_config.busParams.data.canFD.dataBitRate == 2_000_000 bus.shutdown() def test_send_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", _testing=True) msg = can.Message( arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True ) bus.send(msg) can.interfaces.vector.canlib.xldriver.xlCanTransmit.assert_called() can.interfaces.vector.canlib.xldriver.xlCanTransmitEx.assert_not_called() def test_send_fd_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", fd=True, _testing=True) msg = can.Message( arbitration_id=0xC0FFEF, data=[1, 2, 3, 4, 5, 6, 7, 8], is_extended_id=True ) bus.send(msg) can.interfaces.vector.canlib.xldriver.xlCanTransmit.assert_not_called() can.interfaces.vector.canlib.xldriver.xlCanTransmitEx.assert_called() def test_receive_mocked(mock_xldriver) -> None: can.interfaces.vector.canlib.xldriver.xlReceive = Mock(side_effect=xlReceive) bus = can.Bus(channel=0, interface="vector", _testing=True) bus.recv(timeout=0.05) can.interfaces.vector.canlib.xldriver.xlReceive.assert_called() can.interfaces.vector.canlib.xldriver.xlCanReceive.assert_not_called() def test_receive_fd_mocked(mock_xldriver) -> None: can.interfaces.vector.canlib.xldriver.xlCanReceive = Mock(side_effect=xlCanReceive) bus = can.Bus(channel=0, interface="vector", fd=True, _testing=True) bus.recv(timeout=0.05) can.interfaces.vector.canlib.xldriver.xlReceive.assert_not_called() can.interfaces.vector.canlib.xldriver.xlCanReceive.assert_called() @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_send_and_receive() -> None: bus1 = can.Bus(channel=0, serial=_find_virtual_can_serial(), interface="vector") bus2 = can.Bus(channel=0, serial=_find_virtual_can_serial(), interface="vector") msg_std = can.Message( channel=0, arbitration_id=0xFF, data=list(range(8)), is_extended_id=False ) msg_ext = can.Message( channel=0, arbitration_id=0xFFFFFF, data=list(range(8)), is_extended_id=True ) bus1.send(msg_std) msg_std_recv = bus2.recv(None) assert msg_std.equals(msg_std_recv, timestamp_delta=None) bus1.send(msg_ext) msg_ext_recv = bus2.recv(None) assert msg_ext.equals(msg_ext_recv, timestamp_delta=None) bus1.shutdown() bus2.shutdown() @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_send_and_receive_fd() -> None: bus1 = can.Bus( channel=0, serial=_find_virtual_can_serial(), fd=True, interface="vector" ) bus2 = can.Bus( channel=0, serial=_find_virtual_can_serial(), fd=True, interface="vector" ) msg_std = can.Message( channel=0, arbitration_id=0xFF, data=list(range(64)), is_extended_id=False, is_fd=True, ) msg_ext = can.Message( channel=0, arbitration_id=0xFFFFFF, data=list(range(64)), is_extended_id=True, is_fd=True, ) bus1.send(msg_std) msg_std_recv = bus2.recv(None) assert msg_std.equals(msg_std_recv, timestamp_delta=None) bus1.send(msg_ext) msg_ext_recv = bus2.recv(None) assert msg_ext.equals(msg_ext_recv, timestamp_delta=None) bus1.shutdown() bus2.shutdown() def test_receive_non_msg_event_mocked(mock_xldriver) -> None: can.interfaces.vector.canlib.xldriver.xlReceive = Mock( side_effect=xlReceive_chipstate ) bus = can.Bus(channel=0, interface="vector", _testing=True) bus.handle_can_event = Mock() bus.recv(timeout=0.05) can.interfaces.vector.canlib.xldriver.xlReceive.assert_called() can.interfaces.vector.canlib.xldriver.xlCanReceive.assert_not_called() bus.handle_can_event.assert_called() @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_receive_non_msg_event() -> None: bus = canlib.VectorBus( channel=0, serial=_find_virtual_can_serial(), interface="vector" ) bus.handle_can_event = Mock() bus.xldriver.xlCanRequestChipState(bus.port_handle, bus.channel_masks[0]) bus.recv(timeout=0.5) bus.handle_can_event.assert_called() bus.shutdown() def test_receive_fd_non_msg_event_mocked(mock_xldriver) -> None: can.interfaces.vector.canlib.xldriver.xlCanReceive = Mock( side_effect=xlCanReceive_chipstate ) bus = can.Bus(channel=0, interface="vector", fd=True, _testing=True) bus.handle_canfd_event = Mock() bus.recv(timeout=0.05) can.interfaces.vector.canlib.xldriver.xlReceive.assert_not_called() can.interfaces.vector.canlib.xldriver.xlCanReceive.assert_called() bus.handle_canfd_event.assert_called() @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_receive_fd_non_msg_event() -> None: bus = canlib.VectorBus( channel=0, serial=_find_virtual_can_serial(), fd=True, interface="vector" ) bus.handle_canfd_event = Mock() bus.xldriver.xlCanRequestChipState(bus.port_handle, bus.channel_masks[0]) bus.recv(timeout=0.5) bus.handle_canfd_event.assert_called() bus.shutdown() def test_flush_tx_buffer_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", _testing=True) bus.flush_tx_buffer() transmit_args = can.interfaces.vector.canlib.xldriver.xlCanTransmit.call_args[0] num_msg = transmit_args[2] assert num_msg.value == ctypes.c_uint(1).value event = transmit_args[3] assert isinstance(event, xlclass.XLevent) assert event.tag & xldefine.XL_EventTags.XL_TRANSMIT_MSG assert event.tagData.msg.flags & ( xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_OVERRUN | xldefine.XL_MessageFlags.XL_CAN_MSG_FLAG_WAKEUP ) def test_flush_tx_buffer_fd_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", fd=True, _testing=True) bus.flush_tx_buffer() transmit_args = can.interfaces.vector.canlib.xldriver.xlCanTransmitEx.call_args[0] num_msg = transmit_args[2] assert num_msg.value == ctypes.c_uint(1).value num_msg_sent = transmit_args[3] assert num_msg_sent.value == ctypes.c_uint(0).value event = transmit_args[4] assert isinstance(event, xlclass.XLcanTxEvent) assert event.tag & xldefine.XL_CANFD_TX_EventTags.XL_CAN_EV_TAG_TX_MSG assert ( event.tagData.canMsg.msgFlags & xldefine.XL_CANFD_TX_MessageFlags.XL_CAN_TXMSG_FLAG_HIGHPRIO ) @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_flush_tx_buffer() -> None: bus = can.Bus(channel=0, serial=_find_virtual_can_serial(), interface="vector") bus.flush_tx_buffer() bus.shutdown() def test_shutdown_mocked(mock_xldriver) -> None: bus = can.Bus(channel=0, interface="vector", _testing=True) bus.shutdown() can.interfaces.vector.canlib.xldriver.xlDeactivateChannel.assert_called() can.interfaces.vector.canlib.xldriver.xlClosePort.assert_called() can.interfaces.vector.canlib.xldriver.xlCloseDriver.assert_called() @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_shutdown() -> None: bus = can.Bus(channel=0, serial=_find_virtual_can_serial(), interface="vector") xl_channel_config = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=0 ) assert xl_channel_config.isOnBus != 0 bus.shutdown() xl_channel_config = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=0 ) assert xl_channel_config.isOnBus == 0 def test_reset_mocked(mock_xldriver) -> None: bus = canlib.VectorBus(channel=0, interface="vector", _testing=True) bus.reset() can.interfaces.vector.canlib.xldriver.xlDeactivateChannel.assert_called() can.interfaces.vector.canlib.xldriver.xlActivateChannel.assert_called() @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_reset() -> None: bus = canlib.VectorBus( channel=0, serial=_find_virtual_can_serial(), interface="vector" ) bus.reset() bus.shutdown() def test_popup_hw_cfg_mocked(mock_xldriver) -> None: canlib.xldriver.xlPopupHwConfig = Mock() canlib.VectorBus.popup_vector_hw_configuration(10) assert canlib.xldriver.xlPopupHwConfig.called args, kwargs = canlib.xldriver.xlPopupHwConfig.call_args assert isinstance(args[0], ctypes.c_char_p) assert isinstance(args[1], ctypes.c_uint) @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_popup_hw_cfg() -> None: with pytest.raises(VectorOperationError): canlib.VectorBus.popup_vector_hw_configuration(1) def test_get_application_config_mocked(mock_xldriver) -> None: canlib.xldriver.xlGetApplConfig = Mock() canlib.VectorBus.get_application_config(app_name="CANalyzer", app_channel=0) assert canlib.xldriver.xlGetApplConfig.called def test_set_application_config_mocked(mock_xldriver) -> None: canlib.xldriver.xlSetApplConfig = Mock() canlib.VectorBus.set_application_config( app_name="CANalyzer", app_channel=0, hw_type=xldefine.XL_HardwareType.XL_HWTYPE_VN1610, hw_index=0, hw_channel=0, ) assert canlib.xldriver.xlSetApplConfig.called @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_set_and_get_application_config() -> None: xl_channel_config = _find_xl_channel_config( serial=_find_virtual_can_serial(), channel=1 ) canlib.VectorBus.set_application_config( app_name="python-can::test_vector", app_channel=5, hw_channel=xl_channel_config.hwChannel, hw_index=xl_channel_config.hwIndex, hw_type=xldefine.XL_HardwareType(xl_channel_config.hwType), ) hw_type, hw_index, hw_channel = canlib.VectorBus.get_application_config( app_name="python-can::test_vector", app_channel=5, ) assert hw_type == xldefine.XL_HardwareType(xl_channel_config.hwType) assert hw_index == xl_channel_config.hwIndex assert hw_channel == xl_channel_config.hwChannel def test_set_timer_mocked(mock_xldriver) -> None: canlib.xldriver.xlSetTimerRate = Mock() bus = canlib.VectorBus(channel=0, interface="vector", fd=True, _testing=True) bus.set_timer_rate(timer_rate_ms=1) assert canlib.xldriver.xlSetTimerRate.called @pytest.mark.skipif(not XLDRIVER_FOUND, reason="Vector XL API is unavailable") def test_set_timer() -> None: bus = canlib.VectorBus( channel=0, serial=_find_virtual_can_serial(), interface="vector" ) bus.handle_can_event = Mock() bus.set_timer_rate(timer_rate_ms=1) t0 = time.perf_counter() while time.perf_counter() - t0 < 0.5: bus.recv(timeout=-1) # call_count is incorrect when using virtual bus # assert bus.handle_can_event.call_count > 498 # assert bus.handle_can_event.call_count < 502 @pytest.mark.skipif(IS_WINDOWS, reason="Not relevant for Windows.") def test_called_without_testing_argument() -> None: """This tests if an exception is thrown when we are not running on Windows.""" with pytest.raises(can.CanInterfaceNotImplementedError): # do not set the _testing argument, since it would suppress the exception can.Bus(channel=0, interface="vector") def test_vector_error_pickle() -> None: for error_type in [ VectorError, VectorInitializationError, VectorOperationError, ]: error_code = 118 error_string = "XL_ERROR" function = "function_name" exc = error_type(error_code, error_string, function) # pickle and unpickle p = pickle.dumps(exc) exc_unpickled: VectorError = pickle.loads(p) assert str(exc) == str(exc_unpickled) assert error_code == exc_unpickled.error_code with pytest.raises(error_type): raise exc_unpickled def test_vector_subtype_error_from_generic() -> None: for error_type in [VectorInitializationError, VectorOperationError]: error_code = 118 error_string = "XL_ERROR" function = "function_name" generic = VectorError(error_code, error_string, function) # pickle and unpickle specific: VectorError = error_type.from_generic(generic) assert str(generic) == str(specific) assert error_code == specific.error_code with pytest.raises(error_type): raise specific def test_iterate_channel_index() -> None: channel_mask = 0x23 # 100011 channels = list(canlib._iterate_channel_index(channel_mask)) assert channels == [0, 1, 5] @pytest.mark.skipif( sys.byteorder != "little", reason="Test relies on little endian data." ) def test_get_channel_configs() -> None: _original_func = canlib._get_xl_driver_config canlib._get_xl_driver_config = _get_predefined_xl_driver_config channel_configs = canlib.get_channel_configs() assert len(channel_configs) == 12 canlib._get_xl_driver_config = _original_func @pytest.mark.skipif( sys.byteorder != "little", reason="Test relies on little endian data." ) def test_detect_available_configs() -> None: _original_func = canlib._get_xl_driver_config canlib._get_xl_driver_config = _get_predefined_xl_driver_config available_configs = canlib.VectorBus._detect_available_configs() assert len(available_configs) == 5 assert available_configs[0]["interface"] == "vector" assert available_configs[0]["channel"] == 2 assert available_configs[0]["serial"] == 1001 assert available_configs[0]["channel_index"] == 2 assert available_configs[0]["hw_type"] == xldefine.XL_HardwareType.XL_HWTYPE_VN8900 assert available_configs[0]["hw_index"] == 0 assert available_configs[0]["supports_fd"] is True assert isinstance( available_configs[0]["vector_channel_config"], VectorChannelConfig ) canlib._get_xl_driver_config = _original_func @pytest.mark.skipif(not IS_WINDOWS, reason="Windows specific test") def test_winapi_availability() -> None: assert canlib.WaitForSingleObject is not None assert canlib.INFINITE is not None def test_vector_channel_config_attributes(): assert hasattr(VectorChannelConfig, "name") assert hasattr(VectorChannelConfig, "hw_type") assert hasattr(VectorChannelConfig, "hw_index") assert hasattr(VectorChannelConfig, "hw_channel") assert hasattr(VectorChannelConfig, "channel_index") assert hasattr(VectorChannelConfig, "channel_mask") assert hasattr(VectorChannelConfig, "channel_capabilities") assert hasattr(VectorChannelConfig, "channel_bus_capabilities") assert hasattr(VectorChannelConfig, "is_on_bus") assert hasattr(VectorChannelConfig, "bus_params") assert hasattr(VectorChannelConfig, "connected_bus_type") assert hasattr(VectorChannelConfig, "serial_number") assert hasattr(VectorChannelConfig, "article_number") assert hasattr(VectorChannelConfig, "transceiver_name") def test_vector_bus_params_attributes(): assert hasattr(VectorBusParams, "bus_type") assert hasattr(VectorBusParams, "can") assert hasattr(VectorBusParams, "canfd") def test_vector_can_params_attributes(): assert hasattr(VectorCanParams, "bitrate") assert hasattr(VectorCanParams, "sjw") assert hasattr(VectorCanParams, "tseg1") assert hasattr(VectorCanParams, "tseg2") assert hasattr(VectorCanParams, "sam") assert hasattr(VectorCanParams, "output_mode") assert hasattr(VectorCanParams, "can_op_mode") def test_vector_canfd_params_attributes(): assert hasattr(VectorCanFdParams, "bitrate") assert hasattr(VectorCanFdParams, "data_bitrate") assert hasattr(VectorCanFdParams, "sjw_abr") assert hasattr(VectorCanFdParams, "tseg1_abr") assert hasattr(VectorCanFdParams, "tseg2_abr") assert hasattr(VectorCanFdParams, "sam_abr") assert hasattr(VectorCanFdParams, "sjw_dbr") assert hasattr(VectorCanFdParams, "tseg1_dbr") assert hasattr(VectorCanFdParams, "tseg2_dbr") assert hasattr(VectorCanFdParams, "output_mode") assert hasattr(VectorCanFdParams, "can_op_mode") # ***************************************************************************** # Utility functions # ***************************************************************************** def _find_xl_channel_config(serial: int, channel: int) -> xlclass.XLchannelConfig: """Helper function""" xl_driver_config = xlclass.XLdriverConfig() canlib.xldriver.xlOpenDriver() canlib.xldriver.xlGetDriverConfig(xl_driver_config) canlib.xldriver.xlCloseDriver() for i in range(xl_driver_config.channelCount): xl_channel_config: xlclass.XLchannelConfig = xl_driver_config.channel[i] if xl_channel_config.serialNumber != serial: continue if xl_channel_config.hwChannel != channel: continue return xl_channel_config raise LookupError("XLchannelConfig not found.") @functools.lru_cache def _find_virtual_can_serial() -> int: """Serial number might be 0 or 100 depending on driver version.""" xl_driver_config = xlclass.XLdriverConfig() canlib.xldriver.xlOpenDriver() canlib.xldriver.xlGetDriverConfig(xl_driver_config) canlib.xldriver.xlCloseDriver() for i in range(xl_driver_config.channelCount): xl_channel_config: xlclass.XLchannelConfig = xl_driver_config.channel[i] if "Virtual CAN" in xl_channel_config.transceiverName.decode(): return xl_channel_config.serialNumber raise LookupError("Vector virtual CAN not found") XL_DRIVER_CONFIG_EXAMPLE = ( b"\x0E\x00\x1E\x14\x0C\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E\x6E" b"\x65\x6C\x20\x53\x74\x72\x65\x61\x6D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x2D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x04" b"\x0A\x40\x00\x02\x00\x02\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x04\x00\x00\x00\x00\x00\x00\x00\x8E" b"\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00\x08" b"\x1C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31" b"\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x31\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x01\x03\x02\x00\x00\x00\x00\x01\x02\x00\x00" b"\x00\x00\x00\x00\x00\x02\x10\x00\x08\x07\x01\x04\x00\x00\x00\x00\x00\x00\x04\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x04\x00" b"\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x46\x52\x70\x69\x67\x67\x79\x20\x31\x30" b"\x38\x30\x41\x6D\x61\x67\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x05\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x32\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x02\x3C\x01\x00" b"\x00\x00\x00\x02\x04\x00\x00\x00\x00\x00\x00\x00\x12\x00\x00\xA2\x03\x05\x01\x00" b"\x00\x00\x04\x00\x00\x01\x00\x00\x00\x20\xA1\x07\x00\x01\x04\x03\x01\x01\x00\x00" b"\x00\x00\x00\x00\x00\x01\x80\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00\x00\x00\x00" b"\x00\x0C\x00\x02\x0A\x04\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00\x00" b"\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x4F\x6E\x20" b"\x62\x6F\x61\x72\x64\x20\x43\x41\x4E\x20\x31\x30\x35\x31\x63\x61\x70\x28\x48\x69" b"\x67\x68\x73\x70\x65\x65\x64\x29\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03" b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E" b"\x6E\x65\x6C\x20\x33\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x2D\x00\x03\x3C\x01\x00\x00\x00\x00\x03\x08\x00\x00\x00\x00\x00\x00\x00\x12" b"\x00\x00\xA2\x03\x09\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xA1\x07\x00" b"\x01\x04\x03\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x9B\x00\x00\x00\x68\x89\x09" b"\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x04\x00\x00\x00\x00\x00\x00\x00" b"\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00" b"\x08\x1C\x00\x00\x4F\x6E\x20\x62\x6F\x61\x72\x64\x20\x43\x41\x4E\x20\x31\x30\x35" b"\x31\x63\x61\x70\x28\x48\x69\x67\x68\x73\x70\x65\x65\x64\x29\x00\x04\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39" b"\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x34\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x04\x33\x01\x00\x00\x00\x00\x04\x10\x00" b"\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x09\x02\x08\x00\x00\x00\x00\x00\x02" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x03" b"\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x4C\x49\x4E\x70\x69\x67\x67\x79\x20" b"\x37\x32\x36\x39\x6D\x61\x67\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x07\x00\x00\x00\x70\x17\x00\x00\x0C\x09\x03\x04\x58\x02\x10\x0E\x30" b"\x57\x05\x00\x00\x00\x00\x00\x88\x13\x88\x13\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x35\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x05\x00\x00" b"\x00\x00\x02\x00\x05\x20\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x0C\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00" b"\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61" b"\x6E\x6E\x65\x6C\x20\x36\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x2D\x00\x06\x00\x00\x00\x00\x02\x00\x06\x40\x00\x00\x00\x00\x00\x00\x00" b"\x02\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00" b"\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00" b"\x00\x08\x1C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38" b"\x39\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x37\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x07\x00\x00\x00\x00\x02\x00\x07\x80" b"\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A" b"\x00\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x38" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x2D\x00\x08\x3C" b"\x01\x00\x00\x00\x00\x08\x00\x01\x00\x00\x00\x00\x00\x00\x12\x00\x00\xA2\x01\x00" b"\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xA1\x07\x00\x01\x04\x03\x01\x01" b"\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00\x00" b"\x00\x00\x00\x0C\x00\x02\x0A\x04\x00\x00\x00\x00\x00\x00\x00\x8E\x00\x02\x0A\x00" b"\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03\x00\x00\x08\x1C\x00\x00\x4F" b"\x6E\x20\x62\x6F\x61\x72\x64\x20\x43\x41\x4E\x20\x31\x30\x35\x31\x63\x61\x70\x28" b"\x48\x69\x67\x68\x73\x70\x65\x65\x64\x29\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x4E\x38\x39\x31\x34\x20\x43\x68" b"\x61\x6E\x6E\x65\x6C\x20\x39\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x2D\x00\x09\x80\x02\x00\x00\x00\x00\x09\x00\x02\x00\x00\x00\x00\x00" b"\x00\x02\x00\x00\x00\x40\x00\x40\x00\x00\x00\x00\x00\x00\x40\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0C\x00\x02\x0A\x03\x00\x00\x00\x00\x00" b"\x00\x00\x8E\x00\x02\x0A\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xE9\x03" b"\x00\x00\x08\x1C\x00\x00\x44\x2F\x41\x20\x49\x4F\x70\x69\x67\x67\x79\x20\x38\x36" b"\x34\x32\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x03\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x69" b"\x72\x74\x75\x61\x6C\x20\x43\x68\x61\x6E\x6E\x65\x6C\x20\x31\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x16\x00\x00\x00\x00\x00\x0A" b"\x00\x04\x00\x00\x00\x00\x00\x00\x07\x00\x00\xA0\x01\x00\x01\x00\x00\x00\x00\x00" b"\x00\x01\x00\x00\x00\x20\xA1\x07\x00\x01\x04\x03\x01\x01\x00\x00\x00\x00\x00\x00" b"\x00\x01\x00\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x1E" b"\x14\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x69\x72\x74\x75\x61\x6C" b"\x20\x43\x41\x4E\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x56\x69\x72\x74\x75\x61\x6C\x20\x43\x68\x61\x6E\x6E\x65\x6C" b"\x20\x32\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01" b"\x16\x00\x00\x00\x00\x00\x0B\x00\x08\x00\x00\x00\x00\x00\x00\x07\x00\x00\xA0\x01" b"\x00\x01\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x20\xA1\x07\x00\x01\x04\x03\x01" b"\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x68\x89\x09\x00\x00\x00\x00" b"\x00\x00\x00\x00\x10\x00\x1E\x14\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x56\x69\x72\x74\x75\x61\x6C\x20\x43\x41\x4E\x00\x00\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00" b"\x00\x00\x00\x02" + 11832 * b"\x00" ) def _get_predefined_xl_driver_config() -> xlclass.XLdriverConfig: return xlclass.XLdriverConfig.from_buffer_copy(XL_DRIVER_CONFIG_EXAMPLE) # ***************************************************************************** # Mock functions/side effects # ***************************************************************************** def xlGetApplConfig( app_name_p: ctypes.c_char_p, app_channel: ctypes.c_uint, hw_type: ctypes.POINTER(ctypes.c_uint), hw_index: ctypes.POINTER(ctypes.c_uint), hw_channel: ctypes.POINTER(ctypes.c_uint), bus_type: ctypes.c_uint, ) -> int: hw_type.value = 1 hw_channel.value = 0 return 0 def xlGetChannelIndex( hw_type: ctypes.c_int, hw_index: ctypes.c_int, hw_channel: ctypes.c_int ) -> int: return hw_channel def xlOpenPort( port_handle_p: ctypes.POINTER(xlclass.XLportHandle), app_name_p: ctypes.c_char_p, access_mask: int, permission_mask: xlclass.XLaccess, rx_queue_size: ctypes.c_uint, xl_interface_version: ctypes.c_uint, bus_type: ctypes.c_uint, ) -> int: port_handle_p.value = 0 permission_mask.value = access_mask return 0 def xlGetSyncTime( port_handle: xlclass.XLportHandle, time_p: ctypes.POINTER(xlclass.XLuint64) ) -> int: time_p.value = 544219859027581 return 0 def xlSetNotification( port_handle: xlclass.XLportHandle, event_handle: ctypes.POINTER(xlclass.XLhandle), queue_level: ctypes.c_int, ) -> int: event_handle.value = 520 return 0 def xlReceive( port_handle: xlclass.XLportHandle, event_count_p: ctypes.POINTER(ctypes.c_uint), event: ctypes.POINTER(xlclass.XLevent), ) -> int: event.tag = xldefine.XL_EventTags.XL_RECEIVE_MSG.value event.tagData.msg.id = 0x123 event.tagData.msg.dlc = 8 event.tagData.msg.flags = 0 event.timeStamp = 0 event.chanIndex = 0 for idx, value in enumerate([1, 2, 3, 4, 5, 6, 7, 8]): event.tagData.msg.data[idx] = value return 0 def xlCanReceive( port_handle: xlclass.XLportHandle, event: ctypes.POINTER(xlclass.XLcanRxEvent) ) -> int: event.tag = xldefine.XL_CANFD_RX_EventTags.XL_CAN_EV_TAG_RX_OK.value event.tagData.canRxOkMsg.canId = 0x123 event.tagData.canRxOkMsg.dlc = 8 event.tagData.canRxOkMsg.msgFlags = 0 event.timeStamp = 0 event.chanIndex = 0 for idx, value in enumerate([1, 2, 3, 4, 5, 6, 7, 8]): event.tagData.canRxOkMsg.data[idx] = value return 0 def xlReceive_chipstate( port_handle: xlclass.XLportHandle, event_count_p: ctypes.POINTER(ctypes.c_uint), event: ctypes.POINTER(xlclass.XLevent), ) -> int: event.tag = xldefine.XL_EventTags.XL_CHIP_STATE.value event.tagData.chipState.busStatus = 8 event.tagData.chipState.rxErrorCounter = 0 event.tagData.chipState.txErrorCounter = 0 event.timeStamp = 0 event.chanIndex = 2 return 0 def xlCanReceive_chipstate( port_handle: xlclass.XLportHandle, event: ctypes.POINTER(xlclass.XLcanRxEvent) ) -> int: event.tag = xldefine.XL_CANFD_RX_EventTags.XL_CAN_EV_TAG_CHIP_STATE.value event.tagData.canChipState.busStatus = 8 event.tagData.canChipState.rxErrorCounter = 0 event.tagData.canChipState.txErrorCounter = 0 event.timeStamp = 0 event.chanIndex = 2 return 0 python-can-4.5.0/test/test_viewer.py000066400000000000000000000510041472200326600174730ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright (C) 2018 Kristian Sloth Lauszus. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 3 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # Contact information # ------------------- # Kristian Sloth Lauszus # Web : http://www.lauszus.com # e-mail : lauszus@gmail.com import argparse import math import os import random import struct import time import unittest from collections import defaultdict from test.config import IS_CI from unittest.mock import patch import pytest import can from can.viewer import CanViewer, _parse_viewer_args # Allow the curses module to be missing (e.g. on PyPy on Windows) try: import curses CURSES_AVAILABLE = True except ImportError: curses = None # type: ignore CURSES_AVAILABLE = False # noinspection SpellCheckingInspection,PyUnusedLocal class StdscrDummy: def __init__(self): self.key_counter = 0 self.draw_buffer = defaultdict(dict) @staticmethod def clear(): pass @staticmethod def erase(): pass @staticmethod def getmaxyx(): # Set y-value, so scrolling gets tested # Set x-value, so the text will fit in the window return 1, 100 def addstr(self, row, col, txt, *args): assert row >= 0 assert col >= 0 assert txt is not None # Save the text written into the buffer for i, t in enumerate(txt): self.draw_buffer[row][col + i] = t # Raise an exception 50 % of the time, so we can make sure the code handles it if random.random() < 0.5: raise curses.error @staticmethod def nodelay(_bool): pass def getch(self): assert curses is not None self.key_counter += 1 if self.key_counter == 1: # Send invalid key return -1 elif self.key_counter == 2: return ord("c") # Clear elif self.key_counter == 3: return curses.ascii.SP # Pause elif self.key_counter == 4: return curses.ascii.SP # Unpause elif self.key_counter == 5: return ord("s") # Sort # Turn on byte highlighting (toggle) elif self.key_counter == 6: return ord("h") # Turn off byte highlighting (toggle) elif self.key_counter == 7: return ord("h") # Keep scrolling until it exceeds the number of messages elif self.key_counter <= 100: return curses.KEY_DOWN # Scroll until the header is back as the first line and then scroll over the limit elif self.key_counter <= 200: return curses.KEY_UP return curses.ascii.ESC @unittest.skipUnless(CURSES_AVAILABLE, "curses might be missing on some platforms") class CanViewerTest(unittest.TestCase): @classmethod def setUpClass(cls): # Set seed, so the tests are not affected random.seed(0) def setUp(self): self.stdscr_dummy = StdscrDummy() config = {"interface": "virtual", "receive_own_messages": True} bus = can.Bus(**config) data_structs = None patch_curs_set = patch("curses.curs_set") patch_curs_set.start() self.addCleanup(patch_curs_set.stop) patch_use_default_colors = patch("curses.use_default_colors") patch_use_default_colors.start() self.addCleanup(patch_use_default_colors.stop) patch_init_pair = patch("curses.init_pair") patch_init_pair.start() self.addCleanup(patch_init_pair.stop) patch_color_pair = patch("curses.color_pair") patch_color_pair.start() self.addCleanup(patch_color_pair.stop) patch_is_term_resized = patch("curses.is_term_resized") mock_is_term_resized = patch_is_term_resized.start() mock_is_term_resized.return_value = True if random.random() < 0.5 else False self.addCleanup(patch_is_term_resized.stop) if hasattr(curses, "resizeterm"): patch_resizeterm = patch("curses.resizeterm") patch_resizeterm.start() self.addCleanup(patch_resizeterm.stop) self.can_viewer = CanViewer(self.stdscr_dummy, bus, data_structs, testing=True) def tearDown(self): # Run the viewer after the test, this is done, so we can receive the CAN-Bus messages and make sure that they # are parsed correctly self.can_viewer.run() def test_send(self): # CANopen EMCY data = [1, 2, 3, 4, 5, 6, 7] # Wrong length msg = can.Message(arbitration_id=0x080 + 1, data=data, is_extended_id=False) self.can_viewer.bus.send(msg) data = [1, 2, 3, 4, 5, 6, 7, 8] msg = can.Message(arbitration_id=0x080 + 1, data=data, is_extended_id=False) self.can_viewer.bus.send(msg) # CANopen HEARTBEAT data = [0x05] # Operational msg = can.Message(arbitration_id=0x700 + 0x7F, data=data, is_extended_id=False) self.can_viewer.bus.send(msg) # Send non-CANopen message data = [1, 2, 3, 4, 5, 6, 7, 8] msg = can.Message(arbitration_id=0x101, data=data, is_extended_id=False) self.can_viewer.bus.send(msg) # Send the same command, but with another data length data = [1, 2, 3, 4, 5, 6] msg = can.Message(arbitration_id=0x101, data=data, is_extended_id=False) self.can_viewer.bus.send(msg) # Send non-CANopen message with long parsed data length data = [255, 255] msg = can.Message(arbitration_id=0x102, data=data, is_extended_id=False) self.can_viewer.bus.send(msg) # Send the same command, but with shorter parsed data length data = [0, 0] msg = can.Message(arbitration_id=0x102, data=data, is_extended_id=False) self.can_viewer.bus.send(msg) # Message with extended id data = [1, 2, 3, 4, 5, 6, 7, 8] msg = can.Message(arbitration_id=0x123456, data=data, is_extended_id=True) self.can_viewer.bus.send(msg) # self.assertTupleEqual(self.can_viewer.parse_canopen_message(msg), (None, None)) # Send the same message again to make sure that resending works and dt is correct time.sleep(0.1) self.can_viewer.bus.send(msg) # Send error message msg = can.Message(is_error_frame=True) self.can_viewer.bus.send(msg) def test_receive(self): # Send the messages again, but this time the test code will receive it self.test_send() data_structs = { # For converting the EMCY and HEARTBEAT messages 0x080 + 0x01: struct.Struct("ff"), } # Receive the messages we just sent in 'test_canopen' while 1: msg = self.can_viewer.bus.recv(timeout=0) if msg is not None: self.can_viewer.data_structs = ( data_structs if msg.arbitration_id != 0x101 else None ) _id = self.can_viewer.draw_can_bus_message(msg) if _id["msg"].arbitration_id == 0x101: # Check if the counter is reset when the length has changed self.assertEqual(_id["count"], 1) # Make sure the line has been cleared after the shorted message was send for col, v in self.stdscr_dummy.draw_buffer[_id["row"]].items(): if col >= 52 + _id["msg"].dlc * 3: self.assertEqual(v, " ") elif _id["msg"].arbitration_id == 0x102: # Make sure the parsed values have been cleared after the shorted message was send for col, v in self.stdscr_dummy.draw_buffer[_id["row"]].items(): if col >= 77 + _id["values_string_length"]: self.assertEqual(v, " ") elif _id["msg"].arbitration_id == 0x123456: # Check if the counter is incremented if _id["dt"] == 0: self.assertEqual(_id["count"], 1) else: if not IS_CI: # do not test timing in CI assert _id["dt"] == pytest.approx( 0.1, abs=5e-2 ) # dt should be ~0.1 s self.assertEqual(_id["count"], 2) else: # Make sure dt is 0 if _id["count"] == 1: self.assertEqual(_id["dt"], 0) else: break # Convert it into raw integer values and then pack the data @staticmethod def pack_data( cmd, cmd_to_struct, *args ): # type: (int, Dict, Union[*float, *int]) -> bytes if not cmd_to_struct or len(args) == 0: # If no arguments are given, then the message does not contain a data package return b"" for key in cmd_to_struct.keys(): if cmd == key if isinstance(key, int) else cmd in key: value = cmd_to_struct[key] if isinstance(value, tuple): # The struct is given as the fist argument struct_t = value[0] # type: struct.Struct # The conversion from SI-units to raw values are given in the rest of the tuple fmt = struct_t.format if isinstance(fmt, str): # pragma: no cover # Needed for Python 3.7 fmt = fmt.encode() # Make sure the endian is given as the first argument assert fmt[0] == ord("<") or fmt[0] == ord(">") # Disable rounding if the format is a float data = [] for c, arg, val in zip(fmt[1:], args, value[1:]): if c == ord("f"): data.append(arg * val) else: data.append(round(arg * val)) else: # No conversion from SI-units is needed struct_t = value # type: struct.Struct data = args return struct_t.pack(*data) else: raise ValueError(f"Unknown command: 0x{cmd:02X}") def test_pack_unpack(self): CANOPEN_TPDO1 = 0x180 CANOPEN_TPDO2 = 0x280 CANOPEN_TPDO3 = 0x380 CANOPEN_TPDO4 = 0x480 # 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. # # The struct package is used to unpack the received data. # Note the data is assumed to be in little-endian byte order. # < = little-endian, > = big-endian # x = pad byte # c = char # ? = bool # b = int8_t, B = uint8_t # h = int16, H = uint16 # l = int32_t, L = uint32_t # q = int64_t, Q = uint64_t # f = float (32-bits), d = double (64-bits) # # An optional conversion from real units to integers can be given as additional arguments. # In order to convert from raw integer value the SI-units are multiplied with the values and similarly the values # are divided by the value in order to convert from real units to raw integer values. data_structs = { # CANopen node 1 CANOPEN_TPDO1 + 1: struct.Struct("lL"), (CANOPEN_TPDO3 + 2, CANOPEN_TPDO4 + 2): struct.Struct(">LL"), } # type: Dict[Union[int, Tuple[int, ...]], Union[struct.Struct, Tuple, None]] raw_data = self.pack_data( CANOPEN_TPDO1 + 1, data_structs, -7, 13, -1024, 2048, 0xFFFF ) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO1 + 1, data_structs, raw_data) self.assertListEqual(parsed_data, [-7, 13, -1024, 2048, 0xFFFF]) self.assertTrue(all(isinstance(d, int) for d in parsed_data)) raw_data = self.pack_data(CANOPEN_TPDO2 + 1, data_structs, 12.34, 4.5, 6) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO2 + 1, data_structs, raw_data) assert parsed_data == pytest.approx([12.34, 4.5, 6]) self.assertTrue( isinstance(parsed_data[0], float) and isinstance(parsed_data[1], float) and isinstance(parsed_data[2], int) ) raw_data = self.pack_data(CANOPEN_TPDO3 + 1, data_structs, 123.45, 67.89) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO3 + 1, data_structs, raw_data) assert parsed_data == pytest.approx([123.45, 67.89]) self.assertTrue(all(isinstance(d, float) for d in parsed_data)) raw_data = self.pack_data( CANOPEN_TPDO4 + 1, data_structs, math.pi / 2.0, math.pi ) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO4 + 1, data_structs, raw_data) assert parsed_data == pytest.approx([math.pi / 2.0, math.pi]) self.assertTrue(all(isinstance(d, float) for d in parsed_data)) raw_data = self.pack_data(CANOPEN_TPDO1 + 2, data_structs) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO1 + 2, data_structs, raw_data) self.assertListEqual(parsed_data, []) self.assertIsInstance(parsed_data, list) raw_data = self.pack_data( CANOPEN_TPDO2 + 2, data_structs, -2147483648, 0xFFFFFFFF ) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO2 + 2, data_structs, raw_data) self.assertListEqual(parsed_data, [-2147483648, 0xFFFFFFFF]) raw_data = self.pack_data(CANOPEN_TPDO3 + 2, data_structs, 0xFF, 0xFFFF) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO3 + 2, data_structs, raw_data) self.assertListEqual(parsed_data, [0xFF, 0xFFFF]) raw_data = self.pack_data(CANOPEN_TPDO4 + 2, data_structs, 0xFFFFFF, 0xFFFFFFFF) parsed_data = CanViewer.unpack_data(CANOPEN_TPDO4 + 2, data_structs, raw_data) self.assertListEqual(parsed_data, [0xFFFFFF, 0xFFFFFFFF]) self.assertTrue(all(isinstance(d, int) for d in parsed_data)) # Make sure that the ValueError exception is raised with self.assertRaises(ValueError): self.pack_data(0x101, data_structs, 1, 2, 3, 4) with self.assertRaises(ValueError): CanViewer.unpack_data( 0x102, data_structs, b"\x01\x02\x03\x04\x05\x06\x07\x08" ) def test_parse_args(self): parsed_args, _, _ = _parse_viewer_args(["-b", "250000"]) self.assertEqual(parsed_args.bitrate, 250000) parsed_args, _, _ = _parse_viewer_args(["--bitrate", "500000"]) self.assertEqual(parsed_args.bitrate, 500000) parsed_args, _, _ = _parse_viewer_args(["-c", "can0"]) self.assertEqual(parsed_args.channel, "can0") parsed_args, _, _ = _parse_viewer_args(["--channel", "PCAN_USBBUS1"]) self.assertEqual(parsed_args.channel, "PCAN_USBBUS1") parsed_args, data_structs, _ = _parse_viewer_args(["-d", "100:=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 [testenv:docs] description = Build and test the documentation basepython = py312 deps = -r doc/doc-requirements.txt gs-usb extras = canalystii commands = python -m sphinx -b html -Wan --keep-going doc build python -m sphinx -b doctest -W --keep-going doc build [pytest] testpaths = test addopts = -v --timeout=300 --cov=can --cov-config=tox.ini --cov-report=lcov --cov-report=term [coverage:run] # we could also use branch coverage branch = False [coverage:report] # two digits after decimal point precision = 3 show_missing = True exclude_lines = # Have to re-enable the standard pragma, see https://coverage.readthedocs.io/en/coverage-4.5.1a/config.html#syntax pragma: no cover # Don't complain if non-runnable code isn't run: if __name__ == .__main__.: # Don't complain if tests don't hit defensive assertion code: raise NotImplementedError